Files
git.stella-ops.org/docs/product-advisories/27-Nov-2025 - Deep Architecture Brief - SBOM‑First, VEX‑Ready Spine.md
master e950474a77
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
up
2025-11-27 15:16:31 +02:00

27 KiB
Raw Blame History

Heres a clear, SBOMfirst blueprint you can drop into StellaOps without extra context.


SBOMfirst spine (with attestations) — the short, practical version

High-level flow

Why this matters (plain English)

  • SBOMs (CycloneDX/SPDX) = a complete parts list of your software.
  • Attestations (intoto + DSSE) = tamperevident receipts proving who did what, to which artifact, when, and how.
  • Determinism = if you rescan tomorrow, you get the same result for the same inputs.
  • Explainability = every risk decision links back to evidence you can show to auditors/customers.

Core pipeline (modules & responsibilities)

  1. Scan (Scanner)
  • Inputs: container image / dir / repo.
  • Outputs: raw facts (packages, files, symbols), and a ScanEvidence attestation (DSSEwrapped intoto statement).
  • Must support offline feeds (bundle CVE/NVD/OSV/vendor advisories).
  1. Sbomer
  • Normalizes raw facts → canonical SBOM (CycloneDX or SPDX) with:

    • PURLs, license info, checksums, buildIDs (ELF/PE/MachO), source locations.
  • Emits SBOMProduced attestation linking SBOM ↔ image digest.

  1. Authority
  • Verifies every attestation chain (Sigstore/keys; PQ-ready option later).
  • Stamps PolicyVerified attestation (who approved, policy hash, inputs).
  • Persists trustlog: signatures, cert chains, Rekorlike index (mirrorable offline).
  1. Graph Store (Canonical Graph)
  • Ingests SBOM, vulnerabilities, reachability facts, VEX statements.
  • Preserves evidence links (edge predicates: “foundby”, “reachablevia”, “provenby”).
  • Enables deterministic replay (snapshot manifests: feeds+rules+hashes).

Stable APIs (keep these boundaries sharp)

  • /scan → start scan; returns Evidence ID + attestation ref.
  • /sbom → get canonical SBOM (by image digest or Evidence ID).
  • /attest → submit/fetch attestations; verify chain; returns trustproof.
  • /vexgate → policy decision: allow / warn / block with proof bundle.
  • /diff → SBOM↔SBOM + SBOM↔runtime diffs (see below).
  • /unknowns → create/list/resolve Unknowns (signals needing human/vendor input).

Design notes:

  • All responses include decision, explanation, evidence[], hashes, clock.
  • Support airgap: all endpoints operate on local bundles (ZIP/TAR with SBOM+attestations+feeds).

Determinism & “Unknowns” (noisekiller loop)

Smart diffs

  • SBOM↔SBOM: detect added/removed/changed components (by PURL+version+hash).
  • SBOM↔runtime: prove reachability (e.g., symbol/function use, loaded libs, process maps).
  • Score only on provable paths; gate on VEX (vendor/exploitability statements).

Unknowns handler

  • Any unresolved signal (ambiguous CVE mapping, stripped binary, unverified vendor VEX) → Unknowns queue:

    • SLA, owner, evidence snapshot, audit trail.
    • State machine: new → triage → vendorquery → verified → closed.
    • Every VEX or vendor reply becomes an attestation; decisions reevaluated deterministically.

What to store (so you can explain every decision)

  • Artifacts: image digest, SBOM hash, feed versions, rule set hash.

  • Proofs: DSSE envelopes, signatures, certs, inclusion proofs (Rekorstyle).

  • Predicates (edges):

    • contains(component), vulnerable_to(cve), reachable_via(callgraph|runtime),
    • overridden_by(vex), verified_by(authority), derived_from(scan-evidence).
  • Whystrings: humanreadable proof trails (13 sentences) output with every decision.


Minimal policies that work on day 1

  • Block only when: vuln.severity ≥ High AND reachable == true AND no VEX allows.
  • Warn when: High/Critical but reachable == unknown → route to Unknowns with SLA.
  • Allow when: Low/Medium OR VEX says not_affected (trusted signer + policy).

Offline/airgap bundle format (zip)

/bundle/
  feeds/ (NVD, OSV, vendor) + manifest.json (hashes, timestamps)
  sboms/ imageDigest.json
  attestations/ *.jsonl (DSSE)
  proofs/ rekor/ merkle.json
  policy/ lattice.json
  replay/ inputs.lock (contenthashes of everything above)
  • Every API accepts ?bundle=/path/to/bundle.zip.
  • Replay: inputs.lock guarantees deterministic reevaluation.

.NET 10 implementation sketch (pragmatic)

  • Contracts: StellaOps.Contracts.* (Scan, Attest, VexGate, Diff, Unknowns).
  • Attestations: StellaOps.Attest.Dsse (IEnvelope, IStatement); pluggable crypto (FIPS/GOST/SM/PQ).
  • SBOM: StellaOps.Sbom (CycloneDX/SPDX models + mappers; PURL utilities).
  • Graph: StellaOps.Graph (EF Core 9/10 over Mongo/Postgres; edge predicates as enums + JSON evidence).
  • Policy/Lattice: StellaOps.Policy.Lattice (pure functions over graph snapshots; produce Decision+Why).
  • Unknowns: StellaOps.Unknowns (aggregate root; SLA timers; audit events).
  • CLI: stella scan|sbom|attest|vex-gate|diff|unknowns with --bundle.

Key patterns:

  • All public methods return (Result<T> value, Proof proof).
  • Hash everything (inputs, rules, feeds) and bake into Proof.
  • Emit DSSE envelopes at each hop; verify on ingest; never trust unauthenticated input.

Next steps (weekone checklist)

  • Define JSON schemas for: Decision (with Why+Proof), Unknown, DiffResult.
  • Stand up /scan, /sbom, /attest, /vexgate skeletons returning mock Proofs.
  • Implement SBOM↔SBOM diff (PURL+version+hash) and wire to /diff.
  • Create bundle.zip reader/writer + inputs.lock.
  • Add Authority.Verify() with DSSE verification + truststore.
  • Ship a basic policy.lattice.json (3 rules above) and e2e test: image → allow/block with proofs.

If you want, I can generate:

  • The exact C# interfaces/classes (namespaces + DTOs),
  • JSON Schemas for Decision/Proof/Unknowns,
  • A seed bundle.zip layout with fake data for local e2e. Cool, lets turn that blueprint into something your devs can actually build from.

Below is a detailed, opinionated implementation plan you can paste into an engineering doc and assign as epics / tickets.


0. Solution layout (concrete)

Repo structure

/stella-ops
  /src
    StellaOps.Contracts       // DTOs, API contracts, JSON schemas
    StellaOps.Domain          // Core domain types (ArtifactId, Proof, Decision, etc.)
    StellaOps.Attest          // DSSE envelopes, in-toto statements, signing/verification
    StellaOps.Sbom            // SBOM models + normalization
    StellaOps.Graph           // Graph store, entities, queries
    StellaOps.Policy          // Policy engine (lattice evaluation)
    StellaOps.WebApi          // HTTP APIs: /scan, /sbom, /attest, /vex-gate, /diff, /unknowns
    StellaOps.Cli             // `stella` CLI, offline bundles
  /tests
    StellaOps.Tests.Unit
    StellaOps.Tests.Integration
    StellaOps.Tests.E2E

Baseline tech assumptions

  • Runtime: .NET (8+; you can call it “.NET 10” in your roadmap).
  • API: ASP.NET Core minimal APIs.
  • DB: Postgres (via EF Core) for graph + unknowns + metadata.
  • Storage: local filesystem / S3-compatible for bundle zips, scanner DB caches.
  • External scanners: Trivy / Grype / Syft (invoked via CLI with deterministic config).

1. Core domain & shared contracts (Phase 1)

Goal: Have a stable core domain + contracts that all teams can build against.

1.1 Core domain types (StellaOps.Domain)

Implement:

public readonly record struct Digest(string Algorithm, string Value); // e.g. ("sha256", "abcd...")
public readonly record struct ArtifactRef(string Kind, string Value);
// Kind: "container-image", "file", "package", "sbom", etc.

public readonly record struct EvidenceId(Guid Value);
public readonly record struct AttestationId(Guid Value);

public enum PredicateType
{
    ScanEvidence,
    SbomProduced,
    PolicyVerified,
    VulnerabilityFinding,
    ReachabilityFinding,
    VexStatement
}

public sealed class Proof
{
    public string ProofId { get; init; } = default!;
    public Digest InputsLock { get; init; } = default!; // hash of feeds+rules+sbom bundle
    public DateTimeOffset EvaluatedAt { get; init; }
    public IReadOnlyList<string> EvidenceIds { get; init; } = Array.Empty<string>();
    public IReadOnlyDictionary<string,string> Meta { get; init; } = new Dictionary<string,string>();
}

1.2 Attestation model (StellaOps.Attest)

Implement DSSE + intoto abstractions:

public sealed class DsseEnvelope
{
    public string PayloadType { get; init; } = default!;
    public string Payload { get; init; } = default!; // base64url(JSON)
    public IReadOnlyList<DsseSignature> Signatures { get; init; } = Array.Empty<DsseSignature>();
}

public sealed class DsseSignature
{
    public string KeyId { get; init; } = default!;
    public string Sig { get; init; } = default!; // base64url
}

public interface IStatement<out TPredicate>
{
    string Type { get; }               // in-toto type URI
    string PredicateType { get; }      // URI or enum -> string
    TPredicate Predicate { get; }
    string Subject { get; }            // e.g., image digest
}

Attestation services:

public interface IAttestationSigner
{
    Task<DsseEnvelope> SignAsync<TPredicate>(IStatement<TPredicate> statement, CancellationToken ct);
}

public interface IAttestationVerifier
{
    Task VerifyAsync(DsseEnvelope envelope, CancellationToken ct);
}

1.3 Decision & VEX-gate contracts (StellaOps.Contracts)

public enum GateDecisionKind
{
    Allow,
    Warn,
    Block
}

public sealed class GateDecision
{
    public GateDecisionKind Decision { get; init; }
    public string Reason { get; init; } = default!;     // short human-readable
    public Proof Proof { get; init; } = default!;
    public IReadOnlyList<string> Evidence { get; init; } = Array.Empty<string>(); // EvidenceIds / AttestationIds
}

public sealed class VexGateRequest
{
    public ArtifactRef Artifact { get; init; }
    public string? Environment { get; init; }     // "prod", "staging", cluster id, etc.
    public string? BundlePath { get; init; }      // optional offline bundle path
}

Acceptance criteria

  • Shared projects compile.
  • No service references each other directly (only via Contracts + Domain).
  • Example test that serializes/deserializes GateDecision and DsseEnvelope using System.Text.Json.

2. SBOM pipeline (Scanner → Sbomer) (Phase 2)

Goal: For a container image, produce a canonical SBOM + attestation deterministically.

2.1 Scanner integration (StellaOps.WebApi + StellaOps.Cli)

API contract (/scan)

public sealed class ScanRequest
{
    public string SourceType { get; init; } = default!; // "container-image" | "directory" | "git-repo"
    public string Locator { get; init; } = default!;    // e.g. "registry/myapp:1.2.3"
    public bool IncludeFiles { get; init; } = true;
    public bool IncludeLicenses { get; init; } = true;
    public string? BundlePath { get; init; }            // for offline data
}

public sealed class ScanResponse
{
    public EvidenceId EvidenceId { get; init; }
    public AttestationId AttestationId { get; init; }
    public Digest ArtifactDigest { get; init; } = default!;
}

Implementation steps

  1. Scanner abstraction
public interface IArtifactScanner
{
    Task<ScanResult> ScanAsync(ScanRequest request, CancellationToken ct);
}

public sealed class ScanResult
{
    public ArtifactRef Artifact { get; init; } = default!;
    public Digest ArtifactDigest { get; init; } = default!;
    public IReadOnlyList<DiscoveredPackage> Packages { get; init; } = Array.Empty<DiscoveredPackage>();
    public IReadOnlyList<DiscoveredFile> Files { get; init; } = Array.Empty<DiscoveredFile>();
}
  1. CLI wrapper (Trivy/Grype/Syft):
  • Implement SyftScanner : IArtifactScanner:

    • Invoke external CLI with fixed flags.

    • Use JSON output mode.

    • Resolve CLI path from config.

    • Ensure deterministic:

      • Disable auto-updating DB.
      • Use a local DB path versioned and optionally included into bundle.
  • Write parsing code Syft → ScanResult.

  • Add retry & clear error mapping (timeout, auth error, network error).

  1. /scan endpoint
  • Validate request.
  • Call IArtifactScanner.ScanAsync.
  • Build a ScanEvidence predicate:
public sealed class ScanEvidencePredicate
{
    public ArtifactRef Artifact { get; init; } = default!;
    public Digest ArtifactDigest { get; init; } = default!;
    public DateTimeOffset ScannedAt { get; init; }
    public string ScannerName { get; init; } = default!;
    public string ScannerVersion { get; init; } = default!;
    public IReadOnlyList<DiscoveredPackage> Packages { get; init; } = Array.Empty<DiscoveredPackage>();
}
  • Build intoto statement for predicate.

  • Call IAttestationSigner.SignAsync, persist:

    • Raw envelope to attestations table.
    • Map to EvidenceId + AttestationId.

Acceptance criteria

  • Given a fixed image and fixed scanner DB, repeated /scan calls produce identical:

    • ScanResult (up to ordering).
    • ScanEvidence payload.
    • InputsLock proof hash (once implemented).
  • E2E test: run scan on a small public image in CI using a pre-bundled scanner DB.


2.2 Sbomer (StellaOps.Sbom + /sbom)

Goal: Normalize ScanResult into a canonical SBOM (CycloneDX/SPDX) + emit SBOM attestation.

Models

Create neutral SBOM model (internal):

public sealed class CanonicalComponent
{
    public string Name { get; init; } = default!;
    public string Version { get; init; } = default!;
    public string Purl { get; init; } = default!;
    public string? License { get; init; }
    public Digest Digest { get; init; } = default!;
    public string? SourceLocation { get; init; } // file path, layer info
}

public sealed class CanonicalSbom
{
    public string SbomId { get; init; } = default!;
    public ArtifactRef Artifact { get; init; } = default!;
    public Digest ArtifactDigest { get; init; } = default!;
    public IReadOnlyList<CanonicalComponent> Components { get; init; } = Array.Empty<CanonicalComponent>();
    public DateTimeOffset CreatedAt { get; init; }
    public string Format { get; init; } = "CycloneDX-JSON-1.5"; // default
}

Sbomer service

public interface ISbomer
{
    CanonicalSbom FromScan(ScanResult scan);
    string ToCycloneDxJson(CanonicalSbom sbom);
    string ToSpdxJson(CanonicalSbom sbom);
}

Implementation details:

  • Map OS/deps to PURLs (use existing PURL libs or implement minimal helpers).

  • Stable ordering:

    • Sort components by Purl then Version before serialization.
  • Hash the SBOM JSON → Digest (e.g., Digest("sha256", "...")).

SBOM attestation & /sbom endpoint

  • For an ArtifactRef (or ScanEvidence EvidenceId):

    1. Fetch latest ScanResult from DB.
    2. Call ISbomer.FromScan.
    3. Serialize to CycloneDX.
    4. Emit SbomProduced predicate & DSSE envelope.
    5. Persist SBOM JSON blob & link to artifact.

Acceptance criteria

  • Same ScanResult always produces bit-identical SBOM JSON.

  • Unit tests verifying:

    • PURL mapping correctness.
    • Stable ordering.
  • /sbom endpoint can:

    • Build SBOM from scan.
    • Return existing SBOM if already generated (idempotence).

3. Attestation Authority & trust log (Phase 3)

Goal: Verify all attestations, store them with a trust log, and produce PolicyVerified attestations.

3.1 Authority service (StellaOps.Attest + StellaOps.WebApi)

Key interfaces:

public interface IAuthority
{
    Task<AttestationId> RecordAsync(DsseEnvelope envelope, CancellationToken ct);
    Task<Proof> VerifyChainAsync(ArtifactRef artifact, CancellationToken ct);
}

Implementation steps:

  1. Attestations store
  • Table attestations:

    • id (AttestationId, PK)
    • artifact_kind / artifact_value
    • predicate_type (enum)
    • payload_type
    • payload_hash
    • envelope_json
    • created_at
    • signer_keyid
  • Table trust_log:

    • id
    • attestation_id
    • status (verified / failed / pending)
    • reason
    • verified_at
    • verification_data_json (cert chain, Rekor log index, etc.)
  1. Verification pipeline
  • Implement IAttestationVerifier.VerifyAsync:

    • Check envelope integrity (no duplicate signatures, required fields).
    • Verify crypto signature (keys from configuration store or Sigstore if you integrate later).
  • IAuthority.RecordAsync:

    • Verify envelope.
    • Save to attestations.
    • Add entry to trust_log.
  • VerifyChainAsync:

    • For a given ArtifactRef:

      • Load all attestations for that artifact.

      • Ensure each is status=verified.

      • Compute InputsLock = hash of:

        • Sorted predicate payloads.
        • Feeds manifest.
        • Policy rules.
      • Return Proof.

3.2 /attest API

  • POST /attest: submit DSSE envelope (for external tools).
  • GET /attest?artifact=...: list attestations + trust status.
  • GET /attest/{id}/proof: return verification proof (including InputsLock).

Acceptance criteria

  • Invalid signatures rejected.
  • Tampering test: alter a byte in envelope JSON → verification fails.
  • VerifyChainAsync returns same Proof.InputsLock for identical sets of inputs.

4. Graph Store & Policy engine (Phase 4)

Goal: Store SBOM, vulnerabilities, reachability, VEX, and query them to make deterministic VEX-gate decisions.

4.1 Graph model (StellaOps.Graph)

Tables (simplified):

  • artifacts:

    • id (PK), kind, value, digest_algorithm, digest_value
  • components:

    • id, purl, name, version, license, digest_algorithm, digest_value
  • vulnerabilities:

    • id, cve_id, severity, source (NVD/OSV/vendor), data_json
  • vex_statements:

    • id, cve_id, component_purl, status (not_affected, affected, etc.), source, data_json
  • edges:

    • id, from_kind, from_id, to_kind, to_id, relation (enum), evidence_id, data_json

Example relation values:

  • artifact_contains_component
  • component_vulnerable_to
  • component_reachable_via
  • vulnerability_overridden_by_vex
  • artifact_scanned_by
  • decision_verified_by

Graph access abstraction:

public interface IGraphRepository
{
    Task UpsertSbomAsync(CanonicalSbom sbom, EvidenceId evidenceId, CancellationToken ct);
    Task ApplyVulnerabilityFactsAsync(IEnumerable<VulnerabilityFact> facts, CancellationToken ct);
    Task ApplyReachabilityFactsAsync(IEnumerable<ReachabilityFact> facts, CancellationToken ct);
    Task ApplyVexStatementsAsync(IEnumerable<VexStatement> vexStatements, CancellationToken ct);

    Task<ArtifactGraphSnapshot> GetSnapshotAsync(ArtifactRef artifact, CancellationToken ct);
}

ArtifactGraphSnapshot is an in-memory projection used by the policy engine.

4.2 Policy engine (StellaOps.Policy)

Policy lattice (minimal version):

public enum RiskState
{
    Clean,
    VulnerableNotReachable,
    VulnerableReachable,
    Unknown
}

public sealed class PolicyEvaluationContext
{
    public ArtifactRef Artifact { get; init; } = default!;
    public ArtifactGraphSnapshot Snapshot { get; init; } = default!;
    public IReadOnlyDictionary<string,string>? Environment { get; init; }
}

public interface IPolicyEngine
{
    GateDecision Evaluate(PolicyEvaluationContext context);
}

Default policy logic:

  1. For each vulnerability affecting a component in the artifact:

    • Check for VEX:

      • If trusted VEX says not_affected → ignore.
    • Check reachability:

      • If proven reachable → mark as VulnerableReachable.
      • If proven not reachable → VulnerableNotReachable.
      • If unknown → Unknown.
  2. Aggregate:

  • If any Critical/High in VulnerableReachableBlock.
  • Else if any Critical/High in UnknownWarn and log Unknowns.
  • Else → Allow.

4.3 /vex-gate endpoint

Implementation:

  • Resolve ArtifactRef.
  • Build ArtifactGraphSnapshot using IGraphRepository.GetSnapshotAsync.
  • Call IPolicyEngine.Evaluate.
  • Request IAuthority.VerifyChainAsyncProof.
  • Emit PolicyVerified attestation for this decision.
  • Return GateDecision + Proof.

Acceptance criteria

  • Given a fixture DB snapshot, calling /vex-gate twice yields identical decisions & proof IDs.

  • Policy behavior matches the rule text:

    • Regression test that modifies severity or reachability → correct decision changes.

5. Diffs & Unknowns workflow (Phase 5)

5.1 Diff engine (/diff)

Contracts:

public sealed class DiffRequest
{
    public string Kind { get; init; } = default!; // "sbom-sbom" | "sbom-runtime"
    public string LeftId { get; init; } = default!;
    public string RightId { get; init; } = default!;
}

public sealed class DiffComponentChange
{
    public string Purl { get; init; } = default!;
    public string ChangeType { get; init; } = default!; // "added" | "removed" | "changed"
    public string? OldVersion { get; init; }
    public string? NewVersion { get; init; }
}

public sealed class DiffResponse
{
    public IReadOnlyList<DiffComponentChange> Components { get; init; } = Array.Empty<DiffComponentChange>();
}

Implementation:

  • SBOM↔SBOM: compare CanonicalSbom.Components by PURL (+ version).

  • SBOM↔runtime:

    • Input runtime snapshot (process maps, loaded libs, etc.) from agents.
    • Map runtime libs to PURLs.
    • Determine reachable components from runtime usage → ReachabilityFacts into graph.

5.2 Unknowns module (/unknowns)

Data model:

public enum UnknownState
{
    New,
    Triage,
    VendorQuery,
    Verified,
    Closed
}

public sealed class Unknown
{
    public Guid Id { get; init; }
    public ArtifactRef Artifact { get; init; } = default!;
    public string Type { get; init; } = default!; // "vuln-mapping", "reachability", "vex-trust"
    public string Subject { get; init; } = default!; // e.g., "CVE-2024-XXXX / purl:pkg:..."
    public UnknownState State { get; set; }
    public DateTimeOffset CreatedAt { get; init; }
    public DateTimeOffset? SlaDeadline { get; set; }
    public string? Owner { get; set; }
    public string EvidenceJson { get; init; } = default!; // serialized proof / edges
    public string? ResolutionNotes { get; set; }
}

API:

  • GET /unknowns: filter by state, artifact, owner.
  • POST /unknowns: create manual unknown.
  • PATCH /unknowns/{id}: update state, owner, notes.

Integration:

  • Policy engine:

    • For any Unknown risk state, auto-create Unknown with SLA if not already present.
  • When Unknown resolves (e.g., vendor VEX added), re-run policy evaluation for affected artifact(s).

Acceptance criteria

  • When VulnerableReachability is Unknown, /vex-gate both:

    • Returns Warn.
    • Creates an Unknown row.
  • Transitioning Unknown to Verified triggers re-evaluation (integration test).


6. Offline / airgapped bundles (Phase 6)

Goal: Everything works on a single machine with no network.

6.1 Bundle format & IO (StellaOps.Cli + StellaOps.WebApi)

Directory structure inside ZIP:

/bundle/
  feeds/
    manifest.json           // hashes, timestamps for NVD, OSV, vendor feeds
    nvd.json
    osv.json
    vendor-*.json
  sboms/
    {artifactDigest}.json
  attestations/
    *.jsonl                 // one DSSE envelope per line
  proofs/
    rekor/
      merkle.json
  policy/
    lattice.json            // serialized rules / thresholds
  replay/
    inputs.lock             // hash & metadata of all of the above

Implement:

public interface IBundleReader
{
    Task<Bundle> ReadAsync(string path, CancellationToken ct);
}

public interface IBundleWriter
{
    Task WriteAsync(Bundle bundle, string path, CancellationToken ct);
}

Bundle holds strongly-typed representations of the manifest, SBOMs, attestations, proofs, etc.

6.2 CLI commands

  • stella scan --image registry/app:1.2.3 --out bundle.zip

    • Runs scan + sbom locally.

    • Writes bundle with:

      • SBOM.
      • Scan + Sbom attestations.
      • Feeds manifest.
  • stella vex-gate --bundle bundle.zip

    • Loads bundle.
    • Runs policy engine locally.
    • Prints Allow/Warn/Block + proof summary.

Acceptance criteria

  • Given the same bundle.zip, stella vex-gate on different machines produces identical decisions and proof hashes.
  • /vex-gate?bundle=/path/to/bundle.zip in API uses same BundleReader and yields same output as CLI.

7. Testing & quality plan

7.1 Unit tests

  • Domain & Contracts:

    • Serialization roundtrip for all DTOs.
  • Attest:

    • DSSE encode/decode.
    • Signature verification with test key pair.
  • Sbom:

    • Known ScanResult → expected SBOM JSON snapshot.
  • Policy:

    • Table-driven tests:

      • Cases: {severity, reachable, hasVex} → {Allow/Warn/Block}.

7.2 Integration tests

  • Scanner:

    • Use a tiny test image with known components.
  • Graph + Policy:

    • Seed DB with:

      • 1 artifact, 2 components, 1 vuln, 1 VEX, 1 reachability fact.
    • Assert that /vex-gate returns expected decision.

7.3 E2E scenario

Single test flow:

  1. POST /scan → EvidenceId.
  2. POST /sbom → SBOM + SbomProduced attestation.
  3. Load dummy vulnerability feed → ApplyVulnerabilityFactsAsync.
  4. POST /vex-gate → Block (no VEX).
  5. Add VEX statement → ApplyVexStatementsAsync.
  6. POST /vex-gate → Allow.

Assertions:

  • All decisions contain Proof with non-empty InputsLock.
  • InputsLock is identical between runs with unchanged inputs.

8. Concrete backlog (you can paste into Jira)

Epic 1 Foundations

  • Task: Create solution & project skeleton.
  • Task: Implement core domain types (Digest, ArtifactRef, EvidenceId, Proof).
  • Task: Implement DSSE envelope + JSON serialization.
  • Task: Implement basic IAttestationSigner with local key pair.
  • Task: Define GateDecision & VexGateRequest contracts.

Epic 2 Scanner & Sbomer

  • Task: Implement IArtifactScanner + SyftScanner.
  • Task: Implement /scan endpoint + attestation.
  • Task: Implement ISbomer & canonical SBOM model.
  • Task: Implement /sbom endpoint + SbomProduced attestation.
  • Task: Snapshot tests for SBOM determinism.

Epic 3 Authority & Trust log

  • Task: Design attestations & trust_log tables (EF Core migrations).
  • Task: Implement IAuthority.RecordAsync + VerifyChainAsync.
  • Task: Implement /attest endpoints.
  • Task: Add proof generation (InputsLock hashing).

Epic 4 Graph & Policy

  • Task: Create graph schema (artifacts, components, vulnerabilities, edges, vex_statements).
  • Task: Implement IGraphRepository.UpsertSbomAsync.
  • Task: Ingest vulnerability feed (NVD/OSV) into graph facts.
  • Task: Implement minimal IPolicyEngine with rules.
  • Task: Implement /vex-gate endpoint.

Epic 5 Diff & Unknowns

  • Task: Implement SBOM↔SBOM diff logic + /diff.
  • Task: Create unknowns table + API.
  • Task: Wire policy engine to auto-create Unknowns.
  • Task: Add re-evaluation when Unknown state changes.

Epic 6 Offline bundles & CLI

  • Task: Implement BundleReader / BundleWriter.
  • Task: Implement stella scan and stella vex-gate.
  • Task: Add ?bundle= parameter support in APIs.

If youd like, I can next:

  • Turn this into actual C# interface files (ready to drop into your repo), or
  • Produce a JSON OpenAPI sketch for /scan, /sbom, /attest, /vex-gate, /diff, /unknowns.