Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/FindingEvidenceContractsTests.cs

247 lines
8.1 KiB
C#

// -----------------------------------------------------------------------------
// FindingEvidenceContractsTests.cs
// Sprint: SPRINT_4300_0001_0002_findings_evidence_api
// Description: Unit tests for JSON serialization of evidence API contracts.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text.Json;
using StellaOps.Scanner.WebService.Contracts;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests;
public class FindingEvidenceContractsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false
};
[Fact]
public void FindingEvidenceResponse_SerializesToSnakeCase()
{
var response = new FindingEvidenceResponse
{
FindingId = "finding-123",
Cve = "CVE-2021-44228",
Component = new ComponentInfo
{
Name = "log4j-core",
Version = "2.14.1",
Purl = "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
Ecosystem = "maven"
},
ReachablePath = new[] { "com.example.App.main", "org.apache.log4j.Logger.log" },
LastSeen = new DateTimeOffset(2025, 12, 18, 12, 0, 0, TimeSpan.Zero),
AttestationRefs = new[] { "dsse:sha256:abc123" },
Freshness = new FreshnessInfo { IsStale = false }
};
var json = JsonSerializer.Serialize(response, SerializerOptions);
Assert.Contains("\"finding_id\":\"finding-123\"", json);
Assert.Contains("\"cve\":\"CVE-2021-44228\"", json);
Assert.Contains("\"component\":", json);
Assert.Contains("\"reachable_path\":", json);
Assert.Contains("\"freshness\":", json);
}
[Fact]
public void FindingEvidenceResponse_RoundTripsCorrectly()
{
var original = new FindingEvidenceResponse
{
FindingId = "finding-456",
Cve = "CVE-2023-12345",
Component = new ComponentInfo
{
Name = "lodash",
Version = "4.17.20",
Purl = "pkg:npm/lodash@4.17.20",
Ecosystem = "npm"
},
Entrypoint = new EntrypointInfo
{
Type = "http",
Route = "/api/v1/users",
Method = "POST",
Auth = "jwt:write"
},
Score = new ScoreInfo
{
RiskScore = 75,
Contributions = new[]
{
new ScoreContribution
{
Factor = "reachability",
Value = 25,
Reason = "Reachable from entrypoint"
}
}
},
LastSeen = DateTimeOffset.UtcNow,
Freshness = new FreshnessInfo { IsStale = false }
};
var json = JsonSerializer.Serialize(original, SerializerOptions);
var deserialized = JsonSerializer.Deserialize<FindingEvidenceResponse>(json, SerializerOptions);
Assert.NotNull(deserialized);
Assert.Equal(original.FindingId, deserialized.FindingId);
Assert.Equal(original.Cve, deserialized.Cve);
Assert.Equal(original.Component.Purl, deserialized.Component.Purl);
Assert.Equal(original.Entrypoint?.Type, deserialized.Entrypoint?.Type);
Assert.Equal(original.Score?.RiskScore, deserialized.Score?.RiskScore);
}
[Fact]
public void ComponentInfo_SerializesAllFields()
{
var component = new ComponentInfo
{
Name = "Newtonsoft.Json",
Version = "13.0.1",
Purl = "pkg:nuget/Newtonsoft.Json@13.0.1",
Ecosystem = "nuget"
};
var json = JsonSerializer.Serialize(component, SerializerOptions);
Assert.Contains("\"name\":\"Newtonsoft.Json\"", json);
Assert.Contains("\"version\":\"13.0.1\"", json);
Assert.Contains("\"purl\":\"pkg:nuget/Newtonsoft.Json@13.0.1\"", json);
Assert.Contains("\"ecosystem\":\"nuget\"", json);
}
[Fact]
public void EntrypointInfo_SerializesAllFields()
{
var entrypoint = new EntrypointInfo
{
Type = "grpc",
Route = "grpc.UserService.GetUser",
Method = "CALL",
Auth = "mtls"
};
var json = JsonSerializer.Serialize(entrypoint, SerializerOptions);
Assert.Contains("\"type\":\"grpc\"", json);
Assert.Contains("\"route\":\"grpc.UserService.GetUser\"", json);
Assert.Contains("\"method\":\"CALL\"", json);
Assert.Contains("\"auth\":\"mtls\"", json);
}
[Fact]
public void BoundaryInfo_SerializesWithControls()
{
var boundary = new BoundaryInfo
{
Surface = "api",
Exposure = "internet",
Controls = new[] { "waf", "rate_limit" }
};
var json = JsonSerializer.Serialize(boundary, SerializerOptions);
Assert.Contains("\"surface\":\"api\"", json);
Assert.Contains("\"exposure\":\"internet\"", json);
Assert.Contains("\"controls\":[\"waf\",\"rate_limit\"]", json);
}
[Fact]
public void VexStatusInfo_SerializesCorrectly()
{
var vex = new VexStatusInfo
{
Status = "not_affected",
Justification = "vulnerable_code_not_in_execute_path",
Timestamp = new DateTimeOffset(2025, 12, 1, 0, 0, 0, TimeSpan.Zero),
Issuer = "vendor"
};
var json = JsonSerializer.Serialize(vex, SerializerOptions);
Assert.Contains("\"status\":\"not_affected\"", json);
Assert.Contains("\"justification\":\"vulnerable_code_not_in_execute_path\"", json);
Assert.Contains("\"issuer\":\"vendor\"", json);
}
[Fact]
public void ScoreInfo_SerializesContributions()
{
var score = new ScoreInfo
{
RiskScore = 62,
Contributions = new[]
{
new ScoreContribution
{
Factor = "cvss_base",
Value = 40,
Reason = "Critical CVSS base score"
},
new ScoreContribution
{
Factor = "reachability",
Value = 22,
Reason = "Reachable from HTTP entrypoint"
}
}
};
var json = JsonSerializer.Serialize(score, SerializerOptions);
Assert.Contains("\"risk_score\":62", json);
Assert.Contains("\"factor\":\"cvss_base\"", json);
Assert.Contains("\"factor\":\"reachability\"", json);
}
[Fact]
public void FreshnessInfo_SerializesCorrectly()
{
var freshness = new FreshnessInfo
{
IsStale = true,
ExpiresAt = new DateTimeOffset(2025, 12, 31, 0, 0, 0, TimeSpan.Zero),
TtlRemainingHours = 0
};
var json = JsonSerializer.Serialize(freshness, SerializerOptions);
Assert.Contains("\"is_stale\":true", json);
Assert.Contains("\"expires_at\":", json);
Assert.Contains("\"ttl_remaining_hours\":0", json);
}
[Fact]
public void NullOptionalFields_AreOmittedOrNullInJson()
{
var response = new FindingEvidenceResponse
{
FindingId = "finding-minimal",
Cve = "CVE-2025-0001",
Component = new ComponentInfo
{
Name = "unknown",
Version = "unknown"
},
LastSeen = DateTimeOffset.UtcNow,
Freshness = new FreshnessInfo { IsStale = false }
};
var json = JsonSerializer.Serialize(response, SerializerOptions);
var deserialized = JsonSerializer.Deserialize<FindingEvidenceResponse>(json, SerializerOptions);
Assert.NotNull(deserialized);
Assert.Null(deserialized.Entrypoint);
Assert.Null(deserialized.Vex);
Assert.Null(deserialized.Score);
Assert.Null(deserialized.Boundary);
}
}