consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -100,6 +100,12 @@ public static class GovernanceEndpoints
|
||||
.WithName("ValidateRiskProfile")
|
||||
.WithDescription("Validate a candidate risk profile configuration without persisting it. Checks for required fields (name, at least one signal) and emits warnings when signal weights do not sum to 1.0. Used by policy authoring tools to provide inline validation feedback before profile creation.");
|
||||
|
||||
// Risk Budget endpoints
|
||||
governance.MapGet("/risk-budget/dashboard", GetRiskBudgetDashboardAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetRiskBudgetDashboard")
|
||||
.WithDescription("Retrieve the current risk budget dashboard including utilization, headroom, top contributors, active alerts, and KPIs for the tenant.");
|
||||
|
||||
// Audit endpoints
|
||||
governance.MapGet("/audit/events", GetAuditEventsAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAudit))
|
||||
@@ -539,6 +545,117 @@ public static class GovernanceEndpoints
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Risk Budget Handlers
|
||||
// ========================================================================
|
||||
|
||||
private static Task<IResult> GetRiskBudgetDashboardAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
[FromQuery] string? tenantId,
|
||||
[FromQuery] string? projectId)
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var traceId = httpContext.TraceIdentifier;
|
||||
|
||||
var periodStart = new DateTimeOffset(now.Year, ((now.Month - 1) / 3) * 3 + 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
var periodEnd = periodStart.AddMonths(3).AddSeconds(-1);
|
||||
|
||||
var response = new
|
||||
{
|
||||
config = new
|
||||
{
|
||||
id = "budget-001",
|
||||
tenantId = tenant,
|
||||
projectId = projectId ?? (string?)null,
|
||||
name = $"Q{(now.Month - 1) / 3 + 1} {now.Year} Risk Budget",
|
||||
totalBudget = 1000,
|
||||
warningThreshold = 70,
|
||||
criticalThreshold = 90,
|
||||
period = "quarterly",
|
||||
periodStart = periodStart.ToString("O", CultureInfo.InvariantCulture),
|
||||
periodEnd = periodEnd.ToString("O", CultureInfo.InvariantCulture),
|
||||
createdAt = periodStart.AddDays(-15).ToString("O", CultureInfo.InvariantCulture),
|
||||
updatedAt = now.ToString("O", CultureInfo.InvariantCulture)
|
||||
},
|
||||
currentRiskPoints = 720,
|
||||
headroom = 280,
|
||||
utilizationPercent = 72.0,
|
||||
status = "warning",
|
||||
timeSeries = Enumerable.Range(0, 5).Select(i =>
|
||||
{
|
||||
var ts = now.AddDays(-28 + i * 7);
|
||||
var actual = 600 + i * 30;
|
||||
return new
|
||||
{
|
||||
timestamp = ts.ToString("O", CultureInfo.InvariantCulture),
|
||||
actual,
|
||||
budget = 1000,
|
||||
headroom = 1000 - actual
|
||||
};
|
||||
}).ToList(),
|
||||
updatedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
traceId,
|
||||
topContributors = new[]
|
||||
{
|
||||
new { identifier = "pkg:npm/lodash@4.17.20", type = "component", displayName = "lodash", riskPoints = 120, percentOfBudget = 12.0, trend = "stable", delta24h = 0 },
|
||||
new { identifier = "CVE-2024-1234", type = "vulnerability", displayName = "CVE-2024-1234", riskPoints = 95, percentOfBudget = 9.5, trend = "increasing", delta24h = 10 },
|
||||
new { identifier = "vulnerability", type = "category", displayName = "Vulnerabilities", riskPoints = 450, percentOfBudget = 45.0, trend = "stable", delta24h = 5 }
|
||||
},
|
||||
activeAlerts = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = "alert-001",
|
||||
threshold = new { level = 70, severity = "medium", actions = new[] { new { type = "notify", channels = new[] { "slack" } } } },
|
||||
currentUtilization = 72.0,
|
||||
triggeredAt = now.AddDays(-1).ToString("O", CultureInfo.InvariantCulture),
|
||||
acknowledged = false,
|
||||
acknowledgedBy = (string?)null,
|
||||
acknowledgedAt = (string?)null
|
||||
}
|
||||
},
|
||||
governance = new
|
||||
{
|
||||
id = "budget-001",
|
||||
tenantId = tenant,
|
||||
name = $"Q{(now.Month - 1) / 3 + 1} {now.Year} Risk Budget",
|
||||
totalBudget = 1000,
|
||||
warningThreshold = 70,
|
||||
criticalThreshold = 90,
|
||||
period = "quarterly",
|
||||
periodStart = periodStart.ToString("O", CultureInfo.InvariantCulture),
|
||||
periodEnd = periodEnd.ToString("O", CultureInfo.InvariantCulture),
|
||||
createdAt = periodStart.AddDays(-15).ToString("O", CultureInfo.InvariantCulture),
|
||||
updatedAt = now.ToString("O", CultureInfo.InvariantCulture),
|
||||
thresholds = new[]
|
||||
{
|
||||
new { level = 70, severity = "medium", actions = new object[] { new { type = "notify", channels = new[] { "slack", "email" } } } },
|
||||
new { level = 90, severity = "high", actions = new object[] { new { type = "notify", channels = new[] { "slack", "email" } }, new { type = "require_approval" } } },
|
||||
new { level = 100, severity = "critical", actions = new object[] { new { type = "block_deploys" }, new { type = "escalate" } } }
|
||||
},
|
||||
enforceHardLimits = true,
|
||||
gracePeriodHours = 24,
|
||||
autoReset = true,
|
||||
carryoverPercent = 0
|
||||
},
|
||||
kpis = new
|
||||
{
|
||||
headroom = 280,
|
||||
headroomDelta24h = -20,
|
||||
unknownsDelta24h = 3,
|
||||
riskRetired7d = 45,
|
||||
exceptionsExpiring = 2,
|
||||
burnRate = 8.5,
|
||||
projectedDaysToExceeded = 33,
|
||||
traceId
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Audit Handlers
|
||||
// ========================================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="../../../RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Core/StellaOps.RiskEngine.Core.csproj" />
|
||||
<ProjectReference Include="../../../Findings/StellaOps.RiskEngine.Core/StellaOps.RiskEngine.Core.csproj" />
|
||||
<ProjectReference Include="../../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.GoldenSet/StellaOps.BinaryIndex.GoldenSet.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -4,12 +4,21 @@
|
||||
"title": "StellaOps Score Policy v1",
|
||||
"description": "Defines deterministic vulnerability scoring weights, buckets, and overrides",
|
||||
"type": "object",
|
||||
"required": ["policyVersion", "weightsBps"],
|
||||
"required": ["policyVersion", "policyId", "weightsBps"],
|
||||
"properties": {
|
||||
"policyVersion": {
|
||||
"const": "score.v1",
|
||||
"description": "Policy schema version"
|
||||
},
|
||||
"policyId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Deterministic score policy identifier"
|
||||
},
|
||||
"scoringProfile": {
|
||||
"type": "string",
|
||||
"description": "Scoring profile selector"
|
||||
},
|
||||
"weightsBps": {
|
||||
"type": "object",
|
||||
"description": "Weight distribution in basis points (must sum to 10000)",
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class ScorePolicyLoader
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
private static readonly ScorePolicyValidator Validator = new();
|
||||
|
||||
/// <summary>
|
||||
/// Loads a score policy from a YAML file.
|
||||
@@ -56,6 +57,21 @@ public sealed class ScorePolicyLoader
|
||||
throw new ScorePolicyLoadException(
|
||||
$"Unsupported policy version '{policy.PolicyVersion}' in {source}. Expected 'score.v1'");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(policy.PolicyId))
|
||||
{
|
||||
throw new ScorePolicyLoadException($"Missing required field 'policyId' in {source}");
|
||||
}
|
||||
|
||||
var validation = Validator.Validate(policy);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
var details = validation.Errors.Count > 0
|
||||
? string.Join("; ", validation.Errors)
|
||||
: "schema validation returned no detailed errors";
|
||||
throw new ScorePolicyLoadException(
|
||||
$"Schema validation failed in {source}: {details}");
|
||||
}
|
||||
|
||||
// Validate weight sum
|
||||
if (!policy.ValidateWeights())
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace StellaOps.Policy.Scoring;
|
||||
public sealed record ScorePolicy
|
||||
{
|
||||
public required string PolicyVersion { get; init; }
|
||||
public required string PolicyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scoring profile to use. Defaults to "advanced".
|
||||
@@ -35,6 +36,7 @@ public sealed record ScorePolicy
|
||||
public static ScorePolicy Default => new()
|
||||
{
|
||||
PolicyVersion = "score.v1",
|
||||
PolicyId = "score-policy.default.v1",
|
||||
ScoringProfile = "advanced",
|
||||
WeightsBps = WeightsBps.Default,
|
||||
Reachability = ReachabilityPolicyConfig.Default,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Policy.Scoring;
|
||||
|
||||
public static class ScorePolicySchemaResource
|
||||
{
|
||||
private const string SchemaResourceName = "StellaOps.Policy.Schemas.score-policy.v1.schema.json";
|
||||
|
||||
public static Stream OpenSchemaStream()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var stream = assembly.GetManifestResourceStream(SchemaResourceName);
|
||||
if (stream is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to locate embedded schema resource '{SchemaResourceName}'.");
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
public static string ReadSchemaJson()
|
||||
{
|
||||
using var stream = OpenSchemaStream();
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
using Json.Schema;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Scoring;
|
||||
|
||||
@@ -22,7 +23,7 @@ public sealed class ScorePolicyValidator
|
||||
/// </summary>
|
||||
public ScorePolicyValidator()
|
||||
{
|
||||
_schema = JsonSchema.FromText(ScorePolicySchemaJson);
|
||||
_schema = JsonSchema.FromText(ScorePolicySchemaResource.ReadSchemaJson());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -113,179 +114,9 @@ public sealed class ScorePolicyValidator
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Embedded JSON Schema for score.v1 policies.
|
||||
/// </summary>
|
||||
private const string ScorePolicySchemaJson = """
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stellaops.dev/schemas/score-policy.v1.json",
|
||||
"title": "Score Policy",
|
||||
"type": "object",
|
||||
"required": ["policyVersion", "policyId", "weightsBps"],
|
||||
"properties": {
|
||||
"policyVersion": {
|
||||
"type": "string",
|
||||
"const": "score.v1"
|
||||
},
|
||||
"policyId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"policyName": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"weightsBps": {
|
||||
"$ref": "#/$defs/WeightsBps"
|
||||
},
|
||||
"reachabilityConfig": {
|
||||
"$ref": "#/$defs/ReachabilityConfig"
|
||||
},
|
||||
"evidenceConfig": {
|
||||
"$ref": "#/$defs/EvidenceConfig"
|
||||
},
|
||||
"provenanceConfig": {
|
||||
"$ref": "#/$defs/ProvenanceConfig"
|
||||
},
|
||||
"overrides": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ScoreOverride"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"WeightsBps": {
|
||||
"type": "object",
|
||||
"required": ["baseSeverity", "reachability", "evidence", "provenance"],
|
||||
"properties": {
|
||||
"baseSeverity": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10000
|
||||
},
|
||||
"reachability": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10000
|
||||
},
|
||||
"evidence": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10000
|
||||
},
|
||||
"provenance": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReachabilityConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reachableMultiplier": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
},
|
||||
"unreachableMultiplier": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
},
|
||||
"unknownMultiplier": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"EvidenceConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kevWeight": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"epssThreshold": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"epssWeight": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProvenanceConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"signedBonus": {
|
||||
"type": "number"
|
||||
},
|
||||
"rekorVerifiedBonus": {
|
||||
"type": "number"
|
||||
},
|
||||
"unsignedPenalty": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ScoreOverride": {
|
||||
"type": "object",
|
||||
"required": ["id", "match"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"match": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cvePattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"purlPattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"severityEquals": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"setScore": {
|
||||
"type": "number"
|
||||
},
|
||||
"addScore": {
|
||||
"type": "number"
|
||||
},
|
||||
"multiplyScore": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"expires": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<EmbeddedResource Include="Schemas\policy-schema@1.json" />
|
||||
<EmbeddedResource Include="Schemas\policy-scoring-default.json" />
|
||||
<EmbeddedResource Include="Schemas\policy-scoring-schema@1.json" />
|
||||
<EmbeddedResource Include="Schemas\score-policy.v1.schema.json" />
|
||||
<EmbeddedResource Include="Schemas\signals-schema@1.json" />
|
||||
<EmbeddedResource Include="Schemas\spl-schema@1.json" />
|
||||
<EmbeddedResource Include="Schemas\spl-sample@1.json" />
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -9,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
using StellaOps.TestKit.Fixtures;
|
||||
using Xunit;
|
||||
@@ -88,6 +90,10 @@ public sealed class PolicyEngineWebServiceFixture : WebServiceFixture<StellaOps.
|
||||
private static void ConfigureTestServices(IServiceCollection services)
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IConfigureOptions<AuthenticationOptions>>();
|
||||
services.RemoveAll<IPostConfigureOptions<AuthenticationOptions>>();
|
||||
services.RemoveAll<IConfigureOptions<JwtBearerOptions>>();
|
||||
services.RemoveAll<IPostConfigureOptions<JwtBearerOptions>>();
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
@@ -96,6 +102,9 @@ public sealed class PolicyEngineWebServiceFixture : WebServiceFixture<StellaOps.
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
|
||||
TestAuthHandler.SchemeName,
|
||||
_ => { })
|
||||
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
|
||||
StellaOpsAuthenticationDefaults.AuthenticationScheme,
|
||||
_ => { });
|
||||
}
|
||||
|
||||
@@ -105,6 +114,8 @@ public sealed class PolicyEngineWebServiceFixture : WebServiceFixture<StellaOps.
|
||||
{
|
||||
var settings = new Dictionary<string, string?>
|
||||
{
|
||||
["PolicyEngine:ResourceServer:Authority"] = "http://127.0.0.1:59999",
|
||||
["PolicyEngine:ResourceServer:RequireHttpsMetadata"] = "false",
|
||||
["VerdictAttestation:Enabled"] = "true",
|
||||
["VerdictAttestation:FailOnError"] = "true",
|
||||
["VerdictAttestation:RekorEnabled"] = "true",
|
||||
@@ -142,6 +153,7 @@ internal sealed class TestAuthHandler : AuthenticationHandler<AuthenticationSche
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("scope", "policy:read"),
|
||||
new Claim(StellaOpsClaimTypes.Tenant, "test-tenant"),
|
||||
new Claim("tenant_id", "test-tenant"),
|
||||
new Claim(ClaimTypes.NameIdentifier, "test-user")
|
||||
};
|
||||
@@ -153,4 +165,3 @@ internal sealed class TestAuthHandler : AuthenticationHandler<AuthenticationSche
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ public sealed class ScorePolicyServiceCachingTests
|
||||
var policy1 = new ScorePolicy
|
||||
{
|
||||
PolicyVersion = "score.v1",
|
||||
PolicyId = "policy-1",
|
||||
ScoringProfile = "advanced",
|
||||
WeightsBps = new WeightsBps
|
||||
{
|
||||
@@ -206,6 +207,7 @@ public sealed class ScorePolicyServiceCachingTests
|
||||
var policy2 = new ScorePolicy
|
||||
{
|
||||
PolicyVersion = "score.v1",
|
||||
PolicyId = "policy-1",
|
||||
ScoringProfile = "advanced",
|
||||
WeightsBps = new WeightsBps
|
||||
{
|
||||
@@ -225,6 +227,7 @@ public sealed class ScorePolicyServiceCachingTests
|
||||
private static ScorePolicy CreateTestPolicy(string id) => new()
|
||||
{
|
||||
PolicyVersion = "score.v1",
|
||||
PolicyId = id,
|
||||
ScoringProfile = "advanced",
|
||||
WeightsBps = new WeightsBps
|
||||
{
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-005",
|
||||
"policyId": "POL-COMPLEX-001",
|
||||
"policyName": "Complex Multi-Rule Policy",
|
||||
"policyVersion": "3.0.0",
|
||||
"tenantId": "TENANT-003",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:complex123",
|
||||
"outcome": 1,
|
||||
"rulesMatched": 5,
|
||||
"rulesTotal": 8,
|
||||
"violations": [],
|
||||
"warnings": [
|
||||
{
|
||||
"ruleName": "warn_eol_runtime",
|
||||
"severity": "medium",
|
||||
"message": "Runtime marked as EOL; upgrade recommended",
|
||||
"packagePurl": "pkg:deb/debian/python3.9@3.9.2"
|
||||
},
|
||||
{
|
||||
"ruleName": "warn_ruby_git_sources",
|
||||
"severity": "low",
|
||||
"message": "Git-sourced Ruby gem present; review required",
|
||||
"packagePurl": "pkg:gem/custom-gem@1.0.0"
|
||||
}
|
||||
],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"status": 0,
|
||||
"reason": "No critical vulnerabilities"
|
||||
},
|
||||
{
|
||||
"ruleName": "escalate_high_internet",
|
||||
"priority": 4,
|
||||
"status": 0,
|
||||
"reason": "No high severity on internet-exposed assets"
|
||||
},
|
||||
{
|
||||
"ruleName": "require_vex_justification",
|
||||
"priority": 3,
|
||||
"status": 1,
|
||||
"reason": "VEX statement accepted"
|
||||
},
|
||||
{
|
||||
"ruleName": "warn_eol_runtime",
|
||||
"priority": 1,
|
||||
"status": 3,
|
||||
"reason": "EOL runtime detected"
|
||||
},
|
||||
{
|
||||
"ruleName": "warn_ruby_git_sources",
|
||||
"priority": 1,
|
||||
"status": 3,
|
||||
"reason": "Git-sourced gem detected"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": null,
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 123,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24",
|
||||
"ghsa": "2025-12-24",
|
||||
"osv": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:policy789"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-006",
|
||||
"policyId": "POL-PROD-001",
|
||||
"policyName": "Production Baseline Policy",
|
||||
"policyVersion": "1.2.3",
|
||||
"tenantId": "TENANT-001",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:empty123",
|
||||
"outcome": 0,
|
||||
"rulesMatched": 0,
|
||||
"rulesTotal": 5,
|
||||
"violations": [],
|
||||
"warnings": [],
|
||||
"matchedRules": [],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": null,
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 12,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:policy123"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-002",
|
||||
"policyId": "POL-PROD-001",
|
||||
"policyName": "Production Baseline Policy",
|
||||
"policyVersion": "1.2.3",
|
||||
"tenantId": "TENANT-001",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:abc123def456",
|
||||
"outcome": 2,
|
||||
"rulesMatched": 3,
|
||||
"rulesTotal": 5,
|
||||
"violations": [
|
||||
{
|
||||
"ruleName": "block_critical",
|
||||
"severity": "critical",
|
||||
"message": "Critical vulnerability CVE-2024-0001 found",
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"packagePurl": "pkg:npm/lodash@4.17.20",
|
||||
"remediation": "Upgrade to lodash@4.17.21"
|
||||
},
|
||||
{
|
||||
"ruleName": "block_critical",
|
||||
"severity": "critical",
|
||||
"message": "Critical vulnerability CVE-2024-0002 found",
|
||||
"vulnerabilityId": "CVE-2024-0002",
|
||||
"packagePurl": "pkg:npm/express@4.18.0",
|
||||
"remediation": "Upgrade to express@4.19.0"
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"status": 2,
|
||||
"reason": "2 critical vulnerabilities found"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": null,
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 67,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24",
|
||||
"ghsa": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:policy123"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"traceId": "TRACE-2025-002",
|
||||
"policyId": "POL-COMPLEX-001",
|
||||
"policyName": "Complex Multi-Rule Policy",
|
||||
"evaluationContext": {
|
||||
"digestEvaluated": "sha256:xyz789",
|
||||
"tenantId": "TENANT-002",
|
||||
"environment": "staging",
|
||||
"exposure": "internal"
|
||||
},
|
||||
"startedAt": "2025-12-24T12:00:00+00:00",
|
||||
"completedAt": "2025-12-24T12:00:00.089+00:00",
|
||||
"durationMs": 89,
|
||||
"outcome": "Warn",
|
||||
"steps": [
|
||||
{
|
||||
"stepNumber": 1,
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"phase": 3,
|
||||
"condition": "severity.normalized \u003E= \u0022Critical\u0022",
|
||||
"conditionResult": false,
|
||||
"action": null,
|
||||
"explanation": "No critical vulnerabilities",
|
||||
"durationMs": 10,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
},
|
||||
{
|
||||
"stepNumber": 2,
|
||||
"ruleName": "escalate_high_internet",
|
||||
"priority": 4,
|
||||
"phase": 3,
|
||||
"condition": "severity.normalized == \u0022High\u0022 and env.exposure == \u0022internet\u0022",
|
||||
"conditionResult": false,
|
||||
"action": null,
|
||||
"explanation": "Not internet-exposed, skipping escalation",
|
||||
"durationMs": 8,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
},
|
||||
{
|
||||
"stepNumber": 3,
|
||||
"ruleName": "block_ruby_dev",
|
||||
"priority": 4,
|
||||
"phase": 3,
|
||||
"condition": "sbom.any_component(ruby.group(\u0022development\u0022))",
|
||||
"conditionResult": false,
|
||||
"action": null,
|
||||
"explanation": "No development-only Ruby gems",
|
||||
"durationMs": 12,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
},
|
||||
{
|
||||
"stepNumber": 4,
|
||||
"ruleName": "require_vex_justification",
|
||||
"priority": 3,
|
||||
"phase": 3,
|
||||
"condition": "vex.any(status in [\u0022not_affected\u0022,\u0022fixed\u0022])",
|
||||
"conditionResult": true,
|
||||
"action": "status := vex.status",
|
||||
"explanation": "VEX statement found: not_affected (component_not_present)",
|
||||
"durationMs": 25,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
},
|
||||
{
|
||||
"stepNumber": 5,
|
||||
"ruleName": "warn_eol_runtime",
|
||||
"priority": 1,
|
||||
"phase": 3,
|
||||
"condition": "severity.normalized \u003C= \u0022Medium\u0022 and sbom.has_tag(\u0022runtime:eol\u0022)",
|
||||
"conditionResult": true,
|
||||
"action": "warn message \u0022Runtime marked as EOL; upgrade recommended.\u0022",
|
||||
"explanation": "EOL runtime detected: python3.9",
|
||||
"durationMs": 15,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
}
|
||||
],
|
||||
"finalStatus": "warning",
|
||||
"matchedRuleCount": 2,
|
||||
"totalRuleCount": 5
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-001",
|
||||
"policyId": "POL-PROD-001",
|
||||
"policyName": "Production Baseline Policy",
|
||||
"policyVersion": "1.2.3",
|
||||
"tenantId": "TENANT-001",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:abc123def456",
|
||||
"outcome": 0,
|
||||
"rulesMatched": 2,
|
||||
"rulesTotal": 5,
|
||||
"violations": [],
|
||||
"warnings": [],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "allow_low_severity",
|
||||
"priority": 1,
|
||||
"status": 1,
|
||||
"reason": "Severity \u003C= Low, allowing"
|
||||
},
|
||||
{
|
||||
"ruleName": "vex_not_affected",
|
||||
"priority": 2,
|
||||
"status": 1,
|
||||
"reason": "VEX status is not_affected"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": null,
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 42,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24",
|
||||
"ghsa": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:policy123"
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,18 @@ public sealed class PolicyEvaluationTraceSnapshotTests
|
||||
{
|
||||
private static readonly DateTimeOffset FrozenTime = DateTimeOffset.Parse("2025-12-24T12:00:00Z");
|
||||
|
||||
[Fact]
|
||||
public void SnapshotDirectory_ResolvesToSourceControlledSnapshotsFolder()
|
||||
{
|
||||
var snapshotsDirectory = SnapshotAssert.ResolveDefaultSnapshotsDirectory();
|
||||
|
||||
Directory.Exists(snapshotsDirectory).Should().BeTrue();
|
||||
Path.GetFileName(snapshotsDirectory).Should().Be("Snapshots");
|
||||
File.Exists(Path.Combine(snapshotsDirectory, "PolicyEvaluationTraceSnapshotTests.cs"))
|
||||
.Should()
|
||||
.BeTrue("snapshot root should resolve to the source-controlled Snapshots folder");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a simple evaluation trace produces stable structure.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"traceId": "TRACE-2025-004",
|
||||
"policyId": "POL-PROFILE-001",
|
||||
"policyName": "Profile-Based Policy",
|
||||
"evaluationContext": {
|
||||
"digestEvaluated": "sha256:profile123",
|
||||
"tenantId": "TENANT-003",
|
||||
"environment": "production",
|
||||
"exposure": "internet"
|
||||
},
|
||||
"startedAt": "2025-12-24T12:00:00+00:00",
|
||||
"completedAt": "2025-12-24T12:00:00.055+00:00",
|
||||
"durationMs": 55,
|
||||
"outcome": "Pass",
|
||||
"steps": [
|
||||
{
|
||||
"stepNumber": 1,
|
||||
"ruleName": "profile_severity",
|
||||
"priority": 100,
|
||||
"phase": 0,
|
||||
"condition": "profile.severity.enabled",
|
||||
"conditionResult": true,
|
||||
"action": null,
|
||||
"explanation": "Applying severity profile adjustments",
|
||||
"durationMs": 18,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": {
|
||||
"profileName": "severity",
|
||||
"adjustments": [
|
||||
{
|
||||
"finding": "CVE-2024-0001",
|
||||
"originalSeverity": "High",
|
||||
"adjustedSeverity": "Critical",
|
||||
"reason": "GHSA source weight \u002B0.5, internet exposure \u002B0.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
"escalationDetail": null
|
||||
},
|
||||
{
|
||||
"stepNumber": 2,
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"phase": 3,
|
||||
"condition": "severity.normalized \u003E= \u0022Critical\u0022",
|
||||
"conditionResult": false,
|
||||
"action": null,
|
||||
"explanation": "Post-profile: no critical vulnerabilities (VEX override applied)",
|
||||
"durationMs": 10,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
}
|
||||
],
|
||||
"finalStatus": "allowed",
|
||||
"matchedRuleCount": 1,
|
||||
"totalRuleCount": 2
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"traceId": "TRACE-2025-005",
|
||||
"policyId": "POL-ESCALATE-001",
|
||||
"policyName": "Escalation Policy",
|
||||
"evaluationContext": {
|
||||
"digestEvaluated": "sha256:escalate123",
|
||||
"tenantId": "TENANT-001",
|
||||
"environment": "production",
|
||||
"exposure": "internet"
|
||||
},
|
||||
"startedAt": "2025-12-24T12:00:00+00:00",
|
||||
"completedAt": "2025-12-24T12:00:00.045+00:00",
|
||||
"durationMs": 45,
|
||||
"outcome": "Fail",
|
||||
"steps": [
|
||||
{
|
||||
"stepNumber": 1,
|
||||
"ruleName": "escalate_high_internet",
|
||||
"priority": 4,
|
||||
"phase": 2,
|
||||
"condition": "severity.normalized == \u0022High\u0022 and env.exposure == \u0022internet\u0022",
|
||||
"conditionResult": true,
|
||||
"action": "escalate to severity_band(\u0022Critical\u0022)",
|
||||
"explanation": "High severity on internet-exposed asset escalated to Critical",
|
||||
"durationMs": 15,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": {
|
||||
"finding": "CVE-2024-0001",
|
||||
"originalSeverity": "High",
|
||||
"escalatedSeverity": "Critical",
|
||||
"reason": "Internet exposure triggers escalation per policy rule"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stepNumber": 2,
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"phase": 3,
|
||||
"condition": "severity.normalized \u003E= \u0022Critical\u0022",
|
||||
"conditionResult": true,
|
||||
"action": "status := \u0022blocked\u0022",
|
||||
"explanation": "Critical severity (post-escalation) triggers block",
|
||||
"durationMs": 10,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
}
|
||||
],
|
||||
"finalStatus": "blocked",
|
||||
"matchedRuleCount": 2,
|
||||
"totalRuleCount": 2
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"traceId": "TRACE-2025-001",
|
||||
"policyId": "POL-PROD-001",
|
||||
"policyName": "Production Baseline Policy",
|
||||
"evaluationContext": {
|
||||
"digestEvaluated": "sha256:abc123def456",
|
||||
"tenantId": "TENANT-001",
|
||||
"environment": "production",
|
||||
"exposure": "internet"
|
||||
},
|
||||
"startedAt": "2025-12-24T12:00:00+00:00",
|
||||
"completedAt": "2025-12-24T12:00:00.042+00:00",
|
||||
"durationMs": 42,
|
||||
"outcome": "Pass",
|
||||
"steps": [
|
||||
{
|
||||
"stepNumber": 1,
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"phase": 3,
|
||||
"condition": "severity.normalized \u003E= \u0022Critical\u0022",
|
||||
"conditionResult": false,
|
||||
"action": null,
|
||||
"explanation": "No critical vulnerabilities found in scan",
|
||||
"durationMs": 15,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
},
|
||||
{
|
||||
"stepNumber": 2,
|
||||
"ruleName": "allow_low_severity",
|
||||
"priority": 1,
|
||||
"phase": 3,
|
||||
"condition": "severity.normalized \u003C= \u0022Low\u0022",
|
||||
"conditionResult": true,
|
||||
"action": "status := \u0022allowed\u0022",
|
||||
"explanation": "All findings are Low severity or below",
|
||||
"durationMs": 12,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
}
|
||||
],
|
||||
"finalStatus": "allowed",
|
||||
"matchedRuleCount": 1,
|
||||
"totalRuleCount": 2
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-007",
|
||||
"policyId": "POL-SCORE-001",
|
||||
"policyName": "EWS Score-Based Policy",
|
||||
"policyVersion": "1.0.0",
|
||||
"tenantId": "TENANT-001",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:score123",
|
||||
"outcome": 2,
|
||||
"rulesMatched": 2,
|
||||
"rulesTotal": 5,
|
||||
"violations": [
|
||||
{
|
||||
"ruleName": "block_act_now",
|
||||
"severity": "critical",
|
||||
"message": "Score 92 in ActNow bucket requires immediate action",
|
||||
"vulnerabilityId": "CVE-2024-0010",
|
||||
"packagePurl": "pkg:npm/critical-pkg@1.0.0",
|
||||
"remediation": "Upgrade to patched version immediately"
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "block_act_now",
|
||||
"priority": 10,
|
||||
"status": 2,
|
||||
"reason": "score.is_act_now evaluated true (score=92)"
|
||||
},
|
||||
{
|
||||
"ruleName": "score_threshold_80",
|
||||
"priority": 8,
|
||||
"status": 1,
|
||||
"reason": "score \u003E= 80 threshold exceeded"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": {
|
||||
"findingId": "FINDING-CVE-2024-0010",
|
||||
"score": 92,
|
||||
"bucket": "ActNow",
|
||||
"inputs": {
|
||||
"reachability": 0.95,
|
||||
"runtime": 0.8,
|
||||
"backport": 0.1,
|
||||
"exploit": 0.9,
|
||||
"sourceTrust": 0.7,
|
||||
"mitigation": 0.05
|
||||
},
|
||||
"flags": [
|
||||
"live-signal",
|
||||
"public-exploit"
|
||||
],
|
||||
"explanations": [
|
||||
"High reachability (0.95): function is in hot code path",
|
||||
"Active exploit in the wild detected",
|
||||
"No mitigation available"
|
||||
],
|
||||
"calculatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"policyDigest": "sha256:ews-policy-v1"
|
||||
},
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 78,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24",
|
||||
"ghsa": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:score-policy-001"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-009",
|
||||
"policyId": "POL-SCORE-002",
|
||||
"policyName": "KEV-Aware Score Policy",
|
||||
"policyVersion": "1.0.0",
|
||||
"tenantId": "TENANT-002",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:kev-score",
|
||||
"outcome": 2,
|
||||
"rulesMatched": 2,
|
||||
"rulesTotal": 4,
|
||||
"violations": [
|
||||
{
|
||||
"ruleName": "block_kev_flagged",
|
||||
"severity": "critical",
|
||||
"message": "KEV-listed vulnerability must be remediated immediately",
|
||||
"vulnerabilityId": "CVE-2024-0030",
|
||||
"packagePurl": "pkg:npm/vulnerable-pkg@1.0.0",
|
||||
"remediation": "CISA KEV deadline: 2025-01-15"
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "block_kev_flagged",
|
||||
"priority": 15,
|
||||
"status": 2,
|
||||
"reason": "score.has_flag(\u0022kev\u0022) evaluated true"
|
||||
},
|
||||
{
|
||||
"ruleName": "escalate_act_now",
|
||||
"priority": 10,
|
||||
"status": 1,
|
||||
"reason": "score.is_act_now with KEV flag"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": {
|
||||
"findingId": "FINDING-CVE-2024-0030",
|
||||
"score": 98,
|
||||
"bucket": "ActNow",
|
||||
"inputs": {
|
||||
"reachability": 0.7,
|
||||
"runtime": 0.9,
|
||||
"backport": 0,
|
||||
"exploit": 1,
|
||||
"sourceTrust": 0.85,
|
||||
"mitigation": 0
|
||||
},
|
||||
"flags": [
|
||||
"kev",
|
||||
"public-exploit",
|
||||
"weaponized"
|
||||
],
|
||||
"explanations": [
|
||||
"CISA KEV listed: actively exploited in the wild",
|
||||
"Exploit complexity: Low",
|
||||
"No backport available",
|
||||
"No mitigation factors apply"
|
||||
],
|
||||
"calculatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"policyDigest": "sha256:kev-policy-v1"
|
||||
},
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 56,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24",
|
||||
"kev": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:kev-policy-001"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-010",
|
||||
"policyId": "POL-SCORE-001",
|
||||
"policyName": "EWS Score-Based Policy",
|
||||
"policyVersion": "1.0.0",
|
||||
"tenantId": "TENANT-001",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:low-score",
|
||||
"outcome": 0,
|
||||
"rulesMatched": 1,
|
||||
"rulesTotal": 5,
|
||||
"violations": [],
|
||||
"warnings": [],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "allow_low_score",
|
||||
"priority": 1,
|
||||
"status": 1,
|
||||
"reason": "score \u003C 40 - acceptable risk level"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": {
|
||||
"findingId": "FINDING-CVE-2024-0040",
|
||||
"score": 25,
|
||||
"bucket": "Watchlist",
|
||||
"inputs": {
|
||||
"reachability": 0.1,
|
||||
"runtime": 0.2,
|
||||
"backport": 0.9,
|
||||
"exploit": 0.15,
|
||||
"sourceTrust": 0.95,
|
||||
"mitigation": 0.8
|
||||
},
|
||||
"flags": [],
|
||||
"explanations": [
|
||||
"Low reachability (0.1): function not in execution path",
|
||||
"Backport available (0.9)",
|
||||
"Strong mitigation factors (0.8)"
|
||||
],
|
||||
"calculatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"policyDigest": "sha256:ews-policy-v1"
|
||||
},
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 32,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:score-policy-001"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-008",
|
||||
"policyId": "POL-SCORE-001",
|
||||
"policyName": "EWS Score-Based Policy",
|
||||
"policyVersion": "1.0.0",
|
||||
"tenantId": "TENANT-001",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:score-violation",
|
||||
"outcome": 2,
|
||||
"rulesMatched": 1,
|
||||
"rulesTotal": 3,
|
||||
"violations": [
|
||||
{
|
||||
"ruleName": "block_high_exploit_reachable",
|
||||
"severity": "high",
|
||||
"message": "Reachable vulnerability with high exploit score blocked",
|
||||
"vulnerabilityId": "CVE-2024-0020",
|
||||
"packagePurl": "pkg:maven/org.example/lib@2.0.0",
|
||||
"remediation": "Apply patch or configure WAF rules"
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "block_high_exploit_reachable",
|
||||
"priority": 7,
|
||||
"status": 2,
|
||||
"reason": "score.rch \u003E 0.8 and score.xpl \u003E 0.7 condition met"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": {
|
||||
"findingId": "FINDING-CVE-2024-0020",
|
||||
"score": 75,
|
||||
"bucket": "ScheduleNext",
|
||||
"inputs": {
|
||||
"reachability": 0.85,
|
||||
"runtime": 0.6,
|
||||
"backport": 0.3,
|
||||
"exploit": 0.75,
|
||||
"sourceTrust": 0.8,
|
||||
"mitigation": 0.2
|
||||
},
|
||||
"flags": [],
|
||||
"explanations": [
|
||||
"High reachability (0.85): code path confirmed reachable",
|
||||
"Exploit code available (0.75)"
|
||||
],
|
||||
"calculatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"policyDigest": "sha256:ews-policy-v1"
|
||||
},
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 45,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:score-policy-001"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-003",
|
||||
"policyId": "POL-STRICT-001",
|
||||
"policyName": "Strict Unknown Budget Policy",
|
||||
"policyVersion": "2.0.0",
|
||||
"tenantId": "TENANT-002",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:xyz789",
|
||||
"outcome": 2,
|
||||
"rulesMatched": 1,
|
||||
"rulesTotal": 3,
|
||||
"violations": [
|
||||
{
|
||||
"ruleName": "unknowns_budget",
|
||||
"severity": "high",
|
||||
"message": "Unknowns budget exceeded: 5 critical unknowns (max: 0)",
|
||||
"vulnerabilityId": null,
|
||||
"packagePurl": null,
|
||||
"remediation": "Resolve unknown packages or request VEX statements"
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "unknowns_budget",
|
||||
"priority": 10,
|
||||
"status": 2,
|
||||
"reason": "Critical unknowns (5) exceeds budget (0)"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": {
|
||||
"withinBudget": false,
|
||||
"criticalCount": 5,
|
||||
"highCount": 12,
|
||||
"mediumCount": 45,
|
||||
"lowCount": 120,
|
||||
"totalCount": 182,
|
||||
"action": "Block"
|
||||
},
|
||||
"vexMergeTrace": null,
|
||||
"scoreResult": null,
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 89,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:policy456"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"verdictId": "VERDICT-2025-004",
|
||||
"policyId": "POL-PROD-001",
|
||||
"policyName": "Production Baseline Policy",
|
||||
"policyVersion": "1.2.3",
|
||||
"tenantId": "TENANT-001",
|
||||
"evaluatedAt": "2025-12-24T12:00:00+00:00",
|
||||
"digestEvaluated": "sha256:abc123def456",
|
||||
"outcome": 0,
|
||||
"rulesMatched": 2,
|
||||
"rulesTotal": 5,
|
||||
"violations": [],
|
||||
"warnings": [],
|
||||
"matchedRules": [
|
||||
{
|
||||
"ruleName": "require_vex_justification",
|
||||
"priority": 3,
|
||||
"status": 1,
|
||||
"reason": "VEX statement accepted with strong justification"
|
||||
}
|
||||
],
|
||||
"unknownsBudgetResult": null,
|
||||
"vexMergeTrace": {
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"winningSource": "vendor",
|
||||
"winningStatus": "not_affected",
|
||||
"winningJustification": "component_not_present",
|
||||
"conflictsResolved": 1,
|
||||
"sourcesConsidered": 3,
|
||||
"resolutionReason": "TrustWeight"
|
||||
},
|
||||
"scoreResult": null,
|
||||
"metadata": {
|
||||
"evaluationDurationMs": 55,
|
||||
"feedVersions": {
|
||||
"nvd": "2025-12-24",
|
||||
"ghsa": "2025-12-24"
|
||||
},
|
||||
"policyChecksum": "sha256:policy123"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"traceId": "TRACE-2025-003",
|
||||
"policyId": "POL-VEX-001",
|
||||
"policyName": "VEX-Aware Policy",
|
||||
"evaluationContext": {
|
||||
"digestEvaluated": "sha256:vex123",
|
||||
"tenantId": "TENANT-001",
|
||||
"environment": "production",
|
||||
"exposure": "internet"
|
||||
},
|
||||
"startedAt": "2025-12-24T12:00:00+00:00",
|
||||
"completedAt": "2025-12-24T12:00:00.067+00:00",
|
||||
"durationMs": 67,
|
||||
"outcome": "Pass",
|
||||
"steps": [
|
||||
{
|
||||
"stepNumber": 1,
|
||||
"ruleName": "vex_merge_resolution",
|
||||
"priority": 10,
|
||||
"phase": 1,
|
||||
"condition": "vex.statements.count \u003E 1",
|
||||
"conditionResult": true,
|
||||
"action": null,
|
||||
"explanation": "Multiple VEX statements found; merging via K4 lattice",
|
||||
"durationMs": 20,
|
||||
"vexMergeDetail": {
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"statementCount": 3,
|
||||
"sources": [
|
||||
{
|
||||
"source": "vendor",
|
||||
"status": "not_affected",
|
||||
"trust": 1.0
|
||||
},
|
||||
{
|
||||
"source": "maintainer",
|
||||
"status": "affected",
|
||||
"trust": 0.9
|
||||
},
|
||||
{
|
||||
"source": "scanner",
|
||||
"status": "unknown",
|
||||
"trust": 0.5
|
||||
}
|
||||
],
|
||||
"winningSource": "vendor",
|
||||
"winningStatus": "not_affected",
|
||||
"resolutionReason": "TrustWeight",
|
||||
"conflictsResolved": 2
|
||||
},
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
},
|
||||
{
|
||||
"stepNumber": 2,
|
||||
"ruleName": "require_vex_justification",
|
||||
"priority": 3,
|
||||
"phase": 3,
|
||||
"condition": "vex.justification in [\u0022component_not_present\u0022,\u0022vulnerable_code_not_present\u0022]",
|
||||
"conditionResult": true,
|
||||
"action": "status := vex.status",
|
||||
"explanation": "VEX justification accepted: component_not_present",
|
||||
"durationMs": 12,
|
||||
"vexMergeDetail": null,
|
||||
"profileApplicationDetail": null,
|
||||
"escalationDetail": null
|
||||
}
|
||||
],
|
||||
"finalStatus": "allowed",
|
||||
"matchedRuleCount": 2,
|
||||
"totalRuleCount": 2
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
<ProjectReference Include="../../StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.DeltaVerdict/StellaOps.DeltaVerdict.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Provcache/StellaOps.Provcache.csproj" />
|
||||
<ProjectReference Include="../../../Excititor/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
<ProjectReference Include="../../../Concelier/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../Scanner/__Libraries/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj" />
|
||||
|
||||
@@ -149,6 +149,7 @@ public sealed class TenantIsolationTests
|
||||
// Arrange
|
||||
var filter = new TenantContextEndpointFilter();
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSingleton<ITenantContextAccessor>(_accessor);
|
||||
var sp = services.BuildServiceProvider();
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ public sealed class EvidenceWeightedScoreModelTests
|
||||
var policy = new ScorePolicy
|
||||
{
|
||||
PolicyVersion = "score.v1",
|
||||
PolicyId = "test-policy.invalid-weights",
|
||||
WeightsBps = new WeightsBps
|
||||
{
|
||||
BaseSeverity = 1000,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Scoring;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ScorePolicyLoaderContractTests
|
||||
{
|
||||
private readonly ScorePolicyLoader _loader = new();
|
||||
|
||||
[Fact]
|
||||
public void LoadFromYaml_MissingPolicyId_FailsValidation()
|
||||
{
|
||||
var yaml = """
|
||||
policyVersion: score.v1
|
||||
weightsBps:
|
||||
baseSeverity: 2500
|
||||
reachability: 2500
|
||||
evidence: 2500
|
||||
provenance: 2500
|
||||
""";
|
||||
|
||||
var act = () => _loader.LoadFromYaml(yaml, "missing-policy-id");
|
||||
|
||||
act.Should()
|
||||
.Throw<ScorePolicyLoadException>()
|
||||
.WithMessage("*Missing required field 'policyId'*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromYaml_ValidPolicyWithPolicyId_LoadsSuccessfully()
|
||||
{
|
||||
var yaml = """
|
||||
policyVersion: score.v1
|
||||
policyId: tenant-default-score
|
||||
scoringProfile: advanced
|
||||
weightsBps:
|
||||
baseSeverity: 2500
|
||||
reachability: 2500
|
||||
evidence: 2500
|
||||
provenance: 2500
|
||||
reachability:
|
||||
unreachableScore: 0
|
||||
evidence:
|
||||
points:
|
||||
runtime: 60
|
||||
dast: 30
|
||||
sast: 20
|
||||
sca: 10
|
||||
provenance:
|
||||
levels:
|
||||
unsigned: 0
|
||||
signed: 30
|
||||
signedWithSbom: 60
|
||||
signedWithSbomAndAttestations: 80
|
||||
reproducible: 100
|
||||
""";
|
||||
|
||||
var policy = _loader.LoadFromYaml(yaml, "valid-policy");
|
||||
|
||||
policy.PolicyId.Should().Be("tenant-default-score");
|
||||
policy.PolicyVersion.Should().Be("score.v1");
|
||||
policy.ValidateWeights().Should().BeTrue();
|
||||
policy.Reachability.Should().NotBeNull();
|
||||
policy.Evidence.Should().NotBeNull();
|
||||
policy.Provenance.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmbeddedSchemaAndSourceSchema_RemainInParity()
|
||||
{
|
||||
var embeddedSchema = JsonDocument.Parse(ScorePolicySchemaResource.ReadSchemaJson());
|
||||
var sourceSchema = JsonDocument.Parse(File.ReadAllText(FindSourceSchemaPath()));
|
||||
|
||||
var embeddedNormalized = JsonSerializer.Serialize(embeddedSchema.RootElement);
|
||||
var sourceNormalized = JsonSerializer.Serialize(sourceSchema.RootElement);
|
||||
|
||||
embeddedNormalized.Should().Be(sourceNormalized,
|
||||
"embedded schema and source schema must stay identical");
|
||||
}
|
||||
|
||||
private static string FindSourceSchemaPath()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
var candidate = Path.Combine(
|
||||
directory.FullName,
|
||||
"src",
|
||||
"Policy",
|
||||
"__Libraries",
|
||||
"StellaOps.Policy",
|
||||
"Schemas",
|
||||
"score-policy.v1.schema.json");
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to locate score-policy.v1.schema.json from test base directory.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user