Compare commits
20 Commits
4106136782
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 995eb10df8 | |||
| b495c231e0 | |||
| 0fb95e78f7 | |||
| 77d7bb758d | |||
| 707e39e70a | |||
| f096aa9b0a | |||
| 72a891887e | |||
| 3e752c909b | |||
| ebd021eeb7 | |||
| 06caba6243 | |||
| da97867c33 | |||
| 981201dc02 | |||
| 251776b9fd | |||
| e784b7996c | |||
| 89410a8b07 | |||
| 47ea86224a | |||
| dacd8fd7c4 | |||
| 08b1417605 | |||
| b9b06263d8 | |||
| 83ff4e89e9 |
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# .NET Build-Artefakte
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Kompilierte Bibliotheken
|
||||
*.so
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
|
||||
# Datenbank-Dateien (lokale SQLite)
|
||||
*.db
|
||||
20
Components/App.razor
Normal file
20
Components/App.razor
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="SecDevOpsLab.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
23
Components/Layout/MainLayout.razor
Normal file
23
Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,23 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
96
Components/Layout/MainLayout.razor.css
Normal file
96
Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,96 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
30
Components/Layout/NavMenu.razor
Normal file
30
Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">SecDevOpsLab</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
105
Components/Layout/NavMenu.razor.css
Normal file
105
Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,105 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
32
Components/Pages/Books.razor
Normal file
32
Components/Pages/Books.razor
Normal file
@@ -0,0 +1,32 @@
|
||||
@page "/books"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@using SecDevOpsLab.Models
|
||||
@using SecDevOpsLab.Data
|
||||
@inject AppDbContext Db
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<h3>Bücherverwaltung</h3>
|
||||
<input @bind="newBook.Title" placeholder="Titel" />
|
||||
<input @bind="newBook.Author" placeholder="Autor" />
|
||||
<button @onclick="Save">Speichern</button>
|
||||
|
||||
<hr />
|
||||
<ul>
|
||||
@foreach (var b in bookList) {
|
||||
<li>@b.Title von @b.Author</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@code {
|
||||
private Book newBook = new();
|
||||
private List<Book> bookList = new();
|
||||
|
||||
protected override async Task OnInitializedAsync() => bookList = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync(Db.Books);
|
||||
|
||||
private async Task Save() {
|
||||
Db.Books.Add(newBook);
|
||||
await Db.SaveChangesAsync();
|
||||
newBook = new();
|
||||
bookList = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync(Db.Books);
|
||||
}
|
||||
}
|
||||
19
Components/Pages/Counter.razor
Normal file
19
Components/Pages/Counter.razor
Normal file
@@ -0,0 +1,19 @@
|
||||
@page "/counter"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
36
Components/Pages/Error.razor
Normal file
36
Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
7
Components/Pages/Home.razor
Normal file
7
Components/Pages/Home.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
40
Components/Pages/Login.razor
Normal file
40
Components/Pages/Login.razor
Normal file
@@ -0,0 +1,40 @@
|
||||
@page "/login"
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@using Microsoft.AspNetCore.Authentication.Cookies
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<h3>SecDevOps Lab Login</h3>
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger">@errorMessage</div>
|
||||
}
|
||||
|
||||
@* Wichtig: Ein traditionelles HTML-Formular nutzen, um Cookies setzen zu können *@
|
||||
<form action="/api/auth/login" method="post">
|
||||
<AntiforgeryToken />
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Benutzername</label>
|
||||
<input type="text" name="username" class="form-control" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input type="password" name="password" class="form-control" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Einloggen</button>
|
||||
</form>
|
||||
|
||||
@code {
|
||||
private string? errorMessage;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Falls ein Fehler beim Login auftrat, fangen wir ihn über die URL ab
|
||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
||||
if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("error", out var error))
|
||||
{
|
||||
errorMessage = "Ungültige Zugangsdaten.";
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Components/Pages/Weather.razor
Normal file
64
Components/Pages/Weather.razor
Normal file
@@ -0,0 +1,64 @@
|
||||
@page "/weather"
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates showing data.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Temp. (C)</th>
|
||||
<th>Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Simulate asynchronous loading to demonstrate streaming rendering
|
||||
await Task.Delay(500);
|
||||
|
||||
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = startDate.AddDays(index),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public int TemperatureC { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
6
Components/Routes.razor
Normal file
6
Components/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
10
Components/_Imports.razor
Normal file
10
Components/_Imports.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using SecDevOpsLab
|
||||
@using SecDevOpsLab.Components
|
||||
8
Data/AppDbContext.cs
Normal file
8
Data/AppDbContext.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SecDevOpsLab.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SecDevOpsLab.Models;
|
||||
|
||||
public class AppDbContext : DbContext {
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
public DbSet<Book> Books => Set<Book>();
|
||||
}
|
||||
52
Dockerfile
52
Dockerfile
@@ -1,27 +1,51 @@
|
||||
# --- Stage 1: Build (Die Bau-Umgebung) ---
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
|
||||
# Basis Image das für die Build Umgebung verwendet wird
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build-env
|
||||
# Wechseln ins Arbeitsverzeichnis
|
||||
WORKDIR /app
|
||||
|
||||
# 1. Nur Projektdatei kopieren und Abhängigkeiten laden (Nutzt Docker-Caching)
|
||||
COPY *.sln ./
|
||||
COPY MyHelloWorld/*.csproj ./MyHelloWorld/
|
||||
COPY MyHelloWorld.Tests/*.csproj ./MyHelloWorld.Tests/
|
||||
# Kopieren der Projektdatei in Arbeitsverzeichnis
|
||||
COPY *.csproj ./
|
||||
# Laden der Abhängigkeiten
|
||||
RUN dotnet restore
|
||||
|
||||
# 2. Den restlichen Quellcode kopieren und die App kompilieren
|
||||
# Kopieren des restlichen Quellcodes
|
||||
COPY . ./
|
||||
RUN dotnet publish MyHelloWorld/*.csproj -c Release -o out
|
||||
# Kompilieren eds Quellcodes (Projektdatei muss nicht zwingend angegeben werden)
|
||||
RUN dotnet publish "SecDevOpsLab.csproj" -c Release -o out
|
||||
|
||||
# --- Stage 2: Runtime (Das fertige, schlanke Image) ---
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
# Noch schlangere Basis Image für die Runtime Umgebung
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
||||
# Wechseln ins Arbeitsverzeichnis
|
||||
WORKDIR /app
|
||||
|
||||
# 3. Den vorinstallierten User 'app' von Microsoft nutzen
|
||||
# Öffnen des Ports für die Web-App (Standard bei .NET 8 Web-Apps ist 8080)
|
||||
EXPOSE 8080
|
||||
# Konfigurieren des integrierten Kestrel-Webserver einer ASP.NET Core App
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
|
||||
# Wechseln auf root (nur kurz)
|
||||
USER root
|
||||
|
||||
# OS patchen
|
||||
RUN apk update && apk upgrade --no-cache
|
||||
|
||||
# Ändern des File Owner (wichtig dass die Sqlite DB geschrieben werden kann)
|
||||
RUN mkdir -p /app/data && chown -R app:app /app/data && chmod -R 775 /app/data
|
||||
# Wechsel auf non-root User (app ist ein vorinstallierter User von Microsoft)
|
||||
USER app
|
||||
|
||||
# 4. Nur die fertigen Binärdateien aus der Bau-Umgebung rüberschieben
|
||||
# Kopieren der fertigen Binärdateien aus der Build Umgebung
|
||||
COPY --from=build-env /app/out .
|
||||
|
||||
# 5. Startbefehl festlegen
|
||||
# WICHTIG: Falls deine DLL anders heißt (z.B. MyHelloWorld.dll), passe den Namen hier an!
|
||||
# Definieren eines Arguments, das dann beim kaniko Aufruf mitübergeben wird
|
||||
ARG JENKINS_BUILD=unknown
|
||||
|
||||
# Schreiben des Inhalts des übergebenen Werts des Arguments als Label in das Image
|
||||
# kubectl get pods --show-labels
|
||||
# kubectl get pod <pod-name> -n bookmanager-apps -o jsonpath='{.status.containerStatuses[*].imageID}'
|
||||
# kubectl get pods -L jenkins.build.number
|
||||
LABEL org.opencontainers.image.version=${JENKINS_BUILD} \
|
||||
managed-by="Jenkins"
|
||||
|
||||
# Festlegen des Start Befehls
|
||||
ENTRYPOINT ["dotnet", "SecDevOpsLab.dll"]
|
||||
124
Jenkinsfile
vendored
124
Jenkinsfile
vendored
@@ -1,7 +1,8 @@
|
||||
pipeline {
|
||||
|
||||
agent {
|
||||
kubernetes {
|
||||
// Definiert den Pod mit dem .NET 8 SDK Image
|
||||
// Definieren des Pod mit 3 Containern als Build Agent, Trivy und Kaniko
|
||||
yaml '''
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
@@ -26,60 +27,71 @@ pipeline {
|
||||
}
|
||||
|
||||
options {
|
||||
// Log-Rotation: Bewahre nur die letzten 10 Builds auf
|
||||
// und lösche Berichte/Artefakte von Builds, die älter als 7 Tage sind.
|
||||
// Aktivieren von Log Rotation:
|
||||
// *) Letzten 10 Builds werden aufbewahrt
|
||||
// ** Builds, die älter als 7 Tage sind, werden gelöscht
|
||||
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10', daysToKeepStr: '7'))
|
||||
}
|
||||
|
||||
stages {
|
||||
// Auschecken der Sourcen zum App Projekt
|
||||
stage('Checkout Source') {
|
||||
steps {
|
||||
// Ersetze 'dein-user' und 'dein-repo' durch die Namen aus Gitea
|
||||
git url: 'http://130.61.26.230:30080/dev-master/secdevops-csharp-app.git',
|
||||
git url: 'https://gitea.dagobert84.duckdns.org/dev-master/secdevops-csharp-app.git',
|
||||
branch: 'master'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Security: Trivy Scan') {
|
||||
steps {
|
||||
// Ausführen des 'dotnet restore' für den nachfolgenden Trivy Scan, der diese wiederhergestellte Dateien/NuGet Pakete und Abhängigkeiten scanned
|
||||
// Generiert automatisch das obj/ mit der project.assets.json
|
||||
// Alle Container eines Pods teilen sich das Jenkins Arbeitsverzeichnis
|
||||
container('dotnet8') {
|
||||
sh 'dotnet restore'
|
||||
}
|
||||
|
||||
// Ausführen des Trivy Scans
|
||||
//
|
||||
// Wichtig: trivy ersetzt -> dotnet list package --vulnerable --include-transitive
|
||||
container('trivy') {
|
||||
// Erzeugen des Directory zum Speichern des Reports
|
||||
sh 'mkdir -p reports'
|
||||
|
||||
// Ausführen des Scans hinsichtlich Vulnerabilities, Miskonfigurationen, Secrets und Licences im Jenkins Arbeitsverzeichnis
|
||||
// Abbruch bei bei kritischen Fehlern (--exit-code 1 --severity HIGH,CRITICAL)
|
||||
sh 'trivy fs --scanners vuln,misconfig,secret,license --exit-code 1 --severity HIGH,CRITICAL --format template --template "@/contrib/html.tpl" -o reports/trivy-fs-report.html .'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kompilieren der Anwendung (DLLs werden erzeugt)
|
||||
// Schritt muss gar nicht durchgeführt werden, da kaniko das Image erzeugt
|
||||
stage('Build with .NET 8') {
|
||||
steps {
|
||||
// Führt den Build-Befehl im spezialisierten Container aus
|
||||
container('dotnet8') {
|
||||
sh 'dotnet --version' // Zur Bestätigung der Version
|
||||
sh 'dotnet build'
|
||||
|
||||
sh 'dotnet build --configuration Release' // optimierter Build Prozess ohne Debug und ungenutzt Pfade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Security: Trivy Scan') {
|
||||
steps {
|
||||
container('trivy') {
|
||||
// Wir erstellen ein Verzeichnis für den Report
|
||||
sh 'mkdir -p reports'
|
||||
|
||||
// Der Befehl erzeugt die HTML-Datei
|
||||
// --format template: Nutzt ein Layout
|
||||
// --template "@/contrib/html.tpl": Das Standard-Trivy-Layout
|
||||
// Scannt das Dateisystem auf Schwachstellen (NuGet) und Secrets
|
||||
// --exit-code 1 lässt die Pipeline bei kritischen Fehlern abbrechen
|
||||
sh 'trivy fs --scanners vuln,misconfig,secret,license --exit-code 1 --severity HIGH,CRITICAL --format template --template "@/contrib/html.tpl" -o reports/trivy-report.html .'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Unit Tests') {
|
||||
steps {
|
||||
container('dotnet8') {
|
||||
//stage('Unit Tests') {
|
||||
// steps {
|
||||
// container('dotnet8') {
|
||||
// Erstellt eine XML-Datei im Format 'junit', die Jenkins lesen kann
|
||||
// sh 'dotnet test --configuration Release'
|
||||
sh 'dotnet test --configuration Release --logger "junit;LogFileName=results.xml"'
|
||||
}
|
||||
}
|
||||
}
|
||||
// sh 'dotnet test --configuration Release --logger "junit;LogFileName=results.xml"'
|
||||
// }
|
||||
//}
|
||||
//}
|
||||
|
||||
stage('Set Build Name') {
|
||||
steps {
|
||||
script {
|
||||
// Setzt den Namen des aktuellen Laufs auf die Version + Build-Nummer
|
||||
// Setzen des Namen des aktuellen Laufs auf die Version + Build-Nummer
|
||||
currentBuild.displayName = "v1.0.0-build-${env.BUILD_NUMBER}"
|
||||
}
|
||||
}
|
||||
@@ -92,26 +104,32 @@ pipeline {
|
||||
|
||||
steps {
|
||||
container('kaniko') {
|
||||
// Nutze die ID, die du in Jenkins für den Token vergeben hast
|
||||
// Stellt die Informationen aus dem Token in Form von Umgebungsvariablen der Jenkins Pipeline zur Verfügung
|
||||
// Nachfolgend werden diese Credentials im JSON Format in config.json geschrieben
|
||||
// Vorgehen ist zwar nicht extrem sicher, aber die Lebenszeit im Container ist kurz, dass diese base64 kodierten Daten zurückverwandelt werden könnten
|
||||
|
||||
// Erzeugen des Directory zum Speichern des Reports, falls das bei einem vorigen Schritt nicht durchgeführt wurde
|
||||
sh 'mkdir -p reports'
|
||||
|
||||
withCredentials([usernamePassword(credentialsId: 'gitea-registry-token',
|
||||
usernameVariable: 'GITEA_USER',
|
||||
passwordVariable: 'GITEA_TOKEN')]) {
|
||||
sh '''
|
||||
# WORKAROUND: Dem Container beibringen, wer git.example.com ist
|
||||
echo "130.61.26.230 git.example.com" >> /etc/hosts
|
||||
# WORKAROUND: Dem Container beibringen, wer git.example.com ist, ansonsten funktioniert das Übertragen des Images an Git nicht!!!
|
||||
#echo "130.61.26.230 git.example.com" >> /etc/hosts
|
||||
|
||||
# Erstellt die Docker-Konfiguration für Kaniko
|
||||
# Das $(echo ...) Kommando kombiniert User und Token für den Login
|
||||
echo "{\\"auths\\":{\\"130.61.26.230:30080\\":{\\"auth\\":\\"\$(echo -n \${GITEA_USER}:\${GITEA_TOKEN} | base64)\\"}}}" > /kaniko/.docker/config.json
|
||||
echo "{\\"auths\\":{\\"https://gitea.dagobert84.duckdns.org\\":{\\"auth\\":\\"\$(echo -n \${GITEA_USER}:\${GITEA_TOKEN} | base64)\\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
# Der Bau- und Push-Befehl
|
||||
# Wir taggen das Image mit 'latest' UND der Build-Nummer zur Sicherheit
|
||||
/kaniko/executor --context `pwd` \
|
||||
--dockerfile `pwd`/Dockerfile \
|
||||
--insecure \
|
||||
--skip-tls-verify \
|
||||
--destination 130.61.26.230:30080/dev-master/secdevops-csharp-app:latest \
|
||||
--destination 130.61.26.230:30080/dev-master/secdevops-csharp-app:${BUILD_NUMBER}
|
||||
--build-arg JENKINS_BUILD=${BUILD_NUMBER} \
|
||||
--destination https://gitea.dagobert84.duckdns.org/dev-master/secdevops-csharp-app:latest \
|
||||
--destination https://gitea.dagobert84.duckdns.org/dev-master/secdevops-csharp-app:${BUILD_NUMBER}
|
||||
'''
|
||||
}
|
||||
}
|
||||
@@ -122,15 +140,26 @@ pipeline {
|
||||
when {
|
||||
branch 'master'
|
||||
}
|
||||
steps {
|
||||
|
||||
steps {
|
||||
// Trivy Scan wird auf das Image im Git Repository angewendet. Das Image wird heruntergeladen.
|
||||
container('trivy') {
|
||||
// Scannt das frisch gepushte Image direkt aus deiner Gitea Registry
|
||||
// Das Flag '--insecure' erlaubt Trivy den Zugriff über die unverschlüsselte IP
|
||||
sh 'trivy image --insecure --exit-code 1 --severity HIGH,CRITICAL 130.61.26.230:30080/dev-master/secdevops-csharp-app:latest'
|
||||
// 1. Scan ausführen und als HTML-Report speichern (Achte auf den neuen Dateinamen)
|
||||
sh '''
|
||||
trivy image --insecure \
|
||||
--severity HIGH,CRITICAL \
|
||||
--format template \
|
||||
--template "@/contrib/html.tpl" \
|
||||
--exit-code 1 \
|
||||
-o reports/trivy-image-report.html \
|
||||
https://gitea.dagobert84.duckdns.org/dev-master/secdevops-csharp-app:latest
|
||||
'''
|
||||
|
||||
// Den Scan ein zweites Mal kurz ohne Report ausführen, damit die Pipeline bei Lücken blockiert
|
||||
// sh 'trivy image --insecure --exit-code 1 --severity HIGH,CRITICAL 130.61.26.230:30080/dev-master/secdevops-csharp-app:latest'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
@@ -140,21 +169,20 @@ pipeline {
|
||||
}
|
||||
}
|
||||
always {
|
||||
// Sammelt die Testergebnisse ein (die wir im Test-Schritt erzeugen)
|
||||
// Das **/ bedeutet: Suche in allen Unterordnern nach .xml Dateien
|
||||
junit testResults: '**/TestResults/*.xml', allowEmptyResults: true
|
||||
// Suchen und Einelen von etwaigen Testreports von JUnit. Keine Vorhanden? -auch ok
|
||||
junit testResults: '**/*.xml', allowEmptyResults: true
|
||||
|
||||
// Dieser Schritt macht den Report im Jenkins-Menü links sichtbar
|
||||
// Verwenden des HTML Publisher Modules zum Schreiben der gefundenen Testreports in das Build Menu und speichert den HTML Bericht dann historisch ab (keepAll)
|
||||
publishHTML([
|
||||
allowMissing: false,
|
||||
alwaysLinkToLastBuild: true,
|
||||
keepAll: true,
|
||||
reportDir: 'reports',
|
||||
reportFiles: 'trivy-report.html',
|
||||
reportFiles: 'trivy-fs-report.html,trivy-image-report.html',
|
||||
reportName: 'Trivy Security Report'
|
||||
])
|
||||
|
||||
// Meldet den Status zurück, wenn das Gitea-Plugin korrekt konfiguriert ist
|
||||
// Schreiben des Build Status in das Build Log
|
||||
echo "Pipeline beendet: ${currentBuild.result}"
|
||||
}
|
||||
}
|
||||
|
||||
8
Models/Books.cs
Normal file
8
Models/Books.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SecDevOpsLab.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
public class Book {
|
||||
public int Id { get; set; }
|
||||
[Required] public string Title { get; set; } = string.Empty;
|
||||
[Required] public string Author { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<WarningsAsErrors></WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="JunitXml.TestLogger" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyHelloWorld\SecDevOpsLab.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,21 +0,0 @@
|
||||
using Xunit;
|
||||
using MyHelloWorld; // Dein Namespace der Haupt-App
|
||||
|
||||
namespace MyHelloWorld.Tests
|
||||
{
|
||||
public class HelloTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetGreeting_ShouldReturnCorrectText()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new HelloGenerator();
|
||||
|
||||
// Act
|
||||
var result = generator.GetGreeting();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello World", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"framework": {
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
"configProperties": {
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"SecDevOpsLab/1.0.0": {
|
||||
"runtime": {
|
||||
"SecDevOpsLab.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"SecDevOpsLab/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"framework": {
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
"configProperties": {
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user