work work hard work
This commit is contained in:
@@ -270,7 +270,7 @@ internal static class SmartDiffEndpoints
|
||||
return new MaterialChangeDto
|
||||
{
|
||||
VulnId = change.FindingKey.VulnId,
|
||||
Purl = change.FindingKey.Purl,
|
||||
Purl = change.FindingKey.ComponentPurl,
|
||||
HasMaterialChange = change.HasMaterialChange,
|
||||
PriorityScore = change.PriorityScore,
|
||||
PreviousStateHash = change.PreviousStateHash,
|
||||
@@ -284,7 +284,7 @@ internal static class SmartDiffEndpoints
|
||||
PreviousValue = c.PreviousValue,
|
||||
CurrentValue = c.CurrentValue,
|
||||
Weight = c.Weight,
|
||||
SubType = c.SubType
|
||||
SubType = null
|
||||
}).ToImmutableArray()
|
||||
};
|
||||
}
|
||||
@@ -295,7 +295,7 @@ internal static class SmartDiffEndpoints
|
||||
{
|
||||
CandidateId = candidate.CandidateId,
|
||||
VulnId = candidate.FindingKey.VulnId,
|
||||
Purl = candidate.FindingKey.Purl,
|
||||
Purl = candidate.FindingKey.ComponentPurl,
|
||||
ImageDigest = candidate.ImageDigest,
|
||||
SuggestedStatus = candidate.SuggestedStatus.ToString().ToLowerInvariant(),
|
||||
Justification = MapJustificationToString(candidate.Justification),
|
||||
@@ -344,7 +344,7 @@ public sealed class MaterialChangeDto
|
||||
public required string VulnId { get; init; }
|
||||
public required string Purl { get; init; }
|
||||
public bool HasMaterialChange { get; init; }
|
||||
public int PriorityScore { get; init; }
|
||||
public double PriorityScore { get; init; }
|
||||
public required string PreviousStateHash { get; init; }
|
||||
public required string CurrentStateHash { get; init; }
|
||||
public required ImmutableArray<DetectedChangeDto> Changes { get; init; }
|
||||
|
||||
@@ -4,11 +4,15 @@ using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Scanner.Core.Utility;
|
||||
using StellaOps.Scanner.Storage.Models;
|
||||
using StellaOps.Scanner.Storage.Services;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
|
||||
@@ -19,7 +23,12 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||
private const string DefaultTenant = "default";
|
||||
private const string Source = "scanner.webservice";
|
||||
|
||||
private static readonly Guid TenantNamespace = new("ac8f2b54-72ea-43fa-9c3b-6a87ebd2d48a");
|
||||
private static readonly Guid ExecutionNamespace = new("f0b1f40c-0f04-447b-a102-50de3ff79a33");
|
||||
private static readonly Guid ManifestNamespace = new("d9c8858c-e2a4-47d6-bf0f-1e76d2865bea");
|
||||
|
||||
private readonly IPlatformEventPublisher _publisher;
|
||||
private readonly IClassificationChangeTracker _classificationChangeTracker;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<ReportEventDispatcher> _logger;
|
||||
private readonly string[] _apiBaseSegments;
|
||||
@@ -32,11 +41,13 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||
|
||||
public ReportEventDispatcher(
|
||||
IPlatformEventPublisher publisher,
|
||||
IClassificationChangeTracker classificationChangeTracker,
|
||||
IOptions<ScannerWebServiceOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ReportEventDispatcher> logger)
|
||||
{
|
||||
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
|
||||
_classificationChangeTracker = classificationChangeTracker ?? throw new ArgumentNullException(nameof(classificationChangeTracker));
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
@@ -109,6 +120,8 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||
|
||||
await PublishSafelyAsync(reportEvent, document.ReportId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await TrackFnDriftSafelyAsync(request, preview, document, tenant, occurredAt, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var scanCompletedEvent = new OrchestratorEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
@@ -130,6 +143,200 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||
await PublishSafelyAsync(scanCompletedEvent, document.ReportId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task TrackFnDriftSafelyAsync(
|
||||
ReportRequestDto request,
|
||||
PolicyPreviewResponse preview,
|
||||
ReportDocumentDto document,
|
||||
string tenant,
|
||||
DateTimeOffset occurredAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (preview.Diffs.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var changes = BuildClassificationChanges(request, preview, document, tenant, occurredAt);
|
||||
if (changes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _classificationChangeTracker.TrackChangesAsync(changes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to record FN-drift classification changes for report {ReportId}.", document.ReportId);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ClassificationChange> BuildClassificationChanges(
|
||||
ReportRequestDto request,
|
||||
PolicyPreviewResponse preview,
|
||||
ReportDocumentDto document,
|
||||
string tenant,
|
||||
DateTimeOffset occurredAt)
|
||||
{
|
||||
var findings = request.Findings ?? Array.Empty<PolicyPreviewFindingDto>();
|
||||
if (findings.Count == 0)
|
||||
{
|
||||
return Array.Empty<ClassificationChange>();
|
||||
}
|
||||
|
||||
var findingsById = findings
|
||||
.Where(finding => !string.IsNullOrWhiteSpace(finding.Id))
|
||||
.ToDictionary(finding => finding.Id!, StringComparer.Ordinal);
|
||||
|
||||
var tenantId = ResolveTenantId(tenant);
|
||||
var executionId = ResolveExecutionId(tenantId, document.ReportId);
|
||||
var manifestId = ResolveManifestId(tenantId, document);
|
||||
var artifactDigest = string.IsNullOrWhiteSpace(document.ImageDigest) ? request.ImageDigest ?? string.Empty : document.ImageDigest;
|
||||
|
||||
var changes = new List<ClassificationChange>();
|
||||
foreach (var diff in preview.Diffs)
|
||||
{
|
||||
var projected = diff.Projected;
|
||||
if (projected is null || string.IsNullOrWhiteSpace(projected.FindingId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!findingsById.TryGetValue(projected.FindingId, out var finding))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(finding.Cve) || string.IsNullOrWhiteSpace(finding.Purl))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var previousStatus = MapVerdictStatus(diff.Baseline.Status);
|
||||
var newStatus = MapVerdictStatus(projected.Status);
|
||||
|
||||
if (previousStatus == ClassificationStatus.Affected && newStatus == ClassificationStatus.Unaffected)
|
||||
{
|
||||
newStatus = ClassificationStatus.Fixed;
|
||||
}
|
||||
|
||||
changes.Add(new ClassificationChange
|
||||
{
|
||||
ArtifactDigest = artifactDigest,
|
||||
VulnId = finding.Cve!,
|
||||
PackagePurl = finding.Purl!,
|
||||
TenantId = tenantId,
|
||||
ManifestId = manifestId,
|
||||
ExecutionId = executionId,
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = newStatus,
|
||||
Cause = DetermineCause(diff),
|
||||
CauseDetail = BuildCauseDetail(diff, finding),
|
||||
ChangedAt = occurredAt
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static Guid ResolveTenantId(string tenant)
|
||||
{
|
||||
if (Guid.TryParse(tenant, out var tenantId))
|
||||
{
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
var normalized = tenant.Trim().ToLowerInvariant();
|
||||
return ScannerIdentifiers.CreateDeterministicGuid(TenantNamespace, Encoding.UTF8.GetBytes(normalized));
|
||||
}
|
||||
|
||||
private static Guid ResolveExecutionId(Guid tenantId, string reportId)
|
||||
{
|
||||
var payload = $"{tenantId:D}:{reportId}".Trim().ToLowerInvariant();
|
||||
return ScannerIdentifiers.CreateDeterministicGuid(ExecutionNamespace, Encoding.UTF8.GetBytes(payload));
|
||||
}
|
||||
|
||||
private static Guid ResolveManifestId(Guid tenantId, ReportDocumentDto document)
|
||||
{
|
||||
var manifestDigest = document.Surface?.ManifestDigest;
|
||||
var payloadSource = string.IsNullOrWhiteSpace(manifestDigest)
|
||||
? document.ImageDigest
|
||||
: manifestDigest;
|
||||
var payload = $"{tenantId:D}:{payloadSource}".Trim().ToLowerInvariant();
|
||||
return ScannerIdentifiers.CreateDeterministicGuid(ManifestNamespace, Encoding.UTF8.GetBytes(payload));
|
||||
}
|
||||
|
||||
private static ClassificationStatus MapVerdictStatus(PolicyVerdictStatus status) => status switch
|
||||
{
|
||||
PolicyVerdictStatus.Blocked or PolicyVerdictStatus.Escalated => ClassificationStatus.Affected,
|
||||
PolicyVerdictStatus.Warned or PolicyVerdictStatus.Deferred or PolicyVerdictStatus.RequiresVex => ClassificationStatus.Unknown,
|
||||
_ => ClassificationStatus.Unaffected
|
||||
};
|
||||
|
||||
private static DriftCause DetermineCause(PolicyVerdictDiff diff)
|
||||
{
|
||||
if (!string.Equals(diff.Baseline.RuleName, diff.Projected.RuleName, StringComparison.Ordinal)
|
||||
|| !string.Equals(diff.Baseline.RuleAction, diff.Projected.RuleAction, StringComparison.Ordinal))
|
||||
{
|
||||
return DriftCause.RuleDelta;
|
||||
}
|
||||
|
||||
if (!string.Equals(diff.Baseline.Reachability, diff.Projected.Reachability, StringComparison.Ordinal))
|
||||
{
|
||||
return DriftCause.ReachabilityDelta;
|
||||
}
|
||||
|
||||
if (!string.Equals(diff.Baseline.SourceTrust, diff.Projected.SourceTrust, StringComparison.Ordinal))
|
||||
{
|
||||
return DriftCause.FeedDelta;
|
||||
}
|
||||
|
||||
if (diff.Baseline.Quiet != diff.Projected.Quiet
|
||||
|| !string.Equals(diff.Baseline.QuietedBy, diff.Projected.QuietedBy, StringComparison.Ordinal))
|
||||
{
|
||||
return DriftCause.LatticeDelta;
|
||||
}
|
||||
|
||||
return DriftCause.Other;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string>? BuildCauseDetail(PolicyVerdictDiff diff, PolicyPreviewFindingDto finding)
|
||||
{
|
||||
var details = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(diff.Projected.RuleName))
|
||||
{
|
||||
details["ruleName"] = diff.Projected.RuleName!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(diff.Projected.RuleAction))
|
||||
{
|
||||
details["ruleAction"] = diff.Projected.RuleAction!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(diff.Projected.Reachability))
|
||||
{
|
||||
details["reachability"] = diff.Projected.Reachability!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(diff.Projected.SourceTrust))
|
||||
{
|
||||
details["sourceTrust"] = diff.Projected.SourceTrust!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(finding.Source))
|
||||
{
|
||||
details["findingSource"] = finding.Source!;
|
||||
}
|
||||
|
||||
return details.Count == 0 ? null : details;
|
||||
}
|
||||
|
||||
private async Task PublishSafelyAsync(OrchestratorEvent @event, string reportId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user