feat: add Attestation Chain and Triage Evidence API clients and models

- Implemented Attestation Chain API client with methods for verifying, fetching, and managing attestation chains.
- Created models for Attestation Chain, including DSSE envelope structures and verification results.
- Developed Triage Evidence API client for fetching finding evidence, including methods for evidence retrieval by CVE and component.
- Added models for Triage Evidence, encapsulating evidence responses, entry points, boundary proofs, and VEX evidence.
- Introduced mock implementations for both API clients to facilitate testing and development.
This commit is contained in:
master
2025-12-18 13:15:13 +02:00
parent 7d5250238c
commit 00d2c99af9
118 changed files with 13463 additions and 151 deletions

View File

@@ -0,0 +1,293 @@
// -----------------------------------------------------------------------------
// FindingEvidenceContractsTests.cs
// Sprint: SPRINT_3800_0001_0001_evidence_api_models
// 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 ComponentRef
{
Purl = "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
Name = "log4j-core",
Version = "2.14.1",
Type = "maven"
},
ReachablePath = new[] { "com.example.App.main", "org.apache.log4j.Logger.log" },
LastSeen = new DateTimeOffset(2025, 12, 18, 12, 0, 0, TimeSpan.Zero)
};
var json = JsonSerializer.Serialize(response, SerializerOptions);
Assert.Contains("\"finding_id\":\"finding-123\"", json);
Assert.Contains("\"cve\":\"CVE-2021-44228\"", json);
Assert.Contains("\"reachable_path\":", json);
Assert.Contains("\"last_seen\":", json);
}
[Fact]
public void FindingEvidenceResponse_RoundTripsCorrectly()
{
var original = new FindingEvidenceResponse
{
FindingId = "finding-456",
Cve = "CVE-2023-12345",
Component = new ComponentRef
{
Purl = "pkg:npm/lodash@4.17.20",
Name = "lodash",
Version = "4.17.20",
Type = "npm"
},
Entrypoint = new EntrypointProof
{
Type = "http_handler",
Route = "/api/v1/users",
Method = "POST",
Auth = "required",
Fqn = "com.example.UserController.createUser"
},
ScoreExplain = new ScoreExplanationDto
{
Kind = "stellaops_risk_v1",
RiskScore = 7.5,
Contributions = new[]
{
new ScoreContributionDto
{
Factor = "cvss_base",
Weight = 0.4,
RawValue = 9.8,
Contribution = 3.92,
Explanation = "CVSS v4 base score"
}
},
LastSeen = DateTimeOffset.UtcNow
},
LastSeen = DateTimeOffset.UtcNow
};
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.ScoreExplain?.RiskScore, deserialized.ScoreExplain?.RiskScore);
}
[Fact]
public void ComponentRef_SerializesAllFields()
{
var component = new ComponentRef
{
Purl = "pkg:nuget/Newtonsoft.Json@13.0.1",
Name = "Newtonsoft.Json",
Version = "13.0.1",
Type = "nuget"
};
var json = JsonSerializer.Serialize(component, SerializerOptions);
Assert.Contains("\"purl\":\"pkg:nuget/Newtonsoft.Json@13.0.1\"", json);
Assert.Contains("\"name\":\"Newtonsoft.Json\"", json);
Assert.Contains("\"version\":\"13.0.1\"", json);
Assert.Contains("\"type\":\"nuget\"", json);
}
[Fact]
public void EntrypointProof_SerializesWithLocation()
{
var entrypoint = new EntrypointProof
{
Type = "grpc_method",
Route = "grpc.UserService.GetUser",
Auth = "required",
Phase = "runtime",
Fqn = "com.example.UserServiceImpl.getUser",
Location = new SourceLocation
{
File = "src/main/java/com/example/UserServiceImpl.java",
Line = 42,
Column = 5
}
};
var json = JsonSerializer.Serialize(entrypoint, SerializerOptions);
Assert.Contains("\"type\":\"grpc_method\"", json);
Assert.Contains("\"route\":\"grpc.UserService.GetUser\"", json);
Assert.Contains("\"location\":", json);
Assert.Contains("\"file\":\"src/main/java/com/example/UserServiceImpl.java\"", json);
Assert.Contains("\"line\":42", json);
}
[Fact]
public void BoundaryProofDto_SerializesWithControls()
{
var boundary = new BoundaryProofDto
{
Kind = "network",
Surface = new SurfaceDescriptor
{
Type = "api",
Protocol = "https",
Port = 443
},
Exposure = new ExposureDescriptor
{
Level = "public",
InternetFacing = true,
Zone = "dmz"
},
Auth = new AuthDescriptor
{
Required = true,
Type = "jwt",
Roles = new[] { "admin", "user" }
},
Controls = new[]
{
new ControlDescriptor
{
Type = "waf",
Active = true,
Config = "OWASP-ModSecurity"
}
},
LastSeen = DateTimeOffset.UtcNow,
Confidence = 0.95
};
var json = JsonSerializer.Serialize(boundary, SerializerOptions);
Assert.Contains("\"kind\":\"network\"", json);
Assert.Contains("\"internet_facing\":true", json);
Assert.Contains("\"controls\":[", json);
Assert.Contains("\"confidence\":0.95", json);
}
[Fact]
public void VexEvidenceDto_SerializesCorrectly()
{
var vex = new VexEvidenceDto
{
Status = "not_affected",
Justification = "vulnerable_code_not_in_execute_path",
Impact = "The vulnerable code path is never executed in our usage",
AttestationRef = "dsse:sha256:abc123",
IssuedAt = new DateTimeOffset(2025, 12, 1, 0, 0, 0, TimeSpan.Zero),
ExpiresAt = new DateTimeOffset(2026, 12, 1, 0, 0, 0, TimeSpan.Zero),
Source = "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("\"attestation_ref\":\"dsse:sha256:abc123\"", json);
Assert.Contains("\"source\":\"vendor\"", json);
}
[Fact]
public void ScoreExplanationDto_SerializesContributions()
{
var explanation = new ScoreExplanationDto
{
Kind = "stellaops_risk_v1",
RiskScore = 6.2,
Contributions = new[]
{
new ScoreContributionDto
{
Factor = "cvss_base",
Weight = 0.4,
RawValue = 9.8,
Contribution = 3.92,
Explanation = "Critical CVSS base score"
},
new ScoreContributionDto
{
Factor = "epss",
Weight = 0.2,
RawValue = 0.45,
Contribution = 0.09,
Explanation = "45% probability of exploitation"
},
new ScoreContributionDto
{
Factor = "reachability",
Weight = 0.3,
RawValue = 1.0,
Contribution = 0.3,
Explanation = "Reachable from HTTP entrypoint"
},
new ScoreContributionDto
{
Factor = "gate_multiplier",
Weight = 1.0,
RawValue = 0.5,
Contribution = -2.11,
Explanation = "Auth gate reduces exposure by 50%"
}
},
LastSeen = DateTimeOffset.UtcNow
};
var json = JsonSerializer.Serialize(explanation, SerializerOptions);
Assert.Contains("\"kind\":\"stellaops_risk_v1\"", json);
Assert.Contains("\"risk_score\":6.2", json);
Assert.Contains("\"contributions\":[", json);
Assert.Contains("\"factor\":\"cvss_base\"", json);
Assert.Contains("\"factor\":\"epss\"", json);
Assert.Contains("\"factor\":\"reachability\"", json);
Assert.Contains("\"factor\":\"gate_multiplier\"", json);
}
[Fact]
public void NullOptionalFields_AreOmittedOrNullInJson()
{
var response = new FindingEvidenceResponse
{
FindingId = "finding-minimal",
Cve = "CVE-2025-0001",
LastSeen = DateTimeOffset.UtcNow
// All optional fields are null
};
var json = JsonSerializer.Serialize(response, SerializerOptions);
var deserialized = JsonSerializer.Deserialize<FindingEvidenceResponse>(json, SerializerOptions);
Assert.NotNull(deserialized);
Assert.Null(deserialized.Component);
Assert.Null(deserialized.ReachablePath);
Assert.Null(deserialized.Entrypoint);
Assert.Null(deserialized.Boundary);
Assert.Null(deserialized.Vex);
Assert.Null(deserialized.ScoreExplain);
}
}