save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -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);
}
}

View 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
}

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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);
}

View 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
};
}

View File

@@ -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>

View 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
};
}

View 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();
}

View File

@@ -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;
}

View 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);
}

View File

@@ -0,0 +1,155 @@
// <copyright file="AzureDevOpsGeneratorTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Tools.WorkflowGenerator.Tests;
/// <summary>
/// Tests for <see cref="AzureDevOpsGenerator"/>.
/// </summary>
[Trait("Category", "Unit")]
public class AzureDevOpsGeneratorTests
{
private readonly AzureDevOpsGenerator _generator = new();
[Fact]
public void Platform_ReturnsAzureDevOps()
{
_generator.Platform.Should().Be(CiPlatform.AzureDevOps);
}
[Fact]
public void PlatformName_ReturnsAzureDevOps()
{
_generator.PlatformName.Should().Be("Azure DevOps");
}
[Fact]
public void DefaultFileName_ReturnsAzurePipelinesYml()
{
_generator.DefaultFileName.Should().Be("azure-pipelines.yml");
}
[Fact]
public void Generate_DefaultOptions_ContainsPipelineName()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("name: StellaOps Security Scan");
}
[Fact]
public void Generate_DefaultOptions_ContainsTrigger()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("trigger:");
yaml.Should().Contain("branches:");
yaml.Should().Contain("include:");
}
[Fact]
public void Generate_DefaultOptions_ContainsPr()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("pr:");
}
[Fact]
public void Generate_DefaultOptions_ContainsPool()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("pool:");
yaml.Should().Contain("vmImage: 'ubuntu-latest'");
}
[Fact]
public void Generate_DefaultOptions_ContainsStages()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("stages:");
yaml.Should().Contain("- stage: Security");
}
[Fact]
public void Generate_DefaultOptions_ContainsBashTask()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("task: Bash@3");
}
[Fact]
public void Generate_DefaultOptions_ContainsPublishArtifact()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("PublishBuildArtifacts@1");
yaml.Should().Contain("artifactName: 'CodeAnalysisLogs'");
}
[Fact]
public void Generate_DefaultOptions_ContainsCheckout()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("- checkout: self");
yaml.Should().Contain("fetchDepth: 0");
}
[Fact]
public void Generate_WithSchedule_ContainsCron()
{
var options = WorkflowOptions.AzureDevOpsDefault with
{
Triggers = TriggerConfig.WeeklySchedule
};
var yaml = _generator.Generate(options);
yaml.Should().Contain("schedules:");
yaml.Should().Contain("cron:");
}
[Fact]
public void Validate_CorrectPlatform_ReturnsValid()
{
var options = WorkflowOptions.AzureDevOpsDefault;
var result = _generator.Validate(options);
result.IsValid.Should().BeTrue();
}
[Fact]
public void Validate_WrongPlatform_ReturnsInvalid()
{
var options = WorkflowOptions.GitHubActionsDefault;
var result = _generator.Validate(options);
result.IsValid.Should().BeFalse();
}
}

View File

@@ -0,0 +1,246 @@
// <copyright file="GitHubActionsGeneratorTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Tools.WorkflowGenerator.Tests;
/// <summary>
/// Tests for <see cref="GitHubActionsGenerator"/>.
/// </summary>
[Trait("Category", "Unit")]
public class GitHubActionsGeneratorTests
{
private readonly GitHubActionsGenerator _generator = new();
[Fact]
public void Platform_ReturnsGitHubActions()
{
_generator.Platform.Should().Be(CiPlatform.GitHubActions);
}
[Fact]
public void PlatformName_ReturnsGitHubActions()
{
_generator.PlatformName.Should().Be("GitHub Actions");
}
[Fact]
public void DefaultFileName_ReturnsYml()
{
_generator.DefaultFileName.Should().Be("stellaops-scan.yml");
}
[Fact]
public void Generate_DefaultOptions_ContainsWorkflowName()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("name: StellaOps Security Scan");
}
[Fact]
public void Generate_DefaultOptions_ContainsPushTrigger()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("push:");
yaml.Should().Contain("- main");
}
[Fact]
public void Generate_DefaultOptions_ContainsPullRequestTrigger()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("pull_request:");
}
[Fact]
public void Generate_DefaultOptions_ContainsWorkflowDispatch()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("workflow_dispatch:");
}
[Fact]
public void Generate_DefaultOptions_ContainsSecurityEventsPermission()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("security-events: write");
}
[Fact]
public void Generate_DefaultOptions_ContainsCheckoutStep()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("uses: actions/checkout@v4");
}
[Fact]
public void Generate_DefaultOptions_ContainsCliInstallation()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("curl -fsSL https://get.stellaops.io | sh");
}
[Fact]
public void Generate_DefaultOptions_ContainsSarifUpload()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("github/codeql-action/upload-sarif@v3");
}
[Fact]
public void Generate_WithCustomName_UsesCustomName()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Name = "Custom Security Scan"
};
var yaml = _generator.Generate(options);
yaml.Should().Contain("name: Custom Security Scan");
}
[Fact]
public void Generate_WithSchedule_ContainsCron()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Triggers = TriggerConfig.WeeklySchedule
};
var yaml = _generator.Generate(options);
yaml.Should().Contain("schedule:");
yaml.Should().Contain("cron: '0 0 * * 0'");
}
[Fact]
public void Generate_WithNoComments_OmitsComments()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
IncludeComments = false
};
var yaml = _generator.Generate(options);
yaml.Should().NotContain("# StellaOps Security Scan Workflow");
}
[Fact]
public void Generate_WithImageScan_UsesImageSyntax()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Scan = ScanConfig.ContainerImage("myregistry.io/myimage:latest")
};
var yaml = _generator.Generate(options);
yaml.Should().Contain("image:myregistry.io/myimage:latest");
}
[Fact]
public void Generate_WithReachability_IncludesReachabilityFlag()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Scan = ScanConfig.DefaultRepository with { IncludeReachability = true }
};
var yaml = _generator.Generate(options);
yaml.Should().Contain("--reachability");
}
[Fact]
public void Generate_WithFailOnSeverity_IncludesExitCode()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Scan = ScanConfig.DefaultRepository with { FailOnSeverity = "high" }
};
var yaml = _generator.Generate(options);
yaml.Should().Contain("--exit-code-on high");
}
[Fact]
public void Generate_WithEnvironmentVariables_IncludesEnvBlock()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
EnvironmentVariables = new Dictionary<string, string>
{
["STELLAOPS_API_KEY"] = "${{ secrets.STELLAOPS_API_KEY }}"
}
};
var yaml = _generator.Generate(options);
yaml.Should().Contain("env:");
yaml.Should().Contain("STELLAOPS_API_KEY:");
}
[Fact]
public void Generate_WithCategory_IncludesCategory()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Upload = UploadConfig.Default with { Category = "stellaops-scan" }
};
var yaml = _generator.Generate(options);
yaml.Should().Contain("category: stellaops-scan");
}
[Fact]
public void Validate_CorrectPlatform_ReturnsValid()
{
var options = WorkflowOptions.GitHubActionsDefault;
var result = _generator.Validate(options);
result.IsValid.Should().BeTrue();
}
[Fact]
public void Validate_WrongPlatform_ReturnsInvalid()
{
var options = WorkflowOptions.GitLabCiDefault;
var result = _generator.Validate(options);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("GitLabCi"));
}
}

View File

@@ -0,0 +1,155 @@
// <copyright file="GitLabCiGeneratorTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Tools.WorkflowGenerator.Tests;
/// <summary>
/// Tests for <see cref="GitLabCiGenerator"/>.
/// </summary>
[Trait("Category", "Unit")]
public class GitLabCiGeneratorTests
{
private readonly GitLabCiGenerator _generator = new();
[Fact]
public void Platform_ReturnsGitLabCi()
{
_generator.Platform.Should().Be(CiPlatform.GitLabCi);
}
[Fact]
public void PlatformName_ReturnsGitLabCiCd()
{
_generator.PlatformName.Should().Be("GitLab CI/CD");
}
[Fact]
public void DefaultFileName_ReturnsGitLabCiYml()
{
_generator.DefaultFileName.Should().Be(".gitlab-ci.yml");
}
[Fact]
public void Generate_DefaultOptions_ContainsStages()
{
var options = WorkflowOptions.GitLabCiDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("stages:");
yaml.Should().Contain("- security");
}
[Fact]
public void Generate_DefaultOptions_ContainsVariables()
{
var options = WorkflowOptions.GitLabCiDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("variables:");
yaml.Should().Contain("STELLAOPS_VERSION:");
}
[Fact]
public void Generate_DefaultOptions_ContainsStellaopsJob()
{
var options = WorkflowOptions.GitLabCiDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("stellaops-scan:");
yaml.Should().Contain("stage: security");
}
[Fact]
public void Generate_DefaultOptions_ContainsRules()
{
var options = WorkflowOptions.GitLabCiDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("rules:");
yaml.Should().Contain("$CI_COMMIT_BRANCH");
}
[Fact]
public void Generate_DefaultOptions_ContainsArtifacts()
{
var options = WorkflowOptions.GitLabCiDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("artifacts:");
yaml.Should().Contain("reports:");
yaml.Should().Contain("sast:");
}
[Fact]
public void Generate_DefaultOptions_ContainsCliInstallation()
{
var options = WorkflowOptions.GitLabCiDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("before_script:");
yaml.Should().Contain("curl -fsSL https://get.stellaops.io | sh");
}
[Fact]
public void Generate_DefaultOptions_AllowsFailure()
{
var options = WorkflowOptions.GitLabCiDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("allow_failure: true");
}
[Fact]
public void Generate_WithFailOnSeverity_DoesNotAllowFailure()
{
var options = WorkflowOptions.GitLabCiDefault with
{
Scan = ScanConfig.DefaultRepository with { FailOnSeverity = "critical" }
};
var yaml = _generator.Generate(options);
yaml.Should().NotContain("allow_failure: true");
}
[Fact]
public void Generate_WithMergeRequest_IncludesMrRule()
{
var options = WorkflowOptions.GitLabCiDefault;
var yaml = _generator.Generate(options);
yaml.Should().Contain("merge_request_event");
}
[Fact]
public void Validate_CorrectPlatform_ReturnsValid()
{
var options = WorkflowOptions.GitLabCiDefault;
var result = _generator.Validate(options);
result.IsValid.Should().BeTrue();
}
[Fact]
public void Validate_WrongPlatform_ReturnsInvalid()
{
var options = WorkflowOptions.GitHubActionsDefault;
var result = _generator.Validate(options);
result.IsValid.Should().BeFalse();
}
}

View File

@@ -0,0 +1,214 @@
// <copyright file="GoldenFixtureTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Tools.WorkflowGenerator.Tests;
/// <summary>
/// Golden fixture tests for workflow generation.
/// These tests ensure generated workflows match expected output.
/// </summary>
[Trait("Category", "Unit")]
public class GoldenFixtureTests
{
[Fact]
public void GitHubActions_Minimal_MatchesExpected()
{
var generator = new GitHubActionsGenerator();
var options = new WorkflowOptions
{
Platform = CiPlatform.GitHubActions,
Name = "StellaOps Scan",
Triggers = new TriggerConfig { PushBranches = ["main"] },
Scan = new ScanConfig { ScanPath = "." }
};
var result = generator.Generate(options);
// Should contain workflow name
result.Should().Contain("name: StellaOps Scan");
// Should contain push trigger
result.Should().Contain("push:");
// Should have jobs section
result.Should().Contain("jobs:");
}
[Fact]
public void GitHubActions_WithSchedule_IncludesCron()
{
var generator = new GitHubActionsGenerator();
var options = new WorkflowOptions
{
Platform = CiPlatform.GitHubActions,
Name = "Scheduled Scan",
Triggers = new TriggerConfig
{
Schedule = "0 0 * * *"
},
Scan = new ScanConfig { ScanPath = "." }
};
var result = generator.Generate(options);
result.Should().Contain("schedule:");
result.Should().Contain("- cron: '0 0 * * *'");
}
[Fact]
public void GitHubActions_WithSarifUpload_IncludesCodeScanningAction()
{
var generator = new GitHubActionsGenerator();
var options = new WorkflowOptions
{
Platform = CiPlatform.GitHubActions,
Name = "Scan with SARIF",
Triggers = new TriggerConfig { PushBranches = ["main"] },
Scan = new ScanConfig { ScanPath = "." },
Upload = new UploadConfig
{
UploadSarif = true,
SarifFileName = "results.sarif"
}
};
var result = generator.Generate(options);
result.Should().Contain("github/codeql-action/upload-sarif");
result.Should().Contain("sarif_file: results.sarif");
}
[Fact]
public void GitLabCi_Minimal_MatchesExpected()
{
var generator = new GitLabCiGenerator();
var options = new WorkflowOptions
{
Platform = CiPlatform.GitLabCi,
Name = "stellaops-scan",
Scan = new ScanConfig { ScanPath = "." }
};
var result = generator.Generate(options);
// Should contain stages
result.Should().Contain("stages:");
// Should have scan job
result.Should().Contain("stellaops-scan:");
}
[Fact]
public void GitLabCi_WithRules_IncludesBranchFilter()
{
var generator = new GitLabCiGenerator();
var options = new WorkflowOptions
{
Platform = CiPlatform.GitLabCi,
Name = "stellaops-scan",
Triggers = new TriggerConfig { PushBranches = ["main", "develop"] },
Scan = new ScanConfig { ScanPath = "." }
};
var result = generator.Generate(options);
result.Should().Contain("rules:");
result.Should().Contain("$CI_COMMIT_BRANCH == \"main\"");
result.Should().Contain("$CI_COMMIT_BRANCH == \"develop\"");
}
[Fact]
public void AzureDevOps_Minimal_MatchesExpected()
{
var generator = new AzureDevOpsGenerator();
var options = new WorkflowOptions
{
Platform = CiPlatform.AzureDevOps,
Name = "StellaOps Scan",
Triggers = new TriggerConfig { PushBranches = ["main"] },
Scan = new ScanConfig { ScanPath = "." }
};
var result = generator.Generate(options);
// Should contain trigger configuration
result.Should().Contain("trigger:");
// Should have stages section
result.Should().Contain("stages:");
}
[Fact]
public void AzureDevOps_WithPr_IncludesPrTrigger()
{
var generator = new AzureDevOpsGenerator();
var options = new WorkflowOptions
{
Platform = CiPlatform.AzureDevOps,
Name = "PR Scan",
Triggers = new TriggerConfig { PullRequestBranches = ["main"] },
Scan = new ScanConfig { ScanPath = "." }
};
var result = generator.Generate(options);
result.Should().Contain("pr:");
result.Should().Contain("branches:");
result.Should().Contain("include:");
result.Should().Contain("- main");
}
[Fact]
public void AllPlatforms_WithAllTriggers_GenerateValidYaml()
{
var options = new WorkflowOptions
{
Platform = CiPlatform.GitHubActions,
Name = "Full Test",
Triggers = new TriggerConfig
{
PushBranches = ["main", "release/*"],
PullRequestBranches = ["main"],
Schedule = "0 0 * * 0",
ManualTrigger = true
},
Scan = new ScanConfig
{
ScanPath = "./src"
},
Upload = new UploadConfig
{
UploadSarif = true
}
};
var githubGenerator = new GitHubActionsGenerator();
var gitlabGenerator = new GitLabCiGenerator();
var azureGenerator = new AzureDevOpsGenerator();
var githubResult = githubGenerator.Generate(options);
var gitlabResult = gitlabGenerator.Generate(options with { Platform = CiPlatform.GitLabCi });
var azureResult = azureGenerator.Generate(options with { Platform = CiPlatform.AzureDevOps });
// All should generate non-empty YAML
githubResult.Should().NotBeNullOrEmpty();
gitlabResult.Should().NotBeNullOrEmpty();
azureResult.Should().NotBeNullOrEmpty();
// All should have scan path
githubResult.Should().Contain("./src");
gitlabResult.Should().Contain("./src");
azureResult.Should().Contain("./src");
}
[Fact]
public void Factory_CreatesCorrectGeneratorForPlatform()
{
var factory = new WorkflowGeneratorFactory();
factory.GetGenerator(CiPlatform.GitHubActions).Should().BeOfType<GitHubActionsGenerator>();
factory.GetGenerator(CiPlatform.GitLabCi).Should().BeOfType<GitLabCiGenerator>();
factory.GetGenerator(CiPlatform.AzureDevOps).Should().BeOfType<AzureDevOpsGenerator>();
factory.GetGenerator(CiPlatform.GiteaActions).Should().BeOfType<GitHubActionsGenerator>(); // Uses GH syntax
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Tools.WorkflowGenerator\StellaOps.Tools.WorkflowGenerator.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,98 @@
// <copyright file="WorkflowGeneratorFactoryTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Tools.WorkflowGenerator.Tests;
/// <summary>
/// Tests for <see cref="WorkflowGeneratorFactory"/>.
/// </summary>
[Trait("Category", "Unit")]
public class WorkflowGeneratorFactoryTests
{
private readonly WorkflowGeneratorFactory _factory = new();
[Theory]
[InlineData(CiPlatform.GitHubActions)]
[InlineData(CiPlatform.GitLabCi)]
[InlineData(CiPlatform.AzureDevOps)]
[InlineData(CiPlatform.GiteaActions)]
public void GetGenerator_SupportedPlatform_ReturnsGenerator(CiPlatform platform)
{
var generator = _factory.GetGenerator(platform);
generator.Should().NotBeNull();
}
[Fact]
public void GetGenerator_GitHubActions_ReturnsGitHubActionsGenerator()
{
var generator = _factory.GetGenerator(CiPlatform.GitHubActions);
generator.Should().BeOfType<GitHubActionsGenerator>();
}
[Fact]
public void GetGenerator_GitLabCi_ReturnsGitLabCiGenerator()
{
var generator = _factory.GetGenerator(CiPlatform.GitLabCi);
generator.Should().BeOfType<GitLabCiGenerator>();
}
[Fact]
public void GetGenerator_AzureDevOps_ReturnsAzureDevOpsGenerator()
{
var generator = _factory.GetGenerator(CiPlatform.AzureDevOps);
generator.Should().BeOfType<AzureDevOpsGenerator>();
}
[Fact]
public void GetGenerator_GiteaActions_ReturnsGitHubActionsCompatibleGenerator()
{
// Gitea Actions is compatible with GitHub Actions syntax
var generator = _factory.GetGenerator(CiPlatform.GiteaActions);
generator.Should().BeOfType<GitHubActionsGenerator>();
}
[Fact]
public void Generate_ValidOptions_ReturnsYaml()
{
var options = WorkflowOptions.GitHubActionsDefault;
var yaml = _factory.Generate(options);
yaml.Should().NotBeNullOrEmpty();
yaml.Should().Contain("name:");
}
[Fact]
public void Generate_InvalidOptions_ThrowsException()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Scan = new ScanConfig { ImageRef = null, ScanPath = null }
};
var act = () => _factory.Generate(options);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Invalid options*");
}
[Fact]
public void SupportedPlatforms_ContainsAllPlatforms()
{
var supported = _factory.SupportedPlatforms.ToList();
supported.Should().Contain(CiPlatform.GitHubActions);
supported.Should().Contain(CiPlatform.GitLabCi);
supported.Should().Contain(CiPlatform.AzureDevOps);
supported.Should().Contain(CiPlatform.GiteaActions);
}
}

View File

@@ -0,0 +1,102 @@
// <copyright file="WorkflowOptionsTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Tools.WorkflowGenerator.Tests;
/// <summary>
/// Tests for <see cref="WorkflowOptions"/>.
/// </summary>
[Trait("Category", "Unit")]
public class WorkflowOptionsTests
{
[Fact]
public void Validate_ValidOptions_ReturnsValid()
{
var options = WorkflowOptions.GitHubActionsDefault;
var result = options.Validate();
result.IsValid.Should().BeTrue();
result.Errors.Should().BeEmpty();
}
[Fact]
public void Validate_EmptyName_ReturnsInvalid()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Name = ""
};
var result = options.Validate();
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("name"));
}
[Fact]
public void Validate_BothImageAndPath_ReturnsInvalid()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Scan = new ScanConfig
{
ImageRef = "myimage:latest",
ScanPath = "."
}
};
var result = options.Validate();
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("Only one"));
}
[Fact]
public void Validate_NeitherImageNorPath_ReturnsInvalid()
{
var options = WorkflowOptions.GitHubActionsDefault with
{
Scan = new ScanConfig
{
ImageRef = null,
ScanPath = null
}
};
var result = options.Validate();
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("Either"));
}
[Fact]
public void GitHubActionsDefault_HasCorrectPlatform()
{
var options = WorkflowOptions.GitHubActionsDefault;
options.Platform.Should().Be(CiPlatform.GitHubActions);
options.Runner.Should().Be("ubuntu-latest");
}
[Fact]
public void GitLabCiDefault_HasCorrectPlatform()
{
var options = WorkflowOptions.GitLabCiDefault;
options.Platform.Should().Be(CiPlatform.GitLabCi);
}
[Fact]
public void AzureDevOpsDefault_HasCorrectPlatform()
{
var options = WorkflowOptions.AzureDevOpsDefault;
options.Platform.Should().Be(CiPlatform.AzureDevOps);
options.Runner.Should().Be("ubuntu-latest");
}
}