docs consolidation

This commit is contained in:
StellaOps Bot
2025-12-24 21:45:46 +02:00
parent 4231305fec
commit 43e2af88f6
76 changed files with 2887 additions and 796 deletions

View File

@@ -0,0 +1,408 @@
# Evidence API Reference
This document provides the complete API reference for the StellaOps unified evidence model.
## Interfaces
### IEvidence
Base interface for all evidence records.
```csharp
namespace StellaOps.Evidence.Core;
public interface IEvidence
{
/// <summary>
/// Content-addressed evidence identifier (e.g., "sha256:abc123...").
/// </summary>
string EvidenceId { get; }
/// <summary>
/// The type of evidence this record represents.
/// </summary>
EvidenceType Type { get; }
/// <summary>
/// The subject (node ID) this evidence is about.
/// </summary>
string SubjectNodeId { get; }
/// <summary>
/// When the evidence was created (UTC).
/// </summary>
DateTimeOffset CreatedAt { get; }
/// <summary>
/// Cryptographic signatures attesting to this evidence.
/// </summary>
IReadOnlyList<EvidenceSignature> Signatures { get; }
/// <summary>
/// Origin and provenance information.
/// </summary>
EvidenceProvenance? Provenance { get; }
/// <summary>
/// Type-specific properties as key-value pairs.
/// </summary>
IReadOnlyDictionary<string, string> Properties { get; }
}
```
### IEvidenceStore
Storage interface for evidence persistence.
```csharp
namespace StellaOps.Evidence.Core;
public interface IEvidenceStore
{
/// <summary>
/// Retrieves evidence by its content-addressed ID.
/// </summary>
Task<IEvidence?> GetAsync(string evidenceId, CancellationToken ct = default);
/// <summary>
/// Retrieves all evidence records for a given subject.
/// </summary>
Task<IReadOnlyList<IEvidence>> GetBySubjectAsync(
string subjectNodeId,
CancellationToken ct = default);
/// <summary>
/// Retrieves all evidence records of a specific type.
/// </summary>
Task<IReadOnlyList<IEvidence>> GetByTypeAsync(
EvidenceType type,
CancellationToken ct = default);
/// <summary>
/// Stores an evidence record.
/// </summary>
Task StoreAsync(IEvidence evidence, CancellationToken ct = default);
/// <summary>
/// Checks if evidence with the given ID exists.
/// </summary>
Task<bool> ExistsAsync(string evidenceId, CancellationToken ct = default);
}
```
### IEvidenceAdapter<TInput>
Adapter interface for converting module-specific types to evidence.
```csharp
namespace StellaOps.Evidence.Core.Adapters;
public interface IEvidenceAdapter<TInput>
{
/// <summary>
/// Converts a module-specific input to one or more evidence records.
/// </summary>
IReadOnlyList<IEvidence> ToEvidence(TInput input);
}
```
---
## Records
### EvidenceRecord
Standard implementation of IEvidence.
```csharp
namespace StellaOps.Evidence.Core;
public sealed record EvidenceRecord : IEvidence
{
public required string EvidenceId { get; init; }
public required EvidenceType Type { get; init; }
public required string SubjectNodeId { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public IReadOnlyList<EvidenceSignature> Signatures { get; init; } = [];
public EvidenceProvenance? Provenance { get; init; }
public IReadOnlyDictionary<string, string> Properties { get; init; } =
new Dictionary<string, string>();
}
```
### EvidenceSignature
Cryptographic signature attached to evidence.
```csharp
namespace StellaOps.Evidence.Core;
public sealed record EvidenceSignature
{
/// <summary>
/// Identifier of the signer (key ID, tool name, etc.).
/// </summary>
public required string SignerId { get; init; }
/// <summary>
/// Signing algorithm (e.g., "Ed25519", "ES256").
/// </summary>
public required string Algorithm { get; init; }
/// <summary>
/// Base64-encoded signature bytes.
/// </summary>
public required string SignatureBase64 { get; init; }
/// <summary>
/// When the signature was created (UTC).
/// </summary>
public required DateTimeOffset SignedAt { get; init; }
/// <summary>
/// Type of entity that produced the signature.
/// </summary>
public required SignerType SignerType { get; init; }
}
```
### EvidenceProvenance
Origin and provenance information.
```csharp
namespace StellaOps.Evidence.Core;
public sealed record EvidenceProvenance
{
/// <summary>
/// Source that produced this evidence (e.g., "grype", "trivy").
/// </summary>
public required string Source { get; init; }
/// <summary>
/// Version of the source tool.
/// </summary>
public string? SourceVersion { get; init; }
/// <summary>
/// URI where original data was obtained.
/// </summary>
public string? SourceUri { get; init; }
/// <summary>
/// Digest of the original content.
/// </summary>
public string? ContentDigest { get; init; }
}
```
---
## Enumerations
### EvidenceType
```csharp
namespace StellaOps.Evidence.Core;
public enum EvidenceType
{
Unknown = 0,
Sbom = 1,
Vulnerability = 2,
Vex = 3,
Attestation = 4,
PolicyDecision = 5,
ScanResult = 6,
Provenance = 7,
Signature = 8,
ProofSegment = 9,
Exception = 10,
Advisory = 11,
CveMatch = 12,
ReachabilityResult = 13
}
```
### SignerType
```csharp
namespace StellaOps.Evidence.Core;
public enum SignerType
{
Unknown = 0,
Tool = 1,
Human = 2,
Authority = 3,
Vendor = 4,
Service = 5
}
```
---
## Adapters
### EvidenceStatementAdapter
Converts `EvidenceStatement` from Attestor module.
**Input**: `EvidenceStatementInput`
```csharp
public sealed record EvidenceStatementInput
{
public required string StatementId { get; init; }
public required string SubjectDigest { get; init; }
public required string StatementType { get; init; }
public required string PredicateType { get; init; }
public required DateTimeOffset IssuedAt { get; init; }
public IReadOnlyList<EvidenceSignatureInput>? Signatures { get; init; }
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
}
```
**Output**: Single `IEvidence` record with `Type = Attestation`.
---
### ProofSegmentAdapter
Converts `ProofSegment` from Scanner module.
**Input**: `ProofSegmentInput`
```csharp
public sealed record ProofSegmentInput
{
public required string SegmentId { get; init; }
public required string SubjectNodeId { get; init; }
public required string SegmentType { get; init; }
public required string Status { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public string? PreviousSegmentId { get; init; }
public string? PayloadDigest { get; init; }
public IReadOnlyList<EvidenceSignatureInput>? Signatures { get; init; }
public IReadOnlyDictionary<string, string>? Properties { get; init; }
}
```
**Output**: Single `IEvidence` record with `Type = ProofSegment`.
---
### VexObservationAdapter
Converts `VexObservation` from Excititor module.
**Input**: `VexObservationInput`
```csharp
public sealed record VexObservationInput
{
public required string SubjectDigest { get; init; }
public VexObservationUpstreamInput? Upstream { get; init; }
public IReadOnlyList<VexObservationStatementInput>? Statements { get; init; }
public IReadOnlyDictionary<string, string>? Properties { get; init; }
}
public sealed record VexObservationUpstreamInput
{
public required string VexDocumentId { get; init; }
public required string VendorName { get; init; }
public required DateTimeOffset PublishedAt { get; init; }
public string? DocumentDigest { get; init; }
}
public sealed record VexObservationStatementInput
{
public required string VulnerabilityId { get; init; }
public required string ProductId { get; init; }
public required string Status { get; init; }
public required DateTimeOffset Timestamp { get; init; }
public string? Justification { get; init; }
}
```
**Output**: Multiple `IEvidence` records:
- 1 record with `Type = Provenance` (from upstream)
- N records with `Type = Vex` (one per statement)
---
### ExceptionApplicationAdapter
Converts `ExceptionApplication` from Policy module.
**Input**: `ExceptionApplicationInput`
```csharp
public sealed record ExceptionApplicationInput
{
public required string ApplicationId { get; init; }
public required string TenantId { get; init; }
public required string ExceptionId { get; init; }
public required string FindingId { get; init; }
public required DateTimeOffset AppliedAt { get; init; }
public DateTimeOffset? ExpiresAt { get; init; }
public string? Reason { get; init; }
public string? AppliedBy { get; init; }
public IReadOnlyDictionary<string, string>? Properties { get; init; }
}
```
**Output**: Single `IEvidence` record with `Type = Exception`.
---
## Implementations
### InMemoryEvidenceStore
Thread-safe in-memory evidence store for testing and caching.
```csharp
var store = new InMemoryEvidenceStore();
// Store evidence
await store.StoreAsync(record);
// Retrieve by ID
var evidence = await store.GetAsync("sha256:abc123...");
// Query by subject
var subjectEvidence = await store.GetBySubjectAsync("pkg:npm/lodash@4.17.21");
// Query by type
var vexRecords = await store.GetByTypeAsync(EvidenceType.Vex);
// Check existence
var exists = await store.ExistsAsync("sha256:abc123...");
```
**Thread Safety**: Uses `ConcurrentDictionary` for all operations.
---
## Common Property Keys
Standard property keys used across evidence types:
| Key | Used By | Description |
|-----|---------|-------------|
| `cve` | Vulnerability, CveMatch | CVE identifier |
| `severity` | Vulnerability | Severity level (CRITICAL, HIGH, etc.) |
| `cvss` | Vulnerability | CVSS score |
| `status` | Vex, ProofSegment | Current status |
| `justification` | Vex, Exception | Reason for status |
| `productId` | Vex | Affected product identifier |
| `exceptionId` | Exception | Parent exception ID |
| `findingId` | Exception | Finding being excepted |
| `tenantId` | Exception | Tenant context |
| `segmentType` | ProofSegment | Type of proof segment |
| `previousSegmentId` | ProofSegment | Chain link to previous segment |
| `payloadDigest` | ProofSegment | Content digest |
| `predicateType` | Attestation | In-toto predicate type URI |
| `statementType` | Attestation | Statement type identifier |

View File

@@ -0,0 +1,342 @@
# CanonJson API Reference
**Namespace**: `StellaOps.Canonical.Json`
**Assembly**: `StellaOps.Canonical.Json`
**Version**: 1.0.0
---
## Overview
The `CanonJson` class provides RFC 8785-compliant JSON canonicalization and cryptographic hashing utilities for content-addressed identifiers. It ensures deterministic, reproducible JSON serialization across all environments.
---
## CanonVersion Class
Static class containing canonicalization version constants and utilities.
### Constants
| Constant | Type | Value | Description |
|----------|------|-------|-------------|
| `V1` | `string` | `"stella:canon:v1"` | Version 1: RFC 8785 JSON canonicalization |
| `VersionFieldName` | `string` | `"_canonVersion"` | Field name for version marker (underscore ensures first position) |
| `Current` | `string` | `V1` | Current default version for new hashes |
### Methods
#### IsVersioned
```csharp
public static bool IsVersioned(ReadOnlySpan<byte> canonicalJson)
```
Detects if canonical JSON includes a version marker.
**Parameters:**
- `canonicalJson`: UTF-8 encoded canonical JSON bytes
**Returns:** `true` if the JSON starts with `{"_canonVersion":`, `false` otherwise
**Example:**
```csharp
var json = """{"_canonVersion":"stella:canon:v1","foo":"bar"}"""u8;
bool versioned = CanonVersion.IsVersioned(json); // true
var legacy = """{"foo":"bar"}"""u8;
bool legacyVersioned = CanonVersion.IsVersioned(legacy); // false
```
---
#### ExtractVersion
```csharp
public static string? ExtractVersion(ReadOnlySpan<byte> canonicalJson)
```
Extracts the version string from versioned canonical JSON.
**Parameters:**
- `canonicalJson`: UTF-8 encoded canonical JSON bytes
**Returns:** The version string (e.g., `"stella:canon:v1"`) or `null` if not versioned
**Example:**
```csharp
var json = """{"_canonVersion":"stella:canon:v1","foo":"bar"}"""u8;
string? version = CanonVersion.ExtractVersion(json); // "stella:canon:v1"
```
---
## CanonJson Class
Static class providing JSON canonicalization and hashing methods.
### Canonicalization Methods
#### Canonicalize<T>
```csharp
public static byte[] Canonicalize<T>(T obj)
```
Canonicalizes an object to RFC 8785 JSON without version marker (legacy format).
**Parameters:**
- `obj`: The object to canonicalize
**Returns:** UTF-8 encoded canonical JSON bytes
**Example:**
```csharp
var obj = new { z = 3, a = 1 };
byte[] canonical = CanonJson.Canonicalize(obj);
// Result: {"a":1,"z":3}
```
---
#### CanonicalizeVersioned<T>
```csharp
public static byte[] CanonicalizeVersioned<T>(T obj, string version = CanonVersion.Current)
```
Canonicalizes an object with a version marker for content-addressed hashing.
**Parameters:**
- `obj`: The object to canonicalize
- `version`: Canonicalization version (default: `CanonVersion.Current`)
**Returns:** UTF-8 encoded canonical JSON bytes with version marker
**Exceptions:**
- `ArgumentNullException`: When `version` is null
- `ArgumentException`: When `version` is empty
**Example:**
```csharp
var obj = new { z = 3, a = 1 };
byte[] canonical = CanonJson.CanonicalizeVersioned(obj);
// Result: {"_canonVersion":"stella:canon:v1","a":1,"z":3}
// With explicit version
byte[] v2 = CanonJson.CanonicalizeVersioned(obj, "stella:canon:v2");
// Result: {"_canonVersion":"stella:canon:v2","a":1,"z":3}
```
---
### Hashing Methods
#### Hash<T>
```csharp
public static string Hash<T>(T obj)
```
Computes SHA-256 hash of canonical JSON (legacy format, no version marker).
**Parameters:**
- `obj`: The object to hash
**Returns:** Lowercase hex-encoded SHA-256 hash (64 characters)
**Example:**
```csharp
var obj = new { foo = "bar" };
string hash = CanonJson.Hash(obj);
// Result: "7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b"
```
---
#### HashVersioned<T>
```csharp
public static string HashVersioned<T>(T obj, string version = CanonVersion.Current)
```
Computes SHA-256 hash of versioned canonical JSON.
**Parameters:**
- `obj`: The object to hash
- `version`: Canonicalization version (default: `CanonVersion.Current`)
**Returns:** Lowercase hex-encoded SHA-256 hash (64 characters)
**Example:**
```csharp
var obj = new { foo = "bar" };
string hash = CanonJson.HashVersioned(obj);
// Different from legacy hash due to version marker
```
---
#### HashPrefixed<T>
```csharp
public static string HashPrefixed<T>(T obj)
```
Computes SHA-256 hash with `sha256:` prefix (legacy format).
**Parameters:**
- `obj`: The object to hash
**Returns:** Hash in format `sha256:<64-hex-chars>`
**Example:**
```csharp
var obj = new { foo = "bar" };
string hash = CanonJson.HashPrefixed(obj);
// Result: "sha256:7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b"
```
---
#### HashVersionedPrefixed<T>
```csharp
public static string HashVersionedPrefixed<T>(T obj, string version = CanonVersion.Current)
```
Computes SHA-256 hash with `sha256:` prefix (versioned format).
**Parameters:**
- `obj`: The object to hash
- `version`: Canonicalization version (default: `CanonVersion.Current`)
**Returns:** Hash in format `sha256:<64-hex-chars>`
**Example:**
```csharp
var obj = new { foo = "bar" };
string hash = CanonJson.HashVersionedPrefixed(obj);
// Result: "sha256:..." (different from HashPrefixed due to version marker)
```
---
## IJsonCanonicalizer Interface
Interface for JSON canonicalization implementations.
### Methods
#### Canonicalize
```csharp
byte[] Canonicalize(ReadOnlySpan<byte> json)
```
Canonicalizes UTF-8 JSON bytes per RFC 8785.
**Parameters:**
- `json`: Raw UTF-8 JSON bytes to canonicalize
**Returns:** Canonical UTF-8 JSON bytes
---
#### CanonicalizeWithVersion
```csharp
byte[] CanonicalizeWithVersion(ReadOnlySpan<byte> json, string version)
```
Canonicalizes UTF-8 JSON bytes with version marker prepended.
**Parameters:**
- `json`: Raw UTF-8 JSON bytes to canonicalize
- `version`: Version string to embed
**Returns:** Canonical UTF-8 JSON bytes with `_canonVersion` field
---
## Usage Examples
### Computing Content-Addressed IDs
```csharp
using StellaOps.Canonical.Json;
// Evidence predicate hashing
var evidence = new EvidencePredicate
{
Source = "scanner/trivy",
SbomEntryId = "sha256:91f2ab3c:pkg:npm/lodash@4.17.21",
VulnerabilityId = "CVE-2021-23337"
};
// Compute versioned hash (recommended)
string evidenceId = CanonJson.HashVersionedPrefixed(evidence);
// Result: "sha256:..."
```
### Verifying Attestations
```csharp
public bool VerifyAttestation(byte[] payload, string expectedHash)
{
// Detect format and verify accordingly
if (CanonVersion.IsVersioned(payload))
{
var version = CanonVersion.ExtractVersion(payload);
// Re-canonicalize with same version and compare
var computed = CanonJson.HashVersioned(payload, version!);
return computed == expectedHash;
}
// Legacy format
var legacyHash = CanonJson.Hash(payload);
return legacyHash == expectedHash;
}
```
### Migration from Legacy to Versioned
```csharp
// Old code (legacy)
var hash = CanonJson.Hash(predicate);
// New code (versioned) - just add "Versioned"
var hash = CanonJson.HashVersioned(predicate);
```
---
## Algorithm Details
### RFC 8785 Compliance
| Requirement | Implementation |
|-------------|----------------|
| Key ordering | Ordinal string comparison (case-sensitive, ASCII) |
| Number format | IEEE 754, shortest representation |
| String escaping | Minimal (only `"`, `\`, control characters) |
| Whitespace | None (compact output) |
| Encoding | UTF-8 without BOM |
### Version Marker Position
The `_canonVersion` field is **always first** in the output due to:
1. Underscore (`_`) sorts before all letters in ASCII
2. After injecting version, remaining keys are sorted normally
```json
{"_canonVersion":"stella:canon:v1","aaa":1,"bbb":2,"zzz":3}
```
---
## Related Documentation
- [Proof Chain Specification](../modules/attestor/proof-chain-specification.md)
- [Canonicalization Migration Guide](../operations/canon-version-migration.md)
- [RFC 8785 - JSON Canonicalization Scheme](https://datatracker.ietf.org/doc/html/rfc8785)

View File

@@ -0,0 +1,289 @@
# Triage Evidence Export API Reference
Version: 1.0
Sprint: SPRINT_9200_0001_0002, SPRINT_9200_0001_0003
Status: Stable
## Overview
The Triage Evidence Export API provides endpoints for downloading complete evidence packages as archives. These endpoints support both individual finding exports and batch exports for entire scan runs.
## Base URL
```
/api/v1/triage
```
## Endpoints
### Export Finding Evidence Bundle
Downloads a complete evidence bundle for a single finding as a ZIP or TAR.GZ archive.
```
GET /findings/{findingId}/evidence/export
```
#### Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `findingId` | string | Yes | Finding identifier |
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `format` | string | `zip` | Archive format: `zip`, `tar.gz`, `targz`, `tgz` |
#### Response Headers
| Header | Description |
|--------|-------------|
| `Content-Type` | `application/zip` or `application/gzip` |
| `Content-Disposition` | `attachment; filename="evidence-{findingId}.zip"` |
| `X-Archive-Digest` | SHA-256 digest of the archive: `sha256:{digest}` |
#### Response Codes
| Code | Description |
|------|-------------|
| 200 | Success - archive stream returned |
| 400 | Invalid format specified |
| 404 | Finding not found |
#### Example Request
```bash
curl -X GET \
"https://api.stellaops.example/api/v1/triage/findings/f-abc123/evidence/export?format=zip" \
-H "Authorization: Bearer <token>" \
-o evidence-f-abc123.zip
```
#### Example Response
Binary stream of the archive file.
### Get Unified Evidence
Retrieves the unified evidence package as JSON (not downloadable archive).
```
GET /findings/{findingId}/evidence
```
#### Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `findingId` | string | Yes | Finding identifier |
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `includeSbom` | boolean | `true` | Include SBOM evidence |
| `includeReachability` | boolean | `true` | Include reachability evidence |
| `includeVex` | boolean | `true` | Include VEX claims |
| `includeAttestations` | boolean | `true` | Include attestations |
| `includeDeltas` | boolean | `true` | Include delta evidence |
| `includePolicy` | boolean | `true` | Include policy evidence |
| `includeReplayCommand` | boolean | `true` | Include replay command |
#### Response Headers
| Header | Description |
|--------|-------------|
| `ETag` | Content-addressed cache key: `"{cacheKey}"` |
| `Cache-Control` | `private, max-age=300` |
#### Response Codes
| Code | Description |
|------|-------------|
| 200 | Success - evidence returned |
| 304 | Not Modified (ETag match) |
| 404 | Finding not found |
#### Example Request
```bash
curl -X GET \
"https://api.stellaops.example/api/v1/triage/findings/f-abc123/evidence" \
-H "Authorization: Bearer <token>" \
-H "If-None-Match: \"sha256:abc123...\""
```
#### Example Response (200 OK)
```json
{
"findingId": "f-abc123",
"cveId": "CVE-2024-1234",
"componentPurl": "pkg:npm/lodash@4.17.15",
"sbom": {
"format": "cyclonedx",
"version": "1.5",
"documentUri": "/sboms/sha256:abc123",
"digest": "sha256:abc123...",
"component": {
"purl": "pkg:npm/lodash@4.17.15",
"name": "lodash",
"version": "4.17.15",
"ecosystem": "npm"
}
},
"reachability": {
"subgraphId": "sg-xyz789",
"status": "reachable",
"confidence": 0.95,
"method": "static",
"entryPoints": [...]
},
"vexClaims": [...],
"attestations": [...],
"deltas": {...},
"policy": {...},
"manifests": {
"artifactDigest": "sha256:a1b2c3...",
"manifestHash": "sha256:def456...",
"feedSnapshotHash": "sha256:feed789...",
"policyHash": "sha256:policy321..."
},
"verification": {
"status": "verified",
"hashesVerified": true,
"attestationsVerified": true,
"evidenceComplete": true
},
"replayCommand": "stella scan replay --artifact sha256:a1b2c3... --manifest sha256:def456... --feeds sha256:feed789... --policy sha256:policy321...",
"shortReplayCommand": "stella replay snapshot --verdict V-12345",
"evidenceBundleUrl": "/v1/triage/findings/f-abc123/evidence/export",
"generatedAt": "2025-01-15T10:30:00Z",
"cacheKey": "sha256:unique123..."
}
```
### Get Replay Command
Retrieves the replay command for a finding.
```
GET /findings/{findingId}/replay-command
```
#### Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `findingId` | string | Yes | Finding identifier |
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `shells` | string[] | `["bash"]` | Target shells: `bash`, `powershell`, `cmd` |
| `includeOffline` | boolean | `false` | Include offline replay variant |
| `generateBundle` | boolean | `false` | Generate evidence bundle |
#### Response Codes
| Code | Description |
|------|-------------|
| 200 | Success - replay command returned |
| 404 | Finding not found |
#### Example Response
```json
{
"findingId": "f-abc123",
"commands": {
"bash": "stella scan replay --artifact sha256:a1b2c3... --manifest sha256:def456... --feeds sha256:feed789... --policy sha256:policy321...",
"powershell": "stella scan replay --artifact sha256:a1b2c3... --manifest sha256:def456... --feeds sha256:feed789... --policy sha256:policy321..."
},
"shortCommand": "stella replay snapshot --verdict V-12345",
"inputHashes": {
"artifactDigest": "sha256:a1b2c3...",
"manifestHash": "sha256:def456...",
"feedSnapshotHash": "sha256:feed789...",
"policyHash": "sha256:policy321..."
},
"bundleUrl": "/v1/triage/findings/f-abc123/evidence/export",
"generatedAt": "2025-01-15T10:30:00Z"
}
```
### Get Scan Replay Command
Retrieves the replay command for an entire scan.
```
GET /scans/{scanId}/replay-command
```
#### Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `scanId` | string | Yes | Scan identifier |
#### Query Parameters
Same as finding replay command endpoint.
#### Response Codes
| Code | Description |
|------|-------------|
| 200 | Success - replay command returned |
| 404 | Scan not found |
## ETag Caching
The unified evidence endpoint supports HTTP caching via ETag/If-None-Match:
1. **Initial request**: Returns evidence with `ETag` header
2. **Subsequent requests**: Include `If-None-Match: "{etag}"` header
3. **If unchanged**: Returns `304 Not Modified` (no body)
4. **If changed**: Returns `200 OK` with new evidence and ETag
Example flow:
```bash
# Initial request
curl -i "https://api.stellaops.example/api/v1/triage/findings/f-abc123/evidence"
# Response: 200 OK, ETag: "sha256:abc123..."
# Conditional request
curl -i "https://api.stellaops.example/api/v1/triage/findings/f-abc123/evidence" \
-H 'If-None-Match: "sha256:abc123..."'
# Response: 304 Not Modified (if unchanged)
```
## Archive Integrity
To verify downloaded archives:
```bash
# Get expected digest from header
EXPECTED=$(curl -sI ".../evidence/export" | grep X-Archive-Digest | cut -d: -f2-)
# Download and verify
curl -o evidence.zip ".../evidence/export"
ACTUAL=$(sha256sum evidence.zip | cut -d' ' -f1)
if [ "sha256:$ACTUAL" = "$EXPECTED" ]; then
echo "Archive verified"
else
echo "Verification failed!"
exit 1
fi
```
## See Also
- [Evidence Bundle Format Specification](../modules/cli/guides/commands/evidence-bundle-format.md)
- [stella scan replay Command Reference](../modules/cli/guides/commands/scan-replay.md)
- [Unified Evidence Model](./evidence-api-reference.md)