audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user