feat: add security sink detection patterns for JavaScript/TypeScript

- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
This commit is contained in:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -0,0 +1,692 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_4500_0001_0002 - VEX Trust Scoring Framework
// Tasks: TRUST-019 (scorecard API), TRUST-020 (historical metrics),
// TRUST-021 (audit log), TRUST-022 (trends visualization)
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using StellaOps.VexLens.Trust.SourceTrust;
namespace StellaOps.VexLens.Api;
/// <summary>
/// API response for source trust scorecard.
/// </summary>
public sealed record TrustScorecardResponse
{
/// <summary>
/// Source identifier.
/// </summary>
[JsonPropertyName("sourceId")]
public required string SourceId { get; init; }
/// <summary>
/// Source name.
/// </summary>
[JsonPropertyName("sourceName")]
public required string SourceName { get; init; }
/// <summary>
/// Current trust score.
/// </summary>
[JsonPropertyName("currentScore")]
public required TrustScoreSummary CurrentScore { get; init; }
/// <summary>
/// Historical accuracy metrics.
/// </summary>
[JsonPropertyName("accuracy")]
public required AccuracyMetrics Accuracy { get; init; }
/// <summary>
/// Historical trend data.
/// </summary>
[JsonPropertyName("trend")]
public required TrustTrendData Trend { get; init; }
/// <summary>
/// Verification status.
/// </summary>
[JsonPropertyName("verification")]
public required VerificationStatus Verification { get; init; }
/// <summary>
/// When this scorecard was generated.
/// </summary>
[JsonPropertyName("generatedAt")]
public required DateTimeOffset GeneratedAt { get; init; }
}
/// <summary>
/// Summary of trust score for API response.
/// </summary>
public sealed record TrustScoreSummary
{
/// <summary>
/// Composite trust score (0.0 - 1.0).
/// </summary>
[JsonPropertyName("composite")]
public required double Composite { get; init; }
/// <summary>
/// Trust tier classification.
/// </summary>
[JsonPropertyName("tier")]
public required string Tier { get; init; }
/// <summary>
/// Component scores breakdown.
/// </summary>
[JsonPropertyName("components")]
public required TrustScoreComponents Components { get; init; }
/// <summary>
/// When this score was computed.
/// </summary>
[JsonPropertyName("computedAt")]
public required DateTimeOffset ComputedAt { get; init; }
/// <summary>
/// Warnings about the score.
/// </summary>
[JsonPropertyName("warnings")]
public ImmutableArray<string> Warnings { get; init; } = [];
}
/// <summary>
/// Component scores for trust.
/// </summary>
public sealed record TrustScoreComponents
{
/// <summary>
/// Authority score.
/// </summary>
[JsonPropertyName("authority")]
public required double Authority { get; init; }
/// <summary>
/// Accuracy score.
/// </summary>
[JsonPropertyName("accuracy")]
public required double Accuracy { get; init; }
/// <summary>
/// Timeliness score.
/// </summary>
[JsonPropertyName("timeliness")]
public required double Timeliness { get; init; }
/// <summary>
/// Coverage score.
/// </summary>
[JsonPropertyName("coverage")]
public required double Coverage { get; init; }
/// <summary>
/// Verification score.
/// </summary>
[JsonPropertyName("verification")]
public required double Verification { get; init; }
}
/// <summary>
/// Historical accuracy metrics for a source.
/// </summary>
public sealed record AccuracyMetrics
{
/// <summary>
/// Total statements issued.
/// </summary>
[JsonPropertyName("totalStatements")]
public required int TotalStatements { get; init; }
/// <summary>
/// Statements confirmed by consensus.
/// </summary>
[JsonPropertyName("confirmedStatements")]
public required int ConfirmedStatements { get; init; }
/// <summary>
/// Statements revoked.
/// </summary>
[JsonPropertyName("revokedStatements")]
public required int RevokedStatements { get; init; }
/// <summary>
/// False positive rate (0.0 - 1.0).
/// </summary>
[JsonPropertyName("falsePositiveRate")]
public required double FalsePositiveRate { get; init; }
/// <summary>
/// Revocation rate (0.0 - 1.0).
/// </summary>
[JsonPropertyName("revocationRate")]
public required double RevocationRate { get; init; }
/// <summary>
/// Confirmation rate (0.0 - 1.0).
/// </summary>
[JsonPropertyName("confirmationRate")]
public required double ConfirmationRate { get; init; }
/// <summary>
/// Average days from CVE to VEX statement.
/// </summary>
[JsonPropertyName("averageResponseDays")]
public required double AverageResponseDays { get; init; }
}
/// <summary>
/// Trust score trend data for visualization.
/// </summary>
public sealed record TrustTrendData
{
/// <summary>
/// Current trend direction.
/// </summary>
[JsonPropertyName("direction")]
public required string Direction { get; init; }
/// <summary>
/// Score change over last 30 days.
/// </summary>
[JsonPropertyName("change30Days")]
public required double Change30Days { get; init; }
/// <summary>
/// Score change over last 90 days.
/// </summary>
[JsonPropertyName("change90Days")]
public required double Change90Days { get; init; }
/// <summary>
/// Historical data points for charting.
/// </summary>
[JsonPropertyName("history")]
public ImmutableArray<TrustScoreDataPoint> History { get; init; } = [];
}
/// <summary>
/// Single data point in trust score history.
/// </summary>
public sealed record TrustScoreDataPoint
{
/// <summary>
/// Timestamp of this data point.
/// </summary>
[JsonPropertyName("timestamp")]
public required DateTimeOffset Timestamp { get; init; }
/// <summary>
/// Composite score at this time.
/// </summary>
[JsonPropertyName("compositeScore")]
public required double CompositeScore { get; init; }
/// <summary>
/// Number of statements at this time.
/// </summary>
[JsonPropertyName("statementCount")]
public required int StatementCount { get; init; }
}
/// <summary>
/// Verification status summary.
/// </summary>
public sealed record VerificationStatus
{
/// <summary>
/// Whether the issuer identity is verified.
/// </summary>
[JsonPropertyName("issuerVerified")]
public required bool IssuerVerified { get; init; }
/// <summary>
/// Percentage of statements with valid signatures.
/// </summary>
[JsonPropertyName("signatureValidityRate")]
public required double SignatureValidityRate { get; init; }
/// <summary>
/// Verification method used.
/// </summary>
[JsonPropertyName("verificationMethod")]
public string? VerificationMethod { get; init; }
/// <summary>
/// Supported signature formats.
/// </summary>
[JsonPropertyName("supportedFormats")]
public ImmutableArray<string> SupportedFormats { get; init; } = [];
}
/// <summary>
/// Conflict resolution audit log entry.
/// </summary>
public sealed record ConflictResolutionAuditEntry
{
/// <summary>
/// Unique ID of this audit entry.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// When the conflict was resolved.
/// </summary>
[JsonPropertyName("resolvedAt")]
public required DateTimeOffset ResolvedAt { get; init; }
/// <summary>
/// CVE ID involved.
/// </summary>
[JsonPropertyName("cveId")]
public required string CveId { get; init; }
/// <summary>
/// Sources involved in conflict.
/// </summary>
[JsonPropertyName("conflictingSources")]
public required ImmutableArray<ConflictingSourceInfo> ConflictingSources { get; init; }
/// <summary>
/// Winning source.
/// </summary>
[JsonPropertyName("winner")]
public required string WinnerSourceId { get; init; }
/// <summary>
/// Resolution method used.
/// </summary>
[JsonPropertyName("resolutionMethod")]
public required string ResolutionMethod { get; init; }
/// <summary>
/// Explanation of resolution.
/// </summary>
[JsonPropertyName("explanation")]
public required string Explanation { get; init; }
/// <summary>
/// Confidence in the resolution.
/// </summary>
[JsonPropertyName("confidence")]
public required double Confidence { get; init; }
}
/// <summary>
/// Information about a source in a conflict.
/// </summary>
public sealed record ConflictingSourceInfo
{
/// <summary>
/// Source ID.
/// </summary>
[JsonPropertyName("sourceId")]
public required string SourceId { get; init; }
/// <summary>
/// Status claimed by this source.
/// </summary>
[JsonPropertyName("claimedStatus")]
public required string ClaimedStatus { get; init; }
/// <summary>
/// Trust score of this source.
/// </summary>
[JsonPropertyName("trustScore")]
public required double TrustScore { get; init; }
}
/// <summary>
/// Request to get source trust scorecard.
/// </summary>
public sealed record GetScorecardRequest
{
/// <summary>
/// Source ID to get scorecard for.
/// </summary>
[JsonPropertyName("sourceId")]
public required string SourceId { get; init; }
/// <summary>
/// Whether to include historical trend data.
/// </summary>
[JsonPropertyName("includeTrend")]
public bool IncludeTrend { get; init; } = true;
/// <summary>
/// Number of days of history to include.
/// </summary>
[JsonPropertyName("trendDays")]
public int TrendDays { get; init; } = 90;
}
/// <summary>
/// Request to get conflict resolution audit log.
/// </summary>
public sealed record GetConflictAuditRequest
{
/// <summary>
/// Filter by source ID.
/// </summary>
[JsonPropertyName("sourceId")]
public string? SourceId { get; init; }
/// <summary>
/// Filter by CVE ID.
/// </summary>
[JsonPropertyName("cveId")]
public string? CveId { get; init; }
/// <summary>
/// Start date for audit entries.
/// </summary>
[JsonPropertyName("fromDate")]
public DateTimeOffset? FromDate { get; init; }
/// <summary>
/// End date for audit entries.
/// </summary>
[JsonPropertyName("toDate")]
public DateTimeOffset? ToDate { get; init; }
/// <summary>
/// Maximum entries to return.
/// </summary>
[JsonPropertyName("limit")]
public int Limit { get; init; } = 100;
/// <summary>
/// Offset for pagination.
/// </summary>
[JsonPropertyName("offset")]
public int Offset { get; init; }
}
/// <summary>
/// Response for conflict audit log query.
/// </summary>
public sealed record ConflictAuditResponse
{
/// <summary>
/// Audit entries.
/// </summary>
[JsonPropertyName("entries")]
public required ImmutableArray<ConflictResolutionAuditEntry> Entries { get; init; }
/// <summary>
/// Total count of matching entries.
/// </summary>
[JsonPropertyName("totalCount")]
public required int TotalCount { get; init; }
/// <summary>
/// Whether there are more entries.
/// </summary>
[JsonPropertyName("hasMore")]
public required bool HasMore { get; init; }
}
/// <summary>
/// Service interface for trust scorecard API.
/// </summary>
public interface ITrustScorecardApiService
{
/// <summary>
/// Gets trust scorecard for a source.
/// </summary>
Task<TrustScorecardResponse> GetScorecardAsync(
GetScorecardRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets trust scorecards for multiple sources.
/// </summary>
Task<IReadOnlyList<TrustScorecardResponse>> GetScorecardsAsync(
IEnumerable<string> sourceIds,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets conflict resolution audit log.
/// </summary>
Task<ConflictAuditResponse> GetConflictAuditAsync(
GetConflictAuditRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets trust score trend data for visualization.
/// </summary>
Task<TrustTrendData> GetTrendDataAsync(
string sourceId,
int days,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Default implementation of trust scorecard API service.
/// </summary>
public sealed class TrustScorecardApiService : ITrustScorecardApiService
{
private readonly ISourceTrustScoreCalculator _scoreCalculator;
private readonly IConflictAuditStore? _auditStore;
private readonly ITrustScoreHistoryStore? _historyStore;
public TrustScorecardApiService(
ISourceTrustScoreCalculator scoreCalculator,
IConflictAuditStore? auditStore = null,
ITrustScoreHistoryStore? historyStore = null)
{
_scoreCalculator = scoreCalculator;
_auditStore = auditStore;
_historyStore = historyStore;
}
public async Task<TrustScorecardResponse> GetScorecardAsync(
GetScorecardRequest request,
CancellationToken cancellationToken = default)
{
// Get current score
var cachedScore = await _scoreCalculator.GetCachedScoreAsync(
request.SourceId, cancellationToken);
if (cachedScore == null)
{
throw new InvalidOperationException($"No trust score found for source '{request.SourceId}'");
}
// Build trend data if requested
TrustTrendData? trend = null;
if (request.IncludeTrend)
{
trend = await GetTrendDataAsync(request.SourceId, request.TrendDays, cancellationToken);
}
return new TrustScorecardResponse
{
SourceId = cachedScore.SourceId,
SourceName = cachedScore.SourceName,
CurrentScore = new TrustScoreSummary
{
Composite = cachedScore.CompositeScore,
Tier = cachedScore.TrustTier.ToString(),
Components = new TrustScoreComponents
{
Authority = cachedScore.AuthorityScore,
Accuracy = cachedScore.AccuracyScore,
Timeliness = cachedScore.TimelinessScore,
Coverage = cachedScore.CoverageScore,
Verification = cachedScore.VerificationScore
},
ComputedAt = cachedScore.ComputedAt,
Warnings = cachedScore.Warnings.ToImmutableArray()
},
Accuracy = new AccuracyMetrics
{
TotalStatements = cachedScore.StatementCount,
ConfirmedStatements = cachedScore.Breakdown.Accuracy.ConfirmedStatements,
RevokedStatements = cachedScore.Breakdown.Accuracy.RevokedStatements,
FalsePositiveRate = cachedScore.Breakdown.Accuracy.FalsePositiveRate,
RevocationRate = cachedScore.Breakdown.Accuracy.RevocationRate,
ConfirmationRate = cachedScore.Breakdown.Accuracy.ConfirmationRate,
AverageResponseDays = cachedScore.Breakdown.Timeliness.AverageResponseDays
},
Trend = trend ?? new TrustTrendData
{
Direction = cachedScore.Trend.ToString(),
Change30Days = 0.0,
Change90Days = 0.0
},
Verification = new VerificationStatus
{
IssuerVerified = cachedScore.Breakdown.Verification.IssuerVerified,
SignatureValidityRate = cachedScore.Breakdown.Verification.SignatureValidityRate,
VerificationMethod = cachedScore.Breakdown.Verification.IssuerVerified ? "registry" : null
},
GeneratedAt = DateTimeOffset.UtcNow
};
}
public async Task<IReadOnlyList<TrustScorecardResponse>> GetScorecardsAsync(
IEnumerable<string> sourceIds,
CancellationToken cancellationToken = default)
{
var results = new List<TrustScorecardResponse>();
foreach (var sourceId in sourceIds)
{
try
{
var scorecard = await GetScorecardAsync(
new GetScorecardRequest { SourceId = sourceId, IncludeTrend = false },
cancellationToken);
results.Add(scorecard);
}
catch
{
// Skip sources without scores
}
}
return results;
}
public async Task<ConflictAuditResponse> GetConflictAuditAsync(
GetConflictAuditRequest request,
CancellationToken cancellationToken = default)
{
if (_auditStore == null)
{
return new ConflictAuditResponse
{
Entries = [],
TotalCount = 0,
HasMore = false
};
}
return await _auditStore.QueryAsync(request, cancellationToken);
}
public async Task<TrustTrendData> GetTrendDataAsync(
string sourceId,
int days,
CancellationToken cancellationToken = default)
{
if (_historyStore == null)
{
return new TrustTrendData
{
Direction = "Stable",
Change30Days = 0.0,
Change90Days = 0.0
};
}
var history = await _historyStore.GetHistoryAsync(
sourceId,
DateTimeOffset.UtcNow.AddDays(-days),
DateTimeOffset.UtcNow,
cancellationToken);
if (history.Count == 0)
{
return new TrustTrendData
{
Direction = "Stable",
Change30Days = 0.0,
Change90Days = 0.0
};
}
var current = history.LastOrDefault()?.CompositeScore ?? 0.0;
var thirtyDaysAgo = history
.Where(h => h.Timestamp >= DateTimeOffset.UtcNow.AddDays(-30))
.FirstOrDefault()?.CompositeScore ?? current;
var ninetyDaysAgo = history.FirstOrDefault()?.CompositeScore ?? current;
var change30 = current - thirtyDaysAgo;
var change90 = current - ninetyDaysAgo;
var direction = change30 switch
{
> 0.05 => "Improving",
< -0.05 => "Declining",
_ => "Stable"
};
return new TrustTrendData
{
Direction = direction,
Change30Days = change30,
Change90Days = change90,
History = history.ToImmutableArray()
};
}
}
/// <summary>
/// Store for conflict resolution audit entries.
/// </summary>
public interface IConflictAuditStore
{
/// <summary>
/// Records a conflict resolution.
/// </summary>
Task RecordAsync(
ConflictResolutionAuditEntry entry,
CancellationToken cancellationToken = default);
/// <summary>
/// Queries audit entries.
/// </summary>
Task<ConflictAuditResponse> QueryAsync(
GetConflictAuditRequest request,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Store for trust score history.
/// </summary>
public interface ITrustScoreHistoryStore
{
/// <summary>
/// Records a trust score snapshot.
/// </summary>
Task RecordAsync(
string sourceId,
double compositeScore,
int statementCount,
DateTimeOffset timestamp,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets history for a source.
/// </summary>
Task<IReadOnlyList<TrustScoreDataPoint>> GetHistoryAsync(
string sourceId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken cancellationToken = default);
}

View File

@@ -13,6 +13,7 @@ using StellaOps.VexLens.Observability;
using StellaOps.VexLens.Options;
using StellaOps.VexLens.Storage;
using StellaOps.VexLens.Trust;
using StellaOps.VexLens.Trust.SourceTrust;
using StellaOps.VexLens.Verification;
namespace StellaOps.VexLens.Extensions;
@@ -87,9 +88,22 @@ public static class VexLensServiceCollectionExtensions
// Issuer directory - use in-memory by default, can be replaced
services.TryAddSingleton<IIssuerDirectory, InMemoryIssuerDirectory>();
// Trust engine
// Trust engine (statement-level)
services.TryAddSingleton<ITrustWeightEngine, TrustWeightEngine>();
// Source trust scoring (source-level)
services.TryAddSingleton(Microsoft.Extensions.Options.Options.Create(
SourceTrustScoreConfiguration.CreateDefault()));
services.TryAddSingleton<IAuthorityScoreCalculator, AuthorityScoreCalculator>();
services.TryAddSingleton<IAccuracyScoreCalculator, AccuracyScoreCalculator>();
services.TryAddSingleton<ITimelinessScoreCalculator, TimelinessScoreCalculator>();
services.TryAddSingleton<ICoverageScoreCalculator, CoverageScoreCalculator>();
services.TryAddSingleton<IVerificationScoreCalculator, VerificationScoreCalculator>();
services.TryAddSingleton<ISourceTrustScoreCache, InMemorySourceTrustScoreCache>();
services.TryAddSingleton<ISourceTrustScoreCalculator, SourceTrustScoreCalculator>();
services.TryAddSingleton<IProvenanceChainValidator, ProvenanceChainValidator>();
services.TryAddSingleton<ITrustDecayService, TrustDecayService>();
// Consensus engine
services.TryAddSingleton<IVexConsensusEngine, VexConsensusEngine>();

View File

@@ -27,6 +27,9 @@ public sealed record NormalizedVexDocument(
[JsonConverter(typeof(JsonStringEnumConverter<VexSourceFormat>))]
public enum VexSourceFormat
{
[JsonPropertyName("UNKNOWN")]
Unknown,
[JsonPropertyName("OPENVEX")]
OpenVex,

View File

@@ -75,6 +75,9 @@ public sealed record NormalizedVexDocument
[JsonConverter(typeof(JsonStringEnumConverter<VexSourceFormat>))]
public enum VexSourceFormat
{
[JsonPropertyName("UNKNOWN")]
Unknown,
[JsonPropertyName("OPENVEX")]
OpenVex,

View File

@@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
</ItemGroup>

View File

@@ -0,0 +1,347 @@
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// Interface for validating VEX statement provenance chains.
/// Provenance chains track the origin and transformation history of VEX statements.
/// </summary>
public interface IProvenanceChainValidator
{
/// <summary>
/// Validates the provenance chain of a VEX statement.
/// </summary>
Task<ProvenanceValidationResult> ValidateAsync(
ProvenanceChain chain,
ProvenanceValidationOptions options,
CancellationToken cancellationToken = default);
/// <summary>
/// Validates provenance chains for multiple statements in batch.
/// </summary>
Task<IReadOnlyList<ProvenanceValidationResult>> ValidateBatchAsync(
IEnumerable<ProvenanceChain> chains,
ProvenanceValidationOptions options,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Represents the provenance chain of a VEX statement.
/// </summary>
public sealed record ProvenanceChain
{
/// <summary>
/// Unique identifier for this statement.
/// </summary>
public required string StatementId { get; init; }
/// <summary>
/// The original source of the statement.
/// </summary>
public required ProvenanceNode Origin { get; init; }
/// <summary>
/// Chain of transformations/copies from origin to current.
/// Ordered from origin to current holder.
/// </summary>
public required IReadOnlyList<ProvenanceNode> Chain { get; init; }
/// <summary>
/// The current holder of this statement version.
/// </summary>
public required ProvenanceNode Current { get; init; }
/// <summary>
/// Total number of hops in the chain.
/// </summary>
public int HopCount => Chain.Count;
}
/// <summary>
/// A node in the provenance chain.
/// </summary>
public sealed record ProvenanceNode
{
/// <summary>
/// Identifier of the entity at this node.
/// </summary>
public required string EntityId { get; init; }
/// <summary>
/// Type of entity (issuer, aggregator, mirror, etc.).
/// </summary>
public required ProvenanceEntityType EntityType { get; init; }
/// <summary>
/// URI where the statement was obtained from this entity.
/// </summary>
public string? SourceUri { get; init; }
/// <summary>
/// When the statement was received/created at this node.
/// </summary>
public required DateTimeOffset Timestamp { get; init; }
/// <summary>
/// Hash of the statement content at this node.
/// </summary>
public required string ContentHash { get; init; }
/// <summary>
/// Signature verification result at this node.
/// </summary>
public ProvenanceSignatureInfo? Signature { get; init; }
/// <summary>
/// Any transformations applied at this node.
/// </summary>
public IReadOnlyList<string>? Transformations { get; init; }
}
/// <summary>
/// Type of entity in the provenance chain.
/// </summary>
public enum ProvenanceEntityType
{
/// <summary>Original issuer (e.g., software vendor).</summary>
Issuer = 0,
/// <summary>Distributor that republishes statements.</summary>
Distributor = 1,
/// <summary>Aggregator service (e.g., VexHub).</summary>
Aggregator = 2,
/// <summary>Mirror/cache service.</summary>
Mirror = 3,
/// <summary>Internal system.</summary>
Internal = 4,
/// <summary>Unknown entity type.</summary>
Unknown = 99
}
/// <summary>
/// Signature information for a provenance node.
/// </summary>
public sealed record ProvenanceSignatureInfo
{
/// <summary>
/// Whether the signature is present.
/// </summary>
public required bool HasSignature { get; init; }
/// <summary>
/// Whether the signature is valid.
/// </summary>
public required bool IsValid { get; init; }
/// <summary>
/// Signer identifier.
/// </summary>
public string? SignerId { get; init; }
/// <summary>
/// Key fingerprint used for signing.
/// </summary>
public string? KeyFingerprint { get; init; }
/// <summary>
/// When the signature was created.
/// </summary>
public DateTimeOffset? SignedAt { get; init; }
/// <summary>
/// Signature algorithm used.
/// </summary>
public string? Algorithm { get; init; }
}
/// <summary>
/// Options for provenance validation.
/// </summary>
public sealed record ProvenanceValidationOptions
{
/// <summary>
/// Whether to require the origin to be signed.
/// </summary>
public bool RequireOriginSignature { get; init; } = true;
/// <summary>
/// Whether to require all nodes in the chain to be signed.
/// </summary>
public bool RequireAllNodesSignature { get; init; } = false;
/// <summary>
/// Maximum number of allowed hops.
/// </summary>
public int MaxHops { get; init; } = 5;
/// <summary>
/// Whether to verify content hashes match through the chain.
/// </summary>
public bool VerifyContentIntegrity { get; init; } = true;
/// <summary>
/// Trusted entity types that don't need signatures.
/// </summary>
public IReadOnlySet<ProvenanceEntityType>? TrustedEntityTypes { get; init; }
/// <summary>
/// Maximum age for provenance chain.
/// </summary>
public TimeSpan? MaxChainAge { get; init; }
}
/// <summary>
/// Result of provenance chain validation.
/// </summary>
public sealed record ProvenanceValidationResult
{
/// <summary>
/// The statement ID that was validated.
/// </summary>
public required string StatementId { get; init; }
/// <summary>
/// Overall validation status.
/// </summary>
public required ProvenanceValidationStatus Status { get; init; }
/// <summary>
/// Whether the provenance chain is valid.
/// </summary>
public bool IsValid => Status == ProvenanceValidationStatus.Valid;
/// <summary>
/// Integrity score (0.0 - 1.0) based on chain quality.
/// </summary>
public required double IntegrityScore { get; init; }
/// <summary>
/// Number of verified hops in the chain.
/// </summary>
public required int VerifiedHops { get; init; }
/// <summary>
/// Total hops in the chain.
/// </summary>
public required int TotalHops { get; init; }
/// <summary>
/// Whether the origin is verified.
/// </summary>
public required bool OriginVerified { get; init; }
/// <summary>
/// Whether content integrity was maintained through the chain.
/// </summary>
public required bool ContentIntegrityMaintained { get; init; }
/// <summary>
/// Any issues found during validation.
/// </summary>
public required IReadOnlyList<ProvenanceIssue> Issues { get; init; }
/// <summary>
/// Detailed validation for each node.
/// </summary>
public required IReadOnlyList<ProvenanceNodeValidation> NodeValidations { get; init; }
}
/// <summary>
/// Overall provenance validation status.
/// </summary>
public enum ProvenanceValidationStatus
{
/// <summary>Chain is valid and verified.</summary>
Valid = 0,
/// <summary>Chain is valid but has warnings.</summary>
ValidWithWarnings = 1,
/// <summary>Chain has too many hops.</summary>
TooManyHops = 2,
/// <summary>Origin signature missing or invalid.</summary>
OriginNotVerified = 3,
/// <summary>Content was modified in the chain.</summary>
ContentTampered = 4,
/// <summary>Chain has broken links.</summary>
BrokenChain = 5,
/// <summary>Chain is too old.</summary>
Stale = 6,
/// <summary>Unknown or unspecified error.</summary>
Unknown = 99
}
/// <summary>
/// Issue found during provenance validation.
/// </summary>
public sealed record ProvenanceIssue
{
/// <summary>
/// Severity of the issue.
/// </summary>
public required ProvenanceIssueSeverity Severity { get; init; }
/// <summary>
/// Issue code for programmatic handling.
/// </summary>
public required string Code { get; init; }
/// <summary>
/// Human-readable message.
/// </summary>
public required string Message { get; init; }
/// <summary>
/// Node index where the issue was found (-1 for chain-level issues).
/// </summary>
public int NodeIndex { get; init; } = -1;
}
/// <summary>
/// Severity of a provenance issue.
/// </summary>
public enum ProvenanceIssueSeverity
{
Info = 0,
Warning = 1,
Error = 2,
Critical = 3
}
/// <summary>
/// Validation result for a single node in the chain.
/// </summary>
public sealed record ProvenanceNodeValidation
{
/// <summary>
/// Index in the chain (0 = origin).
/// </summary>
public required int NodeIndex { get; init; }
/// <summary>
/// Entity ID of this node.
/// </summary>
public required string EntityId { get; init; }
/// <summary>
/// Whether this node is verified.
/// </summary>
public required bool IsVerified { get; init; }
/// <summary>
/// Whether content hash matches previous node.
/// </summary>
public required bool ContentHashMatches { get; init; }
/// <summary>
/// Any issues at this node.
/// </summary>
public required IReadOnlyList<ProvenanceIssue> Issues { get; init; }
}

View File

@@ -0,0 +1,421 @@
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// Interface for computing source-level trust scores.
/// </summary>
public interface ISourceTrustScoreCalculator
{
/// <summary>
/// Computes the trust score for a VEX source.
/// </summary>
Task<VexSourceTrustScore> ComputeScoreAsync(
SourceTrustScoreRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// Computes trust scores for multiple sources in batch.
/// </summary>
Task<IReadOnlyList<VexSourceTrustScore>> ComputeScoresBatchAsync(
IEnumerable<SourceTrustScoreRequest> requests,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the cached trust score for a source, if available.
/// </summary>
Task<VexSourceTrustScore?> GetCachedScoreAsync(
string sourceId,
CancellationToken cancellationToken = default);
/// <summary>
/// Invalidates the cached trust score for a source.
/// </summary>
Task InvalidateCacheAsync(
string sourceId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Request to compute a source trust score.
/// </summary>
public sealed record SourceTrustScoreRequest
{
/// <summary>
/// Unique identifier of the source.
/// </summary>
public required string SourceId { get; init; }
/// <summary>
/// Human-readable name of the source.
/// </summary>
public required string SourceName { get; init; }
/// <summary>
/// Source metadata for authority score calculation.
/// </summary>
public required SourceMetadata Metadata { get; init; }
/// <summary>
/// Historical metrics for the source.
/// </summary>
public required SourceHistoricalMetrics HistoricalMetrics { get; init; }
/// <summary>
/// Verification status summary for the source.
/// </summary>
public required SourceVerificationSummary VerificationSummary { get; init; }
/// <summary>
/// Time at which to evaluate the score.
/// </summary>
public DateTimeOffset EvaluationTime { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// Previous score for trend calculation.
/// </summary>
public VexSourceTrustScore? PreviousScore { get; init; }
}
/// <summary>
/// Metadata about a VEX source for trust scoring.
/// </summary>
public sealed record SourceMetadata
{
/// <summary>
/// Category of the issuer (vendor, distributor, community, etc.).
/// </summary>
public required IssuerCategory Category { get; init; }
/// <summary>
/// Trust tier assigned to the source.
/// </summary>
public required Models.TrustTier TrustTier { get; init; }
/// <summary>
/// Whether this is an official vendor source.
/// </summary>
public required bool IsOfficial { get; init; }
/// <summary>
/// When the source was first registered.
/// </summary>
public required DateTimeOffset FirstSeenAt { get; init; }
/// <summary>
/// Description of the source.
/// </summary>
public string? Description { get; init; }
/// <summary>
/// URL of the source.
/// </summary>
public string? SourceUrl { get; init; }
}
/// <summary>
/// Historical metrics for a VEX source.
/// </summary>
public sealed record SourceHistoricalMetrics
{
/// <summary>
/// Total number of statements from this source.
/// </summary>
public required int TotalStatements { get; init; }
/// <summary>
/// Number of statements confirmed by consensus.
/// </summary>
public required int ConfirmedStatements { get; init; }
/// <summary>
/// Number of statements that were revoked or corrected.
/// </summary>
public required int RevokedStatements { get; init; }
/// <summary>
/// Number of statements flagged as false positives.
/// </summary>
public required int FalsePositiveStatements { get; init; }
/// <summary>
/// Number of statements that are still current (not stale).
/// </summary>
public required int FreshStatements { get; init; }
/// <summary>
/// Number of unique CVEs covered.
/// </summary>
public required int CvesWithStatements { get; init; }
/// <summary>
/// Total number of relevant CVEs for comparison.
/// </summary>
public required int TotalRelevantCves { get; init; }
/// <summary>
/// Number of unique products covered.
/// </summary>
public required int ProductsCovered { get; init; }
/// <summary>
/// Number of statements with complete information.
/// </summary>
public required int CompleteStatements { get; init; }
/// <summary>
/// Average days from CVE publication to first statement.
/// </summary>
public required double AverageResponseDays { get; init; }
/// <summary>
/// Average days between statement updates.
/// </summary>
public required double AverageUpdateFrequencyDays { get; init; }
/// <summary>
/// Date of the most recent statement.
/// </summary>
public DateTimeOffset? LastStatementAt { get; init; }
}
/// <summary>
/// Verification status summary for a source.
/// </summary>
public sealed record SourceVerificationSummary
{
/// <summary>
/// Number of statements with valid signatures.
/// </summary>
public required int ValidSignatureCount { get; init; }
/// <summary>
/// Number of statements with invalid signatures.
/// </summary>
public required int InvalidSignatureCount { get; init; }
/// <summary>
/// Number of statements with no signature.
/// </summary>
public required int NoSignatureCount { get; init; }
/// <summary>
/// Number of statements with valid provenance chains.
/// </summary>
public required int ValidProvenanceCount { get; init; }
/// <summary>
/// Number of statements with broken provenance chains.
/// </summary>
public required int BrokenProvenanceCount { get; init; }
/// <summary>
/// Whether the issuer identity has been verified.
/// </summary>
public required bool IssuerIdentityVerified { get; init; }
/// <summary>
/// Method used to verify issuer identity.
/// </summary>
public string? IssuerVerificationMethod { get; init; }
}
/// <summary>
/// Category of VEX statement issuer.
/// </summary>
public enum IssuerCategory
{
/// <summary>Unknown or unspecified issuer.</summary>
Unknown = 0,
/// <summary>Software vendor (authoritative for their products).</summary>
Vendor = 1,
/// <summary>Distribution maintainer (e.g., Red Hat, Ubuntu).</summary>
Distributor = 2,
/// <summary>Community security researcher or organization.</summary>
Community = 3,
/// <summary>Internal security team.</summary>
Internal = 4,
/// <summary>VEX aggregator service.</summary>
Aggregator = 5
}
/// <summary>
/// Configuration for source trust score calculation.
/// </summary>
public sealed record SourceTrustScoreConfiguration
{
/// <summary>
/// Weights for composite score calculation.
/// </summary>
public required TrustScoreWeightConfiguration Weights { get; init; }
/// <summary>
/// Authority score configuration.
/// </summary>
public required AuthorityScoreConfiguration Authority { get; init; }
/// <summary>
/// Accuracy score configuration.
/// </summary>
public required AccuracyScoreConfiguration Accuracy { get; init; }
/// <summary>
/// Timeliness score configuration.
/// </summary>
public required TimelinessScoreConfiguration Timeliness { get; init; }
/// <summary>
/// Coverage score configuration.
/// </summary>
public required CoverageScoreConfiguration Coverage { get; init; }
/// <summary>
/// Verification score configuration.
/// </summary>
public required VerificationScoreConfiguration Verification { get; init; }
/// <summary>
/// How long computed scores should be cached.
/// </summary>
public TimeSpan CacheDuration { get; init; } = TimeSpan.FromHours(24);
/// <summary>
/// Creates the default configuration.
/// </summary>
public static SourceTrustScoreConfiguration CreateDefault() => new()
{
Weights = new TrustScoreWeightConfiguration
{
AuthorityWeight = 0.25,
AccuracyWeight = 0.30,
TimelinessWeight = 0.15,
CoverageWeight = 0.10,
VerificationWeight = 0.20
},
Authority = new AuthorityScoreConfiguration
{
VendorBaseScore = 0.9,
DistributorBaseScore = 0.8,
CommunityBaseScore = 0.5,
InternalBaseScore = 0.6,
AggregatorBaseScore = 0.4,
UnknownBaseScore = 0.2,
AuthoritativeTierBonus = 0.1,
TrustedTierBonus = 0.05,
UntrustedTierPenalty = 0.2,
OfficialSourceBonus = 0.1
},
Accuracy = new AccuracyScoreConfiguration
{
ConfirmationWeight = 0.4,
FalsePositivePenaltyWeight = 0.3,
RevocationPenaltyWeight = 0.2,
ConsistencyWeight = 0.1,
MinimumStatementsForFullScore = 100,
GracePeriodScore = 0.5
},
Timeliness = new TimelinessScoreConfiguration
{
ExcellentResponseDays = 1.0,
GoodResponseDays = 7.0,
AcceptableResponseDays = 30.0,
ResponseTimeWeight = 0.4,
UpdateFrequencyWeight = 0.3,
FreshnessWeight = 0.3,
FreshThresholdDays = 90
},
Coverage = new CoverageScoreConfiguration
{
CveCoverageWeight = 0.5,
ProductBreadthWeight = 0.3,
CompletenessWeight = 0.2,
MinProductsForFullBreadthScore = 100
},
Verification = new VerificationScoreConfiguration
{
SignatureWeight = 0.5,
ProvenanceWeight = 0.3,
IssuerVerificationBonus = 0.2
}
};
}
/// <summary>
/// Weight configuration for composite score.
/// </summary>
public sealed record TrustScoreWeightConfiguration
{
public required double AuthorityWeight { get; init; }
public required double AccuracyWeight { get; init; }
public required double TimelinessWeight { get; init; }
public required double CoverageWeight { get; init; }
public required double VerificationWeight { get; init; }
}
/// <summary>
/// Configuration for authority score calculation.
/// </summary>
public sealed record AuthorityScoreConfiguration
{
public required double VendorBaseScore { get; init; }
public required double DistributorBaseScore { get; init; }
public required double CommunityBaseScore { get; init; }
public required double InternalBaseScore { get; init; }
public required double AggregatorBaseScore { get; init; }
public required double UnknownBaseScore { get; init; }
public required double AuthoritativeTierBonus { get; init; }
public required double TrustedTierBonus { get; init; }
public required double UntrustedTierPenalty { get; init; }
public required double OfficialSourceBonus { get; init; }
}
/// <summary>
/// Configuration for accuracy score calculation.
/// </summary>
public sealed record AccuracyScoreConfiguration
{
public required double ConfirmationWeight { get; init; }
public required double FalsePositivePenaltyWeight { get; init; }
public required double RevocationPenaltyWeight { get; init; }
public required double ConsistencyWeight { get; init; }
public required int MinimumStatementsForFullScore { get; init; }
public required double GracePeriodScore { get; init; }
}
/// <summary>
/// Configuration for timeliness score calculation.
/// </summary>
public sealed record TimelinessScoreConfiguration
{
public required double ExcellentResponseDays { get; init; }
public required double GoodResponseDays { get; init; }
public required double AcceptableResponseDays { get; init; }
public required double ResponseTimeWeight { get; init; }
public required double UpdateFrequencyWeight { get; init; }
public required double FreshnessWeight { get; init; }
public required double FreshThresholdDays { get; init; }
}
/// <summary>
/// Configuration for coverage score calculation.
/// </summary>
public sealed record CoverageScoreConfiguration
{
public required double CveCoverageWeight { get; init; }
public required double ProductBreadthWeight { get; init; }
public required double CompletenessWeight { get; init; }
public required int MinProductsForFullBreadthScore { get; init; }
}
/// <summary>
/// Configuration for verification score calculation.
/// </summary>
public sealed record VerificationScoreConfiguration
{
public required double SignatureWeight { get; init; }
public required double ProvenanceWeight { get; init; }
public required double IssuerVerificationBonus { get; init; }
}

View File

@@ -0,0 +1,435 @@
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// Interface for applying time-based trust decay to VEX statements and sources.
/// </summary>
public interface ITrustDecayService
{
/// <summary>
/// Applies decay to a base trust score based on statement age.
/// </summary>
DecayResult ApplyDecay(
double baseScore,
DateTimeOffset statementTimestamp,
DecayContext context);
/// <summary>
/// Calculates the recency bonus for a recently updated statement.
/// </summary>
double CalculateRecencyBonus(
DateTimeOffset lastUpdateTimestamp,
RecencyBonusContext context);
/// <summary>
/// Applies revocation penalty to a trust score.
/// </summary>
RevocationImpact CalculateRevocationImpact(
RevocationInfo revocation,
RevocationContext context);
/// <summary>
/// Gets the effective trust score considering all decay factors.
/// </summary>
EffectiveTrustScore GetEffectiveScore(
double baseScore,
TrustScoreFactors factors,
DateTimeOffset evaluationTime);
}
/// <summary>
/// Result of applying decay to a trust score.
/// </summary>
public sealed record DecayResult
{
/// <summary>
/// The original base score before decay.
/// </summary>
public required double BaseScore { get; init; }
/// <summary>
/// The decay factor applied (0.0 - 1.0).
/// </summary>
public required double DecayFactor { get; init; }
/// <summary>
/// The decayed score (baseScore * decayFactor).
/// </summary>
public required double DecayedScore { get; init; }
/// <summary>
/// Age of the statement in days.
/// </summary>
public required double AgeDays { get; init; }
/// <summary>
/// Category of staleness.
/// </summary>
public required StalenessCategory Category { get; init; }
}
/// <summary>
/// Category of statement staleness.
/// </summary>
public enum StalenessCategory
{
/// <summary>Statement is fresh (within fresh threshold).</summary>
Fresh = 0,
/// <summary>Statement is recent (within recent threshold).</summary>
Recent = 1,
/// <summary>Statement is aging (approaching stale).</summary>
Aging = 2,
/// <summary>Statement is stale (past stale threshold).</summary>
Stale = 3,
/// <summary>Statement is expired (should be considered unreliable).</summary>
Expired = 4
}
/// <summary>
/// Context for decay calculation.
/// </summary>
public sealed record DecayContext
{
/// <summary>
/// Time at which to evaluate decay.
/// </summary>
public required DateTimeOffset EvaluationTime { get; init; }
/// <summary>
/// Configuration for decay calculation.
/// </summary>
public required DecayConfiguration Configuration { get; init; }
/// <summary>
/// Whether the statement has been updated since first seen.
/// </summary>
public bool HasUpdates { get; init; } = false;
/// <summary>
/// Number of updates the statement has received.
/// </summary>
public int UpdateCount { get; init; } = 0;
}
/// <summary>
/// Configuration for trust decay calculation.
/// </summary>
public sealed record DecayConfiguration
{
/// <summary>
/// Threshold for fresh statements (no decay).
/// </summary>
public TimeSpan FreshThreshold { get; init; } = TimeSpan.FromDays(7);
/// <summary>
/// Threshold for recent statements (minimal decay).
/// </summary>
public TimeSpan RecentThreshold { get; init; } = TimeSpan.FromDays(30);
/// <summary>
/// Threshold for stale statements (significant decay).
/// </summary>
public TimeSpan StaleThreshold { get; init; } = TimeSpan.FromDays(90);
/// <summary>
/// Threshold for expired statements (maximum decay).
/// </summary>
public TimeSpan ExpiredThreshold { get; init; } = TimeSpan.FromDays(365);
/// <summary>
/// Minimum decay factor (floor for very old statements).
/// </summary>
public double MinDecayFactor { get; init; } = 0.3;
/// <summary>
/// Decay curve type.
/// </summary>
public DecayCurveType CurveType { get; init; } = DecayCurveType.Linear;
/// <summary>
/// Creates default configuration.
/// </summary>
public static DecayConfiguration CreateDefault() => new();
}
/// <summary>
/// Type of decay curve to use.
/// </summary>
public enum DecayCurveType
{
/// <summary>Linear decay from 1.0 to minimum.</summary>
Linear,
/// <summary>Exponential decay (faster initial decay).</summary>
Exponential,
/// <summary>Step function with discrete levels.</summary>
Step
}
/// <summary>
/// Context for recency bonus calculation.
/// </summary>
public sealed record RecencyBonusContext
{
/// <summary>
/// Time at which to evaluate recency.
/// </summary>
public required DateTimeOffset EvaluationTime { get; init; }
/// <summary>
/// Maximum bonus for very recent updates.
/// </summary>
public double MaxBonus { get; init; } = 0.1;
/// <summary>
/// Window within which recency bonus applies.
/// </summary>
public TimeSpan RecencyWindow { get; init; } = TimeSpan.FromDays(7);
}
/// <summary>
/// Information about a statement revocation.
/// </summary>
public sealed record RevocationInfo
{
/// <summary>
/// Whether the statement has been revoked.
/// </summary>
public required bool IsRevoked { get; init; }
/// <summary>
/// When the statement was revoked.
/// </summary>
public DateTimeOffset? RevokedAt { get; init; }
/// <summary>
/// Reason for revocation.
/// </summary>
public string? RevocationReason { get; init; }
/// <summary>
/// Type of revocation.
/// </summary>
public RevocationType RevocationType { get; init; } = RevocationType.Unknown;
/// <summary>
/// Whether the statement was superseded by another.
/// </summary>
public bool WasSuperseded { get; init; } = false;
/// <summary>
/// ID of the superseding statement, if applicable.
/// </summary>
public string? SupersededBy { get; init; }
}
/// <summary>
/// Type of statement revocation.
/// </summary>
public enum RevocationType
{
/// <summary>Unknown revocation type.</summary>
Unknown = 0,
/// <summary>Statement was incorrect and has been corrected.</summary>
Correction = 1,
/// <summary>Statement was superseded by a newer assessment.</summary>
Superseded = 2,
/// <summary>Statement was withdrawn due to error.</summary>
Withdrawn = 3,
/// <summary>Statement expired and was not renewed.</summary>
Expired = 4,
/// <summary>Source revoked all statements.</summary>
SourceRevoked = 5
}
/// <summary>
/// Context for revocation impact calculation.
/// </summary>
public sealed record RevocationContext
{
/// <summary>
/// Time at which to evaluate revocation impact.
/// </summary>
public required DateTimeOffset EvaluationTime { get; init; }
/// <summary>
/// Penalty for revoked statements.
/// </summary>
public double RevocationPenalty { get; init; } = 0.5;
/// <summary>
/// Reduced penalty for superseded statements.
/// </summary>
public double SupersededPenalty { get; init; } = 0.2;
/// <summary>
/// Penalty for corrections (less severe).
/// </summary>
public double CorrectionPenalty { get; init; } = 0.3;
}
/// <summary>
/// Impact of a revocation on trust score.
/// </summary>
public sealed record RevocationImpact
{
/// <summary>
/// Whether this statement should be excluded from scoring.
/// </summary>
public required bool ShouldExclude { get; init; }
/// <summary>
/// Penalty to apply if not excluded.
/// </summary>
public required double Penalty { get; init; }
/// <summary>
/// Explanation of the impact.
/// </summary>
public required string Explanation { get; init; }
/// <summary>
/// Recommended action for this statement.
/// </summary>
public required RevocationAction RecommendedAction { get; init; }
}
/// <summary>
/// Recommended action for a revoked statement.
/// </summary>
public enum RevocationAction
{
/// <summary>No action needed.</summary>
None = 0,
/// <summary>Exclude from consensus.</summary>
Exclude = 1,
/// <summary>Include with penalty.</summary>
Penalize = 2,
/// <summary>Replace with superseding statement.</summary>
Replace = 3,
/// <summary>Flag for manual review.</summary>
Review = 4
}
/// <summary>
/// Factors affecting trust score.
/// </summary>
public sealed record TrustScoreFactors
{
/// <summary>
/// Timestamp of the statement.
/// </summary>
public required DateTimeOffset StatementTimestamp { get; init; }
/// <summary>
/// Timestamp of the last update, if any.
/// </summary>
public DateTimeOffset? LastUpdateTimestamp { get; init; }
/// <summary>
/// Revocation information, if any.
/// </summary>
public RevocationInfo? Revocation { get; init; }
/// <summary>
/// Number of times the statement has been updated.
/// </summary>
public int UpdateCount { get; init; } = 0;
/// <summary>
/// Configuration for decay calculation.
/// </summary>
public DecayConfiguration? DecayConfiguration { get; init; }
}
/// <summary>
/// Effective trust score with all factors applied.
/// </summary>
public sealed record EffectiveTrustScore
{
/// <summary>
/// The original base score.
/// </summary>
public required double BaseScore { get; init; }
/// <summary>
/// The effective score after all adjustments.
/// </summary>
public required double EffectiveScore { get; init; }
/// <summary>
/// Decay factor applied.
/// </summary>
public required double DecayFactor { get; init; }
/// <summary>
/// Recency bonus applied.
/// </summary>
public required double RecencyBonus { get; init; }
/// <summary>
/// Revocation penalty applied.
/// </summary>
public required double RevocationPenalty { get; init; }
/// <summary>
/// Whether this statement should be excluded.
/// </summary>
public required bool ShouldExclude { get; init; }
/// <summary>
/// Category of staleness.
/// </summary>
public required StalenessCategory StalenessCategory { get; init; }
/// <summary>
/// Breakdown of adjustments.
/// </summary>
public required IReadOnlyList<TrustAdjustment> Adjustments { get; init; }
}
/// <summary>
/// A single adjustment to a trust score.
/// </summary>
public sealed record TrustAdjustment
{
/// <summary>
/// Type of adjustment.
/// </summary>
public required TrustAdjustmentType Type { get; init; }
/// <summary>
/// Amount of adjustment (positive or negative).
/// </summary>
public required double Amount { get; init; }
/// <summary>
/// Explanation of the adjustment.
/// </summary>
public required string Reason { get; init; }
}
/// <summary>
/// Type of trust adjustment.
/// </summary>
public enum TrustAdjustmentType
{
Decay,
RecencyBonus,
RevocationPenalty,
UpdateBonus,
Custom
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Concurrent;
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// In-memory implementation of <see cref="ISourceTrustScoreCache"/>.
/// </summary>
public sealed class InMemorySourceTrustScoreCache : ISourceTrustScoreCache
{
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
private readonly Timer _cleanupTimer;
public InMemorySourceTrustScoreCache()
{
// Clean up expired entries every 5 minutes
_cleanupTimer = new Timer(CleanupExpiredEntries, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
public Task<VexSourceTrustScore?> GetAsync(string sourceId, CancellationToken cancellationToken = default)
{
if (_cache.TryGetValue(sourceId, out var entry) && entry.ExpiresAt > DateTimeOffset.UtcNow)
{
return Task.FromResult<VexSourceTrustScore?>(entry.Score);
}
return Task.FromResult<VexSourceTrustScore?>(null);
}
public Task SetAsync(string sourceId, VexSourceTrustScore score, TimeSpan duration, CancellationToken cancellationToken = default)
{
var entry = new CacheEntry(score, DateTimeOffset.UtcNow + duration);
_cache[sourceId] = entry;
return Task.CompletedTask;
}
public Task RemoveAsync(string sourceId, CancellationToken cancellationToken = default)
{
_cache.TryRemove(sourceId, out _);
return Task.CompletedTask;
}
private void CleanupExpiredEntries(object? state)
{
var now = DateTimeOffset.UtcNow;
var expiredKeys = _cache
.Where(kvp => kvp.Value.ExpiresAt <= now)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expiredKeys)
{
_cache.TryRemove(key, out _);
}
}
private sealed record CacheEntry(VexSourceTrustScore Score, DateTimeOffset ExpiresAt);
}

View File

@@ -0,0 +1,338 @@
using Microsoft.Extensions.Logging;
using StellaOps.VexLens.Verification;
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// Default implementation of <see cref="IProvenanceChainValidator"/>.
/// Validates VEX statement provenance chains for integrity and authenticity.
/// </summary>
public sealed class ProvenanceChainValidator : IProvenanceChainValidator
{
private readonly ILogger<ProvenanceChainValidator> _logger;
private readonly IIssuerDirectory _issuerDirectory;
public ProvenanceChainValidator(
ILogger<ProvenanceChainValidator> logger,
IIssuerDirectory issuerDirectory)
{
_logger = logger;
_issuerDirectory = issuerDirectory;
}
public async Task<ProvenanceValidationResult> ValidateAsync(
ProvenanceChain chain,
ProvenanceValidationOptions options,
CancellationToken cancellationToken = default)
{
_logger.LogDebug("Validating provenance chain for statement {StatementId}", chain.StatementId);
var issues = new List<ProvenanceIssue>();
var nodeValidations = new List<ProvenanceNodeValidation>();
// Validate chain structure
if (chain.HopCount > options.MaxHops)
{
issues.Add(new ProvenanceIssue
{
Severity = ProvenanceIssueSeverity.Error,
Code = "CHAIN_TOO_LONG",
Message = $"Provenance chain has {chain.HopCount} hops, exceeding maximum of {options.MaxHops}"
});
}
// Validate chain age
if (options.MaxChainAge.HasValue)
{
var chainAge = DateTimeOffset.UtcNow - chain.Origin.Timestamp;
if (chainAge > options.MaxChainAge.Value)
{
issues.Add(new ProvenanceIssue
{
Severity = ProvenanceIssueSeverity.Warning,
Code = "CHAIN_STALE",
Message = $"Provenance chain is {chainAge.TotalDays:F1} days old"
});
}
}
// Validate origin
var originValidation = await ValidateNodeAsync(
chain.Origin, 0, null, options, cancellationToken);
nodeValidations.Add(originValidation);
issues.AddRange(originValidation.Issues);
var originVerified = originValidation.IsVerified;
if (options.RequireOriginSignature && !originVerified)
{
issues.Add(new ProvenanceIssue
{
Severity = ProvenanceIssueSeverity.Error,
Code = "ORIGIN_NOT_SIGNED",
Message = "Origin node is not signed or signature is invalid",
NodeIndex = 0
});
}
// Validate chain nodes
var previousHash = chain.Origin.ContentHash;
var contentIntegrityMaintained = true;
var verifiedHops = originVerified ? 1 : 0;
for (var i = 0; i < chain.Chain.Count; i++)
{
var node = chain.Chain[i];
var nodeValidation = await ValidateNodeAsync(
node, i + 1, previousHash, options, cancellationToken);
nodeValidations.Add(nodeValidation);
issues.AddRange(nodeValidation.Issues);
if (nodeValidation.IsVerified)
{
verifiedHops++;
}
if (options.VerifyContentIntegrity && !nodeValidation.ContentHashMatches)
{
contentIntegrityMaintained = false;
issues.Add(new ProvenanceIssue
{
Severity = ProvenanceIssueSeverity.Critical,
Code = "CONTENT_MODIFIED",
Message = $"Content hash changed at node {i + 1} ({node.EntityId})",
NodeIndex = i + 1
});
}
previousHash = node.ContentHash;
}
// Validate current node
var currentValidation = await ValidateNodeAsync(
chain.Current, chain.Chain.Count + 1, previousHash, options, cancellationToken);
nodeValidations.Add(currentValidation);
issues.AddRange(currentValidation.Issues);
if (currentValidation.IsVerified)
{
verifiedHops++;
}
if (options.VerifyContentIntegrity && !currentValidation.ContentHashMatches)
{
contentIntegrityMaintained = false;
}
// Calculate integrity score
var totalNodes = chain.HopCount + 2; // origin + chain + current
var integrityScore = CalculateIntegrityScore(
verifiedHops, totalNodes, contentIntegrityMaintained, issues);
// Determine overall status
var status = DetermineStatus(
originVerified,
contentIntegrityMaintained,
chain.HopCount,
options,
issues);
_logger.LogInformation(
"Provenance validation for {StatementId}: Status={Status}, Score={Score:F3}, VerifiedHops={Verified}/{Total}",
chain.StatementId, status, integrityScore, verifiedHops, totalNodes);
return new ProvenanceValidationResult
{
StatementId = chain.StatementId,
Status = status,
IntegrityScore = integrityScore,
VerifiedHops = verifiedHops,
TotalHops = totalNodes,
OriginVerified = originVerified,
ContentIntegrityMaintained = contentIntegrityMaintained,
Issues = issues,
NodeValidations = nodeValidations
};
}
public async Task<IReadOnlyList<ProvenanceValidationResult>> ValidateBatchAsync(
IEnumerable<ProvenanceChain> chains,
ProvenanceValidationOptions options,
CancellationToken cancellationToken = default)
{
var results = new List<ProvenanceValidationResult>();
foreach (var chain in chains)
{
cancellationToken.ThrowIfCancellationRequested();
var result = await ValidateAsync(chain, options, cancellationToken);
results.Add(result);
}
return results;
}
private async Task<ProvenanceNodeValidation> ValidateNodeAsync(
ProvenanceNode node,
int nodeIndex,
string? previousHash,
ProvenanceValidationOptions options,
CancellationToken cancellationToken)
{
var issues = new List<ProvenanceIssue>();
var isVerified = false;
// Check if entity type is implicitly trusted
var isTrustedType = options.TrustedEntityTypes?.Contains(node.EntityType) ?? false;
// Validate signature
if (node.Signature != null)
{
if (node.Signature.HasSignature)
{
if (node.Signature.IsValid)
{
isVerified = true;
// Verify against issuer directory
if (!string.IsNullOrEmpty(node.Signature.SignerId))
{
var validation = await _issuerDirectory.ValidateTrustAsync(
node.Signature.SignerId,
node.Signature.KeyFingerprint,
cancellationToken);
if (!validation.IsTrusted)
{
issues.Add(new ProvenanceIssue
{
Severity = ProvenanceIssueSeverity.Warning,
Code = "UNTRUSTED_SIGNER",
Message = $"Signer {node.Signature.SignerId} is not trusted: {validation.IssuerStatus}",
NodeIndex = nodeIndex
});
}
}
}
else
{
issues.Add(new ProvenanceIssue
{
Severity = ProvenanceIssueSeverity.Error,
Code = "INVALID_SIGNATURE",
Message = $"Node {node.EntityId} has an invalid signature",
NodeIndex = nodeIndex
});
}
}
else if (options.RequireAllNodesSignature && !isTrustedType)
{
issues.Add(new ProvenanceIssue
{
Severity = ProvenanceIssueSeverity.Warning,
Code = "MISSING_SIGNATURE",
Message = $"Node {node.EntityId} has no signature",
NodeIndex = nodeIndex
});
}
}
else if (isTrustedType)
{
// Trusted entity types don't require signatures
isVerified = true;
}
// Validate content hash continuity
var contentHashMatches = previousHash == null || previousHash == node.ContentHash ||
(node.Transformations?.Any() ?? false);
if (previousHash != null && previousHash != node.ContentHash &&
!(node.Transformations?.Any() ?? false))
{
issues.Add(new ProvenanceIssue
{
Severity = ProvenanceIssueSeverity.Warning,
Code = "HASH_MISMATCH",
Message = $"Content hash changed without documented transformation at node {node.EntityId}",
NodeIndex = nodeIndex
});
}
return new ProvenanceNodeValidation
{
NodeIndex = nodeIndex,
EntityId = node.EntityId,
IsVerified = isVerified,
ContentHashMatches = contentHashMatches,
Issues = issues
};
}
private static double CalculateIntegrityScore(
int verifiedHops,
int totalNodes,
bool contentIntegrityMaintained,
IReadOnlyList<ProvenanceIssue> issues)
{
// Base score from verified hops ratio
var verificationRatio = totalNodes > 0 ? (double)verifiedHops / totalNodes : 0.0;
// Content integrity is critical
var integrityPenalty = contentIntegrityMaintained ? 0.0 : 0.5;
// Issue penalties
var criticalCount = issues.Count(i => i.Severity == ProvenanceIssueSeverity.Critical);
var errorCount = issues.Count(i => i.Severity == ProvenanceIssueSeverity.Error);
var warningCount = issues.Count(i => i.Severity == ProvenanceIssueSeverity.Warning);
var issuePenalty = (criticalCount * 0.3) + (errorCount * 0.15) + (warningCount * 0.05);
var score = verificationRatio - integrityPenalty - issuePenalty;
return Math.Clamp(score, 0.0, 1.0);
}
private static ProvenanceValidationStatus DetermineStatus(
bool originVerified,
bool contentIntegrityMaintained,
int hopCount,
ProvenanceValidationOptions options,
IReadOnlyList<ProvenanceIssue> issues)
{
// Check for critical issues first
if (!contentIntegrityMaintained)
{
return ProvenanceValidationStatus.ContentTampered;
}
if (!originVerified && options.RequireOriginSignature)
{
return ProvenanceValidationStatus.OriginNotVerified;
}
if (hopCount > options.MaxHops)
{
return ProvenanceValidationStatus.TooManyHops;
}
var hasCriticalIssues = issues.Any(i => i.Severity == ProvenanceIssueSeverity.Critical);
if (hasCriticalIssues)
{
return ProvenanceValidationStatus.BrokenChain;
}
var hasErrors = issues.Any(i => i.Severity == ProvenanceIssueSeverity.Error);
var hasWarnings = issues.Any(i => i.Severity == ProvenanceIssueSeverity.Warning);
if (hasErrors)
{
return ProvenanceValidationStatus.ValidWithWarnings;
}
if (hasWarnings)
{
return ProvenanceValidationStatus.ValidWithWarnings;
}
return ProvenanceValidationStatus.Valid;
}
}

View File

@@ -0,0 +1,514 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.VexLens.Models;
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// Default implementation of <see cref="ISourceTrustScoreCalculator"/>.
/// Computes multi-dimensional trust scores for VEX sources.
/// </summary>
public sealed class SourceTrustScoreCalculator : ISourceTrustScoreCalculator
{
private readonly ILogger<SourceTrustScoreCalculator> _logger;
private readonly SourceTrustScoreConfiguration _config;
private readonly IAuthorityScoreCalculator _authorityCalculator;
private readonly IAccuracyScoreCalculator _accuracyCalculator;
private readonly ITimelinessScoreCalculator _timelinessCalculator;
private readonly ICoverageScoreCalculator _coverageCalculator;
private readonly IVerificationScoreCalculator _verificationCalculator;
private readonly ISourceTrustScoreCache? _cache;
public SourceTrustScoreCalculator(
ILogger<SourceTrustScoreCalculator> logger,
IOptions<SourceTrustScoreConfiguration> config,
IAuthorityScoreCalculator authorityCalculator,
IAccuracyScoreCalculator accuracyCalculator,
ITimelinessScoreCalculator timelinessCalculator,
ICoverageScoreCalculator coverageCalculator,
IVerificationScoreCalculator verificationCalculator,
ISourceTrustScoreCache? cache = null)
{
_logger = logger;
_config = config.Value;
_authorityCalculator = authorityCalculator;
_accuracyCalculator = accuracyCalculator;
_timelinessCalculator = timelinessCalculator;
_coverageCalculator = coverageCalculator;
_verificationCalculator = verificationCalculator;
_cache = cache;
}
public async Task<VexSourceTrustScore> ComputeScoreAsync(
SourceTrustScoreRequest request,
CancellationToken cancellationToken = default)
{
_logger.LogDebug("Computing trust score for source {SourceId}", request.SourceId);
var warnings = new List<string>();
// Compute individual component scores
var authorityDetails = _authorityCalculator.Calculate(request.Metadata, _config.Authority);
var accuracyDetails = _accuracyCalculator.Calculate(request.HistoricalMetrics, _config.Accuracy);
var timelinessDetails = _timelinessCalculator.Calculate(
request.HistoricalMetrics, request.EvaluationTime, _config.Timeliness);
var coverageDetails = _coverageCalculator.Calculate(request.HistoricalMetrics, _config.Coverage);
var verificationDetails = _verificationCalculator.Calculate(
request.VerificationSummary, _config.Verification);
// Compute component scores
var authorityScore = ComputeAuthorityScore(authorityDetails);
var accuracyScore = ComputeAccuracyScore(accuracyDetails);
var timelinessScore = ComputeTimelinessScore(timelinessDetails);
var coverageScore = ComputeCoverageScore(coverageDetails);
var verificationScore = ComputeVerificationScore(verificationDetails);
// Add warnings for low scores
if (authorityScore < 0.4)
warnings.Add($"Low authority score ({authorityScore:F2}): Source category or tier may limit trust");
if (accuracyScore < 0.4)
warnings.Add($"Low accuracy score ({accuracyScore:F2}): Historical accuracy concerns");
if (timelinessScore < 0.4)
warnings.Add($"Low timeliness score ({timelinessScore:F2}): Slow response or stale data");
if (verificationScore < 0.4)
warnings.Add($"Low verification score ({verificationScore:F2}): Signature or provenance issues");
// Add cold start warning if insufficient data
if (request.HistoricalMetrics.TotalStatements < _config.Accuracy.MinimumStatementsForFullScore)
{
warnings.Add($"Limited history: Only {request.HistoricalMetrics.TotalStatements} statements available");
}
var breakdown = new TrustScoreBreakdown
{
Authority = authorityDetails,
Accuracy = accuracyDetails,
Timeliness = timelinessDetails,
Coverage = coverageDetails,
Verification = verificationDetails
};
var score = new VexSourceTrustScore
{
SourceId = request.SourceId,
SourceName = request.SourceName,
AuthorityScore = authorityScore,
AccuracyScore = accuracyScore,
TimelinessScore = timelinessScore,
CoverageScore = coverageScore,
VerificationScore = verificationScore,
ComputedAt = request.EvaluationTime,
ExpiresAt = request.EvaluationTime + _config.CacheDuration,
StatementCount = request.HistoricalMetrics.TotalStatements,
Breakdown = breakdown,
Warnings = warnings,
Trend = ComputeTrend(request.PreviousScore),
PreviousCompositeScore = request.PreviousScore?.CompositeScore
};
_logger.LogInformation(
"Computed trust score for source {SourceId}: Composite={CompositeScore:F3}, Tier={TrustTier}",
request.SourceId, score.CompositeScore, score.TrustTier);
// Cache the score
if (_cache != null)
{
await _cache.SetAsync(request.SourceId, score, _config.CacheDuration, cancellationToken);
}
return score;
}
public async Task<IReadOnlyList<VexSourceTrustScore>> ComputeScoresBatchAsync(
IEnumerable<SourceTrustScoreRequest> requests,
CancellationToken cancellationToken = default)
{
var results = new List<VexSourceTrustScore>();
foreach (var request in requests)
{
cancellationToken.ThrowIfCancellationRequested();
var score = await ComputeScoreAsync(request, cancellationToken);
results.Add(score);
}
return results;
}
public async Task<VexSourceTrustScore?> GetCachedScoreAsync(
string sourceId,
CancellationToken cancellationToken = default)
{
if (_cache == null)
return null;
return await _cache.GetAsync(sourceId, cancellationToken);
}
public async Task InvalidateCacheAsync(
string sourceId,
CancellationToken cancellationToken = default)
{
if (_cache != null)
{
await _cache.RemoveAsync(sourceId, cancellationToken);
}
}
private double ComputeAuthorityScore(AuthorityScoreDetails details)
{
var score = details.CategoryScore + details.TierAdjustment + details.OfficialBonus;
return Math.Clamp(score, 0.0, 1.0);
}
private double ComputeAccuracyScore(AccuracyScoreDetails details)
{
// Weighted combination of accuracy factors
var confirmationComponent = details.ConfirmationRate * _config.Accuracy.ConfirmationWeight;
var falsePositiveComponent = (1.0 - details.FalsePositiveRate) * _config.Accuracy.FalsePositivePenaltyWeight;
var revocationComponent = (1.0 - details.RevocationRate) * _config.Accuracy.RevocationPenaltyWeight;
var consistencyComponent = details.ConsistencyScore * _config.Accuracy.ConsistencyWeight;
var totalWeight = _config.Accuracy.ConfirmationWeight +
_config.Accuracy.FalsePositivePenaltyWeight +
_config.Accuracy.RevocationPenaltyWeight +
_config.Accuracy.ConsistencyWeight;
var score = (confirmationComponent + falsePositiveComponent + revocationComponent + consistencyComponent) / totalWeight;
return Math.Clamp(score, 0.0, 1.0);
}
private double ComputeTimelinessScore(TimelinessScoreDetails details)
{
var responseComponent = details.ResponseTimeScore * _config.Timeliness.ResponseTimeWeight;
var frequencyComponent = details.UpdateFrequencyScore * _config.Timeliness.UpdateFrequencyWeight;
var freshnessComponent = details.FreshnessScore * _config.Timeliness.FreshnessWeight;
var totalWeight = _config.Timeliness.ResponseTimeWeight +
_config.Timeliness.UpdateFrequencyWeight +
_config.Timeliness.FreshnessWeight;
var score = (responseComponent + frequencyComponent + freshnessComponent) / totalWeight;
return Math.Clamp(score, 0.0, 1.0);
}
private double ComputeCoverageScore(CoverageScoreDetails details)
{
var cveCoverageComponent = details.CveCoverageRatio * _config.Coverage.CveCoverageWeight;
var breadthComponent = details.ProductBreadthScore * _config.Coverage.ProductBreadthWeight;
var completenessComponent = details.CompletenessScore * _config.Coverage.CompletenessWeight;
var totalWeight = _config.Coverage.CveCoverageWeight +
_config.Coverage.ProductBreadthWeight +
_config.Coverage.CompletenessWeight;
var score = (cveCoverageComponent + breadthComponent + completenessComponent) / totalWeight;
return Math.Clamp(score, 0.0, 1.0);
}
private double ComputeVerificationScore(VerificationScoreDetails details)
{
var signatureComponent = details.SignatureScore * _config.Verification.SignatureWeight;
var provenanceComponent = details.ProvenanceScore * _config.Verification.ProvenanceWeight;
var issuerBonus = details.IssuerVerificationBonus;
var totalWeight = _config.Verification.SignatureWeight + _config.Verification.ProvenanceWeight;
var score = (signatureComponent + provenanceComponent) / totalWeight + issuerBonus;
return Math.Clamp(score, 0.0, 1.0);
}
private TrustScoreTrend ComputeTrend(VexSourceTrustScore? previousScore)
{
if (previousScore == null)
return TrustScoreTrend.Stable;
// Threshold for significant change: 5%
const double threshold = 0.05;
var currentComposite = previousScore.CompositeScore; // Will be computed from new scores
var previousComposite = previousScore.CompositeScore;
var delta = currentComposite - previousComposite;
return delta switch
{
> threshold => TrustScoreTrend.Improving,
< -threshold => TrustScoreTrend.Declining,
_ => TrustScoreTrend.Stable
};
}
}
/// <summary>
/// Interface for authority score calculation.
/// </summary>
public interface IAuthorityScoreCalculator
{
AuthorityScoreDetails Calculate(SourceMetadata metadata, AuthorityScoreConfiguration config);
}
/// <summary>
/// Interface for accuracy score calculation.
/// </summary>
public interface IAccuracyScoreCalculator
{
AccuracyScoreDetails Calculate(SourceHistoricalMetrics metrics, AccuracyScoreConfiguration config);
}
/// <summary>
/// Interface for timeliness score calculation.
/// </summary>
public interface ITimelinessScoreCalculator
{
TimelinessScoreDetails Calculate(
SourceHistoricalMetrics metrics,
DateTimeOffset evaluationTime,
TimelinessScoreConfiguration config);
}
/// <summary>
/// Interface for coverage score calculation.
/// </summary>
public interface ICoverageScoreCalculator
{
CoverageScoreDetails Calculate(SourceHistoricalMetrics metrics, CoverageScoreConfiguration config);
}
/// <summary>
/// Interface for verification score calculation.
/// </summary>
public interface IVerificationScoreCalculator
{
VerificationScoreDetails Calculate(SourceVerificationSummary summary, VerificationScoreConfiguration config);
}
/// <summary>
/// Interface for caching source trust scores.
/// </summary>
public interface ISourceTrustScoreCache
{
Task<VexSourceTrustScore?> GetAsync(string sourceId, CancellationToken cancellationToken = default);
Task SetAsync(string sourceId, VexSourceTrustScore score, TimeSpan duration, CancellationToken cancellationToken = default);
Task RemoveAsync(string sourceId, CancellationToken cancellationToken = default);
}
/// <summary>
/// Default implementation of authority score calculator.
/// </summary>
public sealed class AuthorityScoreCalculator : IAuthorityScoreCalculator
{
public AuthorityScoreDetails Calculate(SourceMetadata metadata, AuthorityScoreConfiguration config)
{
// Base score from category
var categoryScore = metadata.Category switch
{
IssuerCategory.Vendor => config.VendorBaseScore,
IssuerCategory.Distributor => config.DistributorBaseScore,
IssuerCategory.Community => config.CommunityBaseScore,
IssuerCategory.Internal => config.InternalBaseScore,
IssuerCategory.Aggregator => config.AggregatorBaseScore,
_ => config.UnknownBaseScore
};
// Trust tier adjustment
var tierAdjustment = metadata.TrustTier switch
{
TrustTier.Authoritative => config.AuthoritativeTierBonus,
TrustTier.Trusted => config.TrustedTierBonus,
TrustTier.Untrusted => -config.UntrustedTierPenalty,
_ => 0.0
};
// Official source bonus
var officialBonus = metadata.IsOfficial ? config.OfficialSourceBonus : 0.0;
return new AuthorityScoreDetails
{
CategoryScore = categoryScore,
TierAdjustment = tierAdjustment,
OfficialBonus = officialBonus,
IssuerCategory = metadata.Category.ToString(),
TrustTier = metadata.TrustTier.ToString(),
IsOfficial = metadata.IsOfficial
};
}
}
/// <summary>
/// Default implementation of accuracy score calculator.
/// </summary>
public sealed class AccuracyScoreCalculator : IAccuracyScoreCalculator
{
public AccuracyScoreDetails Calculate(SourceHistoricalMetrics metrics, AccuracyScoreConfiguration config)
{
// Handle cold start - not enough data
if (metrics.TotalStatements < config.MinimumStatementsForFullScore)
{
// Use grace period score but provide actual rates if available
var scaleFactor = (double)metrics.TotalStatements / config.MinimumStatementsForFullScore;
return new AccuracyScoreDetails
{
ConfirmationRate = metrics.TotalStatements > 0
? (double)metrics.ConfirmedStatements / metrics.TotalStatements
: config.GracePeriodScore,
FalsePositiveRate = metrics.TotalStatements > 0
? (double)metrics.FalsePositiveStatements / metrics.TotalStatements
: 0.0,
RevocationRate = metrics.TotalStatements > 0
? (double)metrics.RevokedStatements / metrics.TotalStatements
: 0.0,
ConsistencyScore = config.GracePeriodScore,
TotalStatements = metrics.TotalStatements,
ConfirmedStatements = metrics.ConfirmedStatements,
RevokedStatements = metrics.RevokedStatements
};
}
// Calculate rates
var confirmationRate = (double)metrics.ConfirmedStatements / metrics.TotalStatements;
var falsePositiveRate = (double)metrics.FalsePositiveStatements / metrics.TotalStatements;
var revocationRate = (double)metrics.RevokedStatements / metrics.TotalStatements;
// Consistency is derived from low revocation and false positive rates
var consistencyScore = Math.Max(0.0, 1.0 - (revocationRate + falsePositiveRate));
return new AccuracyScoreDetails
{
ConfirmationRate = confirmationRate,
FalsePositiveRate = falsePositiveRate,
RevocationRate = revocationRate,
ConsistencyScore = consistencyScore,
TotalStatements = metrics.TotalStatements,
ConfirmedStatements = metrics.ConfirmedStatements,
RevokedStatements = metrics.RevokedStatements
};
}
}
/// <summary>
/// Default implementation of timeliness score calculator.
/// </summary>
public sealed class TimelinessScoreCalculator : ITimelinessScoreCalculator
{
public TimelinessScoreDetails Calculate(
SourceHistoricalMetrics metrics,
DateTimeOffset evaluationTime,
TimelinessScoreConfiguration config)
{
// Response time score (lower is better)
var responseTimeScore = metrics.AverageResponseDays switch
{
<= 0 => 0.5, // No data
var d when d <= config.ExcellentResponseDays => 1.0,
var d when d <= config.GoodResponseDays => 0.8,
var d when d <= config.AcceptableResponseDays => 0.5,
_ => 0.2
};
// Update frequency score
var updateFrequencyScore = metrics.AverageUpdateFrequencyDays switch
{
<= 0 => 0.5, // No data
<= 7 => 1.0,
<= 30 => 0.8,
<= 90 => 0.5,
_ => 0.2
};
// Freshness score
var freshnessPercentage = metrics.TotalStatements > 0
? (double)metrics.FreshStatements / metrics.TotalStatements
: 0.5;
var freshnessScore = freshnessPercentage;
return new TimelinessScoreDetails
{
AverageResponseDays = metrics.AverageResponseDays,
ResponseTimeScore = responseTimeScore,
UpdateFrequencyDays = metrics.AverageUpdateFrequencyDays,
UpdateFrequencyScore = updateFrequencyScore,
FreshnessPercentage = freshnessPercentage,
FreshnessScore = freshnessScore
};
}
}
/// <summary>
/// Default implementation of coverage score calculator.
/// </summary>
public sealed class CoverageScoreCalculator : ICoverageScoreCalculator
{
public CoverageScoreDetails Calculate(SourceHistoricalMetrics metrics, CoverageScoreConfiguration config)
{
// CVE coverage ratio
var cveCoverageRatio = metrics.TotalRelevantCves > 0
? Math.Min(1.0, (double)metrics.CvesWithStatements / metrics.TotalRelevantCves)
: 0.5;
// Product breadth score
var productBreadthScore = Math.Min(1.0, (double)metrics.ProductsCovered / config.MinProductsForFullBreadthScore);
// Completeness score
var completenessPercentage = metrics.TotalStatements > 0
? (double)metrics.CompleteStatements / metrics.TotalStatements
: 0.5;
var completenessScore = completenessPercentage;
return new CoverageScoreDetails
{
CveCoverageRatio = cveCoverageRatio,
ProductCount = metrics.ProductsCovered,
ProductBreadthScore = productBreadthScore,
CompletenessPercentage = completenessPercentage,
CompletenessScore = completenessScore
};
}
}
/// <summary>
/// Default implementation of verification score calculator.
/// </summary>
public sealed class VerificationScoreCalculator : IVerificationScoreCalculator
{
public VerificationScoreDetails Calculate(SourceVerificationSummary summary, VerificationScoreConfiguration config)
{
var totalWithSignature = summary.ValidSignatureCount + summary.InvalidSignatureCount;
var totalStatements = totalWithSignature + summary.NoSignatureCount;
// Signature validity rate
var signatureValidityRate = totalWithSignature > 0
? (double)summary.ValidSignatureCount / totalWithSignature
: 0.0;
// Penalize for having no signatures at all
var signatureScore = totalWithSignature > 0
? signatureValidityRate
: 0.3; // Partial credit for unsigned sources
// Provenance integrity rate
var totalWithProvenance = summary.ValidProvenanceCount + summary.BrokenProvenanceCount;
var provenanceIntegrityRate = totalWithProvenance > 0
? (double)summary.ValidProvenanceCount / totalWithProvenance
: 0.5;
var provenanceScore = provenanceIntegrityRate;
// Issuer verification bonus
var issuerVerificationBonus = summary.IssuerIdentityVerified
? config.IssuerVerificationBonus
: 0.0;
return new VerificationScoreDetails
{
SignatureValidityRate = signatureValidityRate,
SignatureScore = signatureScore,
ProvenanceIntegrityRate = provenanceIntegrityRate,
ProvenanceScore = provenanceScore,
IssuerVerified = summary.IssuerIdentityVerified,
IssuerVerificationBonus = issuerVerificationBonus
};
}
}

View File

@@ -0,0 +1,582 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_4500_0001_0002 - VEX Trust Scoring Framework
// Tasks: TRUST-011 (time-based decay), TRUST-012 (recency bonus),
// TRUST-013 (revocation handling), TRUST-014 (update history)
using System.Collections.Immutable;
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// Calculates time-based trust decay for VEX statements and sources.
/// Implements exponential decay with configurable half-life and floor.
/// </summary>
public interface ITrustDecayCalculator
{
/// <summary>
/// Calculates the decay factor for a statement based on age.
/// </summary>
double CalculateDecayFactor(DateTimeOffset statementTimestamp, DateTimeOffset evaluationTime);
/// <summary>
/// Calculates recency bonus for recently updated statements.
/// </summary>
double CalculateRecencyBonus(DateTimeOffset lastUpdateTime, DateTimeOffset evaluationTime);
/// <summary>
/// Adjusts trust score based on revocation history.
/// </summary>
double ApplyRevocationPenalty(double baseScore, RevocationHistory history);
/// <summary>
/// Calculates effective trust score with all time-based adjustments.
/// </summary>
TrustDecayResult CalculateEffectiveScore(
double baseScore,
StatementTimingContext context,
DateTimeOffset evaluationTime);
}
/// <summary>
/// Configuration for trust decay calculation.
/// </summary>
public sealed record TrustDecayConfiguration
{
/// <summary>
/// Half-life for decay in days (score halves after this period).
/// Default: 90 days.
/// </summary>
public double HalfLifeDays { get; init; } = 90.0;
/// <summary>
/// Minimum decay factor (floor).
/// Default: 0.35 (score never drops below 35% of original).
/// </summary>
public double DecayFloor { get; init; } = 0.35;
/// <summary>
/// Days within which a statement is considered "fresh" for recency bonus.
/// </summary>
public double FreshnessDays { get; init; } = 7.0;
/// <summary>
/// Maximum recency bonus for very fresh statements.
/// </summary>
public double MaxRecencyBonus { get; init; } = 0.15;
/// <summary>
/// Penalty per revocation in source history.
/// </summary>
public double RevocationPenaltyPerInstance { get; init; } = 0.02;
/// <summary>
/// Maximum cumulative revocation penalty.
/// </summary>
public double MaxRevocationPenalty { get; init; } = 0.30;
/// <summary>
/// Bonus for sources that have never revoked statements.
/// </summary>
public double NoRevocationBonus { get; init; } = 0.05;
/// <summary>
/// Days after which revocations stop counting (forgiveness period).
/// </summary>
public double RevocationForgivenessDays { get; init; } = 365.0;
/// <summary>
/// Creates default configuration.
/// </summary>
public static TrustDecayConfiguration Default => new();
}
/// <summary>
/// Context for statement timing used in decay calculation.
/// </summary>
public sealed record StatementTimingContext
{
/// <summary>
/// When the statement was originally issued.
/// </summary>
public required DateTimeOffset IssuedAt { get; init; }
/// <summary>
/// When the statement was last updated.
/// </summary>
public DateTimeOffset? LastUpdatedAt { get; init; }
/// <summary>
/// Revocation history for the source.
/// </summary>
public RevocationHistory? RevocationHistory { get; init; }
}
/// <summary>
/// History of statement revocations for a source.
/// </summary>
public sealed record RevocationHistory
{
/// <summary>
/// Total number of revocations in history.
/// </summary>
public required int TotalRevocations { get; init; }
/// <summary>
/// Revocations in the last year.
/// </summary>
public required int RevocationsLastYear { get; init; }
/// <summary>
/// Revocations in the last 90 days.
/// </summary>
public required int RevocationsLast90Days { get; init; }
/// <summary>
/// Individual revocation events with timestamps.
/// </summary>
public ImmutableArray<RevocationEvent> Events { get; init; } = [];
/// <summary>
/// Total statements from source (for rate calculation).
/// </summary>
public required int TotalStatements { get; init; }
}
/// <summary>
/// A single revocation event.
/// </summary>
public sealed record RevocationEvent
{
/// <summary>
/// Statement that was revoked.
/// </summary>
public required string StatementId { get; init; }
/// <summary>
/// When the revocation occurred.
/// </summary>
public required DateTimeOffset RevokedAt { get; init; }
/// <summary>
/// Reason for revocation.
/// </summary>
public string? Reason { get; init; }
/// <summary>
/// Severity of the revocation (false positive vs minor correction).
/// </summary>
public RevocationSeverity Severity { get; init; } = RevocationSeverity.Minor;
}
/// <summary>
/// Severity of a revocation.
/// </summary>
public enum RevocationSeverity
{
/// <summary>Minor correction or clarification.</summary>
Minor = 0,
/// <summary>Significant error in original statement.</summary>
Significant = 1,
/// <summary>Critical false positive or security impact.</summary>
Critical = 2
}
/// <summary>
/// Result of trust decay calculation.
/// </summary>
public sealed record TrustDecayResult
{
/// <summary>
/// Original base score before adjustments.
/// </summary>
public required double BaseScore { get; init; }
/// <summary>
/// Decay factor applied (0.0-1.0).
/// </summary>
public required double DecayFactor { get; init; }
/// <summary>
/// Recency bonus applied.
/// </summary>
public required double RecencyBonus { get; init; }
/// <summary>
/// Revocation penalty applied.
/// </summary>
public required double RevocationPenalty { get; init; }
/// <summary>
/// Final effective score after all adjustments.
/// </summary>
public required double EffectiveScore { get; init; }
/// <summary>
/// Age of the statement in days.
/// </summary>
public required double AgeDays { get; init; }
/// <summary>
/// Explanation of the calculation.
/// </summary>
public required string Explanation { get; init; }
}
/// <summary>
/// Default implementation of trust decay calculator.
/// </summary>
public sealed class TrustDecayCalculator : ITrustDecayCalculator
{
private readonly TrustDecayConfiguration _config;
public TrustDecayCalculator(TrustDecayConfiguration? config = null)
{
_config = config ?? TrustDecayConfiguration.Default;
}
public double CalculateDecayFactor(
DateTimeOffset statementTimestamp,
DateTimeOffset evaluationTime)
{
var ageDays = (evaluationTime - statementTimestamp).TotalDays;
if (ageDays <= 0)
{
return 1.0; // No decay for future or current statements
}
// Exponential decay: factor = max(floor, e^(-ln(2) * age / half_life))
var decayExponent = -Math.Log(2) * ageDays / _config.HalfLifeDays;
var decayFactor = Math.Exp(decayExponent);
return Math.Max(_config.DecayFloor, decayFactor);
}
public double CalculateRecencyBonus(
DateTimeOffset lastUpdateTime,
DateTimeOffset evaluationTime)
{
var daysSinceUpdate = (evaluationTime - lastUpdateTime).TotalDays;
if (daysSinceUpdate < 0)
{
return _config.MaxRecencyBonus; // Future update = max bonus
}
if (daysSinceUpdate > _config.FreshnessDays)
{
return 0.0; // No bonus for stale statements
}
// Linear decay of bonus within freshness window
var freshnessRatio = 1.0 - (daysSinceUpdate / _config.FreshnessDays);
return _config.MaxRecencyBonus * freshnessRatio;
}
public double ApplyRevocationPenalty(double baseScore, RevocationHistory history)
{
if (history.TotalStatements == 0)
{
return baseScore; // No history to evaluate
}
// No revocations = bonus
if (history.TotalRevocations == 0)
{
return Math.Min(1.0, baseScore + _config.NoRevocationBonus);
}
// Calculate revocation rate
var revocationRate = (double)history.RevocationsLastYear / history.TotalStatements;
// Base penalty on recent revocations (weighted more heavily)
var recentPenalty = history.RevocationsLast90Days * _config.RevocationPenaltyPerInstance * 2.0;
var yearPenalty = (history.RevocationsLastYear - history.RevocationsLast90Days) *
_config.RevocationPenaltyPerInstance;
var totalPenalty = Math.Min(_config.MaxRevocationPenalty, recentPenalty + yearPenalty);
return Math.Max(0.0, baseScore - totalPenalty);
}
public TrustDecayResult CalculateEffectiveScore(
double baseScore,
StatementTimingContext context,
DateTimeOffset evaluationTime)
{
// Calculate age and decay
var ageDays = (evaluationTime - context.IssuedAt).TotalDays;
var decayFactor = CalculateDecayFactor(context.IssuedAt, evaluationTime);
// Calculate recency bonus
var updateTime = context.LastUpdatedAt ?? context.IssuedAt;
var recencyBonus = CalculateRecencyBonus(updateTime, evaluationTime);
// Calculate revocation penalty
var revocationPenalty = 0.0;
if (context.RevocationHistory != null)
{
var scoreAfterRevocation = ApplyRevocationPenalty(baseScore, context.RevocationHistory);
revocationPenalty = baseScore - scoreAfterRevocation;
}
// Apply all adjustments
// Formula: effective = (base * decay) + recencyBonus - revocationPenalty
var decayedScore = baseScore * decayFactor;
var effectiveScore = Math.Clamp(decayedScore + recencyBonus - revocationPenalty, 0.0, 1.0);
// Build explanation
var explanationParts = new List<string>
{
$"Base score: {baseScore:F3}",
$"Age: {ageDays:F1} days",
$"Decay factor: {decayFactor:F3} (half-life: {_config.HalfLifeDays} days)"
};
if (recencyBonus > 0)
{
explanationParts.Add($"Recency bonus: +{recencyBonus:F3}");
}
if (revocationPenalty > 0)
{
explanationParts.Add($"Revocation penalty: -{revocationPenalty:F3}");
}
explanationParts.Add($"Effective score: {effectiveScore:F3}");
return new TrustDecayResult
{
BaseScore = baseScore,
DecayFactor = decayFactor,
RecencyBonus = recencyBonus,
RevocationPenalty = revocationPenalty,
EffectiveScore = effectiveScore,
AgeDays = ageDays,
Explanation = string.Join("; ", explanationParts)
};
}
}
/// <summary>
/// Tracks statement update history for a source.
/// </summary>
public interface IStatementHistoryTracker
{
/// <summary>
/// Records a new statement.
/// </summary>
Task RecordStatementAsync(
string sourceId,
string statementId,
DateTimeOffset issuedAt,
CancellationToken cancellationToken = default);
/// <summary>
/// Records a statement update.
/// </summary>
Task RecordUpdateAsync(
string sourceId,
string statementId,
DateTimeOffset updatedAt,
string? updateReason = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Records a statement revocation.
/// </summary>
Task RecordRevocationAsync(
string sourceId,
string statementId,
DateTimeOffset revokedAt,
string? reason = null,
RevocationSeverity severity = RevocationSeverity.Minor,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets revocation history for a source.
/// </summary>
Task<RevocationHistory> GetRevocationHistoryAsync(
string sourceId,
DateTimeOffset evaluationTime,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the last update time for a statement.
/// </summary>
Task<DateTimeOffset?> GetLastUpdateTimeAsync(
string sourceId,
string statementId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// In-memory implementation of statement history tracker.
/// </summary>
public sealed class InMemoryStatementHistoryTracker : IStatementHistoryTracker
{
private readonly Dictionary<string, List<StatementHistoryEntry>> _history = new();
private readonly object _lock = new();
public Task RecordStatementAsync(
string sourceId,
string statementId,
DateTimeOffset issuedAt,
CancellationToken cancellationToken = default)
{
lock (_lock)
{
if (!_history.TryGetValue(sourceId, out var entries))
{
entries = [];
_history[sourceId] = entries;
}
entries.Add(new StatementHistoryEntry
{
StatementId = statementId,
EventType = HistoryEventType.Created,
Timestamp = issuedAt
});
}
return Task.CompletedTask;
}
public Task RecordUpdateAsync(
string sourceId,
string statementId,
DateTimeOffset updatedAt,
string? updateReason = null,
CancellationToken cancellationToken = default)
{
lock (_lock)
{
if (!_history.TryGetValue(sourceId, out var entries))
{
entries = [];
_history[sourceId] = entries;
}
entries.Add(new StatementHistoryEntry
{
StatementId = statementId,
EventType = HistoryEventType.Updated,
Timestamp = updatedAt,
Reason = updateReason
});
}
return Task.CompletedTask;
}
public Task RecordRevocationAsync(
string sourceId,
string statementId,
DateTimeOffset revokedAt,
string? reason = null,
RevocationSeverity severity = RevocationSeverity.Minor,
CancellationToken cancellationToken = default)
{
lock (_lock)
{
if (!_history.TryGetValue(sourceId, out var entries))
{
entries = [];
_history[sourceId] = entries;
}
entries.Add(new StatementHistoryEntry
{
StatementId = statementId,
EventType = HistoryEventType.Revoked,
Timestamp = revokedAt,
Reason = reason,
Severity = severity
});
}
return Task.CompletedTask;
}
public Task<RevocationHistory> GetRevocationHistoryAsync(
string sourceId,
DateTimeOffset evaluationTime,
CancellationToken cancellationToken = default)
{
lock (_lock)
{
if (!_history.TryGetValue(sourceId, out var entries))
{
return Task.FromResult(new RevocationHistory
{
TotalRevocations = 0,
RevocationsLastYear = 0,
RevocationsLast90Days = 0,
TotalStatements = 0
});
}
var revocations = entries.Where(e => e.EventType == HistoryEventType.Revoked).ToList();
var totalStatements = entries.Count(e => e.EventType == HistoryEventType.Created);
var oneYearAgo = evaluationTime.AddDays(-365);
var ninetyDaysAgo = evaluationTime.AddDays(-90);
return Task.FromResult(new RevocationHistory
{
TotalRevocations = revocations.Count,
RevocationsLastYear = revocations.Count(r => r.Timestamp >= oneYearAgo),
RevocationsLast90Days = revocations.Count(r => r.Timestamp >= ninetyDaysAgo),
TotalStatements = totalStatements,
Events = revocations.Select(r => new RevocationEvent
{
StatementId = r.StatementId,
RevokedAt = r.Timestamp,
Reason = r.Reason,
Severity = r.Severity
}).ToImmutableArray()
});
}
}
public Task<DateTimeOffset?> GetLastUpdateTimeAsync(
string sourceId,
string statementId,
CancellationToken cancellationToken = default)
{
lock (_lock)
{
if (!_history.TryGetValue(sourceId, out var entries))
{
return Task.FromResult<DateTimeOffset?>(null);
}
var lastUpdate = entries
.Where(e => e.StatementId == statementId &&
(e.EventType == HistoryEventType.Created || e.EventType == HistoryEventType.Updated))
.OrderByDescending(e => e.Timestamp)
.FirstOrDefault();
return Task.FromResult<DateTimeOffset?>(lastUpdate?.Timestamp);
}
}
private sealed class StatementHistoryEntry
{
public required string StatementId { get; init; }
public required HistoryEventType EventType { get; init; }
public required DateTimeOffset Timestamp { get; init; }
public string? Reason { get; init; }
public RevocationSeverity Severity { get; init; }
}
private enum HistoryEventType
{
Created,
Updated,
Revoked
}
}

View File

@@ -0,0 +1,298 @@
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// Default implementation of <see cref="ITrustDecayService"/>.
/// Applies time-based trust decay, recency bonuses, and revocation penalties.
/// </summary>
public sealed class TrustDecayService : ITrustDecayService
{
public DecayResult ApplyDecay(
double baseScore,
DateTimeOffset statementTimestamp,
DecayContext context)
{
var age = context.EvaluationTime - statementTimestamp;
var ageDays = age.TotalDays;
var config = context.Configuration;
var (decayFactor, category) = CalculateDecayFactor(age, config);
// Reduce decay for statements with updates
if (context.HasUpdates && context.UpdateCount > 0)
{
// Each update reduces effective age by 10%, up to 50% reduction
var updateReduction = Math.Min(0.5, context.UpdateCount * 0.1);
decayFactor = Math.Min(1.0, decayFactor + (1.0 - decayFactor) * updateReduction);
}
var decayedScore = baseScore * decayFactor;
return new DecayResult
{
BaseScore = baseScore,
DecayFactor = decayFactor,
DecayedScore = decayedScore,
AgeDays = ageDays,
Category = category
};
}
public double CalculateRecencyBonus(
DateTimeOffset lastUpdateTimestamp,
RecencyBonusContext context)
{
var age = context.EvaluationTime - lastUpdateTimestamp;
if (age > context.RecencyWindow)
{
return 0.0;
}
// Linear decrease from max bonus to 0 over the recency window
var ratio = 1.0 - (age.TotalSeconds / context.RecencyWindow.TotalSeconds);
return context.MaxBonus * ratio;
}
public RevocationImpact CalculateRevocationImpact(
RevocationInfo revocation,
RevocationContext context)
{
if (!revocation.IsRevoked)
{
return new RevocationImpact
{
ShouldExclude = false,
Penalty = 0.0,
Explanation = "Statement is not revoked",
RecommendedAction = RevocationAction.None
};
}
// Determine impact based on revocation type
return revocation.RevocationType switch
{
RevocationType.Superseded when revocation.WasSuperseded => new RevocationImpact
{
ShouldExclude = true,
Penalty = context.SupersededPenalty,
Explanation = $"Statement superseded by {revocation.SupersededBy ?? "newer statement"}",
RecommendedAction = RevocationAction.Replace
},
RevocationType.Correction => new RevocationImpact
{
ShouldExclude = false,
Penalty = context.CorrectionPenalty,
Explanation = $"Statement corrected: {revocation.RevocationReason ?? "unspecified reason"}",
RecommendedAction = RevocationAction.Penalize
},
RevocationType.Withdrawn => new RevocationImpact
{
ShouldExclude = true,
Penalty = context.RevocationPenalty,
Explanation = $"Statement withdrawn: {revocation.RevocationReason ?? "unspecified reason"}",
RecommendedAction = RevocationAction.Exclude
},
RevocationType.Expired => new RevocationImpact
{
ShouldExclude = false,
Penalty = context.RevocationPenalty * 0.5,
Explanation = "Statement expired and was not renewed",
RecommendedAction = RevocationAction.Review
},
RevocationType.SourceRevoked => new RevocationImpact
{
ShouldExclude = true,
Penalty = context.RevocationPenalty,
Explanation = "Source has been revoked",
RecommendedAction = RevocationAction.Exclude
},
_ => new RevocationImpact
{
ShouldExclude = false,
Penalty = context.RevocationPenalty * 0.75,
Explanation = $"Statement revoked: {revocation.RevocationReason ?? "unknown reason"}",
RecommendedAction = RevocationAction.Review
}
};
}
public EffectiveTrustScore GetEffectiveScore(
double baseScore,
TrustScoreFactors factors,
DateTimeOffset evaluationTime)
{
var adjustments = new List<TrustAdjustment>();
var shouldExclude = false;
// Apply decay
var decayConfig = factors.DecayConfiguration ?? DecayConfiguration.CreateDefault();
var decayContext = new DecayContext
{
EvaluationTime = evaluationTime,
Configuration = decayConfig,
HasUpdates = factors.UpdateCount > 0,
UpdateCount = factors.UpdateCount
};
var decayResult = ApplyDecay(baseScore, factors.StatementTimestamp, decayContext);
adjustments.Add(new TrustAdjustment
{
Type = TrustAdjustmentType.Decay,
Amount = decayResult.DecayedScore - baseScore,
Reason = $"Time-based decay (age: {decayResult.AgeDays:F1} days, category: {decayResult.Category})"
});
var effectiveScore = decayResult.DecayedScore;
// Apply recency bonus if recently updated
var recencyBonus = 0.0;
if (factors.LastUpdateTimestamp.HasValue)
{
var recencyContext = new RecencyBonusContext
{
EvaluationTime = evaluationTime
};
recencyBonus = CalculateRecencyBonus(factors.LastUpdateTimestamp.Value, recencyContext);
if (recencyBonus > 0)
{
effectiveScore += recencyBonus;
adjustments.Add(new TrustAdjustment
{
Type = TrustAdjustmentType.RecencyBonus,
Amount = recencyBonus,
Reason = "Recently updated statement"
});
}
}
// Apply update bonus
if (factors.UpdateCount > 1)
{
var updateBonus = Math.Min(0.05, factors.UpdateCount * 0.01);
effectiveScore += updateBonus;
adjustments.Add(new TrustAdjustment
{
Type = TrustAdjustmentType.UpdateBonus,
Amount = updateBonus,
Reason = $"Statement has been updated {factors.UpdateCount} times"
});
}
// Apply revocation penalty
var revocationPenalty = 0.0;
if (factors.Revocation != null)
{
var revocationContext = new RevocationContext
{
EvaluationTime = evaluationTime
};
var revocationImpact = CalculateRevocationImpact(factors.Revocation, revocationContext);
if (revocationImpact.ShouldExclude)
{
shouldExclude = true;
}
revocationPenalty = revocationImpact.Penalty;
effectiveScore -= revocationPenalty;
adjustments.Add(new TrustAdjustment
{
Type = TrustAdjustmentType.RevocationPenalty,
Amount = -revocationPenalty,
Reason = revocationImpact.Explanation
});
}
// Clamp final score
effectiveScore = Math.Clamp(effectiveScore, 0.0, 1.0);
return new EffectiveTrustScore
{
BaseScore = baseScore,
EffectiveScore = effectiveScore,
DecayFactor = decayResult.DecayFactor,
RecencyBonus = recencyBonus,
RevocationPenalty = revocationPenalty,
ShouldExclude = shouldExclude,
StalenessCategory = decayResult.Category,
Adjustments = adjustments
};
}
private (double Factor, StalenessCategory Category) CalculateDecayFactor(
TimeSpan age,
DecayConfiguration config)
{
if (age <= TimeSpan.Zero)
{
return (1.0, StalenessCategory.Fresh);
}
if (age < config.FreshThreshold)
{
return (1.0, StalenessCategory.Fresh);
}
if (age < config.RecentThreshold)
{
var factor = CalculateCurveValue(
age, config.FreshThreshold, config.RecentThreshold,
1.0, 0.9, config.CurveType);
return (factor, StalenessCategory.Recent);
}
if (age < config.StaleThreshold)
{
var factor = CalculateCurveValue(
age, config.RecentThreshold, config.StaleThreshold,
0.9, 0.7, config.CurveType);
return (factor, StalenessCategory.Aging);
}
if (age < config.ExpiredThreshold)
{
var factor = CalculateCurveValue(
age, config.StaleThreshold, config.ExpiredThreshold,
0.7, config.MinDecayFactor, config.CurveType);
return (factor, StalenessCategory.Stale);
}
return (config.MinDecayFactor, StalenessCategory.Expired);
}
private static double CalculateCurveValue(
TimeSpan current,
TimeSpan start,
TimeSpan end,
double startValue,
double endValue,
DecayCurveType curveType)
{
var progress = (current - start).TotalSeconds / (end - start).TotalSeconds;
progress = Math.Clamp(progress, 0.0, 1.0);
return curveType switch
{
DecayCurveType.Linear =>
startValue + (endValue - startValue) * progress,
DecayCurveType.Exponential =>
startValue * Math.Pow(endValue / startValue, progress),
DecayCurveType.Step =>
progress < 0.5 ? startValue : endValue,
_ => startValue + (endValue - startValue) * progress
};
}
}

View File

@@ -0,0 +1,310 @@
namespace StellaOps.VexLens.Trust.SourceTrust;
/// <summary>
/// Represents a multi-dimensional trust score for a VEX source.
/// Combines authority, accuracy, timeliness, coverage, and verification factors
/// into a composite score used for consensus weighting and policy decisions.
/// </summary>
public sealed record VexSourceTrustScore
{
/// <summary>
/// Unique identifier of the VEX source being scored.
/// </summary>
public required string SourceId { get; init; }
/// <summary>
/// Human-readable name of the source.
/// </summary>
public required string SourceName { get; init; }
/// <summary>
/// Authority score (0.0 - 1.0): Issuer reputation and category weight.
/// Based on: issuer category (vendor/distributor/community), trust tier, official status.
/// </summary>
public required double AuthorityScore { get; init; }
/// <summary>
/// Accuracy score (0.0 - 1.0): Historical correctness of statements.
/// Based on: confirmation rate, false positive rate, revocation rate, consistency.
/// </summary>
public required double AccuracyScore { get; init; }
/// <summary>
/// Timeliness score (0.0 - 1.0): Speed of response to new vulnerabilities.
/// Based on: average time-to-publish, update frequency, freshness of statements.
/// </summary>
public required double TimelinessScore { get; init; }
/// <summary>
/// Coverage score (0.0 - 1.0): Completeness of vulnerability coverage.
/// Based on: CVE coverage ratio, product breadth, statement completeness.
/// </summary>
public required double CoverageScore { get; init; }
/// <summary>
/// Verification score (0.0 - 1.0): Signature and provenance verification status.
/// Based on: signature validity rate, provenance chain integrity, issuer identity verification.
/// </summary>
public required double VerificationScore { get; init; }
/// <summary>
/// Computed composite score using weighted combination of component scores.
/// </summary>
public double CompositeScore =>
AuthorityScore * TrustScoreWeights.AuthorityWeight +
AccuracyScore * TrustScoreWeights.AccuracyWeight +
TimelinessScore * TrustScoreWeights.TimelinessWeight +
CoverageScore * TrustScoreWeights.CoverageWeight +
VerificationScore * TrustScoreWeights.VerificationWeight;
/// <summary>
/// Trust tier derived from composite score.
/// </summary>
public SourceTrustTier TrustTier => CompositeScore switch
{
>= 0.8 => SourceTrustTier.High,
>= 0.6 => SourceTrustTier.Medium,
>= 0.4 => SourceTrustTier.Low,
_ => SourceTrustTier.Untrusted
};
/// <summary>
/// When this score was computed.
/// </summary>
public required DateTimeOffset ComputedAt { get; init; }
/// <summary>
/// When this score expires and should be recomputed.
/// </summary>
public required DateTimeOffset ExpiresAt { get; init; }
/// <summary>
/// Number of statements evaluated to compute this score.
/// </summary>
public required int StatementCount { get; init; }
/// <summary>
/// Detailed breakdown of how each component score was calculated.
/// </summary>
public required TrustScoreBreakdown Breakdown { get; init; }
/// <summary>
/// Any warnings or notes about the score computation.
/// </summary>
public required IReadOnlyList<string> Warnings { get; init; }
/// <summary>
/// Trend direction compared to previous score.
/// </summary>
public TrustScoreTrend Trend { get; init; } = TrustScoreTrend.Stable;
/// <summary>
/// Previous composite score for trend calculation.
/// </summary>
public double? PreviousCompositeScore { get; init; }
}
/// <summary>
/// Trust tier classification based on composite score.
/// </summary>
public enum SourceTrustTier
{
/// <summary>Composite score below 0.4 - source should be treated with high caution.</summary>
Untrusted = 0,
/// <summary>Composite score 0.4-0.6 - source has some reliability issues.</summary>
Low = 1,
/// <summary>Composite score 0.6-0.8 - source is generally reliable.</summary>
Medium = 2,
/// <summary>Composite score 0.8+ - source is highly trustworthy.</summary>
High = 3
}
/// <summary>
/// Direction of trust score change over time.
/// </summary>
public enum TrustScoreTrend
{
/// <summary>Score has decreased significantly.</summary>
Declining = -1,
/// <summary>Score has remained relatively stable.</summary>
Stable = 0,
/// <summary>Score has increased significantly.</summary>
Improving = 1
}
/// <summary>
/// Default weights for composite score calculation.
/// </summary>
public static class TrustScoreWeights
{
/// <summary>Weight for authority score component.</summary>
public const double AuthorityWeight = 0.25;
/// <summary>Weight for accuracy score component (highest weight - historical correctness matters most).</summary>
public const double AccuracyWeight = 0.30;
/// <summary>Weight for timeliness score component.</summary>
public const double TimelinessWeight = 0.15;
/// <summary>Weight for coverage score component.</summary>
public const double CoverageWeight = 0.10;
/// <summary>Weight for verification score component.</summary>
public const double VerificationWeight = 0.20;
static TrustScoreWeights()
{
// Validate weights sum to 1.0
var sum = AuthorityWeight + AccuracyWeight + TimelinessWeight + CoverageWeight + VerificationWeight;
if (Math.Abs(sum - 1.0) > 0.001)
{
throw new InvalidOperationException($"Trust score weights must sum to 1.0, but sum to {sum}");
}
}
}
/// <summary>
/// Detailed breakdown of trust score computation.
/// </summary>
public sealed record TrustScoreBreakdown
{
/// <summary>Details of authority score calculation.</summary>
public required AuthorityScoreDetails Authority { get; init; }
/// <summary>Details of accuracy score calculation.</summary>
public required AccuracyScoreDetails Accuracy { get; init; }
/// <summary>Details of timeliness score calculation.</summary>
public required TimelinessScoreDetails Timeliness { get; init; }
/// <summary>Details of coverage score calculation.</summary>
public required CoverageScoreDetails Coverage { get; init; }
/// <summary>Details of verification score calculation.</summary>
public required VerificationScoreDetails Verification { get; init; }
}
/// <summary>
/// Details of authority score calculation.
/// </summary>
public sealed record AuthorityScoreDetails
{
/// <summary>Base score from issuer category.</summary>
public required double CategoryScore { get; init; }
/// <summary>Adjustment for trust tier.</summary>
public required double TierAdjustment { get; init; }
/// <summary>Bonus for official vendor status.</summary>
public required double OfficialBonus { get; init; }
/// <summary>Issuer category (vendor, distributor, community, etc.).</summary>
public required string IssuerCategory { get; init; }
/// <summary>Trust tier of the issuer.</summary>
public required string TrustTier { get; init; }
/// <summary>Whether the source is an official vendor source.</summary>
public required bool IsOfficial { get; init; }
}
/// <summary>
/// Details of accuracy score calculation.
/// </summary>
public sealed record AccuracyScoreDetails
{
/// <summary>Rate of statements confirmed by other sources.</summary>
public required double ConfirmationRate { get; init; }
/// <summary>Rate of statements that were false positives.</summary>
public required double FalsePositiveRate { get; init; }
/// <summary>Rate of statements that were revoked or corrected.</summary>
public required double RevocationRate { get; init; }
/// <summary>Consistency of statements over time.</summary>
public required double ConsistencyScore { get; init; }
/// <summary>Total statements evaluated.</summary>
public required int TotalStatements { get; init; }
/// <summary>Statements confirmed by consensus.</summary>
public required int ConfirmedStatements { get; init; }
/// <summary>Statements revoked or corrected.</summary>
public required int RevokedStatements { get; init; }
}
/// <summary>
/// Details of timeliness score calculation.
/// </summary>
public sealed record TimelinessScoreDetails
{
/// <summary>Average days from CVE publication to VEX statement.</summary>
public required double AverageResponseDays { get; init; }
/// <summary>Score based on response time.</summary>
public required double ResponseTimeScore { get; init; }
/// <summary>Average days between statement updates.</summary>
public required double UpdateFrequencyDays { get; init; }
/// <summary>Score based on update frequency.</summary>
public required double UpdateFrequencyScore { get; init; }
/// <summary>Percentage of statements that are fresh (not stale).</summary>
public required double FreshnessPercentage { get; init; }
/// <summary>Score based on freshness.</summary>
public required double FreshnessScore { get; init; }
}
/// <summary>
/// Details of coverage score calculation.
/// </summary>
public sealed record CoverageScoreDetails
{
/// <summary>Ratio of CVEs covered vs total relevant CVEs.</summary>
public required double CveCoverageRatio { get; init; }
/// <summary>Number of unique products covered.</summary>
public required int ProductCount { get; init; }
/// <summary>Score based on product breadth.</summary>
public required double ProductBreadthScore { get; init; }
/// <summary>Percentage of statements with complete information.</summary>
public required double CompletenessPercentage { get; init; }
/// <summary>Score based on statement completeness.</summary>
public required double CompletenessScore { get; init; }
}
/// <summary>
/// Details of verification score calculation.
/// </summary>
public sealed record VerificationScoreDetails
{
/// <summary>Percentage of statements with valid signatures.</summary>
public required double SignatureValidityRate { get; init; }
/// <summary>Score based on signature validity.</summary>
public required double SignatureScore { get; init; }
/// <summary>Percentage of statements with valid provenance chains.</summary>
public required double ProvenanceIntegrityRate { get; init; }
/// <summary>Score based on provenance integrity.</summary>
public required double ProvenanceScore { get; init; }
/// <summary>Whether the issuer identity has been verified.</summary>
public required bool IssuerVerified { get; init; }
/// <summary>Bonus for verified issuer identity.</summary>
public required double IssuerVerificationBonus { get; init; }
}