audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration

This commit is contained in:
master
2026-01-14 10:48:00 +02:00
parent d7be6ba34b
commit 95d5898650
379 changed files with 40695 additions and 19041 deletions

View File

@@ -211,36 +211,132 @@ public sealed class SourceConfigValidator : ISourceConfigValidator
{
var root = config.RootElement;
// Optional: acceptedFormats
if (root.TryGetProperty("acceptedFormats", out var formats) &&
formats.ValueKind == JsonValueKind.Array)
// Required: allowedTools
if (!root.TryGetProperty("allowedTools", out var allowedTools) ||
allowedTools.ValueKind != JsonValueKind.Array ||
allowedTools.GetArrayLength() == 0)
{
foreach (var format in formats.EnumerateArray())
errors.Add("allowedTools is required");
}
else
{
foreach (var tool in allowedTools.EnumerateArray())
{
var formatStr = format.GetString();
if (!Enum.TryParse<SbomFormat>(formatStr, true, out _))
if (string.IsNullOrWhiteSpace(tool.GetString()))
{
errors.Add($"Invalid SBOM format: {formatStr}. Valid values: {string.Join(", ", Enum.GetNames<SbomFormat>())}");
errors.Add("allowedTools contains empty value");
break;
}
}
}
// Optional: validationRules
if (root.TryGetProperty("validationRules", out var validation))
// Optional: allowedCiSystems
if (root.TryGetProperty("allowedCiSystems", out var allowedCi))
{
if (validation.TryGetProperty("maxFileSizeBytes", out var maxSize))
if (allowedCi.ValueKind != JsonValueKind.Array)
{
errors.Add("allowedCiSystems must be an array");
}
else
{
foreach (var system in allowedCi.EnumerateArray())
{
if (string.IsNullOrWhiteSpace(system.GetString()))
{
errors.Add("allowedCiSystems contains empty value");
break;
}
}
}
}
// Required: validation
if (!root.TryGetProperty("validation", out var validation) ||
validation.ValueKind != JsonValueKind.Object)
{
errors.Add("validation is required");
}
else
{
if (!validation.TryGetProperty("allowedFormats", out var formats) ||
formats.ValueKind != JsonValueKind.Array ||
formats.GetArrayLength() == 0)
{
errors.Add("validation.allowedFormats is required");
}
else
{
foreach (var format in formats.EnumerateArray())
{
var formatStr = format.GetString();
if (string.IsNullOrWhiteSpace(formatStr))
{
errors.Add("validation.allowedFormats contains empty value");
continue;
}
if (!Enum.TryParse<SbomFormat>(formatStr, true, out _))
{
errors.Add($"Invalid SBOM format: {formatStr}. Valid values: {string.Join(", ", Enum.GetNames<SbomFormat>())}");
}
}
}
if (validation.TryGetProperty("maxSbomSizeBytes", out var maxSize))
{
if (maxSize.TryGetInt64(out var size) && size <= 0)
{
errors.Add("maxFileSizeBytes must be positive");
errors.Add("validation.maxSbomSizeBytes must be positive");
}
}
if (validation.TryGetProperty("minSpecVersion", out var minSpec))
{
if (string.IsNullOrWhiteSpace(minSpec.GetString()))
{
errors.Add("validation.minSpecVersion must be a non-empty string");
}
}
if (validation.TryGetProperty("allowedSigners", out var allowedSigners) &&
allowedSigners.ValueKind != JsonValueKind.Array)
{
errors.Add("validation.allowedSigners must be an array");
}
if (validation.TryGetProperty("requiredFields", out var requiredFields) &&
requiredFields.ValueKind != JsonValueKind.Array)
{
errors.Add("validation.requiredFields must be an array");
}
}
// Warnings for missing recommended settings
if (!root.TryGetProperty("validationRules", out _))
// Required: attribution
if (!root.TryGetProperty("attribution", out var attribution) ||
attribution.ValueKind != JsonValueKind.Object)
{
warnings.Add("No validation rules specified - using defaults");
errors.Add("attribution is required");
}
else
{
if (attribution.TryGetProperty("allowedRepositories", out var allowedRepos))
{
if (allowedRepos.ValueKind != JsonValueKind.Array)
{
errors.Add("attribution.allowedRepositories must be an array");
}
else
{
foreach (var repo in allowedRepos.EnumerateArray())
{
if (string.IsNullOrWhiteSpace(repo.GetString()))
{
errors.Add("attribution.allowedRepositories contains empty value");
break;
}
}
}
}
}
}
catch (Exception ex)
@@ -256,6 +352,8 @@ public sealed class SourceConfigValidator : ISourceConfigValidator
: ConfigValidationResult.Success();
}
private ConfigValidationResult ValidateGitConfig(JsonDocument config)
{
var errors = new List<string>();
@@ -275,7 +373,7 @@ public sealed class SourceConfigValidator : ISourceConfigValidator
{
var url = repoUrl.GetString()!;
// Allow git://, https://, ssh:// URLs
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
if (!Uri.TryCreate(url, UriKind.Absolute, out _))
{
// Also check for SSH-style URLs (git@github.com:org/repo.git)
if (!url.Contains('@') || !url.Contains(':'))
@@ -285,11 +383,19 @@ public sealed class SourceConfigValidator : ISourceConfigValidator
}
}
// Optional: provider (for better integration)
if (root.TryGetProperty("provider", out var provider))
// Required: provider (for better integration)
if (!root.TryGetProperty("provider", out var provider))
{
errors.Add("provider is required");
}
else
{
var providerStr = provider.GetString();
if (!Enum.TryParse<GitProvider>(providerStr, true, out _))
if (string.IsNullOrWhiteSpace(providerStr))
{
errors.Add("provider is required");
}
else if (!Enum.TryParse<GitProvider>(providerStr, true, out _))
{
errors.Add($"Invalid provider: {providerStr}. Valid values: {string.Join(", ", Enum.GetNames<GitProvider>())}");
}
@@ -305,16 +411,37 @@ public sealed class SourceConfigValidator : ISourceConfigValidator
}
}
// Optional: branchConfig
if (root.TryGetProperty("branchConfig", out var branchConfig))
// Required: branches
if (!root.TryGetProperty("branches", out var branches) ||
branches.ValueKind != JsonValueKind.Object)
{
ValidateBranchConfig(branchConfig, errors);
errors.Add("branches is required");
}
else
{
ValidateBranchConfig(branches, errors);
}
// Warnings
if (!root.TryGetProperty("branchConfig", out _))
// Required: triggers
if (!root.TryGetProperty("triggers", out var triggers) ||
triggers.ValueKind != JsonValueKind.Object)
{
warnings.Add("No branch configuration - using default branch only");
errors.Add("triggers is required");
}
else
{
ValidateTriggerConfig(triggers, errors);
}
// Required: scanOptions
if (!root.TryGetProperty("scanOptions", out var scanOptions) ||
scanOptions.ValueKind != JsonValueKind.Object)
{
errors.Add("scanOptions is required");
}
else
{
ValidateGitScanOptions(scanOptions, errors);
}
}
catch (Exception ex)
@@ -372,24 +499,164 @@ public sealed class SourceConfigValidator : ISourceConfigValidator
}
}
private static void ValidateBranchConfig(JsonElement branchConfig, List<string> errors)
{
if (branchConfig.TryGetProperty("branchPatterns", out var patterns) &&
patterns.ValueKind == JsonValueKind.Array)
if (!branchConfig.TryGetProperty("include", out var include) ||
include.ValueKind != JsonValueKind.Array ||
include.GetArrayLength() == 0)
{
foreach (var pattern in patterns.EnumerateArray())
errors.Add("branches.include is required");
}
else
{
foreach (var pattern in include.EnumerateArray())
{
var patternStr = pattern.GetString();
if (string.IsNullOrWhiteSpace(patternStr))
if (string.IsNullOrWhiteSpace(pattern.GetString()))
{
errors.Add("branchConfig.branchPatterns contains empty pattern");
errors.Add("branches.include contains empty pattern");
}
}
}
if (branchConfig.TryGetProperty("exclude", out var exclude))
{
if (exclude.ValueKind != JsonValueKind.Array)
{
errors.Add("branches.exclude must be an array");
}
else
{
foreach (var pattern in exclude.EnumerateArray())
{
if (string.IsNullOrWhiteSpace(pattern.GetString()))
{
errors.Add("branches.exclude contains empty pattern");
}
}
}
}
}
#region JSON Schemas
private static void ValidateTriggerConfig(JsonElement triggers, List<string> errors)
{
if (triggers.TryGetProperty("tagPatterns", out var tagPatterns))
{
if (tagPatterns.ValueKind != JsonValueKind.Array)
{
errors.Add("triggers.tagPatterns must be an array");
}
else
{
foreach (var pattern in tagPatterns.EnumerateArray())
{
if (string.IsNullOrWhiteSpace(pattern.GetString()))
{
errors.Add("triggers.tagPatterns contains empty pattern");
}
}
}
}
if (triggers.TryGetProperty("prActions", out var prActions))
{
if (prActions.ValueKind != JsonValueKind.Array)
{
errors.Add("triggers.prActions must be an array");
}
else
{
foreach (var action in prActions.EnumerateArray())
{
var actionStr = action.GetString();
if (string.IsNullOrWhiteSpace(actionStr) ||
!Enum.TryParse<PullRequestAction>(actionStr, true, out _))
{
errors.Add($"Invalid prAction: {actionStr}. Valid values: {string.Join(", ", Enum.GetNames<PullRequestAction>())}");
}
}
}
}
}
private static void ValidateGitScanOptions(JsonElement scanOptions, List<string> errors)
{
if (!scanOptions.TryGetProperty("analyzers", out var analyzers) ||
analyzers.ValueKind != JsonValueKind.Array ||
analyzers.GetArrayLength() == 0)
{
errors.Add("scanOptions.analyzers is required");
}
else
{
foreach (var analyzer in analyzers.EnumerateArray())
{
if (string.IsNullOrWhiteSpace(analyzer.GetString()))
{
errors.Add("scanOptions.analyzers contains empty value");
break;
}
}
}
if (scanOptions.TryGetProperty("scanPaths", out var scanPaths))
{
if (scanPaths.ValueKind != JsonValueKind.Array)
{
errors.Add("scanOptions.scanPaths must be an array");
}
else
{
foreach (var path in scanPaths.EnumerateArray())
{
if (string.IsNullOrWhiteSpace(path.GetString()))
{
errors.Add("scanOptions.scanPaths contains empty value");
break;
}
}
}
}
if (scanOptions.TryGetProperty("excludePaths", out var excludePaths))
{
if (excludePaths.ValueKind != JsonValueKind.Array)
{
errors.Add("scanOptions.excludePaths must be an array");
}
else
{
foreach (var path in excludePaths.EnumerateArray())
{
if (string.IsNullOrWhiteSpace(path.GetString()))
{
errors.Add("scanOptions.excludePaths contains empty value");
break;
}
}
}
}
if (scanOptions.TryGetProperty("cloneDepth", out var cloneDepth))
{
if (cloneDepth.TryGetInt32(out var depth) && depth < 0)
{
errors.Add("scanOptions.cloneDepth must be zero or greater");
}
}
if (scanOptions.TryGetProperty("maxRepoSizeMb", out var maxRepoSize))
{
if (maxRepoSize.TryGetInt32(out var size) && size <= 0)
{
errors.Add("scanOptions.maxRepoSizeMb must be positive");
}
}
}
#region JSON Schemas
private static string GetZastavaSchema() => """
{
"$schema": "http://json-schema.org/draft-07/schema#",
@@ -455,31 +722,51 @@ public sealed class SourceConfigValidator : ISourceConfigValidator
}
""";
private static string GetCliSchema() => """
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["allowedTools", "validation", "attribution"],
"properties": {
"acceptedFormats": {
"type": "array",
"items": {
"type": "string",
"enum": ["CycloneDX", "SPDX", "Syft", "Auto"]
"allowedTools": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
"allowedCiSystems": { "type": "array", "items": { "type": "string" } },
"validation": {
"type": "object",
"required": ["allowedFormats"],
"properties": {
"requireSignedSbom": { "type": "boolean" },
"allowedSigners": { "type": "array", "items": { "type": "string" } },
"maxSbomSizeBytes": { "type": "integer", "minimum": 1 },
"allowedFormats": {
"type": "array",
"items": {
"type": "string",
"enum": ["SpdxJson", "CycloneDxJson", "CycloneDxXml"]
}
},
"minSpecVersion": { "type": "string" },
"requiredFields": { "type": "array", "items": { "type": "string" } }
}
},
"validationRules": {
"type": "object",
"properties": {
"requireSignature": { "type": "boolean" },
"maxFileSizeBytes": { "type": "integer", "minimum": 1 },
"maxComponents": { "type": "integer", "minimum": 1 }
}
},
"attributionRules": {
"attribution": {
"type": "object",
"properties": {
"requireBuildId": { "type": "boolean" },
"requireRepository": { "type": "boolean" },
"requireCommitSha": { "type": "boolean" },
"requirePipelineId": { "type": "boolean" },
"requireArtifactRef": { "type": "boolean" }
"allowedRepositories": { "type": "array", "items": { "type": "string" } }
}
},
"postProcessing": {
"type": "object",
"properties": {
"runVulnMatching": { "type": "boolean" },
"runReachability": { "type": "boolean" },
"applyVex": { "type": "boolean" },
"generateAttestation": { "type": "boolean" }
}
}
}
@@ -490,31 +777,55 @@ public sealed class SourceConfigValidator : ISourceConfigValidator
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["repositoryUrl"],
"required": ["provider", "repositoryUrl", "branches", "triggers", "scanOptions"],
"properties": {
"repositoryUrl": { "type": "string" },
"provider": {
"type": "string",
"enum": ["GitHub", "GitLab", "Bitbucket", "AzureDevOps", "Gitea", "Custom"]
"enum": ["GitHub", "GitLab", "Bitbucket", "AzureDevOps", "Gitea", "Generic"]
},
"authMethod": {
"type": "string",
"enum": ["None", "Token", "SshKey", "App", "BasicAuth"]
"enum": ["Token", "Ssh", "OAuth", "GitHubApp"]
},
"branchConfig": {
"branches": {
"type": "object",
"required": ["include"],
"properties": {
"defaultBranch": { "type": "string" },
"branchPatterns": { "type": "array", "items": { "type": "string" } },
"excludeBranches": { "type": "array", "items": { "type": "string" } }
"include": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
"exclude": { "type": "array", "items": { "type": "string" } }
}
},
"triggerConfig": {
"triggers": {
"type": "object",
"properties": {
"onPush": { "type": "boolean" },
"onPullRequest": { "type": "boolean" },
"onTag": { "type": "boolean" }
"onTag": { "type": "boolean" },
"tagPatterns": { "type": "array", "items": { "type": "string" } },
"prActions": {
"type": "array",
"items": {
"type": "string",
"enum": ["Opened", "Synchronize", "Reopened", "ReadyForReview"]
}
}
}
},
"scanOptions": {
"type": "object",
"required": ["analyzers"],
"properties": {
"analyzers": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
"scanPaths": { "type": "array", "items": { "type": "string" } },
"excludePaths": { "type": "array", "items": { "type": "string" } },
"lockfileOnly": { "type": "boolean" },
"enableReachability": { "type": "boolean" },
"enableVexLookup": { "type": "boolean" },
"cloneDepth": { "type": "integer", "minimum": 0 },
"includeSubmodules": { "type": "boolean" },
"maxRepoSizeMb": { "type": "integer", "minimum": 1 }
}
}
}

View File

@@ -3,7 +3,6 @@ using StellaOps.Determinism;
namespace StellaOps.Scanner.Sources.Domain;
#pragma warning disable CA1062 // Validate arguments of public methods - TimeProvider validated at DI boundary
/// <summary>
/// Represents a configured SBOM ingestion source.

View File

@@ -2,7 +2,6 @@ using StellaOps.Determinism;
namespace StellaOps.Scanner.Sources.Domain;
#pragma warning disable CA1062 // Validate arguments of public methods - TimeProvider validated at DI boundary
/// <summary>
/// Represents a single execution run of an SBOM source.
@@ -138,6 +137,7 @@ public sealed class SbomSourceRun
/// </summary>
public void RecordItemSkipped()
{
ItemsScanned++;
ItemsSkipped++;
}

View File

@@ -359,8 +359,7 @@ public sealed class SbomSourceService : ISbomSourceService
"Triggered manual scan for source {SourceId} ({Name}), run {RunId}",
sourceId, source.Name, run.RunId);
// TODO: Actually dispatch the scan to the trigger service
// For now, just return the run info
// Dispatch is not initiated here; return run metadata only.
return new TriggerScanResult
{
RunId = run.RunId,

View File

@@ -5,6 +5,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0766-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0766-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0766-A | DONE | Already compliant (revalidated 2026-01-07). |
| AUDIT-0684-M | DONE | Revalidated 2026-01-12. |
| AUDIT-0684-T | DONE | Revalidated 2026-01-12. |
| AUDIT-0684-A | DONE | Applied 2026-01-14. |