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:
@@ -0,0 +1,25 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Authority.Core.Verdicts;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Core.Tests.Verdicts;
|
||||
|
||||
public sealed class InMemoryVerdictManifestStoreTests
|
||||
{
|
||||
private readonly InMemoryVerdictManifestStore _store = new();
|
||||
|
||||
[Fact]
|
||||
public async Task StoreAndRetrieve_ByManifestId()
|
||||
{
|
||||
var manifest = CreateManifest("manifest-1", "tenant-1");
|
||||
|
||||
await _store.StoreAsync(manifest);
|
||||
|
||||
var retrieved = await _store.GetByIdAsync("tenant-1", "manifest-1");
|
||||
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.ManifestId.Should().Be("manifest-1");
|
||||
retrieved.Tenant.Should().Be("tenant-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByScope_ReturnsLatest()
|
||||
{
|
||||
var older = CreateManifest("m1", "t", evaluatedAt: DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
|
||||
var newer = CreateManifest("m2", "t", evaluatedAt: DateTimeOffset.Parse("2025-01-02T00:00:00Z"));
|
||||
|
||||
await _store.StoreAsync(older);
|
||||
await _store.StoreAsync(newer);
|
||||
|
||||
var result = await _store.GetByScopeAsync("t", "sha256:asset", "CVE-2024-1234");
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.ManifestId.Should().Be("m2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByScope_FiltersOnPolicyAndLattice()
|
||||
{
|
||||
var m1 = CreateManifest("m1", "t", policyHash: "p1", latticeVersion: "v1");
|
||||
var m2 = CreateManifest("m2", "t", policyHash: "p2", latticeVersion: "v1");
|
||||
|
||||
await _store.StoreAsync(m1);
|
||||
await _store.StoreAsync(m2);
|
||||
|
||||
var result = await _store.GetByScopeAsync("t", "sha256:asset", "CVE-2024-1234", policyHash: "p1");
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.ManifestId.Should().Be("m1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListByPolicy_Paginates()
|
||||
{
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var manifest = CreateManifest($"m{i}", "t", policyHash: "p1", latticeVersion: "v1",
|
||||
evaluatedAt: DateTimeOffset.UtcNow.AddMinutes(-i));
|
||||
await _store.StoreAsync(manifest);
|
||||
}
|
||||
|
||||
var page1 = await _store.ListByPolicyAsync("t", "p1", "v1", limit: 2);
|
||||
page1.Manifests.Should().HaveCount(2);
|
||||
page1.NextPageToken.Should().NotBeNull();
|
||||
|
||||
var page2 = await _store.ListByPolicyAsync("t", "p1", "v1", limit: 2, pageToken: page1.NextPageToken);
|
||||
page2.Manifests.Should().HaveCount(2);
|
||||
page2.NextPageToken.Should().NotBeNull();
|
||||
|
||||
var page3 = await _store.ListByPolicyAsync("t", "p1", "v1", limit: 2, pageToken: page2.NextPageToken);
|
||||
page3.Manifests.Should().HaveCount(1);
|
||||
page3.NextPageToken.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_RemovesManifest()
|
||||
{
|
||||
var manifest = CreateManifest("m1", "t");
|
||||
await _store.StoreAsync(manifest);
|
||||
|
||||
var deleted = await _store.DeleteAsync("t", "m1");
|
||||
deleted.Should().BeTrue();
|
||||
|
||||
var retrieved = await _store.GetByIdAsync("t", "m1");
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_ReturnsFalseWhenNotFound()
|
||||
{
|
||||
var deleted = await _store.DeleteAsync("t", "nonexistent");
|
||||
deleted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantIsolation_Works()
|
||||
{
|
||||
var m1 = CreateManifest("shared-id", "tenant-a");
|
||||
var m2 = CreateManifest("shared-id", "tenant-b");
|
||||
|
||||
await _store.StoreAsync(m1);
|
||||
await _store.StoreAsync(m2);
|
||||
|
||||
var fromA = await _store.GetByIdAsync("tenant-a", "shared-id");
|
||||
var fromB = await _store.GetByIdAsync("tenant-b", "shared-id");
|
||||
|
||||
fromA.Should().NotBeNull();
|
||||
fromB.Should().NotBeNull();
|
||||
fromA!.Tenant.Should().Be("tenant-a");
|
||||
fromB!.Tenant.Should().Be("tenant-b");
|
||||
|
||||
_store.Count.Should().Be(2);
|
||||
}
|
||||
|
||||
private static VerdictManifest CreateManifest(
|
||||
string manifestId,
|
||||
string tenant,
|
||||
string assetDigest = "sha256:asset",
|
||||
string vulnerabilityId = "CVE-2024-1234",
|
||||
string policyHash = "sha256:policy",
|
||||
string latticeVersion = "1.0.0",
|
||||
DateTimeOffset? evaluatedAt = null)
|
||||
{
|
||||
return new VerdictManifest
|
||||
{
|
||||
ManifestId = manifestId,
|
||||
Tenant = tenant,
|
||||
AssetDigest = assetDigest,
|
||||
VulnerabilityId = vulnerabilityId,
|
||||
Inputs = new VerdictInputs
|
||||
{
|
||||
SbomDigests = ImmutableArray.Create("sha256:sbom"),
|
||||
VulnFeedSnapshotIds = ImmutableArray.Create("feed-1"),
|
||||
VexDocumentDigests = ImmutableArray.Create("sha256:vex"),
|
||||
ReachabilityGraphIds = ImmutableArray<string>.Empty,
|
||||
ClockCutoff = DateTimeOffset.UtcNow,
|
||||
},
|
||||
Result = new VerdictResult
|
||||
{
|
||||
Status = VexStatus.NotAffected,
|
||||
Confidence = 0.85,
|
||||
Explanations = ImmutableArray<VerdictExplanation>.Empty,
|
||||
EvidenceRefs = ImmutableArray<string>.Empty,
|
||||
},
|
||||
PolicyHash = policyHash,
|
||||
LatticeVersion = latticeVersion,
|
||||
EvaluatedAt = evaluatedAt ?? DateTimeOffset.UtcNow,
|
||||
ManifestDigest = $"sha256:{manifestId}",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Authority.Core.Verdicts;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Core.Tests.Verdicts;
|
||||
|
||||
public sealed class VerdictManifestBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_CreatesValidManifest()
|
||||
{
|
||||
var builder = new VerdictManifestBuilder(() => "test-manifest-id")
|
||||
.WithTenant("tenant-1")
|
||||
.WithAsset("sha256:abc123", "CVE-2024-1234")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "sha256:sbom1" },
|
||||
vulnFeedSnapshotIds: new[] { "feed-snapshot-1" },
|
||||
vexDocumentDigests: new[] { "sha256:vex1" },
|
||||
clockCutoff: DateTimeOffset.Parse("2025-01-01T00:00:00Z"))
|
||||
.WithResult(
|
||||
status: VexStatus.NotAffected,
|
||||
confidence: 0.85,
|
||||
explanations: new[]
|
||||
{
|
||||
new VerdictExplanation
|
||||
{
|
||||
SourceId = "vendor-a",
|
||||
Reason = "Official vendor VEX",
|
||||
ProvenanceScore = 0.9,
|
||||
CoverageScore = 0.8,
|
||||
ReplayabilityScore = 0.7,
|
||||
StrengthMultiplier = 1.0,
|
||||
FreshnessMultiplier = 0.95,
|
||||
ClaimScore = 0.85,
|
||||
AssertedStatus = VexStatus.NotAffected,
|
||||
Accepted = true,
|
||||
},
|
||||
})
|
||||
.WithPolicy("sha256:policy123", "1.0.0")
|
||||
.WithClock(DateTimeOffset.Parse("2025-01-01T12:00:00Z"));
|
||||
|
||||
var manifest = builder.Build();
|
||||
|
||||
manifest.ManifestId.Should().Be("test-manifest-id");
|
||||
manifest.Tenant.Should().Be("tenant-1");
|
||||
manifest.AssetDigest.Should().Be("sha256:abc123");
|
||||
manifest.VulnerabilityId.Should().Be("CVE-2024-1234");
|
||||
manifest.Result.Status.Should().Be(VexStatus.NotAffected);
|
||||
manifest.Result.Confidence.Should().Be(0.85);
|
||||
manifest.ManifestDigest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_IsDeterministic()
|
||||
{
|
||||
var clock = DateTimeOffset.Parse("2025-01-01T12:00:00Z");
|
||||
var inputClock = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
|
||||
|
||||
VerdictManifest BuildManifest(int seed)
|
||||
{
|
||||
return new VerdictManifestBuilder(() => "fixed-id")
|
||||
.WithTenant("tenant")
|
||||
.WithAsset("sha256:asset", "CVE-2024-0001")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "sha256:sbom" },
|
||||
vulnFeedSnapshotIds: new[] { "feed-1" },
|
||||
vexDocumentDigests: new[] { "sha256:vex" },
|
||||
clockCutoff: inputClock)
|
||||
.WithResult(
|
||||
status: VexStatus.Fixed,
|
||||
confidence: 0.9,
|
||||
explanations: new[]
|
||||
{
|
||||
new VerdictExplanation
|
||||
{
|
||||
SourceId = "source",
|
||||
Reason = "Fixed",
|
||||
ProvenanceScore = 0.9,
|
||||
CoverageScore = 0.9,
|
||||
ReplayabilityScore = 0.9,
|
||||
StrengthMultiplier = 1.0,
|
||||
FreshnessMultiplier = 1.0,
|
||||
ClaimScore = 0.9,
|
||||
AssertedStatus = VexStatus.Fixed,
|
||||
Accepted = true,
|
||||
},
|
||||
})
|
||||
.WithPolicy("sha256:policy", "1.0")
|
||||
.WithClock(clock)
|
||||
.Build();
|
||||
}
|
||||
|
||||
var first = BuildManifest(1);
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var next = BuildManifest(i);
|
||||
next.ManifestDigest.Should().Be(first.ManifestDigest, "manifests should be deterministic");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_SortsInputsDeterministically()
|
||||
{
|
||||
var clock = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
|
||||
|
||||
var manifestA = new VerdictManifestBuilder(() => "id")
|
||||
.WithTenant("t")
|
||||
.WithAsset("sha256:a", "CVE-1")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "c", "a", "b" },
|
||||
vulnFeedSnapshotIds: new[] { "z", "y" },
|
||||
vexDocumentDigests: new[] { "3", "1", "2" },
|
||||
clockCutoff: clock)
|
||||
.WithResult(VexStatus.Affected, 0.5, Enumerable.Empty<VerdictExplanation>())
|
||||
.WithPolicy("p", "v")
|
||||
.WithClock(clock)
|
||||
.Build();
|
||||
|
||||
var manifestB = new VerdictManifestBuilder(() => "id")
|
||||
.WithTenant("t")
|
||||
.WithAsset("sha256:a", "CVE-1")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "b", "c", "a" },
|
||||
vulnFeedSnapshotIds: new[] { "y", "z" },
|
||||
vexDocumentDigests: new[] { "2", "3", "1" },
|
||||
clockCutoff: clock)
|
||||
.WithResult(VexStatus.Affected, 0.5, Enumerable.Empty<VerdictExplanation>())
|
||||
.WithPolicy("p", "v")
|
||||
.WithClock(clock)
|
||||
.Build();
|
||||
|
||||
manifestA.ManifestDigest.Should().Be(manifestB.ManifestDigest);
|
||||
manifestA.Inputs.SbomDigests.Should().Equal("a", "b", "c");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ThrowsOnMissingRequiredFields()
|
||||
{
|
||||
var builder = new VerdictManifestBuilder();
|
||||
|
||||
var act = () => builder.Build();
|
||||
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*validation failed*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NormalizesVulnerabilityIdToUpperCase()
|
||||
{
|
||||
var manifest = new VerdictManifestBuilder(() => "id")
|
||||
.WithTenant("t")
|
||||
.WithAsset("sha256:a", "cve-2024-1234")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "sha256:s" },
|
||||
vulnFeedSnapshotIds: new[] { "f" },
|
||||
vexDocumentDigests: new[] { "v" },
|
||||
clockCutoff: DateTimeOffset.UtcNow)
|
||||
.WithResult(VexStatus.Affected, 0.5, Enumerable.Empty<VerdictExplanation>())
|
||||
.WithPolicy("p", "v")
|
||||
.Build();
|
||||
|
||||
manifest.VulnerabilityId.Should().Be("CVE-2024-1234");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Authority.Core.Verdicts;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Core.Tests.Verdicts;
|
||||
|
||||
public sealed class VerdictManifestSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Serialize_ProducesValidJson()
|
||||
{
|
||||
var manifest = CreateTestManifest();
|
||||
|
||||
var json = VerdictManifestSerializer.Serialize(manifest);
|
||||
|
||||
json.Should().Contain("\"manifest_id\"");
|
||||
json.Should().Contain("\"tenant\"");
|
||||
json.Should().Contain("\"not_affected\"");
|
||||
json.Should().NotContain("\"ManifestId\""); // Should use snake_case
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeDeserialize_RoundTrips()
|
||||
{
|
||||
var manifest = CreateTestManifest();
|
||||
|
||||
var json = VerdictManifestSerializer.Serialize(manifest);
|
||||
var deserialized = VerdictManifestSerializer.Deserialize(json);
|
||||
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.ManifestId.Should().Be(manifest.ManifestId);
|
||||
deserialized.Result.Status.Should().Be(manifest.Result.Status);
|
||||
deserialized.Result.Confidence.Should().Be(manifest.Result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_IsDeterministic()
|
||||
{
|
||||
var manifest = CreateTestManifest();
|
||||
|
||||
var digest1 = VerdictManifestSerializer.ComputeDigest(manifest);
|
||||
var digest2 = VerdictManifestSerializer.ComputeDigest(manifest);
|
||||
|
||||
digest1.Should().Be(digest2);
|
||||
digest1.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_ChangesWithContent()
|
||||
{
|
||||
var manifest1 = CreateTestManifest();
|
||||
var manifest2 = manifest1 with
|
||||
{
|
||||
Result = manifest1.Result with { Confidence = 0.5 }
|
||||
};
|
||||
|
||||
var digest1 = VerdictManifestSerializer.ComputeDigest(manifest1);
|
||||
var digest2 = VerdictManifestSerializer.ComputeDigest(manifest2);
|
||||
|
||||
digest1.Should().NotBe(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_IgnoresSignatureFields()
|
||||
{
|
||||
var manifest1 = CreateTestManifest();
|
||||
var manifest2 = manifest1 with
|
||||
{
|
||||
SignatureBase64 = "some-signature",
|
||||
RekorLogId = "some-log-id"
|
||||
};
|
||||
|
||||
var digest1 = VerdictManifestSerializer.ComputeDigest(manifest1);
|
||||
var digest2 = VerdictManifestSerializer.ComputeDigest(manifest2);
|
||||
|
||||
digest1.Should().Be(digest2);
|
||||
}
|
||||
|
||||
private static VerdictManifest CreateTestManifest()
|
||||
{
|
||||
return new VerdictManifest
|
||||
{
|
||||
ManifestId = "test-id",
|
||||
Tenant = "test-tenant",
|
||||
AssetDigest = "sha256:asset123",
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
Inputs = new VerdictInputs
|
||||
{
|
||||
SbomDigests = ImmutableArray.Create("sha256:sbom1"),
|
||||
VulnFeedSnapshotIds = ImmutableArray.Create("feed-1"),
|
||||
VexDocumentDigests = ImmutableArray.Create("sha256:vex1"),
|
||||
ReachabilityGraphIds = ImmutableArray<string>.Empty,
|
||||
ClockCutoff = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
|
||||
},
|
||||
Result = new VerdictResult
|
||||
{
|
||||
Status = VexStatus.NotAffected,
|
||||
Confidence = 0.85,
|
||||
Explanations = ImmutableArray.Create(
|
||||
new VerdictExplanation
|
||||
{
|
||||
SourceId = "vendor-a",
|
||||
Reason = "Official vendor statement",
|
||||
ProvenanceScore = 0.9,
|
||||
CoverageScore = 0.8,
|
||||
ReplayabilityScore = 0.7,
|
||||
StrengthMultiplier = 1.0,
|
||||
FreshnessMultiplier = 0.95,
|
||||
ClaimScore = 0.85,
|
||||
AssertedStatus = VexStatus.NotAffected,
|
||||
Accepted = true,
|
||||
}),
|
||||
EvidenceRefs = ImmutableArray.Create("evidence-1"),
|
||||
},
|
||||
PolicyHash = "sha256:policy123",
|
||||
LatticeVersion = "1.0.0",
|
||||
EvaluatedAt = DateTimeOffset.Parse("2025-01-01T12:00:00Z"),
|
||||
ManifestDigest = "sha256:placeholder",
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user