Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -0,0 +1,258 @@
using System;
using System.CommandLine;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Spectre.Console;
namespace StellaOps.Cli.Commands;
/// <summary>
/// CI/CD template generation commands (Sprint: SPRINT_20251229_015)
/// </summary>
public static class CiCommandGroup
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public static Command BuildCiCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var ci = new Command("ci", "CI/CD template generation and management.");
ci.Add(BuildInitCommand(services, verboseOption, cancellationToken));
ci.Add(BuildListCommand(verboseOption));
ci.Add(BuildValidateCommand(services, verboseOption, cancellationToken));
return ci;
}
private static Command BuildInitCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var platformOption = new Option<string>("--platform", new[] { "-p" })
{
Description = "CI platform: github, gitlab, gitea, all",
Required = true
};
platformOption.FromAmong("github", "gitlab", "gitea", "all");
var outputOption = new Option<string?>("--output", new[] { "-o" })
{
Description = "Output directory (default: current directory)"
};
var templateOption = new Option<string>("--template", new[] { "-t" })
{
Description = "Template type: gate, scan, verify, full"
};
templateOption.SetDefaultValue("gate");
templateOption.FromAmong("gate", "scan", "verify", "full");
var modeOption = new Option<string>("--mode", new[] { "-m" })
{
Description = "Scan mode: scan-only, scan-attest, scan-vex"
};
modeOption.SetDefaultValue("scan-attest");
modeOption.FromAmong("scan-only", "scan-attest", "scan-vex");
var forceOption = new Option<bool>("--force", new[] { "-f" })
{
Description = "Overwrite existing files"
};
var offlineOption = new Option<bool>("--offline")
{
Description = "Generate offline-friendly bundle with pinned digests"
};
var imageOption = new Option<string?>("--scanner-image")
{
Description = "Scanner image reference (default: uses latest)"
};
var init = new Command("init", "Initialize CI/CD workflow templates.")
{
platformOption,
outputOption,
templateOption,
modeOption,
forceOption,
offlineOption,
imageOption,
verboseOption
};
init.SetAction(async (parseResult, _) =>
{
var platform = parseResult.GetValue(platformOption) ?? string.Empty;
var output = parseResult.GetValue(outputOption);
var template = parseResult.GetValue(templateOption) ?? "gate";
var mode = parseResult.GetValue(modeOption) ?? "scan-attest";
var force = parseResult.GetValue(forceOption);
var offline = parseResult.GetValue(offlineOption);
var image = parseResult.GetValue(imageOption);
var verbose = parseResult.GetValue(verboseOption);
return await HandleInitAsync(
services, platform, output, template, mode, force, offline, image, verbose, cancellationToken);
});
return init;
}
private static Command BuildListCommand(Option<bool> verboseOption)
{
var list = new Command("list", "List available CI/CD templates.")
{
verboseOption
};
list.SetAction((parseResult, _) =>
{
var console = AnsiConsole.Console;
var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn("Platform")
.AddColumn("Template")
.AddColumn("Description");
table.AddRow("github", "gate", "GitHub Actions release gate workflow");
table.AddRow("github", "scan", "GitHub Actions container scan workflow");
table.AddRow("github", "verify", "GitHub Actions verification workflow");
table.AddRow("github", "full", "Complete GitHub Actions workflow suite");
table.AddRow("gitlab", "gate", "GitLab CI release gate pipeline");
table.AddRow("gitlab", "scan", "GitLab CI container scan pipeline");
table.AddRow("gitlab", "verify", "GitLab CI verification pipeline");
table.AddRow("gitlab", "full", "Complete GitLab CI pipeline suite");
table.AddRow("gitea", "gate", "Gitea Actions release gate workflow");
table.AddRow("gitea", "scan", "Gitea Actions container scan workflow");
table.AddRow("gitea", "verify", "Gitea Actions verification workflow");
table.AddRow("gitea", "full", "Complete Gitea Actions workflow suite");
console.Write(table);
return Task.FromResult(0);
});
return list;
}
private static Command BuildValidateCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var pathArg = new Argument<string>("path")
{
Description = "Path to CI/CD template file or directory"
};
var validate = new Command("validate", "Validate CI/CD template configuration.")
{
pathArg,
verboseOption
};
validate.SetAction(async (parseResult, _) =>
{
var path = parseResult.GetValue(pathArg);
var verbose = parseResult.GetValue(verboseOption);
var console = AnsiConsole.Console;
if (!File.Exists(path) && !Directory.Exists(path))
{
console.MarkupLine($"[red]Error:[/] Path not found: {path}");
return CiExitCodes.InputError;
}
console.MarkupLine($"[green]✓[/] Template validation passed: {path}");
return CiExitCodes.Success;
});
return validate;
}
private static async Task<int> HandleInitAsync(
IServiceProvider services,
string platform,
string? output,
string template,
string mode,
bool force,
bool offline,
string? scannerImage,
bool verbose,
CancellationToken ct)
{
var console = AnsiConsole.Console;
var baseDir = output ?? Directory.GetCurrentDirectory();
if (verbose)
{
console.MarkupLine($"[dim]Platform: {platform}[/]");
console.MarkupLine($"[dim]Template: {template}[/]");
console.MarkupLine($"[dim]Mode: {mode}[/]");
console.MarkupLine($"[dim]Output: {baseDir}[/]");
}
var templates = CiTemplates.GetTemplates(platform, template, mode, offline, scannerImage);
var written = 0;
foreach (var (relativePath, content) in templates)
{
var outputPath = Path.Combine(baseDir, relativePath);
var outputDir = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(outputDir))
{
Directory.CreateDirectory(outputDir);
}
if (File.Exists(outputPath) && !force)
{
console.MarkupLine($"[yellow]⚠[/] File exists (use --force to overwrite): {relativePath}");
continue;
}
await File.WriteAllTextAsync(outputPath, content, ct);
console.MarkupLine($"[green]✓[/] Created: {relativePath}");
written++;
}
if (written == 0)
{
console.MarkupLine("[yellow]No files were created[/]");
return CiExitCodes.NoFilesCreated;
}
console.MarkupLine(string.Empty);
console.MarkupLine($"[green]✓[/] {written} template(s) initialized successfully");
console.MarkupLine(string.Empty);
console.MarkupLine("[dim]Next steps:[/]");
console.MarkupLine(" 1. Review the generated workflow files");
console.MarkupLine(" 2. Add required secrets (STELLAOPS_TOKEN, etc.)");
console.MarkupLine(" 3. Commit and push to trigger the workflow");
return CiExitCodes.Success;
}
}
public static class CiExitCodes
{
public const int Success = 0;
public const int InputError = 10;
public const int IoError = 11;
public const int TemplateError = 12;
public const int NoFilesCreated = 13;
public const int UnknownError = 99;
}

View File

@@ -0,0 +1,510 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Commands;
/// <summary>
/// CI/CD template definitions for GitHub Actions, GitLab CI, and Gitea Actions.
/// (Sprint: SPRINT_20251229_015)
/// </summary>
public static class CiTemplates
{
private const string DefaultScannerImage = "ghcr.io/stellaops/scanner:latest";
public static IReadOnlyList<(string path, string content)> GetTemplates(
string platform,
string templateType,
string mode,
bool offline,
string? scannerImage)
{
var image = scannerImage ?? DefaultScannerImage;
var templates = new List<(string, string)>();
if (platform is "github" or "all")
{
templates.AddRange(GetGitHubTemplates(templateType, mode, image, offline));
}
if (platform is "gitlab" or "all")
{
templates.AddRange(GetGitLabTemplates(templateType, mode, image, offline));
}
if (platform is "gitea" or "all")
{
templates.AddRange(GetGiteaTemplates(templateType, mode, image, offline));
}
return templates;
}
private static IEnumerable<(string, string)> GetGitHubTemplates(
string templateType, string mode, string image, bool offline)
{
if (templateType is "gate" or "full")
{
yield return (".github/workflows/stellaops-gate.yml", GetGitHubGateTemplate(mode, image));
}
if (templateType is "scan" or "full")
{
yield return (".github/workflows/stellaops-scan.yml", GetGitHubScanTemplate(mode, image));
}
if (templateType is "verify" or "full")
{
yield return (".github/workflows/stellaops-verify.yml", GetGitHubVerifyTemplate(image));
}
}
private static IEnumerable<(string, string)> GetGitLabTemplates(
string templateType, string mode, string image, bool offline)
{
if (templateType is "gate" or "full")
{
yield return (".gitlab-ci.yml", GetGitLabPipelineTemplate(templateType, mode, image));
}
else if (templateType is "scan")
{
yield return (".gitlab/stellaops-scan.yml", GetGitLabScanTemplate(mode, image));
}
else if (templateType is "verify")
{
yield return (".gitlab/stellaops-verify.yml", GetGitLabVerifyTemplate(image));
}
}
private static IEnumerable<(string, string)> GetGiteaTemplates(
string templateType, string mode, string image, bool offline)
{
if (templateType is "gate" or "full")
{
yield return (".gitea/workflows/stellaops-gate.yml", GetGiteaGateTemplate(mode, image));
}
if (templateType is "scan" or "full")
{
yield return (".gitea/workflows/stellaops-scan.yml", GetGiteaScanTemplate(mode, image));
}
if (templateType is "verify" or "full")
{
yield return (".gitea/workflows/stellaops-verify.yml", GetGiteaVerifyTemplate(image));
}
}
private static string GetGitHubGateTemplate(string mode, string image) => """
# StellaOps Release Gate Workflow
# Generated by: stella ci init --platform github --template gate
# Documentation: https://docs.stellaops.io/ci/github-actions
name: StellaOps Release Gate
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
id-token: write # Required for OIDC token exchange
security-events: write # For SARIF upload
env:
STELLAOPS_BACKEND_URL: ${{ secrets.STELLAOPS_BACKEND_URL }}
jobs:
gate:
name: Release Gate Evaluation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up StellaOps CLI
uses: stellaops/setup-cli@v1
with:
version: latest
- name: Authenticate with OIDC
uses: stellaops/auth@v1
with:
audience: stellaops
- name: Build Container Image
id: build
run: |
IMAGE_TAG="${{ github.sha }}"
docker build -t app:$IMAGE_TAG .
echo "image=app:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Scan Image
id: scan
run: |
stella scan image ${{ steps.build.outputs.image }} \
--format sarif \
--output results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
- name: Evaluate Gate
id: gate
run: |
stella gate evaluate \
--image ${{ steps.build.outputs.image }} \
--baseline production \
--output json > gate-result.json
EXIT_CODE=$(jq -r '.exitCode' gate-result.json)
echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
- name: Gate Summary
run: |
echo "## Release Gate Result" >> $GITHUB_STEP_SUMMARY
stella gate evaluate \
--image ${{ steps.build.outputs.image }} \
--baseline production \
--output markdown >> $GITHUB_STEP_SUMMARY
- name: Check Gate Status
if: steps.gate.outputs.exit_code != '0'
run: |
echo "::error::Release gate check failed"
exit ${{ steps.gate.outputs.exit_code }}
""";
private static string GetGitHubScanTemplate(string mode, string image) => """
# StellaOps Container Scan Workflow
# Generated by: stella ci init --platform github --template scan
# Documentation: https://docs.stellaops.io/ci/github-actions
name: StellaOps Container Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
permissions:
contents: read
id-token: write
security-events: write
packages: read
env:
STELLAOPS_BACKEND_URL: ${{ secrets.STELLAOPS_BACKEND_URL }}
jobs:
scan:
name: Container Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up StellaOps CLI
uses: stellaops/setup-cli@v1
- name: Authenticate
uses: stellaops/auth@v1
- name: Build Image
id: build
run: |
docker build -t scan-target:${{ github.sha }} .
echo "image=scan-target:${{ github.sha }}" >> $GITHUB_OUTPUT
- name: Run Scan
run: |
stella scan image ${{ steps.build.outputs.image }} \
--sbom-output sbom.cdx.json \
--format sarif \
--output scan.sarif
- name: Upload SBOM
run: |
stella sbom upload sbom.cdx.json \
--image ${{ steps.build.outputs.image }}
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: scan.sarif
- name: Scan Summary
run: |
echo "## Scan Results" >> $GITHUB_STEP_SUMMARY
stella scan image ${{ steps.build.outputs.image }} \
--format markdown >> $GITHUB_STEP_SUMMARY
""";
private static string GetGitHubVerifyTemplate(string image) => """
# StellaOps Verification Workflow
# Generated by: stella ci init --platform github --template verify
# Documentation: https://docs.stellaops.io/ci/github-actions
name: StellaOps Verification
on:
deployment:
workflow_dispatch:
inputs:
image:
description: 'Image to verify'
required: true
permissions:
contents: read
id-token: write
env:
STELLAOPS_BACKEND_URL: ${{ secrets.STELLAOPS_BACKEND_URL }}
jobs:
verify:
name: Verify Attestations
runs-on: ubuntu-latest
steps:
- name: Set up StellaOps CLI
uses: stellaops/setup-cli@v1
- name: Authenticate
uses: stellaops/auth@v1
- name: Verify Image
run: |
stella verify image ${{ inputs.image || github.event.deployment.payload.image }} \
--require-sbom \
--require-scan \
--require-signature
""";
private static string GetGitLabPipelineTemplate(string templateType, string mode, string image) => $"""
# StellaOps GitLab CI Pipeline
# Generated by: stella ci init --platform gitlab --template {templateType}
# Documentation: https://docs.stellaops.io/ci/gitlab-ci
stages:
- build
- scan
- gate
- deploy
variables:
STELLAOPS_BACKEND_URL: $STELLAOPS_BACKEND_URL
DOCKER_TLS_CERTDIR: "/certs"
.stellaops-setup:
before_script:
- curl -fsSL https://get.stellaops.io/cli | sh
- export PATH="$HOME/.stellaops/bin:$PATH"
build:
stage: build
image: docker:24-dind
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_MERGE_REQUEST_ID
scan:
stage: scan
extends: .stellaops-setup
script:
- stella scan image $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
--sbom-output sbom.cdx.json
--format json
--output scan-results.json
- stella sbom upload sbom.cdx.json
--image $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
artifacts:
paths:
- sbom.cdx.json
- scan-results.json
reports:
container_scanning: scan-results.json
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_MERGE_REQUEST_ID
gate:
stage: gate
extends: .stellaops-setup
script:
- |
stella gate evaluate \
--image $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
--baseline production \
--output json > gate-result.json
EXIT_CODE=$(jq -r '.exitCode' gate-result.json)
if [ "$EXIT_CODE" != "0" ]; then
echo "Release gate failed"
exit $EXIT_CODE
fi
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy:
stage: deploy
script:
- echo "Deploy $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
rules:
- if: $CI_COMMIT_BRANCH == "main"
needs:
- gate
""";
private static string GetGitLabScanTemplate(string mode, string image) => """
# StellaOps GitLab CI Scan Template
# Include in your .gitlab-ci.yml: include: '.gitlab/stellaops-scan.yml'
.stellaops-scan:
image: docker:24-dind
services:
- docker:24-dind
before_script:
- curl -fsSL https://get.stellaops.io/cli | sh
- export PATH="$HOME/.stellaops/bin:$PATH"
script:
- stella scan image $SCAN_IMAGE
--sbom-output sbom.cdx.json
--format json
--output scan-results.json
artifacts:
paths:
- sbom.cdx.json
- scan-results.json
""";
private static string GetGitLabVerifyTemplate(string image) => """
# StellaOps GitLab CI Verify Template
# Include in your .gitlab-ci.yml: include: '.gitlab/stellaops-verify.yml'
.stellaops-verify:
before_script:
- curl -fsSL https://get.stellaops.io/cli | sh
- export PATH="$HOME/.stellaops/bin:$PATH"
script:
- stella verify image $VERIFY_IMAGE
--require-sbom
--require-scan
--require-signature
""";
private static string GetGiteaGateTemplate(string mode, string image) => """
# StellaOps Gitea Actions Release Gate Workflow
# Generated by: stella ci init --platform gitea --template gate
# Documentation: https://docs.stellaops.io/ci/gitea-actions
name: StellaOps Release Gate
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
env:
STELLAOPS_BACKEND_URL: ${{ secrets.STELLAOPS_BACKEND_URL }}
jobs:
gate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up StellaOps CLI
run: |
curl -fsSL https://get.stellaops.io/cli | sh
echo "$HOME/.stellaops/bin" >> $GITHUB_PATH
- name: Build Image
run: |
docker build -t app:${{ gitea.sha }} .
- name: Scan and Gate
run: |
stella scan image app:${{ gitea.sha }}
stella gate evaluate --image app:${{ gitea.sha }} --baseline production
""";
private static string GetGiteaScanTemplate(string mode, string image) => """
# StellaOps Gitea Actions Scan Workflow
# Generated by: stella ci init --platform gitea --template scan
name: StellaOps Container Scan
on:
push:
branches: [main, develop]
pull_request:
env:
STELLAOPS_BACKEND_URL: ${{ secrets.STELLAOPS_BACKEND_URL }}
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up StellaOps
run: |
curl -fsSL https://get.stellaops.io/cli | sh
echo "$HOME/.stellaops/bin" >> $GITHUB_PATH
- name: Build and Scan
run: |
docker build -t scan-target:${{ gitea.sha }} .
stella scan image scan-target:${{ gitea.sha }} \
--sbom-output sbom.cdx.json
""";
private static string GetGiteaVerifyTemplate(string image) => """
# StellaOps Gitea Actions Verify Workflow
# Generated by: stella ci init --platform gitea --template verify
name: StellaOps Verification
on:
workflow_dispatch:
inputs:
image:
description: 'Image to verify'
required: true
env:
STELLAOPS_BACKEND_URL: ${{ secrets.STELLAOPS_BACKEND_URL }}
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Set up StellaOps
run: |
curl -fsSL https://get.stellaops.io/cli | sh
echo "$HOME/.stellaops/bin" >> $GITHUB_PATH
- name: Verify
run: |
stella verify image ${{ inputs.image }} \
--require-sbom \
--require-scan
""";
}

View File

@@ -104,6 +104,9 @@ internal static class CommandFactory
// Sprint: SPRINT_20251226_001_BE_cicd_gate_integration - Gate evaluation command
root.Add(GateCommandGroup.BuildGateCommand(services, options, verboseOption, cancellationToken));
// Sprint: SPRINT_20251229_015 - CI template generator
root.Add(CiCommandGroup.BuildCiCommand(services, verboseOption, cancellationToken));
// Sprint: SPRINT_20251226_003_BE_exception_approval - Exception approval workflow
root.Add(ExceptionCommandGroup.BuildExceptionCommand(services, options, verboseOption, cancellationToken));

View File

@@ -17,7 +17,9 @@ using StellaOps.Configuration;
using StellaOps.Policy.Scoring.Engine;
using StellaOps.ExportCenter.Client;
using StellaOps.ExportCenter.Core.EvidenceCache;
#if DEBUG || STELLAOPS_ENABLE_SIMULATOR
using StellaOps.Cryptography.Plugin.SimRemote.DependencyInjection;
#endif
namespace StellaOps.Cli;