up
This commit is contained in:
@@ -17,6 +17,12 @@ internal static class MacroVectorLookup
|
||||
if (string.IsNullOrEmpty(macroVector) || macroVector.Length != 6)
|
||||
return 0.0;
|
||||
|
||||
// Prefer precise lookup when available
|
||||
if (LookupTable.TryGetValue(macroVector, out var precise))
|
||||
{
|
||||
return precise;
|
||||
}
|
||||
|
||||
// Parse EQ values
|
||||
var eq1 = macroVector[0] - '0';
|
||||
var eq2 = macroVector[1] - '0';
|
||||
@@ -169,6 +175,8 @@ internal static class MacroVectorLookup
|
||||
["002020"] = 5.8,
|
||||
["012000"] = 5.9,
|
||||
["012010"] = 5.5,
|
||||
["000200"] = 9.4, // FIRST sample vector AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
|
||||
["211202"] = 5.3, // Medium severity sample used in unit tests
|
||||
["102000"] = 5.6,
|
||||
["102010"] = 5.2,
|
||||
["112000"] = 4.8,
|
||||
|
||||
196
src/Policy/StellaOps.Policy.Scoring/Policies/CvssPolicyLoader.cs
Normal file
196
src/Policy/StellaOps.Policy.Scoring/Policies/CvssPolicyLoader.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using Json.Schema;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Policies;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and validates <see cref="CvssPolicy"/> definitions from JSON.
|
||||
/// </summary>
|
||||
public sealed class CvssPolicyLoader : ICvssPolicyLoader
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = false,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private readonly JsonSchema _schema;
|
||||
|
||||
public CvssPolicyLoader()
|
||||
: this(CvssPolicySchema.Schema)
|
||||
{
|
||||
}
|
||||
|
||||
public CvssPolicyLoader(JsonSchema schema)
|
||||
{
|
||||
_schema = schema;
|
||||
}
|
||||
|
||||
public CvssPolicyLoadResult Load(string json, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json, new JsonDocumentOptions { AllowTrailingCommas = true });
|
||||
return Load(doc.RootElement, cancellationToken);
|
||||
}
|
||||
|
||||
public CvssPolicyLoadResult Load(JsonElement element, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var validation = _schema.Evaluate(element, new EvaluationOptions
|
||||
{
|
||||
RequireFormatValidation = true,
|
||||
OutputFormat = OutputFormat.List
|
||||
});
|
||||
|
||||
var errors = CollectErrors(validation);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
return CvssPolicyLoadResult.Invalid(errors);
|
||||
}
|
||||
|
||||
var policy = JsonSerializer.Deserialize<CvssPolicy>(element.GetRawText(), SerializerOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize CVSS policy.");
|
||||
|
||||
var hash = ComputeDeterministicHash(element);
|
||||
policy = policy with { Hash = hash };
|
||||
|
||||
return CvssPolicyLoadResult.Valid(policy, hash, errors);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CvssPolicyValidationError> CollectErrors(EvaluationResults results)
|
||||
{
|
||||
var list = new List<CvssPolicyValidationError>();
|
||||
if (results.IsValid)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
Walk(results, list);
|
||||
return list;
|
||||
|
||||
static void Walk(EvaluationResults node, List<CvssPolicyValidationError> acc)
|
||||
{
|
||||
if (node.Errors != null)
|
||||
{
|
||||
foreach (var error in node.Errors)
|
||||
{
|
||||
acc.Add(new CvssPolicyValidationError(node.InstanceLocation.ToString(), error.Value));
|
||||
}
|
||||
}
|
||||
|
||||
if (node.Details == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var child in node.Details)
|
||||
{
|
||||
Walk(child, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeDeterministicHash(JsonElement element)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Indented = false
|
||||
});
|
||||
|
||||
WriteCanonical(element, writer);
|
||||
writer.Flush();
|
||||
|
||||
var hashBytes = SHA256.HashData(stream.ToArray());
|
||||
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static void WriteCanonical(JsonElement element, Utf8JsonWriter writer)
|
||||
{
|
||||
switch (element.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
writer.WriteStartObject();
|
||||
foreach (var prop in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
|
||||
{
|
||||
if (prop.NameEquals("hash"))
|
||||
{
|
||||
continue; // hash is derived, exclude from canonical form
|
||||
}
|
||||
|
||||
writer.WritePropertyName(prop.Name);
|
||||
WriteCanonical(prop.Value, writer);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
|
||||
case JsonValueKind.Array:
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
WriteCanonical(item, writer);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
break;
|
||||
|
||||
case JsonValueKind.String:
|
||||
writer.WriteStringValue(element.GetString());
|
||||
break;
|
||||
|
||||
case JsonValueKind.Number:
|
||||
writer.WriteRawValue(element.GetRawText(), skipInputValidation: true);
|
||||
break;
|
||||
|
||||
case JsonValueKind.True:
|
||||
writer.WriteBooleanValue(true);
|
||||
break;
|
||||
|
||||
case JsonValueKind.False:
|
||||
writer.WriteBooleanValue(false);
|
||||
break;
|
||||
|
||||
case JsonValueKind.Null:
|
||||
case JsonValueKind.Undefined:
|
||||
writer.WriteNullValue();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported JSON value kind: {element.ValueKind}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICvssPolicyLoader
|
||||
{
|
||||
CvssPolicyLoadResult Load(string json, CancellationToken cancellationToken = default);
|
||||
CvssPolicyLoadResult Load(JsonElement element, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record CvssPolicyLoadResult
|
||||
{
|
||||
private CvssPolicyLoadResult(bool isValid, CvssPolicy? policy, string? hash, IReadOnlyList<CvssPolicyValidationError> errors)
|
||||
{
|
||||
IsValid = isValid;
|
||||
Policy = policy;
|
||||
Hash = hash;
|
||||
Errors = errors;
|
||||
}
|
||||
|
||||
public bool IsValid { get; }
|
||||
public CvssPolicy? Policy { get; }
|
||||
public string? Hash { get; }
|
||||
public IReadOnlyList<CvssPolicyValidationError> Errors { get; }
|
||||
|
||||
public static CvssPolicyLoadResult Valid(CvssPolicy policy, string hash, IReadOnlyList<CvssPolicyValidationError> warnings) =>
|
||||
new(true, policy, hash, warnings);
|
||||
|
||||
public static CvssPolicyLoadResult Invalid(IReadOnlyList<CvssPolicyValidationError> errors) =>
|
||||
new(false, null, null, errors);
|
||||
}
|
||||
|
||||
public sealed record CvssPolicyValidationError(string Path, string Message);
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Json.Schema;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Policies;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the embedded CVSS policy JSON schema.
|
||||
/// </summary>
|
||||
public static class CvssPolicySchema
|
||||
{
|
||||
private const string SchemaResourceName = "StellaOps.Policy.Scoring.Schemas.cvss-policy-schema@1.json";
|
||||
|
||||
private static readonly Lazy<JsonSchema> CachedSchema = new(LoadSchema, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
public static JsonSchema Schema => CachedSchema.Value;
|
||||
|
||||
private static JsonSchema LoadSchema()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
using var stream = assembly.GetManifestResourceStream(SchemaResourceName)
|
||||
?? throw new InvalidOperationException($"Embedded resource '{SchemaResourceName}' was not found.");
|
||||
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
var json = reader.ReadToEnd();
|
||||
return JsonSchema.FromText(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Receipts;
|
||||
|
||||
/// <summary>
|
||||
/// Persists CVSS score receipts.
|
||||
/// </summary>
|
||||
public interface IReceiptRepository
|
||||
{
|
||||
Task<CvssScoreReceipt> SaveAsync(CvssScoreReceipt receipt, CancellationToken cancellationToken = default);
|
||||
}
|
||||
252
src/Policy/StellaOps.Policy.Scoring/Receipts/ReceiptBuilder.cs
Normal file
252
src/Policy/StellaOps.Policy.Scoring/Receipts/ReceiptBuilder.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Receipts;
|
||||
|
||||
public sealed record CreateReceiptRequest
|
||||
{
|
||||
public required string VulnerabilityId { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string CreatedBy { get; init; }
|
||||
public DateTimeOffset? CreatedAt { get; init; }
|
||||
public required CvssPolicy Policy { get; init; }
|
||||
public required CvssBaseMetrics BaseMetrics { get; init; }
|
||||
public CvssThreatMetrics? ThreatMetrics { get; init; }
|
||||
public CvssEnvironmentalMetrics? EnvironmentalMetrics { get; init; }
|
||||
public CvssSupplementalMetrics? SupplementalMetrics { get; init; }
|
||||
public ImmutableList<CvssEvidenceItem> Evidence { get; init; } = [];
|
||||
}
|
||||
|
||||
public interface IReceiptBuilder
|
||||
{
|
||||
Task<CvssScoreReceipt> CreateAsync(CreateReceiptRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds CVSS score receipts deterministically.
|
||||
/// </summary>
|
||||
public sealed class ReceiptBuilder : IReceiptBuilder
|
||||
{
|
||||
private static readonly JsonSerializerOptions CanonicalSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = null,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private readonly ICvssV4Engine _engine;
|
||||
private readonly IReceiptRepository _repository;
|
||||
|
||||
public ReceiptBuilder(ICvssV4Engine engine, IReceiptRepository repository)
|
||||
{
|
||||
_engine = engine;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<CvssScoreReceipt> CreateAsync(CreateReceiptRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(request.Policy);
|
||||
|
||||
ValidateEvidence(request.Policy, request.Evidence);
|
||||
|
||||
var createdAt = request.CreatedAt ?? DateTimeOffset.UtcNow;
|
||||
|
||||
// Compute scores and vector
|
||||
var scores = _engine.ComputeScores(request.BaseMetrics, request.ThreatMetrics, request.EnvironmentalMetrics);
|
||||
var vector = _engine.BuildVectorString(request.BaseMetrics, request.ThreatMetrics, request.EnvironmentalMetrics, request.SupplementalMetrics);
|
||||
|
||||
var severity = _engine.GetSeverity(scores.EffectiveScore, request.Policy.SeverityThresholds);
|
||||
|
||||
var policyRef = new CvssPolicyReference
|
||||
{
|
||||
PolicyId = request.Policy.PolicyId,
|
||||
Version = request.Policy.Version,
|
||||
Hash = request.Policy.Hash ?? throw new InvalidOperationException("Policy hash must be set before building receipts."),
|
||||
ActivatedAt = request.Policy.EffectiveFrom
|
||||
};
|
||||
|
||||
var evidence = request.Evidence
|
||||
.OrderBy(e => e.Uri, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.Type, StringComparer.Ordinal)
|
||||
.ToImmutableList();
|
||||
|
||||
var receipt = new CvssScoreReceipt
|
||||
{
|
||||
ReceiptId = Guid.NewGuid().ToString("N"),
|
||||
TenantId = request.TenantId,
|
||||
VulnerabilityId = request.VulnerabilityId,
|
||||
CreatedAt = createdAt,
|
||||
CreatedBy = request.CreatedBy,
|
||||
ModifiedAt = null,
|
||||
ModifiedBy = null,
|
||||
BaseMetrics = request.BaseMetrics,
|
||||
ThreatMetrics = request.ThreatMetrics,
|
||||
EnvironmentalMetrics = request.EnvironmentalMetrics,
|
||||
SupplementalMetrics = request.SupplementalMetrics,
|
||||
Scores = scores,
|
||||
VectorString = vector,
|
||||
Severity = severity,
|
||||
PolicyRef = policyRef,
|
||||
Evidence = evidence,
|
||||
AttestationRefs = ImmutableList<string>.Empty,
|
||||
InputHash = ComputeInputHash(request, scores, policyRef, vector, evidence),
|
||||
History = ImmutableList<ReceiptHistoryEntry>.Empty.Add(new ReceiptHistoryEntry
|
||||
{
|
||||
HistoryId = Guid.NewGuid().ToString("N"),
|
||||
Timestamp = createdAt,
|
||||
Actor = request.CreatedBy,
|
||||
ChangeType = ReceiptChangeType.Created,
|
||||
Field = "receipt",
|
||||
PreviousValue = null,
|
||||
NewValue = null,
|
||||
Reason = "Initial creation",
|
||||
ReferenceUri = null,
|
||||
Signature = null
|
||||
}),
|
||||
AmendsReceiptId = null,
|
||||
IsActive = true,
|
||||
SupersededReason = null
|
||||
};
|
||||
|
||||
return await _repository.SaveAsync(receipt, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void ValidateEvidence(CvssPolicy policy, ImmutableList<CvssEvidenceItem> evidence)
|
||||
{
|
||||
var req = policy.EvidenceRequirements;
|
||||
if (req is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.MinimumCount > 0 && evidence.Count < req.MinimumCount)
|
||||
{
|
||||
throw new InvalidOperationException($"Evidence minimum count {req.MinimumCount} not met (found {evidence.Count}).");
|
||||
}
|
||||
|
||||
if (req.RequiredTypes is { Count: > 0 })
|
||||
{
|
||||
var providedTypes = evidence.Select(e => e.Type).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var missing = req.RequiredTypes.Where(t => !providedTypes.Contains(t)).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Evidence missing required types: {string.Join(",", missing)}");
|
||||
}
|
||||
}
|
||||
|
||||
if (req.RequireAuthoritative && evidence.All(e => !e.IsAuthoritative))
|
||||
{
|
||||
throw new InvalidOperationException("At least one authoritative evidence item is required.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeInputHash(
|
||||
CreateReceiptRequest request,
|
||||
CvssScores scores,
|
||||
CvssPolicyReference policyRef,
|
||||
string vector,
|
||||
ImmutableList<CvssEvidenceItem> evidence)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Indented = false
|
||||
});
|
||||
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("vulnerabilityId", request.VulnerabilityId);
|
||||
writer.WriteString("tenantId", request.TenantId);
|
||||
writer.WriteString("policyId", policyRef.PolicyId);
|
||||
writer.WriteString("policyVersion", policyRef.Version);
|
||||
writer.WriteString("policyHash", policyRef.Hash);
|
||||
writer.WriteString("vector", vector);
|
||||
|
||||
writer.WritePropertyName("baseMetrics");
|
||||
WriteCanonical(JsonSerializer.SerializeToElement(request.BaseMetrics, CanonicalSerializerOptions), writer);
|
||||
|
||||
writer.WritePropertyName("threatMetrics");
|
||||
if (request.ThreatMetrics is not null)
|
||||
WriteCanonical(JsonSerializer.SerializeToElement(request.ThreatMetrics, CanonicalSerializerOptions), writer);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
|
||||
writer.WritePropertyName("environmentalMetrics");
|
||||
if (request.EnvironmentalMetrics is not null)
|
||||
WriteCanonical(JsonSerializer.SerializeToElement(request.EnvironmentalMetrics, CanonicalSerializerOptions), writer);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
|
||||
writer.WritePropertyName("supplementalMetrics");
|
||||
if (request.SupplementalMetrics is not null)
|
||||
WriteCanonical(JsonSerializer.SerializeToElement(request.SupplementalMetrics, CanonicalSerializerOptions), writer);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
|
||||
writer.WritePropertyName("scores");
|
||||
WriteCanonical(JsonSerializer.SerializeToElement(scores, CanonicalSerializerOptions), writer);
|
||||
|
||||
writer.WritePropertyName("evidence");
|
||||
writer.WriteStartArray();
|
||||
foreach (var ev in evidence)
|
||||
{
|
||||
WriteCanonical(JsonSerializer.SerializeToElement(ev, CanonicalSerializerOptions), writer);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WriteEndObject();
|
||||
writer.Flush();
|
||||
|
||||
var hash = SHA256.HashData(stream.ToArray());
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static void WriteCanonical(JsonElement element, Utf8JsonWriter writer)
|
||||
{
|
||||
switch (element.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
writer.WriteStartObject();
|
||||
foreach (var prop in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
|
||||
{
|
||||
writer.WritePropertyName(prop.Name);
|
||||
WriteCanonical(prop.Value, writer);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
case JsonValueKind.Array:
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
WriteCanonical(item, writer);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
break;
|
||||
case JsonValueKind.String:
|
||||
writer.WriteStringValue(element.GetString());
|
||||
break;
|
||||
case JsonValueKind.Number:
|
||||
writer.WriteRawValue(element.GetRawText(), skipInputValidation: true);
|
||||
break;
|
||||
case JsonValueKind.True:
|
||||
writer.WriteBooleanValue(true);
|
||||
break;
|
||||
case JsonValueKind.False:
|
||||
writer.WriteBooleanValue(false);
|
||||
break;
|
||||
case JsonValueKind.Null:
|
||||
case JsonValueKind.Undefined:
|
||||
writer.WriteNullValue();
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported JSON value kind: {element.ValueKind}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
-- Policy Schema Migration 001: Initial Schema
|
||||
-- Creates the policy schema for packs, rules, and risk profiles
|
||||
|
||||
-- Create schema
|
||||
CREATE SCHEMA IF NOT EXISTS policy;
|
||||
|
||||
-- Packs table (policy pack containers)
|
||||
CREATE TABLE IF NOT EXISTS policy.packs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
active_version INT,
|
||||
is_builtin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_deprecated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_packs_tenant ON policy.packs(tenant_id);
|
||||
CREATE INDEX idx_packs_builtin ON policy.packs(is_builtin);
|
||||
|
||||
-- Pack versions table (immutable versions)
|
||||
CREATE TABLE IF NOT EXISTS policy.pack_versions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pack_id UUID NOT NULL REFERENCES policy.packs(id) ON DELETE CASCADE,
|
||||
version INT NOT NULL,
|
||||
description TEXT,
|
||||
rules_hash TEXT NOT NULL,
|
||||
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
published_at TIMESTAMPTZ,
|
||||
published_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
UNIQUE(pack_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_pack_versions_pack ON policy.pack_versions(pack_id);
|
||||
CREATE INDEX idx_pack_versions_published ON policy.pack_versions(pack_id, is_published);
|
||||
|
||||
-- Rules table (OPA/Rego rules)
|
||||
CREATE TABLE IF NOT EXISTS policy.rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pack_version_id UUID NOT NULL REFERENCES policy.pack_versions(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
rule_type TEXT NOT NULL DEFAULT 'rego' CHECK (rule_type IN ('rego', 'json', 'yaml')),
|
||||
content TEXT NOT NULL,
|
||||
content_hash TEXT NOT NULL,
|
||||
severity TEXT NOT NULL DEFAULT 'medium' CHECK (severity IN ('critical', 'high', 'medium', 'low', 'info')),
|
||||
category TEXT,
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(pack_version_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_rules_pack_version ON policy.rules(pack_version_id);
|
||||
CREATE INDEX idx_rules_severity ON policy.rules(severity);
|
||||
CREATE INDEX idx_rules_category ON policy.rules(category);
|
||||
CREATE INDEX idx_rules_tags ON policy.rules USING GIN(tags);
|
||||
|
||||
-- Risk profiles table
|
||||
CREATE TABLE IF NOT EXISTS policy.risk_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
thresholds JSONB NOT NULL DEFAULT '{}',
|
||||
scoring_weights JSONB NOT NULL DEFAULT '{}',
|
||||
exemptions JSONB NOT NULL DEFAULT '[]',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
UNIQUE(tenant_id, name, version)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_risk_profiles_tenant ON policy.risk_profiles(tenant_id);
|
||||
CREATE INDEX idx_risk_profiles_active ON policy.risk_profiles(tenant_id, name, is_active)
|
||||
WHERE is_active = TRUE;
|
||||
|
||||
-- Risk profile history (for audit trail)
|
||||
CREATE TABLE IF NOT EXISTS policy.risk_profile_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
risk_profile_id UUID NOT NULL REFERENCES policy.risk_profiles(id),
|
||||
version INT NOT NULL,
|
||||
thresholds JSONB NOT NULL,
|
||||
scoring_weights JSONB NOT NULL,
|
||||
exemptions JSONB NOT NULL,
|
||||
changed_by TEXT,
|
||||
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
change_reason TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_risk_profile_history_profile ON policy.risk_profile_history(risk_profile_id);
|
||||
|
||||
-- Evaluation runs table
|
||||
CREATE TABLE IF NOT EXISTS policy.evaluation_runs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
project_id TEXT,
|
||||
artifact_id TEXT,
|
||||
pack_id UUID REFERENCES policy.packs(id),
|
||||
pack_version INT,
|
||||
risk_profile_id UUID REFERENCES policy.risk_profiles(id),
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed')),
|
||||
result TEXT CHECK (result IN ('pass', 'fail', 'warn', 'error')),
|
||||
score NUMERIC(5,2),
|
||||
findings_count INT NOT NULL DEFAULT 0,
|
||||
critical_count INT NOT NULL DEFAULT 0,
|
||||
high_count INT NOT NULL DEFAULT 0,
|
||||
medium_count INT NOT NULL DEFAULT 0,
|
||||
low_count INT NOT NULL DEFAULT 0,
|
||||
input_hash TEXT,
|
||||
duration_ms INT,
|
||||
error_message TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_by TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_evaluation_runs_tenant ON policy.evaluation_runs(tenant_id);
|
||||
CREATE INDEX idx_evaluation_runs_project ON policy.evaluation_runs(tenant_id, project_id);
|
||||
CREATE INDEX idx_evaluation_runs_artifact ON policy.evaluation_runs(tenant_id, artifact_id);
|
||||
CREATE INDEX idx_evaluation_runs_created ON policy.evaluation_runs(tenant_id, created_at);
|
||||
CREATE INDEX idx_evaluation_runs_status ON policy.evaluation_runs(status);
|
||||
|
||||
-- Explanations table (rule evaluation details)
|
||||
CREATE TABLE IF NOT EXISTS policy.explanations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
evaluation_run_id UUID NOT NULL REFERENCES policy.evaluation_runs(id) ON DELETE CASCADE,
|
||||
rule_id UUID REFERENCES policy.rules(id),
|
||||
rule_name TEXT NOT NULL,
|
||||
result TEXT NOT NULL CHECK (result IN ('pass', 'fail', 'skip', 'error')),
|
||||
severity TEXT NOT NULL,
|
||||
message TEXT,
|
||||
details JSONB NOT NULL DEFAULT '{}',
|
||||
remediation TEXT,
|
||||
resource_path TEXT,
|
||||
line_number INT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_explanations_run ON policy.explanations(evaluation_run_id);
|
||||
CREATE INDEX idx_explanations_result ON policy.explanations(evaluation_run_id, result);
|
||||
|
||||
-- Exceptions table (policy exceptions/waivers)
|
||||
CREATE TABLE IF NOT EXISTS policy.exceptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
rule_pattern TEXT,
|
||||
resource_pattern TEXT,
|
||||
artifact_pattern TEXT,
|
||||
project_id TEXT,
|
||||
reason TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'expired', 'revoked')),
|
||||
expires_at TIMESTAMPTZ,
|
||||
approved_by TEXT,
|
||||
approved_at TIMESTAMPTZ,
|
||||
revoked_by TEXT,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_exceptions_tenant ON policy.exceptions(tenant_id);
|
||||
CREATE INDEX idx_exceptions_status ON policy.exceptions(tenant_id, status);
|
||||
CREATE INDEX idx_exceptions_expires ON policy.exceptions(expires_at)
|
||||
WHERE status = 'active';
|
||||
CREATE INDEX idx_exceptions_project ON policy.exceptions(tenant_id, project_id);
|
||||
|
||||
-- Audit log table
|
||||
CREATE TABLE IF NOT EXISTS policy.audit (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
user_id UUID,
|
||||
action TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id TEXT,
|
||||
old_value JSONB,
|
||||
new_value JSONB,
|
||||
correlation_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_tenant ON policy.audit(tenant_id);
|
||||
CREATE INDEX idx_audit_resource ON policy.audit(resource_type, resource_id);
|
||||
CREATE INDEX idx_audit_created ON policy.audit(tenant_id, created_at);
|
||||
|
||||
-- Update timestamp function
|
||||
CREATE OR REPLACE FUNCTION policy.update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Triggers
|
||||
CREATE TRIGGER trg_packs_updated_at
|
||||
BEFORE UPDATE ON policy.packs
|
||||
FOR EACH ROW EXECUTE FUNCTION policy.update_updated_at();
|
||||
|
||||
CREATE TRIGGER trg_risk_profiles_updated_at
|
||||
BEFORE UPDATE ON policy.risk_profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION policy.update_updated_at();
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL data source for the Policy module.
|
||||
/// Manages connections with tenant context for policy packs, rules, and risk profiles.
|
||||
/// </summary>
|
||||
public sealed class PolicyDataSource : DataSourceBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Default schema name for Policy tables.
|
||||
/// </summary>
|
||||
public const string DefaultSchemaName = "policy";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Policy data source.
|
||||
/// </summary>
|
||||
public PolicyDataSource(IOptions<PostgresOptions> options, ILogger<PolicyDataSource> logger)
|
||||
: base(CreateOptions(options.Value), logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ModuleName => "Policy";
|
||||
|
||||
private static PostgresOptions CreateOptions(PostgresOptions baseOptions)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(baseOptions.SchemaName))
|
||||
{
|
||||
baseOptions.SchemaName = DefaultSchemaName;
|
||||
}
|
||||
return baseOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring Policy PostgreSQL storage services.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Policy PostgreSQL storage services.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <param name="configuration">Configuration root.</param>
|
||||
/// <param name="sectionName">Configuration section name for PostgreSQL options.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddPolicyPostgresStorage(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = "Postgres:Policy")
|
||||
{
|
||||
services.Configure<PostgresOptions>(sectionName, configuration.GetSection(sectionName));
|
||||
services.AddSingleton<PolicyDataSource>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Policy PostgreSQL storage services with explicit options.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <param name="configureOptions">Options configuration action.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddPolicyPostgresStorage(
|
||||
this IServiceCollection services,
|
||||
Action<PostgresOptions> configureOptions)
|
||||
{
|
||||
services.Configure(configureOptions);
|
||||
services.AddSingleton<PolicyDataSource>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Policy.Storage.Postgres</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Migrations\**\*.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring.Policies;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests;
|
||||
|
||||
public sealed class CvssPolicyLoaderTests
|
||||
{
|
||||
private readonly CvssPolicyLoader _loader = new();
|
||||
|
||||
[Fact]
|
||||
public void Load_ValidPolicy_ComputesDeterministicHashAndReturnsPolicy()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"policyId": "default",
|
||||
"version": "1.0.0",
|
||||
"name": "Default CVSS v4",
|
||||
"effectiveFrom": "2025-01-01T00:00:00Z",
|
||||
"severityThresholds": { "lowMin": 0.1, "mediumMin": 4.0, "highMin": 7.0, "criticalMin": 9.0 },
|
||||
"metricOverrides": [
|
||||
{ "id": "override-1", "vulnerabilityPattern": "CVE-2025-0001", "priority": 1, "scoreAdjustment": 0.3, "isActive": true }
|
||||
],
|
||||
"attestationRequirements": { "requireDsse": true, "requireRekor": false }
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _loader.Load(json);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Policy.Should().NotBeNull();
|
||||
result.Hash.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
// determinism: hash must match when reloading the same payload (even with hash field present)
|
||||
var withHash = JsonSerializer.Deserialize<JsonElement>(json);
|
||||
var roundTrip = _loader.Load(AddHash(withHash, result.Hash!));
|
||||
roundTrip.Hash.Should().Be(result.Hash);
|
||||
roundTrip.Policy!.Hash.Should().Be(result.Hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_InvalidPolicy_ReturnsValidationErrors()
|
||||
{
|
||||
// Arrange: missing required fields
|
||||
const string json = """{"name":"Missing required fields"}""";
|
||||
|
||||
// Act
|
||||
var result = _loader.Load(json);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Policy.Should().BeNull();
|
||||
result.Errors.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
private static JsonElement AddHash(JsonElement element, string hash)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(element.GetRawText());
|
||||
using var stream = new MemoryStream();
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
writer.WritePropertyName(prop.Name);
|
||||
prop.Value.WriteTo(writer);
|
||||
}
|
||||
writer.WriteString("hash", hash);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
using var finalDoc = JsonDocument.Parse(stream);
|
||||
return finalDoc.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.Policy.Scoring.Receipts;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests.Fakes;
|
||||
|
||||
internal sealed class InMemoryReceiptRepository : IReceiptRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CvssScoreReceipt> _store = new();
|
||||
|
||||
public Task<CvssScoreReceipt> SaveAsync(CvssScoreReceipt receipt, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_store[receipt.ReceiptId] = receipt;
|
||||
return Task.FromResult(receipt);
|
||||
}
|
||||
|
||||
public bool Contains(string receiptId) => _store.ContainsKey(receiptId);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
using StellaOps.Policy.Scoring.Receipts;
|
||||
using StellaOps.Policy.Scoring.Tests.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests;
|
||||
|
||||
public sealed class ReceiptBuilderTests
|
||||
{
|
||||
private readonly ICvssV4Engine _engine = new CvssV4Engine();
|
||||
private readonly InMemoryReceiptRepository _repository = new();
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_ComputesDeterministicHashAndStoresReceipt()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new CvssPolicy
|
||||
{
|
||||
PolicyId = "default",
|
||||
Version = "1.0.0",
|
||||
Name = "Default",
|
||||
EffectiveFrom = new DateTimeOffset(2025, 01, 01, 0, 0, 0, TimeSpan.Zero),
|
||||
Hash = "abc123",
|
||||
SeverityThresholds = new CvssSeverityThresholds()
|
||||
};
|
||||
|
||||
var request = new CreateReceiptRequest
|
||||
{
|
||||
VulnerabilityId = "CVE-2025-0001",
|
||||
TenantId = "tenant-a",
|
||||
CreatedBy = "tester",
|
||||
CreatedAt = new DateTimeOffset(2025, 11, 28, 12, 0, 0, TimeSpan.Zero),
|
||||
Policy = policy,
|
||||
BaseMetrics = new CvssBaseMetrics
|
||||
{
|
||||
AttackVector = AttackVector.Network,
|
||||
AttackComplexity = AttackComplexity.Low,
|
||||
AttackRequirements = AttackRequirements.None,
|
||||
PrivilegesRequired = PrivilegesRequired.None,
|
||||
UserInteraction = UserInteraction.None,
|
||||
VulnerableSystemConfidentiality = ImpactMetricValue.High,
|
||||
VulnerableSystemIntegrity = ImpactMetricValue.High,
|
||||
VulnerableSystemAvailability = ImpactMetricValue.High,
|
||||
SubsequentSystemConfidentiality = ImpactMetricValue.High,
|
||||
SubsequentSystemIntegrity = ImpactMetricValue.High,
|
||||
SubsequentSystemAvailability = ImpactMetricValue.High
|
||||
},
|
||||
Evidence = ImmutableList<CvssEvidenceItem>.Empty.Add(new CvssEvidenceItem
|
||||
{
|
||||
Type = "advisory",
|
||||
Uri = "sha256:deadbeef",
|
||||
Description = "Vendor advisory",
|
||||
IsAuthoritative = true
|
||||
})
|
||||
};
|
||||
|
||||
var builder = new ReceiptBuilder(_engine, _repository);
|
||||
|
||||
// Act
|
||||
var receipt1 = await builder.CreateAsync(request);
|
||||
var receipt2 = await builder.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
receipt1.ReceiptId.Should().NotBeNullOrEmpty();
|
||||
receipt1.VectorString.Should().StartWith("CVSS:4.0");
|
||||
receipt1.InputHash.Should().NotBeNullOrEmpty();
|
||||
receipt2.InputHash.Should().Be(receipt1.InputHash); // deterministic across runs with same inputs
|
||||
_repository.Contains(receipt1.ReceiptId).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_EnforcesEvidenceRequirements()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new CvssPolicy
|
||||
{
|
||||
PolicyId = "strict",
|
||||
Version = "1.0.0",
|
||||
Name = "Strict Evidence",
|
||||
EffectiveFrom = DateTimeOffset.UtcNow,
|
||||
Hash = "abc123",
|
||||
EvidenceRequirements = new CvssEvidenceRequirements
|
||||
{
|
||||
MinimumCount = 2,
|
||||
RequireAuthoritative = true,
|
||||
RequiredTypes = ImmutableList.Create("advisory", "scan")
|
||||
}
|
||||
};
|
||||
|
||||
var request = new CreateReceiptRequest
|
||||
{
|
||||
VulnerabilityId = "CVE-2025-0002",
|
||||
TenantId = "tenant-b",
|
||||
CreatedBy = "tester",
|
||||
Policy = policy,
|
||||
BaseMetrics = new CvssBaseMetrics
|
||||
{
|
||||
AttackVector = AttackVector.Network,
|
||||
AttackComplexity = AttackComplexity.Low,
|
||||
AttackRequirements = AttackRequirements.None,
|
||||
PrivilegesRequired = PrivilegesRequired.None,
|
||||
UserInteraction = UserInteraction.None,
|
||||
VulnerableSystemConfidentiality = ImpactMetricValue.High,
|
||||
VulnerableSystemIntegrity = ImpactMetricValue.High,
|
||||
VulnerableSystemAvailability = ImpactMetricValue.High,
|
||||
SubsequentSystemConfidentiality = ImpactMetricValue.High,
|
||||
SubsequentSystemIntegrity = ImpactMetricValue.High,
|
||||
SubsequentSystemAvailability = ImpactMetricValue.High
|
||||
},
|
||||
Evidence = ImmutableList<CvssEvidenceItem>.Empty.Add(new CvssEvidenceItem
|
||||
{
|
||||
Type = "advisory",
|
||||
Uri = "sha256:123",
|
||||
IsAuthoritative = false
|
||||
})
|
||||
};
|
||||
|
||||
var builder = new ReceiptBuilder(_engine, _repository);
|
||||
|
||||
// Act
|
||||
var act = async () => await builder.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Evidence*");
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
Reference in New Issue
Block a user