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:
692
src/VexLens/StellaOps.VexLens/Api/TrustScorecardApiModels.cs
Normal file
692
src/VexLens/StellaOps.VexLens/Api/TrustScorecardApiModels.cs
Normal 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);
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ public sealed record NormalizedVexDocument(
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<VexSourceFormat>))]
|
||||
public enum VexSourceFormat
|
||||
{
|
||||
[JsonPropertyName("UNKNOWN")]
|
||||
Unknown,
|
||||
|
||||
[JsonPropertyName("OPENVEX")]
|
||||
OpenVex,
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ public sealed record NormalizedVexDocument
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<VexSourceFormat>))]
|
||||
public enum VexSourceFormat
|
||||
{
|
||||
[JsonPropertyName("UNKNOWN")]
|
||||
Unknown,
|
||||
|
||||
[JsonPropertyName("OPENVEX")]
|
||||
OpenVex,
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user