more audit work
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// <copyright file="CallstackEvidenceBuilderTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="CallstackEvidenceBuilder"/>.
|
||||
/// Sprint: SPRINT_20260107_005_001 Task EV-011
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class CallstackEvidenceBuilderTests
|
||||
{
|
||||
private readonly CallstackEvidenceBuilder _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Build_WithReachabilityEvidence_ReturnsCallstackEvidence()
|
||||
{
|
||||
// Arrange - builder looks for "reachability", "callgraph", or "call-path" kinds
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "reachability",
|
||||
Value = "main() -> process() -> vulnerable_fn()",
|
||||
Source = "static-analysis",
|
||||
});
|
||||
var component = CreateComponent(evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Frames.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithNoReachabilityEvidence_ReturnsNull()
|
||||
{
|
||||
// Arrange - only non-reachability evidence
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "manifest", Value = "package.json", Source = "file" });
|
||||
var component = CreateComponent(evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithEmptyEvidence_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent();
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMultipleReachabilityEvidence_AggregatesFrames()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "callgraph",
|
||||
Value = "main() -> handler()",
|
||||
Source = "static-analysis",
|
||||
},
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "reachability",
|
||||
Value = "worker() -> process()",
|
||||
Source = "dynamic-analysis",
|
||||
});
|
||||
var component = CreateComponent(evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Frames.Should().HaveCountGreaterThan(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithCallPathEvidence_ParsesFrames()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "call-path",
|
||||
Value = "main() -> lib.process() -> vulnerable_fn()",
|
||||
Source = "static-analysis",
|
||||
});
|
||||
var component = CreateComponent(evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
var frames = result!.Frames.ToList();
|
||||
// Verify the call path frames are present (implementation may include additional metadata frames)
|
||||
frames.Should().HaveCountGreaterThanOrEqualTo(3);
|
||||
frames.Should().Contain(f => f.Function == "main()");
|
||||
frames.Should().Contain(f => f.Function == "lib.process()");
|
||||
frames.Should().Contain(f => f.Function == "vulnerable_fn()");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithReachabilityAnalysisSource_BuildsFrames()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "reachability",
|
||||
Value = "entrypoint() -> vulnerable()",
|
||||
Source = "reachability-analysis",
|
||||
});
|
||||
var component = CreateComponent(evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Frames.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
private static AggregatedComponent CreateComponent(
|
||||
string? purl = null,
|
||||
string? name = null,
|
||||
ImmutableArray<ComponentEvidence>? evidence = null)
|
||||
{
|
||||
var identity = new ComponentIdentity
|
||||
{
|
||||
Purl = purl,
|
||||
Name = name ?? "test-component",
|
||||
Version = "1.0.0",
|
||||
Key = Guid.NewGuid().ToString(),
|
||||
};
|
||||
|
||||
return new AggregatedComponent
|
||||
{
|
||||
Identity = identity,
|
||||
Evidence = evidence ?? ImmutableArray<ComponentEvidence>.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// <copyright file="CycloneDxEvidenceMapperTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using CycloneDX.Models;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="CycloneDxEvidenceMapper"/>.
|
||||
/// Sprint: SPRINT_20260107_005_001 Task EV-010
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class CycloneDxEvidenceMapperTests
|
||||
{
|
||||
private readonly CycloneDxEvidenceMapper _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Map_WithIdentityEvidence_MapsToIdentity()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(
|
||||
purl: "pkg:npm/lodash@4.17.21",
|
||||
evidence: ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "manifest",
|
||||
Value = "package.json",
|
||||
Source = "/app/package.json",
|
||||
}));
|
||||
|
||||
// Act
|
||||
var result = _sut.Map(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Identity.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_WithLicenseEvidence_MapsToLicenses()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(
|
||||
purl: "pkg:npm/lodash@4.17.21",
|
||||
evidence: ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "license",
|
||||
Value = "MIT",
|
||||
Source = "/app/LICENSE",
|
||||
}));
|
||||
|
||||
// Act
|
||||
var result = _sut.Map(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Licenses.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_WithCopyrightEvidence_MapsToCopyright()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(
|
||||
purl: "pkg:npm/lodash@4.17.21",
|
||||
evidence: ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "copyright",
|
||||
Value = "Copyright 2024 StellaOps",
|
||||
Source = "/app/LICENSE",
|
||||
}));
|
||||
|
||||
// Act
|
||||
var result = _sut.Map(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Copyright.Should().NotBeNullOrEmpty();
|
||||
result.Copyright![0].Text.Should().Be("Copyright 2024 StellaOps");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_WithNoEvidence_ReturnsIdentityBasedOnPurl()
|
||||
{
|
||||
// Arrange - component with PURL but no explicit evidence
|
||||
var component = CreateComponent(
|
||||
purl: "pkg:npm/lodash@4.17.21",
|
||||
evidence: ImmutableArray<ComponentEvidence>.Empty);
|
||||
|
||||
// Act
|
||||
var result = _sut.Map(component);
|
||||
|
||||
// Assert - Should return identity evidence based on PURL
|
||||
// The behavior depends on whether the mapper creates identity from PURL alone
|
||||
// If PURL is present, identity evidence is generated
|
||||
result.Should().NotBeNull();
|
||||
result!.Identity.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_WithNoPurlAndNoEvidence_ReturnsNull()
|
||||
{
|
||||
// Arrange - component without PURL and no evidence
|
||||
var component = new AggregatedComponent
|
||||
{
|
||||
Identity = new ComponentIdentity
|
||||
{
|
||||
Name = "unnamed-component",
|
||||
Version = "1.0.0",
|
||||
Purl = null,
|
||||
Key = Guid.NewGuid().ToString(),
|
||||
},
|
||||
Evidence = ImmutableArray<ComponentEvidence>.Empty,
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Map(component);
|
||||
|
||||
// Assert - Without evidence or PURL, should return null or minimal evidence
|
||||
// The actual behavior depends on implementation
|
||||
if (result is not null)
|
||||
{
|
||||
// If evidence is returned, it should have minimal data
|
||||
result.Identity.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_WithMixedEvidence_MapsAllTypes()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(
|
||||
purl: "pkg:npm/lodash@4.17.21",
|
||||
evidence: ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "manifest",
|
||||
Value = "package.json",
|
||||
Source = "/app/package.json",
|
||||
},
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "license",
|
||||
Value = "MIT",
|
||||
Source = "/app/LICENSE",
|
||||
},
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "copyright",
|
||||
Value = "Copyright 2024",
|
||||
Source = "/app/LICENSE",
|
||||
}));
|
||||
|
||||
// Act
|
||||
var result = _sut.Map(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Identity.Should().NotBeNullOrEmpty();
|
||||
result.Licenses.Should().NotBeNullOrEmpty();
|
||||
result.Copyright.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseLegacyProperties_WithValidProperties_ReturnsRecords()
|
||||
{
|
||||
// Arrange
|
||||
var properties = new List<Property>
|
||||
{
|
||||
new Property
|
||||
{
|
||||
Name = "stellaops:evidence[0]",
|
||||
Value = "crypto:aes-256@/src/crypto.c",
|
||||
},
|
||||
new Property
|
||||
{
|
||||
Name = "stellaops:evidence[1]",
|
||||
Value = "license:MIT@/LICENSE",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = CycloneDxEvidenceMapper.ParseLegacyProperties(properties);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(2);
|
||||
results[0].Kind.Should().Be("crypto");
|
||||
results[0].Value.Should().Be("aes-256");
|
||||
results[0].Source.Should().Be("/src/crypto.c");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseLegacyProperties_WithNullProperties_ReturnsEmpty()
|
||||
{
|
||||
// Act
|
||||
var results = CycloneDxEvidenceMapper.ParseLegacyProperties(null);
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseLegacyProperties_WithEmptyProperties_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var properties = new List<Property>();
|
||||
|
||||
// Act
|
||||
var results = CycloneDxEvidenceMapper.ParseLegacyProperties(properties);
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseLegacyProperties_WithInvalidFormat_SkipsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var properties = new List<Property>
|
||||
{
|
||||
new Property { Name = "stellaops:evidence[0]", Value = "invalid-format" },
|
||||
new Property { Name = "stellaops:evidence[1]", Value = "crypto:aes@/file.c" },
|
||||
new Property { Name = "other:property", Value = "ignored" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = CycloneDxEvidenceMapper.ParseLegacyProperties(properties);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(1);
|
||||
results[0].Kind.Should().Be("crypto");
|
||||
}
|
||||
|
||||
private static AggregatedComponent CreateComponent(
|
||||
string purl,
|
||||
ImmutableArray<ComponentEvidence> evidence)
|
||||
{
|
||||
return new AggregatedComponent
|
||||
{
|
||||
Identity = new ComponentIdentity
|
||||
{
|
||||
Name = "test-component",
|
||||
Version = "1.0.0",
|
||||
Purl = purl,
|
||||
Key = Guid.NewGuid().ToString(),
|
||||
},
|
||||
Evidence = evidence,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// <copyright file="EvidenceConfidenceNormalizerTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Emit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="EvidenceConfidenceNormalizer"/>.
|
||||
/// Sprint: SPRINT_20260107_005_001 Task EV-010
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class EvidenceConfidenceNormalizerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, 0.0)]
|
||||
[InlineData(50, 0.5)]
|
||||
[InlineData(100, 1.0)]
|
||||
[InlineData(75.5, 0.755)]
|
||||
public void NormalizeFromPercentage_ReturnsCorrectValue(double percentage, double expected)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromPercentage(percentage);
|
||||
|
||||
// Assert
|
||||
result.Should().BeApproximately(expected, 0.001);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-10, 0.0)]
|
||||
[InlineData(150, 1.0)]
|
||||
public void NormalizeFromPercentage_ClampsOutOfRangeValues(double percentage, double expected)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromPercentage(percentage);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 0.2)]
|
||||
[InlineData(2, 0.4)]
|
||||
[InlineData(3, 0.6)]
|
||||
[InlineData(4, 0.8)]
|
||||
[InlineData(5, 1.0)]
|
||||
public void NormalizeFromScale5_ReturnsCorrectValue(int scale, double expected)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromScale5(scale);
|
||||
|
||||
// Assert
|
||||
result.Should().BeApproximately(expected, 0.001);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 0.1)]
|
||||
[InlineData(5, 0.5)]
|
||||
[InlineData(10, 1.0)]
|
||||
public void NormalizeFromScale10_ReturnsCorrectValue(int scale, double expected)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromScale10(scale);
|
||||
|
||||
// Assert
|
||||
result.Should().BeApproximately(expected, 0.001);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0.85", "syft", 0.85)]
|
||||
[InlineData("0.5", "syft", 0.5)]
|
||||
[InlineData("1.0", "syft", 1.0)]
|
||||
public void NormalizeFromAnalyzer_Syft_UsesDirect01Scale(string value, string analyzer, double expected)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromAnalyzer(value, analyzer);
|
||||
|
||||
// Assert
|
||||
result.Should().BeApproximately(expected, 0.001);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0.85", "grype", 0.85)] // Grype uses 0.0-1.0 scale like Syft
|
||||
[InlineData("0.5", "grype", 0.5)]
|
||||
[InlineData("1.0", "grype", 1.0)]
|
||||
public void NormalizeFromAnalyzer_Grype_UsesDirect01Scale(string value, string analyzer, double expected)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromAnalyzer(value, analyzer);
|
||||
|
||||
// Assert
|
||||
result.Should().BeApproximately(expected, 0.001);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "syft")]
|
||||
[InlineData("", "syft")]
|
||||
[InlineData(" ", "syft")]
|
||||
public void NormalizeFromAnalyzer_NullOrEmpty_ReturnsNull(string? value, string analyzer)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromAnalyzer(value, analyzer);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("high", 0.9)]
|
||||
[InlineData("HIGH", 0.9)]
|
||||
[InlineData("medium", 0.6)]
|
||||
[InlineData("low", 0.3)]
|
||||
public void NormalizeFromAnalyzer_TextualConfidence_ReturnsMapping(string value, double expected)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromAnalyzer(value, "unknown");
|
||||
|
||||
// Assert
|
||||
result.Should().BeApproximately(expected, 0.001);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("unknown")]
|
||||
[InlineData("none")]
|
||||
public void NormalizeFromAnalyzer_UnknownTextualConfidence_ReturnsNull(string value)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.NormalizeFromAnalyzer(value, "unknown");
|
||||
|
||||
// Assert - "unknown" means "no confidence data" hence null
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.75, "0.75")]
|
||||
[InlineData(0.123456, "0.12")]
|
||||
[InlineData(1.0, "1.00")]
|
||||
[InlineData(0.0, "0.00")]
|
||||
public void FormatConfidence_ReturnsInvariantCultureString(double confidence, string expected)
|
||||
{
|
||||
// Act
|
||||
var result = EvidenceConfidenceNormalizer.FormatConfidence(confidence);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// <copyright file="IdentityEvidenceBuilderTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="IdentityEvidenceBuilder"/>.
|
||||
/// Sprint: SPRINT_20260107_005_001 Task EV-011
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class IdentityEvidenceBuilderTests
|
||||
{
|
||||
private readonly IdentityEvidenceBuilder _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Build_WithPurl_ReturnsFieldAsPurl()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(purl: "pkg:npm/lodash@4.17.21");
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Field.Should().Be("purl");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithNameOnly_ReturnsFieldAsName()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(name: "my-package");
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Field.Should().Be("name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithManifestEvidence_IncludesManifestAnalysisMethod()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "manifest", Value = "package.json", Source = "/app/package.json" });
|
||||
var component = CreateComponent(purl: "pkg:npm/lodash@4.17.21", evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result!.Methods.Should().ContainSingle(m =>
|
||||
m.Technique == IdentityEvidenceTechnique.ManifestAnalysis);
|
||||
result.Methods![0].Confidence.Should().Be(0.95);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithBinaryEvidence_IncludesBinaryAnalysisMethod()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "binary", Value = "lodash.dll", Source = "/app/lodash.dll" });
|
||||
var component = CreateComponent(purl: "pkg:npm/lodash@4.17.21", evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result!.Methods.Should().ContainSingle(m =>
|
||||
m.Technique == IdentityEvidenceTechnique.BinaryAnalysis);
|
||||
result.Methods![0].Confidence.Should().Be(0.80);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithHashEvidence_IncludesHighConfidenceMethod()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "hash", Value = "sha256:abc123", Source = "/app/lib.so" });
|
||||
var component = CreateComponent(purl: "pkg:npm/lodash@4.17.21", evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result!.Methods.Should().ContainSingle(m =>
|
||||
m.Technique == IdentityEvidenceTechnique.HashComparison);
|
||||
result.Methods![0].Confidence.Should().Be(0.99);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMultipleSources_IncludesAllMethods()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "manifest", Value = "package.json", Source = "/app/package.json" },
|
||||
new ComponentEvidence { Kind = "binary", Value = "lib.dll", Source = "/app/lib.dll" },
|
||||
new ComponentEvidence { Kind = "hash", Value = "sha256:abc", Source = "/app/lib.so" });
|
||||
var component = CreateComponent(purl: "pkg:npm/lodash@4.17.21", evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result!.Methods.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_CalculatesOverallConfidenceFromHighestMethod()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "binary", Value = "lib.dll", Source = "/app/lib.dll" },
|
||||
new ComponentEvidence { Kind = "hash", Value = "sha256:abc", Source = "/app/lib.so" });
|
||||
var component = CreateComponent(purl: "pkg:npm/lodash@4.17.21", evidence: evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result!.Confidence.Should().Be(0.99); // Hash match has highest confidence
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithNoIdentifyingData_ReturnsNull()
|
||||
{
|
||||
// Arrange - unknown name means no identification
|
||||
var component = new AggregatedComponent
|
||||
{
|
||||
Identity = ComponentIdentity.Create("unknown", "unknown"),
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithPurlNoEvidence_ReturnsAttestationMethod()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(purl: "pkg:npm/lodash@4.17.21");
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result!.Methods.Should().ContainSingle(m =>
|
||||
m.Technique == IdentityEvidenceTechnique.Attestation);
|
||||
result.Methods![0].Confidence.Should().Be(0.70);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NullComponent_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var act = () => _sut.Build(null!);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
private static AggregatedComponent CreateComponent(
|
||||
string? purl = null,
|
||||
string? name = null,
|
||||
ImmutableArray<ComponentEvidence> evidence = default)
|
||||
{
|
||||
var identity = ComponentIdentity.Create(
|
||||
key: purl ?? name ?? "unknown",
|
||||
name: name ?? "test-component",
|
||||
purl: purl);
|
||||
|
||||
return new AggregatedComponent
|
||||
{
|
||||
Identity = identity,
|
||||
Evidence = evidence.IsDefault ? ImmutableArray<ComponentEvidence>.Empty : evidence,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// <copyright file="LegacyEvidencePropertyWriterTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using CycloneDX.Models;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="LegacyEvidencePropertyWriter"/>.
|
||||
/// Sprint: SPRINT_20260107_005_001 Task EV-010
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class LegacyEvidencePropertyWriterTests
|
||||
{
|
||||
private readonly LegacyEvidencePropertyWriter _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void WriteEvidenceProperties_WithEvidence_AddsProperties()
|
||||
{
|
||||
// Arrange
|
||||
var component = new Component { Name = "test-component" };
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "manifest",
|
||||
Value = "package.json",
|
||||
Source = "/app/package.json",
|
||||
});
|
||||
var options = new LegacyEvidenceOptions();
|
||||
|
||||
// Act
|
||||
_sut.WriteEvidenceProperties(component, evidence, options);
|
||||
|
||||
// Assert
|
||||
component.Properties.Should().NotBeEmpty();
|
||||
component.Properties.Should().Contain(p => p.Name == "stellaops:evidence[0]:kind");
|
||||
component.Properties.Should().Contain(p => p.Name == "stellaops:evidence[0]:value");
|
||||
component.Properties.Should().Contain(p => p.Name == "stellaops:evidence[0]:source");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteEvidenceProperties_WithEmptyEvidence_DoesNotAddProperties()
|
||||
{
|
||||
// Arrange
|
||||
var component = new Component { Name = "test-component" };
|
||||
var evidence = ImmutableArray<ComponentEvidence>.Empty;
|
||||
var options = new LegacyEvidenceOptions();
|
||||
|
||||
// Act
|
||||
_sut.WriteEvidenceProperties(component, evidence, options);
|
||||
|
||||
// Assert
|
||||
component.Properties.Should().BeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteEvidenceProperties_WithMultipleEvidence_AddsIndexedProperties()
|
||||
{
|
||||
// Arrange
|
||||
var component = new Component { Name = "test-component" };
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "manifest", Value = "package.json", Source = "file" },
|
||||
new ComponentEvidence { Kind = "binary", Value = "lib.dll", Source = "binary-scan" });
|
||||
var options = new LegacyEvidenceOptions();
|
||||
|
||||
// Act
|
||||
_sut.WriteEvidenceProperties(component, evidence, options);
|
||||
|
||||
// Assert
|
||||
component.Properties.Should().Contain(p => p.Name == "stellaops:evidence[0]:kind");
|
||||
component.Properties.Should().Contain(p => p.Name == "stellaops:evidence[1]:kind");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteEvidenceProperties_WithNullComponent_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
Component component = null!;
|
||||
var evidence = ImmutableArray<ComponentEvidence>.Empty;
|
||||
var options = new LegacyEvidenceOptions();
|
||||
|
||||
// Act
|
||||
var act = () => _sut.WriteEvidenceProperties(component, evidence, options);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteEvidenceProperties_PreservesExistingProperties()
|
||||
{
|
||||
// Arrange
|
||||
var component = new Component
|
||||
{
|
||||
Name = "test-component",
|
||||
Properties = [new Property { Name = "existing", Value = "value" }],
|
||||
};
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "manifest", Value = "package.json", Source = "file" });
|
||||
var options = new LegacyEvidenceOptions();
|
||||
|
||||
// Act
|
||||
_sut.WriteEvidenceProperties(component, evidence, options);
|
||||
|
||||
// Assert
|
||||
component.Properties.Should().Contain(p => p.Name == "existing");
|
||||
component.Properties.Should().Contain(p => p.Name == "stellaops:evidence[0]:kind");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteEvidenceProperties_WithMethodsReference_IncludesMethodsProperty()
|
||||
{
|
||||
// Arrange
|
||||
var component = new Component { Name = "test-component" };
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence
|
||||
{
|
||||
Kind = "identity",
|
||||
Value = "pkg:npm/lodash@4.17.21",
|
||||
Source = "manifest-analysis",
|
||||
});
|
||||
var options = new LegacyEvidenceOptions { IncludeMethodsReference = true };
|
||||
|
||||
// Act
|
||||
_sut.WriteEvidenceProperties(component, evidence, options);
|
||||
|
||||
// Assert
|
||||
component.Properties.Should().Contain(p => p.Name!.StartsWith("stellaops:evidence"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLegacyProperties_RemovesAllEvidenceProperties()
|
||||
{
|
||||
// Arrange
|
||||
var component = new Component
|
||||
{
|
||||
Name = "test-component",
|
||||
Properties =
|
||||
[
|
||||
new Property { Name = "stellaops:evidence[0]:kind", Value = "manifest" },
|
||||
new Property { Name = "stellaops:evidence[0]:value", Value = "package.json" },
|
||||
new Property { Name = "other:property", Value = "preserved" },
|
||||
],
|
||||
};
|
||||
|
||||
// Act
|
||||
_sut.RemoveLegacyProperties(component);
|
||||
|
||||
// Assert
|
||||
component.Properties.Should().HaveCount(1);
|
||||
component.Properties.Should().Contain(p => p.Name == "other:property");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLegacyProperties_WithNullProperties_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var component = new Component { Name = "test-component", Properties = null };
|
||||
|
||||
// Act
|
||||
var act = () => _sut.RemoveLegacyProperties(component);
|
||||
|
||||
// Assert
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// <copyright file="LicenseEvidenceBuilderTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="LicenseEvidenceBuilder"/>.
|
||||
/// Sprint: SPRINT_20260107_005_001 Task EV-011
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class LicenseEvidenceBuilderTests
|
||||
{
|
||||
private readonly LicenseEvidenceBuilder _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Build_WithLicenseEvidence_ReturnsLicenseChoices()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "license", Value = "MIT", Source = "/app/LICENSE" });
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
result[0].License.Should().NotBeNull();
|
||||
result[0].License!.License.Should().NotBeNull();
|
||||
result[0].License!.License!.Id.Should().Be("MIT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMultipleLicenses_ReturnsAllLicenses()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "license", Value = "MIT", Source = "/app/LICENSE" },
|
||||
new ComponentEvidence { Kind = "license", Value = "Apache-2.0", Source = "/app/LICENSE.apache" },
|
||||
new ComponentEvidence { Kind = "license", Value = "GPL-3.0", Source = "/app/COPYING" });
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithNonLicenseEvidence_FiltersOutNonLicenses()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "license", Value = "MIT", Source = "/app/LICENSE" },
|
||||
new ComponentEvidence { Kind = "file", Value = "readme.md", Source = "/app/README.md" });
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
result[0].License!.License!.Id.Should().Be("MIT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithNoEvidence_ReturnsEmptyArray()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(ImmutableArray<ComponentEvidence>.Empty);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NullComponent_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var act = () => _sut.Build(null!);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithExpression_ParsesAsExpression()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "license", Value = "MIT OR Apache-2.0", Source = "/app/LICENSE" });
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
// SPDX expressions are parsed as expression rather than ID
|
||||
result[0].License.Expression.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DeduplicatesSameLicense()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "license", Value = "MIT", Source = "/app/LICENSE" },
|
||||
new ComponentEvidence { Kind = "license", Value = "MIT", Source = "/app/package.json" }); // Same license
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
private static AggregatedComponent CreateComponent(ImmutableArray<ComponentEvidence> evidence)
|
||||
{
|
||||
var identity = ComponentIdentity.Create(
|
||||
key: "pkg:npm/lodash@4.17.21",
|
||||
name: "lodash",
|
||||
purl: "pkg:npm/lodash@4.17.21");
|
||||
|
||||
return new AggregatedComponent
|
||||
{
|
||||
Identity = identity,
|
||||
Evidence = evidence,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// <copyright file="OccurrenceEvidenceBuilderTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="OccurrenceEvidenceBuilder"/>.
|
||||
/// Sprint: SPRINT_20260107_005_001 Task EV-011
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class OccurrenceEvidenceBuilderTests
|
||||
{
|
||||
private readonly OccurrenceEvidenceBuilder _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Build_WithFileEvidence_ReturnsOccurrences()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "file", Value = "lodash.min.js", Source = "/app/node_modules/lodash/lodash.min.js" });
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
result[0].Location.Should().Be("/app/node_modules/lodash/lodash.min.js");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMultipleFiles_ReturnsAllOccurrences()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "file", Value = "lodash.js", Source = "/app/src/lodash.js" },
|
||||
new ComponentEvidence { Kind = "file", Value = "lodash.min.js", Source = "/app/dist/lodash.min.js" },
|
||||
new ComponentEvidence { Kind = "file", Value = "lodash.core.js", Source = "/app/lib/lodash.core.js" });
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMixedEvidenceKinds_IncludesAllWithSource()
|
||||
{
|
||||
// Arrange - OccurrenceBuilder captures all evidence with source locations
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "file", Value = "lodash.js", Source = "/app/src/lodash.js" },
|
||||
new ComponentEvidence { Kind = "manifest", Value = "package.json", Source = "/app/package.json" });
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert - All evidence types with sources are captured as occurrences
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().Contain(o => o.Location == "/app/src/lodash.js");
|
||||
result.Should().Contain(o => o.Location == "/app/package.json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithNoEvidence_ReturnsEmptyArray()
|
||||
{
|
||||
// Arrange
|
||||
var component = CreateComponent(ImmutableArray<ComponentEvidence>.Empty);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NullComponent_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var act = () => _sut.Build(null!);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DeduplicatesSameLocation()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = ImmutableArray.Create(
|
||||
new ComponentEvidence { Kind = "file", Value = "lodash.js", Source = "/app/lodash.js" },
|
||||
new ComponentEvidence { Kind = "file", Value = "lodash.js", Source = "/app/lodash.js" }); // Duplicate
|
||||
var component = CreateComponent(evidence);
|
||||
|
||||
// Act
|
||||
var result = _sut.Build(component);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
private static AggregatedComponent CreateComponent(ImmutableArray<ComponentEvidence> evidence)
|
||||
{
|
||||
var identity = ComponentIdentity.Create(
|
||||
key: "pkg:npm/lodash@4.17.21",
|
||||
name: "lodash",
|
||||
purl: "pkg:npm/lodash@4.17.21");
|
||||
|
||||
return new AggregatedComponent
|
||||
{
|
||||
Identity = identity,
|
||||
Evidence = evidence,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user