diff --git a/bench/reachability-benchmark/cases/js/express-eval/outputs/SINK_REACHED b/bench/reachability-benchmark/cases/js/express-eval/outputs/SINK_REACHED new file mode 100644 index 000000000..f32a5804e --- /dev/null +++ b/bench/reachability-benchmark/cases/js/express-eval/outputs/SINK_REACHED @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/express-eval/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/js/express-eval/outputs/binary.tar.gz new file mode 100644 index 000000000..d3bad9329 Binary files /dev/null and b/bench/reachability-benchmark/cases/js/express-eval/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/js/express-eval/outputs/coverage.json b/bench/reachability-benchmark/cases/js/express-eval/outputs/coverage.json new file mode 100644 index 000000000..f9fab484d --- /dev/null +++ b/bench/reachability-benchmark/cases/js/express-eval/outputs/coverage.json @@ -0,0 +1,15 @@ +{ + "files": { + "src/app.js": { + "lines_covered": [ + 5, + 6, + 7, + 13, + 18, + 19 + ], + "lines_total": 40 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/express-eval/outputs/traces/traces.json b/bench/reachability-benchmark/cases/js/express-eval/outputs/traces/traces.json new file mode 100644 index 000000000..9695e5157 --- /dev/null +++ b/bench/reachability-benchmark/cases/js/express-eval/outputs/traces/traces.json @@ -0,0 +1,10 @@ +{ + "entry": "POST /api/admin/exec", + "path": [ + "app.js::createServer", + "handler", + "eval(code)" + ], + "sink": "ExpressEval::exec", + "notes": "Admin exec reached" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/express-guarded/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/js/express-guarded/outputs/binary.tar.gz new file mode 100644 index 000000000..4fc170e49 Binary files /dev/null and b/bench/reachability-benchmark/cases/js/express-guarded/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/js/express-guarded/outputs/coverage.json b/bench/reachability-benchmark/cases/js/express-guarded/outputs/coverage.json new file mode 100644 index 000000000..dfd568cf6 --- /dev/null +++ b/bench/reachability-benchmark/cases/js/express-guarded/outputs/coverage.json @@ -0,0 +1,16 @@ +{ + "files": { + "src/app.js": { + "lines_covered": [ + 5, + 6, + 7, + 12, + 13, + 14, + 15 + ], + "lines_total": 50 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/express-guarded/outputs/traces/traces.json b/bench/reachability-benchmark/cases/js/express-guarded/outputs/traces/traces.json new file mode 100644 index 000000000..b3c9f7a9e --- /dev/null +++ b/bench/reachability-benchmark/cases/js/express-guarded/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /api/admin/exec", + "path": [ + "app.js::createServer", + "guard: ALLOW_EXEC!=true" + ], + "sink": "ExpressGuarded::exec", + "notes": "Guard blocked sink" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/fastify-template/outputs/SINK_REACHED b/bench/reachability-benchmark/cases/js/fastify-template/outputs/SINK_REACHED new file mode 100644 index 000000000..f32a5804e --- /dev/null +++ b/bench/reachability-benchmark/cases/js/fastify-template/outputs/SINK_REACHED @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/fastify-template/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/js/fastify-template/outputs/binary.tar.gz new file mode 100644 index 000000000..37fff98e0 Binary files /dev/null and b/bench/reachability-benchmark/cases/js/fastify-template/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/js/fastify-template/outputs/coverage.json b/bench/reachability-benchmark/cases/js/fastify-template/outputs/coverage.json new file mode 100644 index 000000000..c349e0859 --- /dev/null +++ b/bench/reachability-benchmark/cases/js/fastify-template/outputs/coverage.json @@ -0,0 +1,15 @@ +{ + "files": { + "src/app.js": { + "lines_covered": [ + 5, + 6, + 7, + 13, + 18, + 20 + ], + "lines_total": 45 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/fastify-template/outputs/traces/traces.json b/bench/reachability-benchmark/cases/js/fastify-template/outputs/traces/traces.json new file mode 100644 index 000000000..9af737fde --- /dev/null +++ b/bench/reachability-benchmark/cases/js/fastify-template/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /api/render", + "path": [ + "app.js::createServer", + "render template" + ], + "sink": "FastifyTemplate::render", + "notes": "Template rendered with user input" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/guarded-eval/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/js/guarded-eval/outputs/binary.tar.gz new file mode 100644 index 000000000..c37572eb1 Binary files /dev/null and b/bench/reachability-benchmark/cases/js/guarded-eval/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/js/guarded-eval/outputs/coverage.json b/bench/reachability-benchmark/cases/js/guarded-eval/outputs/coverage.json new file mode 100644 index 000000000..dc828d474 --- /dev/null +++ b/bench/reachability-benchmark/cases/js/guarded-eval/outputs/coverage.json @@ -0,0 +1,15 @@ +{ + "files": { + "src/app.js": { + "lines_covered": [ + 5, + 6, + 7, + 9, + 10, + 11 + ], + "lines_total": 32 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/guarded-eval/outputs/traces/traces.json b/bench/reachability-benchmark/cases/js/guarded-eval/outputs/traces/traces.json new file mode 100644 index 000000000..eda3523b0 --- /dev/null +++ b/bench/reachability-benchmark/cases/js/guarded-eval/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /api/exec", + "path": [ + "app.js:handleRequest", + "guard: FEATURE_ENABLE != 1" + ], + "sink": "GuardedEval::handleRequest", + "notes": "Guard prevented sink execution" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/SINK_REACHED b/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/SINK_REACHED new file mode 100644 index 000000000..f32a5804e --- /dev/null +++ b/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/SINK_REACHED @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/binary.tar.gz new file mode 100644 index 000000000..12c1a9f24 Binary files /dev/null and b/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/coverage.json b/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/coverage.json new file mode 100644 index 000000000..5122d2042 --- /dev/null +++ b/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/coverage.json @@ -0,0 +1,14 @@ +{ + "files": { + "src/app.js": { + "lines_covered": [ + 5, + 6, + 7, + 12, + 15 + ], + "lines_total": 30 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/traces/traces.json b/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/traces/traces.json new file mode 100644 index 000000000..eb7cac9b2 --- /dev/null +++ b/bench/reachability-benchmark/cases/js/unsafe-eval/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /api/exec", + "path": [ + "app.js:handleRequest", + "eval(code)" + ], + "sink": "UnsafeEval::handleRequest", + "notes": "Test-driven dynamic trace" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/django-ssti/outputs/SINK_REACHED b/bench/reachability-benchmark/cases/py/django-ssti/outputs/SINK_REACHED new file mode 100644 index 000000000..f32a5804e --- /dev/null +++ b/bench/reachability-benchmark/cases/py/django-ssti/outputs/SINK_REACHED @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/django-ssti/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/py/django-ssti/outputs/binary.tar.gz new file mode 100644 index 000000000..608f5f7d1 Binary files /dev/null and b/bench/reachability-benchmark/cases/py/django-ssti/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/py/django-ssti/outputs/coverage.json b/bench/reachability-benchmark/cases/py/django-ssti/outputs/coverage.json new file mode 100644 index 000000000..e46fc234c --- /dev/null +++ b/bench/reachability-benchmark/cases/py/django-ssti/outputs/coverage.json @@ -0,0 +1,16 @@ +{ + "files": { + "src/app.py": { + "lines_covered": [ + 3, + 4, + 5, + 7, + 8, + 9, + 10 + ], + "lines_total": 38 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/django-ssti/outputs/traces/traces.json b/bench/reachability-benchmark/cases/py/django-ssti/outputs/traces/traces.json new file mode 100644 index 000000000..692c53fc8 --- /dev/null +++ b/bench/reachability-benchmark/cases/py/django-ssti/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /render", + "path": [ + "app.py::handle_request", + "render" + ], + "sink": "DjangoSSTI::render", + "notes": "Template rendered (autoescape off)" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/binary.tar.gz new file mode 100644 index 000000000..8b6001ce2 Binary files /dev/null and b/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/coverage.json b/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/coverage.json new file mode 100644 index 000000000..b024ed4b6 --- /dev/null +++ b/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/coverage.json @@ -0,0 +1,15 @@ +{ + "files": { + "src/app.py": { + "lines_covered": [ + 3, + 4, + 5, + 8, + 9, + 11 + ], + "lines_total": 40 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/traces/traces.json b/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/traces/traces.json new file mode 100644 index 000000000..41c4eabd8 --- /dev/null +++ b/bench/reachability-benchmark/cases/py/fastapi-guarded/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /exec", + "path": [ + "app.py::handle_request", + "guard: ALLOW_EXEC!=true" + ], + "sink": "FastApiGuarded::handle_request", + "notes": "Guard blocked eval" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/flask-template/outputs/SINK_REACHED b/bench/reachability-benchmark/cases/py/flask-template/outputs/SINK_REACHED new file mode 100644 index 000000000..f32a5804e --- /dev/null +++ b/bench/reachability-benchmark/cases/py/flask-template/outputs/SINK_REACHED @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/flask-template/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/py/flask-template/outputs/binary.tar.gz new file mode 100644 index 000000000..5ea77af56 Binary files /dev/null and b/bench/reachability-benchmark/cases/py/flask-template/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/py/flask-template/outputs/coverage.json b/bench/reachability-benchmark/cases/py/flask-template/outputs/coverage.json new file mode 100644 index 000000000..4b67cf44d --- /dev/null +++ b/bench/reachability-benchmark/cases/py/flask-template/outputs/coverage.json @@ -0,0 +1,16 @@ +{ + "files": { + "src/app.py": { + "lines_covered": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "lines_total": 40 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/flask-template/outputs/traces/traces.json b/bench/reachability-benchmark/cases/py/flask-template/outputs/traces/traces.json new file mode 100644 index 000000000..5f99bfd53 --- /dev/null +++ b/bench/reachability-benchmark/cases/py/flask-template/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /render", + "path": [ + "app.py::handle_request", + "render" + ], + "sink": "FlaskTemplate::render", + "notes": "Template rendered" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/guarded-exec/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/py/guarded-exec/outputs/binary.tar.gz new file mode 100644 index 000000000..4b30ff628 Binary files /dev/null and b/bench/reachability-benchmark/cases/py/guarded-exec/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/py/guarded-exec/outputs/coverage.json b/bench/reachability-benchmark/cases/py/guarded-exec/outputs/coverage.json new file mode 100644 index 000000000..b8afa952b --- /dev/null +++ b/bench/reachability-benchmark/cases/py/guarded-exec/outputs/coverage.json @@ -0,0 +1,15 @@ +{ + "files": { + "src/app.py": { + "lines_covered": [ + 3, + 4, + 5, + 8, + 9, + 11 + ], + "lines_total": 34 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/guarded-exec/outputs/traces/traces.json b/bench/reachability-benchmark/cases/py/guarded-exec/outputs/traces/traces.json new file mode 100644 index 000000000..022ac21d8 --- /dev/null +++ b/bench/reachability-benchmark/cases/py/guarded-exec/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /api/exec", + "path": [ + "app.py::handle_request", + "guard: FEATURE_ENABLE != 1" + ], + "sink": "PyGuardedExec::handle_request", + "notes": "Guard blocked eval" +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/SINK_REACHED b/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/SINK_REACHED new file mode 100644 index 000000000..f32a5804e --- /dev/null +++ b/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/SINK_REACHED @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/binary.tar.gz b/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/binary.tar.gz new file mode 100644 index 000000000..8cdbdd2b4 Binary files /dev/null and b/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/binary.tar.gz differ diff --git a/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/coverage.json b/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/coverage.json new file mode 100644 index 000000000..a82cb27d6 --- /dev/null +++ b/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/coverage.json @@ -0,0 +1,14 @@ +{ + "files": { + "src/app.py": { + "lines_covered": [ + 3, + 4, + 5, + 8, + 10 + ], + "lines_total": 30 + } + } +} \ No newline at end of file diff --git a/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/traces/traces.json b/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/traces/traces.json new file mode 100644 index 000000000..fe92c1fca --- /dev/null +++ b/bench/reachability-benchmark/cases/py/unsafe-exec/outputs/traces/traces.json @@ -0,0 +1,9 @@ +{ + "entry": "POST /api/exec", + "path": [ + "app.py::handle_request", + "eval(code)" + ], + "sink": "PyUnsafeExec::handle_request", + "notes": "Eval reached" +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 7109331a5..c75f89fe3 100755 --- a/docs/README.md +++ b/docs/README.md @@ -21,8 +21,9 @@ 1. **Value in context** – [Overview](overview.md) compresses the “Why” + “What” stories and shows how Stella Ops stands apart. 2. **Try it fast** – [Quickstart](quickstart.md) walks through fetching the signed bundles, configuring `.env`, and verifying the first scan. -3. **Feature confidence** – [Key Features](key-features.md) gives five capability cards covering Delta SBOM, VEX‑first policy, Sovereign crypto, Deterministic replay, and Transparent quotas. -4. **Up-next checkpoints** – [Evaluation checklist](evaluate/checklist.md) helps teams plan Day‑0 to Day‑30 adoption milestones. +3. **Feature confidence** – [Key Features](key-features.md) gives five capability cards covering Delta SBOM, VEX-first policy, Sovereign crypto, Deterministic replay, and Transparent quotas. +4. **Up-next checkpoints** – [Evaluation checklist](evaluate/checklist.md) helps teams plan Day-0 to Day-30 adoption milestones. +5. **Be dev-ready** – [Developer Quickstart](onboarding/dev-quickstart.md) (29-Nov-2025 advisory) walks through the core repos, determinism tests, attestations, and starter issues for a mid-level .NET engineer. ## Key capabilities that define Stella Ops diff --git a/docs/implplan/SPRINT_0212_0001_0001_web_i.md b/docs/implplan/SPRINT_0212_0001_0001_web_i.md index f35954da3..d3636084f 100644 --- a/docs/implplan/SPRINT_0212_0001_0001_web_i.md +++ b/docs/implplan/SPRINT_0212_0001_0001_web_i.md @@ -24,7 +24,7 @@ | 1 | WEB-AIAI-31-001 | BLOCKED (2025-11-22) | Gateway policy/contract for `/advisory/ai/*` not present in Web workspace; need backend gateway service location + policy spec to proceed. | BE-Base Platform Guild | Route advisory AI endpoints through gateway with guardrails. | | 2 | WEB-AIAI-31-002 | BLOCKED (2025-11-22) | Blocked by WEB-AIAI-31-001; batching/streaming cannot start until gateway contract exists. | BE-Base Platform Guild | Streaming responses for CLI automation with job orchestration. | | 3 | WEB-AIAI-31-003 | BLOCKED (2025-11-22) | Blocked by WEB-AIAI-31-002; telemetry targets depend on routing/batching contract. | BE-Base Platform Guild; Observability Guild | Telemetry + audit for advisory AI, guardrail block visibility. | -| 4 | WEB-AOC-19-002 | TODO | Depends on WEB-AOC-19-001; align DSSE/CMS helper APIs. | BE-Base Platform Guild | Ship `ProvenanceBuilder`, checksum utilities, signature verification helper with tests. | +| 4 | WEB-AOC-19-002 | DONE (2025-11-30) | Depends on WEB-AOC-19-001; align DSSE/CMS helper APIs. | BE-Base Platform Guild | Ship `ProvenanceBuilder`, checksum utilities, signature verification helper with tests. | | 5 | WEB-AOC-19-003 | TODO | Depends on WEB-AOC-19-002; confirm Roslyn analyzer rules. | QA Guild; BE-Base Platform Guild | Analyzer to prevent forbidden key writes; shared guard-validation fixtures. | | 6 | WEB-CONSOLE-23-001 | DONE (2025-11-28) | `/console/dashboard` and `/console/filters` endpoints implemented with tenant-scoped aggregates. | BE-Base Platform Guild; Product Analytics Guild | Tenant-scoped aggregates for findings, VEX overrides, advisory deltas, run health, policy change log. | | 7 | CONSOLE-VULN-29-001 | BLOCKED (2025-11-19) | Blocked on WEB-CONSOLE-23-001 contract and Concelier graph schema freeze. | Console Guild; BE-Base Platform Guild | `/console/vuln/*` workspace endpoints with filters/reachability badges and DTOs once schemas stabilize. | @@ -80,3 +80,4 @@ | 2025-11-22 | Added completion dates in `tasks-all` for WEB-CONTAINERS-44/45/46 and aligned BLOCKED dates for VULN-29-001/VEX-30-001. | Planning | | 2025-11-22 | Harmonized all `CONTAINERS-44/45/46` rows in `tasks-all` to DONE with dates to match sprint status. | Planning | | 2025-11-28 | Completed WEB-CONSOLE-23-001: Implemented `/console/dashboard` and `/console/filters` endpoints in Authority module. Dashboard returns tenant-scoped aggregates (findings summary, VEX overrides, advisory deltas, run health, policy change log) with 30-day trend data. Filters endpoint returns deterministic filter categories with counts and cache-validation hash. Added 8 unit tests for dashboard/filters endpoints. Implementation in `src/Authority/StellaOps.Authority/StellaOps.Authority/Console/`. | Policy Guild | +| 2025-11-30 | Completed WEB-AOC-19-002: added deterministic provenance builder, checksum utilities, and DSSE/CMS signature verification helpers with unit tests under `src/Web/StellaOps.Web/src/app/core/aoc`. Added Web TASKS board and marked task DONE. | BE-Base Platform Guild | diff --git a/docs/implplan/SPRINT_0513_0001_0001_public_reachability_benchmark.md b/docs/implplan/SPRINT_0513_0001_0001_public_reachability_benchmark.md index 66eb437cd..553bd59cd 100644 --- a/docs/implplan/SPRINT_0513_0001_0001_public_reachability_benchmark.md +++ b/docs/implplan/SPRINT_0513_0001_0001_public_reachability_benchmark.md @@ -32,7 +32,7 @@ | 4 | BENCH-CASES-PY-513-004 | DONE (2025-11-30) | Depends on 513-002. | Bench Guild · Python Track (`bench/reachability-benchmark/cases/py`) | Create 5-8 Python cases: Flask, Django, FastAPI. Include requirements.txt pinned, pytest oracles, coverage.py output. Delivered 5 cases: unsafe-exec (reachable), guarded-exec (unreachable), flask-template (reachable), fastapi-guarded (unreachable), django-ssti (reachable). | | 5 | BENCH-CASES-JAVA-513-005 | BLOCKED (2025-11-30) | Depends on 513-002. | Bench Guild · Java Track (`bench/reachability-benchmark/cases/java`) | Create 5-8 Java cases: Spring Boot, Micronaut. Include pom.xml locked, JUnit oracles, JaCoCo coverage. Progress: 2/5 seeded (`spring-deserialize` reachable, `spring-guarded` unreachable); build/test blocked by missing JDK (`javac` not available in runner). | | 6 | BENCH-CASES-C-513-006 | TODO | Depends on 513-002. | Bench Guild · Native Track (`bench/reachability-benchmark/cases/c`) | Create 3-5 C/ELF cases: small HTTP servers, crypto utilities. Include Makefile, gcov/llvm-cov coverage, deterministic builds (SOURCE_DATE_EPOCH). | -| 7 | BENCH-BUILD-513-007 | DOING | Depends on 513-003 through 513-006. | Bench Guild · DevOps Guild | Implement `build_all.py` and `validate_builds.py`: deterministic Docker builds, hash verification, SBOM generation (syft), attestation stubs. Progress: added scripts (hash check, deterministic ordering); SBOM/attestation stubs pending. | +| 7 | BENCH-BUILD-513-007 | DOING | Depends on 513-003 through 513-006. | Bench Guild · DevOps Guild | Implement `build_all.py` and `validate_builds.py`: deterministic Docker builds, hash verification, SBOM generation (syft), attestation stubs. Progress: added scripts (hash check, deterministic ordering) + README; SBOM/attestation stubs pending. | | 8 | BENCH-SCORER-513-008 | DONE (2025-11-30) | Depends on 513-002. | Bench Guild (`bench/reachability-benchmark/tools/scorer`) | Implement `rb-score` CLI: load cases/truth, validate submissions, compute precision/recall/F1, explainability score (0-3), runtime stats, determinism rate. | | 9 | BENCH-EXPLAIN-513-009 | DONE (2025-11-30) | Depends on 513-008. | Bench Guild | Implement explainability scoring rules: 0=no context, 1=path with ≥2 nodes, 2=entry+≥3 nodes, 3=guards/constraints included. Unit tests for each level. | | 10 | BENCH-BASELINE-SEMGREP-513-010 | TODO | Depends on 513-008 and cases. | Bench Guild | Semgrep baseline runner: `baselines/semgrep/run_case.sh`, rule config, output normalization to submission format. | @@ -98,3 +98,4 @@ | 2025-11-30 | BLOCKED BENCH-CASES-JAVA-513-005: `javac`/JDK not available in runner; Java builds/tests cannot execute. Need JDK (>=17) in CI/runner before unblocking. | Implementer | | 2025-11-30 | BENCH-EXPLAIN-513-009 DONE: added explainability tier tests (0–3) to scorer; tiers already implemented (guards→3, entry+path>=3→2, path>=2→1, else 0). | Implementer | | 2025-11-30 | BENCH-BUILD-513-007 DOING: added `tools/build/build_all.py` and `tools/build/validate_builds.py` for deterministic builds and hash checks; SBOM/attestation stubs still pending. | Implementer | +| 2025-11-30 | BENCH-BUILD-513-007: build_all/validate_builds run; all JS/PY cases deterministic, Java cases fail due to missing `javac` (same blocker as task 5). | Implementer | diff --git a/docs/implplan/SPRINT_300_documentation_process.md b/docs/implplan/SPRINT_300_documentation_process.md index a478b21e6..a0ed11d0b 100644 --- a/docs/implplan/SPRINT_300_documentation_process.md +++ b/docs/implplan/SPRINT_300_documentation_process.md @@ -18,12 +18,34 @@ | --- | --- | --- | --- | --- | | 200.A Docs Tasks.md ladder (Sprint 301 onwards) | BLOCKED (2025-11-19) | Docs Guild · Ops Guild | Attestor 100.A; Advisory AI 110.A; AirGap 120.A; Scanner 130.A; Graph 140.A; Orchestrator 150.A; EvidenceLocker 160.A; Notifier 170.A; CLI 180.A; Ops Deployment 190.A | Awaiting upstream artefacts (SBOM/CLI/Policy/AirGap determinism) before Md.I template rollout can continue. | | 200.B Module dossiers (Sprints 312–335) | TODO | Docs Guild · Module Guild owners | Docs Tasks Md ladder to at least Md.II; Ops deployment evidence | Stays queued until Docs Tasks Md ladder provides updated process + assets. | +| Developer quickstart advisory sync | TODO | Docs Guild | 29-Nov-2025 advisory + onboarding doc draft | Publish the onboarding quickstart advisory + `docs/onboarding/dev-quickstart.md`, update `docs/README.md`, `modules/platform/architecture-overview.md`, and `ADVISORY_INDEX.md`, and confirm sprint/AGENTS references per the advisory workflow. | +| Acceptance tests guardrails sync | TODO | Docs Guild | 29-Nov-2025 advisory + checklist draft | Publish the Acceptance Tests Pack advisory, cross-link to sprint/guardrail docs, and capture sprint board checklist for CI/DB/rew definitions. | +| CVSS v4.0 momentum sync | TODO | Docs Guild | 29-Nov-2025 advisory + briefing draft | Publish the CVSS v4.0 momentum briefing, highlight adoption signals, and link to sprint decisions for SPRINT_0190.* and docs coverage. | +| SBOM→VEX proof blueprint sync | TODO | Docs Guild | 29-Nov-2025 advisory + blueprint draft | Publish the SBOM→VEX blueprint, link to platform/blueprint docs, and capture diagram/stub updates for DSSE/Rekor/VEX. | +| SCA failure catalogue sync | TODO | Docs Guild | 29-Nov-2025 advisory + catalogue draft | Publish the SCA failure catalogue, reference the concrete regressions, and tie the test-vector guidance back into sprint risk logs. | +| Implementor guidelines sync | TODO | Docs Guild | 30-Nov-2025 advisory + checklist draft | Publish the Implementor Guidelines advisory, note the checklist extraction, and mention the doc in sprint/AGENTS references. | +| Rekor receipt checklist sync | TODO | Docs Guild | 30-Nov-2025 advisory + checklist draft | Publish the Rekor Receipt Checklist, update module docs (Authority/Sbomer/Vexer) with ownership map, highlight offline metadata requirements. | +| Unknowns decay/triage sync | TODO | Docs Guild | 30-Nov-2025 advisory + heuristic draft | Publish the Unknowns Decay & Triage brief, link to UnknownsRegistry docs, and capture UI artifacts for cards + queue exports. | +| Ecosystem reality test cases sync | TODO | Docs Guild | 30-Nov-2025 advisory + test spec draft | Publish the Ecosystem Reality Test Cases advisory, link each incident to an acceptance test, and note exported artifacts/commands. | +| Standup sprint kickstarters sync | TODO | Docs Guild | 30-Nov-2025 advisory + task plan draft | Publish the Standup Sprint Kickstarters advisory, surface ticket names, and tie the tasks into MSC sprint logs. | +| Evidence + suppression pattern sync | TODO | Docs Guild | 30-Nov-2025 advisory + comparison draft | Publish the Comparative Evidence Patterns advisory, highlight the UX/data-model takeaways, and reference doc links per tool. | ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | | 2025-11-13 | Sprint 300 switched to topic-oriented template; Docs Tasks Md ladder marked DOING to reflect ongoing restructuring work. | Docs Guild | | 2025-11-19 | Marked Docs Tasks Md ladder BLOCKED pending upstream artefacts for Md.I dossier rollouts. | Implementer | +| 2025-11-30 | Added the 29-Nov-2025 Developer Quickstart advisory, `docs/onboarding/dev-quickstart.md`, and cross-links (README/platform/ADVISORY_INDEX); created this advisory sync task row. | Docs Guild | +| 2025-11-30 | Added the 29-Nov-2025 Acceptance Tests Pack advisory and checklist; noted new task row for guardrail sprint artifacts. | Docs Guild | +| 2025-11-30 | Added the 29-Nov-2025 CVSS v4.0 Momentum advisory and indexed the adoption briefing; noted sprint sync row for CVSS momentum context. | Docs Guild | +| 2025-11-30 | Added the 29-Nov-2025 SCA Failure Catalogue advisory and indexed the concrete test vectors; noted sprint sync row for failure catalog references. | Docs Guild | +| 2025-11-30 | Added the 29-Nov-2025 SBOM→VEX Proof Blueprint advisory and outlined diagram/stub follow-up; logged sprint sync row for the blueprint. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Rekor Receipt Checklist advisory and noted the ownership/action map for Authority/Sbomer/Vexer. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Ecosystem Reality Test Cases advisory (credential leak, Trivy offline DB, SBOM parity, Grype divergence) and logged the acceptance test intent. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Unknowns Decay & Triage advisory and noted UI + export artifacts for UnknownsRegistry + queues. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Standup Sprint Kickstarters advisory, highlighting the three unblocker tasks/tickets and the proposed owners. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Comparative Evidence Patterns advisory and recorded cross-tool evidence/suppression nuggets for UX designers. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Implementor Guidelines advisory and checked the docs + sprint sync references; the row stays TODO until docs link updates finish. | Docs Guild | ## Decisions & Risks | Item | Type | Owner(s) | Due | Notes | diff --git a/docs/implplan/SPRINT_503_ops_devops_i.md b/docs/implplan/SPRINT_503_ops_devops_i.md index 7847fdb6a..ebd903268 100644 --- a/docs/implplan/SPRINT_503_ops_devops_i.md +++ b/docs/implplan/SPRINT_503_ops_devops_i.md @@ -30,7 +30,7 @@ Depends on: Sprint 100.A - Attestor, Sprint 110.A - AdvisoryAI, Sprint 120.A - A | DEVOPS-AIRGAP-56-003 | DONE (2025-11-30) | Build Bootstrap Pack pipeline bundling images/charts, generating checksums, and publishing manifest for offline transfer. Dependencies: DEVOPS-AIRGAP-56-002. | DevOps Guild, Container Distribution Guild (ops/devops) | | DEVOPS-AIRGAP-57-001 | DONE (2025-11-30) | Automate Mirror Bundle creation jobs with dual-control approvals, artifact signing, and checksum publication. Dependencies: DEVOPS-AIRGAP-56-003. | DevOps Guild, Mirror Creator Guild (ops/devops) | | DEVOPS-AIRGAP-57-002 | BLOCKED (2025-11-18) | Waiting on upstream DEVOPS-AIRGAP-57-001 (mirror bundle automation) to provide artifacts/endpoints for sealed-mode CI; no sealed fixtures available to exercise tests. | DevOps Guild, Authority Guild (ops/devops) | -| DEVOPS-AIRGAP-58-001 | TODO | Provide local SMTP/syslog container templates and health checks for sealed environments; integrate into Bootstrap Pack. Dependencies: DEVOPS-AIRGAP-57-002. | DevOps Guild, Notifications Guild (ops/devops) | +| DEVOPS-AIRGAP-58-001 | DONE (2025-11-30) | Provide local SMTP/syslog container templates and health checks for sealed environments; integrate into Bootstrap Pack. Dependencies: DEVOPS-AIRGAP-57-002. | DevOps Guild, Notifications Guild (ops/devops) | | DEVOPS-AIRGAP-58-002 | TODO | Ship sealed-mode observability stack (Prometheus/Grafana/Tempo/Loki) pre-configured with offline dashboards and no remote exporters. Dependencies: DEVOPS-AIRGAP-58-001. | DevOps Guild, Observability Guild (ops/devops) | | DEVOPS-AOC-19-001 | BLOCKED (2025-10-26) | Integrate the AOC Roslyn analyzer and guard tests into CI, failing builds when ingestion projects attempt banned writes. | DevOps Guild, Platform Guild (ops/devops) | | DEVOPS-AOC-19-002 | BLOCKED (2025-10-26) | Add pipeline stage executing `stella aoc verify --since` against seeded Mongo snapshots for Concelier + Excititor, publishing violation report artefacts. Dependencies: DEVOPS-AOC-19-001. | DevOps Guild (ops/devops) | @@ -47,13 +47,14 @@ Depends on: Sprint 100.A - Attestor, Sprint 110.A - AdvisoryAI, Sprint 120.A - A | DEVOPS-CONCELIER-CI-24-101 | DONE (2025-11-25) | Provide clean CI runner + warmed NuGet cache + vstest harness for Concelier WebService & Storage; deliver TRX/binlogs and unblock CONCELIER-GRAPH-24-101/28-102 and LNM-21-004..203. | DevOps Guild, Concelier Core Guild (ops/devops) | | DEVOPS-SCANNER-CI-11-001 | DONE (2025-11-30) | Supply warmed cache/diag runner for Scanner analyzers (LANG-11-001, JAVA 21-005/008) with binlogs + TRX; unblock restore/test hangs. | DevOps Guild, Scanner EPDR Guild (ops/devops) | | DEVOPS-SCANNER-JAVA-21-011-REL | TODO | Package/sign Java analyzer plug-in once dev task 21-011 delivers; publish to Offline Kit/CLI release pipelines with provenance. | DevOps Guild, Scanner Release Guild (ops/devops) | -| DEVOPS-SBOM-23-001 | TODO | Publish vetted offline NuGet feed + CI recipe for SbomService; prove with `dotnet test` run and share cache hashes; unblock SBOM-CONSOLE-23-001/002. | DevOps Guild, SBOM Service Guild (ops/devops) | +| DEVOPS-SBOM-23-001 | DONE (2025-11-30) | Publish vetted offline NuGet feed + CI recipe for SbomService; prove with `dotnet test` run and share cache hashes; unblock SBOM-CONSOLE-23-001/002. | DevOps Guild, SBOM Service Guild (ops/devops) | | FEED-REMEDIATION-1001 | BLOCKED (2025-11-24) | Define remediation scope and runbook for overdue feeds (CCCS/CERTBUND); schedule refresh; depends on PREP-FEEDCONN-ICS-KISA-PLAN. | Concelier Feed Owners (ops/devops) | | FEEDCONN-ICSCISA-02-012 / FEEDCONN-KISA-02-008 | BLOCKED (2025-11-24) | Publish provenance refresh/connector schedule for ICSCISA/KISA feeds; execute remediation per runbook once owners provide plan. | Concelier Feed Owners (ops/devops) | ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-30 | Completed DEVOPS-SBOM-23-001: added SBOM CI runner (`ops/devops/sbom-ci-runner/run-sbom-ci.sh`) with warmed-cache restore, binlog/TRX outputs, and NuGet cache hash evidence; documented in runner README. | DevOps | | 2025-11-30 | Completed DEVOPS-SCANNER-CI-11-001: added offline-friendly Scanner CI runner (`ops/devops/scanner-ci-runner/run-scanner-ci.sh`) and README; produces build binlog + TRX outputs from key test projects with warmed NuGet cache. | DevOps | | 2025-11-30 | Completed DEVOPS-ATTEST-73-001/73-002: added attestor CI stub (`ops/devops/attestation/ci.yml`) and secrets/rotation plan in `ops/devops/attestation/README.md`; pending mirror into `.gitea/workflows/attestor-ci.yml` for live runs. | DevOps | | 2025-11-30 | Completed DEVOPS-SPANSINK-31-003: added OTLP span sink compose stack + collector config (`docker-compose.spansink.yml`, `otel-spansink.yaml`), run script, and Grafana dashboard stub (`ops/devops/signals/dashboards/excititor-vex-traces.json`). | DevOps | @@ -68,6 +69,7 @@ Depends on: Sprint 100.A - Attestor, Sprint 110.A - AdvisoryAI, Sprint 120.A - A | 2025-11-24 | Added DEVOPS-SCANNER-JAVA-21-011-REL (moved from SPRINT_0131_0001_0001_scanner_surface.md) to keep DevOps release packaging in ops track. | Project Mgmt | | 2025-11-24 | Added DEVOPS-SPANSINK-31-003 (Excititor span sink for 31-003 traces) moved from SPRINT_0119_0001_0001_excititor_i per ops-only directive. | Project Mgmt | | 2025-11-24 | Imported Concelier feed ops items FEED-REMEDIATION-1001 and FEEDCONN-ICSCISA/KISA from Sprint 110; keeping feed remediation in ops track. | Project Mgmt | +| 2025-11-30 | Completed DEVOPS-AIRGAP-58-001: added syslog/SMTP compose stack (`ops/devops/airgap/compose-syslog-smtp.yaml`) and health script (`health_syslog_smtp.sh`); documented in airgap README for sealed environments. | DevOps | | 2025-11-30 | DEVOPS-AIAI-31-001 DONE: added Advisory AI CI harness (`ops/devops/advisoryai-ci-runner/run-advisoryai-ci.sh`) producing binlog/TRX/summary; warmed local NuGet cache for offline runs; docs in runner README. | DevOps | ## Decisions & Risks diff --git a/docs/implplan/tasks-all.md b/docs/implplan/tasks-all.md index cae62e14a..2769fe78a 100644 --- a/docs/implplan/tasks-all.md +++ b/docs/implplan/tasks-all.md @@ -2100,7 +2100,7 @@ | WEB-AIRGAP-56-002 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AIRGAP-57-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, AirGap Policy Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AIRGAP-58-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, AirGap Importer Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | -| WEB-AOC-19-002 | TODO | | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. Dependencies: WEB-AOC-19-001. | | | +| WEB-AOC-19-002 | DONE (2025-11-30) | | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. Dependencies: WEB-AOC-19-001. | | | | WEB-AOC-19-003 | TODO | | SPRINT_116_concelier_v | QA Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AOC-19-004 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AOC-19-005 | TODO | 2025-11-08 | SPRINT_116_concelier_v | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | @@ -4290,7 +4290,7 @@ | WEB-AIRGAP-56-002 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AIRGAP-57-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, AirGap Policy Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AIRGAP-58-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, AirGap Importer Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | -| WEB-AOC-19-002 | TODO | | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. Dependencies: WEB-AOC-19-001. | | | +| WEB-AOC-19-002 | DONE (2025-11-30) | | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. Dependencies: WEB-AOC-19-001. | | | | WEB-AOC-19-003 | TODO | | SPRINT_116_concelier_v | QA Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AOC-19-004 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AOC-19-005 | TODO | 2025-11-08 | SPRINT_116_concelier_v | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | diff --git a/docs/modules/platform/architecture-overview.md b/docs/modules/platform/architecture-overview.md index 5ec2aae36..5670aee1b 100644 --- a/docs/modules/platform/architecture-overview.md +++ b/docs/modules/platform/architecture-overview.md @@ -8,6 +8,23 @@ This dossier summarises the end-to-end runtime topology after the Aggregation-On --- +> Need a quick orientation? The [Developer Quickstart](../onboarding/dev-quickstart.md) (29-Nov-2025 advisory) captures the core repositories, determinism checks, DSSE conventions, and starter tasks that explain how the platform pieces fit together. + +> Planner note: the [SBOM→VEX proof blueprint](../product-advisories/29-Nov-2025 - SBOM to VEX Proof Pipeline Blueprint.md) shows the DSSE → Rekor v2 tiles → VEX linkage, so threat-model and compliance teams can copy the capture/verification checkpoints. + +> Working on a feature? Check the [Implementor Guidelines](../product-advisories/30-Nov-2025 - Implementor Guidelines for Stella Ops.md) to align with the SRS + release playbook checklist before you merge anything into main. + +> Need to prove Rekor receipts? The [Rekor Receipt Checklist](../product-advisories/30-Nov-2025 - Rekor Receipt Checklist for Stella Ops.md) maps each field to a module owner and explains offline metadata for deterministic re-verification. + +> Taming unknowns? The [Unknowns Decay & Triage Heuristics](../product-advisories/30-Nov-2025 - Unknowns Decay & Triage Heuristics.md) explains the confidence decay card, triage queue view, and the daily export artifact for planning. + +> Check the [Ecosystem Reality Test Cases](../product-advisories/30-Nov-2025 - Ecosystem Reality Test Cases for StellaOps.md) for reproducible acceptance tests based on credential leaks, offline DB schema issues, SBOM parity drift, and scanner version divergence. + +> Need unblocker tasks? The [Standup Sprint Kickstarters](../product-advisories/30-Nov-2025 - Standup Sprint Kickstarters.md) lists three day-0 wins (scanner regressions, Postgres slice, DSSE/Rekor sweep) plus ready-to-copy ticket names. +> Compare how evidence/suppression/audit flows work elsewhere via the [Comparative Evidence Patterns](../product-advisories/30-Nov-2025 - Comparative Evidence Patterns for Stella Ops.md) brief—Snyk, GitHub, Aqua, Anchore/Grype, Prisma Cloud, and the UX trade-offs. + +> Evaluate public scanner incidents? The [Ecosystem Test Cases](../product-advisories/30-Nov-2025 - Ecosystem Test Cases for StellaOps.md) document five hardened regressions (Grype credential leak, Trivy offline schema, SBOM parity, Grype instability) that you can turn into acceptance tests today. + ## 1 · System landscape ```mermaid diff --git a/docs/onboarding/dev-quickstart.md b/docs/onboarding/dev-quickstart.md new file mode 100644 index 000000000..2dce02dee --- /dev/null +++ b/docs/onboarding/dev-quickstart.md @@ -0,0 +1,326 @@ +# StellaOps Developer Quickstart + +> **Audience:** Mid-level .NET developers +> **Goal:** Get you productive on StellaOps in 1–2 days, with special focus on determinism, cryptographic attestations, and the canonical data model. + +--- + +This quickstart mirrors the 29-Nov-2025 Developer Onboarding advisory (`docs/product-advisories/29-Nov-2025 - StellaOps – Mid-Level .NET Onboarding (Quick Start).md`) and keeps the determinism-first guidance in sync with that release note. + +## 1. What You’re Building (Context) + +StellaOps is a sovereign, air-gap-friendly platform that turns **SBOMs → VEX** with a fully **replayable, deterministic trust graph**. + +Core concepts: + +- **Deterministic scans:** Same inputs → same graph, hashes, and verdicts. +- **Cryptographic attestations:** DSSE/in-toto envelopes, optional PQC. +- **Trust lattice:** Merges vendor VEX, runtime signals, configs, etc. into a single deterministic verdict. +- **Audit trail:** Every decision is reproducible from stored inputs and proofs. + +If you think “content-addressed trust pipeline for SBOMs + VEX,” you’re in the right mental model. + +--- + +## 2. Repository & Docs Map + +Start by opening these projects **in order**: + +1. `src/StellaOps.Scanner.WebService/` + Scanning endpoints, rule plumbing, and calls into the trust lattice. +2. `src/StellaOps.Vexer/` (a.k.a. *Excititor*) + VEX verdict engine and trust-merge logic. +3. `src/StellaOps.Sbomer/` + SBOM ingest / normalize (CycloneDX, SPDX). +4. `src/StellaOps.Authority/` + Key management, DSSE/in-toto attestations, license tokens, Rekor integration. +5. `src/StellaOps.Scheduler/` + Batch processing, replay orchestration. +6. `src/StellaOps.Shared/CanonicalModel/` + Canonical entities & graph IDs. **Read this carefully** – it underpins determinism. + +Helpful docs: + +- `docs/modules/platform/*` – protocols (DSSE envelopes, lattice terms, trust receipts). +- `docs/architecture/*` – high-level diagrams and flows. + +--- + +## 3. Local Dev Setup + +### 3.1 Prerequisites + +- **.NET 10 SDK** (preview as specified in repo). +- **Docker** (for DB, queues, object storage). +- **Node.js** (for Angular UI, if you’re touching the frontend). +- **WSL2** (optional but convenient on Windows). + +### 3.2 Bring Up Infra + +From the repo root: + +```bash +# Bring up core infra for offline / air-gap friendly dev +docker compose -f compose/offline-kit.yml up -d +``` + +This usually includes: + +- MongoDB or Postgres (configurable). +- RabbitMQ (or equivalent queue). +- MinIO / object storage (depending on profile). + +### 3.3 Configure Environment + +```bash +cp env/example.local.env .env +``` + +Key settings: + +- `STELLAOPS_DB=Mongo` or `Postgres`. +- `AUTHORITY_*` – key material and config (see comments in `example.local.env`). +- Optional: `AUTHORITY_PQC=on` to enable post-quantum keys (Dilithium). + +### 3.4 Build & Run Backend + +```bash +# Restore & build everything +dotnet restore +dotnet build -c Debug + +# Run a focused slice for development +dotnet run --project src/StellaOps.Authority/StellaOps.Authority.csproj +dotnet run --project src/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj +``` + +Health checks (adjust ports if needed): + +```bash +curl -s http://localhost:5080/health # Authority +curl -s http://localhost:5081/health # Scanner +``` + +--- + +## 4. Deterministic Sanity Tests + +These tests prove your local environment is configured correctly for **determinism**. If any of these fail due to snapshot mismatch, fix your environment before writing new features. + +### 4.1 SBOM → VEX “Not Affected” (Reachability False) + +```bash +dotnet test tests/Determinism/Det_SbomToVex_NotAffected.csproj +``` + +**What it checks:** + +- Two consecutive runs with the same SBOM produce identical `GraphRevisionID` and DSSE payload hashes. + +If they differ, inspect: + +- JSON canonicalization. +- Locale / culture. +- Line endings. + +### 4.2 In-toto Chain: Source → Build → Image Attestation + +```bash +dotnet test tests/Attestations/Att_InToto_Chain.csproj +``` + +**What it checks:** + +- DSSE envelope canonicalization is stable. +- Signature over CBOR-canonical JSON matches the stored hash. +- Full in-toto chain can be replayed deterministically. + +### 4.3 Lattice Merge: Vendor VEX + Runtime Signal + +```bash +dotnet test tests/Lattice/Lattice_VendorPlusRuntime.csproj +``` + +**What it checks:** + +- Merge verdict is stable regardless of input set order. +- Resulting `TrustReceipt` is byte-for-byte identical between runs. + +If any “golden” snapshots differ, you likely have: + +- Non-canonical JSON. +- Unstable enumeration (e.g., iterating `Dictionary<>` directly). +- Locale or newline drift. + +--- + +## 5. Coding Conventions (Determinism & Crypto) + +These are **non-negotiable** in code that affects trust graphs, proofs, or attestations. + +### 5.1 JSON & Canonicalization + +- Use the **`CanonicalJson`** helper whenever a payload is hashed, signed, or used for IDs. +- Rules: UTF-8, sorted keys, no insignificant whitespace, `\n` line endings. + +### 5.2 DSSE Envelopes + +- `payloadType` must always be `application/vnd.stellaops.trust+json`. +- Sign over the canonicalized bytes. + +```csharp +var payload = CanonicalJson.Serialize(trustDoc); +var env = DsseEnvelope.Create("application/vnd.stellaops.trust+json", payload); +var signed = await keyRing.SignAsync(env.CanonicalizeBytes()); +await rekor.SubmitAsync(signed, RekorMode.OfflineMirrorIfAirgapped); +``` + +### 5.3 Hashing + +- **BLAKE3** for internal content addressing. +- **SHA-256** where interop demands it. +- Never mix algorithms within the same ID type. + +### 5.4 Keys & Algorithms + +- Default signatures: **Ed25519** via `Authority.KeyRing`. +- Optional PQC: **Dilithium** when `AUTHORITY_PQC=on`. +- Always go through the keyring abstraction; never manage raw keys manually. + +### 5.5 Time & Clocks + +- Use `Instant`/`DateTimeOffset` (UTC), truncated to milliseconds. +- Never use `DateTime.Now` or local clocks in canonical data. + +### 5.6 IDs & Graph Nodes + +- Canonical/public IDs derive from hashes of canonical bytes. +- DB primary keys are implementation details. +- Do not depend on DB auto-increment or implicit sort order when hashing. + +### 5.7 VEX Verdicts + +Every VEX verdict must: + +- Carry `proofs[]` (reachability, config guards, runtime paths). +- Emit a `receipt` signed by Authority, covering verdict, proof hashes, and context. + +--- + +## 6. Daily Workflow + +1. Pick a focused issue (see starter tasks below). +2. Write tests first, especially determinism scenarios. +3. Implement changes with canonicalization boundaries explicit and signing centralized. +4. Run `dotnet test --filter Category=Determinism`. +5. Commit with the appropriate prefix (`feat(scanner):`, `feat(vexer):`, `feat(authority):`) and mention the affected `GraphRevisionID` if your change alters the trust graph. + +--- + +## 7. Suggested Starter Tasks + +These introduce the canonical data model and determinism mindset. + +### 7.1 Normalize CycloneDX Components → Canonical Packages + +**Area:** `StellaOps.Sbomer` + +**Tests:** `tests/Determinism/Det_SbomMapping` + +**Definition of done:** + +- Equivalent SBOMs (even if fields shuffle) yield identical package sets and canonical IDs. +- `CanonicalPackageSet.hash` is stable. +- Edge cases covered: missing `purl`, duplicate components, case variation. + +### 7.2 Implement “Not-Affected by Configuration” Proof + +**Area:** `StellaOps.Vexer/Proofs/ConfigSwitchProof.cs` + +**Definition of done:** + +- With `FeatureX=false`, CVE-1234 reports `status = not_affected` and the proof records `configPath` + `observed=false`. +- Proof hash is deterministic and included in the DSSE receipt. +- Lattice merge flips the verdict to `not_affected` when the runtime/config proof weight crosses the threshold, even if the vendor says `affected`. + +### 7.3 Authority Offline Rekor Mirror Submitter + +**Area:** `StellaOps.Authority/Rekor/RekorMirrorClient.cs` + +**Definition of done:** + +- `RekorMode.OfflineMirrorIfAirgapped` records canonical entries (JSON + hash path) locally. +- `rekor sync` replays entries in order, preserving entry IDs. +- Golden test ensures the same input sequence → same mirror tree hash. + +--- + +## 8. Database Notes (Mongo ↔ Postgres) + +- Use `StellaOps.Shared.Persistence` repository interfaces. +- Canonical/public IDs are hash-derived; DB keys are internal details. +- Never rely on DB sort order for anything that affects hashes or verdicts; re-canonicalize before hashing and apply deterministic ordering afterwards. + +--- + +## 9. Common Pitfalls + +1. Non-canonical JSON (unsorted keys, extra whitespace, mixed `\r\n`). +2. Local time creeping into proofs (`DateTime.Now`). +3. Unstable GUIDs in tests or canonical entities. +4. Unordered collections (`Dictionary<>` iterations, LINQ without `OrderBy`) while hashing or serializing. +5. Platform drift (Windows vs Linux newline/culture differences) – always use invariant culture and `\n` in canonical data. + +--- + +## 10. Useful Commands + +### 10.1 Determinism Pack + +```bash +# Run determinism-tagged fixtures +dotnet test --filter Category=Determinism +``` + +Update golden snapshots deliberately: + +```bash +dotnet test --filter Category=Determinism -- \ + TestRunParameters.Parameter(name="UpdateSnapshots", value="true") +``` + +### 10.2 Quick API Smoke + +```bash +curl -s http://localhost:5080/health + +curl -s -X POST \ + http://localhost:5081/scan \ + -H "Content-Type: application/json" \ + -d @samples/nginx.sbom.json +``` + +### 10.3 Verify DSSE Signature Locally + +```bash +dotnet run --project tools/StellaOps.Tools.Verify -- file trust.receipt.json +``` + +--- + +## 11. Glossary (Ask-Once) + +- **SBOM** – Software Bill of Materials (CycloneDX/SPDX). +- **VEX** – Vulnerability Exploitability eXchange: verdicts include `affected`, `not_affected`, `under_investigation`. +- **DSSE** – Dead Simple Signing Envelope; we sign canonical bytes. +- **In-toto** – Supply-chain attestation framework for source → build → artifact chains. +- **Lattice** – Rule system merging multiple verdicts/proofs into deterministic outcomes. +- **GraphRevisionID** – Hash of the canonical trust graph; acts like a build number for audits. + +Welcome aboard. Your best “map” is: + +1. Read the CanonicalModel types. +2. Run the determinism tests. +3. Ship one of the starter tasks with deterministic, test-covered changes. + +Keep everything **canonical, hashable, and replayable** and you’ll fit right in. diff --git a/ops/devops/airgap/README.md b/ops/devops/airgap/README.md index 98842378b..8d446bf1c 100644 --- a/ops/devops/airgap/README.md +++ b/ops/devops/airgap/README.md @@ -10,5 +10,7 @@ Artifacts supporting `DEVOPS-AIRGAP-56-001`: - `build_bootstrap_pack.py` — Builds a Bootstrap Pack from images/charts/extras listed in a JSON config, writing `bootstrap-manifest.json` + `checksums.sha256` deterministically. - `build_bootstrap_pack.sh` — Wrapper for the bootstrap pack builder. - `build_mirror_bundle.py` — Generates mirror bundle manifest + checksums with dual-control approvals; optional cosign signing. Outputs `mirror-bundle-manifest.json`, `checksums.sha256`, and optional signature/cert. +- `compose-syslog-smtp.yaml` — Local SMTP (MailHog) + syslog-ng stack for sealed environments. +- `health_syslog_smtp.sh` — Brings up the syslog/SMTP stack via docker compose and performs health checks (MailHog API + syslog logger). See also `ops/devops/sealed-mode-ci/` for the full sealed-mode compose harness and `egress_probe.py`, which this verification script wraps. diff --git a/ops/devops/airgap/compose-syslog-smtp.yaml b/ops/devops/airgap/compose-syslog-smtp.yaml new file mode 100644 index 000000000..55d630445 --- /dev/null +++ b/ops/devops/airgap/compose-syslog-smtp.yaml @@ -0,0 +1,31 @@ +version: "3.9" + +services: + smtp: + image: mailhog/mailhog:v1.0.1 + container_name: mailhog + ports: + - "1025:1025" # SMTP (plain) + - "8025:8025" # Web UI + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8025/api/v2/health"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s + restart: unless-stopped + + syslog: + image: balabit/syslog-ng:4.7.1 + container_name: syslog-ng + ports: + - "514:514/udp" + - "514:514/tcp" + command: ["/usr/sbin/syslog-ng", "-F", "-p", "/var/run/syslogd.pid"] + healthcheck: + test: ["CMD", "syslog-ng-ctl", "stats"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s + restart: unless-stopped diff --git a/ops/devops/airgap/health_syslog_smtp.sh b/ops/devops/airgap/health_syslog_smtp.sh new file mode 100644 index 000000000..1383b2357 --- /dev/null +++ b/ops/devops/airgap/health_syslog_smtp.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Health check for compose-syslog-smtp.yaml (DEVOPS-AIRGAP-58-001) + +COMPOSE_FILE="$(cd "$(dirname "$0")" && pwd)/compose-syslog-smtp.yaml" + +echo "Starting syslog+smtp stack..." +docker compose -f "$COMPOSE_FILE" up -d + +echo "Waiting for health checks..." +docker compose -f "$COMPOSE_FILE" wait >/dev/null 2>&1 || true + +echo "Current health status:" +docker compose -f "$COMPOSE_FILE" ps + +echo "Sending test syslog message (UDP)..." +logger -n 127.0.0.1 -P 514 -d "test syslog message $(date -u +%s)" + +echo "SMTP health endpoint:" +curl -sf http://127.0.0.1:8025/api/v2/health || exit 1 + +echo "Done." diff --git a/ops/devops/sbom-ci-runner/README.md b/ops/devops/sbom-ci-runner/README.md new file mode 100644 index 000000000..76590820d --- /dev/null +++ b/ops/devops/sbom-ci-runner/README.md @@ -0,0 +1,29 @@ +# SBOM Service CI Runner Harness (DEVOPS-SBOM-23-001) + +Purpose: deterministic, offline-friendly CI harness for SBOM Service. Produces warmed-cache restore, build binlog, TRX outputs, and a NuGet cache hash to unblock SBOM console/consumer sprints. + +Usage +- From repo root run: `ops/devops/sbom-ci-runner/run-sbom-ci.sh` +- Outputs land in `ops/devops/artifacts/sbom-ci//`: + - `build.binlog` (solution build) + - `tests/sbom.trx` (VSTest results) + - `nuget-cache.hash` (sha256 over file name+size listing for offline cache traceability) + - `summary.json` (paths + sources + cache hash) + +Environment defaults +- `DOTNET_CLI_TELEMETRY_OPTOUT=1`, `DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1`, `DOTNET_RESTORE_DISABLE_PARALLEL=1` +- `NUGET_PACKAGES=$REPO/.nuget/packages` +- `NUGET_SOURCES=$REPO/local-nugets;$REPO/.nuget/packages` +- `TEST_FILTER` empty (set to narrow tests) + +What it does +1) Warm NuGet cache from `local-nugets/` into `$NUGET_PACKAGES` for air-gap parity. +2) `dotnet restore` + `dotnet build` on `src/SbomService/StellaOps.SbomService.sln` with `/bl`. +3) Run `StellaOps.SbomService.Tests` with TRX output (honors `TEST_FILTER`). +4) Produce `nuget-cache.hash` using sorted file name+size list hashed with sha256 (lightweight evidence of cache contents). +5) Emit `summary.json` with artefact paths and cache hash value. + +Notes +- Offline-only; no external services required. +- Timestamped output folders keep ordering deterministic; consumers should sort lexicographically. +- Extend `test_project` in the script if additional SBOM test projects are added. diff --git a/ops/devops/sbom-ci-runner/run-sbom-ci.sh b/ops/devops/sbom-ci-runner/run-sbom-ci.sh new file mode 100644 index 000000000..283aa615d --- /dev/null +++ b/ops/devops/sbom-ci-runner/run-sbom-ci.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +# SBOM Service CI runner (DEVOPS-SBOM-23-001) +# Builds SBOM solution and runs tests with warmed NuGet cache; emits binlog + TRX + cache hash summary. + +repo_root="$(cd "$(dirname "$0")/../../.." && pwd)" +ts="$(date -u +%Y%m%dT%H%M%SZ)" +out_dir="$repo_root/ops/devops/artifacts/sbom-ci/$ts" +logs_dir="$out_dir/tests" +mkdir -p "$logs_dir" + +export DOTNET_CLI_TELEMETRY_OPTOUT=${DOTNET_CLI_TELEMETRY_OPTOUT:-1} +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=${DOTNET_SKIP_FIRST_TIME_EXPERIENCE:-1} +export DOTNET_RESTORE_DISABLE_PARALLEL=${DOTNET_RESTORE_DISABLE_PARALLEL:-1} +export NUGET_PACKAGES=${NUGET_PACKAGES:-$repo_root/.nuget/packages} +export NUGET_SOURCES=${NUGET_SOURCES:-"$repo_root/local-nugets;$repo_root/.nuget/packages"} +export TEST_FILTER=${TEST_FILTER:-""} + +mkdir -p "$NUGET_PACKAGES" +rsync -a "$repo_root/local-nugets/" "$NUGET_PACKAGES/" >/dev/null 2>&1 || true + +restore_sources=() +IFS=';' read -ra SRC_ARR <<< "$NUGET_SOURCES" +for s in "${SRC_ARR[@]}"; do + [[ -n "$s" ]] && restore_sources+=(--source "$s") +done + +solution="$repo_root/src/SbomService/StellaOps.SbomService.sln" +dotnet restore "$solution" --ignore-failed-sources "${restore_sources[@]}" + +build_binlog="$out_dir/build.binlog" +dotnet build "$solution" -c Release /p:ContinuousIntegrationBuild=true /bl:"$build_binlog" + +trx_name="sbom.trx" +test_project="$repo_root/src/SbomService/StellaOps.SbomService.Tests/StellaOps.SbomService.Tests.csproj" +common_test_args=( -c Release --no-build --results-directory "$logs_dir" ) +if [[ -n "$TEST_FILTER" ]]; then + common_test_args+=( --filter "$TEST_FILTER" ) +fi + +if [[ -f "$test_project" ]]; then + dotnet test "$test_project" "${common_test_args[@]}" --logger "trx;LogFileName=$trx_name" +fi + +# Lightweight cache hash: list files with size, hash the listing +cache_listing="$out_dir/nuget-cache.list" +find "$NUGET_PACKAGES" -type f -printf "%P %s\n" | sort > "$cache_listing" +cache_hash=$(sha256sum "$cache_listing" | awk '{print $1}') + +echo "$cache_hash nuget-cache.list" > "$out_dir/nuget-cache.hash" + +summary="$out_dir/summary.json" +{ + printf '{\n' + printf ' "timestamp_utc": "%s",\n' "$ts" + printf ' "build_binlog": "%s",\n' "${build_binlog#${repo_root}/}" + printf ' "tests": [\n' + printf ' {"project":"SbomService","trx":"%s"}\n' "${logs_dir#${repo_root}/}/$trx_name" + printf ' ],\n' + printf ' "nuget_packages": "%s",\n' "${NUGET_PACKAGES#${repo_root}/}" + printf ' "cache_hash": "%s",\n' "$cache_hash" + printf ' "sources": [\n' + for i in "${!SRC_ARR[@]}"; do + sep=","; [[ $i -eq $((${#SRC_ARR[@]}-1)) ]] && sep="" + printf ' "%s"%s\n' "${SRC_ARR[$i]}" "$sep" + done + printf ' ]\n' + printf '}\n' +} > "$summary" + +echo "Artifacts written to ${out_dir#${repo_root}/}" diff --git a/src/Web/StellaOps.Web/TASKS.md b/src/Web/StellaOps.Web/TASKS.md new file mode 100644 index 000000000..02943143a --- /dev/null +++ b/src/Web/StellaOps.Web/TASKS.md @@ -0,0 +1,8 @@ +# Web Guild Tasks + +| Task ID | State | Notes | +| --- | --- | --- | +| WEB-AOC-19-002 | DONE (2025-11-30) | Added provenance builder, checksum utilities, and DSSE/CMS signature verification helpers with unit tests. | +| WEB-AOC-19-003 | TODO | Analyzer/guard validation remains; will align once helper APIs settle. | +| WEB-CONSOLE-23-002 | TODO | Status/stream endpoints to proxy Scheduler once contracts finalized. | +| WEB-EXC-25-001 | TODO | Exceptions workflow CRUD pending policy scopes. | diff --git a/src/Web/StellaOps.Web/src/app/core/aoc/checksum.util.ts b/src/Web/StellaOps.Web/src/app/core/aoc/checksum.util.ts new file mode 100644 index 000000000..f3cf579b8 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/aoc/checksum.util.ts @@ -0,0 +1,71 @@ +export type HashAlgorithm = 'SHA-256' | 'SHA-512'; + +export interface DigestResult { + algorithm: HashAlgorithm; + hex: string; + uri: string; // e.g. sha256:abcd... +} + +const encoder = new TextEncoder(); + +function toUint8(payload: string | ArrayBuffer | Uint8Array): Uint8Array { + if (typeof payload === 'string') { + return encoder.encode(payload); + } + + if (payload instanceof ArrayBuffer) { + return new Uint8Array(payload); + } + + return payload; +} + +function toHex(buffer: ArrayBuffer): string { + return Array.from(new Uint8Array(buffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +/** + * Deterministically compute a digest over an input payload using WebCrypto. + * Returns both the raw hex and a URI-style prefix (sha256:...). + */ +export async function computeDigest( + payload: string | ArrayBuffer | Uint8Array, + algorithm: HashAlgorithm = 'SHA-256' +): Promise { + if (!globalThis.crypto?.subtle) { + throw new Error('WebCrypto unavailable: cannot compute digest'); + } + + const data = toUint8(payload); + const digestBuffer = await globalThis.crypto.subtle.digest(algorithm, data); + const hex = toHex(digestBuffer); + const prefix = algorithm.toLowerCase().replace('-', ''); + + return { + algorithm, + hex, + uri: `${prefix}:${hex}`, + }; +} + +/** + * Convenience helper to deterministically serialize an object before hashing. + * Uses stable key ordering and JSON without spaces. + */ +export function canonicalJson(input: unknown): string { + const replacer = (_key: string, value: unknown) => { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return Object.keys(value as Record) + .sort() + .reduce>((acc, key) => { + acc[key] = (value as Record)[key]; + return acc; + }, {}); + } + return value; + }; + + return JSON.stringify(input, replacer); +} diff --git a/src/Web/StellaOps.Web/src/app/core/aoc/provenance-builder.spec.ts b/src/Web/StellaOps.Web/src/app/core/aoc/provenance-builder.spec.ts new file mode 100644 index 000000000..cc0423d4b --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/aoc/provenance-builder.spec.ts @@ -0,0 +1,128 @@ +import { ProvenanceBuilder } from './provenance-builder'; +import { canonicalJson, computeDigest } from './checksum.util'; +import { + dssePreAuthEncode, + verifyCmsSignature, + verifyDsseSignature, +} from './signature-verifier'; + +async function exportPublicKeyPem(publicKey: CryptoKey): Promise { + const spki = await crypto.subtle.exportKey('spki', publicKey); + const base64 = btoa(String.fromCharCode(...new Uint8Array(spki))); + const wrapped = base64.match(/.{1,64}/g)?.join('\n') ?? base64; + return `-----BEGIN PUBLIC KEY-----\n${wrapped}\n-----END PUBLIC KEY-----`; +} + +describe('Provenance utilities', () => { + it('builds deterministic provenance for objects', async () => { + const builder = new ProvenanceBuilder(() => new Date('2025-11-30T12:00:00Z')); + const provenance = await builder.build( + { b: 2, a: 1 }, + { + sourceId: 'registry-1', + sourceType: 'registry', + sourceUrl: 'https://example.test/manifest', + submitter: 'ci-bot', + } + ); + + expect(provenance.ingestedAt).toBe('2025-11-30T12:00:00.000Z'); + expect(provenance.digest.startsWith('sha256:')).toBeTrue(); + + const canonical = canonicalJson({ b: 2, a: 1 }); + const digest = await computeDigest(canonical, 'SHA-256'); + expect(provenance.digest).toBe(digest.uri); + }); + + it('computes DSSE pre-auth encoding with correct prefix', () => { + const payload = new TextEncoder().encode('hello'); + const pae = dssePreAuthEncode('text/plain', payload); + const asText = new TextDecoder().decode(pae); + expect(asText.startsWith('DSSEv1 10 text/plain 5 ')).toBeTrue(); + expect(asText.endsWith('hello')).toBeTrue(); + }); + + it('verifies CMS signatures', async () => { + const keyPair = await crypto.subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'] + ); + + const message = new TextEncoder().encode('aoc-proof'); + const signature = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', keyPair.privateKey, message); + const pem = await exportPublicKeyPem(keyPair.publicKey); + + const result = await verifyCmsSignature({ + payload: message, + signature, + publicKeyPem: pem, + algorithm: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + }); + + expect(result.valid).toBeTrue(); + }); + + it('fails verification when payload changes', async () => { + const keyPair = await crypto.subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'] + ); + + const message = new TextEncoder().encode('aoc-proof'); + const signature = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', keyPair.privateKey, message); + const pem = await exportPublicKeyPem(keyPair.publicKey); + + const result = await verifyCmsSignature({ + payload: 'tampered', + signature, + publicKeyPem: pem, + algorithm: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + }); + + expect(result.valid).toBeFalse(); + }); + + it('verifies DSSE signatures using pre-auth encoding', async () => { + const keyPair = await crypto.subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'] + ); + + const payloadBytes = new TextEncoder().encode('{"sub":"example"}'); + const pae = dssePreAuthEncode('application/json', payloadBytes); + const signature = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', keyPair.privateKey, pae); + const pem = await exportPublicKeyPem(keyPair.publicKey); + + const result = await verifyDsseSignature({ + payload: payloadBytes, + payloadType: 'application/json', + signature, + publicKeyPem: pem, + algorithm: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + }); + + expect(result.valid).toBeTrue(); + expect(result.message?.startsWith('sha256:')).toBeTrue(); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/core/aoc/provenance-builder.ts b/src/Web/StellaOps.Web/src/app/core/aoc/provenance-builder.ts new file mode 100644 index 000000000..58a568c5c --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/aoc/provenance-builder.ts @@ -0,0 +1,40 @@ +import { AocProvenance } from '../api/aoc.models'; +import { HashAlgorithm, canonicalJson, computeDigest } from './checksum.util'; + +export interface ProvenanceOptions { + sourceId: string; + sourceType?: AocProvenance['sourceType']; + sourceUrl?: string; + submitter?: string; + ingestedAt?: string; + hashAlgorithm?: HashAlgorithm; +} + +/** + * Deterministic provenance builder used by the AOC workspace to attach + * digests and timing metadata to documents before verification/signing. + */ +export class ProvenanceBuilder { + constructor(private readonly clock: () => Date = () => new Date()) {} + + async build( + payload: string | ArrayBuffer | Uint8Array | Record, + options: ProvenanceOptions + ): Promise { + const serialised = + typeof payload === 'string' || payload instanceof ArrayBuffer || payload instanceof Uint8Array + ? payload + : canonicalJson(payload); + + const digest = await computeDigest(serialised, options.hashAlgorithm ?? 'SHA-256'); + + return { + sourceId: options.sourceId, + sourceType: options.sourceType, + sourceUrl: options.sourceUrl, + submitter: options.submitter, + ingestedAt: options.ingestedAt ?? this.clock().toISOString(), + digest: digest.uri, + }; + } +} diff --git a/src/Web/StellaOps.Web/src/app/core/aoc/signature-verifier.ts b/src/Web/StellaOps.Web/src/app/core/aoc/signature-verifier.ts new file mode 100644 index 000000000..005fb7c4e --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/aoc/signature-verifier.ts @@ -0,0 +1,133 @@ +import { HashAlgorithm, computeDigest } from './checksum.util'; + +export interface VerificationResult { + valid: boolean; + message?: string; +} + +export interface VerifyOptions { + payload: string | ArrayBuffer | Uint8Array; + signature: ArrayBuffer | Uint8Array | string; // base64 if string + publicKeyPem: string; + algorithm?: 'RSASSA-PKCS1-v1_5' | 'RSA-PSS'; + hash?: HashAlgorithm; + saltLength?: number; // RSA-PSS only +} + +function base64ToArrayBuffer(input: string): ArrayBuffer { + const normalized = input.replace(/\s+/g, ''); + const binary = atob(normalized); + const buffer = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) { + buffer[i] = binary.charCodeAt(i); + } + return buffer.buffer; +} + +async function importPublicKey( + pem: string, + algorithm: 'RSASSA-PKCS1-v1_5' | 'RSA-PSS', + hash: HashAlgorithm +): Promise { + const clean = pem + .replace('-----BEGIN PUBLIC KEY-----', '') + .replace('-----END PUBLIC KEY-----', '') + .replace(/\s+/g, ''); + const binaryDer = base64ToArrayBuffer(clean); + + return globalThis.crypto.subtle.importKey( + 'spki', + binaryDer, + { + name: algorithm, + hash: { name: hash }, + }, + true, + ['verify'] + ); +} + +function toUint8(payload: string | ArrayBuffer | Uint8Array): Uint8Array { + if (typeof payload === 'string') { + return new TextEncoder().encode(payload); + } + if (payload instanceof ArrayBuffer) { + return new Uint8Array(payload); + } + return payload; +} + +function normalizeSignature(sig: ArrayBuffer | Uint8Array | string): ArrayBuffer { + if (typeof sig === 'string') { + return base64ToArrayBuffer(sig); + } + if (sig instanceof ArrayBuffer) { + return sig; + } + return sig.buffer; +} + +export async function verifyCmsSignature(options: VerifyOptions): Promise { + if (!globalThis.crypto?.subtle) { + return { valid: false, message: 'WebCrypto unavailable' }; + } + + const algorithm = options.algorithm ?? 'RSASSA-PKCS1-v1_5'; + const hash = options.hash ?? 'SHA-256'; + + const key = await importPublicKey(options.publicKeyPem, algorithm, hash); + const payload = toUint8(options.payload); + const signature = normalizeSignature(options.signature); + + const verified = await globalThis.crypto.subtle.verify( + { name: algorithm, saltLength: options.saltLength ?? 32 }, + key, + signature, + payload + ); + + return verified + ? { valid: true } + : { valid: false, message: 'Signature verification failed' }; +} + +export interface DsseVerifyOptions extends VerifyOptions { + payloadType: string; +} + +/** + * DSSE pre-authentication encoding (PAE) as defined by sigstore. + * Format: `DSSEv1 ` + */ +export function dssePreAuthEncode(payloadType: string, payload: Uint8Array): Uint8Array { + const enc = new TextEncoder(); + const header = `DSSEv1 ${payloadType.length} ${payloadType} ${payload.length} `; + const headerBytes = enc.encode(header); + const output = new Uint8Array(headerBytes.length + payload.length); + output.set(headerBytes, 0); + output.set(payload, headerBytes.length); + return output; +} + +export async function verifyDsseSignature( + options: DsseVerifyOptions +): Promise { + if (!globalThis.crypto?.subtle) { + return { valid: false, message: 'WebCrypto unavailable' }; + } + + const payloadBytes = toUint8(options.payload); + const pae = dssePreAuthEncode(options.payloadType, payloadBytes); + const cmsResult = await verifyCmsSignature({ + ...options, + payload: pae, + }); + + if (!cmsResult.valid) { + return cmsResult; + } + + // Provide a digest hint for downstream display/debugging + const digest = await computeDigest(payloadBytes, options.hash ?? 'SHA-256'); + return { valid: true, message: digest.uri }; +}