sprints work
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
// <copyright file="ReachabilityCoreBridge.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Reachability.Core;
|
||||
|
||||
namespace StellaOps.Policy.Engine.ReachabilityFacts;
|
||||
|
||||
/// <summary>
|
||||
/// Bridge between Reachability.Core types and Policy.Engine types.
|
||||
/// Enables gradual migration from ReachabilityFact to HybridReachabilityResult.
|
||||
/// Sprint: SPRINT_20260109_009_005 Task: Integrate Reachability.Core
|
||||
/// </summary>
|
||||
public static class ReachabilityCoreBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="HybridReachabilityResult"/> to a <see cref="ReachabilityFact"/>.
|
||||
/// Used to maintain backward compatibility with existing VEX emission.
|
||||
/// </summary>
|
||||
public static ReachabilityFact ToReachabilityFact(
|
||||
HybridReachabilityResult result,
|
||||
string tenantId,
|
||||
string advisoryId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
ArgumentException.ThrowIfNullOrEmpty(tenantId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(advisoryId);
|
||||
|
||||
var state = MapLatticeToState(result.LatticeState);
|
||||
var method = DetermineMethod(result);
|
||||
|
||||
return new ReachabilityFact
|
||||
{
|
||||
Id = $"rf-{result.ContentDigest[7..23]}", // Use part of content digest as ID
|
||||
TenantId = tenantId,
|
||||
ComponentPurl = result.ArtifactDigest,
|
||||
AdvisoryId = advisoryId,
|
||||
State = state,
|
||||
Confidence = (decimal)result.Confidence,
|
||||
Score = ComputeScore(result),
|
||||
HasRuntimeEvidence = result.RuntimeResult is not null,
|
||||
Source = "StellaOps.Reachability.Core",
|
||||
Method = method,
|
||||
EvidenceRef = result.Evidence.Uris.Length > 0 ? result.Evidence.Uris[0] : null,
|
||||
EvidenceHash = result.ContentDigest,
|
||||
ComputedAt = result.ComputedAt,
|
||||
ExpiresAt = result.ComputedAt.AddDays(7),
|
||||
Metadata = BuildMetadata(result)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps lattice state enum to string representation.
|
||||
/// </summary>
|
||||
public static string MapLatticeStateToString(LatticeState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
LatticeState.Unknown => "U",
|
||||
LatticeState.StaticReachable => "SR",
|
||||
LatticeState.StaticUnreachable => "SU",
|
||||
LatticeState.RuntimeObserved => "RO",
|
||||
LatticeState.RuntimeUnobserved => "RU",
|
||||
LatticeState.ConfirmedReachable => "CR",
|
||||
LatticeState.ConfirmedUnreachable => "CU",
|
||||
LatticeState.Contested => "X",
|
||||
_ => "U"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses string lattice state to enum.
|
||||
/// </summary>
|
||||
public static LatticeState ParseLatticeState(string? state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
"U" or null => LatticeState.Unknown,
|
||||
"SR" => LatticeState.StaticReachable,
|
||||
"SU" => LatticeState.StaticUnreachable,
|
||||
"RO" => LatticeState.RuntimeObserved,
|
||||
"RU" => LatticeState.RuntimeUnobserved,
|
||||
"CR" => LatticeState.ConfirmedReachable,
|
||||
"CU" => LatticeState.ConfirmedUnreachable,
|
||||
"X" => LatticeState.Contested,
|
||||
_ => LatticeState.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps lattice state to triage bucket.
|
||||
/// </summary>
|
||||
public static string MapToBucket(LatticeState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
LatticeState.ConfirmedReachable or LatticeState.RuntimeObserved => "critical",
|
||||
LatticeState.StaticReachable => "high",
|
||||
LatticeState.Contested or LatticeState.Unknown => "medium",
|
||||
LatticeState.RuntimeUnobserved => "low",
|
||||
LatticeState.StaticUnreachable or LatticeState.ConfirmedUnreachable => "informational",
|
||||
_ => "medium"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps HybridReachabilityResult to VEX status.
|
||||
/// </summary>
|
||||
public static string MapToVexStatus(HybridReachabilityResult result)
|
||||
{
|
||||
return result.LatticeState switch
|
||||
{
|
||||
LatticeState.ConfirmedUnreachable or LatticeState.StaticUnreachable => "not_affected",
|
||||
LatticeState.RuntimeUnobserved when result.Confidence >= 0.7 => "not_affected",
|
||||
LatticeState.ConfirmedReachable or LatticeState.RuntimeObserved => "affected",
|
||||
LatticeState.StaticReachable => "under_investigation",
|
||||
_ => "under_investigation"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps HybridReachabilityResult to VEX justification.
|
||||
/// </summary>
|
||||
public static string? MapToVexJustification(HybridReachabilityResult result)
|
||||
{
|
||||
return result.LatticeState switch
|
||||
{
|
||||
LatticeState.ConfirmedUnreachable or LatticeState.StaticUnreachable =>
|
||||
"vulnerable_code_not_in_execute_path",
|
||||
LatticeState.RuntimeUnobserved when result.Confidence >= 0.7 =>
|
||||
"vulnerable_code_not_in_execute_path",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static ReachabilityState MapLatticeToState(LatticeState lattice)
|
||||
{
|
||||
return lattice switch
|
||||
{
|
||||
LatticeState.ConfirmedReachable or
|
||||
LatticeState.RuntimeObserved or
|
||||
LatticeState.StaticReachable => ReachabilityState.Reachable,
|
||||
|
||||
LatticeState.ConfirmedUnreachable or
|
||||
LatticeState.StaticUnreachable or
|
||||
LatticeState.RuntimeUnobserved => ReachabilityState.Unreachable,
|
||||
|
||||
LatticeState.Contested => ReachabilityState.UnderInvestigation,
|
||||
|
||||
_ => ReachabilityState.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private static AnalysisMethod DetermineMethod(HybridReachabilityResult result)
|
||||
{
|
||||
var hasStatic = result.StaticResult is not null;
|
||||
var hasRuntime = result.RuntimeResult is not null;
|
||||
|
||||
return (hasStatic, hasRuntime) switch
|
||||
{
|
||||
(true, true) => AnalysisMethod.Hybrid,
|
||||
(true, false) => AnalysisMethod.Static,
|
||||
(false, true) => AnalysisMethod.Dynamic,
|
||||
_ => AnalysisMethod.Static // Default to static when no analysis available
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal ComputeScore(HybridReachabilityResult result)
|
||||
{
|
||||
// Score based on lattice state - higher means more reachable
|
||||
return result.LatticeState switch
|
||||
{
|
||||
LatticeState.ConfirmedReachable => 1.0m,
|
||||
LatticeState.RuntimeObserved => 0.9m,
|
||||
LatticeState.StaticReachable => 0.7m,
|
||||
LatticeState.Contested => 0.5m,
|
||||
LatticeState.Unknown => 0.5m,
|
||||
LatticeState.RuntimeUnobserved => 0.3m,
|
||||
LatticeState.StaticUnreachable => 0.1m,
|
||||
LatticeState.ConfirmedUnreachable => 0.0m,
|
||||
_ => 0.5m
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> BuildMetadata(HybridReachabilityResult result)
|
||||
{
|
||||
var metadata = new Dictionary<string, object?>
|
||||
{
|
||||
["lattice_state"] = MapLatticeStateToString(result.LatticeState),
|
||||
["symbol_canonical_id"] = result.Symbol.CanonicalId,
|
||||
["symbol_purl"] = result.Symbol.Purl,
|
||||
["symbol_type"] = result.Symbol.Type,
|
||||
["symbol_method"] = result.Symbol.Method
|
||||
};
|
||||
|
||||
if (result.StaticResult is not null)
|
||||
{
|
||||
metadata["static_reachable"] = result.StaticResult.IsReachable;
|
||||
metadata["static_path_count"] = result.StaticResult.PathCount;
|
||||
}
|
||||
|
||||
if (result.RuntimeResult is not null)
|
||||
{
|
||||
metadata["runtime_observed"] = result.RuntimeResult.WasObserved;
|
||||
metadata["runtime_hit_count"] = result.RuntimeResult.HitCount;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
|
||||
<ProjectReference Include="../../Router/__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Provcache/StellaOps.Provcache.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Reachability.Core/StellaOps.Reachability.Core.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy.Determinization/StellaOps.Policy.Determinization.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
// <copyright file="ReachabilityCoreBridgeTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Engine.ReachabilityFacts;
|
||||
using StellaOps.Reachability.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.ReachabilityFacts;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ReachabilityCoreBridge"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class ReachabilityCoreBridgeTests
|
||||
{
|
||||
private readonly DateTimeOffset _now = new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_MapsConfirmedReachableToReachable()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.ConfirmedReachable, 0.95);
|
||||
|
||||
// Act
|
||||
var fact = ReachabilityCoreBridge.ToReachabilityFact(result, "tenant1", "CVE-2024-1234");
|
||||
|
||||
// Assert
|
||||
fact.State.Should().Be(ReachabilityState.Reachable);
|
||||
fact.Confidence.Should().Be(0.95m);
|
||||
fact.Score.Should().Be(1.0m);
|
||||
fact.TenantId.Should().Be("tenant1");
|
||||
fact.AdvisoryId.Should().Be("CVE-2024-1234");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_MapsStaticUnreachableToUnreachable()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.StaticUnreachable, 0.8);
|
||||
|
||||
// Act
|
||||
var fact = ReachabilityCoreBridge.ToReachabilityFact(result, "tenant1", "CVE-2024-5678");
|
||||
|
||||
// Assert
|
||||
fact.State.Should().Be(ReachabilityState.Unreachable);
|
||||
fact.Score.Should().Be(0.1m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_MapsContestedToUnderInvestigation()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.Contested, 0.5);
|
||||
|
||||
// Act
|
||||
var fact = ReachabilityCoreBridge.ToReachabilityFact(result, "tenant1", "CVE-2024-9999");
|
||||
|
||||
// Assert
|
||||
fact.State.Should().Be(ReachabilityState.UnderInvestigation);
|
||||
fact.Score.Should().Be(0.5m);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LatticeState.ConfirmedReachable, "critical")]
|
||||
[InlineData(LatticeState.RuntimeObserved, "critical")]
|
||||
[InlineData(LatticeState.StaticReachable, "high")]
|
||||
[InlineData(LatticeState.Contested, "medium")]
|
||||
[InlineData(LatticeState.Unknown, "medium")]
|
||||
[InlineData(LatticeState.RuntimeUnobserved, "low")]
|
||||
[InlineData(LatticeState.StaticUnreachable, "informational")]
|
||||
[InlineData(LatticeState.ConfirmedUnreachable, "informational")]
|
||||
public void MapToBucket_ReturnsCorrectBucket(LatticeState state, string expectedBucket)
|
||||
{
|
||||
// Act
|
||||
var bucket = ReachabilityCoreBridge.MapToBucket(state);
|
||||
|
||||
// Assert
|
||||
bucket.Should().Be(expectedBucket);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LatticeState.Unknown, "U")]
|
||||
[InlineData(LatticeState.StaticReachable, "SR")]
|
||||
[InlineData(LatticeState.StaticUnreachable, "SU")]
|
||||
[InlineData(LatticeState.RuntimeObserved, "RO")]
|
||||
[InlineData(LatticeState.RuntimeUnobserved, "RU")]
|
||||
[InlineData(LatticeState.ConfirmedReachable, "CR")]
|
||||
[InlineData(LatticeState.ConfirmedUnreachable, "CU")]
|
||||
[InlineData(LatticeState.Contested, "X")]
|
||||
public void MapLatticeStateToString_ReturnsCorrectCode(LatticeState state, string expectedCode)
|
||||
{
|
||||
// Act
|
||||
var code = ReachabilityCoreBridge.MapLatticeStateToString(state);
|
||||
|
||||
// Assert
|
||||
code.Should().Be(expectedCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("U", LatticeState.Unknown)]
|
||||
[InlineData("SR", LatticeState.StaticReachable)]
|
||||
[InlineData("SU", LatticeState.StaticUnreachable)]
|
||||
[InlineData("RO", LatticeState.RuntimeObserved)]
|
||||
[InlineData("RU", LatticeState.RuntimeUnobserved)]
|
||||
[InlineData("CR", LatticeState.ConfirmedReachable)]
|
||||
[InlineData("CU", LatticeState.ConfirmedUnreachable)]
|
||||
[InlineData("X", LatticeState.Contested)]
|
||||
[InlineData(null, LatticeState.Unknown)]
|
||||
[InlineData("invalid", LatticeState.Unknown)]
|
||||
public void ParseLatticeState_ReturnsCorrectState(string? code, LatticeState expectedState)
|
||||
{
|
||||
// Act
|
||||
var state = ReachabilityCoreBridge.ParseLatticeState(code);
|
||||
|
||||
// Assert
|
||||
state.Should().Be(expectedState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_WithStaticResult_SetsMethodToStatic()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.StaticReachable, 0.75);
|
||||
result = result with
|
||||
{
|
||||
StaticResult = new StaticReachabilityResult
|
||||
{
|
||||
Symbol = result.Symbol,
|
||||
ArtifactDigest = result.ArtifactDigest,
|
||||
IsReachable = true,
|
||||
AnalyzedAt = _now
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var fact = ReachabilityCoreBridge.ToReachabilityFact(result, "tenant1", "CVE-TEST");
|
||||
|
||||
// Assert
|
||||
fact.Method.Should().Be(AnalysisMethod.Static);
|
||||
fact.HasRuntimeEvidence.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_WithRuntimeResult_SetsMethodToDynamic()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.RuntimeObserved, 0.9);
|
||||
result = result with
|
||||
{
|
||||
RuntimeResult = new RuntimeReachabilityResult
|
||||
{
|
||||
Symbol = result.Symbol,
|
||||
ArtifactDigest = result.ArtifactDigest,
|
||||
WasObserved = true,
|
||||
ObservationWindow = TimeSpan.FromDays(7),
|
||||
WindowStart = _now.AddDays(-7),
|
||||
WindowEnd = _now
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var fact = ReachabilityCoreBridge.ToReachabilityFact(result, "tenant1", "CVE-TEST");
|
||||
|
||||
// Assert
|
||||
fact.Method.Should().Be(AnalysisMethod.Dynamic);
|
||||
fact.HasRuntimeEvidence.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_WithBothResults_SetsMethodToHybrid()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.ConfirmedReachable, 0.95);
|
||||
result = result with
|
||||
{
|
||||
StaticResult = new StaticReachabilityResult
|
||||
{
|
||||
Symbol = result.Symbol,
|
||||
ArtifactDigest = result.ArtifactDigest,
|
||||
IsReachable = true,
|
||||
AnalyzedAt = _now
|
||||
},
|
||||
RuntimeResult = new RuntimeReachabilityResult
|
||||
{
|
||||
Symbol = result.Symbol,
|
||||
ArtifactDigest = result.ArtifactDigest,
|
||||
WasObserved = true,
|
||||
ObservationWindow = TimeSpan.FromDays(7),
|
||||
WindowStart = _now.AddDays(-7),
|
||||
WindowEnd = _now
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var fact = ReachabilityCoreBridge.ToReachabilityFact(result, "tenant1", "CVE-TEST");
|
||||
|
||||
// Assert
|
||||
fact.Method.Should().Be(AnalysisMethod.Hybrid);
|
||||
fact.HasRuntimeEvidence.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LatticeState.ConfirmedUnreachable, "not_affected")]
|
||||
[InlineData(LatticeState.StaticUnreachable, "not_affected")]
|
||||
[InlineData(LatticeState.ConfirmedReachable, "affected")]
|
||||
[InlineData(LatticeState.RuntimeObserved, "affected")]
|
||||
[InlineData(LatticeState.StaticReachable, "under_investigation")]
|
||||
[InlineData(LatticeState.Contested, "under_investigation")]
|
||||
public void MapToVexStatus_ReturnsCorrectStatus(LatticeState state, string expectedStatus)
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(state, 0.8);
|
||||
|
||||
// Act
|
||||
var status = ReachabilityCoreBridge.MapToVexStatus(result);
|
||||
|
||||
// Assert
|
||||
status.Should().Be(expectedStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapToVexJustification_WhenUnreachable_ReturnsJustification()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.ConfirmedUnreachable, 0.9);
|
||||
|
||||
// Act
|
||||
var justification = ReachabilityCoreBridge.MapToVexJustification(result);
|
||||
|
||||
// Assert
|
||||
justification.Should().Be("vulnerable_code_not_in_execute_path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapToVexJustification_WhenReachable_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.ConfirmedReachable, 0.9);
|
||||
|
||||
// Act
|
||||
var justification = ReachabilityCoreBridge.MapToVexJustification(result);
|
||||
|
||||
// Assert
|
||||
justification.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_IncludesMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.StaticReachable, 0.75);
|
||||
|
||||
// Act
|
||||
var fact = ReachabilityCoreBridge.ToReachabilityFact(result, "tenant1", "CVE-TEST");
|
||||
|
||||
// Assert
|
||||
fact.Metadata.Should().NotBeNull();
|
||||
fact.Metadata!["lattice_state"].Should().Be("SR");
|
||||
fact.Metadata!["symbol_canonical_id"].Should().Be(result.Symbol.CanonicalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_NullResultThrows()
|
||||
{
|
||||
// Act
|
||||
var act = () => ReachabilityCoreBridge.ToReachabilityFact(null!, "tenant1", "CVE-TEST");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToReachabilityFact_EmptyTenantIdThrows()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateHybridResult(LatticeState.Unknown, 0.5);
|
||||
|
||||
// Act
|
||||
var act = () => ReachabilityCoreBridge.ToReachabilityFact(result, "", "CVE-TEST");
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
private HybridReachabilityResult CreateHybridResult(LatticeState state, double confidence)
|
||||
{
|
||||
var symbol = new SymbolRef
|
||||
{
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
Namespace = "lodash",
|
||||
Type = "_",
|
||||
Method = "template",
|
||||
Signature = "(string)"
|
||||
};
|
||||
|
||||
return new HybridReachabilityResult
|
||||
{
|
||||
Symbol = symbol,
|
||||
ArtifactDigest = "sha256:abc123",
|
||||
LatticeState = state,
|
||||
Confidence = confidence,
|
||||
Verdict = VerdictRecommendation.UnderInvestigation(),
|
||||
Evidence = new EvidenceBundle
|
||||
{
|
||||
Uris = ["stellaops://evidence/test"],
|
||||
CollectedAt = _now
|
||||
},
|
||||
ComputedAt = _now
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user