Add call graph fixtures for various languages and scenarios
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

- Introduced `all-edge-reasons.json` to test edge resolution reasons in .NET.
- Added `all-visibility-levels.json` to validate method visibility levels in .NET.
- Created `dotnet-aspnetcore-minimal.json` for a minimal ASP.NET Core application.
- Included `go-gin-api.json` for a Go Gin API application structure.
- Added `java-spring-boot.json` for the Spring PetClinic application in Java.
- Introduced `legacy-no-schema.json` for legacy application structure without schema.
- Created `node-express-api.json` for an Express.js API application structure.
This commit is contained in:
master
2025-12-16 10:44:24 +02:00
parent 4391f35d8a
commit 5a480a3c2a
223 changed files with 19367 additions and 727 deletions

View File

@@ -0,0 +1,179 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Scoring;
namespace StellaOps.Policy.Engine.Scoring;
/// <summary>
/// Provides score policies with caching and digest computation.
/// </summary>
public interface IScorePolicyService
{
/// <summary>
/// Gets the active score policy for a tenant.
/// </summary>
ScorePolicy GetPolicy(string tenantId);
/// <summary>
/// Computes the canonical digest of a score policy for determinism tracking.
/// </summary>
string ComputePolicyDigest(ScorePolicy policy);
/// <summary>
/// Gets the cached digest for a tenant's policy.
/// </summary>
string? GetCachedDigest(string tenantId);
/// <summary>
/// Reloads policies from disk (cache invalidation).
/// </summary>
void Reload();
}
public sealed class ScorePolicyService : IScorePolicyService
{
private readonly IScorePolicyProvider _provider;
private readonly ConcurrentDictionary<string, (ScorePolicy Policy, string Digest)> _cache = new();
private readonly ILogger<ScorePolicyService> _logger;
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
public ScorePolicyService(
IScorePolicyProvider provider,
ILogger<ScorePolicyService> logger)
{
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public ScorePolicy GetPolicy(string tenantId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
return _cache.GetOrAdd(tenantId, tid =>
{
var policy = _provider.GetPolicy(tid);
var digest = ComputePolicyDigest(policy);
_logger.LogInformation(
"Loaded score policy for tenant {TenantId}, digest: {Digest}",
tid, digest);
return (policy, digest);
}).Policy;
}
public string? GetCachedDigest(string tenantId)
{
return _cache.TryGetValue(tenantId, out var entry) ? entry.Digest : null;
}
public string ComputePolicyDigest(ScorePolicy policy)
{
ArgumentNullException.ThrowIfNull(policy);
// Canonical JSON serialization for deterministic digest
var json = JsonSerializer.Serialize(policy, CanonicalJsonOptions);
var bytes = Encoding.UTF8.GetBytes(json);
var hash = SHA256.HashData(bytes);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
public void Reload()
{
var count = _cache.Count;
_cache.Clear();
_logger.LogInformation("Score policy cache cleared ({Count} entries removed)", count);
}
}
/// <summary>
/// Provides score policies from a configured source.
/// </summary>
public interface IScorePolicyProvider
{
/// <summary>
/// Gets the score policy for a tenant.
/// </summary>
ScorePolicy GetPolicy(string tenantId);
}
/// <summary>
/// File-based score policy provider.
/// </summary>
public sealed class FileScorePolicyProvider : IScorePolicyProvider
{
private readonly ScorePolicyLoader _loader;
private readonly string _basePath;
private readonly ILogger<FileScorePolicyProvider> _logger;
public FileScorePolicyProvider(
ScorePolicyLoader loader,
string basePath,
ILogger<FileScorePolicyProvider> logger)
{
_loader = loader ?? throw new ArgumentNullException(nameof(loader));
_basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public ScorePolicy GetPolicy(string tenantId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
// Try tenant-specific policy first
var tenantPath = Path.Combine(_basePath, $"score-policy.{tenantId}.yaml");
if (File.Exists(tenantPath))
{
_logger.LogDebug("Loading tenant-specific score policy from {Path}", tenantPath);
return _loader.LoadFromFile(tenantPath);
}
// Fall back to default policy
var defaultPath = Path.Combine(_basePath, "score-policy.yaml");
if (File.Exists(defaultPath))
{
_logger.LogDebug("Loading default score policy from {Path}", defaultPath);
return _loader.LoadFromFile(defaultPath);
}
// Use built-in default
_logger.LogDebug("Using built-in default score policy for tenant {TenantId}", tenantId);
return ScorePolicy.Default;
}
}
/// <summary>
/// In-memory score policy provider for testing.
/// </summary>
public sealed class InMemoryScorePolicyProvider : IScorePolicyProvider
{
private readonly ConcurrentDictionary<string, ScorePolicy> _policies = new();
private ScorePolicy _defaultPolicy = ScorePolicy.Default;
public ScorePolicy GetPolicy(string tenantId)
{
return _policies.TryGetValue(tenantId, out var policy) ? policy : _defaultPolicy;
}
public void SetPolicy(string tenantId, ScorePolicy policy)
{
_policies[tenantId] = policy;
}
public void SetDefaultPolicy(ScorePolicy policy)
{
_defaultPolicy = policy;
}
public void Clear()
{
_policies.Clear();
_defaultPolicy = ScorePolicy.Default;
}
}

View File

@@ -37,6 +37,7 @@
<ProjectReference Include="../../Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj" />
<ProjectReference Include="../StellaOps.Policy.RiskProfile/StellaOps.Policy.RiskProfile.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Policy.Storage.Postgres/StellaOps.Policy.Storage.Postgres.csproj" />
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.ProofSpine/StellaOps.Scanner.ProofSpine.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Policy.Engine.Tests" />

View File

@@ -0,0 +1,207 @@
using Microsoft.Extensions.Logging;
using StellaOps.Cryptography;
using StellaOps.Scanner.ProofSpine;
namespace StellaOps.Policy.Engine.Vex;
/// <summary>
/// Service for creating proof spines from VEX decisions.
/// </summary>
public interface IVexProofSpineService
{
/// <summary>
/// Creates a proof spine for a VEX decision.
/// </summary>
Task<ProofSpineResult> CreateSpineAsync(
VexStatement statement,
VexProofSpineContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates proof spines for all statements in a VEX document.
/// </summary>
Task<IReadOnlyList<ProofSpineResult>> CreateSpinesForDocumentAsync(
VexDecisionDocument document,
VexProofSpineContext context,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Context information for proof spine creation.
/// </summary>
public sealed record VexProofSpineContext
{
public required string TenantId { get; init; }
public string? ScanId { get; init; }
public string? PolicyProfileId { get; init; }
public string? SbomDigest { get; init; }
public string? GraphDigest { get; init; }
}
/// <summary>
/// Result of proof spine creation.
/// </summary>
public sealed record ProofSpineResult
{
public required string SpineId { get; init; }
public required string ArtifactId { get; init; }
public required string VulnerabilityId { get; init; }
public required string Verdict { get; init; }
public required int SegmentCount { get; init; }
public string? RootHash { get; init; }
public DateTimeOffset CreatedAt { get; init; }
}
/// <summary>
/// Default implementation of <see cref="IVexProofSpineService"/>.
/// </summary>
public sealed class VexProofSpineService : IVexProofSpineService
{
private readonly IDsseSigningService _signer;
private readonly ICryptoProfile _cryptoProfile;
private readonly ICryptoHash _cryptoHash;
private readonly TimeProvider _timeProvider;
private readonly ILogger<VexProofSpineService> _logger;
public VexProofSpineService(
IDsseSigningService signer,
ICryptoProfile cryptoProfile,
ICryptoHash cryptoHash,
TimeProvider timeProvider,
ILogger<VexProofSpineService> logger)
{
_signer = signer ?? throw new ArgumentNullException(nameof(signer));
_cryptoProfile = cryptoProfile ?? throw new ArgumentNullException(nameof(cryptoProfile));
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
private const string ToolId = "stellaops/policy-engine";
private const string ToolVersion = "1.0.0";
/// <inheritdoc/>
public async Task<ProofSpineResult> CreateSpineAsync(
VexStatement statement,
VexProofSpineContext context,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(statement);
ArgumentNullException.ThrowIfNull(context);
var artifactId = statement.Products.FirstOrDefault()?.Id ?? "unknown";
var vulnId = statement.Vulnerability.Id;
var builder = new ProofSpineBuilder(_signer, _cryptoProfile, _cryptoHash, _timeProvider)
.ForArtifact(artifactId)
.ForVulnerability(vulnId);
if (!string.IsNullOrEmpty(context.ScanId))
{
builder.WithScanRun(context.ScanId);
}
if (!string.IsNullOrEmpty(context.PolicyProfileId))
{
builder.WithPolicyProfile(context.PolicyProfileId);
}
// Add SBOM slice segment if available
if (!string.IsNullOrEmpty(context.SbomDigest))
{
builder.AddSbomSlice(
context.SbomDigest,
new[] { artifactId },
ToolId,
ToolVersion);
}
// Add reachability analysis segment if evidence is present
if (statement.Evidence is not null)
{
var graphHash = statement.Evidence.GraphHash ?? context.GraphDigest;
if (!string.IsNullOrEmpty(graphHash))
{
builder.AddReachability(
graphHash,
statement.Evidence.LatticeState ?? "U",
statement.Evidence.Confidence,
statement.Evidence.CallPath?.ToList(),
ToolId,
ToolVersion);
}
}
// Add policy evaluation segment with final verdict
var factors = new Dictionary<string, string>
{
["lattice_state"] = statement.Evidence?.LatticeState ?? "U",
["confidence"] = statement.Evidence?.Confidence.ToString("F2") ?? "0.00"
};
builder.AddPolicyEval(
context.PolicyProfileId ?? "default",
factors,
statement.Status,
statement.Justification ?? "VEX decision based on reachability analysis",
ToolId,
ToolVersion);
// Build the spine
var spine = await builder.BuildAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"Created proof spine {SpineId} for {VulnId}:{ArtifactId} with verdict {Verdict}",
spine.SpineId,
vulnId,
artifactId,
statement.Status);
return new ProofSpineResult
{
SpineId = spine.SpineId,
ArtifactId = artifactId,
VulnerabilityId = vulnId,
Verdict = statement.Status,
SegmentCount = spine.Segments.Count,
RootHash = spine.RootHash,
CreatedAt = spine.CreatedAt
};
}
/// <inheritdoc/>
public async Task<IReadOnlyList<ProofSpineResult>> CreateSpinesForDocumentAsync(
VexDecisionDocument document,
VexProofSpineContext context,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(document);
ArgumentNullException.ThrowIfNull(context);
var results = new List<ProofSpineResult>();
foreach (var statement in document.Statements)
{
try
{
var result = await CreateSpineAsync(statement, context, cancellationToken)
.ConfigureAwait(false);
results.Add(result);
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Failed to create proof spine for {VulnId}",
statement.Vulnerability.Id);
}
}
_logger.LogInformation(
"Created {Count} proof spines for VEX document {DocumentId}",
results.Count,
document.Id);
return results;
}
}