save progress
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
// <copyright file="AzureDevOpsGenerator.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Azure DevOps pipeline YAML.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Implement generators
|
||||
/// </summary>
|
||||
public sealed class AzureDevOpsGenerator : IWorkflowGenerator
|
||||
{
|
||||
public CiPlatform Platform => CiPlatform.AzureDevOps;
|
||||
|
||||
public string PlatformName => "Azure DevOps";
|
||||
|
||||
public string DefaultFileName => "azure-pipelines.yml";
|
||||
|
||||
public string Generate(WorkflowOptions options)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Header comment
|
||||
if (options.IncludeComments)
|
||||
{
|
||||
sb.AppendLine("# StellaOps Security Scan Pipeline");
|
||||
sb.AppendLine("# Generated by StellaOps Workflow Generator");
|
||||
sb.AppendLine("# https://stellaops.io/docs/ci-integration");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Pipeline name
|
||||
sb.AppendLine($"name: {options.Name}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Triggers
|
||||
GenerateTriggers(sb, options);
|
||||
sb.AppendLine();
|
||||
|
||||
// Variables
|
||||
sb.AppendLine("variables:");
|
||||
sb.AppendLine($" STELLAOPS_VERSION: '{options.Scan.CliVersion}'");
|
||||
sb.AppendLine($" SARIF_FILE: '{options.Upload.SarifFileName}'");
|
||||
if (options.Scan.GenerateSbom)
|
||||
{
|
||||
sb.AppendLine($" SBOM_FILE: '{options.Upload.SbomFileName}'");
|
||||
}
|
||||
foreach (var (key, value) in options.EnvironmentVariables)
|
||||
{
|
||||
sb.AppendLine($" {key}: '{value}'");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Pool
|
||||
sb.AppendLine("pool:");
|
||||
sb.AppendLine($" vmImage: '{options.Runner ?? "ubuntu-latest"}'");
|
||||
sb.AppendLine();
|
||||
|
||||
// Stages
|
||||
sb.AppendLine("stages:");
|
||||
sb.AppendLine(" - stage: Security");
|
||||
sb.AppendLine(" displayName: 'Security Scan'");
|
||||
sb.AppendLine(" jobs:");
|
||||
sb.AppendLine(" - job: StellaOpsScan");
|
||||
sb.AppendLine(" displayName: 'StellaOps Security Scan'");
|
||||
sb.AppendLine(" steps:");
|
||||
sb.AppendLine();
|
||||
|
||||
// Checkout
|
||||
sb.AppendLine(" - checkout: self");
|
||||
sb.AppendLine(" fetchDepth: 0");
|
||||
sb.AppendLine();
|
||||
|
||||
// Install CLI
|
||||
sb.AppendLine(" - task: Bash@3");
|
||||
sb.AppendLine(" displayName: 'Install StellaOps CLI'");
|
||||
sb.AppendLine(" inputs:");
|
||||
sb.AppendLine(" targetType: 'inline'");
|
||||
sb.AppendLine(" script: |");
|
||||
if (options.Scan.CliVersion == "latest")
|
||||
{
|
||||
sb.AppendLine(" curl -fsSL https://get.stellaops.io | sh");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" curl -fsSL https://get.stellaops.io | sh -s -- --version $(STELLAOPS_VERSION)");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Run scan
|
||||
sb.AppendLine(" - task: Bash@3");
|
||||
sb.AppendLine(" displayName: 'Run StellaOps Scan'");
|
||||
sb.AppendLine(" inputs:");
|
||||
sb.AppendLine(" targetType: 'inline'");
|
||||
sb.AppendLine(" script: |");
|
||||
sb.AppendLine($" stella scan {BuildScanArgs(options)} --output sarif=$(SARIF_FILE)");
|
||||
if (options.Scan.GenerateSbom)
|
||||
{
|
||||
sb.AppendLine(" stella sbom --output $(SBOM_FILE)");
|
||||
}
|
||||
if (options.Scan.FailOnSeverity is null)
|
||||
{
|
||||
sb.AppendLine(" continueOnError: true");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Publish SARIF
|
||||
if (options.Upload.UploadSarif)
|
||||
{
|
||||
sb.AppendLine(" - task: PublishBuildArtifacts@1");
|
||||
sb.AppendLine(" displayName: 'Publish SARIF Results'");
|
||||
sb.AppendLine(" inputs:");
|
||||
sb.AppendLine(" pathToPublish: '$(SARIF_FILE)'");
|
||||
sb.AppendLine(" artifactName: 'CodeAnalysisLogs'");
|
||||
sb.AppendLine();
|
||||
|
||||
// Upload to Advanced Security if available
|
||||
sb.AppendLine(" - task: AdvancedSecurity-Codeql-Autobuild@1");
|
||||
sb.AppendLine(" displayName: 'Upload to Advanced Security'");
|
||||
sb.AppendLine(" condition: always()");
|
||||
sb.AppendLine(" continueOnError: true");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Publish SBOM
|
||||
if (options.Upload.UploadSbom && options.Scan.GenerateSbom)
|
||||
{
|
||||
sb.AppendLine(" - task: PublishBuildArtifacts@1");
|
||||
sb.AppendLine(" displayName: 'Publish SBOM'");
|
||||
sb.AppendLine(" inputs:");
|
||||
sb.AppendLine(" pathToPublish: '$(SBOM_FILE)'");
|
||||
sb.AppendLine(" artifactName: 'SBOM'");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public ValidationResult Validate(WorkflowOptions options)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (options.Platform != CiPlatform.AzureDevOps)
|
||||
errors.Add($"Platform must be AzureDevOps, got {options.Platform}");
|
||||
|
||||
var baseValidation = options.Validate();
|
||||
if (!baseValidation.IsValid)
|
||||
errors.AddRange(baseValidation.Errors);
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
private static void GenerateTriggers(StringBuilder sb, WorkflowOptions options)
|
||||
{
|
||||
var triggers = options.Triggers;
|
||||
|
||||
// CI trigger (push)
|
||||
if (triggers.PushBranches.Length > 0)
|
||||
{
|
||||
sb.AppendLine("trigger:");
|
||||
sb.AppendLine(" branches:");
|
||||
sb.AppendLine(" include:");
|
||||
foreach (var branch in triggers.PushBranches)
|
||||
{
|
||||
sb.AppendLine($" - {branch}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("trigger: none");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// PR trigger
|
||||
if (triggers.PullRequestBranches.Length > 0)
|
||||
{
|
||||
sb.AppendLine("pr:");
|
||||
sb.AppendLine(" branches:");
|
||||
sb.AppendLine(" include:");
|
||||
foreach (var branch in triggers.PullRequestBranches)
|
||||
{
|
||||
sb.AppendLine($" - {branch}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("pr: none");
|
||||
}
|
||||
|
||||
// Schedule trigger
|
||||
if (!string.IsNullOrEmpty(triggers.Schedule))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("schedules:");
|
||||
sb.AppendLine($" - cron: '{triggers.Schedule}'");
|
||||
sb.AppendLine(" displayName: 'Scheduled Security Scan'");
|
||||
sb.AppendLine(" branches:");
|
||||
sb.AppendLine(" include:");
|
||||
sb.AppendLine(" - main");
|
||||
sb.AppendLine(" always: true");
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildScanArgs(WorkflowOptions options)
|
||||
{
|
||||
var args = new List<string>();
|
||||
var scan = options.Scan;
|
||||
|
||||
if (scan.ImageRef is not null)
|
||||
{
|
||||
args.Add($"image:{scan.ImageRef}");
|
||||
}
|
||||
else if (scan.ScanPath is not null)
|
||||
{
|
||||
args.Add(scan.ScanPath);
|
||||
}
|
||||
|
||||
args.Add($"--severity {scan.MinSeverity}");
|
||||
|
||||
if (!scan.ScanVulnerabilities)
|
||||
args.Add("--skip-vulnerabilities");
|
||||
|
||||
if (!scan.ScanSecrets)
|
||||
args.Add("--skip-secrets");
|
||||
|
||||
if (scan.IncludeReachability)
|
||||
args.Add("--reachability");
|
||||
|
||||
if (scan.FailOnSeverity is not null)
|
||||
args.Add($"--exit-code-on {scan.FailOnSeverity}");
|
||||
|
||||
foreach (var arg in scan.AdditionalArgs)
|
||||
{
|
||||
args.Add(arg);
|
||||
}
|
||||
|
||||
return string.Join(" ", args);
|
||||
}
|
||||
}
|
||||
24
src/Tools/StellaOps.Tools.WorkflowGenerator/CiPlatform.cs
Normal file
24
src/Tools/StellaOps.Tools.WorkflowGenerator/CiPlatform.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// <copyright file="CiPlatform.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Target CI/CD platform.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Create models
|
||||
/// </summary>
|
||||
public enum CiPlatform
|
||||
{
|
||||
/// <summary>GitHub Actions.</summary>
|
||||
GitHubActions,
|
||||
|
||||
/// <summary>GitLab CI/CD.</summary>
|
||||
GitLabCi,
|
||||
|
||||
/// <summary>Azure DevOps Pipelines.</summary>
|
||||
AzureDevOps,
|
||||
|
||||
/// <summary>Gitea Actions.</summary>
|
||||
GiteaActions
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// <copyright file="GitHubActionsGenerator.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Generates GitHub Actions workflow YAML.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Implement generators
|
||||
/// </summary>
|
||||
public sealed class GitHubActionsGenerator : IWorkflowGenerator
|
||||
{
|
||||
public CiPlatform Platform => CiPlatform.GitHubActions;
|
||||
|
||||
public string PlatformName => "GitHub Actions";
|
||||
|
||||
public string DefaultFileName => "stellaops-scan.yml";
|
||||
|
||||
public string Generate(WorkflowOptions options)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Header comment
|
||||
if (options.IncludeComments)
|
||||
{
|
||||
sb.AppendLine("# StellaOps Security Scan Workflow");
|
||||
sb.AppendLine("# Generated by StellaOps Workflow Generator");
|
||||
sb.AppendLine("# https://stellaops.io/docs/ci-integration");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Workflow name
|
||||
sb.AppendLine($"name: {options.Name}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Triggers
|
||||
GenerateTriggers(sb, options);
|
||||
sb.AppendLine();
|
||||
|
||||
// Permissions for security scanning
|
||||
sb.AppendLine("permissions:");
|
||||
sb.AppendLine(" contents: read");
|
||||
sb.AppendLine(" security-events: write");
|
||||
if (options.Upload.UploadSbom)
|
||||
{
|
||||
sb.AppendLine(" actions: read");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Environment variables
|
||||
if (options.EnvironmentVariables.Count > 0)
|
||||
{
|
||||
sb.AppendLine("env:");
|
||||
foreach (var (key, value) in options.EnvironmentVariables)
|
||||
{
|
||||
sb.AppendLine($" {key}: {QuoteIfNeeded(value)}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Jobs
|
||||
sb.AppendLine("jobs:");
|
||||
sb.AppendLine(" security-scan:");
|
||||
sb.AppendLine($" name: Security Scan");
|
||||
sb.AppendLine($" runs-on: {options.Runner ?? "ubuntu-latest"}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" steps:");
|
||||
|
||||
// Checkout
|
||||
sb.AppendLine(" - name: Checkout repository");
|
||||
sb.AppendLine(" uses: actions/checkout@v4");
|
||||
sb.AppendLine();
|
||||
|
||||
// Install StellaOps CLI
|
||||
sb.AppendLine(" - name: Install StellaOps CLI");
|
||||
sb.AppendLine(" run: |");
|
||||
if (options.Scan.CliVersion == "latest")
|
||||
{
|
||||
sb.AppendLine(" curl -fsSL https://get.stellaops.io | sh");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" curl -fsSL https://get.stellaops.io | sh -s -- --version {options.Scan.CliVersion}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Run scan
|
||||
sb.AppendLine(" - name: Run StellaOps scan");
|
||||
sb.AppendLine(" run: |");
|
||||
sb.AppendLine($" stella scan {BuildScanArgs(options)} --output sarif={options.Upload.SarifFileName}");
|
||||
if (options.Scan.GenerateSbom)
|
||||
{
|
||||
sb.AppendLine($" stella sbom --output {options.Upload.SbomFileName}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Upload SARIF
|
||||
if (options.Upload.UploadSarif)
|
||||
{
|
||||
sb.AppendLine(" - name: Upload SARIF to GitHub Code Scanning");
|
||||
sb.AppendLine(" uses: github/codeql-action/upload-sarif@v3");
|
||||
sb.AppendLine(" with:");
|
||||
sb.AppendLine($" sarif_file: {options.Upload.SarifFileName}");
|
||||
if (options.Upload.Category is not null)
|
||||
{
|
||||
sb.AppendLine($" category: {options.Upload.Category}");
|
||||
}
|
||||
if (options.Upload.WaitForProcessing)
|
||||
{
|
||||
sb.AppendLine(" wait-for-processing: true");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Upload SBOM artifact
|
||||
if (options.Upload.UploadSbom && options.Scan.GenerateSbom)
|
||||
{
|
||||
sb.AppendLine(" - name: Upload SBOM artifact");
|
||||
sb.AppendLine(" uses: actions/upload-artifact@v4");
|
||||
sb.AppendLine(" with:");
|
||||
sb.AppendLine(" name: sbom");
|
||||
sb.AppendLine($" path: {options.Upload.SbomFileName}");
|
||||
sb.AppendLine(" retention-days: 90");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public ValidationResult Validate(WorkflowOptions options)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (options.Platform != CiPlatform.GitHubActions)
|
||||
errors.Add($"Platform must be GitHubActions, got {options.Platform}");
|
||||
|
||||
var baseValidation = options.Validate();
|
||||
if (!baseValidation.IsValid)
|
||||
errors.AddRange(baseValidation.Errors);
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
private static void GenerateTriggers(StringBuilder sb, WorkflowOptions options)
|
||||
{
|
||||
sb.AppendLine("on:");
|
||||
|
||||
var triggers = options.Triggers;
|
||||
|
||||
// Push trigger
|
||||
if (triggers.PushBranches.Length > 0)
|
||||
{
|
||||
sb.AppendLine(" push:");
|
||||
sb.AppendLine(" branches:");
|
||||
foreach (var branch in triggers.PushBranches)
|
||||
{
|
||||
sb.AppendLine($" - {branch}");
|
||||
}
|
||||
}
|
||||
|
||||
// Pull request trigger
|
||||
if (triggers.PullRequestBranches.Length > 0)
|
||||
{
|
||||
sb.AppendLine(" pull_request:");
|
||||
sb.AppendLine(" branches:");
|
||||
foreach (var branch in triggers.PullRequestBranches)
|
||||
{
|
||||
sb.AppendLine($" - {branch}");
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule trigger
|
||||
if (!string.IsNullOrEmpty(triggers.Schedule))
|
||||
{
|
||||
sb.AppendLine(" schedule:");
|
||||
sb.AppendLine($" - cron: '{triggers.Schedule}'");
|
||||
}
|
||||
|
||||
// Manual trigger
|
||||
if (triggers.ManualTrigger)
|
||||
{
|
||||
sb.AppendLine(" workflow_dispatch:");
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildScanArgs(WorkflowOptions options)
|
||||
{
|
||||
var args = new List<string>();
|
||||
var scan = options.Scan;
|
||||
|
||||
if (scan.ImageRef is not null)
|
||||
{
|
||||
args.Add($"image:{scan.ImageRef}");
|
||||
}
|
||||
else if (scan.ScanPath is not null)
|
||||
{
|
||||
args.Add(scan.ScanPath);
|
||||
}
|
||||
|
||||
args.Add($"--severity {scan.MinSeverity}");
|
||||
|
||||
if (!scan.ScanVulnerabilities)
|
||||
args.Add("--skip-vulnerabilities");
|
||||
|
||||
if (!scan.ScanSecrets)
|
||||
args.Add("--skip-secrets");
|
||||
|
||||
if (scan.IncludeReachability)
|
||||
args.Add("--reachability");
|
||||
|
||||
if (scan.FailOnSeverity is not null)
|
||||
args.Add($"--exit-code-on {scan.FailOnSeverity}");
|
||||
|
||||
foreach (var arg in scan.AdditionalArgs)
|
||||
{
|
||||
args.Add(arg);
|
||||
}
|
||||
|
||||
return string.Join(" ", args);
|
||||
}
|
||||
|
||||
private static string QuoteIfNeeded(string value)
|
||||
{
|
||||
if (value.Contains(' ') || value.Contains(':') || value.Contains('#'))
|
||||
return $"'{value}'";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
188
src/Tools/StellaOps.Tools.WorkflowGenerator/GitLabCiGenerator.cs
Normal file
188
src/Tools/StellaOps.Tools.WorkflowGenerator/GitLabCiGenerator.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
// <copyright file="GitLabCiGenerator.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Generates GitLab CI/CD pipeline YAML.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Implement generators
|
||||
/// </summary>
|
||||
public sealed class GitLabCiGenerator : IWorkflowGenerator
|
||||
{
|
||||
public CiPlatform Platform => CiPlatform.GitLabCi;
|
||||
|
||||
public string PlatformName => "GitLab CI/CD";
|
||||
|
||||
public string DefaultFileName => ".gitlab-ci.yml";
|
||||
|
||||
public string Generate(WorkflowOptions options)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Header comment
|
||||
if (options.IncludeComments)
|
||||
{
|
||||
sb.AppendLine("# StellaOps Security Scan Pipeline");
|
||||
sb.AppendLine("# Generated by StellaOps Workflow Generator");
|
||||
sb.AppendLine("# https://stellaops.io/docs/ci-integration");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Stages
|
||||
sb.AppendLine("stages:");
|
||||
sb.AppendLine(" - security");
|
||||
sb.AppendLine();
|
||||
|
||||
// Variables
|
||||
sb.AppendLine("variables:");
|
||||
sb.AppendLine($" STELLAOPS_VERSION: \"{options.Scan.CliVersion}\"");
|
||||
sb.AppendLine($" SARIF_FILE: \"{options.Upload.SarifFileName}\"");
|
||||
if (options.Scan.GenerateSbom)
|
||||
{
|
||||
sb.AppendLine($" SBOM_FILE: \"{options.Upload.SbomFileName}\"");
|
||||
}
|
||||
foreach (var (key, value) in options.EnvironmentVariables)
|
||||
{
|
||||
sb.AppendLine($" {key}: \"{value}\"");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Default image
|
||||
sb.AppendLine("default:");
|
||||
sb.AppendLine($" image: {options.Runner ?? "ubuntu:22.04"}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Security scan job
|
||||
sb.AppendLine("stellaops-scan:");
|
||||
sb.AppendLine(" stage: security");
|
||||
|
||||
// Rules (triggers)
|
||||
GenerateRules(sb, options);
|
||||
|
||||
// Before script - install CLI
|
||||
sb.AppendLine(" before_script:");
|
||||
sb.AppendLine(" - apt-get update && apt-get install -y curl");
|
||||
if (options.Scan.CliVersion == "latest")
|
||||
{
|
||||
sb.AppendLine(" - curl -fsSL https://get.stellaops.io | sh");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" - curl -fsSL https://get.stellaops.io | sh -s -- --version $STELLAOPS_VERSION");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Script - run scan
|
||||
sb.AppendLine(" script:");
|
||||
sb.AppendLine($" - stella scan {BuildScanArgs(options)} --output sarif=$SARIF_FILE");
|
||||
if (options.Scan.GenerateSbom)
|
||||
{
|
||||
sb.AppendLine(" - stella sbom --output $SBOM_FILE");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Artifacts
|
||||
sb.AppendLine(" artifacts:");
|
||||
sb.AppendLine(" reports:");
|
||||
sb.AppendLine(" sast: $SARIF_FILE");
|
||||
sb.AppendLine(" paths:");
|
||||
sb.AppendLine(" - $SARIF_FILE");
|
||||
if (options.Scan.GenerateSbom)
|
||||
{
|
||||
sb.AppendLine(" - $SBOM_FILE");
|
||||
}
|
||||
sb.AppendLine(" expire_in: 90 days");
|
||||
sb.AppendLine();
|
||||
|
||||
// Allow failure if not blocking
|
||||
if (options.Scan.FailOnSeverity is null)
|
||||
{
|
||||
sb.AppendLine(" allow_failure: true");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public ValidationResult Validate(WorkflowOptions options)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (options.Platform != CiPlatform.GitLabCi)
|
||||
errors.Add($"Platform must be GitLabCi, got {options.Platform}");
|
||||
|
||||
var baseValidation = options.Validate();
|
||||
if (!baseValidation.IsValid)
|
||||
errors.AddRange(baseValidation.Errors);
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
private static void GenerateRules(StringBuilder sb, WorkflowOptions options)
|
||||
{
|
||||
sb.AppendLine(" rules:");
|
||||
var triggers = options.Triggers;
|
||||
|
||||
// Push to branches
|
||||
foreach (var branch in triggers.PushBranches)
|
||||
{
|
||||
sb.AppendLine($" - if: $CI_COMMIT_BRANCH == \"{branch}\"");
|
||||
}
|
||||
|
||||
// Merge requests
|
||||
if (triggers.PullRequestBranches.Length > 0)
|
||||
{
|
||||
sb.AppendLine(" - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"");
|
||||
}
|
||||
|
||||
// Scheduled
|
||||
if (!string.IsNullOrEmpty(triggers.Schedule))
|
||||
{
|
||||
sb.AppendLine(" - if: $CI_PIPELINE_SOURCE == \"schedule\"");
|
||||
}
|
||||
|
||||
// Manual
|
||||
if (triggers.ManualTrigger)
|
||||
{
|
||||
sb.AppendLine(" - if: $CI_PIPELINE_SOURCE == \"web\"");
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildScanArgs(WorkflowOptions options)
|
||||
{
|
||||
var args = new List<string>();
|
||||
var scan = options.Scan;
|
||||
|
||||
if (scan.ImageRef is not null)
|
||||
{
|
||||
args.Add($"image:{scan.ImageRef}");
|
||||
}
|
||||
else if (scan.ScanPath is not null)
|
||||
{
|
||||
args.Add(scan.ScanPath);
|
||||
}
|
||||
|
||||
args.Add($"--severity {scan.MinSeverity}");
|
||||
|
||||
if (!scan.ScanVulnerabilities)
|
||||
args.Add("--skip-vulnerabilities");
|
||||
|
||||
if (!scan.ScanSecrets)
|
||||
args.Add("--skip-secrets");
|
||||
|
||||
if (scan.IncludeReachability)
|
||||
args.Add("--reachability");
|
||||
|
||||
if (scan.FailOnSeverity is not null)
|
||||
args.Add($"--exit-code-on {scan.FailOnSeverity}");
|
||||
|
||||
foreach (var arg in scan.AdditionalArgs)
|
||||
{
|
||||
args.Add(arg);
|
||||
}
|
||||
|
||||
return string.Join(" ", args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// <copyright file="IWorkflowGenerator.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Generates CI/CD workflow definitions.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Create interfaces
|
||||
/// </summary>
|
||||
public interface IWorkflowGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Platform identifier.
|
||||
/// </summary>
|
||||
CiPlatform Platform { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Platform display name.
|
||||
/// </summary>
|
||||
string PlatformName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Default filename for the workflow.
|
||||
/// </summary>
|
||||
string DefaultFileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Generate workflow YAML.
|
||||
/// </summary>
|
||||
/// <param name="options">Workflow options.</param>
|
||||
/// <returns>Generated YAML content.</returns>
|
||||
string Generate(WorkflowOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Validate options for this platform.
|
||||
/// </summary>
|
||||
/// <param name="options">Options to validate.</param>
|
||||
/// <returns>Validation result.</returns>
|
||||
ValidationResult Validate(WorkflowOptions options);
|
||||
}
|
||||
89
src/Tools/StellaOps.Tools.WorkflowGenerator/ScanConfig.cs
Normal file
89
src/Tools/StellaOps.Tools.WorkflowGenerator/ScanConfig.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
// <copyright file="ScanConfig.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Scan configuration for the workflow.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Create models
|
||||
/// </summary>
|
||||
public sealed record ScanConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// StellaOps CLI version to use.
|
||||
/// </summary>
|
||||
public string CliVersion { get; init; } = "latest";
|
||||
|
||||
/// <summary>
|
||||
/// Image to scan (container image reference).
|
||||
/// </summary>
|
||||
public string? ImageRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to scan (file system path).
|
||||
/// </summary>
|
||||
public string? ScanPath { get; init; } = ".";
|
||||
|
||||
/// <summary>
|
||||
/// Minimum severity to report.
|
||||
/// </summary>
|
||||
public string MinSeverity { get; init; } = "medium";
|
||||
|
||||
/// <summary>
|
||||
/// Enable vulnerability scanning.
|
||||
/// </summary>
|
||||
public bool ScanVulnerabilities { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable secret scanning.
|
||||
/// </summary>
|
||||
public bool ScanSecrets { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable SBOM generation.
|
||||
/// </summary>
|
||||
public bool GenerateSbom { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include reachability analysis.
|
||||
/// </summary>
|
||||
public bool IncludeReachability { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Fail build on findings above this severity.
|
||||
/// </summary>
|
||||
public string? FailOnSeverity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional CLI arguments.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> AdditionalArgs { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Default configuration for repository scanning.
|
||||
/// </summary>
|
||||
public static ScanConfig DefaultRepository => new()
|
||||
{
|
||||
ScanPath = ".",
|
||||
MinSeverity = "medium",
|
||||
ScanVulnerabilities = true,
|
||||
ScanSecrets = true,
|
||||
GenerateSbom = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for container image scanning.
|
||||
/// </summary>
|
||||
public static ScanConfig ContainerImage(string imageRef) => new()
|
||||
{
|
||||
ImageRef = imageRef,
|
||||
ScanPath = null,
|
||||
MinSeverity = "low",
|
||||
ScanVulnerabilities = true,
|
||||
ScanSecrets = false,
|
||||
GenerateSbom = true
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RootNamespace>StellaOps.Tools.WorkflowGenerator</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
50
src/Tools/StellaOps.Tools.WorkflowGenerator/TriggerConfig.cs
Normal file
50
src/Tools/StellaOps.Tools.WorkflowGenerator/TriggerConfig.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// <copyright file="TriggerConfig.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Workflow trigger configuration.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Create models
|
||||
/// </summary>
|
||||
public sealed record TriggerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger on push to branches.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> PushBranches { get; init; } = ["main", "master"];
|
||||
|
||||
/// <summary>
|
||||
/// Trigger on pull request to branches.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> PullRequestBranches { get; init; } = ["main", "master"];
|
||||
|
||||
/// <summary>
|
||||
/// Scheduled cron expression (optional).
|
||||
/// </summary>
|
||||
public string? Schedule { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Allow manual trigger (workflow_dispatch).
|
||||
/// </summary>
|
||||
public bool ManualTrigger { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Default trigger configuration for security scanning.
|
||||
/// </summary>
|
||||
public static TriggerConfig Default => new();
|
||||
|
||||
/// <summary>
|
||||
/// Weekly security scan schedule.
|
||||
/// </summary>
|
||||
public static TriggerConfig WeeklySchedule => new()
|
||||
{
|
||||
PushBranches = ["main"],
|
||||
PullRequestBranches = [],
|
||||
Schedule = "0 0 * * 0", // Weekly on Sunday
|
||||
ManualTrigger = true
|
||||
};
|
||||
}
|
||||
47
src/Tools/StellaOps.Tools.WorkflowGenerator/UploadConfig.cs
Normal file
47
src/Tools/StellaOps.Tools.WorkflowGenerator/UploadConfig.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// <copyright file="UploadConfig.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// SARIF upload configuration.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Create models
|
||||
/// </summary>
|
||||
public sealed record UploadConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Upload SARIF to code scanning platform.
|
||||
/// </summary>
|
||||
public bool UploadSarif { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Upload SBOM as artifact.
|
||||
/// </summary>
|
||||
public bool UploadSbom { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Wait for SARIF processing.
|
||||
/// </summary>
|
||||
public bool WaitForProcessing { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Category for SARIF upload (used for deduplication).
|
||||
/// </summary>
|
||||
public string? Category { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SARIF file name.
|
||||
/// </summary>
|
||||
public string SarifFileName { get; init; } = "stellaops-results.sarif";
|
||||
|
||||
/// <summary>
|
||||
/// SBOM file name.
|
||||
/// </summary>
|
||||
public string SbomFileName { get; init; } = "sbom.cdx.json";
|
||||
|
||||
/// <summary>
|
||||
/// Default upload configuration.
|
||||
/// </summary>
|
||||
public static UploadConfig Default => new();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// <copyright file="WorkflowGeneratorFactory.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating workflow generators.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Create factory
|
||||
/// </summary>
|
||||
public sealed class WorkflowGeneratorFactory
|
||||
{
|
||||
private readonly Dictionary<CiPlatform, IWorkflowGenerator> _generators;
|
||||
|
||||
public WorkflowGeneratorFactory()
|
||||
{
|
||||
_generators = new Dictionary<CiPlatform, IWorkflowGenerator>
|
||||
{
|
||||
[CiPlatform.GitHubActions] = new GitHubActionsGenerator(),
|
||||
[CiPlatform.GitLabCi] = new GitLabCiGenerator(),
|
||||
[CiPlatform.AzureDevOps] = new AzureDevOpsGenerator(),
|
||||
[CiPlatform.GiteaActions] = new GitHubActionsGenerator() // Gitea Actions is compatible with GitHub Actions
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a generator for the specified platform.
|
||||
/// </summary>
|
||||
/// <param name="platform">Target CI/CD platform.</param>
|
||||
/// <returns>Workflow generator.</returns>
|
||||
/// <exception cref="ArgumentException">If platform is not supported.</exception>
|
||||
public IWorkflowGenerator GetGenerator(CiPlatform platform)
|
||||
{
|
||||
if (_generators.TryGetValue(platform, out var generator))
|
||||
return generator;
|
||||
|
||||
throw new ArgumentException($"Unsupported platform: {platform}", nameof(platform));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a workflow for the specified options.
|
||||
/// </summary>
|
||||
/// <param name="options">Workflow options.</param>
|
||||
/// <returns>Generated workflow YAML.</returns>
|
||||
public string Generate(WorkflowOptions options)
|
||||
{
|
||||
var generator = GetGenerator(options.Platform);
|
||||
var validation = generator.Validate(options);
|
||||
|
||||
if (!validation.IsValid)
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid options: {string.Join(", ", validation.Errors)}");
|
||||
|
||||
return generator.Generate(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all supported platforms.
|
||||
/// </summary>
|
||||
public IEnumerable<CiPlatform> SupportedPlatforms => _generators.Keys;
|
||||
}
|
||||
106
src/Tools/StellaOps.Tools.WorkflowGenerator/WorkflowOptions.cs
Normal file
106
src/Tools/StellaOps.Tools.WorkflowGenerator/WorkflowOptions.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// <copyright file="WorkflowOptions.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Tools.WorkflowGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Options for workflow generation.
|
||||
/// Sprint: SPRINT_20260109_010_003 Task: Create models
|
||||
/// </summary>
|
||||
public sealed record WorkflowOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Target CI/CD platform.
|
||||
/// </summary>
|
||||
public required CiPlatform Platform { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Workflow name.
|
||||
/// </summary>
|
||||
public string Name { get; init; } = "StellaOps Security Scan";
|
||||
|
||||
/// <summary>
|
||||
/// Trigger configuration.
|
||||
/// </summary>
|
||||
public TriggerConfig Triggers { get; init; } = TriggerConfig.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Scan configuration.
|
||||
/// </summary>
|
||||
public ScanConfig Scan { get; init; } = ScanConfig.DefaultRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Upload configuration.
|
||||
/// </summary>
|
||||
public UploadConfig Upload { get; init; } = UploadConfig.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Runner/image to use.
|
||||
/// </summary>
|
||||
public string? Runner { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variables.
|
||||
/// </summary>
|
||||
public IDictionary<string, string> EnvironmentVariables { get; init; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Include comments in generated YAML.
|
||||
/// </summary>
|
||||
public bool IncludeComments { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Creates default options for GitHub Actions.
|
||||
/// </summary>
|
||||
public static WorkflowOptions GitHubActionsDefault => new()
|
||||
{
|
||||
Platform = CiPlatform.GitHubActions,
|
||||
Runner = "ubuntu-latest"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates default options for GitLab CI.
|
||||
/// </summary>
|
||||
public static WorkflowOptions GitLabCiDefault => new()
|
||||
{
|
||||
Platform = CiPlatform.GitLabCi
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates default options for Azure DevOps.
|
||||
/// </summary>
|
||||
public static WorkflowOptions AzureDevOpsDefault => new()
|
||||
{
|
||||
Platform = CiPlatform.AzureDevOps,
|
||||
Runner = "ubuntu-latest"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Validates the options.
|
||||
/// </summary>
|
||||
public ValidationResult Validate()
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
errors.Add("Workflow name is required");
|
||||
|
||||
if (Scan.ImageRef is null && Scan.ScanPath is null)
|
||||
errors.Add("Either ImageRef or ScanPath must be specified");
|
||||
|
||||
if (Scan.ImageRef is not null && Scan.ScanPath is not null)
|
||||
errors.Add("Only one of ImageRef or ScanPath should be specified");
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validation result.
|
||||
/// </summary>
|
||||
public sealed record ValidationResult(bool IsValid, IReadOnlyList<string> Errors)
|
||||
{
|
||||
public static ValidationResult Success => new(true, []);
|
||||
public static ValidationResult Failure(params string[] errors) => new(false, errors);
|
||||
}
|
||||
Reference in New Issue
Block a user