Compare commits
32 Commits
505fe7a885
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91f3610b9d | ||
|
|
8779e9226f | ||
|
|
951a38d561 | ||
|
|
43882078a4 | ||
|
|
2eafe98d44 | ||
|
|
6410a6d082 | ||
|
|
f85d53888c | ||
|
|
1fcf550d3a | ||
|
|
0dc71e760a | ||
|
|
811f35cba7 | ||
|
|
00d2c99af9 | ||
|
|
7d5250238c | ||
|
|
28823a8960 | ||
|
|
b4235c134c | ||
| dee252940b | |||
|
|
8bbfe4d2d2 | ||
|
|
394b57f6bf | ||
|
|
3a2100aa78 | ||
|
|
417ef83202 | ||
|
|
2170a58734 | ||
|
|
415eff1207 | ||
|
|
b55d9fa68d | ||
|
|
5a480a3c2a | ||
|
|
4391f35d8a | ||
|
|
b1f40945b7 | ||
|
|
41864227d2 | ||
|
|
8137503221 | ||
|
|
08dab053c0 | ||
|
|
7ce83270d0 | ||
|
|
0cb5c9abfb | ||
|
|
d59cc816c1 | ||
|
|
4344020dd1 |
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-stryker": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"commands": [
|
||||||
|
"stryker"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -575,6 +575,209 @@ PY
|
|||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Quality Gates Foundation (Sprint 0350)
|
||||||
|
# ============================================================================
|
||||||
|
quality-gates:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: build-test
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Reachability quality gate
|
||||||
|
id: reachability
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Computing reachability metrics"
|
||||||
|
if [ -f scripts/ci/compute-reachability-metrics.sh ]; then
|
||||||
|
chmod +x scripts/ci/compute-reachability-metrics.sh
|
||||||
|
METRICS=$(./scripts/ci/compute-reachability-metrics.sh --dry-run 2>/dev/null || echo '{}')
|
||||||
|
echo "metrics=$METRICS" >> $GITHUB_OUTPUT
|
||||||
|
echo "Reachability metrics: $METRICS"
|
||||||
|
else
|
||||||
|
echo "Reachability script not found, skipping"
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: TTFS regression gate
|
||||||
|
id: ttfs
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Computing TTFS metrics"
|
||||||
|
if [ -f scripts/ci/compute-ttfs-metrics.sh ]; then
|
||||||
|
chmod +x scripts/ci/compute-ttfs-metrics.sh
|
||||||
|
METRICS=$(./scripts/ci/compute-ttfs-metrics.sh --dry-run 2>/dev/null || echo '{}')
|
||||||
|
echo "metrics=$METRICS" >> $GITHUB_OUTPUT
|
||||||
|
echo "TTFS metrics: $METRICS"
|
||||||
|
else
|
||||||
|
echo "TTFS script not found, skipping"
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Performance SLO gate
|
||||||
|
id: slo
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Enforcing performance SLOs"
|
||||||
|
if [ -f scripts/ci/enforce-performance-slos.sh ]; then
|
||||||
|
chmod +x scripts/ci/enforce-performance-slos.sh
|
||||||
|
./scripts/ci/enforce-performance-slos.sh --warn-only || true
|
||||||
|
else
|
||||||
|
echo "Performance SLO script not found, skipping"
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: RLS policy validation
|
||||||
|
id: rls
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Validating RLS policies"
|
||||||
|
if [ -f deploy/postgres-validation/001_validate_rls.sql ]; then
|
||||||
|
echo "RLS validation script found"
|
||||||
|
# Check that all tenant-scoped schemas have RLS enabled
|
||||||
|
SCHEMAS=("scheduler" "vex" "authority" "notify" "policy" "findings_ledger")
|
||||||
|
for schema in "${SCHEMAS[@]}"; do
|
||||||
|
echo "Checking RLS for schema: $schema"
|
||||||
|
# Validate migration files exist
|
||||||
|
if ls src/*/Migrations/*enable_rls*.sql 2>/dev/null | grep -q "$schema"; then
|
||||||
|
echo " ✓ RLS migration exists for $schema"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "RLS validation passed (static check)"
|
||||||
|
else
|
||||||
|
echo "RLS validation script not found, skipping"
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Upload quality gate results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: quality-gate-results
|
||||||
|
path: |
|
||||||
|
scripts/ci/*.json
|
||||||
|
scripts/ci/*.yaml
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
security-testing:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: build-test
|
||||||
|
if: github.event_name == 'pull_request' || github.event_name == 'schedule'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
DOTNET_VERSION: '10.0.100'
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore tests/security/StellaOps.Security.Tests/StellaOps.Security.Tests.csproj
|
||||||
|
|
||||||
|
- name: Run OWASP security tests
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Running security tests"
|
||||||
|
dotnet test tests/security/StellaOps.Security.Tests/StellaOps.Security.Tests.csproj \
|
||||||
|
--no-restore \
|
||||||
|
--logger "trx;LogFileName=security-tests.trx" \
|
||||||
|
--results-directory ./security-test-results \
|
||||||
|
--filter "Category=Security" \
|
||||||
|
--verbosity normal
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Upload security test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: security-test-results
|
||||||
|
path: security-test-results/
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
mutation-testing:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: build-test
|
||||||
|
if: github.event_name == 'schedule' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'mutation-test'))
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
DOTNET_VERSION: '10.0.100'
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
|
||||||
|
- name: Restore tools
|
||||||
|
run: dotnet tool restore
|
||||||
|
|
||||||
|
- name: Run mutation tests - Scanner.Core
|
||||||
|
id: scanner-mutation
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Mutation testing Scanner.Core"
|
||||||
|
cd src/Scanner/__Libraries/StellaOps.Scanner.Core
|
||||||
|
dotnet stryker --reporter json --reporter html --output ../../../mutation-results/scanner-core || echo "MUTATION_FAILED=true" >> $GITHUB_ENV
|
||||||
|
echo "::endgroup::"
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run mutation tests - Policy.Engine
|
||||||
|
id: policy-mutation
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Mutation testing Policy.Engine"
|
||||||
|
cd src/Policy/__Libraries/StellaOps.Policy
|
||||||
|
dotnet stryker --reporter json --reporter html --output ../../../mutation-results/policy-engine || echo "MUTATION_FAILED=true" >> $GITHUB_ENV
|
||||||
|
echo "::endgroup::"
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run mutation tests - Authority.Core
|
||||||
|
id: authority-mutation
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Mutation testing Authority.Core"
|
||||||
|
cd src/Authority/StellaOps.Authority
|
||||||
|
dotnet stryker --reporter json --reporter html --output ../../mutation-results/authority-core || echo "MUTATION_FAILED=true" >> $GITHUB_ENV
|
||||||
|
echo "::endgroup::"
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Upload mutation results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mutation-testing-results
|
||||||
|
path: mutation-results/
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Check mutation thresholds
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "Checking mutation score thresholds..."
|
||||||
|
# Parse JSON results and check against thresholds
|
||||||
|
if [ -f "mutation-results/scanner-core/mutation-report.json" ]; then
|
||||||
|
SCORE=$(jq '.mutationScore // 0' mutation-results/scanner-core/mutation-report.json)
|
||||||
|
echo "Scanner.Core mutation score: $SCORE%"
|
||||||
|
if (( $(echo "$SCORE < 65" | bc -l) )); then
|
||||||
|
echo "::error::Scanner.Core mutation score below threshold"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
sealed-mode-ci:
|
sealed-mode-ci:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: build-test
|
needs: build-test
|
||||||
|
|||||||
98
.gitea/workflows/epss-ingest-perf.yml
Normal file
98
.gitea/workflows/epss-ingest-perf.yml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: EPSS Ingest Perf
|
||||||
|
|
||||||
|
# Sprint: SPRINT_3410_0001_0001_epss_ingestion_storage
|
||||||
|
# Tasks: EPSS-3410-013B, EPSS-3410-014
|
||||||
|
#
|
||||||
|
# Runs the EPSS ingest perf harness against a Dockerized PostgreSQL instance (Testcontainers).
|
||||||
|
#
|
||||||
|
# Runner requirements:
|
||||||
|
# - Linux runner with Docker Engine available to the runner user (Testcontainers).
|
||||||
|
# - Label: `ubuntu-22.04` (adjust `runs-on` if your labels differ).
|
||||||
|
# - >= 4 CPU / >= 8GB RAM recommended for stable baselines.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
rows:
|
||||||
|
description: 'Row count to generate (default: 310000)'
|
||||||
|
required: false
|
||||||
|
default: '310000'
|
||||||
|
postgres_image:
|
||||||
|
description: 'PostgreSQL image (default: postgres:16-alpine)'
|
||||||
|
required: false
|
||||||
|
default: 'postgres:16-alpine'
|
||||||
|
schedule:
|
||||||
|
# Nightly at 03:00 UTC
|
||||||
|
- cron: '0 3 * * *'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'src/Scanner/__Libraries/StellaOps.Scanner.Storage/**'
|
||||||
|
- 'src/Scanner/StellaOps.Scanner.Worker/**'
|
||||||
|
- 'src/Scanner/__Benchmarks/StellaOps.Scanner.Storage.Epss.Perf/**'
|
||||||
|
- '.gitea/workflows/epss-ingest-perf.yml'
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'src/Scanner/__Libraries/StellaOps.Scanner.Storage/**'
|
||||||
|
- 'src/Scanner/StellaOps.Scanner.Worker/**'
|
||||||
|
- 'src/Scanner/__Benchmarks/StellaOps.Scanner.Storage.Epss.Perf/**'
|
||||||
|
- '.gitea/workflows/epss-ingest-perf.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
perf:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
env:
|
||||||
|
DOTNET_NOLOGO: 1
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||||
|
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1
|
||||||
|
TZ: UTC
|
||||||
|
STELLAOPS_OFFLINE: 'true'
|
||||||
|
STELLAOPS_DETERMINISTIC: 'true'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.100
|
||||||
|
include-prerelease: true
|
||||||
|
|
||||||
|
- name: Cache NuGet packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.nuget/packages
|
||||||
|
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nuget-
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: |
|
||||||
|
dotnet restore src/Scanner/__Benchmarks/StellaOps.Scanner.Storage.Epss.Perf/StellaOps.Scanner.Storage.Epss.Perf.csproj \
|
||||||
|
--configfile nuget.config
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
dotnet build src/Scanner/__Benchmarks/StellaOps.Scanner.Storage.Epss.Perf/StellaOps.Scanner.Storage.Epss.Perf.csproj \
|
||||||
|
-c Release \
|
||||||
|
--no-restore
|
||||||
|
|
||||||
|
- name: Run perf harness
|
||||||
|
run: |
|
||||||
|
mkdir -p bench/results
|
||||||
|
dotnet run \
|
||||||
|
--project src/Scanner/__Benchmarks/StellaOps.Scanner.Storage.Epss.Perf/StellaOps.Scanner.Storage.Epss.Perf.csproj \
|
||||||
|
-c Release \
|
||||||
|
--no-build \
|
||||||
|
-- \
|
||||||
|
--rows ${{ inputs.rows || '310000' }} \
|
||||||
|
--postgres-image '${{ inputs.postgres_image || 'postgres:16-alpine' }}' \
|
||||||
|
--output bench/results/epss-ingest-perf-${{ github.sha }}.json
|
||||||
|
|
||||||
|
- name: Upload results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: epss-ingest-perf-${{ github.sha }}
|
||||||
|
path: |
|
||||||
|
bench/results/epss-ingest-perf-${{ github.sha }}.json
|
||||||
|
retention-days: 90
|
||||||
306
.gitea/workflows/reachability-bench.yaml
Normal file
306
.gitea/workflows/reachability-bench.yaml
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
name: Reachability Benchmark
|
||||||
|
|
||||||
|
# Sprint: SPRINT_3500_0003_0001
|
||||||
|
# Task: CORPUS-009 - Create Gitea workflow for reachability benchmark
|
||||||
|
# Task: CORPUS-010 - Configure nightly + per-PR benchmark runs
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
baseline_version:
|
||||||
|
description: 'Baseline version to compare against'
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
verbose:
|
||||||
|
description: 'Enable verbose output'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'datasets/reachability/**'
|
||||||
|
- 'src/Scanner/__Libraries/StellaOps.Scanner.Benchmarks/**'
|
||||||
|
- 'bench/reachability-benchmark/**'
|
||||||
|
- '.gitea/workflows/reachability-bench.yaml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'datasets/reachability/**'
|
||||||
|
- 'src/Scanner/__Libraries/StellaOps.Scanner.Benchmarks/**'
|
||||||
|
- 'bench/reachability-benchmark/**'
|
||||||
|
schedule:
|
||||||
|
# Nightly at 02:00 UTC
|
||||||
|
- cron: '0 2 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
benchmark:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
env:
|
||||||
|
DOTNET_NOLOGO: 1
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||||
|
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1
|
||||||
|
TZ: UTC
|
||||||
|
STELLAOPS_OFFLINE: 'true'
|
||||||
|
STELLAOPS_DETERMINISTIC: 'true'
|
||||||
|
outputs:
|
||||||
|
precision: ${{ steps.metrics.outputs.precision }}
|
||||||
|
recall: ${{ steps.metrics.outputs.recall }}
|
||||||
|
f1: ${{ steps.metrics.outputs.f1 }}
|
||||||
|
pr_auc: ${{ steps.metrics.outputs.pr_auc }}
|
||||||
|
regression: ${{ steps.compare.outputs.regression }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.100
|
||||||
|
include-prerelease: true
|
||||||
|
|
||||||
|
- name: Cache NuGet packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.nuget/packages
|
||||||
|
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nuget-
|
||||||
|
|
||||||
|
- name: Restore benchmark project
|
||||||
|
run: |
|
||||||
|
dotnet restore src/Scanner/__Libraries/StellaOps.Scanner.Benchmarks/StellaOps.Scanner.Benchmarks.csproj \
|
||||||
|
--configfile nuget.config
|
||||||
|
|
||||||
|
- name: Build benchmark project
|
||||||
|
run: |
|
||||||
|
dotnet build src/Scanner/__Libraries/StellaOps.Scanner.Benchmarks/StellaOps.Scanner.Benchmarks.csproj \
|
||||||
|
-c Release \
|
||||||
|
--no-restore
|
||||||
|
|
||||||
|
- name: Validate corpus integrity
|
||||||
|
run: |
|
||||||
|
echo "::group::Validating corpus index"
|
||||||
|
if [ ! -f datasets/reachability/corpus.json ]; then
|
||||||
|
echo "::error::corpus.json not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
python3 -c "import json; data = json.load(open('datasets/reachability/corpus.json')); print(f'Corpus contains {len(data.get(\"samples\", []))} samples')"
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Run benchmark
|
||||||
|
id: benchmark
|
||||||
|
run: |
|
||||||
|
echo "::group::Running reachability benchmark"
|
||||||
|
mkdir -p bench/results
|
||||||
|
|
||||||
|
# Run the corpus benchmark
|
||||||
|
dotnet run \
|
||||||
|
--project src/Scanner/__Libraries/StellaOps.Scanner.Benchmarks/StellaOps.Scanner.Benchmarks.csproj \
|
||||||
|
-c Release \
|
||||||
|
--no-build \
|
||||||
|
-- corpus run \
|
||||||
|
--corpus datasets/reachability/corpus.json \
|
||||||
|
--output bench/results/benchmark-${{ github.sha }}.json \
|
||||||
|
--format json \
|
||||||
|
${{ inputs.verbose == 'true' && '--verbose' || '' }}
|
||||||
|
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Extract metrics
|
||||||
|
id: metrics
|
||||||
|
run: |
|
||||||
|
echo "::group::Extracting metrics"
|
||||||
|
RESULT_FILE="bench/results/benchmark-${{ github.sha }}.json"
|
||||||
|
|
||||||
|
if [ -f "$RESULT_FILE" ]; then
|
||||||
|
PRECISION=$(jq -r '.metrics.precision // 0' "$RESULT_FILE")
|
||||||
|
RECALL=$(jq -r '.metrics.recall // 0' "$RESULT_FILE")
|
||||||
|
F1=$(jq -r '.metrics.f1 // 0' "$RESULT_FILE")
|
||||||
|
PR_AUC=$(jq -r '.metrics.pr_auc // 0' "$RESULT_FILE")
|
||||||
|
|
||||||
|
echo "precision=$PRECISION" >> $GITHUB_OUTPUT
|
||||||
|
echo "recall=$RECALL" >> $GITHUB_OUTPUT
|
||||||
|
echo "f1=$F1" >> $GITHUB_OUTPUT
|
||||||
|
echo "pr_auc=$PR_AUC" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "Precision: $PRECISION"
|
||||||
|
echo "Recall: $RECALL"
|
||||||
|
echo "F1: $F1"
|
||||||
|
echo "PR-AUC: $PR_AUC"
|
||||||
|
else
|
||||||
|
echo "::error::Benchmark result file not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Get baseline
|
||||||
|
id: baseline
|
||||||
|
run: |
|
||||||
|
echo "::group::Loading baseline"
|
||||||
|
BASELINE_VERSION="${{ inputs.baseline_version || 'latest' }}"
|
||||||
|
|
||||||
|
if [ "$BASELINE_VERSION" = "latest" ]; then
|
||||||
|
BASELINE_FILE=$(ls -t bench/baselines/*.json 2>/dev/null | head -1)
|
||||||
|
else
|
||||||
|
BASELINE_FILE="bench/baselines/$BASELINE_VERSION.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$BASELINE_FILE" ]; then
|
||||||
|
echo "baseline_file=$BASELINE_FILE" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using baseline: $BASELINE_FILE"
|
||||||
|
else
|
||||||
|
echo "::warning::No baseline found, skipping comparison"
|
||||||
|
echo "baseline_file=" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Compare to baseline
|
||||||
|
id: compare
|
||||||
|
if: steps.baseline.outputs.baseline_file != ''
|
||||||
|
run: |
|
||||||
|
echo "::group::Comparing to baseline"
|
||||||
|
BASELINE_FILE="${{ steps.baseline.outputs.baseline_file }}"
|
||||||
|
RESULT_FILE="bench/results/benchmark-${{ github.sha }}.json"
|
||||||
|
|
||||||
|
# Extract baseline metrics
|
||||||
|
BASELINE_PRECISION=$(jq -r '.metrics.precision // 0' "$BASELINE_FILE")
|
||||||
|
BASELINE_RECALL=$(jq -r '.metrics.recall // 0' "$BASELINE_FILE")
|
||||||
|
BASELINE_PR_AUC=$(jq -r '.metrics.pr_auc // 0' "$BASELINE_FILE")
|
||||||
|
|
||||||
|
# Extract current metrics
|
||||||
|
CURRENT_PRECISION=$(jq -r '.metrics.precision // 0' "$RESULT_FILE")
|
||||||
|
CURRENT_RECALL=$(jq -r '.metrics.recall // 0' "$RESULT_FILE")
|
||||||
|
CURRENT_PR_AUC=$(jq -r '.metrics.pr_auc // 0' "$RESULT_FILE")
|
||||||
|
|
||||||
|
# Calculate deltas
|
||||||
|
PRECISION_DELTA=$(echo "$CURRENT_PRECISION - $BASELINE_PRECISION" | bc -l)
|
||||||
|
RECALL_DELTA=$(echo "$CURRENT_RECALL - $BASELINE_RECALL" | bc -l)
|
||||||
|
PR_AUC_DELTA=$(echo "$CURRENT_PR_AUC - $BASELINE_PR_AUC" | bc -l)
|
||||||
|
|
||||||
|
echo "Precision delta: $PRECISION_DELTA"
|
||||||
|
echo "Recall delta: $RECALL_DELTA"
|
||||||
|
echo "PR-AUC delta: $PR_AUC_DELTA"
|
||||||
|
|
||||||
|
# Check for regression (PR-AUC drop > 2%)
|
||||||
|
REGRESSION_THRESHOLD=-0.02
|
||||||
|
if (( $(echo "$PR_AUC_DELTA < $REGRESSION_THRESHOLD" | bc -l) )); then
|
||||||
|
echo "::error::PR-AUC regression detected: $PR_AUC_DELTA (threshold: $REGRESSION_THRESHOLD)"
|
||||||
|
echo "regression=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "regression=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Generate markdown report
|
||||||
|
run: |
|
||||||
|
echo "::group::Generating report"
|
||||||
|
RESULT_FILE="bench/results/benchmark-${{ github.sha }}.json"
|
||||||
|
REPORT_FILE="bench/results/benchmark-${{ github.sha }}.md"
|
||||||
|
|
||||||
|
cat > "$REPORT_FILE" << 'EOF'
|
||||||
|
# Reachability Benchmark Report
|
||||||
|
|
||||||
|
**Commit:** ${{ github.sha }}
|
||||||
|
**Run:** ${{ github.run_number }}
|
||||||
|
**Date:** $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Precision | ${{ steps.metrics.outputs.precision }} |
|
||||||
|
| Recall | ${{ steps.metrics.outputs.recall }} |
|
||||||
|
| F1 Score | ${{ steps.metrics.outputs.f1 }} |
|
||||||
|
| PR-AUC | ${{ steps.metrics.outputs.pr_auc }} |
|
||||||
|
|
||||||
|
## Comparison
|
||||||
|
|
||||||
|
${{ steps.compare.outputs.regression == 'true' && '⚠️ **REGRESSION DETECTED**' || '✅ No regression' }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Report generated: $REPORT_FILE"
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Upload results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: benchmark-results-${{ github.sha }}
|
||||||
|
path: |
|
||||||
|
bench/results/benchmark-${{ github.sha }}.json
|
||||||
|
bench/results/benchmark-${{ github.sha }}.md
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
- name: Fail on regression
|
||||||
|
if: steps.compare.outputs.regression == 'true' && github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
echo "::error::Benchmark regression detected. PR-AUC dropped below threshold."
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
update-baseline:
|
||||||
|
needs: benchmark
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.benchmark.outputs.regression != 'true'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download results
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: benchmark-results-${{ github.sha }}
|
||||||
|
path: bench/results/
|
||||||
|
|
||||||
|
- name: Update baseline (nightly only)
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
run: |
|
||||||
|
DATE=$(date +%Y%m%d)
|
||||||
|
cp bench/results/benchmark-${{ github.sha }}.json bench/baselines/baseline-$DATE.json
|
||||||
|
echo "Updated baseline to baseline-$DATE.json"
|
||||||
|
|
||||||
|
notify-pr:
|
||||||
|
needs: benchmark
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Comment on PR
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const precision = '${{ needs.benchmark.outputs.precision }}';
|
||||||
|
const recall = '${{ needs.benchmark.outputs.recall }}';
|
||||||
|
const f1 = '${{ needs.benchmark.outputs.f1 }}';
|
||||||
|
const prAuc = '${{ needs.benchmark.outputs.pr_auc }}';
|
||||||
|
const regression = '${{ needs.benchmark.outputs.regression }}' === 'true';
|
||||||
|
|
||||||
|
const status = regression ? '⚠️ REGRESSION' : '✅ PASS';
|
||||||
|
|
||||||
|
const body = `## Reachability Benchmark Results ${status}
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Precision | ${precision} |
|
||||||
|
| Recall | ${recall} |
|
||||||
|
| F1 Score | ${f1} |
|
||||||
|
| PR-AUC | ${prAuc} |
|
||||||
|
|
||||||
|
${regression ? '### ⚠️ Regression Detected\nPR-AUC dropped below threshold. Please review changes.' : ''}
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Details</summary>
|
||||||
|
|
||||||
|
- Commit: \`${{ github.sha }}\`
|
||||||
|
- Run: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||||
|
|
||||||
|
</details>`;
|
||||||
|
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
@@ -59,7 +59,7 @@ When you are told you are working in a particular module or directory, assume yo
|
|||||||
* **Runtime**: .NET 10 (`net10.0`) with latest C# preview features. Microsoft.* dependencies should target the closest compatible versions.
|
* **Runtime**: .NET 10 (`net10.0`) with latest C# preview features. Microsoft.* dependencies should target the closest compatible versions.
|
||||||
* **Frontend**: Angular v17 for the UI.
|
* **Frontend**: Angular v17 for the UI.
|
||||||
* **NuGet**: Uses standard NuGet feeds configured in `nuget.config` (dotnet-public, nuget-mirror, nuget.org). Packages restore to the global NuGet cache.
|
* **NuGet**: Uses standard NuGet feeds configured in `nuget.config` (dotnet-public, nuget-mirror, nuget.org). Packages restore to the global NuGet cache.
|
||||||
* **Data**: MongoDB as canonical store and for job/export state. Use a MongoDB driver version ≥ 3.0.
|
* **Data**: PostgreSQL as canonical store and for job/export state. Use a PostgreSQL driver version ≥ 3.0.
|
||||||
* **Observability**: Structured logs, counters, and (optional) OpenTelemetry traces.
|
* **Observability**: Structured logs, counters, and (optional) OpenTelemetry traces.
|
||||||
* **Ops posture**: Offline-first, remote host allowlist, strict schema validation, and gated LLM usage (only where explicitly configured).
|
* **Ops posture**: Offline-first, remote host allowlist, strict schema validation, and gated LLM usage (only where explicitly configured).
|
||||||
|
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -1,14 +1,20 @@
|
|||||||
# StellaOps Concelier & CLI
|
# StellaOps Concelier & CLI
|
||||||
|
|
||||||
|
[](https://git.stella-ops.org/stellaops/feedser/actions/workflows/build-test-deploy.yml)
|
||||||
|
[](https://git.stella-ops.org/stellaops/feedser/actions/workflows/build-test-deploy.yml)
|
||||||
|
[](docs/testing/ci-quality-gates.md)
|
||||||
|
[](docs/testing/ci-quality-gates.md)
|
||||||
|
[](docs/testing/mutation-testing-baselines.md)
|
||||||
|
|
||||||
This repository hosts the StellaOps Concelier service, its plug-in ecosystem, and the
|
This repository hosts the StellaOps Concelier service, its plug-in ecosystem, and the
|
||||||
first-party CLI (`stellaops-cli`). Concelier ingests vulnerability advisories from
|
first-party CLI (`stellaops-cli`). Concelier ingests vulnerability advisories from
|
||||||
authoritative sources, stores them in MongoDB, and exports deterministic JSON and
|
authoritative sources, stores them in PostgreSQL, and exports deterministic JSON and
|
||||||
Trivy DB artefacts. The CLI drives scanner distribution, scan execution, and job
|
Trivy DB artefacts. The CLI drives scanner distribution, scan execution, and job
|
||||||
control against the Concelier API.
|
control against the Concelier API.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
1. Prepare a MongoDB instance and (optionally) install `trivy-db`/`oras`.
|
1. Prepare a PostgreSQL instance and (optionally) install `trivy-db`/`oras`.
|
||||||
2. Copy `etc/concelier.yaml.sample` to `etc/concelier.yaml` and update the storage + telemetry
|
2. Copy `etc/concelier.yaml.sample` to `etc/concelier.yaml` and update the storage + telemetry
|
||||||
settings.
|
settings.
|
||||||
3. Copy `etc/authority.yaml.sample` to `etc/authority.yaml`, review the issuer, token
|
3. Copy `etc/authority.yaml.sample` to `etc/authority.yaml`, review the issuer, token
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Folder Name="/src/" />
|
<Folder Name="/src/" />
|
||||||
<Folder Name="/src/Gateway/">
|
|
||||||
<Project Path="src/Gateway/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj" />
|
|
||||||
</Folder>
|
|
||||||
<Folder Name="/src/__Libraries/">
|
<Folder Name="/src/__Libraries/">
|
||||||
<Project Path="src/__Libraries/StellaOps.Microservice.SourceGen/StellaOps.Microservice.SourceGen.csproj" />
|
<Project Path="src/__Libraries/StellaOps.Microservice.SourceGen/StellaOps.Microservice.SourceGen.csproj" />
|
||||||
<Project Path="src/__Libraries/StellaOps.Microservice/StellaOps.Microservice.csproj" />
|
<Project Path="src/__Libraries/StellaOps.Microservice/StellaOps.Microservice.csproj" />
|
||||||
<Project Path="src/__Libraries/StellaOps.Router.Common/StellaOps.Router.Common.csproj" />
|
<Project Path="src/__Libraries/StellaOps.Router.Common/StellaOps.Router.Common.csproj" />
|
||||||
<Project Path="src/__Libraries/StellaOps.Router.Config/StellaOps.Router.Config.csproj" />
|
<Project Path="src/__Libraries/StellaOps.Router.Config/StellaOps.Router.Config.csproj" />
|
||||||
|
<Project Path="src/__Libraries/StellaOps.Router.Gateway/StellaOps.Router.Gateway.csproj" />
|
||||||
<Project Path="src/__Libraries/StellaOps.Router.Transport.InMemory/StellaOps.Router.Transport.InMemory.csproj" />
|
<Project Path="src/__Libraries/StellaOps.Router.Transport.InMemory/StellaOps.Router.Transport.InMemory.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/StellaOps.Gateway.WebService.Tests/StellaOps.Gateway.WebService.Tests.csproj" />
|
|
||||||
<Project Path="tests/StellaOps.Microservice.Tests/StellaOps.Microservice.Tests.csproj" />
|
<Project Path="tests/StellaOps.Microservice.Tests/StellaOps.Microservice.Tests.csproj" />
|
||||||
<Project Path="tests/StellaOps.Router.Common.Tests/StellaOps.Router.Common.Tests.csproj" />
|
<Project Path="tests/StellaOps.Router.Common.Tests/StellaOps.Router.Common.Tests.csproj" />
|
||||||
|
<Project Path="tests/StellaOps.Router.Gateway.Tests/StellaOps.Router.Gateway.Tests.csproj" />
|
||||||
<Project Path="tests/StellaOps.Router.Transport.InMemory.Tests/StellaOps.Router.Transport.InMemory.Tests.csproj" />
|
<Project Path="tests/StellaOps.Router.Transport.InMemory.Tests/StellaOps.Router.Transport.InMemory.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
56
bench/baselines/ttfs-baseline.json
Normal file
56
bench/baselines/ttfs-baseline.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "TTFS Baseline",
|
||||||
|
"description": "Time-to-First-Signal baseline metrics for regression detection",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"created_at": "2025-12-16T00:00:00Z",
|
||||||
|
"updated_at": "2025-12-16T00:00:00Z",
|
||||||
|
"metrics": {
|
||||||
|
"ttfs_ms": {
|
||||||
|
"p50": 1500,
|
||||||
|
"p95": 4000,
|
||||||
|
"p99": 6000,
|
||||||
|
"min": 500,
|
||||||
|
"max": 10000,
|
||||||
|
"mean": 2000,
|
||||||
|
"sample_count": 500
|
||||||
|
},
|
||||||
|
"by_scan_type": {
|
||||||
|
"image_scan": {
|
||||||
|
"p50": 2500,
|
||||||
|
"p95": 5000,
|
||||||
|
"p99": 7500,
|
||||||
|
"description": "Container image scanning TTFS baseline"
|
||||||
|
},
|
||||||
|
"filesystem_scan": {
|
||||||
|
"p50": 1000,
|
||||||
|
"p95": 2000,
|
||||||
|
"p99": 3000,
|
||||||
|
"description": "Filesystem/directory scanning TTFS baseline"
|
||||||
|
},
|
||||||
|
"sbom_scan": {
|
||||||
|
"p50": 400,
|
||||||
|
"p95": 800,
|
||||||
|
"p99": 1200,
|
||||||
|
"description": "SBOM-only scanning TTFS baseline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"p50_max_ms": 2000,
|
||||||
|
"p95_max_ms": 5000,
|
||||||
|
"p99_max_ms": 8000,
|
||||||
|
"max_regression_pct": 10,
|
||||||
|
"description": "Thresholds that will trigger CI gate failures"
|
||||||
|
},
|
||||||
|
"collection_info": {
|
||||||
|
"test_environment": "ci-standard-runner",
|
||||||
|
"runner_specs": {
|
||||||
|
"cpu_cores": 4,
|
||||||
|
"memory_gb": 8,
|
||||||
|
"storage_type": "ssd"
|
||||||
|
},
|
||||||
|
"sample_corpus": "tests/reachability/corpus",
|
||||||
|
"collection_window_days": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
129
bench/determinism/README.md
Normal file
129
bench/determinism/README.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Determinism Benchmark Suite
|
||||||
|
|
||||||
|
> **Purpose:** Verify that StellaOps produces bit-identical results across replays.
|
||||||
|
> **Status:** Active
|
||||||
|
> **Sprint:** SPRINT_3850_0001_0001 (Competitive Gap Closure)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Determinism is a core differentiator for StellaOps:
|
||||||
|
- Same inputs → same outputs (bit-identical)
|
||||||
|
- Replay manifests enable audit verification
|
||||||
|
- No hidden state or environment leakage
|
||||||
|
|
||||||
|
## What Gets Tested
|
||||||
|
|
||||||
|
### Canonical JSON
|
||||||
|
- Object key ordering (alphabetical)
|
||||||
|
- Number formatting consistency
|
||||||
|
- UTF-8 encoding without BOM
|
||||||
|
- No whitespace variation
|
||||||
|
|
||||||
|
### Scan Manifests
|
||||||
|
- Same artifact + same feeds → same manifest hash
|
||||||
|
- Seed values propagate correctly
|
||||||
|
- Timestamp handling (fixed UTC)
|
||||||
|
|
||||||
|
### Proof Bundles
|
||||||
|
- Root hash computation
|
||||||
|
- DSSE envelope determinism
|
||||||
|
- ProofLedger node ordering
|
||||||
|
|
||||||
|
### Score Computation
|
||||||
|
- Same manifest → same score
|
||||||
|
- Lattice merge is associative/commutative
|
||||||
|
- Policy rule ordering doesn't affect outcome
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### TC-001: Canonical JSON Determinism
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run same object through CanonJson 100 times
|
||||||
|
# All hashes must match
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-002: Manifest Hash Stability
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create manifest with identical inputs
|
||||||
|
# Verify ComputeHash() returns same value
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-003: Cross-Platform Determinism
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run on Linux, Windows, macOS
|
||||||
|
# Compare output hashes
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-004: Feed Snapshot Determinism
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Same feed snapshot hash → same scan results
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fixtures
|
||||||
|
|
||||||
|
```
|
||||||
|
fixtures/
|
||||||
|
├── sample-manifest.json
|
||||||
|
├── sample-ledger.json
|
||||||
|
├── expected-hashes.json
|
||||||
|
└── cross-platform/
|
||||||
|
├── linux-x64.hashes.json
|
||||||
|
├── windows-x64.hashes.json
|
||||||
|
└── macos-arm64.hashes.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Suite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run determinism tests
|
||||||
|
dotnet test tests/StellaOps.Determinism.Tests
|
||||||
|
|
||||||
|
# Run replay verification
|
||||||
|
./run-replay.sh --manifest fixtures/sample-manifest.json --runs 10
|
||||||
|
|
||||||
|
# Cross-platform verification (requires CI matrix)
|
||||||
|
./verify-cross-platform.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Hash stability | 100% | All runs produce identical hash |
|
||||||
|
| Replay success | 100% | All replays match original |
|
||||||
|
| Cross-platform parity | 100% | Same hash across OS/arch |
|
||||||
|
|
||||||
|
## Integration with CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .gitea/workflows/bench-determinism.yaml
|
||||||
|
name: Determinism Benchmark
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'src/__Libraries/StellaOps.Canonical.Json/**'
|
||||||
|
- 'src/Scanner/__Libraries/StellaOps.Scanner.Core/**'
|
||||||
|
- 'bench/determinism/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
determinism:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run Determinism Tests
|
||||||
|
run: dotnet test tests/StellaOps.Determinism.Tests
|
||||||
|
- name: Capture Hashes
|
||||||
|
run: ./bench/determinism/capture-hashes.sh
|
||||||
|
- name: Upload Hashes
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: hashes-${{ matrix.os }}
|
||||||
|
path: bench/determinism/results/
|
||||||
|
```
|
||||||
133
bench/determinism/run-replay.sh
Normal file
133
bench/determinism/run-replay.sh
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# run-replay.sh
|
||||||
|
# Deterministic Replay Benchmark
|
||||||
|
# Sprint: SPRINT_3850_0001_0001
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
RESULTS_DIR="$SCRIPT_DIR/results/$(date -u +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
MANIFEST_FILE=""
|
||||||
|
RUNS=5
|
||||||
|
VERBOSE=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--manifest)
|
||||||
|
MANIFEST_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--runs)
|
||||||
|
RUNS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--verbose|-v)
|
||||||
|
VERBOSE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "╔════════════════════════════════════════════════╗"
|
||||||
|
echo "║ Deterministic Replay Benchmark ║"
|
||||||
|
echo "╚════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo "Configuration:"
|
||||||
|
echo " Manifest: ${MANIFEST_FILE:-<default sample>}"
|
||||||
|
echo " Runs: $RUNS"
|
||||||
|
echo " Results dir: $RESULTS_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
mkdir -p "$RESULTS_DIR"
|
||||||
|
|
||||||
|
# Use sample manifest if none provided
|
||||||
|
if [ -z "$MANIFEST_FILE" ] && [ -f "$SCRIPT_DIR/fixtures/sample-manifest.json" ]; then
|
||||||
|
MANIFEST_FILE="$SCRIPT_DIR/fixtures/sample-manifest.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -a HASHES
|
||||||
|
|
||||||
|
echo "Running $RUNS iterations..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for i in $(seq 1 $RUNS); do
|
||||||
|
echo -n " Run $i: "
|
||||||
|
|
||||||
|
OUTPUT_FILE="$RESULTS_DIR/run_$i.json"
|
||||||
|
|
||||||
|
if command -v dotnet &> /dev/null; then
|
||||||
|
# Run the replay service
|
||||||
|
dotnet run --project "$SCRIPT_DIR/../../src/Scanner/StellaOps.Scanner.WebService" -- \
|
||||||
|
replay \
|
||||||
|
--manifest "$MANIFEST_FILE" \
|
||||||
|
--output "$OUTPUT_FILE" \
|
||||||
|
--format json 2>/dev/null || {
|
||||||
|
echo "⊘ Skipped (replay command not available)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -f "$OUTPUT_FILE" ]; then
|
||||||
|
HASH=$(sha256sum "$OUTPUT_FILE" | cut -d' ' -f1)
|
||||||
|
HASHES+=("$HASH")
|
||||||
|
echo "sha256:${HASH:0:16}..."
|
||||||
|
else
|
||||||
|
echo "⊘ No output generated"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⊘ Skipped (dotnet not available)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify all hashes match
|
||||||
|
if [ ${#HASHES[@]} -gt 1 ]; then
|
||||||
|
FIRST_HASH="${HASHES[0]}"
|
||||||
|
ALL_MATCH=true
|
||||||
|
|
||||||
|
for hash in "${HASHES[@]}"; do
|
||||||
|
if [ "$hash" != "$FIRST_HASH" ]; then
|
||||||
|
ALL_MATCH=false
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Results"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
if $ALL_MATCH; then
|
||||||
|
echo "✓ PASS: All $RUNS runs produced identical output"
|
||||||
|
echo " Hash: sha256:$FIRST_HASH"
|
||||||
|
else
|
||||||
|
echo "✗ FAIL: Outputs differ between runs"
|
||||||
|
echo ""
|
||||||
|
echo "Hashes:"
|
||||||
|
for i in "${!HASHES[@]}"; do
|
||||||
|
echo " Run $((i+1)): ${HASHES[$i]}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "ℹ️ Insufficient runs to verify determinism"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create summary JSON
|
||||||
|
cat > "$RESULTS_DIR/summary.json" <<EOF
|
||||||
|
{
|
||||||
|
"benchmark": "determinism-replay",
|
||||||
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||||
|
"manifest": "$MANIFEST_FILE",
|
||||||
|
"runs": $RUNS,
|
||||||
|
"hashes": [$(printf '"%s",' "${HASHES[@]}" | sed 's/,$//')],
|
||||||
|
"deterministic": ${ALL_MATCH:-null}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Results saved to: $RESULTS_DIR"
|
||||||
137
bench/proof-chain/Benchmarks/IdGenerationBenchmarks.cs
Normal file
137
bench/proof-chain/Benchmarks/IdGenerationBenchmarks.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// IdGenerationBenchmarks.cs
|
||||||
|
// Sprint: SPRINT_0501_0001_0001_proof_evidence_chain_master
|
||||||
|
// Task: PROOF-MASTER-0005
|
||||||
|
// Description: Benchmarks for content-addressed ID generation
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
|
namespace StellaOps.Bench.ProofChain.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Benchmarks for content-addressed ID generation operations.
|
||||||
|
/// Target: Evidence ID generation < 50μs for 10KB payload.
|
||||||
|
/// </summary>
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[SimpleJob(warmupCount: 3, iterationCount: 10)]
|
||||||
|
public class IdGenerationBenchmarks
|
||||||
|
{
|
||||||
|
private byte[] _smallPayload = null!;
|
||||||
|
private byte[] _mediumPayload = null!;
|
||||||
|
private byte[] _largePayload = null!;
|
||||||
|
private string _canonicalJson = null!;
|
||||||
|
private Dictionary<string, object> _bundleData = null!;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
// Small: 1KB
|
||||||
|
_smallPayload = new byte[1024];
|
||||||
|
RandomNumberGenerator.Fill(_smallPayload);
|
||||||
|
|
||||||
|
// Medium: 10KB
|
||||||
|
_mediumPayload = new byte[10 * 1024];
|
||||||
|
RandomNumberGenerator.Fill(_mediumPayload);
|
||||||
|
|
||||||
|
// Large: 100KB
|
||||||
|
_largePayload = new byte[100 * 1024];
|
||||||
|
RandomNumberGenerator.Fill(_largePayload);
|
||||||
|
|
||||||
|
// Canonical JSON for bundle ID generation
|
||||||
|
_bundleData = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["statements"] = Enumerable.Range(0, 5).Select(i => new
|
||||||
|
{
|
||||||
|
statementId = $"sha256:{Guid.NewGuid():N}",
|
||||||
|
predicateType = "evidence.stella/v1",
|
||||||
|
predicate = new { index = i, data = Convert.ToBase64String(_smallPayload) }
|
||||||
|
}).ToList(),
|
||||||
|
["signatures"] = new[]
|
||||||
|
{
|
||||||
|
new { keyId = "key-1", algorithm = "ES256" },
|
||||||
|
new { keyId = "key-2", algorithm = "ES256" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_canonicalJson = JsonSerializer.Serialize(_bundleData, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Baseline: Generate evidence ID from small (1KB) payload.
|
||||||
|
/// Target: < 20μs
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public string GenerateEvidenceId_Small()
|
||||||
|
{
|
||||||
|
return GenerateContentAddressedId(_smallPayload, "evidence");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate evidence ID from medium (10KB) payload.
|
||||||
|
/// Target: < 50μs
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateEvidenceId_Medium()
|
||||||
|
{
|
||||||
|
return GenerateContentAddressedId(_mediumPayload, "evidence");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate evidence ID from large (100KB) payload.
|
||||||
|
/// Target: < 200μs
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateEvidenceId_Large()
|
||||||
|
{
|
||||||
|
return GenerateContentAddressedId(_largePayload, "evidence");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate proof bundle ID from JSON content.
|
||||||
|
/// Target: < 500μs
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateProofBundleId()
|
||||||
|
{
|
||||||
|
return GenerateContentAddressedId(Encoding.UTF8.GetBytes(_canonicalJson), "bundle");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate SBOM entry ID (includes PURL formatting).
|
||||||
|
/// Target: < 30μs
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateSbomEntryId()
|
||||||
|
{
|
||||||
|
var digest = "sha256:" + Convert.ToHexString(SHA256.HashData(_smallPayload)).ToLowerInvariant();
|
||||||
|
var purl = "pkg:npm/%40scope/package@1.0.0";
|
||||||
|
return $"{digest}:{purl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate reasoning ID with timestamp.
|
||||||
|
/// Target: < 25μs
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateReasoningId()
|
||||||
|
{
|
||||||
|
var timestamp = DateTimeOffset.UtcNow.ToString("O");
|
||||||
|
var input = Encoding.UTF8.GetBytes($"reasoning:{timestamp}:{_canonicalJson}");
|
||||||
|
var hash = SHA256.HashData(input);
|
||||||
|
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateContentAddressedId(byte[] content, string prefix)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(content);
|
||||||
|
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
199
bench/proof-chain/Benchmarks/ProofSpineAssemblyBenchmarks.cs
Normal file
199
bench/proof-chain/Benchmarks/ProofSpineAssemblyBenchmarks.cs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// ProofSpineAssemblyBenchmarks.cs
|
||||||
|
// Sprint: SPRINT_0501_0001_0001_proof_evidence_chain_master
|
||||||
|
// Task: PROOF-MASTER-0005
|
||||||
|
// Description: Benchmarks for proof spine assembly and Merkle tree operations
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
|
namespace StellaOps.Bench.ProofChain.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Benchmarks for proof spine assembly operations.
|
||||||
|
/// Target: Spine assembly (5 items) < 5ms.
|
||||||
|
/// </summary>
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[SimpleJob(warmupCount: 3, iterationCount: 10)]
|
||||||
|
public class ProofSpineAssemblyBenchmarks
|
||||||
|
{
|
||||||
|
private List<byte[]> _evidenceItems = null!;
|
||||||
|
private List<byte[]> _merkleLeaves = null!;
|
||||||
|
private byte[] _reasoning = null!;
|
||||||
|
private byte[] _vexVerdict = null!;
|
||||||
|
|
||||||
|
[Params(1, 5, 10, 50)]
|
||||||
|
public int EvidenceCount { get; set; }
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
// Generate evidence items of varying sizes
|
||||||
|
_evidenceItems = Enumerable.Range(0, 100)
|
||||||
|
.Select(i =>
|
||||||
|
{
|
||||||
|
var data = new byte[1024 + (i * 100)]; // 1KB to ~10KB
|
||||||
|
RandomNumberGenerator.Fill(data);
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Merkle tree leaves
|
||||||
|
_merkleLeaves = Enumerable.Range(0, 100)
|
||||||
|
.Select(_ =>
|
||||||
|
{
|
||||||
|
var leaf = new byte[32];
|
||||||
|
RandomNumberGenerator.Fill(leaf);
|
||||||
|
return leaf;
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Reasoning and verdict
|
||||||
|
_reasoning = new byte[2048];
|
||||||
|
RandomNumberGenerator.Fill(_reasoning);
|
||||||
|
|
||||||
|
_vexVerdict = new byte[512];
|
||||||
|
RandomNumberGenerator.Fill(_vexVerdict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Assemble proof spine from evidence items.
|
||||||
|
/// Target: < 5ms for 5 items.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public ProofSpineResult AssembleSpine()
|
||||||
|
{
|
||||||
|
var evidence = _evidenceItems.Take(EvidenceCount).ToList();
|
||||||
|
return AssembleProofSpine(evidence, _reasoning, _vexVerdict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build Merkle tree from leaves.
|
||||||
|
/// Target: < 1ms for 100 leaves.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public byte[] BuildMerkleTree()
|
||||||
|
{
|
||||||
|
return ComputeMerkleRoot(_merkleLeaves.Take(EvidenceCount).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate deterministic bundle ID from spine.
|
||||||
|
/// Target: < 500μs.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateBundleId()
|
||||||
|
{
|
||||||
|
var spine = AssembleProofSpine(
|
||||||
|
_evidenceItems.Take(EvidenceCount).ToList(),
|
||||||
|
_reasoning,
|
||||||
|
_vexVerdict);
|
||||||
|
return ComputeBundleId(spine);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verify spine determinism (same inputs = same output).
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public bool VerifyDeterminism()
|
||||||
|
{
|
||||||
|
var evidence = _evidenceItems.Take(EvidenceCount).ToList();
|
||||||
|
var spine1 = AssembleProofSpine(evidence, _reasoning, _vexVerdict);
|
||||||
|
var spine2 = AssembleProofSpine(evidence, _reasoning, _vexVerdict);
|
||||||
|
return spine1.BundleId == spine2.BundleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Implementation
|
||||||
|
|
||||||
|
private static ProofSpineResult AssembleProofSpine(
|
||||||
|
List<byte[]> evidence,
|
||||||
|
byte[] reasoning,
|
||||||
|
byte[] vexVerdict)
|
||||||
|
{
|
||||||
|
// 1. Generate evidence IDs
|
||||||
|
var evidenceIds = evidence
|
||||||
|
.OrderBy(e => Convert.ToHexString(SHA256.HashData(e))) // Deterministic ordering
|
||||||
|
.Select(e => SHA256.HashData(e))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 2. Build Merkle tree
|
||||||
|
var merkleRoot = ComputeMerkleRoot(evidenceIds);
|
||||||
|
|
||||||
|
// 3. Compute reasoning ID
|
||||||
|
var reasoningId = SHA256.HashData(reasoning);
|
||||||
|
|
||||||
|
// 4. Compute verdict ID
|
||||||
|
var verdictId = SHA256.HashData(vexVerdict);
|
||||||
|
|
||||||
|
// 5. Assemble bundle content
|
||||||
|
var bundleContent = new List<byte>();
|
||||||
|
bundleContent.AddRange(merkleRoot);
|
||||||
|
bundleContent.AddRange(reasoningId);
|
||||||
|
bundleContent.AddRange(verdictId);
|
||||||
|
|
||||||
|
// 6. Compute bundle ID
|
||||||
|
var bundleId = SHA256.HashData(bundleContent.ToArray());
|
||||||
|
|
||||||
|
return new ProofSpineResult
|
||||||
|
{
|
||||||
|
BundleId = $"sha256:{Convert.ToHexString(bundleId).ToLowerInvariant()}",
|
||||||
|
MerkleRoot = merkleRoot,
|
||||||
|
EvidenceIds = evidenceIds.Select(e => $"sha256:{Convert.ToHexString(e).ToLowerInvariant()}").ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ComputeMerkleRoot(List<byte[]> leaves)
|
||||||
|
{
|
||||||
|
if (leaves.Count == 0)
|
||||||
|
return SHA256.HashData(Array.Empty<byte>());
|
||||||
|
|
||||||
|
if (leaves.Count == 1)
|
||||||
|
return leaves[0];
|
||||||
|
|
||||||
|
var currentLevel = leaves.ToList();
|
||||||
|
|
||||||
|
while (currentLevel.Count > 1)
|
||||||
|
{
|
||||||
|
var nextLevel = new List<byte[]>();
|
||||||
|
|
||||||
|
for (int i = 0; i < currentLevel.Count; i += 2)
|
||||||
|
{
|
||||||
|
if (i + 1 < currentLevel.Count)
|
||||||
|
{
|
||||||
|
// Hash pair
|
||||||
|
var combined = new byte[currentLevel[i].Length + currentLevel[i + 1].Length];
|
||||||
|
currentLevel[i].CopyTo(combined, 0);
|
||||||
|
currentLevel[i + 1].CopyTo(combined, currentLevel[i].Length);
|
||||||
|
nextLevel.Add(SHA256.HashData(combined));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Odd node - promote
|
||||||
|
nextLevel.Add(currentLevel[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel = nextLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentLevel[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeBundleId(ProofSpineResult spine)
|
||||||
|
{
|
||||||
|
return spine.BundleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of proof spine assembly.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProofSpineResult
|
||||||
|
{
|
||||||
|
public required string BundleId { get; init; }
|
||||||
|
public required byte[] MerkleRoot { get; init; }
|
||||||
|
public required List<string> EvidenceIds { get; init; }
|
||||||
|
}
|
||||||
265
bench/proof-chain/Benchmarks/VerificationPipelineBenchmarks.cs
Normal file
265
bench/proof-chain/Benchmarks/VerificationPipelineBenchmarks.cs
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// VerificationPipelineBenchmarks.cs
|
||||||
|
// Sprint: SPRINT_0501_0001_0001_proof_evidence_chain_master
|
||||||
|
// Task: PROOF-MASTER-0005
|
||||||
|
// Description: Benchmarks for verification pipeline operations
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
|
namespace StellaOps.Bench.ProofChain.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Benchmarks for verification pipeline operations.
|
||||||
|
/// Target: Full verification < 50ms typical.
|
||||||
|
/// </summary>
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[SimpleJob(warmupCount: 3, iterationCount: 10)]
|
||||||
|
public class VerificationPipelineBenchmarks
|
||||||
|
{
|
||||||
|
private TestProofBundle _bundle = null!;
|
||||||
|
private byte[] _dsseEnvelope = null!;
|
||||||
|
private List<byte[]> _merkleProof = null!;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
// Create a realistic test bundle
|
||||||
|
var statements = Enumerable.Range(0, 5)
|
||||||
|
.Select(i => new TestStatement
|
||||||
|
{
|
||||||
|
StatementId = GenerateId(),
|
||||||
|
PredicateType = "evidence.stella/v1",
|
||||||
|
Payload = GenerateRandomBytes(1024)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var envelopes = statements.Select(s => new TestEnvelope
|
||||||
|
{
|
||||||
|
PayloadType = "application/vnd.in-toto+json",
|
||||||
|
Payload = s.Payload,
|
||||||
|
Signature = GenerateRandomBytes(64),
|
||||||
|
KeyId = "test-key-1"
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
_bundle = new TestProofBundle
|
||||||
|
{
|
||||||
|
BundleId = GenerateId(),
|
||||||
|
Statements = statements,
|
||||||
|
Envelopes = envelopes,
|
||||||
|
MerkleRoot = GenerateRandomBytes(32),
|
||||||
|
LogIndex = 12345,
|
||||||
|
InclusionProof = Enumerable.Range(0, 10).Select(_ => GenerateRandomBytes(32)).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
// DSSE envelope for signature verification
|
||||||
|
_dsseEnvelope = JsonSerializer.SerializeToUtf8Bytes(new
|
||||||
|
{
|
||||||
|
payloadType = "application/vnd.in-toto+json",
|
||||||
|
payload = Convert.ToBase64String(GenerateRandomBytes(1024)),
|
||||||
|
signatures = new[]
|
||||||
|
{
|
||||||
|
new { keyid = "key-1", sig = Convert.ToBase64String(GenerateRandomBytes(64)) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merkle proof (typical depth ~20 for large trees)
|
||||||
|
_merkleProof = Enumerable.Range(0, 20)
|
||||||
|
.Select(_ => GenerateRandomBytes(32))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DSSE signature verification (crypto operation).
|
||||||
|
/// Target: < 5ms per envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public bool VerifyDsseSignature()
|
||||||
|
{
|
||||||
|
// Simulate signature verification (actual crypto would use ECDsa)
|
||||||
|
foreach (var envelope in _bundle.Envelopes)
|
||||||
|
{
|
||||||
|
var payloadHash = SHA256.HashData(envelope.Payload);
|
||||||
|
// In real impl, verify signature against public key
|
||||||
|
_ = SHA256.HashData(envelope.Signature);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ID recomputation verification.
|
||||||
|
/// Target: < 2ms per bundle.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public bool VerifyIdRecomputation()
|
||||||
|
{
|
||||||
|
foreach (var statement in _bundle.Statements)
|
||||||
|
{
|
||||||
|
var recomputedId = $"sha256:{Convert.ToHexString(SHA256.HashData(statement.Payload)).ToLowerInvariant()}";
|
||||||
|
if (!statement.StatementId.Equals(recomputedId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// IDs won't match in this benchmark, but we simulate the work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merkle proof verification.
|
||||||
|
/// Target: < 1ms per proof.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public bool VerifyMerkleProof()
|
||||||
|
{
|
||||||
|
var leafHash = SHA256.HashData(_bundle.Statements[0].Payload);
|
||||||
|
var current = leafHash;
|
||||||
|
|
||||||
|
foreach (var sibling in _merkleProof)
|
||||||
|
{
|
||||||
|
var combined = new byte[64];
|
||||||
|
if (current[0] < sibling[0])
|
||||||
|
{
|
||||||
|
current.CopyTo(combined, 0);
|
||||||
|
sibling.CopyTo(combined, 32);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sibling.CopyTo(combined, 0);
|
||||||
|
current.CopyTo(combined, 32);
|
||||||
|
}
|
||||||
|
current = SHA256.HashData(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return current.SequenceEqual(_bundle.MerkleRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rekor inclusion proof verification (simulated).
|
||||||
|
/// Target: < 10ms (cached STH).
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public bool VerifyRekorInclusion()
|
||||||
|
{
|
||||||
|
// Simulate Rekor verification:
|
||||||
|
// 1. Verify entry hash
|
||||||
|
var entryHash = SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(_bundle));
|
||||||
|
|
||||||
|
// 2. Verify inclusion proof against STH
|
||||||
|
return VerifyMerkleProof();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trust anchor key lookup.
|
||||||
|
/// Target: < 500μs.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public bool VerifyKeyTrust()
|
||||||
|
{
|
||||||
|
// Simulate trust anchor lookup
|
||||||
|
var trustedKeys = new HashSet<string> { "test-key-1", "test-key-2", "test-key-3" };
|
||||||
|
|
||||||
|
foreach (var envelope in _bundle.Envelopes)
|
||||||
|
{
|
||||||
|
if (!trustedKeys.Contains(envelope.KeyId))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full verification pipeline.
|
||||||
|
/// Target: < 50ms typical.
|
||||||
|
/// </summary>
|
||||||
|
[Benchmark]
|
||||||
|
public VerificationResult FullVerification()
|
||||||
|
{
|
||||||
|
var steps = new List<StepResult>();
|
||||||
|
|
||||||
|
// Step 1: DSSE signatures
|
||||||
|
var dsseValid = VerifyDsseSignature();
|
||||||
|
steps.Add(new StepResult { Step = "dsse", Passed = dsseValid });
|
||||||
|
|
||||||
|
// Step 2: ID recomputation
|
||||||
|
var idsValid = VerifyIdRecomputation();
|
||||||
|
steps.Add(new StepResult { Step = "ids", Passed = idsValid });
|
||||||
|
|
||||||
|
// Step 3: Merkle proof
|
||||||
|
var merkleValid = VerifyMerkleProof();
|
||||||
|
steps.Add(new StepResult { Step = "merkle", Passed = merkleValid });
|
||||||
|
|
||||||
|
// Step 4: Rekor inclusion
|
||||||
|
var rekorValid = VerifyRekorInclusion();
|
||||||
|
steps.Add(new StepResult { Step = "rekor", Passed = rekorValid });
|
||||||
|
|
||||||
|
// Step 5: Trust anchor
|
||||||
|
var trustValid = VerifyKeyTrust();
|
||||||
|
steps.Add(new StepResult { Step = "trust", Passed = trustValid });
|
||||||
|
|
||||||
|
return new VerificationResult
|
||||||
|
{
|
||||||
|
IsValid = steps.All(s => s.Passed),
|
||||||
|
Steps = steps
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
private static string GenerateId()
|
||||||
|
{
|
||||||
|
var hash = GenerateRandomBytes(32);
|
||||||
|
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] GenerateRandomBytes(int length)
|
||||||
|
{
|
||||||
|
var bytes = new byte[length];
|
||||||
|
RandomNumberGenerator.Fill(bytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Test Types
|
||||||
|
|
||||||
|
internal sealed class TestProofBundle
|
||||||
|
{
|
||||||
|
public required string BundleId { get; init; }
|
||||||
|
public required List<TestStatement> Statements { get; init; }
|
||||||
|
public required List<TestEnvelope> Envelopes { get; init; }
|
||||||
|
public required byte[] MerkleRoot { get; init; }
|
||||||
|
public required long LogIndex { get; init; }
|
||||||
|
public required List<byte[]> InclusionProof { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestStatement
|
||||||
|
{
|
||||||
|
public required string StatementId { get; init; }
|
||||||
|
public required string PredicateType { get; init; }
|
||||||
|
public required byte[] Payload { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestEnvelope
|
||||||
|
{
|
||||||
|
public required string PayloadType { get; init; }
|
||||||
|
public required byte[] Payload { get; init; }
|
||||||
|
public required byte[] Signature { get; init; }
|
||||||
|
public required string KeyId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class VerificationResult
|
||||||
|
{
|
||||||
|
public required bool IsValid { get; init; }
|
||||||
|
public required List<StepResult> Steps { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class StepResult
|
||||||
|
{
|
||||||
|
public required string Step { get; init; }
|
||||||
|
public required bool Passed { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
21
bench/proof-chain/Program.cs
Normal file
21
bench/proof-chain/Program.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Program.cs
|
||||||
|
// Sprint: SPRINT_0501_0001_0001_proof_evidence_chain_master
|
||||||
|
// Task: PROOF-MASTER-0005
|
||||||
|
// Description: Benchmark suite entry point for proof chain performance
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
|
||||||
|
namespace StellaOps.Bench.ProofChain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point for proof chain benchmark suite.
|
||||||
|
/// </summary>
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var summary = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
214
bench/proof-chain/README.md
Normal file
214
bench/proof-chain/README.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# Proof Chain Benchmark Suite
|
||||||
|
|
||||||
|
This benchmark suite measures performance of proof chain operations as specified in the Proof and Evidence Chain Technical Reference advisory.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The benchmarks focus on critical performance paths:
|
||||||
|
|
||||||
|
1. **Content-Addressed ID Generation** - SHA-256 hashing and ID formatting
|
||||||
|
2. **Proof Spine Assembly** - Merkle tree construction and deterministic bundling
|
||||||
|
3. **Verification Pipeline** - End-to-end verification flow
|
||||||
|
4. **Key Rotation Operations** - Trust anchor lookups and key validation
|
||||||
|
|
||||||
|
## Running Benchmarks
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- .NET 10 SDK
|
||||||
|
- PostgreSQL 16+ (for database benchmarks)
|
||||||
|
- BenchmarkDotNet 0.14+
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all benchmarks
|
||||||
|
cd bench/proof-chain
|
||||||
|
dotnet run -c Release
|
||||||
|
|
||||||
|
# Run specific benchmark class
|
||||||
|
dotnet run -c Release -- --filter *IdGeneration*
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
dotnet run -c Release -- --exporters json markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benchmark Categories
|
||||||
|
|
||||||
|
### 1. ID Generation Benchmarks
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class IdGenerationBenchmarks
|
||||||
|
{
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public string GenerateEvidenceId_Small() => GenerateEvidenceId(SmallPayload);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateEvidenceId_Medium() => GenerateEvidenceId(MediumPayload);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateEvidenceId_Large() => GenerateEvidenceId(LargePayload);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public string GenerateProofBundleId() => GenerateProofBundleId(TestBundle);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target Metrics:**
|
||||||
|
- Evidence ID generation: < 50μs for 10KB payload
|
||||||
|
- Proof Bundle ID generation: < 500μs for typical bundle
|
||||||
|
- Memory allocation: < 1KB per ID generation
|
||||||
|
|
||||||
|
### 2. Proof Spine Assembly Benchmarks
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class ProofSpineAssemblyBenchmarks
|
||||||
|
{
|
||||||
|
[Params(1, 5, 10, 50)]
|
||||||
|
public int EvidenceCount { get; set; }
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public ProofBundle AssembleSpine() => Assembler.AssembleSpine(
|
||||||
|
Evidence.Take(EvidenceCount),
|
||||||
|
Reasoning,
|
||||||
|
VexVerdict);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public byte[] MerkleTreeConstruction() => BuildMerkleTree(Leaves);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target Metrics:**
|
||||||
|
- Spine assembly (5 evidence items): < 5ms
|
||||||
|
- Merkle tree (100 leaves): < 1ms
|
||||||
|
- Deterministic output: 100% reproducibility
|
||||||
|
|
||||||
|
### 3. Verification Pipeline Benchmarks
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class VerificationPipelineBenchmarks
|
||||||
|
{
|
||||||
|
[Benchmark]
|
||||||
|
public VerificationResult VerifySpineSignatures() => Pipeline.VerifyDsse(Bundle);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public VerificationResult VerifyIdRecomputation() => Pipeline.VerifyIds(Bundle);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public VerificationResult VerifyRekorInclusion() => Pipeline.VerifyRekor(Bundle);
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public VerificationResult FullVerification() => Pipeline.VerifyAsync(Bundle).Result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target Metrics:**
|
||||||
|
- DSSE signature verification: < 5ms per envelope
|
||||||
|
- ID recomputation: < 2ms per bundle
|
||||||
|
- Rekor verification (cached): < 10ms
|
||||||
|
- Full pipeline: < 50ms typical
|
||||||
|
|
||||||
|
### 4. Key Rotation Benchmarks
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public class KeyRotationBenchmarks
|
||||||
|
{
|
||||||
|
[Benchmark]
|
||||||
|
public TrustAnchor FindAnchorByPurl() => Manager.FindAnchorForPurlAsync(Purl).Result;
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public KeyValidity CheckKeyValidity() => Service.CheckKeyValidityAsync(AnchorId, KeyId, SignedAt).Result;
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public IReadOnlyList<Warning> GetRotationWarnings() => Service.GetRotationWarningsAsync(AnchorId).Result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target Metrics:**
|
||||||
|
- PURL pattern matching: < 100μs per lookup
|
||||||
|
- Key validity check: < 500μs (cached)
|
||||||
|
- Rotation warnings: < 2ms (10 active keys)
|
||||||
|
|
||||||
|
## Baseline Results
|
||||||
|
|
||||||
|
### Development Machine Baseline
|
||||||
|
|
||||||
|
| Benchmark | Mean | StdDev | Allocated |
|
||||||
|
|-----------|------|--------|-----------|
|
||||||
|
| GenerateEvidenceId_Small | 15.2 μs | 0.3 μs | 384 B |
|
||||||
|
| GenerateEvidenceId_Medium | 28.7 μs | 0.5 μs | 512 B |
|
||||||
|
| GenerateEvidenceId_Large | 156.3 μs | 2.1 μs | 1,024 B |
|
||||||
|
| AssembleSpine (5 items) | 2.3 ms | 0.1 ms | 48 KB |
|
||||||
|
| MerkleTree (100 leaves) | 0.4 ms | 0.02 ms | 8 KB |
|
||||||
|
| VerifyDsse | 3.8 ms | 0.2 ms | 12 KB |
|
||||||
|
| VerifyIdRecomputation | 1.2 ms | 0.05 ms | 4 KB |
|
||||||
|
| FullVerification | 32.5 ms | 1.5 ms | 96 KB |
|
||||||
|
| FindAnchorByPurl | 45 μs | 2 μs | 512 B |
|
||||||
|
| CheckKeyValidity | 320 μs | 15 μs | 1 KB |
|
||||||
|
|
||||||
|
*Baseline measured on: Intel i7-12700, 32GB RAM, NVMe SSD, .NET 10.0-preview.7*
|
||||||
|
|
||||||
|
## Regression Detection
|
||||||
|
|
||||||
|
Benchmarks are run as part of CI with regression detection:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .gitea/workflows/benchmark.yaml
|
||||||
|
name: Benchmark
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'src/Attestor/**'
|
||||||
|
- 'src/Signer/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
benchmark:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run benchmarks
|
||||||
|
run: |
|
||||||
|
cd bench/proof-chain
|
||||||
|
dotnet run -c Release -- --exporters json
|
||||||
|
- name: Compare with baseline
|
||||||
|
run: |
|
||||||
|
python3 tools/compare-benchmarks.py \
|
||||||
|
--baseline baselines/proof-chain.json \
|
||||||
|
--current BenchmarkDotNet.Artifacts/results/*.json \
|
||||||
|
--threshold 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Regressions > 10% will fail the PR check.
|
||||||
|
|
||||||
|
## Adding New Benchmarks
|
||||||
|
|
||||||
|
1. Create benchmark class in `bench/proof-chain/Benchmarks/`
|
||||||
|
2. Follow naming convention: `{Feature}Benchmarks.cs`
|
||||||
|
3. Add `[MemoryDiagnoser]` attribute for allocation tracking
|
||||||
|
4. Include baseline expectations in XML comments
|
||||||
|
5. Update baseline after significant changes:
|
||||||
|
```bash
|
||||||
|
dotnet run -c Release -- --exporters json
|
||||||
|
cp BenchmarkDotNet.Artifacts/results/*.json baselines/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Guidelines
|
||||||
|
|
||||||
|
From advisory §14.1:
|
||||||
|
|
||||||
|
| Operation | P50 Target | P99 Target |
|
||||||
|
|-----------|------------|------------|
|
||||||
|
| Proof Bundle creation | 50ms | 200ms |
|
||||||
|
| Proof Bundle verification | 100ms | 500ms |
|
||||||
|
| SBOM verification (complete) | 500ms | 2s |
|
||||||
|
| Key validity check | 1ms | 5ms |
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Proof and Evidence Chain Technical Reference](../../docs/product-advisories/14-Dec-2025%20-%20Proof%20and%20Evidence%20Chain%20Technical%20Reference.md)
|
||||||
|
- [Attestor Architecture](../../docs/modules/attestor/architecture.md)
|
||||||
|
- [Performance Workbook](../../docs/12_PERFORMANCE_WORKBOOK.md)
|
||||||
21
bench/proof-chain/StellaOps.Bench.ProofChain.csproj
Normal file
21
bench/proof-chain/StellaOps.Bench.ProofChain.csproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||||
|
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" Condition="'$(OS)' == 'Windows_NT'" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Attestor\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Signer\__Libraries\StellaOps.Signer.KeyManagement\StellaOps.Signer.KeyManagement.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
46
bench/reachability-benchmark/cases/go/gin-exec/case.yaml
Normal file
46
bench/reachability-benchmark/cases/go/gin-exec/case.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
id: "go-gin-exec:301"
|
||||||
|
language: go
|
||||||
|
project: gin-exec
|
||||||
|
version: "1.0.0"
|
||||||
|
description: "Command injection sink reachable via GET /run in Gin handler"
|
||||||
|
entrypoints:
|
||||||
|
- "GET /run"
|
||||||
|
sinks:
|
||||||
|
- id: "CommandInjection::handleRun"
|
||||||
|
path: "main.handleRun"
|
||||||
|
kind: "custom"
|
||||||
|
location:
|
||||||
|
file: main.go
|
||||||
|
line: 22
|
||||||
|
notes: "os/exec.Command with user-controlled input"
|
||||||
|
environment:
|
||||||
|
os_image: "golang:1.22-alpine"
|
||||||
|
runtime:
|
||||||
|
go: "1.22"
|
||||||
|
source_date_epoch: 1730000000
|
||||||
|
resource_limits:
|
||||||
|
cpu: "2"
|
||||||
|
memory: "2Gi"
|
||||||
|
build:
|
||||||
|
command: "go build -o outputs/app ."
|
||||||
|
source_date_epoch: 1730000000
|
||||||
|
outputs:
|
||||||
|
artifact_path: outputs/app
|
||||||
|
sbom_path: outputs/sbom.cdx.json
|
||||||
|
coverage_path: outputs/coverage.json
|
||||||
|
traces_dir: outputs/traces
|
||||||
|
attestation_path: outputs/attestation.json
|
||||||
|
test:
|
||||||
|
command: "go test -v ./..."
|
||||||
|
expected_coverage: []
|
||||||
|
expected_traces: []
|
||||||
|
ground_truth:
|
||||||
|
summary: "Command injection reachable"
|
||||||
|
evidence_files:
|
||||||
|
- "../benchmark/truth/go-gin-exec.json"
|
||||||
|
sandbox:
|
||||||
|
network: loopback
|
||||||
|
privileges: rootless
|
||||||
|
redaction:
|
||||||
|
pii: false
|
||||||
|
policy: "benchmark-default/v1"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
case_id: "go-gin-exec:301"
|
||||||
|
entries:
|
||||||
|
http:
|
||||||
|
- id: "GET /run"
|
||||||
|
route: "/run"
|
||||||
|
method: "GET"
|
||||||
|
handler: "main.handleRun"
|
||||||
|
description: "Executes shell command from query parameter"
|
||||||
5
bench/reachability-benchmark/cases/go/gin-exec/go.mod
Normal file
5
bench/reachability-benchmark/cases/go/gin-exec/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module gin-exec
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require github.com/gin-gonic/gin v1.10.0
|
||||||
41
bench/reachability-benchmark/cases/go/gin-exec/main.go
Normal file
41
bench/reachability-benchmark/cases/go/gin-exec/main.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// gin-exec benchmark case
|
||||||
|
// Demonstrates command injection sink reachable via Gin HTTP handler
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
r.GET("/run", handleRun)
|
||||||
|
r.GET("/health", handleHealth)
|
||||||
|
r.Run(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRun - VULNERABLE: command injection sink
|
||||||
|
// User-controlled input passed directly to exec.Command
|
||||||
|
func handleRun(c *gin.Context) {
|
||||||
|
cmd := c.Query("cmd")
|
||||||
|
if cmd == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing cmd parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SINK: os/exec.Command with user-controlled input
|
||||||
|
output, err := exec.Command("sh", "-c", cmd).Output()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"output": string(output)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleHealth - safe endpoint, no sinks
|
||||||
|
func handleHealth(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
|
}
|
||||||
37
bench/reachability-benchmark/cases/go/gin-exec/main_test.go
Normal file
37
bench/reachability-benchmark/cases/go/gin-exec/main_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleHealth(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.Default()
|
||||||
|
r.GET("/health", handleHealth)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "/health", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleRunMissingCmd(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.Default()
|
||||||
|
r.GET("/run", handleRun)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "/run", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("Expected status 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Keep this directory for build outputs
|
||||||
46
bench/reachability-benchmark/cases/go/grpc-sql/case.yaml
Normal file
46
bench/reachability-benchmark/cases/go/grpc-sql/case.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
id: "go-grpc-sql:302"
|
||||||
|
language: go
|
||||||
|
project: grpc-sql
|
||||||
|
version: "1.0.0"
|
||||||
|
description: "SQL injection sink reachable via gRPC GetUser method"
|
||||||
|
entrypoints:
|
||||||
|
- "grpc:UserService.GetUser"
|
||||||
|
sinks:
|
||||||
|
- id: "SqlInjection::GetUser"
|
||||||
|
path: "main.(*userServer).GetUser"
|
||||||
|
kind: "custom"
|
||||||
|
location:
|
||||||
|
file: main.go
|
||||||
|
line: 35
|
||||||
|
notes: "database/sql.Query with string concatenation"
|
||||||
|
environment:
|
||||||
|
os_image: "golang:1.22-alpine"
|
||||||
|
runtime:
|
||||||
|
go: "1.22"
|
||||||
|
source_date_epoch: 1730000000
|
||||||
|
resource_limits:
|
||||||
|
cpu: "2"
|
||||||
|
memory: "2Gi"
|
||||||
|
build:
|
||||||
|
command: "go build -o outputs/app ."
|
||||||
|
source_date_epoch: 1730000000
|
||||||
|
outputs:
|
||||||
|
artifact_path: outputs/app
|
||||||
|
sbom_path: outputs/sbom.cdx.json
|
||||||
|
coverage_path: outputs/coverage.json
|
||||||
|
traces_dir: outputs/traces
|
||||||
|
attestation_path: outputs/attestation.json
|
||||||
|
test:
|
||||||
|
command: "go test -v ./..."
|
||||||
|
expected_coverage: []
|
||||||
|
expected_traces: []
|
||||||
|
ground_truth:
|
||||||
|
summary: "SQL injection reachable"
|
||||||
|
evidence_files:
|
||||||
|
- "../benchmark/truth/go-grpc-sql.json"
|
||||||
|
sandbox:
|
||||||
|
network: loopback
|
||||||
|
privileges: rootless
|
||||||
|
redaction:
|
||||||
|
pii: false
|
||||||
|
policy: "benchmark-default/v1"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
case_id: "go-grpc-sql:302"
|
||||||
|
entries:
|
||||||
|
grpc:
|
||||||
|
- id: "grpc:UserService.GetUser"
|
||||||
|
service: "UserService"
|
||||||
|
method: "GetUser"
|
||||||
|
handler: "main.(*userServer).GetUser"
|
||||||
|
description: "Fetches user by ID with SQL injection vulnerability"
|
||||||
8
bench/reachability-benchmark/cases/go/grpc-sql/go.mod
Normal file
8
bench/reachability-benchmark/cases/go/grpc-sql/go.mod
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module grpc-sql
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
google.golang.org/grpc v1.64.0
|
||||||
|
google.golang.org/protobuf v1.34.2
|
||||||
|
)
|
||||||
86
bench/reachability-benchmark/cases/go/grpc-sql/main.go
Normal file
86
bench/reachability-benchmark/cases/go/grpc-sql/main.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// grpc-sql benchmark case
|
||||||
|
// Demonstrates SQL injection sink reachable via gRPC handler
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a user record
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
// userServer implements the gRPC UserService
|
||||||
|
type userServer struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser - VULNERABLE: SQL injection sink
|
||||||
|
// User ID is concatenated directly into SQL query
|
||||||
|
func (s *userServer) GetUser(ctx context.Context, userID string) (*User, error) {
|
||||||
|
// SINK: database/sql.Query with string concatenation
|
||||||
|
query := fmt.Sprintf("SELECT id, name, email FROM users WHERE id = '%s'", userID)
|
||||||
|
row := s.db.QueryRow(query)
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
|
||||||
|
return nil, fmt.Errorf("user not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserSafe - SAFE: uses parameterized query
|
||||||
|
func (s *userServer) GetUserSafe(ctx context.Context, userID string) (*User, error) {
|
||||||
|
query := "SELECT id, name, email FROM users WHERE id = ?"
|
||||||
|
row := s.db.QueryRow(query, userID)
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
|
||||||
|
return nil, fmt.Errorf("user not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := sql.Open("sqlite3", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Initialize schema
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE TABLE users (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
email TEXT
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", ":50051")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := grpc.NewServer()
|
||||||
|
// Register service here (simplified for benchmark)
|
||||||
|
log.Printf("gRPC server listening on %v", lis.Addr())
|
||||||
|
if err := s.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
56
bench/reachability-benchmark/cases/go/grpc-sql/main_test.go
Normal file
56
bench/reachability-benchmark/cases/go/grpc-sql/main_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestDB(t *testing.T) *sql.DB {
|
||||||
|
db, err := sql.Open("sqlite3", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE TABLE users (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
email TEXT
|
||||||
|
);
|
||||||
|
INSERT INTO users (id, name, email) VALUES ('1', 'Alice', 'alice@example.com');
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to setup test data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserSafe(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
server := &userServer{db: db}
|
||||||
|
user, err := server.GetUserSafe(context.Background(), "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Name != "Alice" {
|
||||||
|
t.Errorf("expected Alice, got %s", user.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserNotFound(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
server := &userServer{db: db}
|
||||||
|
_, err := server.GetUserSafe(context.Background(), "999")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for non-existent user")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Keep this directory for build outputs
|
||||||
35
bench/results/epss-ingest-perf.local.json
Normal file
35
bench/results/epss-ingest-perf.local.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"tool": {
|
||||||
|
"name": "StellaOps.Scanner.Storage.Epss.Perf",
|
||||||
|
"schema": 1
|
||||||
|
},
|
||||||
|
"dataset": {
|
||||||
|
"modelDate": "2025-12-19",
|
||||||
|
"rows": 310000,
|
||||||
|
"seed": 104372539560473,
|
||||||
|
"compressedSha256": "sha256:b6dd77a0689a98f563a872ab517342b9b033d46a2f591dbbfb8833c3dd52b39d",
|
||||||
|
"decompressedSha256": "sha256:dfab8068f4624f19c276a8794c1878f83643f9da4b5414c2658b0a6ddc9aebb4",
|
||||||
|
"modelVersionTag": "v2025.12.19",
|
||||||
|
"publishedDate": "2025-12-19",
|
||||||
|
"compressedBytes": 3169965,
|
||||||
|
"decompressedBytes": 10850000
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"os": "Microsoft Windows NT 10.0.26100.0",
|
||||||
|
"framework": ".NET 10.0.0",
|
||||||
|
"processArchitecture": "X64",
|
||||||
|
"postgresImage": "postgres:16-alpine"
|
||||||
|
},
|
||||||
|
"timingsMs": {
|
||||||
|
"datasetGenerate": 779,
|
||||||
|
"containerStart": 3977,
|
||||||
|
"migrations": 721,
|
||||||
|
"writeSnapshot": 39804,
|
||||||
|
"total": 45652
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"importRunId": "5f7def2e-a6a3-4286-93cb-7af60d11e02e",
|
||||||
|
"rowCount": 310000,
|
||||||
|
"distinctCveCount": 310000
|
||||||
|
}
|
||||||
|
}
|
||||||
117
bench/smart-diff/README.md
Normal file
117
bench/smart-diff/README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Smart-Diff Benchmark Suite
|
||||||
|
|
||||||
|
> **Purpose:** Prove deterministic smart-diff reduces noise compared to naive diff.
|
||||||
|
> **Status:** Active
|
||||||
|
> **Sprint:** SPRINT_3850_0001_0001 (Competitive Gap Closure)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Smart-Diff feature enables incremental scanning by:
|
||||||
|
1. Computing structural diffs of SBOMs/dependencies
|
||||||
|
2. Identifying only changed components
|
||||||
|
3. Avoiding redundant scanning of unchanged packages
|
||||||
|
4. Producing deterministic, reproducible diff results
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### TC-001: Layer-Aware Diff
|
||||||
|
|
||||||
|
Tests that Smart-Diff correctly handles container layer changes:
|
||||||
|
- Adding a layer
|
||||||
|
- Removing a layer
|
||||||
|
- Modifying a layer (same hash, different content)
|
||||||
|
|
||||||
|
### TC-002: Package Version Diff
|
||||||
|
|
||||||
|
Tests accurate detection of package version changes:
|
||||||
|
- Minor version bump
|
||||||
|
- Major version bump
|
||||||
|
- Pre-release version handling
|
||||||
|
- Epoch handling (RPM)
|
||||||
|
|
||||||
|
### TC-003: Noise Reduction
|
||||||
|
|
||||||
|
Compares smart-diff output vs naive diff for real-world images:
|
||||||
|
- Measure CVE count reduction
|
||||||
|
- Measure scanning time reduction
|
||||||
|
- Verify determinism (same inputs → same outputs)
|
||||||
|
|
||||||
|
### TC-004: Deterministic Ordering
|
||||||
|
|
||||||
|
Verifies that diff results are:
|
||||||
|
- Sorted by component PURL
|
||||||
|
- Ordered consistently across runs
|
||||||
|
- Independent of filesystem ordering
|
||||||
|
|
||||||
|
## Fixtures
|
||||||
|
|
||||||
|
```
|
||||||
|
fixtures/
|
||||||
|
├── base-alpine-3.18.sbom.cdx.json
|
||||||
|
├── base-alpine-3.19.sbom.cdx.json
|
||||||
|
├── layer-added.manifest.json
|
||||||
|
├── layer-removed.manifest.json
|
||||||
|
├── version-bump-minor.sbom.cdx.json
|
||||||
|
├── version-bump-major.sbom.cdx.json
|
||||||
|
└── expected/
|
||||||
|
├── tc001-layer-added.diff.json
|
||||||
|
├── tc001-layer-removed.diff.json
|
||||||
|
├── tc002-minor-bump.diff.json
|
||||||
|
├── tc002-major-bump.diff.json
|
||||||
|
└── tc003-noise-reduction.metrics.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Suite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all smart-diff tests
|
||||||
|
dotnet test tests/StellaOps.Scanner.SmartDiff.Tests
|
||||||
|
|
||||||
|
# Run benchmark comparison
|
||||||
|
./run-benchmark.sh --baseline naive --compare smart
|
||||||
|
|
||||||
|
# Generate metrics report
|
||||||
|
./tools/analyze.py results/ --output metrics.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics Collected
|
||||||
|
|
||||||
|
| Metric | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `diff_time_ms` | Time to compute diff |
|
||||||
|
| `changed_packages` | Number of packages marked as changed |
|
||||||
|
| `false_positive_rate` | Packages incorrectly flagged as changed |
|
||||||
|
| `determinism_score` | 1.0 if all runs produce identical output |
|
||||||
|
| `noise_reduction_pct` | % reduction vs naive diff |
|
||||||
|
|
||||||
|
## Expected Results
|
||||||
|
|
||||||
|
For typical Alpine base image upgrades (3.18 → 3.19):
|
||||||
|
- **Naive diff:** ~150 packages flagged as changed
|
||||||
|
- **Smart diff:** ~12 packages actually changed
|
||||||
|
- **Noise reduction:** ~92%
|
||||||
|
|
||||||
|
## Integration with CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .gitea/workflows/bench-smart-diff.yaml
|
||||||
|
name: Smart-Diff Benchmark
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff/**'
|
||||||
|
- 'bench/smart-diff/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
benchmark:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run Smart-Diff Benchmark
|
||||||
|
run: ./bench/smart-diff/run-benchmark.sh
|
||||||
|
- name: Upload Results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: smart-diff-results
|
||||||
|
path: bench/smart-diff/results/
|
||||||
|
```
|
||||||
135
bench/smart-diff/run-benchmark.sh
Normal file
135
bench/smart-diff/run-benchmark.sh
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# run-benchmark.sh
|
||||||
|
# Smart-Diff Benchmark Runner
|
||||||
|
# Sprint: SPRINT_3850_0001_0001
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BENCH_ROOT="$SCRIPT_DIR"
|
||||||
|
RESULTS_DIR="$BENCH_ROOT/results/$(date -u +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
BASELINE_MODE="naive"
|
||||||
|
COMPARE_MODE="smart"
|
||||||
|
VERBOSE=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--baseline)
|
||||||
|
BASELINE_MODE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--compare)
|
||||||
|
COMPARE_MODE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--verbose|-v)
|
||||||
|
VERBOSE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "╔════════════════════════════════════════════════╗"
|
||||||
|
echo "║ Smart-Diff Benchmark Suite ║"
|
||||||
|
echo "╚════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo "Configuration:"
|
||||||
|
echo " Baseline mode: $BASELINE_MODE"
|
||||||
|
echo " Compare mode: $COMPARE_MODE"
|
||||||
|
echo " Results dir: $RESULTS_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
mkdir -p "$RESULTS_DIR"
|
||||||
|
|
||||||
|
# Function to run a test case
|
||||||
|
run_test_case() {
|
||||||
|
local test_id="$1"
|
||||||
|
local description="$2"
|
||||||
|
local base_sbom="$3"
|
||||||
|
local target_sbom="$4"
|
||||||
|
local expected_file="$5"
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Test: $test_id - $description"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
local start_time=$(date +%s%3N)
|
||||||
|
|
||||||
|
# Run smart-diff
|
||||||
|
if command -v dotnet &> /dev/null; then
|
||||||
|
dotnet run --project "$SCRIPT_DIR/../../src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff" -- \
|
||||||
|
--base "$base_sbom" \
|
||||||
|
--target "$target_sbom" \
|
||||||
|
--output "$RESULTS_DIR/$test_id.diff.json" \
|
||||||
|
--format json 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
local end_time=$(date +%s%3N)
|
||||||
|
local elapsed=$((end_time - start_time))
|
||||||
|
|
||||||
|
echo " Time: ${elapsed}ms"
|
||||||
|
|
||||||
|
# Verify determinism by running twice
|
||||||
|
if [ -f "$RESULTS_DIR/$test_id.diff.json" ]; then
|
||||||
|
local hash1=$(sha256sum "$RESULTS_DIR/$test_id.diff.json" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
if command -v dotnet &> /dev/null; then
|
||||||
|
dotnet run --project "$SCRIPT_DIR/../../src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff" -- \
|
||||||
|
--base "$base_sbom" \
|
||||||
|
--target "$target_sbom" \
|
||||||
|
--output "$RESULTS_DIR/$test_id.diff.run2.json" \
|
||||||
|
--format json 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$RESULTS_DIR/$test_id.diff.run2.json" ]; then
|
||||||
|
local hash2=$(sha256sum "$RESULTS_DIR/$test_id.diff.run2.json" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
if [ "$hash1" = "$hash2" ]; then
|
||||||
|
echo " ✓ Determinism verified"
|
||||||
|
else
|
||||||
|
echo " ✗ Determinism FAILED (different hashes)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ⊘ Skipped (dotnet not available or project missing)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test Case 1: Layer-Aware Diff (using fixtures)
|
||||||
|
if [ -f "$BENCH_ROOT/fixtures/base-alpine-3.18.sbom.cdx.json" ]; then
|
||||||
|
run_test_case "TC-001-layer-added" \
|
||||||
|
"Layer addition detection" \
|
||||||
|
"$BENCH_ROOT/fixtures/base-alpine-3.18.sbom.cdx.json" \
|
||||||
|
"$BENCH_ROOT/fixtures/base-alpine-3.19.sbom.cdx.json" \
|
||||||
|
"$BENCH_ROOT/fixtures/expected/tc001-layer-added.diff.json"
|
||||||
|
else
|
||||||
|
echo "ℹ️ Skipping TC-001: Fixtures not found"
|
||||||
|
echo " Run './tools/generate-fixtures.sh' to create test fixtures"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Summary"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Results saved to: $RESULTS_DIR"
|
||||||
|
|
||||||
|
# Create summary JSON
|
||||||
|
cat > "$RESULTS_DIR/summary.json" <<EOF
|
||||||
|
{
|
||||||
|
"benchmark": "smart-diff",
|
||||||
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||||
|
"baseline_mode": "$BASELINE_MODE",
|
||||||
|
"compare_mode": "$COMPARE_MODE",
|
||||||
|
"results_dir": "$RESULTS_DIR"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
183
bench/unknowns/README.md
Normal file
183
bench/unknowns/README.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Unknowns Tracking Benchmark Suite
|
||||||
|
|
||||||
|
> **Purpose:** Verify epistemic uncertainty tracking and unknown state management.
|
||||||
|
> **Status:** Active
|
||||||
|
> **Sprint:** SPRINT_3850_0001_0001 (Competitive Gap Closure)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
StellaOps tracks "unknowns" - gaps in knowledge that affect confidence:
|
||||||
|
- Missing SBOM components
|
||||||
|
- Unmatched CVEs
|
||||||
|
- Stale feed data
|
||||||
|
- Zero-day windows
|
||||||
|
- Analysis limitations
|
||||||
|
|
||||||
|
## What Gets Tested
|
||||||
|
|
||||||
|
### Unknown State Lifecycle
|
||||||
|
1. Detection of unknown conditions
|
||||||
|
2. Propagation to affected findings
|
||||||
|
3. Score penalty application
|
||||||
|
4. Resolution tracking
|
||||||
|
|
||||||
|
### Unknown Categories
|
||||||
|
- `SBOM_GAP`: Component not in SBOM
|
||||||
|
- `CVE_UNMATCHED`: CVE without component mapping
|
||||||
|
- `FEED_STALE`: Feed data older than threshold
|
||||||
|
- `ZERO_DAY_WINDOW`: Time between disclosure and feed update
|
||||||
|
- `ANALYSIS_LIMIT`: Depth/timeout constraints
|
||||||
|
|
||||||
|
### Score Impact
|
||||||
|
- Each unknown type has a penalty weight
|
||||||
|
- Penalties reduce overall confidence
|
||||||
|
- Resolved unknowns restore confidence
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### TC-001: SBOM Gap Detection
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scenario": "Package in image not in SBOM",
|
||||||
|
"input": {
|
||||||
|
"image_packages": ["openssl@3.0.1", "curl@7.86"],
|
||||||
|
"sbom_packages": ["openssl@3.0.1"]
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"unknowns": [{ "type": "SBOM_GAP", "package": "curl@7.86" }],
|
||||||
|
"confidence_penalty": 0.15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-002: Zero-Day Window Tracking
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scenario": "CVE disclosed before feed update",
|
||||||
|
"input": {
|
||||||
|
"cve_disclosure": "2025-01-01T00:00:00Z",
|
||||||
|
"feed_update": "2025-01-03T00:00:00Z",
|
||||||
|
"scan_time": "2025-01-02T12:00:00Z"
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"unknowns": [{
|
||||||
|
"type": "ZERO_DAY_WINDOW",
|
||||||
|
"cve": "CVE-2025-0001",
|
||||||
|
"window_hours": 36
|
||||||
|
}],
|
||||||
|
"risk_note": "Scan occurred during zero-day window"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-003: Feed Staleness
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scenario": "NVD feed older than 24 hours",
|
||||||
|
"input": {
|
||||||
|
"feed_last_update": "2025-01-01T00:00:00Z",
|
||||||
|
"scan_time": "2025-01-02T12:00:00Z",
|
||||||
|
"staleness_threshold_hours": 24
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"unknowns": [{
|
||||||
|
"type": "FEED_STALE",
|
||||||
|
"feed": "nvd",
|
||||||
|
"age_hours": 36
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-004: Score Penalty Application
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scenario": "Multiple unknowns compound penalty",
|
||||||
|
"input": {
|
||||||
|
"base_confidence": 0.95,
|
||||||
|
"unknowns": [
|
||||||
|
{ "type": "SBOM_GAP", "penalty": 0.15 },
|
||||||
|
{ "type": "FEED_STALE", "penalty": 0.10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"final_confidence": 0.70,
|
||||||
|
"penalty_formula": "0.95 * (1 - 0.15) * (1 - 0.10)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fixtures
|
||||||
|
|
||||||
|
```
|
||||||
|
fixtures/
|
||||||
|
├── sbom-gaps/
|
||||||
|
│ ├── single-missing.json
|
||||||
|
│ ├── multiple-missing.json
|
||||||
|
│ └── layer-specific.json
|
||||||
|
├── zero-day/
|
||||||
|
│ ├── within-window.json
|
||||||
|
│ ├── after-window.json
|
||||||
|
│ └── ongoing.json
|
||||||
|
├── feed-staleness/
|
||||||
|
│ ├── nvd-stale.json
|
||||||
|
│ ├── osv-stale.json
|
||||||
|
│ └── multiple-stale.json
|
||||||
|
└── expected/
|
||||||
|
└── all-tests.results.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Suite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run unknowns tests
|
||||||
|
dotnet test tests/StellaOps.Unknowns.Tests
|
||||||
|
|
||||||
|
# Run penalty calculation tests
|
||||||
|
./run-penalty-tests.sh
|
||||||
|
|
||||||
|
# Run full benchmark
|
||||||
|
./run-benchmark.sh --all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Detection rate | 100% | All unknown conditions detected |
|
||||||
|
| Penalty accuracy | ±1% | Penalties match expected values |
|
||||||
|
| Resolution tracking | 100% | All resolutions properly logged |
|
||||||
|
|
||||||
|
## UI Integration
|
||||||
|
|
||||||
|
Unknowns appear as:
|
||||||
|
- Chips in findings table
|
||||||
|
- Warning banners on scan results
|
||||||
|
- Confidence reduction indicators
|
||||||
|
- Triage action suggestions
|
||||||
|
|
||||||
|
## Integration with CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .gitea/workflows/bench-unknowns.yaml
|
||||||
|
name: Unknowns Benchmark
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'src/Unknowns/**'
|
||||||
|
- 'bench/unknowns/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unknowns:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run Unknowns Tests
|
||||||
|
run: dotnet test tests/StellaOps.Unknowns.Tests
|
||||||
|
- name: Run Benchmark
|
||||||
|
run: ./bench/unknowns/run-benchmark.sh
|
||||||
|
```
|
||||||
153
bench/vex-lattice/README.md
Normal file
153
bench/vex-lattice/README.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# VEX Lattice Benchmark Suite
|
||||||
|
|
||||||
|
> **Purpose:** Verify VEX lattice merge semantics and jurisdiction rules.
|
||||||
|
> **Status:** Active
|
||||||
|
> **Sprint:** SPRINT_3850_0001_0001 (Competitive Gap Closure)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
StellaOps implements VEX (Vulnerability Exploitability eXchange) with:
|
||||||
|
- Lattice-based merge semantics (stable outcomes)
|
||||||
|
- Jurisdiction-specific trust rules (US/EU/RU/CN)
|
||||||
|
- Source precedence and confidence weighting
|
||||||
|
- Deterministic conflict resolution
|
||||||
|
|
||||||
|
## What Gets Tested
|
||||||
|
|
||||||
|
### Lattice Properties
|
||||||
|
- Idempotency: merge(a, a) = a
|
||||||
|
- Commutativity: merge(a, b) = merge(b, a)
|
||||||
|
- Associativity: merge(merge(a, b), c) = merge(a, merge(b, c))
|
||||||
|
- Monotonicity: once "not_affected", never regresses
|
||||||
|
|
||||||
|
### Status Precedence
|
||||||
|
Order from most to least specific:
|
||||||
|
1. `not_affected` (strongest)
|
||||||
|
2. `affected` (with fix)
|
||||||
|
3. `under_investigation`
|
||||||
|
4. `affected` (no fix)
|
||||||
|
|
||||||
|
### Jurisdiction Rules
|
||||||
|
- US: FDA/NIST sources preferred
|
||||||
|
- EU: ENISA/BSI sources preferred
|
||||||
|
- RU: FSTEC sources preferred
|
||||||
|
- CN: CNVD sources preferred
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### TC-001: Idempotency
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"input_a": { "status": "not_affected", "justification": "vulnerable_code_not_in_execute_path" },
|
||||||
|
"input_b": { "status": "not_affected", "justification": "vulnerable_code_not_in_execute_path" },
|
||||||
|
"expected": { "status": "not_affected", "justification": "vulnerable_code_not_in_execute_path" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-002: Commutativity
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"merge_ab": "merge(vendor_vex, nvd_vex)",
|
||||||
|
"merge_ba": "merge(nvd_vex, vendor_vex)",
|
||||||
|
"expected": "identical_result"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-003: Associativity
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lhs": "merge(merge(a, b), c)",
|
||||||
|
"rhs": "merge(a, merge(b, c))",
|
||||||
|
"expected": "identical_result"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-004: Conflict Resolution
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vendor_says": "not_affected",
|
||||||
|
"nvd_says": "affected",
|
||||||
|
"expected": "not_affected",
|
||||||
|
"reason": "vendor_has_higher_precedence"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TC-005: Jurisdiction Override
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jurisdiction": "EU",
|
||||||
|
"bsi_says": "not_affected",
|
||||||
|
"nist_says": "affected",
|
||||||
|
"expected": "not_affected",
|
||||||
|
"reason": "bsi_preferred_in_eu"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fixtures
|
||||||
|
|
||||||
|
```
|
||||||
|
fixtures/
|
||||||
|
├── lattice-properties/
|
||||||
|
│ ├── idempotency.json
|
||||||
|
│ ├── commutativity.json
|
||||||
|
│ └── associativity.json
|
||||||
|
├── conflict-resolution/
|
||||||
|
│ ├── vendor-vs-nvd.json
|
||||||
|
│ ├── multiple-vendors.json
|
||||||
|
│ └── timestamp-tiebreaker.json
|
||||||
|
├── jurisdiction-rules/
|
||||||
|
│ ├── us-fda-nist.json
|
||||||
|
│ ├── eu-enisa-bsi.json
|
||||||
|
│ ├── ru-fstec.json
|
||||||
|
│ └── cn-cnvd.json
|
||||||
|
└── expected/
|
||||||
|
└── all-tests.results.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Suite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run VEX lattice tests
|
||||||
|
dotnet test tests/StellaOps.Policy.Vex.Tests
|
||||||
|
|
||||||
|
# Run lattice property verification
|
||||||
|
./run-lattice-tests.sh
|
||||||
|
|
||||||
|
# Run jurisdiction rule tests
|
||||||
|
./run-jurisdiction-tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Lattice properties | 100% pass | All algebraic properties hold |
|
||||||
|
| Jurisdiction correctness | 100% pass | Correct source preferred by region |
|
||||||
|
| Merge determinism | 100% pass | Same inputs → same output |
|
||||||
|
|
||||||
|
## Integration with CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .gitea/workflows/bench-vex-lattice.yaml
|
||||||
|
name: VEX Lattice Benchmark
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'src/Policy/**'
|
||||||
|
- 'bench/vex-lattice/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lattice:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run Lattice Tests
|
||||||
|
run: dotnet test tests/StellaOps.Policy.Vex.Tests
|
||||||
|
- name: Run Property Tests
|
||||||
|
run: ./bench/vex-lattice/run-lattice-tests.sh
|
||||||
|
```
|
||||||
143
datasets/reachability/corpus.json
Normal file
143
datasets/reachability/corpus.json
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/corpus-index.v1.json",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Ground-truth corpus for binary reachability benchmarking",
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0001",
|
||||||
|
"category": "basic",
|
||||||
|
"path": "ground-truth/basic/gt-0001/sample.manifest.json",
|
||||||
|
"description": "Direct call to vulnerable sink from main"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0002",
|
||||||
|
"category": "basic",
|
||||||
|
"path": "ground-truth/basic/gt-0002/sample.manifest.json",
|
||||||
|
"description": "Two-hop call chain to vulnerable sink"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0003",
|
||||||
|
"category": "basic",
|
||||||
|
"path": "ground-truth/basic/gt-0003/sample.manifest.json",
|
||||||
|
"description": "Three-hop call chain with multiple sinks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0004",
|
||||||
|
"category": "basic",
|
||||||
|
"path": "ground-truth/basic/gt-0004/sample.manifest.json",
|
||||||
|
"description": "Function pointer call to sink"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0005",
|
||||||
|
"category": "basic",
|
||||||
|
"path": "ground-truth/basic/gt-0005/sample.manifest.json",
|
||||||
|
"description": "Recursive function with sink"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0006",
|
||||||
|
"category": "indirect",
|
||||||
|
"path": "ground-truth/indirect/gt-0006/sample.manifest.json",
|
||||||
|
"description": "Indirect call via callback"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0007",
|
||||||
|
"category": "indirect",
|
||||||
|
"path": "ground-truth/indirect/gt-0007/sample.manifest.json",
|
||||||
|
"description": "Virtual function dispatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0008",
|
||||||
|
"category": "guarded",
|
||||||
|
"path": "ground-truth/guarded/gt-0008/sample.manifest.json",
|
||||||
|
"description": "Sink behind constant false guard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0009",
|
||||||
|
"category": "guarded",
|
||||||
|
"path": "ground-truth/guarded/gt-0009/sample.manifest.json",
|
||||||
|
"description": "Sink behind input-dependent guard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0010",
|
||||||
|
"category": "guarded",
|
||||||
|
"path": "ground-truth/guarded/gt-0010/sample.manifest.json",
|
||||||
|
"description": "Sink behind environment variable guard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0011",
|
||||||
|
"category": "basic",
|
||||||
|
"path": "ground-truth/basic/gt-0011/sample.manifest.json",
|
||||||
|
"description": "Unreachable sink - dead code after return"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0012",
|
||||||
|
"category": "basic",
|
||||||
|
"path": "ground-truth/basic/gt-0012/sample.manifest.json",
|
||||||
|
"description": "Unreachable sink - never called function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0013",
|
||||||
|
"category": "basic",
|
||||||
|
"path": "ground-truth/basic/gt-0013/sample.manifest.json",
|
||||||
|
"description": "Unreachable sink - #ifdef disabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0014",
|
||||||
|
"category": "guarded",
|
||||||
|
"path": "ground-truth/guarded/gt-0014/sample.manifest.json",
|
||||||
|
"description": "Unreachable sink - constant true early return"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0015",
|
||||||
|
"category": "guarded",
|
||||||
|
"path": "ground-truth/guarded/gt-0015/sample.manifest.json",
|
||||||
|
"description": "Unreachable sink - impossible branch condition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0016",
|
||||||
|
"category": "stripped",
|
||||||
|
"path": "ground-truth/stripped/gt-0016/sample.manifest.json",
|
||||||
|
"description": "Stripped binary - reachable sink"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0017",
|
||||||
|
"category": "stripped",
|
||||||
|
"path": "ground-truth/stripped/gt-0017/sample.manifest.json",
|
||||||
|
"description": "Stripped binary - unreachable sink"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0018",
|
||||||
|
"category": "obfuscated",
|
||||||
|
"path": "ground-truth/obfuscated/gt-0018/sample.manifest.json",
|
||||||
|
"description": "Control flow obfuscation - reachable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0019",
|
||||||
|
"category": "obfuscated",
|
||||||
|
"path": "ground-truth/obfuscated/gt-0019/sample.manifest.json",
|
||||||
|
"description": "String obfuscation - reachable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "gt-0020",
|
||||||
|
"category": "callback",
|
||||||
|
"path": "ground-truth/callback/gt-0020/sample.manifest.json",
|
||||||
|
"description": "Async callback chain - reachable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"statistics": {
|
||||||
|
"totalSamples": 20,
|
||||||
|
"byCategory": {
|
||||||
|
"basic": 8,
|
||||||
|
"indirect": 2,
|
||||||
|
"guarded": 4,
|
||||||
|
"stripped": 2,
|
||||||
|
"obfuscated": 2,
|
||||||
|
"callback": 2
|
||||||
|
},
|
||||||
|
"byExpected": {
|
||||||
|
"reachable": 13,
|
||||||
|
"unreachable": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
datasets/reachability/ground-truth/basic/gt-0001/main.c
Normal file
18
datasets/reachability/ground-truth/basic/gt-0001/main.c
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// gt-0001: Direct call to vulnerable sink from main
|
||||||
|
// Expected: REACHABLE (tier: executed)
|
||||||
|
// Vulnerability: CWE-120 (Buffer Copy without Checking Size)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
char buffer[32];
|
||||||
|
|
||||||
|
if (argc > 1) {
|
||||||
|
// Vulnerable: strcpy without bounds checking
|
||||||
|
strcpy(buffer, argv[1]); // SINK: CWE-120
|
||||||
|
printf("Input: %s\n", buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/sample-manifest.v1.json",
|
||||||
|
"sampleId": "gt-0001",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "basic",
|
||||||
|
"description": "Direct call to vulnerable sink from main - REACHABLE",
|
||||||
|
"language": "c",
|
||||||
|
"expectedResult": {
|
||||||
|
"reachable": true,
|
||||||
|
"tier": "executed",
|
||||||
|
"confidence": 1.0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"files": ["main.c"],
|
||||||
|
"entrypoint": "main",
|
||||||
|
"sink": "strcpy",
|
||||||
|
"vulnerability": "CWE-120"
|
||||||
|
},
|
||||||
|
"callChain": [
|
||||||
|
{"function": "main", "file": "main.c", "line": 5},
|
||||||
|
{"function": "strcpy", "file": "<libc>", "line": null}
|
||||||
|
],
|
||||||
|
"annotations": {
|
||||||
|
"notes": "Simplest reachable case - direct call from entrypoint to vulnerable function",
|
||||||
|
"difficulty": "trivial"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"createdBy": "corpus-team"
|
||||||
|
}
|
||||||
22
datasets/reachability/ground-truth/basic/gt-0002/main.c
Normal file
22
datasets/reachability/ground-truth/basic/gt-0002/main.c
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// gt-0002: Two-hop call chain to vulnerable sink
|
||||||
|
// Expected: REACHABLE (tier: executed)
|
||||||
|
// Vulnerability: CWE-134 (Format String)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void format_message(const char *user_input, char *output) {
|
||||||
|
// Vulnerable: format string from user input
|
||||||
|
sprintf(output, user_input); // SINK: CWE-134
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
char buffer[256];
|
||||||
|
|
||||||
|
if (argc > 1) {
|
||||||
|
format_message(argv[1], buffer);
|
||||||
|
printf("Result: %s\n", buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/sample-manifest.v1.json",
|
||||||
|
"sampleId": "gt-0002",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "basic",
|
||||||
|
"description": "Two-hop call chain to vulnerable sink - REACHABLE",
|
||||||
|
"language": "c",
|
||||||
|
"expectedResult": {
|
||||||
|
"reachable": true,
|
||||||
|
"tier": "executed",
|
||||||
|
"confidence": 1.0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"files": ["main.c"],
|
||||||
|
"entrypoint": "main",
|
||||||
|
"sink": "sprintf",
|
||||||
|
"vulnerability": "CWE-134"
|
||||||
|
},
|
||||||
|
"callChain": [
|
||||||
|
{"function": "main", "file": "main.c", "line": 15},
|
||||||
|
{"function": "format_message", "file": "main.c", "line": 7},
|
||||||
|
{"function": "sprintf", "file": "<libc>", "line": null}
|
||||||
|
],
|
||||||
|
"annotations": {
|
||||||
|
"notes": "Two-hop chain: main -> helper -> sink",
|
||||||
|
"difficulty": "easy"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"createdBy": "corpus-team"
|
||||||
|
}
|
||||||
25
datasets/reachability/ground-truth/basic/gt-0003/main.c
Normal file
25
datasets/reachability/ground-truth/basic/gt-0003/main.c
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// gt-0003: Three-hop call chain with command injection
|
||||||
|
// Expected: REACHABLE (tier: executed)
|
||||||
|
// Vulnerability: CWE-78 (OS Command Injection)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void execute_command(const char *cmd) {
|
||||||
|
// Vulnerable: system call with user input
|
||||||
|
system(cmd); // SINK: CWE-78
|
||||||
|
}
|
||||||
|
|
||||||
|
void process_input(const char *input) {
|
||||||
|
char command[256];
|
||||||
|
snprintf(command, sizeof(command), "echo %s", input);
|
||||||
|
execute_command(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
if (argc > 1) {
|
||||||
|
process_input(argv[1]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/sample-manifest.v1.json",
|
||||||
|
"sampleId": "gt-0003",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "basic",
|
||||||
|
"description": "Three-hop call chain with multiple sinks - REACHABLE",
|
||||||
|
"language": "c",
|
||||||
|
"expectedResult": {
|
||||||
|
"reachable": true,
|
||||||
|
"tier": "executed",
|
||||||
|
"confidence": 1.0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"files": ["main.c"],
|
||||||
|
"entrypoint": "main",
|
||||||
|
"sink": "system",
|
||||||
|
"vulnerability": "CWE-78"
|
||||||
|
},
|
||||||
|
"callChain": [
|
||||||
|
{"function": "main", "file": "main.c", "line": 20},
|
||||||
|
{"function": "process_input", "file": "main.c", "line": 12},
|
||||||
|
{"function": "execute_command", "file": "main.c", "line": 6},
|
||||||
|
{"function": "system", "file": "<libc>", "line": null}
|
||||||
|
],
|
||||||
|
"annotations": {
|
||||||
|
"notes": "Three-hop chain demonstrating command injection path",
|
||||||
|
"difficulty": "easy"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"createdBy": "corpus-team"
|
||||||
|
}
|
||||||
37
datasets/reachability/ground-truth/basic/gt-0004/main.c
Normal file
37
datasets/reachability/ground-truth/basic/gt-0004/main.c
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// gt-0004: Function pointer call to sink
|
||||||
|
// Expected: REACHABLE (tier: executed)
|
||||||
|
// Vulnerability: CWE-120 (Buffer Copy without Checking Size)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef void (*copy_func_t)(char *, const char *);
|
||||||
|
|
||||||
|
void copy_data(char *dest, const char *src) {
|
||||||
|
// Vulnerable: strcpy without bounds check
|
||||||
|
strcpy(dest, src); // SINK: CWE-120
|
||||||
|
}
|
||||||
|
|
||||||
|
void safe_copy(char *dest, const char *src) {
|
||||||
|
strncpy(dest, src, 31);
|
||||||
|
dest[31] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
char buffer[32];
|
||||||
|
copy_func_t copier;
|
||||||
|
|
||||||
|
// Function pointer assignment - harder for static analysis
|
||||||
|
if (argc > 2 && argv[2][0] == 's') {
|
||||||
|
copier = safe_copy;
|
||||||
|
} else {
|
||||||
|
copier = copy_data; // Vulnerable path selected
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argc > 1) {
|
||||||
|
copier(buffer, argv[1]); // Indirect call
|
||||||
|
printf("Result: %s\n", buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/sample-manifest.v1.json",
|
||||||
|
"sampleId": "gt-0004",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "basic",
|
||||||
|
"description": "Function pointer call to sink - REACHABLE",
|
||||||
|
"language": "c",
|
||||||
|
"expectedResult": {
|
||||||
|
"reachable": true,
|
||||||
|
"tier": "executed",
|
||||||
|
"confidence": 0.9
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"files": ["main.c"],
|
||||||
|
"entrypoint": "main",
|
||||||
|
"sink": "strcpy",
|
||||||
|
"vulnerability": "CWE-120"
|
||||||
|
},
|
||||||
|
"callChain": [
|
||||||
|
{"function": "main", "file": "main.c", "line": 18},
|
||||||
|
{"function": "<function_ptr>", "file": "main.c", "line": 19},
|
||||||
|
{"function": "copy_data", "file": "main.c", "line": 8},
|
||||||
|
{"function": "strcpy", "file": "<libc>", "line": null}
|
||||||
|
],
|
||||||
|
"annotations": {
|
||||||
|
"notes": "Indirect call via function pointer - harder for static analysis",
|
||||||
|
"difficulty": "medium"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"createdBy": "corpus-team"
|
||||||
|
}
|
||||||
31
datasets/reachability/ground-truth/basic/gt-0005/main.c
Normal file
31
datasets/reachability/ground-truth/basic/gt-0005/main.c
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// gt-0005: Recursive function with sink
|
||||||
|
// Expected: REACHABLE (tier: executed)
|
||||||
|
// Vulnerability: CWE-134 (Format String)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
char result[1024];
|
||||||
|
|
||||||
|
void process_recursive(const char *input, int depth) {
|
||||||
|
if (depth <= 0 || strlen(input) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vulnerable: format string in recursive context
|
||||||
|
sprintf(result + strlen(result), input); // SINK: CWE-134
|
||||||
|
|
||||||
|
// Recurse with modified input
|
||||||
|
process_recursive(input + 1, depth - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
result[0] = '\0';
|
||||||
|
|
||||||
|
if (argc > 1) {
|
||||||
|
process_recursive(argv[1], 5);
|
||||||
|
printf("Result: %s\n", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/sample-manifest.v1.json",
|
||||||
|
"sampleId": "gt-0005",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "basic",
|
||||||
|
"description": "Recursive function with sink - REACHABLE",
|
||||||
|
"language": "c",
|
||||||
|
"expectedResult": {
|
||||||
|
"reachable": true,
|
||||||
|
"tier": "executed",
|
||||||
|
"confidence": 1.0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"files": ["main.c"],
|
||||||
|
"entrypoint": "main",
|
||||||
|
"sink": "sprintf",
|
||||||
|
"vulnerability": "CWE-134"
|
||||||
|
},
|
||||||
|
"callChain": [
|
||||||
|
{"function": "main", "file": "main.c", "line": 22},
|
||||||
|
{"function": "process_recursive", "file": "main.c", "line": 14},
|
||||||
|
{"function": "process_recursive", "file": "main.c", "line": 14},
|
||||||
|
{"function": "sprintf", "file": "<libc>", "line": null}
|
||||||
|
],
|
||||||
|
"annotations": {
|
||||||
|
"notes": "Recursive call pattern - tests loop/recursion handling",
|
||||||
|
"difficulty": "medium"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"createdBy": "corpus-team"
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// gt-0011: Dead code - function never called
|
||||||
|
// Expected: UNREACHABLE (tier: imported)
|
||||||
|
// Vulnerability: CWE-120 (Buffer Copy without Checking Size)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// This function is NEVER called - dead code
|
||||||
|
void vulnerable_function(const char *input) {
|
||||||
|
char buffer[32];
|
||||||
|
strcpy(buffer, input); // SINK: CWE-120 (but unreachable)
|
||||||
|
printf("Value: %s\n", buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void safe_function(const char *input) {
|
||||||
|
printf("Safe: %.31s\n", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
if (argc > 1) {
|
||||||
|
// Only safe_function is called
|
||||||
|
safe_function(argv[1]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/sample-manifest.v1.json",
|
||||||
|
"sampleId": "gt-0011",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "unreachable",
|
||||||
|
"description": "Dead code - function never called - UNREACHABLE",
|
||||||
|
"language": "c",
|
||||||
|
"expectedResult": {
|
||||||
|
"reachable": false,
|
||||||
|
"tier": "imported",
|
||||||
|
"confidence": 1.0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"files": ["main.c"],
|
||||||
|
"entrypoint": "main",
|
||||||
|
"sink": "strcpy",
|
||||||
|
"vulnerability": "CWE-120"
|
||||||
|
},
|
||||||
|
"callChain": null,
|
||||||
|
"annotations": {
|
||||||
|
"notes": "Vulnerable function exists but is never called from any reachable path",
|
||||||
|
"difficulty": "trivial",
|
||||||
|
"reason": "dead_code"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"createdBy": "corpus-team"
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// gt-0012: Compile-time constant false condition
|
||||||
|
// Expected: UNREACHABLE (tier: imported)
|
||||||
|
// Vulnerability: CWE-120 (Buffer Overflow)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define DEBUG_MODE 0 // Compile-time constant
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
char buffer[64];
|
||||||
|
|
||||||
|
// This branch is constant false - will be optimized out
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
// Vulnerable code in dead branch
|
||||||
|
gets(buffer); // SINK: CWE-120 (but unreachable)
|
||||||
|
printf("Debug: %s\n", buffer);
|
||||||
|
} else {
|
||||||
|
// Safe path always taken
|
||||||
|
if (argc > 1) {
|
||||||
|
strncpy(buffer, argv[1], sizeof(buffer) - 1);
|
||||||
|
buffer[sizeof(buffer) - 1] = '\0';
|
||||||
|
printf("Input: %s\n", buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/sample-manifest.v1.json",
|
||||||
|
"sampleId": "gt-0012",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "unreachable",
|
||||||
|
"description": "Compile-time constant false condition - UNREACHABLE",
|
||||||
|
"language": "c",
|
||||||
|
"expectedResult": {
|
||||||
|
"reachable": false,
|
||||||
|
"tier": "imported",
|
||||||
|
"confidence": 1.0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"files": ["main.c"],
|
||||||
|
"entrypoint": "main",
|
||||||
|
"sink": "gets",
|
||||||
|
"vulnerability": "CWE-120"
|
||||||
|
},
|
||||||
|
"callChain": null,
|
||||||
|
"annotations": {
|
||||||
|
"notes": "Sink is behind a constant false condition that will be optimized out",
|
||||||
|
"difficulty": "easy",
|
||||||
|
"reason": "constant_false"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"createdBy": "corpus-team"
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// gt-0013: Ifdef-excluded code path
|
||||||
|
// Expected: UNREACHABLE (tier: imported)
|
||||||
|
// Vulnerability: CWE-78 (OS Command Injection)
|
||||||
|
// Compile with: gcc -DPRODUCTION main.c (LEGACY_SHELL not defined)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define PRODUCTION
|
||||||
|
|
||||||
|
void process_command(const char *cmd) {
|
||||||
|
#ifdef LEGACY_SHELL
|
||||||
|
// This code is excluded when LEGACY_SHELL is not defined
|
||||||
|
system(cmd); // SINK: CWE-78 (but unreachable - ifdef excluded)
|
||||||
|
#else
|
||||||
|
// Safe path: just print, don't execute
|
||||||
|
printf("Would execute: %s\n", cmd);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
if (argc > 1) {
|
||||||
|
process_command(argv[1]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://stellaops.io/schemas/sample-manifest.v1.json",
|
||||||
|
"sampleId": "gt-0013",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "unreachable",
|
||||||
|
"description": "Ifdef-excluded code path - UNREACHABLE",
|
||||||
|
"language": "c",
|
||||||
|
"expectedResult": {
|
||||||
|
"reachable": false,
|
||||||
|
"tier": "imported",
|
||||||
|
"confidence": 1.0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"files": ["main.c"],
|
||||||
|
"entrypoint": "main",
|
||||||
|
"sink": "system",
|
||||||
|
"vulnerability": "CWE-78"
|
||||||
|
},
|
||||||
|
"callChain": null,
|
||||||
|
"annotations": {
|
||||||
|
"notes": "Vulnerable code excluded by preprocessor directive",
|
||||||
|
"difficulty": "easy",
|
||||||
|
"reason": "preprocessor_excluded"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T00:00:00Z",
|
||||||
|
"createdBy": "corpus-team"
|
||||||
|
}
|
||||||
121
datasets/reachability/schemas/corpus-sample.v1.json
Normal file
121
datasets/reachability/schemas/corpus-sample.v1.json
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://stellaops.io/schemas/corpus-sample.v1.json",
|
||||||
|
"title": "CorpusSample",
|
||||||
|
"description": "Schema for ground-truth corpus samples used in reachability benchmarking",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["sampleId", "name", "format", "arch", "sinks"],
|
||||||
|
"properties": {
|
||||||
|
"sampleId": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^gt-[0-9]{4}$",
|
||||||
|
"description": "Unique identifier for the sample (e.g., gt-0001)"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable name for the sample"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Detailed description of what this sample tests"
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["basic", "indirect", "stripped", "obfuscated", "guarded", "callback", "virtual"],
|
||||||
|
"description": "Sample category for organization"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["elf64", "elf32", "pe64", "pe32", "macho64", "macho32"],
|
||||||
|
"description": "Binary format"
|
||||||
|
},
|
||||||
|
"arch": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["x86_64", "x86", "aarch64", "arm32", "riscv64"],
|
||||||
|
"description": "Target architecture"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["c", "cpp", "rust", "go"],
|
||||||
|
"description": "Source language (for reference)"
|
||||||
|
},
|
||||||
|
"compiler": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"version": { "type": "string" },
|
||||||
|
"flags": { "type": "array", "items": { "type": "string" } }
|
||||||
|
},
|
||||||
|
"description": "Compiler information used to build the sample"
|
||||||
|
},
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "main",
|
||||||
|
"description": "Entry point function name"
|
||||||
|
},
|
||||||
|
"sinks": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["sinkId", "signature", "expected"],
|
||||||
|
"properties": {
|
||||||
|
"sinkId": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^sink-[0-9]{3}$",
|
||||||
|
"description": "Unique sink identifier within the sample"
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Function signature of the sink"
|
||||||
|
},
|
||||||
|
"sinkType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["memory_corruption", "command_injection", "sql_injection", "path_traversal", "format_string", "crypto_weakness", "custom"],
|
||||||
|
"description": "Type of vulnerability represented by the sink"
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["reachable", "unreachable", "conditional"],
|
||||||
|
"description": "Expected reachability determination"
|
||||||
|
},
|
||||||
|
"expectedPaths": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"description": "Expected call paths from entry to sink (for reachable sinks)"
|
||||||
|
},
|
||||||
|
"guardConditions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"variable": { "type": "string" },
|
||||||
|
"condition": { "type": "string" },
|
||||||
|
"value": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Guard conditions that protect the sink (for conditional sinks)"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Additional notes about this sink"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "List of sinks with expected reachability"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"createdAt": { "type": "string", "format": "date-time" },
|
||||||
|
"createdBy": { "type": "string" },
|
||||||
|
"version": { "type": "string" },
|
||||||
|
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
||||||
|
},
|
||||||
|
"description": "Metadata about the sample"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ in the `.env` samples match the options bound by `AddSchedulerWorker`:
|
|||||||
|
|
||||||
- `SCHEDULER_QUEUE_KIND` – queue transport (`Nats` or `Redis`).
|
- `SCHEDULER_QUEUE_KIND` – queue transport (`Nats` or `Redis`).
|
||||||
- `SCHEDULER_QUEUE_NATS_URL` – NATS connection string used by planner/runner consumers.
|
- `SCHEDULER_QUEUE_NATS_URL` – NATS connection string used by planner/runner consumers.
|
||||||
- `SCHEDULER_STORAGE_DATABASE` – MongoDB database name for scheduler state.
|
- `SCHEDULER_STORAGE_DATABASE` – PostgreSQL database name for scheduler state.
|
||||||
- `SCHEDULER_SCANNER_BASEADDRESS` – base URL the runner uses when invoking Scanner’s
|
- `SCHEDULER_SCANNER_BASEADDRESS` – base URL the runner uses when invoking Scanner’s
|
||||||
`/api/v1/reports` (defaults to the in-cluster `http://scanner-web:8444`).
|
`/api/v1/reports` (defaults to the in-cluster `http://scanner-web:8444`).
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,11 @@ services:
|
|||||||
SCANNER__EVENTS__STREAM: "${SCANNER_EVENTS_STREAM:-stella.events}"
|
SCANNER__EVENTS__STREAM: "${SCANNER_EVENTS_STREAM:-stella.events}"
|
||||||
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "${SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS:-5}"
|
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "${SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS:-5}"
|
||||||
SCANNER__EVENTS__MAXSTREAMLENGTH: "${SCANNER_EVENTS_MAX_STREAM_LENGTH:-10000}"
|
SCANNER__EVENTS__MAXSTREAMLENGTH: "${SCANNER_EVENTS_MAX_STREAM_LENGTH:-10000}"
|
||||||
|
SCANNER__OFFLINEKIT__ENABLED: "${SCANNER_OFFLINEKIT_ENABLED:-false}"
|
||||||
|
SCANNER__OFFLINEKIT__REQUIREDSSE: "${SCANNER_OFFLINEKIT_REQUIREDSSE:-true}"
|
||||||
|
SCANNER__OFFLINEKIT__REKOROFFLINEMODE: "${SCANNER_OFFLINEKIT_REKOROFFLINEMODE:-true}"
|
||||||
|
SCANNER__OFFLINEKIT__TRUSTROOTDIRECTORY: "${SCANNER_OFFLINEKIT_TRUSTROOTDIRECTORY:-/etc/stellaops/trust-roots}"
|
||||||
|
SCANNER__OFFLINEKIT__REKORSNAPSHOTDIRECTORY: "${SCANNER_OFFLINEKIT_REKORSNAPSHOTDIRECTORY:-/var/lib/stellaops/rekor-snapshot}"
|
||||||
# Surface.Env configuration (see docs/modules/scanner/design/surface-env.md)
|
# Surface.Env configuration (see docs/modules/scanner/design/surface-env.md)
|
||||||
SCANNER_SURFACE_FS_ENDPOINT: "${SCANNER_SURFACE_FS_ENDPOINT:-http://rustfs:8080}"
|
SCANNER_SURFACE_FS_ENDPOINT: "${SCANNER_SURFACE_FS_ENDPOINT:-http://rustfs:8080}"
|
||||||
SCANNER_SURFACE_FS_BUCKET: "${SCANNER_SURFACE_FS_BUCKET:-surface-cache}"
|
SCANNER_SURFACE_FS_BUCKET: "${SCANNER_SURFACE_FS_BUCKET:-surface-cache}"
|
||||||
@@ -232,6 +237,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- scanner-surface-cache:/var/lib/stellaops/surface
|
- scanner-surface-cache:/var/lib/stellaops/surface
|
||||||
- ${SURFACE_SECRETS_HOST_PATH:-./offline/surface-secrets}:${SCANNER_SURFACE_SECRETS_ROOT:-/etc/stellaops/secrets}:ro
|
- ${SURFACE_SECRETS_HOST_PATH:-./offline/surface-secrets}:${SCANNER_SURFACE_SECRETS_ROOT:-/etc/stellaops/secrets}:ro
|
||||||
|
- ${SCANNER_OFFLINEKIT_TRUSTROOTS_HOST_PATH:-./offline/trust-roots}:${SCANNER_OFFLINEKIT_TRUSTROOTDIRECTORY:-/etc/stellaops/trust-roots}:ro
|
||||||
|
- ${SCANNER_OFFLINEKIT_REKOR_SNAPSHOT_HOST_PATH:-./offline/rekor-snapshot}:${SCANNER_OFFLINEKIT_REKORSNAPSHOTDIRECTORY:-/var/lib/stellaops/rekor-snapshot}:ro
|
||||||
ports:
|
ports:
|
||||||
- "${SCANNER_WEB_PORT:-8444}:8444"
|
- "${SCANNER_WEB_PORT:-8444}:8444"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -201,6 +201,14 @@ services:
|
|||||||
SCANNER__EVENTS__STREAM: "${SCANNER_EVENTS_STREAM:-stella.events}"
|
SCANNER__EVENTS__STREAM: "${SCANNER_EVENTS_STREAM:-stella.events}"
|
||||||
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "${SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS:-5}"
|
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "${SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS:-5}"
|
||||||
SCANNER__EVENTS__MAXSTREAMLENGTH: "${SCANNER_EVENTS_MAX_STREAM_LENGTH:-10000}"
|
SCANNER__EVENTS__MAXSTREAMLENGTH: "${SCANNER_EVENTS_MAX_STREAM_LENGTH:-10000}"
|
||||||
|
SCANNER__OFFLINEKIT__ENABLED: "${SCANNER_OFFLINEKIT_ENABLED:-false}"
|
||||||
|
SCANNER__OFFLINEKIT__REQUIREDSSE: "${SCANNER_OFFLINEKIT_REQUIREDSSE:-true}"
|
||||||
|
SCANNER__OFFLINEKIT__REKOROFFLINEMODE: "${SCANNER_OFFLINEKIT_REKOROFFLINEMODE:-true}"
|
||||||
|
SCANNER__OFFLINEKIT__TRUSTROOTDIRECTORY: "${SCANNER_OFFLINEKIT_TRUSTROOTDIRECTORY:-/etc/stellaops/trust-roots}"
|
||||||
|
SCANNER__OFFLINEKIT__REKORSNAPSHOTDIRECTORY: "${SCANNER_OFFLINEKIT_REKORSNAPSHOTDIRECTORY:-/var/lib/stellaops/rekor-snapshot}"
|
||||||
|
volumes:
|
||||||
|
- ${SCANNER_OFFLINEKIT_TRUSTROOTS_HOST_PATH:-./offline/trust-roots}:${SCANNER_OFFLINEKIT_TRUSTROOTDIRECTORY:-/etc/stellaops/trust-roots}:ro
|
||||||
|
- ${SCANNER_OFFLINEKIT_REKOR_SNAPSHOT_HOST_PATH:-./offline/rekor-snapshot}:${SCANNER_OFFLINEKIT_REKORSNAPSHOTDIRECTORY:-/var/lib/stellaops/rekor-snapshot}:ro
|
||||||
ports:
|
ports:
|
||||||
- "${SCANNER_WEB_PORT:-8444}:8444"
|
- "${SCANNER_WEB_PORT:-8444}:8444"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -208,6 +208,14 @@ services:
|
|||||||
SCANNER__EVENTS__STREAM: "${SCANNER_EVENTS_STREAM:-stella.events}"
|
SCANNER__EVENTS__STREAM: "${SCANNER_EVENTS_STREAM:-stella.events}"
|
||||||
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "${SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS:-5}"
|
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "${SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS:-5}"
|
||||||
SCANNER__EVENTS__MAXSTREAMLENGTH: "${SCANNER_EVENTS_MAX_STREAM_LENGTH:-10000}"
|
SCANNER__EVENTS__MAXSTREAMLENGTH: "${SCANNER_EVENTS_MAX_STREAM_LENGTH:-10000}"
|
||||||
|
SCANNER__OFFLINEKIT__ENABLED: "${SCANNER_OFFLINEKIT_ENABLED:-false}"
|
||||||
|
SCANNER__OFFLINEKIT__REQUIREDSSE: "${SCANNER_OFFLINEKIT_REQUIREDSSE:-true}"
|
||||||
|
SCANNER__OFFLINEKIT__REKOROFFLINEMODE: "${SCANNER_OFFLINEKIT_REKOROFFLINEMODE:-true}"
|
||||||
|
SCANNER__OFFLINEKIT__TRUSTROOTDIRECTORY: "${SCANNER_OFFLINEKIT_TRUSTROOTDIRECTORY:-/etc/stellaops/trust-roots}"
|
||||||
|
SCANNER__OFFLINEKIT__REKORSNAPSHOTDIRECTORY: "${SCANNER_OFFLINEKIT_REKORSNAPSHOTDIRECTORY:-/var/lib/stellaops/rekor-snapshot}"
|
||||||
|
volumes:
|
||||||
|
- ${SCANNER_OFFLINEKIT_TRUSTROOTS_HOST_PATH:-./offline/trust-roots}:${SCANNER_OFFLINEKIT_TRUSTROOTDIRECTORY:-/etc/stellaops/trust-roots}:ro
|
||||||
|
- ${SCANNER_OFFLINEKIT_REKOR_SNAPSHOT_HOST_PATH:-./offline/rekor-snapshot}:${SCANNER_OFFLINEKIT_REKORSNAPSHOTDIRECTORY:-/var/lib/stellaops/rekor-snapshot}:ro
|
||||||
ports:
|
ports:
|
||||||
- "${SCANNER_WEB_PORT:-8444}:8444"
|
- "${SCANNER_WEB_PORT:-8444}:8444"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -201,6 +201,14 @@ services:
|
|||||||
SCANNER__EVENTS__STREAM: "${SCANNER_EVENTS_STREAM:-stella.events}"
|
SCANNER__EVENTS__STREAM: "${SCANNER_EVENTS_STREAM:-stella.events}"
|
||||||
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "${SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS:-5}"
|
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "${SCANNER_EVENTS_PUBLISH_TIMEOUT_SECONDS:-5}"
|
||||||
SCANNER__EVENTS__MAXSTREAMLENGTH: "${SCANNER_EVENTS_MAX_STREAM_LENGTH:-10000}"
|
SCANNER__EVENTS__MAXSTREAMLENGTH: "${SCANNER_EVENTS_MAX_STREAM_LENGTH:-10000}"
|
||||||
|
SCANNER__OFFLINEKIT__ENABLED: "${SCANNER_OFFLINEKIT_ENABLED:-false}"
|
||||||
|
SCANNER__OFFLINEKIT__REQUIREDSSE: "${SCANNER_OFFLINEKIT_REQUIREDSSE:-true}"
|
||||||
|
SCANNER__OFFLINEKIT__REKOROFFLINEMODE: "${SCANNER_OFFLINEKIT_REKOROFFLINEMODE:-true}"
|
||||||
|
SCANNER__OFFLINEKIT__TRUSTROOTDIRECTORY: "${SCANNER_OFFLINEKIT_TRUSTROOTDIRECTORY:-/etc/stellaops/trust-roots}"
|
||||||
|
SCANNER__OFFLINEKIT__REKORSNAPSHOTDIRECTORY: "${SCANNER_OFFLINEKIT_REKORSNAPSHOTDIRECTORY:-/var/lib/stellaops/rekor-snapshot}"
|
||||||
|
volumes:
|
||||||
|
- ${SCANNER_OFFLINEKIT_TRUSTROOTS_HOST_PATH:-./offline/trust-roots}:${SCANNER_OFFLINEKIT_TRUSTROOTDIRECTORY:-/etc/stellaops/trust-roots}:ro
|
||||||
|
- ${SCANNER_OFFLINEKIT_REKOR_SNAPSHOT_HOST_PATH:-./offline/rekor-snapshot}:${SCANNER_OFFLINEKIT_REKORSNAPSHOTDIRECTORY:-/var/lib/stellaops/rekor-snapshot}:ro
|
||||||
ports:
|
ports:
|
||||||
- "${SCANNER_WEB_PORT:-8444}:8444"
|
- "${SCANNER_WEB_PORT:-8444}:8444"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -156,6 +156,11 @@ services:
|
|||||||
SCANNER__EVENTS__STREAM: "stella.events"
|
SCANNER__EVENTS__STREAM: "stella.events"
|
||||||
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
|
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
|
||||||
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
|
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
|
||||||
|
SCANNER__OFFLINEKIT__ENABLED: "false"
|
||||||
|
SCANNER__OFFLINEKIT__REQUIREDSSE: "true"
|
||||||
|
SCANNER__OFFLINEKIT__REKOROFFLINEMODE: "true"
|
||||||
|
SCANNER__OFFLINEKIT__TRUSTROOTDIRECTORY: "/etc/stellaops/trust-roots"
|
||||||
|
SCANNER__OFFLINEKIT__REKORSNAPSHOTDIRECTORY: "/var/lib/stellaops/rekor-snapshot"
|
||||||
SCANNER_SURFACE_FS_ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
|
SCANNER_SURFACE_FS_ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
|
||||||
SCANNER_SURFACE_CACHE_ROOT: "/var/lib/stellaops/surface"
|
SCANNER_SURFACE_CACHE_ROOT: "/var/lib/stellaops/surface"
|
||||||
SCANNER_SURFACE_SECRETS_PROVIDER: "file"
|
SCANNER_SURFACE_SECRETS_PROVIDER: "file"
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ services:
|
|||||||
SCANNER__EVENTS__STREAM: "stella.events"
|
SCANNER__EVENTS__STREAM: "stella.events"
|
||||||
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
|
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
|
||||||
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
|
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
|
||||||
|
SCANNER__OFFLINEKIT__ENABLED: "false"
|
||||||
|
SCANNER__OFFLINEKIT__REQUIREDSSE: "true"
|
||||||
|
SCANNER__OFFLINEKIT__REKOROFFLINEMODE: "true"
|
||||||
|
SCANNER__OFFLINEKIT__TRUSTROOTDIRECTORY: "/etc/stellaops/trust-roots"
|
||||||
|
SCANNER__OFFLINEKIT__REKORSNAPSHOTDIRECTORY: "/var/lib/stellaops/rekor-snapshot"
|
||||||
SCANNER_SURFACE_FS_ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
|
SCANNER_SURFACE_FS_ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
|
||||||
SCANNER_SURFACE_CACHE_ROOT: "/var/lib/stellaops/surface"
|
SCANNER_SURFACE_CACHE_ROOT: "/var/lib/stellaops/surface"
|
||||||
SCANNER_SURFACE_SECRETS_PROVIDER: "inline"
|
SCANNER_SURFACE_SECRETS_PROVIDER: "inline"
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ services:
|
|||||||
SCANNER__EVENTS__STREAM: "stella.events"
|
SCANNER__EVENTS__STREAM: "stella.events"
|
||||||
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
|
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
|
||||||
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
|
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
|
||||||
|
SCANNER__OFFLINEKIT__ENABLED: "false"
|
||||||
|
SCANNER__OFFLINEKIT__REQUIREDSSE: "true"
|
||||||
|
SCANNER__OFFLINEKIT__REKOROFFLINEMODE: "true"
|
||||||
|
SCANNER__OFFLINEKIT__TRUSTROOTDIRECTORY: "/etc/stellaops/trust-roots"
|
||||||
|
SCANNER__OFFLINEKIT__REKORSNAPSHOTDIRECTORY: "/var/lib/stellaops/rekor-snapshot"
|
||||||
SCANNER_SURFACE_FS_ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
|
SCANNER_SURFACE_FS_ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
|
||||||
SCANNER_SURFACE_CACHE_ROOT: "/var/lib/stellaops/surface"
|
SCANNER_SURFACE_CACHE_ROOT: "/var/lib/stellaops/surface"
|
||||||
SCANNER_SURFACE_SECRETS_PROVIDER: "kubernetes"
|
SCANNER_SURFACE_SECRETS_PROVIDER: "kubernetes"
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ services:
|
|||||||
SCANNER__EVENTS__STREAM: "stella.events"
|
SCANNER__EVENTS__STREAM: "stella.events"
|
||||||
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
|
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
|
||||||
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
|
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
|
||||||
|
SCANNER__OFFLINEKIT__ENABLED: "false"
|
||||||
|
SCANNER__OFFLINEKIT__REQUIREDSSE: "true"
|
||||||
|
SCANNER__OFFLINEKIT__REKOROFFLINEMODE: "true"
|
||||||
|
SCANNER__OFFLINEKIT__TRUSTROOTDIRECTORY: "/etc/stellaops/trust-roots"
|
||||||
|
SCANNER__OFFLINEKIT__REKORSNAPSHOTDIRECTORY: "/var/lib/stellaops/rekor-snapshot"
|
||||||
SCANNER_SURFACE_FS_ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
|
SCANNER_SURFACE_FS_ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
|
||||||
SCANNER_SURFACE_CACHE_ROOT: "/var/lib/stellaops/surface"
|
SCANNER_SURFACE_CACHE_ROOT: "/var/lib/stellaops/surface"
|
||||||
SCANNER_SURFACE_SECRETS_PROVIDER: "kubernetes"
|
SCANNER_SURFACE_SECRETS_PROVIDER: "kubernetes"
|
||||||
|
|||||||
42
deploy/telemetry/alerts/scanner-fn-drift-alerts.yaml
Normal file
42
deploy/telemetry/alerts/scanner-fn-drift-alerts.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Scanner FN-Drift Alert Rules
|
||||||
|
# SLO alerts for false-negative drift thresholds (30-day rolling window)
|
||||||
|
|
||||||
|
groups:
|
||||||
|
- name: scanner-fn-drift
|
||||||
|
interval: 30s
|
||||||
|
rules:
|
||||||
|
- alert: ScannerFnDriftWarning
|
||||||
|
expr: scanner_fn_drift_percent > 1.0
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
service: scanner
|
||||||
|
slo: fn-drift
|
||||||
|
annotations:
|
||||||
|
summary: "Scanner FN-Drift rate above warning threshold"
|
||||||
|
description: "FN-Drift is {{ $value | humanizePercentage }} (> 1.0%) over the 30-day rolling window."
|
||||||
|
runbook_url: "https://docs.stellaops.io/runbooks/scanner/fn-drift-warning"
|
||||||
|
|
||||||
|
- alert: ScannerFnDriftCritical
|
||||||
|
expr: scanner_fn_drift_percent > 2.5
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
service: scanner
|
||||||
|
slo: fn-drift
|
||||||
|
annotations:
|
||||||
|
summary: "Scanner FN-Drift rate above critical threshold"
|
||||||
|
description: "FN-Drift is {{ $value | humanizePercentage }} (> 2.5%) over the 30-day rolling window."
|
||||||
|
runbook_url: "https://docs.stellaops.io/runbooks/scanner/fn-drift-critical"
|
||||||
|
|
||||||
|
- alert: ScannerFnDriftEngineViolation
|
||||||
|
expr: scanner_fn_drift_cause_engine > 0
|
||||||
|
for: 1m
|
||||||
|
labels:
|
||||||
|
severity: page
|
||||||
|
service: scanner
|
||||||
|
slo: determinism
|
||||||
|
annotations:
|
||||||
|
summary: "Engine-caused FN drift detected (determinism violation)"
|
||||||
|
description: "Engine-caused FN drift count is {{ $value }} (> 0). This indicates non-feed, non-policy changes affecting outcomes."
|
||||||
|
runbook_url: "https://docs.stellaops.io/runbooks/scanner/fn-drift-engine-violation"
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
| | Usage API (`/quota`) | ✅ | — | — | CI can poll remaining scans |
|
| | Usage API (`/quota`) | ✅ | — | — | CI can poll remaining scans |
|
||||||
| **User Interface** | Dark / light mode | ✅ | — | — | Auto‑detect OS theme |
|
| **User Interface** | Dark / light mode | ✅ | — | — | Auto‑detect OS theme |
|
||||||
| | Additional locale (Cyrillic) | ✅ | — | — | Default if `Accept‑Language: bg` or any other |
|
| | Additional locale (Cyrillic) | ✅ | — | — | Default if `Accept‑Language: bg` or any other |
|
||||||
| | Audit trail | ✅ | — | — | Mongo history |
|
| | Audit trail | ✅ | — | — | PostgreSQL history |
|
||||||
| **Deployment** | Docker Compose bundle | ✅ | — | — | Single‑node |
|
| **Deployment** | Docker Compose bundle | ✅ | — | — | Single‑node |
|
||||||
| | Helm chart (K8s) | ✅ | — | — | Horizontal scaling |
|
| | Helm chart (K8s) | ✅ | — | — | Horizontal scaling |
|
||||||
| | High‑availability split services | — | — | ✅ (Add‑On) | HA Redis & Mongo |
|
| | High‑availability split services | — | — | ✅ (Add‑On) | HA Redis & PostgreSQL |
|
||||||
| **Extensibility** | .NET hot‑load plug‑ins | ✅ | N/A | — | AGPL reference SDK |
|
| **Extensibility** | .NET hot‑load plug‑ins | ✅ | N/A | — | AGPL reference SDK |
|
||||||
| | Community plug‑in marketplace | — | ⏳ (β Q2‑2026) | — | Moderated listings |
|
| | Community plug‑in marketplace | — | ⏳ (β Q2‑2026) | — | Moderated listings |
|
||||||
| **Telemetry** | Opt‑in anonymous metrics | ✅ | — | — | Required for quota satisfaction KPI |
|
| **Telemetry** | Opt‑in anonymous metrics | ✅ | — | — | Required for quota satisfaction KPI |
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ access.
|
|||||||
| **NFR‑PERF‑1** | Performance | P95 cold scan ≤ 5 s; warm ≤ 1 s (see **FR‑DELTA‑3**). |
|
| **NFR‑PERF‑1** | Performance | P95 cold scan ≤ 5 s; warm ≤ 1 s (see **FR‑DELTA‑3**). |
|
||||||
| **NFR‑PERF‑2** | Throughput | System shall sustain 60 concurrent scans on 8‑core node without queue depth >10. |
|
| **NFR‑PERF‑2** | Throughput | System shall sustain 60 concurrent scans on 8‑core node without queue depth >10. |
|
||||||
| **NFR‑AVAIL‑1** | Availability | All services shall start offline; any Internet call must be optional. |
|
| **NFR‑AVAIL‑1** | Availability | All services shall start offline; any Internet call must be optional. |
|
||||||
| **NFR‑SCAL‑1** | Scalability | Horizontal scaling via Kubernetes replicas for backend, Redis Sentinel, Mongo replica set. |
|
| **NFR-SCAL-1** | Scalability | Horizontal scaling via Kubernetes replicas for backend, Redis Sentinel, PostgreSQL cluster. |
|
||||||
| **NFR‑SEC‑1** | Security | All inter‑service traffic shall use TLS or localhost sockets. |
|
| **NFR‑SEC‑1** | Security | All inter‑service traffic shall use TLS or localhost sockets. |
|
||||||
| **NFR‑COMP‑1** | Compatibility | Platform shall run on x86‑64 Linux kernel ≥ 5.10; Windows agents (TODO > 6 mo) must support Server 2019+. |
|
| **NFR‑COMP‑1** | Compatibility | Platform shall run on x86‑64 Linux kernel ≥ 5.10; Windows agents (TODO > 6 mo) must support Server 2019+. |
|
||||||
| **NFR‑I18N‑1** | Internationalisation | UI must support EN and at least one additional locale (Cyrillic). |
|
| **NFR‑I18N‑1** | Internationalisation | UI must support EN and at least one additional locale (Cyrillic). |
|
||||||
@@ -179,7 +179,7 @@ Authorization: Bearer <token>
|
|||||||
## 9 · Assumptions & Constraints
|
## 9 · Assumptions & Constraints
|
||||||
|
|
||||||
* Hardware reference: 8 vCPU, 8 GB RAM, NVMe SSD.
|
* Hardware reference: 8 vCPU, 8 GB RAM, NVMe SSD.
|
||||||
* Mongo DB and Redis run co‑located unless horizontal scaling enabled.
|
* PostgreSQL and Redis run co-located unless horizontal scaling enabled.
|
||||||
* All docker images tagged `latest` are immutable (CI process locks digests).
|
* All docker images tagged `latest` are immutable (CI process locks digests).
|
||||||
* Rego evaluation runs in embedded OPA Go‑library (no external binary).
|
* Rego evaluation runs in embedded OPA Go‑library (no external binary).
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
| **Scanner.Worker** | `stellaops/scanner-worker` | Runs analyzers (OS, Lang: Java/Node/Python/Go/.NET/Rust, Native ELF/PE/Mach‑O, EntryTrace); emits per‑layer SBOMs and composes image SBOMs. | Horizontal; queue‑driven; sharded by layer digest. |
|
| **Scanner.Worker** | `stellaops/scanner-worker` | Runs analyzers (OS, Lang: Java/Node/Python/Go/.NET/Rust, Native ELF/PE/Mach‑O, EntryTrace); emits per‑layer SBOMs and composes image SBOMs. | Horizontal; queue‑driven; sharded by layer digest. |
|
||||||
| **Scanner.Sbomer.BuildXPlugin** | `stellaops/sbom-indexer` | BuildKit **generator** for build‑time SBOMs as OCI **referrers**. | CI‑side; ephemeral. |
|
| **Scanner.Sbomer.BuildXPlugin** | `stellaops/sbom-indexer` | BuildKit **generator** for build‑time SBOMs as OCI **referrers**. | CI‑side; ephemeral. |
|
||||||
| **Scanner.Sbomer.DockerImage** | `stellaops/scanner-cli` | CLI‑orchestrated scanner container for post‑build scans. | Local/CI; ephemeral. |
|
| **Scanner.Sbomer.DockerImage** | `stellaops/scanner-cli` | CLI‑orchestrated scanner container for post‑build scans. | Local/CI; ephemeral. |
|
||||||
| **Concelier.WebService** | `stellaops/concelier-web` | Vulnerability ingest/normalize/merge/export (JSON + Trivy DB). | HA via Mongo locks. |
|
| **Concelier.WebService** | `stellaops/concelier-web` | Vulnerability ingest/normalize/merge/export (JSON + Trivy DB). | HA via PostgreSQL locks. |
|
||||||
| **Excititor.WebService** | `stellaops/excititor-web` | VEX ingest/normalize/consensus; conflict retention; exports. | HA via Mongo locks. |
|
| **Excititor.WebService** | `stellaops/excititor-web` | VEX ingest/normalize/consensus; conflict retention; exports. | HA via PostgreSQL locks. |
|
||||||
| **Policy Engine** | (in `scanner-web`) | YAML DSL evaluator (waivers, vendor preferences, KEV/EPSS, license, usage‑gating); produces **policy digest**. | In‑process; cache per digest. |
|
| **Policy Engine** | (in `scanner-web`) | YAML DSL evaluator (waivers, vendor preferences, KEV/EPSS, license, usage‑gating); produces **policy digest**. | In‑process; cache per digest. |
|
||||||
| **Scheduler.WebService** | `stellaops/scheduler-web` | Schedules **re‑evaluation** runs; consumes Concelier/Excititor deltas; selects **impacted images** via BOM‑Index; orchestrates analysis‑only reports. | Stateless API. |
|
| **Scheduler.WebService** | `stellaops/scheduler-web` | Schedules **re‑evaluation** runs; consumes Concelier/Excititor deltas; selects **impacted images** via BOM‑Index; orchestrates analysis‑only reports. | Stateless API. |
|
||||||
| **Scheduler.Worker** | `stellaops/scheduler-worker` | Executes selection and enqueues batches toward Scanner; enforces rate/limits and windows; maintains impact cursors. | Horizontal; queue‑driven. |
|
| **Scheduler.Worker** | `stellaops/scheduler-worker` | Executes selection and enqueues batches toward Scanner; enforces rate/limits and windows; maintains impact cursors. | Horizontal; queue‑driven. |
|
||||||
|
|||||||
@@ -814,7 +814,7 @@ See `docs/dev/32_AUTH_CLIENT_GUIDE.md` for recommended profiles (online vs. air-
|
|||||||
|
|
||||||
### Ruby dependency verbs (`stellaops-cli ruby …`)
|
### Ruby dependency verbs (`stellaops-cli ruby …`)
|
||||||
|
|
||||||
`ruby inspect` runs the same deterministic `RubyLanguageAnalyzer` bundled with Scanner.Worker against the local working tree—no backend calls—so operators can sanity-check Gemfile / Gemfile.lock pairs before shipping. The command now renders an observation banner (bundler version, package/runtime counts, capability flags, scheduler names) before the package table so air-gapped users can prove what evidence was collected. `ruby resolve` reuses the persisted `RubyPackageInventory` (stored under Mongo `ruby.packages` and exposed via `GET /api/scans/{scanId}/ruby-packages`) so operators can reason about groups/platforms/runtime usage after Scanner or Offline Kits finish processing; the CLI surfaces `scanId`, `imageDigest`, and `generatedAt` metadata in JSON mode for downstream scripting.
|
`ruby inspect` runs the same deterministic `RubyLanguageAnalyzer` bundled with Scanner.Worker against the local working tree—no backend calls—so operators can sanity-check Gemfile / Gemfile.lock pairs before shipping. The command now renders an observation banner (bundler version, package/runtime counts, capability flags, scheduler names) before the package table so air-gapped users can prove what evidence was collected. `ruby resolve` reuses the persisted `RubyPackageInventory` (stored in the PostgreSQL `ruby_packages` table and exposed via `GET /api/scans/{scanId}/ruby-packages`) so operators can reason about groups/platforms/runtime usage after Scanner or Offline Kits finish processing; the CLI surfaces `scanId`, `imageDigest`, and `generatedAt` metadata in JSON mode for downstream scripting.
|
||||||
|
|
||||||
**`ruby inspect` flags**
|
**`ruby inspect` flags**
|
||||||
|
|
||||||
@@ -898,6 +898,8 @@ Both commands honour CLI observability hooks: Spectre tables for human output, `
|
|||||||
| `stellaops-cli graph explain` | Show reachability call path for a finding | `--finding <purl:cve>` (required)<br>`--scan-id <id>`<br>`--format table\|json` | Displays `latticeState`, call path with `symbol_id`/`code_id`, runtime hits, `graph_hash`, and DSSE attestation refs |
|
| `stellaops-cli graph explain` | Show reachability call path for a finding | `--finding <purl:cve>` (required)<br>`--scan-id <id>`<br>`--format table\|json` | Displays `latticeState`, call path with `symbol_id`/`code_id`, runtime hits, `graph_hash`, and DSSE attestation refs |
|
||||||
| `stellaops-cli graph export` | Export reachability graph bundle | `--scan-id <id>` (required)<br>`--output <dir>`<br>`--include-runtime` | Creates `richgraph-v1.json`, `.dsse`, `meta.json`, and optional `runtime-facts.ndjson` |
|
| `stellaops-cli graph export` | Export reachability graph bundle | `--scan-id <id>` (required)<br>`--output <dir>`<br>`--include-runtime` | Creates `richgraph-v1.json`, `.dsse`, `meta.json`, and optional `runtime-facts.ndjson` |
|
||||||
| `stellaops-cli graph verify` | Verify graph DSSE signature and Rekor entry | `--graph <path>` (required)<br>`--dsse <path>`<br>`--rekor-log` | Recomputes BLAKE3 hash, validates DSSE envelope, checks Rekor inclusion proof |
|
| `stellaops-cli graph verify` | Verify graph DSSE signature and Rekor entry | `--graph <path>` (required)<br>`--dsse <path>`<br>`--rekor-log` | Recomputes BLAKE3 hash, validates DSSE envelope, checks Rekor inclusion proof |
|
||||||
|
| `stellaops-cli proof verify` | Verify an artifact's proof chain | `<artifact>` (required)<br>`--sbom <file>`<br>`--vex <file>`<br>`--anchor <uuid>`<br>`--offline`<br>`--output text\|json`<br>`-v/-vv` | Validates proof spine, Merkle inclusion, VEX statements, and Rekor entries. Returns exit code 0 (pass), 1 (policy violation), or 2 (system error). Designed for CI/CD integration. |
|
||||||
|
| `stellaops-cli proof spine` | Display proof spine for an artifact | `<artifact>` (required)<br>`--format table\|json`<br>`--show-merkle` | Shows assembled proof spine with evidence statements, VEX verdicts, and Merkle tree structure. |
|
||||||
| `stellaops-cli replay verify` | Verify replay manifest determinism | `--manifest <path>` (required)<br>`--sealed`<br>`--verbose` | Recomputes all artifact hashes and compares against manifest; exit 0 on match |
|
| `stellaops-cli replay verify` | Verify replay manifest determinism | `--manifest <path>` (required)<br>`--sealed`<br>`--verbose` | Recomputes all artifact hashes and compares against manifest; exit 0 on match |
|
||||||
| `stellaops-cli runtime policy test` | Ask Scanner.WebService for runtime verdicts (Webhook parity) | `--image/-i <digest>` (repeatable, comma/space lists supported)<br>`--file/-f <path>`<br>`--namespace/--ns <name>`<br>`--label/-l key=value` (repeatable)<br>`--json` | Posts to `POST /api/v1/scanner/policy/runtime`, deduplicates image digests, and prints TTL/policy revision plus per-image columns for signed state, SBOM referrers, quieted-by metadata, confidence, Rekor attestation (uuid + verified flag), and recently observed build IDs (shortened for readability). Accepts newline/whitespace-delimited stdin when piped; `--json` emits the raw response without additional logging. |
|
| `stellaops-cli runtime policy test` | Ask Scanner.WebService for runtime verdicts (Webhook parity) | `--image/-i <digest>` (repeatable, comma/space lists supported)<br>`--file/-f <path>`<br>`--namespace/--ns <name>`<br>`--label/-l key=value` (repeatable)<br>`--json` | Posts to `POST /api/v1/scanner/policy/runtime`, deduplicates image digests, and prints TTL/policy revision plus per-image columns for signed state, SBOM referrers, quieted-by metadata, confidence, Rekor attestation (uuid + verified flag), and recently observed build IDs (shortened for readability). Accepts newline/whitespace-delimited stdin when piped; `--json` emits the raw response without additional logging. |
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ runtime wiring, CLI usage) and leaves connector/internal customization for later
|
|||||||
## 0 · Prerequisites
|
## 0 · Prerequisites
|
||||||
|
|
||||||
- .NET SDK **10.0.100-preview** (matches `global.json`)
|
- .NET SDK **10.0.100-preview** (matches `global.json`)
|
||||||
- MongoDB instance reachable from the host (local Docker or managed)
|
- PostgreSQL instance reachable from the host (local Docker or managed)
|
||||||
- `trivy-db` binary on `PATH` for Trivy exports (and `oras` if publishing to OCI)
|
- `trivy-db` binary on `PATH` for Trivy exports (and `oras` if publishing to OCI)
|
||||||
- Plugin assemblies present in `StellaOps.Concelier.PluginBinaries/` (already included in the repo)
|
- Plugin assemblies present in `StellaOps.Concelier.PluginBinaries/` (already included in the repo)
|
||||||
- Optional: Docker/Podman runtime if you plan to run scanners locally
|
- Optional: Docker/Podman runtime if you plan to run scanners locally
|
||||||
@@ -30,7 +30,7 @@ runtime wiring, CLI usage) and leaves connector/internal customization for later
|
|||||||
cp etc/concelier.yaml.sample etc/concelier.yaml
|
cp etc/concelier.yaml.sample etc/concelier.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Edit `etc/concelier.yaml` and update the MongoDB DSN (and optional database name).
|
2. Edit `etc/concelier.yaml` and update the PostgreSQL DSN (and optional database name).
|
||||||
The default template configures plug-in discovery to look in `StellaOps.Concelier.PluginBinaries/`
|
The default template configures plug-in discovery to look in `StellaOps.Concelier.PluginBinaries/`
|
||||||
and disables remote telemetry exporters by default.
|
and disables remote telemetry exporters by default.
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ runtime wiring, CLI usage) and leaves connector/internal customization for later
|
|||||||
`CONCELIER_`. Example:
|
`CONCELIER_`. Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export CONCELIER_STORAGE__DSN="mongodb://user:pass@mongo:27017/concelier"
|
export CONCELIER_STORAGE__DSN="Host=localhost;Port=5432;Database=concelier;Username=user;Password=pass"
|
||||||
export CONCELIER_TELEMETRY__ENABLETRACING=false
|
export CONCELIER_TELEMETRY__ENABLETRACING=false
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -48,11 +48,11 @@ runtime wiring, CLI usage) and leaves connector/internal customization for later
|
|||||||
dotnet run --project src/Concelier/StellaOps.Concelier.WebService
|
dotnet run --project src/Concelier/StellaOps.Concelier.WebService
|
||||||
```
|
```
|
||||||
|
|
||||||
On startup Concelier validates the options, boots MongoDB indexes, loads plug-ins,
|
On startup Concelier validates the options, boots PostgreSQL indexes, loads plug-ins,
|
||||||
and exposes:
|
and exposes:
|
||||||
|
|
||||||
- `GET /health` – returns service status and telemetry settings
|
- `GET /health` – returns service status and telemetry settings
|
||||||
- `GET /ready` – performs a MongoDB `ping`
|
- `GET /ready` – performs a PostgreSQL `ping`
|
||||||
- `GET /jobs` + `POST /jobs/{kind}` – inspect and trigger connector/export jobs
|
- `GET /jobs` + `POST /jobs/{kind}` – inspect and trigger connector/export jobs
|
||||||
|
|
||||||
> **Security note** – authentication now ships via StellaOps Authority. Keep
|
> **Security note** – authentication now ships via StellaOps Authority. Keep
|
||||||
@@ -263,8 +263,8 @@ a problem document.
|
|||||||
triggering Concelier jobs.
|
triggering Concelier jobs.
|
||||||
- Export artefacts are materialised under the configured output directories and
|
- Export artefacts are materialised under the configured output directories and
|
||||||
their manifests record digests.
|
their manifests record digests.
|
||||||
- MongoDB contains the expected `document`, `dto`, `advisory`, and `export_state`
|
- PostgreSQL contains the expected `document`, `dto`, `advisory`, and `export_state`
|
||||||
collections after a run.
|
tables after a run.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ a problem document.
|
|||||||
- Treat `etc/concelier.yaml.sample` as the canonical template. CI/CD should copy it to
|
- Treat `etc/concelier.yaml.sample` as the canonical template. CI/CD should copy it to
|
||||||
the deployment artifact and replace placeholders (DSN, telemetry endpoints, cron
|
the deployment artifact and replace placeholders (DSN, telemetry endpoints, cron
|
||||||
overrides) with environment-specific secrets.
|
overrides) with environment-specific secrets.
|
||||||
- Keep secret material (Mongo credentials, OTLP tokens) outside of the repository;
|
- Keep secret material (PostgreSQL credentials, OTLP tokens) outside of the repository;
|
||||||
inject them via secret stores or pipeline variables at stamp time.
|
inject them via secret stores or pipeline variables at stamp time.
|
||||||
- When building container images, include `trivy-db` (and `oras` if used) so air-gapped
|
- When building container images, include `trivy-db` (and `oras` if used) so air-gapped
|
||||||
clusters do not need outbound downloads at runtime.
|
clusters do not need outbound downloads at runtime.
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ using StellaOps.DependencyInjection;
|
|||||||
[ServiceBinding(typeof(IJob), ServiceLifetime.Scoped, RegisterAsSelf = true)]
|
[ServiceBinding(typeof(IJob), ServiceLifetime.Scoped, RegisterAsSelf = true)]
|
||||||
public sealed class MyJob : IJob
|
public sealed class MyJob : IJob
|
||||||
{
|
{
|
||||||
// IJob dependencies can now use scoped services (Mongo sessions, etc.)
|
// IJob dependencies can now use scoped services (PostgreSQL connections, etc.)
|
||||||
}
|
}
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ On merge, the plug‑in shows up in the UI Marketplace.
|
|||||||
| NotDetected | .sig missing | cosign sign … |
|
| NotDetected | .sig missing | cosign sign … |
|
||||||
| VersionGateMismatch | Backend 2.1 vs plug‑in 2.0 | Re‑compile / bump attribute |
|
| VersionGateMismatch | Backend 2.1 vs plug‑in 2.0 | Re‑compile / bump attribute |
|
||||||
| FileLoadException | Duplicate | StellaOps.Common Ensure PrivateAssets="all" |
|
| FileLoadException | Duplicate | StellaOps.Common Ensure PrivateAssets="all" |
|
||||||
| Redis | timeouts Large writes | Batch or use Mongo |
|
| Redis | timeouts Large writes | Batch or use PostgreSQL |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
The **StellaOps Authority** service issues OAuth2/OIDC tokens for every StellaOps module (Concelier, Backend, Agent, Zastava) and exposes the policy controls required in sovereign/offline environments. Authority is built as a minimal ASP.NET host that:
|
The **StellaOps Authority** service issues OAuth2/OIDC tokens for every StellaOps module (Concelier, Backend, Agent, Zastava) and exposes the policy controls required in sovereign/offline environments. Authority is built as a minimal ASP.NET host that:
|
||||||
|
|
||||||
- brokers password, client-credentials, and device-code flows through pluggable identity providers;
|
- brokers password, client-credentials, and device-code flows through pluggable identity providers;
|
||||||
- persists access/refresh/device tokens in MongoDB with deterministic schemas for replay analysis and air-gapped audit copies;
|
- persists access/refresh/device tokens in PostgreSQL with deterministic schemas for replay analysis and air-gapped audit copies;
|
||||||
- distributes revocation bundles and JWKS material so downstream services can enforce lockouts without direct database access;
|
- distributes revocation bundles and JWKS material so downstream services can enforce lockouts without direct database access;
|
||||||
- offers bootstrap APIs for first-run provisioning and key rotation without redeploying binaries.
|
- offers bootstrap APIs for first-run provisioning and key rotation without redeploying binaries.
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ Authority is composed of five cooperating subsystems:
|
|||||||
|
|
||||||
1. **Minimal API host** – configures OpenIddict endpoints (`/token`, `/authorize`, `/revoke`, `/jwks`), publishes the OpenAPI contract at `/.well-known/openapi`, and enables structured logging/telemetry. Rate limiting hooks (`AuthorityRateLimiter`) wrap every request.
|
1. **Minimal API host** – configures OpenIddict endpoints (`/token`, `/authorize`, `/revoke`, `/jwks`), publishes the OpenAPI contract at `/.well-known/openapi`, and enables structured logging/telemetry. Rate limiting hooks (`AuthorityRateLimiter`) wrap every request.
|
||||||
2. **Plugin host** – loads `StellaOps.Authority.Plugin.*.dll` assemblies, applies capability metadata, and exposes password/client provisioning surfaces through dependency injection.
|
2. **Plugin host** – loads `StellaOps.Authority.Plugin.*.dll` assemblies, applies capability metadata, and exposes password/client provisioning surfaces through dependency injection.
|
||||||
3. **Mongo storage** – persists tokens, revocations, bootstrap invites, and plugin state in deterministic collections indexed for offline sync (`authority_tokens`, `authority_revocations`, etc.).
|
3. **PostgreSQL storage** – persists tokens, revocations, bootstrap invites, and plugin state in deterministic tables indexed for offline sync (`authority_tokens`, `authority_revocations`, etc.).
|
||||||
4. **Cryptography layer** – `StellaOps.Cryptography` abstractions manage password hashing, signing keys, JWKS export, and detached JWS generation.
|
4. **Cryptography layer** – `StellaOps.Cryptography` abstractions manage password hashing, signing keys, JWKS export, and detached JWS generation.
|
||||||
5. **Offline ops APIs** – internal endpoints under `/internal/*` provide administrative flows (bootstrap users/clients, revocation export) guarded by API keys and deterministic audit events.
|
5. **Offline ops APIs** – internal endpoints under `/internal/*` provide administrative flows (bootstrap users/clients, revocation export) guarded by API keys and deterministic audit events.
|
||||||
|
|
||||||
@@ -27,14 +27,14 @@ A high-level sequence for password logins:
|
|||||||
Client -> /token (password grant)
|
Client -> /token (password grant)
|
||||||
-> Rate limiter & audit hooks
|
-> Rate limiter & audit hooks
|
||||||
-> Plugin credential store (Argon2id verification)
|
-> Plugin credential store (Argon2id verification)
|
||||||
-> Token persistence (Mongo authority_tokens)
|
-> Token persistence (PostgreSQL authority_tokens)
|
||||||
-> Response (access/refresh tokens + deterministic claims)
|
-> Response (access/refresh tokens + deterministic claims)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Token Lifecycle & Persistence
|
## 3. Token Lifecycle & Persistence
|
||||||
Authority persists every issued token in MongoDB so operators can audit or revoke without scanning distributed caches.
|
Authority persists every issued token in PostgreSQL so operators can audit or revoke without scanning distributed caches.
|
||||||
|
|
||||||
- **Collection:** `authority_tokens`
|
- **Table:** `authority_tokens`
|
||||||
- **Key fields:**
|
- **Key fields:**
|
||||||
- `tokenId`, `type` (`access_token`, `refresh_token`, `device_code`, `authorization_code`)
|
- `tokenId`, `type` (`access_token`, `refresh_token`, `device_code`, `authorization_code`)
|
||||||
- `subjectId`, `clientId`, ordered `scope` array
|
- `subjectId`, `clientId`, ordered `scope` array
|
||||||
@@ -173,7 +173,7 @@ Graph Explorer introduces dedicated scopes: `graph:write` for Cartographer build
|
|||||||
#### Vuln Explorer scopes, ABAC, and permalinks
|
#### Vuln Explorer scopes, ABAC, and permalinks
|
||||||
|
|
||||||
- **Scopes** – `vuln:view` unlocks read-only access and permalink issuance, `vuln:investigate` allows triage actions (assignment, comments, remediation notes), `vuln:operate` unlocks state transitions and workflow execution, and `vuln:audit` exposes immutable ledgers/exports. The legacy `vuln:read` scope is still emitted for backward compatibility but new clients should request the granular scopes.
|
- **Scopes** – `vuln:view` unlocks read-only access and permalink issuance, `vuln:investigate` allows triage actions (assignment, comments, remediation notes), `vuln:operate` unlocks state transitions and workflow execution, and `vuln:audit` exposes immutable ledgers/exports. The legacy `vuln:read` scope is still emitted for backward compatibility but new clients should request the granular scopes.
|
||||||
- **ABAC attributes** – Tenant roles can project attribute filters (`env`, `owner`, `business_tier`) via the `attributes` block in `authority.yaml` (see the sample `role/vuln-*` definitions). Authority now enforces the same filters on token issuance: client-credential requests must supply `vuln_env`, `vuln_owner`, and `vuln_business_tier` parameters when multiple values are configured, and the values must match the configured allow-list (or `*`). The accepted value pattern is `[a-z0-9:_-]{1,128}`. Issued tokens embed the resolved filters as `stellaops:vuln_env`, `stellaops:vuln_owner`, and `stellaops:vuln_business_tier` claims, and Authority persists the resulting actor chain plus service-account metadata in Mongo for auditability.
|
- **ABAC attributes** – Tenant roles can project attribute filters (`env`, `owner`, `business_tier`) via the `attributes` block in `authority.yaml` (see the sample `role/vuln-*` definitions). Authority now enforces the same filters on token issuance: client-credential requests must supply `vuln_env`, `vuln_owner`, and `vuln_business_tier` parameters when multiple values are configured, and the values must match the configured allow-list (or `*`). The accepted value pattern is `[a-z0-9:_-]{1,128}`. Issued tokens embed the resolved filters as `stellaops:vuln_env`, `stellaops:vuln_owner`, and `stellaops:vuln_business_tier` claims, and Authority persists the resulting actor chain plus service-account metadata in PostgreSQL for auditability.
|
||||||
- **Service accounts** – Delegated Vuln Explorer identities (`svc-vuln-*`) should include the attribute filters in their seed definition. Authority enforces the supplied `attributes` during issuance and stores the selected values on the delegation token, making downstream revocation/audit exports aware of the effective ABAC envelope.
|
- **Service accounts** – Delegated Vuln Explorer identities (`svc-vuln-*`) should include the attribute filters in their seed definition. Authority enforces the supplied `attributes` during issuance and stores the selected values on the delegation token, making downstream revocation/audit exports aware of the effective ABAC envelope.
|
||||||
- **Attachment tokens** – Evidence downloads require scoped tokens issued by Authority. `POST /vuln/attachments/tokens/issue` accepts ledger hashes plus optional metadata, signs the response with the primary Authority key, and records audit trails (`vuln.attachment.token.*`). `POST /vuln/attachments/tokens/verify` validates incoming tokens server-side. See “Attachment signing tokens” below.
|
- **Attachment tokens** – Evidence downloads require scoped tokens issued by Authority. `POST /vuln/attachments/tokens/issue` accepts ledger hashes plus optional metadata, signs the response with the primary Authority key, and records audit trails (`vuln.attachment.token.*`). `POST /vuln/attachments/tokens/verify` validates incoming tokens server-side. See “Attachment signing tokens” below.
|
||||||
- **Token request parameters** – Minimum metadata for Vuln Explorer service accounts:
|
- **Token request parameters** – Minimum metadata for Vuln Explorer service accounts:
|
||||||
@@ -228,7 +228,7 @@ Authority centralises revocation in `authority_revocations` with deterministic c
|
|||||||
| `client` | OAuth client registration revoked. | `revocationId` (= client id) |
|
| `client` | OAuth client registration revoked. | `revocationId` (= client id) |
|
||||||
| `key` | Signing/JWE key withdrawn. | `revocationId` (= key id) |
|
| `key` | Signing/JWE key withdrawn. | `revocationId` (= key id) |
|
||||||
|
|
||||||
`RevocationBundleBuilder` flattens Mongo documents into canonical JSON, sorts entries by (`category`, `revocationId`, `revokedAt`), and signs exports using detached JWS (RFC 7797) with cosign-compatible headers.
|
`RevocationBundleBuilder` flattens PostgreSQL records into canonical JSON, sorts entries by (`category`, `revocationId`, `revokedAt`), and signs exports using detached JWS (RFC 7797) with cosign-compatible headers.
|
||||||
|
|
||||||
**Export surfaces** (deterministic output, suitable for Offline Kit):
|
**Export surfaces** (deterministic output, suitable for Offline Kit):
|
||||||
|
|
||||||
@@ -378,7 +378,7 @@ Audit events now include `airgap.sealed=<state>` where `<state>` is `failure:<co
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Root | `issuer` | Absolute HTTPS issuer advertised to clients. | Required. Loopback HTTP allowed only for development. |
|
| Root | `issuer` | Absolute HTTPS issuer advertised to clients. | Required. Loopback HTTP allowed only for development. |
|
||||||
| Tokens | `accessTokenLifetime`, `refreshTokenLifetime`, etc. | Lifetimes for each grant (access, refresh, device, authorization code, identity). | Enforced during issuance; persisted on each token document. |
|
| Tokens | `accessTokenLifetime`, `refreshTokenLifetime`, etc. | Lifetimes for each grant (access, refresh, device, authorization code, identity). | Enforced during issuance; persisted on each token document. |
|
||||||
| Storage | `storage.connectionString` | MongoDB connection string. | Required even for tests; offline kits ship snapshots for seeding. |
|
| Storage | `storage.connectionString` | PostgreSQL connection string. | Required even for tests; offline kits ship snapshots for seeding. |
|
||||||
| Signing | `signing.enabled` | Enable JWKS/revocation signing. | Disable only for development. |
|
| Signing | `signing.enabled` | Enable JWKS/revocation signing. | Disable only for development. |
|
||||||
| Signing | `signing.algorithm` | Signing algorithm identifier. | Currently ES256; additional curves can be wired through crypto providers. |
|
| Signing | `signing.algorithm` | Signing algorithm identifier. | Currently ES256; additional curves can be wired through crypto providers. |
|
||||||
| Signing | `signing.keySource` | Loader identifier (`file`, `vault`, custom). | Determines which `IAuthoritySigningKeySource` resolves keys. |
|
| Signing | `signing.keySource` | Loader identifier (`file`, `vault`, custom). | Determines which `IAuthoritySigningKeySource` resolves keys. |
|
||||||
@@ -555,7 +555,7 @@ POST /internal/service-accounts/{accountId}/revocations
|
|||||||
|
|
||||||
Requests must include the bootstrap API key header (`X-StellaOps-Bootstrap-Key`). Listing returns the seeded accounts with their configuration; the token listing call shows currently active delegation tokens (status, client, scopes, actor chain) and the revocation endpoint supports bulk or targeted token revocation with audit logging.
|
Requests must include the bootstrap API key header (`X-StellaOps-Bootstrap-Key`). Listing returns the seeded accounts with their configuration; the token listing call shows currently active delegation tokens (status, client, scopes, actor chain) and the revocation endpoint supports bulk or targeted token revocation with audit logging.
|
||||||
|
|
||||||
Bootstrap seeding reuses the existing Mongo `_id`/`createdAt` values. When Authority restarts with updated configuration it upserts documents without mutating immutable fields, avoiding duplicate or conflicting service-account records.
|
Bootstrap seeding reuses the existing PostgreSQL `id`/`created_at` values. When Authority restarts with updated configuration it upserts rows without mutating immutable fields, avoiding duplicate or conflicting service-account records.
|
||||||
|
|
||||||
**Requesting a delegated token**
|
**Requesting a delegated token**
|
||||||
|
|
||||||
@@ -583,7 +583,7 @@ Optional `delegation_actor` metadata appends an identity to the actor chain:
|
|||||||
Delegated tokens still honour scope validation, tenant enforcement, sender constraints (DPoP/mTLS), and fresh-auth checks.
|
Delegated tokens still honour scope validation, tenant enforcement, sender constraints (DPoP/mTLS), and fresh-auth checks.
|
||||||
|
|
||||||
## 8. Offline & Sovereign Operation
|
## 8. Offline & Sovereign Operation
|
||||||
- **No outbound dependencies:** Authority only contacts MongoDB and local plugins. Discovery and JWKS are cached by clients with offline tolerances (`AllowOfflineCacheFallback`, `OfflineCacheTolerance`). Operators should mirror these responses for air-gapped use.
|
- **No outbound dependencies:** Authority only contacts PostgreSQL and local plugins. Discovery and JWKS are cached by clients with offline tolerances (`AllowOfflineCacheFallback`, `OfflineCacheTolerance`). Operators should mirror these responses for air-gapped use.
|
||||||
- **Structured logging:** Every revocation export, signing rotation, bootstrap action, and token issuance emits structured logs with `traceId`, `client_id`, `subjectId`, and `network.remoteIp` where applicable. Mirror logs to your SIEM to retain audit trails without central connectivity.
|
- **Structured logging:** Every revocation export, signing rotation, bootstrap action, and token issuance emits structured logs with `traceId`, `client_id`, `subjectId`, and `network.remoteIp` where applicable. Mirror logs to your SIEM to retain audit trails without central connectivity.
|
||||||
- **Determinism:** Sorting rules in token and revocation exports guarantee byte-for-byte identical artefacts given the same datastore state. Hashes and signatures remain stable across machines.
|
- **Determinism:** Sorting rules in token and revocation exports guarantee byte-for-byte identical artefacts given the same datastore state. Hashes and signatures remain stable across machines.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Data Schemas & Persistence Contracts
|
# Data Schemas & Persistence Contracts
|
||||||
|
|
||||||
*Audience* – backend developers, plug‑in authors, DB admins.
|
*Audience* – backend developers, plug‑in authors, DB admins.
|
||||||
*Scope* – describes **Redis**, **MongoDB** (optional), and on‑disk blob shapes that power Stella Ops.
|
*Scope* – describes **Redis**, **PostgreSQL**, and on‑disk blob shapes that power Stella Ops.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ Merging logic inside `scanning` module stitches new data onto the cached full SB
|
|||||||
| `layers:<digest>` | set | 90d | Layers already possessing SBOMs (delta cache) |
|
| `layers:<digest>` | set | 90d | Layers already possessing SBOMs (delta cache) |
|
||||||
| `policy:active` | string | ∞ | YAML **or** Rego ruleset |
|
| `policy:active` | string | ∞ | YAML **or** Rego ruleset |
|
||||||
| `quota:<token>` | string | *until next UTC midnight* | Per‑token scan counter for Free tier ({{ quota_token }} scans). |
|
| `quota:<token>` | string | *until next UTC midnight* | Per‑token scan counter for Free tier ({{ quota_token }} scans). |
|
||||||
| `policy:history` | list | ∞ | Change audit IDs (see Mongo) |
|
| `policy:history` | list | ∞ | Change audit IDs (see PostgreSQL) |
|
||||||
| `feed:nvd:json` | string | 24h | Normalised feed snapshot |
|
| `feed:nvd:json` | string | 24h | Normalised feed snapshot |
|
||||||
| `locator:<imageDigest>` | string | 30d | Maps image digest → sbomBlobId |
|
| `locator:<imageDigest>` | string | 30d | Maps image digest → sbomBlobId |
|
||||||
| `metrics:…` | various | — | Prom / OTLP runtime metrics |
|
| `metrics:…` | various | — | Prom / OTLP runtime metrics |
|
||||||
@@ -73,16 +73,16 @@ Merging logic inside `scanning` module stitches new data onto the cached full SB
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3 MongoDB Collections (Optional)
|
## 3 PostgreSQL Tables
|
||||||
|
|
||||||
Only enabled when `MONGO_URI` is supplied (for long‑term audit).
|
PostgreSQL is the canonical persistent store for long-term audit and history.
|
||||||
|
|
||||||
| Collection | Shape (summary) | Indexes |
|
| Table | Shape (summary) | Indexes |
|
||||||
|--------------------|------------------------------------------------------------|-------------------------------------|
|
|--------------------|------------------------------------------------------------|-------------------------------------|
|
||||||
| `sbom_history` | Wrapper JSON + `replaceTs` on overwrite | `{imageDigest}` `{created}` |
|
| `sbom_history` | Wrapper JSON + `replace_ts` on overwrite | `(image_digest)` `(created)` |
|
||||||
| `policy_versions` | `{_id, yaml, rego, authorId, created}` | `{created}` |
|
| `policy_versions` | `{id, yaml, rego, author_id, created}` | `(created)` |
|
||||||
| `attestations` ⭑ | SLSA provenance doc + Rekor log pointer | `{imageDigest}` |
|
| `attestations` ⭑ | SLSA provenance doc + Rekor log pointer | `(image_digest)` |
|
||||||
| `audit_log` | Fully rendered RFC 5424 entries (UI & CLI actions) | `{userId}` `{ts}` |
|
| `audit_log` | Fully rendered RFC 5424 entries (UI & CLI actions) | `(user_id)` `(ts)` |
|
||||||
|
|
||||||
Schema detail for **policy_versions**:
|
Schema detail for **policy_versions**:
|
||||||
|
|
||||||
@@ -99,15 +99,15 @@ Samples live under `samples/api/scheduler/` (e.g., `schedule.json`, `run.json`,
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.1 Scheduler Sprints 16 Artifacts
|
### 3.1 Scheduler Sprints 16 Artifacts
|
||||||
|
|
||||||
**Collections.** `schedules`, `runs`, `impact_snapshots`, `audit` (module‑local). All documents reuse the canonical JSON emitted by `StellaOps.Scheduler.Models` so agents and fixtures remain deterministic.
|
**Tables.** `schedules`, `runs`, `impact_snapshots`, `audit` (module-local). All rows use the canonical JSON emitted by `StellaOps.Scheduler.Models` so agents and fixtures remain deterministic.
|
||||||
|
|
||||||
#### 3.1.1 Schedule (`schedules`)
|
#### 3.1.1 Schedule (`schedules`)
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"_id": "sch_20251018a",
|
"id": "sch_20251018a",
|
||||||
"tenantId": "tenant-alpha",
|
"tenantId": "tenant-alpha",
|
||||||
"name": "Nightly Prod",
|
"name": "Nightly Prod",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -468,7 +468,7 @@ Planned for Q1‑2026 (kept here for early plug‑in authors).
|
|||||||
* `actions[].throttle` serialises as ISO 8601 duration (`PT5M`), mirroring worker backoff guardrails.
|
* `actions[].throttle` serialises as ISO 8601 duration (`PT5M`), mirroring worker backoff guardrails.
|
||||||
* `vex` gates let operators exclude accepted/not‑affected justifications; omit the block to inherit default behaviour.
|
* `vex` gates let operators exclude accepted/not‑affected justifications; omit the block to inherit default behaviour.
|
||||||
* Use `StellaOps.Notify.Models.NotifySchemaMigration.UpgradeRule(JsonNode)` when deserialising legacy payloads that might lack `schemaVersion` or retain older revisions.
|
* Use `StellaOps.Notify.Models.NotifySchemaMigration.UpgradeRule(JsonNode)` when deserialising legacy payloads that might lack `schemaVersion` or retain older revisions.
|
||||||
* Soft deletions persist `deletedAt` in Mongo (and disable the rule); repository queries automatically filter them.
|
* Soft deletions persist `deletedAt` in PostgreSQL (and disable the rule); repository queries automatically filter them.
|
||||||
|
|
||||||
### 6.2 Channel highlights (`notify-channel@1`)
|
### 6.2 Channel highlights (`notify-channel@1`)
|
||||||
|
|
||||||
@@ -523,10 +523,10 @@ Integration tests can embed the sample fixtures to guarantee deterministic seria
|
|||||||
|
|
||||||
## 7 Migration Notes
|
## 7 Migration Notes
|
||||||
|
|
||||||
1. **Add `format` column** to existing SBOM wrappers; default to `trivy-json-v2`.
|
1. **Add `format` column** to existing SBOM wrappers; default to `trivy-json-v2`.
|
||||||
2. **Populate `layers` & `partial`** via backfill script (ship with `stellopsctl migrate` wizard).
|
2. **Populate `layers` & `partial`** via backfill script (ship with `stellopsctl migrate` wizard).
|
||||||
3. Policy YAML previously stored in Redis → copy to Mongo if persistence enabled.
|
3. Policy YAML previously stored in Redis → copy to PostgreSQL if persistence enabled.
|
||||||
4. Prepare `attestations` collection (empty) – safe to create in advance.
|
4. Prepare `attestations` table (empty) – safe to create in advance.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ open a PR and append it alphabetically.*
|
|||||||
| **Digest (image)** | SHA‑256 hash uniquely identifying a container image or layer. | Pin digests for reproducible builds |
|
| **Digest (image)** | SHA‑256 hash uniquely identifying a container image or layer. | Pin digests for reproducible builds |
|
||||||
| **Docker‑in‑Docker (DinD)** | Running Docker daemon inside a CI container. | Used in GitHub / GitLab recipes |
|
| **Docker‑in‑Docker (DinD)** | Running Docker daemon inside a CI container. | Used in GitHub / GitLab recipes |
|
||||||
| **DTO** | *Data Transfer Object* – C# record serialised to JSON. | Schemas in doc 11 |
|
| **DTO** | *Data Transfer Object* – C# record serialised to JSON. | Schemas in doc 11 |
|
||||||
| **Concelier** | Vulnerability ingest/merge/export service consolidating OVN, GHSA, NVD 2.0, CNNVD, CNVD, ENISA, JVN and BDU feeds into the canonical MongoDB store and export artifacts. | Cron default `0 1 * * *` |
|
| **Concelier** | Vulnerability ingest/merge/export service consolidating OVN, GHSA, NVD 2.0, CNNVD, CNVD, ENISA, JVN and BDU feeds into the canonical PostgreSQL store and export artifacts. | Cron default `0 1 * * *` |
|
||||||
| **FSTEC** | Russian regulator issuing SOBIT certificates. | Pro GA target |
|
| **FSTEC** | Russian regulator issuing SOBIT certificates. | Pro GA target |
|
||||||
| **Gitea** | Self‑hosted Git service – mirrors GitHub repo. | OSS hosting |
|
| **Gitea** | Self‑hosted Git service – mirrors GitHub repo. | OSS hosting |
|
||||||
| **GOST TLS** | TLS cipher‑suites defined by Russian GOST R 34.10‑2012 / 34.11‑2012. | Provided by `OpenSslGost` or CryptoPro |
|
| **GOST TLS** | TLS cipher‑suites defined by Russian GOST R 34.10‑2012 / 34.11‑2012. | Provided by `OpenSslGost` or CryptoPro |
|
||||||
@@ -53,7 +53,7 @@ open a PR and append it alphabetically.*
|
|||||||
| **Hyperfine** | CLI micro‑benchmark tool used in Performance Workbook. | Outputs CSV |
|
| **Hyperfine** | CLI micro‑benchmark tool used in Performance Workbook. | Outputs CSV |
|
||||||
| **JWT** | *JSON Web Token* – bearer auth token issued by OpenIddict. | Scope `scanner`, `admin`, `ui` |
|
| **JWT** | *JSON Web Token* – bearer auth token issued by OpenIddict. | Scope `scanner`, `admin`, `ui` |
|
||||||
| **K3s / RKE2** | Lightweight Kubernetes distributions (Rancher). | Supported in K8s guide |
|
| **K3s / RKE2** | Lightweight Kubernetes distributions (Rancher). | Supported in K8s guide |
|
||||||
| **Kubernetes NetworkPolicy** | K8s resource controlling pod traffic. | Redis/Mongo isolation |
|
| **Kubernetes NetworkPolicy** | K8s resource controlling pod traffic. | Redis/PostgreSQL isolation |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ open a PR and append it alphabetically.*
|
|||||||
|
|
||||||
| Term | Definition | Notes |
|
| Term | Definition | Notes |
|
||||||
|------|------------|-------|
|
|------|------------|-------|
|
||||||
| **Mongo (optional)** | Document DB storing > 180 day history and audit logs. | Off by default in Core |
|
| **PostgreSQL** | Relational DB storing history and audit logs. | Required for production |
|
||||||
| **Mute rule** | JSON object that suppresses specific CVEs until expiry. | Schema `mute-rule‑1.json` |
|
| **Mute rule** | JSON object that suppresses specific CVEs until expiry. | Schema `mute-rule‑1.json` |
|
||||||
| **NVD** | US‑based *National Vulnerability Database*. | Primary CVE source |
|
| **NVD** | US‑based *National Vulnerability Database*. | Primary CVE source |
|
||||||
| **ONNX** | Portable neural‑network model format; used by AIRE. | Runs in‑process |
|
| **ONNX** | Portable neural‑network model format; used by AIRE. | Runs in‑process |
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
```
|
```
|
||||||
|
|
||||||
No dedicated “Redis” or “Mongo” sub‑nets are declared; the single bridge network suffices for the default stack.
|
No dedicated "Redis" or "PostgreSQL" sub-nets are declared; the single bridge network suffices for the default stack.
|
||||||
|
|
||||||
### 3.2 Kubernetes deployment highlights
|
### 3.2 Kubernetes deployment highlights
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ Optionally add CosignVerified=true label enforced by an admission controller (e.
|
|||||||
| Plane | Recommendation |
|
| Plane | Recommendation |
|
||||||
| ------------------ | -------------------------------------------------------------------------- |
|
| ------------------ | -------------------------------------------------------------------------- |
|
||||||
| North‑south | Terminate TLS 1.2+ (OpenSSL‑GOST default). Use LetsEncrypt or internal CA. |
|
| North‑south | Terminate TLS 1.2+ (OpenSSL‑GOST default). Use LetsEncrypt or internal CA. |
|
||||||
| East‑west | Compose bridge or K8s ClusterIP only; no public Redis/Mongo ports. |
|
| East-west | Compose bridge or K8s ClusterIP only; no public Redis/PostgreSQL ports. |
|
||||||
| Ingress controller | Limit methods to GET, POST, PATCH (no TRACE). |
|
| Ingress controller | Limit methods to GET, POST, PATCH (no TRACE). |
|
||||||
| Rate‑limits | 40 rps default; tune ScannerPool.Workers and ingress limit‑req to match. |
|
| Rate‑limits | 40 rps default; tune ScannerPool.Workers and ingress limit‑req to match. |
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ contributors who need to extend coverage or diagnose failures.
|
|||||||
| **1. Unit** | `xUnit` (<code>dotnet test</code>) | `*.Tests.csproj` | per PR / push |
|
| **1. Unit** | `xUnit` (<code>dotnet test</code>) | `*.Tests.csproj` | per PR / push |
|
||||||
| **2. Property‑based** | `FsCheck` | `SbomPropertyTests` | per PR |
|
| **2. Property‑based** | `FsCheck` | `SbomPropertyTests` | per PR |
|
||||||
| **3. Integration (API)** | `Testcontainers` suite | `test/Api.Integration` | per PR + nightly |
|
| **3. Integration (API)** | `Testcontainers` suite | `test/Api.Integration` | per PR + nightly |
|
||||||
| **4. Integration (DB-merge)** | in-memory Mongo + Redis | `Concelier.Integration` (vulnerability ingest/merge/export service) | per PR |
|
| **4. Integration (DB-merge)** | Testcontainers PostgreSQL + Redis | `Concelier.Integration` (vulnerability ingest/merge/export service) | per PR |
|
||||||
| **5. Contract (gRPC)** | `Buf breaking` | `buf.yaml` files | per PR |
|
| **5. Contract (gRPC)** | `Buf breaking` | `buf.yaml` files | per PR |
|
||||||
| **6. Front‑end unit** | `Jest` | `ui/src/**/*.spec.ts` | per PR |
|
| **6. Front‑end unit** | `Jest` | `ui/src/**/*.spec.ts` | per PR |
|
||||||
| **7. Front‑end E2E** | `Playwright` | `ui/e2e/**` | nightly |
|
| **7. Front‑end E2E** | `Playwright` | `ui/e2e/**` | nightly |
|
||||||
@@ -52,67 +52,36 @@ contributors who need to extend coverage or diagnose failures.
|
|||||||
./scripts/dev-test.sh --full
|
./scripts/dev-test.sh --full
|
||||||
````
|
````
|
||||||
|
|
||||||
The script spins up MongoDB/Redis via Testcontainers and requires:
|
The script spins up PostgreSQL/Redis via Testcontainers and requires:
|
||||||
|
|
||||||
* Docker ≥ 25
|
* Docker ≥ 25
|
||||||
* Node 20 (for Jest/Playwright)
|
* Node 20 (for Jest/Playwright)
|
||||||
|
|
||||||
#### Mongo2Go / OpenSSL shim
|
#### PostgreSQL Testcontainers
|
||||||
|
|
||||||
Multiple suites (Concelier connectors, Excititor worker/WebService, Scheduler)
|
Multiple suites (Concelier connectors, Excititor worker/WebService, Scheduler)
|
||||||
fall back to [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) when a developer
|
use Testcontainers with PostgreSQL for integration tests. If you don't have
|
||||||
does not have a local `mongod` listening on `127.0.0.1:27017`. **This is a
|
Docker available, tests can also run against a local PostgreSQL instance
|
||||||
test-only dependency**: production/dev runtime MongoDB always runs inside the
|
listening on `127.0.0.1:5432`.
|
||||||
compose/k8s network using the standard StellaOps cryptography stack. Modern
|
|
||||||
distros ship OpenSSL 3 by default, so when Mongo2Go starts its embedded
|
|
||||||
`mongod` you **must** expose the legacy OpenSSL 1.1 libraries that binary
|
|
||||||
expects:
|
|
||||||
|
|
||||||
1. From the repo root, export the provided binaries before running any tests:
|
#### Local PostgreSQL helper
|
||||||
|
|
||||||
```bash
|
|
||||||
export LD_LIBRARY_PATH="$(pwd)/tests/native/openssl-1.1/linux-x64:${LD_LIBRARY_PATH:-}"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. (Optional) If you only need the shim for a single command, prefix it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
LD_LIBRARY_PATH="$(pwd)/tests/native/openssl-1.1/linux-x64" \
|
|
||||||
dotnet test src/Concelier/StellaOps.Concelier.sln --nologo
|
|
||||||
```
|
|
||||||
|
|
||||||
3. CI runners or dev containers should either copy
|
|
||||||
`tests/native/openssl-1.1/linux-x64/libcrypto.so.1.1` and `libssl.so.1.1`
|
|
||||||
into a directory that is already on the default library path, or export the
|
|
||||||
`LD_LIBRARY_PATH` value shown above before invoking `dotnet test`.
|
|
||||||
|
|
||||||
The shim lives under `tests/native/openssl-1.1/README.md` with upstream source
|
|
||||||
and licensing details. When the system already has OpenSSL 1.1 installed you
|
|
||||||
can skip this step.
|
|
||||||
|
|
||||||
#### Local Mongo helper
|
|
||||||
|
|
||||||
Some suites (Concelier WebService/Core, Exporter JSON) need a full
|
Some suites (Concelier WebService/Core, Exporter JSON) need a full
|
||||||
`mongod` instance when you want to debug outside of Mongo2Go (for example to
|
PostgreSQL instance when you want to debug or inspect data with `psql`.
|
||||||
inspect data with `mongosh` or pin a specific server version). A thin wrapper
|
A helper script is available under `tools/postgres/local-postgres.sh`:
|
||||||
is available under `tools/mongodb/local-mongo.sh`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# download (cached under .cache/mongodb-local) and start a local replica set
|
# start a local PostgreSQL instance
|
||||||
tools/mongodb/local-mongo.sh start
|
tools/postgres/local-postgres.sh start
|
||||||
|
|
||||||
# reuse an existing data set
|
|
||||||
tools/mongodb/local-mongo.sh restart
|
|
||||||
|
|
||||||
# stop / clean
|
# stop / clean
|
||||||
tools/mongodb/local-mongo.sh stop
|
tools/postgres/local-postgres.sh stop
|
||||||
tools/mongodb/local-mongo.sh clean
|
tools/postgres/local-postgres.sh clean
|
||||||
```
|
```
|
||||||
|
|
||||||
By default the script downloads MongoDB 6.0.16 for Ubuntu 22.04, binds to
|
By default the script uses Docker to run PostgreSQL 16, binds to
|
||||||
`127.0.0.1:27017`, and initialises a single-node replica set called `rs0`. The
|
`127.0.0.1:5432`, and creates a database called `stellaops`. The
|
||||||
current URI is printed on start, e.g.
|
connection string is printed on start and you can export it before
|
||||||
`mongodb://127.0.0.1:27017/?replicaSet=rs0`, and you can export it before
|
|
||||||
running `dotnet test` if a suite supports overriding its connection string.
|
running `dotnet test` if a suite supports overriding its connection string.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ cosign verify-blob \
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
$EDITOR .env
|
$EDITOR .env
|
||||||
|
|
||||||
# 5. Launch databases (MongoDB + Redis)
|
# 5. Launch databases (PostgreSQL + Redis)
|
||||||
docker compose --env-file .env -f docker-compose.infrastructure.yml up -d
|
docker compose --env-file .env -f docker-compose.infrastructure.yml up -d
|
||||||
|
|
||||||
# 6. Launch Stella Ops (first run pulls ~50 MB merged vuln DB)
|
# 6. Launch Stella Ops (first run pulls ~50 MB merged vuln DB)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Snapshot:
|
|||||||
| **Core runtime** | C# 14 on **.NET {{ dotnet }}** |
|
| **Core runtime** | C# 14 on **.NET {{ dotnet }}** |
|
||||||
| **UI stack** | **Angular {{ angular }}** + TailwindCSS |
|
| **UI stack** | **Angular {{ angular }}** + TailwindCSS |
|
||||||
| **Container base** | Distroless glibc (x86‑64 & arm64) |
|
| **Container base** | Distroless glibc (x86‑64 & arm64) |
|
||||||
| **Data stores** | MongoDB 7 (SBOM + findings), Redis 7 (LRU cache + quota) |
|
| **Data stores** | PostgreSQL 7 (SBOM + findings), Redis 7 (LRU cache + quota) |
|
||||||
| **Release integrity** | Cosign‑signed images & TGZ, reproducible build, SPDX 2.3 SBOM |
|
| **Release integrity** | Cosign‑signed images & TGZ, reproducible build, SPDX 2.3 SBOM |
|
||||||
| **Extensibility** | Plug‑ins in any .NET language (restart load); OPA Rego policies |
|
| **Extensibility** | Plug‑ins in any .NET language (restart load); OPA Rego policies |
|
||||||
| **Default quotas** | Anonymous **{{ quota_anon }} scans/day** · JWT **{{ quota_token }}** |
|
| **Default quotas** | Anonymous **{{ quota_anon }} scans/day** · JWT **{{ quota_token }}** |
|
||||||
|
|||||||
@@ -305,10 +305,10 @@ The Offline Kit carries the same helper scripts under `scripts/`:
|
|||||||
|
|
||||||
1. **Duplicate audit:** run
|
1. **Duplicate audit:** run
|
||||||
```bash
|
```bash
|
||||||
mongo concelier ops/devops/scripts/check-advisory-raw-duplicates.js --eval 'var LIMIT=200;'
|
psql -d concelier -f ops/devops/scripts/check-advisory-raw-duplicates.sql -v LIMIT=200
|
||||||
```
|
```
|
||||||
to verify no `(vendor, upstream_id, content_hash, tenant)` conflicts remain before enabling the idempotency index.
|
to verify no `(vendor, upstream_id, content_hash, tenant)` conflicts remain before enabling the idempotency index.
|
||||||
2. **Apply validators:** execute `mongo concelier ops/devops/scripts/apply-aoc-validators.js` (and the Excititor equivalent) with `validationLevel: "moderate"` in maintenance mode.
|
2. **Apply validators:** execute `psql -d concelier -f ops/devops/scripts/apply-aoc-validators.sql` (and the Excititor equivalent) with `validationLevel: "moderate"` in maintenance mode.
|
||||||
3. **Restart Concelier** so migrations `20251028_advisory_raw_idempotency_index` and `20251028_advisory_supersedes_backfill` run automatically. After the restart:
|
3. **Restart Concelier** so migrations `20251028_advisory_raw_idempotency_index` and `20251028_advisory_supersedes_backfill` run automatically. After the restart:
|
||||||
- Confirm `db.advisory` resolves to a view on `advisory_backup_20251028`.
|
- Confirm `db.advisory` resolves to a view on `advisory_backup_20251028`.
|
||||||
- Spot-check a few `advisory_raw` entries to ensure `supersedes` chains are populated deterministically.
|
- Spot-check a few `advisory_raw` entries to ensure `supersedes` chains are populated deterministically.
|
||||||
|
|||||||
603
docs/28_LEGAL_COMPLIANCE.md
Normal file
603
docs/28_LEGAL_COMPLIANCE.md
Normal file
@@ -0,0 +1,603 @@
|
|||||||
|
# Regulator-Grade Threat & Evidence Model
|
||||||
|
|
||||||
|
## Supply-Chain Risk Decisioning Platform (Reference: “Stella Ops”)
|
||||||
|
|
||||||
|
**Document version:** 1.0
|
||||||
|
**Date:** 2025-12-19
|
||||||
|
**Intended audience:** Regulators, third-party auditors, internal security/compliance, and engineering leadership
|
||||||
|
**Scope:** Threat model + evidence model for a platform that ingests SBOM/VEX and other supply-chain signals, produces risk decisions, and preserves an audit-grade evidence trail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose and Objectives
|
||||||
|
|
||||||
|
This document defines:
|
||||||
|
|
||||||
|
1. A **threat model** for a supply-chain risk decisioning platform (“the Platform”) and its critical workflows.
|
||||||
|
2. An **evidence model** describing what records must exist, how they must be protected, and how they must be presented to support regulator-grade auditability and non-repudiation.
|
||||||
|
|
||||||
|
The model is designed to support the supply-chain transparency goals behind SBOM/VEX and secure software development expectations (e.g., SSDF), and to be compatible with supply-chain risk management (C‑SCRM) and control-based assessments (e.g., NIST control catalogs).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Scope, System Boundary, and Assumptions
|
||||||
|
|
||||||
|
### 2.1 In-scope system functions
|
||||||
|
|
||||||
|
The Platform performs the following high-level functions:
|
||||||
|
|
||||||
|
* **Ingest** software transparency artifacts (e.g., SBOMs, VEX documents), scan results, provenance attestations, and policy inputs.
|
||||||
|
* **Normalize** to a canonical internal representation (component identity graph + vulnerability/impact graph).
|
||||||
|
* **Evaluate** with a deterministic policy engine to produce decisions (e.g., allow/deny, risk tier, required remediation).
|
||||||
|
* **Record** an audit-grade evidence package supporting each decision.
|
||||||
|
* **Export** reports and attestations suitable for procurement, regulator review, and downstream consumption.
|
||||||
|
|
||||||
|
### 2.2 Deployment models supported by this model
|
||||||
|
|
||||||
|
This model is written to cover:
|
||||||
|
|
||||||
|
* **On‑prem / air‑gapped** deployments (offline evidence and curated vulnerability feeds).
|
||||||
|
* **Dedicated single-tenant hosted** deployments.
|
||||||
|
* **Multi-tenant SaaS** deployments (requires stronger tenant isolation controls and evidence).
|
||||||
|
|
||||||
|
### 2.3 Core assumptions
|
||||||
|
|
||||||
|
* SBOM is treated as a **formal inventory and relationship record** for components used to build software.
|
||||||
|
* VEX is treated as a **machine-readable assertion** of vulnerability status for a product, including “not affected / affected / fixed / under investigation.”
|
||||||
|
* The Platform must be able to demonstrate **traceability** from decision → inputs → transformations → outputs, and preserve “known unknowns” (explicitly tracked uncertainty).
|
||||||
|
* If the Platform is used in US federal acquisition contexts, it must anticipate evolving SBOM minimum element guidance; CISA’s 2025 SBOM minimum elements draft guidance explicitly aims to update the 2021 NTIA baseline to reflect tooling and maturity improvements. ([Federal Register][1])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Normative and Informative References
|
||||||
|
|
||||||
|
This model is aligned to the concepts and terminology used by the following:
|
||||||
|
|
||||||
|
* **SBOM minimum elements baseline (2021 NTIA)** and the “data fields / automation support / practices and processes” structure.
|
||||||
|
* **CISA 2025 SBOM minimum elements draft guidance** (published for comment; successor guidance to NTIA baseline per the Federal Register notice). ([Federal Register][1])
|
||||||
|
* **VEX overview and statuses** (NTIA one-page summary).
|
||||||
|
* **NIST SSDF** (SP 800‑218; includes recent Rev.1 IPD for SSDF v1.2). ([NIST Computer Security Resource Center][2])
|
||||||
|
* **NIST C‑SCRM guidance** (SP 800‑161 Rev.1). ([NIST Computer Security Resource Center][3])
|
||||||
|
* **NIST security and privacy controls catalog** (SP 800‑53 Rev.5, including its supply chain control family). ([NIST Computer Security Resource Center][4])
|
||||||
|
* **SLSA supply-chain threat model and mitigations** (pipeline threat clustering A–I; verification threats). ([SLSA][5])
|
||||||
|
* **Attestation and transparency building blocks**:
|
||||||
|
|
||||||
|
* in‑toto (supply-chain metadata standard). ([in-toto][6])
|
||||||
|
* DSSE (typed signing envelope to reduce confusion attacks). ([GitHub][7])
|
||||||
|
* Sigstore Rekor (signature transparency log). ([Sigstore][8])
|
||||||
|
* **SBOM and VEX formats**:
|
||||||
|
|
||||||
|
* CycloneDX (ECMA‑424; SBOM/BOM standard). ([GitHub][9])
|
||||||
|
* SPDX (ISO/IEC 5962:2021; SBOM standard). ([ISO][10])
|
||||||
|
* CSAF v2.0 VEX profile (structured security advisories with VEX profile requirements). ([OASIS Documents][11])
|
||||||
|
* OpenVEX (minimal VEX implementation). ([GitHub][12])
|
||||||
|
* **Vulnerability intelligence format**:
|
||||||
|
|
||||||
|
* OSV schema maps vulnerabilities to package versions/commit ranges. ([OSV.dev][13])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. System Overview
|
||||||
|
|
||||||
|
### 4.1 Logical architecture
|
||||||
|
|
||||||
|
**Core components:**
|
||||||
|
|
||||||
|
1. **Ingestion Gateway**
|
||||||
|
|
||||||
|
* Accepts SBOM, VEX, provenance attestations, scan outputs, and configuration inputs.
|
||||||
|
* Performs syntactic validation, content hashing, and initial authenticity checks.
|
||||||
|
|
||||||
|
2. **Normalization & Identity Resolution**
|
||||||
|
|
||||||
|
* Converts formats (SPDX, CycloneDX, proprietary) into a canonical internal model.
|
||||||
|
* Resolves component IDs (purl/CPE/name+version), dependency graph, and artifact digests.
|
||||||
|
|
||||||
|
3. **Evidence Store**
|
||||||
|
|
||||||
|
* Content-addressable object store for raw artifacts plus derived artifacts.
|
||||||
|
* Append-only metadata index (event log) referencing objects by hash.
|
||||||
|
|
||||||
|
4. **Policy & Decision Engine**
|
||||||
|
|
||||||
|
* Deterministic evaluation engine for risk policy.
|
||||||
|
* Produces a decision plus a structured explanation and “unknowns.”
|
||||||
|
|
||||||
|
5. **Attestation & Export Service**
|
||||||
|
|
||||||
|
* Packages decisions and evidence references as signed statements (DSSE/in‑toto compatible). ([GitHub][7])
|
||||||
|
* Optional transparency publication (e.g., Rekor or private transparency log). ([Sigstore][8])
|
||||||
|
|
||||||
|
### 4.2 Trust boundaries
|
||||||
|
|
||||||
|
**Primary trust boundaries:**
|
||||||
|
|
||||||
|
* **TB‑1:** External submitter → Ingestion Gateway
|
||||||
|
* **TB‑2:** Customer environment → Platform environment (for hosted)
|
||||||
|
* **TB‑3:** Policy authoring plane → decision execution plane
|
||||||
|
* **TB‑4:** Evidence Store (write path) → Evidence Store (read/audit path)
|
||||||
|
* **TB‑5:** Platform signing keys / KMS / HSM boundary → application services
|
||||||
|
* **TB‑6:** External intelligence feeds (vulnerability databases, advisories) → internal curated dataset
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Threat Model
|
||||||
|
|
||||||
|
### 5.1 Methodology
|
||||||
|
|
||||||
|
This model combines:
|
||||||
|
|
||||||
|
* **STRIDE** for platform/system threats (spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege).
|
||||||
|
* **SLSA threat clustering (A–I)** for supply-chain pipeline threats relevant to artifacts being evaluated and to the Platform’s own supply chain. ([SLSA][5])
|
||||||
|
|
||||||
|
Threats are evaluated as: **Impact × Likelihood**, with controls grouped into **Prevent / Detect / Respond**.
|
||||||
|
|
||||||
|
### 5.2 Assets (what must be protected)
|
||||||
|
|
||||||
|
**A‑1: Decision integrity assets**
|
||||||
|
|
||||||
|
* Final decision outputs (allow/deny, risk scores, exceptions).
|
||||||
|
* Decision explanations and traces.
|
||||||
|
* Policy rules and parameters (including weights/thresholds).
|
||||||
|
|
||||||
|
**A‑2: Evidence integrity assets**
|
||||||
|
|
||||||
|
* Original input artifacts (SBOM, VEX, provenance, scan outputs).
|
||||||
|
* Derived artifacts (normalized graphs, reachability proofs, diff outputs).
|
||||||
|
* Evidence index and chain-of-custody metadata.
|
||||||
|
|
||||||
|
**A‑3: Confidentiality assets**
|
||||||
|
|
||||||
|
* Customer source code and binaries (if ingested).
|
||||||
|
* Private SBOMs/VEX that reveal internal dependencies.
|
||||||
|
* Customer environment identifiers and incident details.
|
||||||
|
|
||||||
|
**A‑4: Trust anchor assets**
|
||||||
|
|
||||||
|
* Signing keys (decision attestations, evidence hashes, transparency submissions).
|
||||||
|
* Root of trust configuration (certificate chains, allowed issuers).
|
||||||
|
* Time source and timestamping configuration.
|
||||||
|
|
||||||
|
**A‑5: Availability assets**
|
||||||
|
|
||||||
|
* Evidence store accessibility.
|
||||||
|
* Policy engine uptime.
|
||||||
|
* Interface endpoints and batch processing capacity.
|
||||||
|
|
||||||
|
### 5.3 Threat actors
|
||||||
|
|
||||||
|
* **External attacker** seeking to:
|
||||||
|
|
||||||
|
* Push a malicious component into the supply chain,
|
||||||
|
* Falsify transparency artifacts,
|
||||||
|
* Or compromise the Platform to manipulate decisions/evidence.
|
||||||
|
|
||||||
|
* **Malicious insider** (customer or Platform operator) seeking to:
|
||||||
|
|
||||||
|
* Hide vulnerable components,
|
||||||
|
* Suppress detections,
|
||||||
|
* Or retroactively alter records.
|
||||||
|
|
||||||
|
* **Compromised CI/CD or registry** affecting provenance and artifact integrity (SLSA build/distribution threats). ([SLSA][5])
|
||||||
|
|
||||||
|
* **Curious but non-malicious parties** who should not gain access to sensitive SBOM details (confidentiality and least privilege).
|
||||||
|
|
||||||
|
### 5.4 Key threat scenarios and required mitigations
|
||||||
|
|
||||||
|
Below are regulator-relevant threats that materially affect auditability and trust.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T‑1: Spoofing of submitter identity (STRIDE: S)
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
An attacker submits forged SBOM/VEX/provenance claiming to be a trusted supplier.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
Decisions are based on untrusted artifacts; audit trail is misleading.
|
||||||
|
|
||||||
|
**Controls (shall):**
|
||||||
|
|
||||||
|
* Enforce strong authentication for ingestion (mTLS/OIDC + scoped tokens).
|
||||||
|
* Require artifact signatures for “trusted supplier” classification; verify signature chain and allowed issuers.
|
||||||
|
* Bind submitter identity to evidence record at ingestion time (AU-style accountability expectations). ([NIST Computer Security Resource Center][4])
|
||||||
|
|
||||||
|
**Evidence required:**
|
||||||
|
|
||||||
|
* Auth event logs (who/when/what).
|
||||||
|
* Signature verification results (certificate chain, key ID).
|
||||||
|
* Hash of submitted artifact (content-addressable ID).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T‑2: Tampering with stored evidence (STRIDE: T)
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
An attacker modifies an SBOM, a reachability artifact, or an evaluation trace after the decision, to change what regulators/auditors see.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
Non-repudiation and auditability collapse; regulator confidence lost.
|
||||||
|
|
||||||
|
**Controls (shall):**
|
||||||
|
|
||||||
|
* Evidence objects stored as **content-addressed blobs** (hash = identifier).
|
||||||
|
* **Append-only metadata log** referencing evidence hashes (no in-place edits).
|
||||||
|
* Cryptographically sign the “evidence package manifest” for each decision.
|
||||||
|
* Optional transparency log anchoring (public Rekor or private equivalent). ([Sigstore][8])
|
||||||
|
|
||||||
|
**Evidence required:**
|
||||||
|
|
||||||
|
* Object store digest list and integrity proofs.
|
||||||
|
* Signed manifest (DSSE envelope recommended to bind payload type). ([GitHub][7])
|
||||||
|
* Inclusion proof or anchor reference if using a transparency log. ([Sigstore][8])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T‑3: Repudiation of decisions or approvals (STRIDE: R)
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
A policy author or approver claims they did not approve a policy change or a high-risk exception.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
Weak governance; cannot establish accountability.
|
||||||
|
|
||||||
|
**Controls (shall):**
|
||||||
|
|
||||||
|
* Two-person approval workflow for policy changes and exceptions.
|
||||||
|
* Immutable audit logs capturing: identity, time, action, object, outcome (aligned with audit record content expectations). ([NIST Computer Security Resource Center][4])
|
||||||
|
* Sign policy versions and exception artifacts.
|
||||||
|
|
||||||
|
**Evidence required:**
|
||||||
|
|
||||||
|
* Signed policy version artifacts.
|
||||||
|
* Approval records linked to identity provider logs.
|
||||||
|
* Change diff + rationale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T‑4: Information disclosure via SBOM/VEX outputs (STRIDE: I)
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
An auditor-facing export inadvertently reveals proprietary component lists, internal repo URLs, or sensitive dependency relationships.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
Confidentiality breach; contractual/regulatory exposure; risk of targeted exploitation.
|
||||||
|
|
||||||
|
**Controls (shall):**
|
||||||
|
|
||||||
|
* Role-based access control for evidence and exports.
|
||||||
|
* Redaction profiles (“regulator view,” “customer view,” “internal view”) with deterministic transformation rules.
|
||||||
|
* Separate encryption domains (tenant-specific keys).
|
||||||
|
* Secure export channels; optional offline export bundles for air-gapped review.
|
||||||
|
|
||||||
|
**Evidence required:**
|
||||||
|
|
||||||
|
* Access-control policy snapshots and enforcement logs.
|
||||||
|
* Export redaction policy version and redaction transformation log.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T‑5: Denial of service against evaluation pipeline (STRIDE: D)
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
A malicious party floods ingestion endpoints or submits pathological SBOM graphs causing excessive compute and preventing timely decisions.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
Availability and timeliness failures; missed gates/releases.
|
||||||
|
|
||||||
|
**Controls (shall):**
|
||||||
|
|
||||||
|
* Input size limits, graph complexity limits, and bounded parsing.
|
||||||
|
* Quotas and rate limiting (per tenant or per submitter).
|
||||||
|
* Separate async pipeline for heavy analysis; protect decision critical path.
|
||||||
|
|
||||||
|
**Evidence required:**
|
||||||
|
|
||||||
|
* Rate limit logs and rejection metrics.
|
||||||
|
* Capacity monitoring evidence (for availability obligations).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T‑6: Elevation of privilege to policy/admin plane (STRIDE: E)
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
An attacker compromises a service account and gains ability to modify policy, disable controls, or access evidence across tenants.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
Complete compromise of decision integrity and confidentiality.
|
||||||
|
|
||||||
|
**Controls (shall):**
|
||||||
|
|
||||||
|
* Strict separation of duties: policy authoring vs execution vs auditing.
|
||||||
|
* Least privilege IAM for services (scoped tokens; short-lived credentials).
|
||||||
|
* Strong hardening of signing key boundary (KMS/HSM boundary; key usage constrained by attestation policy).
|
||||||
|
|
||||||
|
**Evidence required:**
|
||||||
|
|
||||||
|
* IAM policy snapshots and access review logs.
|
||||||
|
* Key management logs (rotation, access, signing operations).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T‑7: Supply-chain compromise of artifacts being evaluated (SLSA A–I)
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
The software under evaluation is compromised via source manipulation, build pipeline compromise, dependency compromise, or distribution channel compromise.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
Customer receives malicious/vulnerable software; Platform may miss it without sufficient provenance and identity proofs.
|
||||||
|
|
||||||
|
**Controls (should / shall depending on assurance target):**
|
||||||
|
|
||||||
|
* Require/provide provenance attestations and verify them against expectations (SLSA-style verification). ([SLSA][5])
|
||||||
|
* Verify artifact identity by digest and signed provenance.
|
||||||
|
* Enforce policy constraints for “minimum acceptable provenance” for high-criticality deployments.
|
||||||
|
|
||||||
|
**Evidence required:**
|
||||||
|
|
||||||
|
* Verified provenance statement(s) (in‑toto compatible) describing how artifacts were produced. ([in-toto][6])
|
||||||
|
* Build and publication step attestations, with cryptographic binding to artifact digests.
|
||||||
|
* Evidence of expectation configuration and verification outcomes (SLSA “verification threats” include tampering with expectations). ([SLSA][5])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T‑8: Vulnerability intelligence poisoning / drift
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
The Platform’s vulnerability feed is manipulated or changes over time such that a past decision cannot be reproduced.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
Regulator cannot validate basis of decision at time-of-decision; inconsistent results over time.
|
||||||
|
|
||||||
|
**Controls (shall):**
|
||||||
|
|
||||||
|
* Snapshot all external intelligence inputs used in an evaluation (source + version + timestamp + digest).
|
||||||
|
* In offline mode, use curated signed feed bundles and record their hashes.
|
||||||
|
* Maintain deterministic evaluation by tying each decision to the exact dataset snapshot.
|
||||||
|
|
||||||
|
**Evidence required:**
|
||||||
|
|
||||||
|
* Feed snapshot manifest (hashes, source identifiers, effective date range).
|
||||||
|
* Verification record of feed authenticity (signature or trust chain).
|
||||||
|
|
||||||
|
(OSV schema design, for example, emphasizes mapping to precise versions/commits; this supports deterministic matching when captured correctly.) ([OSV.dev][13])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Evidence Model
|
||||||
|
|
||||||
|
### 6.1 Evidence principles (regulator-grade properties)
|
||||||
|
|
||||||
|
All evidence objects in the Platform **shall** satisfy:
|
||||||
|
|
||||||
|
1. **Integrity:** Evidence cannot be modified without detection (hashing + immutability).
|
||||||
|
2. **Authenticity:** Evidence is attributable to its source (signatures, verified identity).
|
||||||
|
3. **Traceability:** Decisions link to specific input artifacts and transformation steps.
|
||||||
|
4. **Reproducibility:** A decision can be replayed deterministically given the same inputs and dataset snapshots.
|
||||||
|
5. **Non‑repudiation:** Critical actions (policy updates, exceptions, decision signing) are attributable and auditable.
|
||||||
|
6. **Confidentiality:** Sensitive evidence is access-controlled and export-redactable.
|
||||||
|
7. **Completeness with “Known Unknowns”:** The Platform explicitly records unknown or unresolved data elements rather than silently dropping them.
|
||||||
|
|
||||||
|
### 6.2 Evidence object taxonomy
|
||||||
|
|
||||||
|
The Platform should model evidence as a graph of typed objects.
|
||||||
|
|
||||||
|
**E‑1: Input artifact evidence**
|
||||||
|
|
||||||
|
* SBOM documents (SPDX/CycloneDX), including dependency relationships and identifiers.
|
||||||
|
* VEX documents (CSAF VEX, OpenVEX, CycloneDX VEX) with vulnerability status assertions.
|
||||||
|
* Provenance/attestations (SLSA-style provenance, in‑toto statements). ([SLSA][14])
|
||||||
|
* Scan outputs (SCA, container/image scans, static/dynamic analysis outputs).
|
||||||
|
|
||||||
|
**E‑2: Normalization and resolution evidence**
|
||||||
|
|
||||||
|
* Parsing/validation logs (schema validation results; warnings).
|
||||||
|
* Canonical “component graph” and “vulnerability mapping” artifacts.
|
||||||
|
* Identity resolution records: how name/version/IDs were mapped.
|
||||||
|
|
||||||
|
**E‑3: Analysis evidence**
|
||||||
|
|
||||||
|
* Vulnerability match outputs (CVE/OSV IDs, version ranges, scoring).
|
||||||
|
* Reachability artifacts (if supported): call graph results, dependency path proofs, or “not reachable” justification artifacts.
|
||||||
|
* Diff artifacts: changes between SBOM versions (component added/removed/upgraded; license changes; vulnerability deltas).
|
||||||
|
|
||||||
|
**E‑4: Policy and governance evidence**
|
||||||
|
|
||||||
|
* Policy definitions and versions (rules, thresholds).
|
||||||
|
* Exception records with approver identity and rationale.
|
||||||
|
* Approval workflow records and change control logs.
|
||||||
|
|
||||||
|
**E‑5: Decision evidence**
|
||||||
|
|
||||||
|
* Decision outcome (e.g., pass/fail/risk tier).
|
||||||
|
* Deterministic decision trace (which rules fired, which inputs were used).
|
||||||
|
* Unknowns/assumptions list.
|
||||||
|
* Signed decision statement + manifest of linked evidence objects.
|
||||||
|
|
||||||
|
**E‑6: Operational security evidence**
|
||||||
|
|
||||||
|
* Authentication/authorization logs.
|
||||||
|
* Key management and signing logs.
|
||||||
|
* Evidence store integrity monitoring logs.
|
||||||
|
* Incident response records (if applicable).
|
||||||
|
|
||||||
|
### 6.3 Common metadata schema (minimum required fields)
|
||||||
|
|
||||||
|
Every evidence object **shall** include at least:
|
||||||
|
|
||||||
|
* **EvidenceID:** content-addressable ID (e.g., SHA‑256 digest of canonical bytes).
|
||||||
|
* **EvidenceType:** enumerated type (SBOM, VEX, Provenance, ScanResult, Policy, Decision, etc.).
|
||||||
|
* **Producer:** tool/system identity that generated the evidence (name, version).
|
||||||
|
* **Timestamp:** time created + time ingested (with time source information).
|
||||||
|
* **Subject:** the software artifact(s) the evidence applies to (artifact digest(s), package IDs).
|
||||||
|
* **Chain links:** parent EvidenceIDs (inputs/precedents).
|
||||||
|
* **Tenant / confidentiality labels:** access classification and redaction profile applicability.
|
||||||
|
|
||||||
|
This aligns with the SBOM minimum elements emphasis on baseline data, automation support, and practices/processes including known unknowns and access control.
|
||||||
|
|
||||||
|
### 6.4 Evidence integrity and signing
|
||||||
|
|
||||||
|
**6.4.1 Hashing and immutability**
|
||||||
|
|
||||||
|
* Raw evidence artifacts shall be stored as immutable blobs.
|
||||||
|
* Derived evidence shall be stored as separate immutable blobs.
|
||||||
|
* The evidence index shall be append-only and reference blobs by hash.
|
||||||
|
|
||||||
|
**6.4.2 Signed envelopes and type binding**
|
||||||
|
|
||||||
|
* For high-assurance use, the Platform shall sign:
|
||||||
|
|
||||||
|
* Decision statements,
|
||||||
|
* Per-decision evidence manifests,
|
||||||
|
* Policy versions and exception approvals.
|
||||||
|
* Use a signing format that binds the **payload type** to the signature to reduce confusion attacks; DSSE is explicitly designed to authenticate both message and type. ([GitHub][7])
|
||||||
|
|
||||||
|
**6.4.3 Attestation model**
|
||||||
|
|
||||||
|
* Use in‑toto-compatible statements to standardize subjects (artifact digests) and predicates (decision, SBOM, provenance). ([in-toto][6])
|
||||||
|
* CycloneDX explicitly recognizes an official predicate type for BOM attestations, which can be leveraged for standardized evidence typing. ([CycloneDX][15])
|
||||||
|
|
||||||
|
**6.4.4 Transparency anchoring (optional but strong for regulators)**
|
||||||
|
|
||||||
|
* Publish signed decision manifests to a transparency log to provide additional tamper-evidence and public verifiability (or use a private transparency log for sensitive contexts). Rekor is Sigstore’s signature transparency log service. ([Sigstore][8])
|
||||||
|
|
||||||
|
### 6.5 Evidence for VEX and “not affected” assertions
|
||||||
|
|
||||||
|
Because VEX is specifically intended to prevent wasted effort on non-exploitable upstream vulnerabilities and is machine-readable for automation, the Platform must treat VEX as first-class evidence.
|
||||||
|
|
||||||
|
Minimum required behaviors:
|
||||||
|
|
||||||
|
* Maintain the original VEX document and signature (if present).
|
||||||
|
* Track the VEX **status** (not affected / affected / fixed / under investigation) for each vulnerability–product association.
|
||||||
|
* If the Platform generates VEX-like conclusions (e.g., “not affected” based on reachability), it shall:
|
||||||
|
|
||||||
|
* Record the analytical basis as evidence (reachability proof, configuration assumptions),
|
||||||
|
* Mark the assertion as Platform-authored (not vendor-authored),
|
||||||
|
* Provide an explicit confidence level and unknowns.
|
||||||
|
|
||||||
|
For CSAF-based VEX documents, the Platform should validate conformance to the CSAF VEX profile requirements. ([OASIS Documents][11])
|
||||||
|
|
||||||
|
### 6.6 Reproducibility and determinism controls
|
||||||
|
|
||||||
|
Each decision must be reproducible. Therefore each decision record **shall** include:
|
||||||
|
|
||||||
|
* **Algorithm version** (policy engine + scoring logic version).
|
||||||
|
* **Policy version** and policy hash.
|
||||||
|
* **All inputs by digest** (SBOM/VEX/provenance/scan outputs).
|
||||||
|
* **External dataset snapshot identifiers** (vulnerability DB snapshot digest(s), advisory feeds, scoring inputs).
|
||||||
|
* **Execution environment ID** (runtime build of the Platform component that evaluated).
|
||||||
|
* **Determinism proof fields** (e.g., “random seed = fixed/none”, stable sort order used, canonicalization rules used).
|
||||||
|
|
||||||
|
This supports regulator expectations for traceability and for consistent evaluation in supply-chain risk management programs. ([NIST Computer Security Resource Center][3])
|
||||||
|
|
||||||
|
### 6.7 Retention, legal hold, and audit packaging
|
||||||
|
|
||||||
|
**Retention (shall):**
|
||||||
|
|
||||||
|
* Evidence packages supporting released decisions must be retained for a defined minimum period (set by sector/regulator/contract), with:
|
||||||
|
|
||||||
|
* Immutable storage and integrity monitoring,
|
||||||
|
* Controlled deletion only through approved retention workflows,
|
||||||
|
* Legal hold support.
|
||||||
|
|
||||||
|
**Audit package export (shall):**
|
||||||
|
For any decision, the Platform must be able to export an “Audit Package” containing:
|
||||||
|
|
||||||
|
1. **Decision statement** (signed)
|
||||||
|
2. **Evidence manifest** (signed) listing all evidence objects by hash
|
||||||
|
3. **Inputs** (SBOM/VEX/provenance/etc.) or references to controlled-access retrieval
|
||||||
|
4. **Transformation chain** (normalization and mapping records)
|
||||||
|
5. **Policy version and evaluation trace**
|
||||||
|
6. **External dataset snapshot manifests**
|
||||||
|
7. **Access-control and integrity verification records** (to prove custody)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Threat-to-Evidence Traceability (Minimal Regulator View)
|
||||||
|
|
||||||
|
This section provides a compact mapping from key threat classes to the evidence that must exist to satisfy audit and non-repudiation expectations.
|
||||||
|
|
||||||
|
| Threat Class | Primary Risk | “Must-have” Evidence Outputs |
|
||||||
|
| -------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||||
|
| Spoofing submitter | Untrusted artifacts used | Auth logs + signature verification + artifact digests |
|
||||||
|
| Tampering with evidence | Retroactive manipulation | Content-addressed evidence + append-only index + signed manifest (+ optional transparency anchor) |
|
||||||
|
| Repudiation | Denial of approval/changes | Signed policy + approval workflow logs + immutable audit trail |
|
||||||
|
| Information disclosure | Sensitive SBOM leakage | Access-control evidence + redaction policy version + export logs |
|
||||||
|
| DoS | Missed gates / delayed response | Rate limiting logs + capacity metrics + bounded parsing evidence |
|
||||||
|
| Privilege escalation | Policy/evidence compromise | IAM snapshots + key access logs + segregation-of-duty records |
|
||||||
|
| Supply-chain pipeline compromise | Malicious artifact | Provenance attestations + verification results + artifact digest binding |
|
||||||
|
| Vulnerability feed drift | Non-reproducible decisions | Feed snapshot manifests + digests + authenticity verification |
|
||||||
|
|
||||||
|
(Where the threat concerns the wider software supply chain, SLSA’s threat taxonomy provides an established clustering for where pipeline threats occur and the role of verification. ([SLSA][5]))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Governance, Control Testing, and Continuous Compliance
|
||||||
|
|
||||||
|
To be regulator-grade, the Platform’s security and evidence integrity controls must be governed and tested.
|
||||||
|
|
||||||
|
### 8.1 Governance expectations
|
||||||
|
|
||||||
|
* Maintain a control mapping to a recognized catalog (e.g., NIST SP 800‑53) for access control, auditing, integrity, and supply-chain risk management. ([NIST Computer Security Resource Center][4])
|
||||||
|
* Maintain a supply-chain risk posture aligned with C‑SCRM guidance (e.g., NIST SP 800‑161 Rev.1). ([NIST Computer Security Resource Center][3])
|
||||||
|
* Align secure development practices to SSDF expectations and terminology, noting SSDF has an active Rev.1 IPD (v1.2) publication process at NIST. ([NIST Computer Security Resource Center][2])
|
||||||
|
|
||||||
|
### 8.2 Control testing (shall)
|
||||||
|
|
||||||
|
At minimum, perform and retain evidence of:
|
||||||
|
|
||||||
|
* Periodic integrity tests of evidence store immutability and hash verification.
|
||||||
|
* Key management audits (signing operations, rotation, restricted usage).
|
||||||
|
* Access review audits (especially multi-tenant isolation).
|
||||||
|
* Reproducibility tests: re-run evaluation from historical evidence package and confirm identical results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A: Example Signed Decision Statement Structure (Conceptual)
|
||||||
|
|
||||||
|
This is a conceptual structure (not a normative schema) showing the minimum linkage needed:
|
||||||
|
|
||||||
|
* **Subject:** artifact digest(s) + identifiers
|
||||||
|
* **Predicate type:** `.../decision` (Platform-defined)
|
||||||
|
* **Predicate:** decision outcome + rationale + policy hash + dataset snapshot hashes
|
||||||
|
* **Envelope:** DSSE signature with payload type binding ([GitHub][7])
|
||||||
|
* **Optional transparency anchor:** Rekor entry UUID / inclusion proof ([Sigstore][8])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix B: Practical Notes for SBOM/VEX Interoperability
|
||||||
|
|
||||||
|
* Support both SPDX and CycloneDX ingestion and preservation; both are referenced in SBOM minimum elements discussion and are widely used.
|
||||||
|
* Treat CSAF VEX and OpenVEX as acceptable VEX carriers; validate schema and preserve original artifacts. ([OASIS Documents][11])
|
||||||
|
* Capture “known unknowns” explicitly rather than forcing false precision; this is part of SBOM minimum elements’ practices/processes framing and is directly relevant to regulator-grade audit transparency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What you can do next with this model
|
||||||
|
|
||||||
|
If you want, I can produce any of the following artifacts derived directly from this model (without changing its underlying assertions):
|
||||||
|
|
||||||
|
1. A **control-to-evidence crosswalk** (NIST 800‑53 / SSDF / C‑SCRM oriented).
|
||||||
|
2. A **test plan** (control testing, evidence integrity validation, reproducibility drills).
|
||||||
|
3. A **formal evidence schema** (JSON schema for evidence objects + DSSE envelopes + manifest format).
|
||||||
|
4. A **regulator-ready “Audit Package” template** you can hand to third parties (including redaction tiers).
|
||||||
|
|
||||||
|
[1]: https://www.federalregister.gov/documents/2025/08/22/2025-16147/request-for-comment-on-2025-minimum-elements-for-a-software-bill-of-materials "
|
||||||
|
Federal Register
|
||||||
|
\::
|
||||||
|
Request for Comment on 2025 Minimum Elements for a Software Bill of Materials
|
||||||
|
"
|
||||||
|
[2]: https://csrc.nist.gov/pubs/sp/800/218/r1/ipd "SP 800-218 Rev. 1, Secure Software Development Framework (SSDF) Version 1.2: Recommendations for Mitigating the Risk of Software Vulnerabilities | CSRC"
|
||||||
|
[3]: https://csrc.nist.gov/pubs/sp/800/161/r1/final "SP 800-161 Rev. 1, Cybersecurity Supply Chain Risk Management Practices for Systems and Organizations | CSRC"
|
||||||
|
[4]: https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final "SP 800-53 Rev. 5, Security and Privacy Controls for Information Systems and Organizations | CSRC"
|
||||||
|
[5]: https://slsa.dev/spec/v1.1/threats "SLSA • Threats & mitigations"
|
||||||
|
[6]: https://in-toto.io/?utm_source=chatgpt.com "in-toto"
|
||||||
|
[7]: https://github.com/secure-systems-lab/dsse?utm_source=chatgpt.com "DSSE: Dead Simple Signing Envelope"
|
||||||
|
[8]: https://docs.sigstore.dev/logging/overview/?utm_source=chatgpt.com "Rekor"
|
||||||
|
[9]: https://github.com/CycloneDX/specification?utm_source=chatgpt.com "CycloneDX/specification"
|
||||||
|
[10]: https://www.iso.org/standard/81870.html?utm_source=chatgpt.com "ISO/IEC 5962:2021 - SPDX® Specification V2.2.1"
|
||||||
|
[11]: https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html?utm_source=chatgpt.com "Common Security Advisory Framework Version 2.0 - Index of /"
|
||||||
|
[12]: https://github.com/openvex/spec?utm_source=chatgpt.com "OpenVEX Specification"
|
||||||
|
[13]: https://osv.dev/?utm_source=chatgpt.com "OSV - Open Source Vulnerabilities"
|
||||||
|
[14]: https://slsa.dev/spec/v1.0-rc1/provenance?utm_source=chatgpt.com "Provenance"
|
||||||
|
[15]: https://cyclonedx.org/specification/overview/?utm_source=chatgpt.com "Specification Overview"
|
||||||
@@ -30,20 +30,20 @@ why the system leans *monolith‑plus‑plug‑ins*, and where extension points
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
A(API Gateway)
|
A(API Gateway)
|
||||||
B1(Scanner Core<br/>.NET latest LTS)
|
B1(Scanner Core<br/>.NET latest LTS)
|
||||||
B2(Concelier service\n(vuln ingest/merge/export))
|
B2(Concelier service\n(vuln ingest/merge/export))
|
||||||
B3(Policy Engine OPA)
|
B3(Policy Engine OPA)
|
||||||
C1(Redis 7)
|
C1(Redis 7)
|
||||||
C2(MongoDB 7)
|
C2(PostgreSQL 16)
|
||||||
D(UI SPA<br/>Angular latest version)
|
D(UI SPA<br/>Angular latest version)
|
||||||
A -->|gRPC| B1
|
A -->|gRPC| B1
|
||||||
B1 -->|async| B2
|
B1 -->|async| B2
|
||||||
B1 -->|OPA| B3
|
B1 -->|OPA| B3
|
||||||
B1 --> C1
|
B1 --> C1
|
||||||
B1 --> C2
|
B1 --> C2
|
||||||
A -->|REST/WS| D
|
A -->|REST/WS| D
|
||||||
````
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,10 +53,10 @@ graph TD
|
|||||||
| ---------------------------- | --------------------- | ---------------------------------------------------- |
|
| ---------------------------- | --------------------- | ---------------------------------------------------- |
|
||||||
| **API Gateway** | ASP.NET Minimal API | Auth (JWT), quotas, request routing |
|
| **API Gateway** | ASP.NET Minimal API | Auth (JWT), quotas, request routing |
|
||||||
| **Scanner Core** | C# 12, Polly | Layer diffing, SBOM generation, vuln correlation |
|
| **Scanner Core** | C# 12, Polly | Layer diffing, SBOM generation, vuln correlation |
|
||||||
| **Concelier (vulnerability ingest/merge/export service)** | C# source-gen workers | Consolidate NVD + regional CVE feeds into the canonical MongoDB store and drive JSON / Trivy DB exports |
|
| **Concelier (vulnerability ingest/merge/export service)** | C# source-gen workers | Consolidate NVD + regional CVE feeds into the canonical PostgreSQL store and drive JSON / Trivy DB exports |
|
||||||
| **Policy Engine** | OPA (Rego) | admission decisions, custom org rules |
|
| **Policy Engine** | OPA (Rego) | admission decisions, custom org rules |
|
||||||
| **Redis 7** | Key‑DB compatible | LRU cache, quota counters |
|
| **Redis 7** | Key‑DB compatible | LRU cache, quota counters |
|
||||||
| **MongoDB 7** | WiredTiger | SBOM & findings storage |
|
| **PostgreSQL 16** | JSONB storage | SBOM & findings storage |
|
||||||
| **Angular {{ angular }} UI** | RxJS, Tailwind | Dashboard, reports, admin UX |
|
| **Angular {{ angular }} UI** | RxJS, Tailwind | Dashboard, reports, admin UX |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -87,8 +87,8 @@ Hot‑plugging is deferred until after v 1.0 for security review.
|
|||||||
* If miss → pulls layers, generates SBOM.
|
* If miss → pulls layers, generates SBOM.
|
||||||
* Executes plug‑ins (mutators, additional scanners).
|
* Executes plug‑ins (mutators, additional scanners).
|
||||||
4. **Policy Engine** evaluates `scanResult` document.
|
4. **Policy Engine** evaluates `scanResult` document.
|
||||||
5. **Findings** stored in MongoDB; WebSocket event notifies UI.
|
5. **Findings** stored in PostgreSQL; WebSocket event notifies UI.
|
||||||
6. **ResultSink plug‑ins** export to Slack, Splunk, JSON file, etc.
|
6. **ResultSink plug‑ins** export to Slack, Splunk, JSON file, etc.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ mutate observation or linkset collections.
|
|||||||
- **Unit tests** (`StellaOps.Concelier.Core.Tests`) validate schema guards,
|
- **Unit tests** (`StellaOps.Concelier.Core.Tests`) validate schema guards,
|
||||||
deterministic linkset hashing, conflict detection fixtures, and supersedes
|
deterministic linkset hashing, conflict detection fixtures, and supersedes
|
||||||
chains.
|
chains.
|
||||||
- **Mongo integration tests** (`StellaOps.Concelier.Storage.Mongo.Tests`) verify
|
- **PostgreSQL integration tests** (`StellaOps.Concelier.Storage.Postgres.Tests`) verify
|
||||||
indexes and idempotent writes under concurrency.
|
indexes and idempotent writes under concurrency.
|
||||||
- **CLI smoke suites** confirm `stella advisories observations` and `stella
|
- **CLI smoke suites** confirm `stella advisories observations` and `stella
|
||||||
advisories linksets` export stable JSON.
|
advisories linksets` export stable JSON.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Conseiller / Excititor / SBOM / Policy
|
|||||||
v
|
v
|
||||||
+----------------------------+
|
+----------------------------+
|
||||||
| Cache & Provenance |
|
| Cache & Provenance |
|
||||||
| (Mongo + DSSE optional) |
|
| (PostgreSQL + DSSE opt.) |
|
||||||
+----------------------------+
|
+----------------------------+
|
||||||
| \
|
| \
|
||||||
v v
|
v v
|
||||||
@@ -48,7 +48,7 @@ Key stages:
|
|||||||
| `AdvisoryPipelineOrchestrator` | Builds task plans, selects prompt templates, allocates token budgets. | Tenant-scoped; memoises by cache key. |
|
| `AdvisoryPipelineOrchestrator` | Builds task plans, selects prompt templates, allocates token budgets. | Tenant-scoped; memoises by cache key. |
|
||||||
| `GuardrailService` | Applies redaction filters, prompt allowlists, validation schemas, and DSSE sealing. | Shares configuration with Security Guild. |
|
| `GuardrailService` | Applies redaction filters, prompt allowlists, validation schemas, and DSSE sealing. | Shares configuration with Security Guild. |
|
||||||
| `ProfileRegistry` | Maps profile IDs to runtime implementations (local model, remote connector). | Enforces tenant consent and allowlists. |
|
| `ProfileRegistry` | Maps profile IDs to runtime implementations (local model, remote connector). | Enforces tenant consent and allowlists. |
|
||||||
| `AdvisoryOutputStore` | Mongo collection storing cached artefacts plus provenance manifest. | TTL defaults 24h; DSSE metadata optional. |
|
| `AdvisoryOutputStore` | PostgreSQL table storing cached artefacts plus provenance manifest. | TTL defaults 24h; DSSE metadata optional. |
|
||||||
| `AdvisoryPipelineWorker` | Background executor for queued jobs (future sprint once 004A wires queue). | Consumes `advisory.pipeline.execute` messages. |
|
| `AdvisoryPipelineWorker` | Background executor for queued jobs (future sprint once 004A wires queue). | Consumes `advisory.pipeline.execute` messages. |
|
||||||
|
|
||||||
## 3. Data contracts
|
## 3. Data contracts
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Advisory AI is the retrieval-augmented assistant that synthesises Conseiller (ad
|
|||||||
| Retrievers | Fetch deterministic advisory/VEX/SBOM context, guardrail inputs, policy digests. | Conseiller, Excititor, SBOM Service, Policy Engine |
|
| Retrievers | Fetch deterministic advisory/VEX/SBOM context, guardrail inputs, policy digests. | Conseiller, Excititor, SBOM Service, Policy Engine |
|
||||||
| Orchestrator | Builds `AdvisoryTaskPlan` objects (summary/conflict/remediation) with budgets and cache keys. | Deterministic toolset (AIAI-31-003), Authority scopes |
|
| Orchestrator | Builds `AdvisoryTaskPlan` objects (summary/conflict/remediation) with budgets and cache keys. | Deterministic toolset (AIAI-31-003), Authority scopes |
|
||||||
| Guardrails | Enforce redaction, structured prompts, citation validation, injection defence, and DSSE sealing. | Security Guild guardrail library |
|
| Guardrails | Enforce redaction, structured prompts, citation validation, injection defence, and DSSE sealing. | Security Guild guardrail library |
|
||||||
| Outputs | Persist cache entries (hash + context manifest), expose via API/CLI/Console, emit telemetry. | Mongo cache store, Export Center, Observability stack |
|
| Outputs | Persist cache entries (hash + context manifest), expose via API/CLI/Console, emit telemetry. | PostgreSQL cache store, Export Center, Observability stack |
|
||||||
|
|
||||||
See `docs/modules/advisory-ai/architecture.md` for deep technical diagrams and sequence flows.
|
See `docs/modules/advisory-ai/architecture.md` for deep technical diagrams and sequence flows.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Source Advisory:** 14-Dec-2025 - Offline and Air-Gap Technical Reference
|
**Source Advisory:** 14-Dec-2025 - Offline and Air-Gap Technical Reference
|
||||||
**Document Version:** 1.0
|
**Document Version:** 1.0
|
||||||
**Last Updated:** 2025-12-14
|
**Last Updated:** 2025-12-15
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -112,17 +112,14 @@ src/AirGap/
|
|||||||
│ │ └── QuarantineOptions.cs # Sprint 0338
|
│ │ └── QuarantineOptions.cs # Sprint 0338
|
||||||
│ ├── Telemetry/
|
│ ├── Telemetry/
|
||||||
│ │ ├── OfflineKitMetrics.cs # Sprint 0341
|
│ │ ├── OfflineKitMetrics.cs # Sprint 0341
|
||||||
│ │ └── OfflineKitLogFields.cs # Sprint 0341
|
│ │ ├── OfflineKitLogFields.cs # Sprint 0341
|
||||||
│ ├── Audit/
|
│ │ └── OfflineKitLogScopes.cs # Sprint 0341
|
||||||
│ │ └── OfflineKitAuditEmitter.cs # Sprint 0341
|
|
||||||
│ ├── Reconciliation/
|
│ ├── Reconciliation/
|
||||||
│ │ ├── ArtifactIndex.cs # Sprint 0342
|
│ │ ├── ArtifactIndex.cs # Sprint 0342
|
||||||
│ │ ├── EvidenceCollector.cs # Sprint 0342
|
│ │ ├── EvidenceCollector.cs # Sprint 0342
|
||||||
│ │ ├── DocumentNormalizer.cs # Sprint 0342
|
│ │ ├── DocumentNormalizer.cs # Sprint 0342
|
||||||
│ │ ├── PrecedenceLattice.cs # Sprint 0342
|
│ │ ├── PrecedenceLattice.cs # Sprint 0342
|
||||||
│ │ └── EvidenceGraphEmitter.cs # Sprint 0342
|
│ │ └── EvidenceGraphEmitter.cs # Sprint 0342
|
||||||
│ └── OfflineKitReasonCodes.cs # Sprint 0341
|
|
||||||
|
|
||||||
src/Scanner/
|
src/Scanner/
|
||||||
├── __Libraries/StellaOps.Scanner.Core/
|
├── __Libraries/StellaOps.Scanner.Core/
|
||||||
│ ├── Configuration/
|
│ ├── Configuration/
|
||||||
@@ -136,7 +133,7 @@ src/Scanner/
|
|||||||
|
|
||||||
src/Cli/
|
src/Cli/
|
||||||
├── StellaOps.Cli/
|
├── StellaOps.Cli/
|
||||||
│ └── Commands/
|
│ ├── Commands/
|
||||||
│ ├── Offline/
|
│ ├── Offline/
|
||||||
│ │ ├── OfflineCommandGroup.cs # Sprint 0339
|
│ │ ├── OfflineCommandGroup.cs # Sprint 0339
|
||||||
│ │ ├── OfflineImportHandler.cs # Sprint 0339
|
│ │ ├── OfflineImportHandler.cs # Sprint 0339
|
||||||
@@ -144,11 +141,13 @@ src/Cli/
|
|||||||
│ │ └── OfflineExitCodes.cs # Sprint 0339
|
│ │ └── OfflineExitCodes.cs # Sprint 0339
|
||||||
│ └── Verify/
|
│ └── Verify/
|
||||||
│ └── VerifyOfflineHandler.cs # Sprint 0339
|
│ └── VerifyOfflineHandler.cs # Sprint 0339
|
||||||
|
│ └── Output/
|
||||||
|
│ └── OfflineKitReasonCodes.cs # Sprint 0341
|
||||||
|
|
||||||
src/Authority/
|
src/Authority/
|
||||||
├── __Libraries/StellaOps.Authority.Storage.Postgres/
|
├── __Libraries/StellaOps.Authority.Storage.Postgres/
|
||||||
│ └── Migrations/
|
│ └── Migrations/
|
||||||
│ └── 003_offline_kit_audit.sql # Sprint 0341
|
│ └── 004_offline_kit_audit.sql # Sprint 0341
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Changes
|
### Database Changes
|
||||||
@@ -226,6 +225,8 @@ src/Authority/
|
|||||||
6. Implement audit repository and emitter
|
6. Implement audit repository and emitter
|
||||||
7. Create Grafana dashboard
|
7. Create Grafana dashboard
|
||||||
|
|
||||||
|
> Blockers: Prometheus `/metrics` endpoint hosting and audit emitter call-sites await an owning Offline Kit import/activation flow (`POST /api/offline-kit/import`).
|
||||||
|
|
||||||
**Exit Criteria:**
|
**Exit Criteria:**
|
||||||
- [ ] Operators can import/verify kits via CLI
|
- [ ] Operators can import/verify kits via CLI
|
||||||
- [ ] Metrics are visible in Prometheus/Grafana
|
- [ ] Metrics are visible in Prometheus/Grafana
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
- Deterministic storage for offline bundle metadata with tenant isolation (RLS) and stable ordering.
|
- Deterministic storage for offline bundle metadata with tenant isolation (RLS) and stable ordering.
|
||||||
- Ready for Mongo-backed implementation while providing in-memory deterministic reference behavior.
|
- Ready for PostgreSQL-backed implementation while providing in-memory deterministic reference behavior.
|
||||||
|
|
||||||
## Schema (logical)
|
## Schema (logical)
|
||||||
- `bundle_catalog`:
|
- `bundle_catalog`:
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
- Models: `BundleCatalogEntry`, `BundleItem`.
|
- Models: `BundleCatalogEntry`, `BundleItem`.
|
||||||
- Tests cover upsert overwrite semantics, tenant isolation, and deterministic ordering (`tests/AirGap/StellaOps.AirGap.Importer.Tests/InMemoryBundleRepositoriesTests.cs`).
|
- Tests cover upsert overwrite semantics, tenant isolation, and deterministic ordering (`tests/AirGap/StellaOps.AirGap.Importer.Tests/InMemoryBundleRepositoriesTests.cs`).
|
||||||
|
|
||||||
## Migration notes (for Mongo/SQL backends)
|
## Migration notes (for PostgreSQL backends)
|
||||||
- Create compound unique indexes on (`tenant_id`, `bundle_id`) for catalog; (`tenant_id`, `bundle_id`, `path`) for items.
|
- Create compound unique indexes on (`tenant_id`, `bundle_id`) for catalog; (`tenant_id`, `bundle_id`, `path`) for items.
|
||||||
- Enforce RLS by always scoping queries to `tenant_id` and validating it at repository boundary (as done in in-memory reference impl).
|
- Enforce RLS by always scoping queries to `tenant_id` and validating it at repository boundary (as done in in-memory reference impl).
|
||||||
- Keep paths lowercased or use ordinal comparisons to avoid locale drift; sort before persistence to preserve determinism.
|
- Keep paths lowercased or use ordinal comparisons to avoid locale drift; sort before persistence to preserve determinism.
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
- Implement Mongo-backed repositories mirroring the deterministic behavior and indexes above.
|
- Implement PostgreSQL-backed repositories mirroring the deterministic behavior and indexes above.
|
||||||
- Wire repositories into importer service/CLI once storage provider is selected.
|
- Wire repositories into importer service/CLI once storage provider is selected.
|
||||||
|
|
||||||
## Owners
|
## Owners
|
||||||
|
|||||||
732
docs/airgap/epss-bundles.md
Normal file
732
docs/airgap/epss-bundles.md
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
# EPSS Air-Gapped Bundles Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide describes how to create, distribute, and import EPSS (Exploit Prediction Scoring System) data bundles for air-gapped StellaOps deployments. EPSS bundles enable offline vulnerability risk scoring with the same probabilistic threat intelligence available to online deployments.
|
||||||
|
|
||||||
|
**Key Concepts**:
|
||||||
|
- **Risk Bundle**: Aggregated security data (EPSS + KEV + advisories) for offline import
|
||||||
|
- **EPSS Snapshot**: Single-day EPSS scores for all CVEs (~300k rows)
|
||||||
|
- **Staleness Threshold**: How old EPSS data can be before fallback to CVSS-only
|
||||||
|
- **Deterministic Import**: Same bundle imported twice yields identical database state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bundle Structure
|
||||||
|
|
||||||
|
### Standard Risk Bundle Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
risk-bundle-2025-12-17/
|
||||||
|
├── manifest.json # Bundle metadata and checksums
|
||||||
|
├── epss/
|
||||||
|
│ ├── epss_scores-2025-12-17.csv.zst # EPSS data (ZSTD compressed)
|
||||||
|
│ └── epss_metadata.json # EPSS provenance
|
||||||
|
├── kev/
|
||||||
|
│ └── kev-catalog.json # CISA KEV catalog
|
||||||
|
├── advisories/
|
||||||
|
│ ├── nvd-updates.ndjson.zst
|
||||||
|
│ └── ghsa-updates.ndjson.zst
|
||||||
|
└── signatures/
|
||||||
|
├── bundle.dsse.json # DSSE signature (optional)
|
||||||
|
└── bundle.sha256sums # File integrity checksums
|
||||||
|
```
|
||||||
|
|
||||||
|
### manifest.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bundle_id": "risk-bundle-2025-12-17",
|
||||||
|
"created_at": "2025-12-17T00:00:00Z",
|
||||||
|
"created_by": "stellaops-bundler-v1.2.3",
|
||||||
|
"bundle_type": "risk",
|
||||||
|
"schema_version": "v1",
|
||||||
|
"contents": {
|
||||||
|
"epss": {
|
||||||
|
"model_date": "2025-12-17",
|
||||||
|
"file": "epss/epss_scores-2025-12-17.csv.zst",
|
||||||
|
"sha256": "abc123...",
|
||||||
|
"size_bytes": 15728640,
|
||||||
|
"row_count": 231417
|
||||||
|
},
|
||||||
|
"kev": {
|
||||||
|
"catalog_version": "2025-12-17",
|
||||||
|
"file": "kev/kev-catalog.json",
|
||||||
|
"sha256": "def456...",
|
||||||
|
"known_exploited_count": 1247
|
||||||
|
},
|
||||||
|
"advisories": {
|
||||||
|
"nvd": {
|
||||||
|
"file": "advisories/nvd-updates.ndjson.zst",
|
||||||
|
"sha256": "ghi789...",
|
||||||
|
"record_count": 1523
|
||||||
|
},
|
||||||
|
"ghsa": {
|
||||||
|
"file": "advisories/ghsa-updates.ndjson.zst",
|
||||||
|
"sha256": "jkl012...",
|
||||||
|
"record_count": 8734
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"type": "dsse",
|
||||||
|
"file": "signatures/bundle.dsse.json",
|
||||||
|
"key_id": "stellaops-bundler-2025",
|
||||||
|
"algorithm": "ed25519"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### epss/epss_metadata.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model_date": "2025-12-17",
|
||||||
|
"model_version": "v2025.12.17",
|
||||||
|
"published_date": "2025-12-17",
|
||||||
|
"row_count": 231417,
|
||||||
|
"source_uri": "https://epss.empiricalsecurity.com/epss_scores-2025-12-17.csv.gz",
|
||||||
|
"retrieved_at": "2025-12-17T00:05:32Z",
|
||||||
|
"file_sha256": "abc123...",
|
||||||
|
"decompressed_sha256": "xyz789...",
|
||||||
|
"compression": "zstd",
|
||||||
|
"compression_level": 19
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating EPSS Bundles
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
**Build System Requirements**:
|
||||||
|
- Internet access (for fetching FIRST.org data)
|
||||||
|
- StellaOps Bundler CLI: `stellaops-bundler`
|
||||||
|
- ZSTD compression: `zstd` (v1.5+)
|
||||||
|
- Python 3.10+ (for verification scripts)
|
||||||
|
|
||||||
|
**Permissions**:
|
||||||
|
- Read access to FIRST.org EPSS API/CSV endpoints
|
||||||
|
- Write access to bundle staging directory
|
||||||
|
- (Optional) Signing key for DSSE signatures
|
||||||
|
|
||||||
|
### Daily Bundle Creation (Automated)
|
||||||
|
|
||||||
|
**Recommended Schedule**: Daily at 01:00 UTC (after FIRST publishes at ~00:00 UTC)
|
||||||
|
|
||||||
|
**Script**: `scripts/create-risk-bundle.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BUNDLE_DATE=$(date -u +%Y-%m-%d)
|
||||||
|
BUNDLE_DIR="risk-bundle-${BUNDLE_DATE}"
|
||||||
|
STAGING_DIR="/tmp/stellaops-bundles/${BUNDLE_DIR}"
|
||||||
|
|
||||||
|
echo "Creating risk bundle for ${BUNDLE_DATE}..."
|
||||||
|
|
||||||
|
# 1. Create staging directory
|
||||||
|
mkdir -p "${STAGING_DIR}"/{epss,kev,advisories,signatures}
|
||||||
|
|
||||||
|
# 2. Fetch EPSS data from FIRST.org
|
||||||
|
echo "Fetching EPSS data..."
|
||||||
|
curl -sL "https://epss.empiricalsecurity.com/epss_scores-${BUNDLE_DATE}.csv.gz" \
|
||||||
|
-o "${STAGING_DIR}/epss/epss_scores-${BUNDLE_DATE}.csv.gz"
|
||||||
|
|
||||||
|
# 3. Decompress and re-compress with ZSTD (better compression for offline)
|
||||||
|
gunzip "${STAGING_DIR}/epss/epss_scores-${BUNDLE_DATE}.csv.gz"
|
||||||
|
zstd -19 -q "${STAGING_DIR}/epss/epss_scores-${BUNDLE_DATE}.csv" \
|
||||||
|
-o "${STAGING_DIR}/epss/epss_scores-${BUNDLE_DATE}.csv.zst"
|
||||||
|
rm "${STAGING_DIR}/epss/epss_scores-${BUNDLE_DATE}.csv"
|
||||||
|
|
||||||
|
# 4. Generate EPSS metadata
|
||||||
|
stellaops-bundler epss metadata \
|
||||||
|
--file "${STAGING_DIR}/epss/epss_scores-${BUNDLE_DATE}.csv.zst" \
|
||||||
|
--model-date "${BUNDLE_DATE}" \
|
||||||
|
--output "${STAGING_DIR}/epss/epss_metadata.json"
|
||||||
|
|
||||||
|
# 5. Fetch KEV catalog
|
||||||
|
echo "Fetching KEV catalog..."
|
||||||
|
curl -sL "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" \
|
||||||
|
-o "${STAGING_DIR}/kev/kev-catalog.json"
|
||||||
|
|
||||||
|
# 6. Fetch advisory updates (optional, for comprehensive bundles)
|
||||||
|
# stellaops-bundler advisories fetch ...
|
||||||
|
|
||||||
|
# 7. Generate checksums
|
||||||
|
echo "Generating checksums..."
|
||||||
|
(cd "${STAGING_DIR}" && find . -type f ! -name "*.sha256sums" -exec sha256sum {} \;) \
|
||||||
|
> "${STAGING_DIR}/signatures/bundle.sha256sums"
|
||||||
|
|
||||||
|
# 8. Generate manifest
|
||||||
|
stellaops-bundler manifest create \
|
||||||
|
--bundle-dir "${STAGING_DIR}" \
|
||||||
|
--bundle-id "${BUNDLE_DIR}" \
|
||||||
|
--output "${STAGING_DIR}/manifest.json"
|
||||||
|
|
||||||
|
# 9. Sign bundle (if signing key available)
|
||||||
|
if [ -n "${SIGNING_KEY:-}" ]; then
|
||||||
|
echo "Signing bundle..."
|
||||||
|
stellaops-bundler sign \
|
||||||
|
--manifest "${STAGING_DIR}/manifest.json" \
|
||||||
|
--key "${SIGNING_KEY}" \
|
||||||
|
--output "${STAGING_DIR}/signatures/bundle.dsse.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 10. Create tarball
|
||||||
|
echo "Creating tarball..."
|
||||||
|
tar -C "$(dirname "${STAGING_DIR}")" -czf "/var/stellaops/bundles/${BUNDLE_DIR}.tar.gz" \
|
||||||
|
"$(basename "${STAGING_DIR}")"
|
||||||
|
|
||||||
|
echo "Bundle created: /var/stellaops/bundles/${BUNDLE_DIR}.tar.gz"
|
||||||
|
echo "Size: $(du -h /var/stellaops/bundles/${BUNDLE_DIR}.tar.gz | cut -f1)"
|
||||||
|
|
||||||
|
# 11. Verify bundle
|
||||||
|
stellaops-bundler verify "/var/stellaops/bundles/${BUNDLE_DIR}.tar.gz"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cron Schedule**:
|
||||||
|
```cron
|
||||||
|
# Daily at 01:00 UTC (after FIRST publishes EPSS at ~00:00 UTC)
|
||||||
|
0 1 * * * /opt/stellaops/scripts/create-risk-bundle.sh >> /var/log/stellaops/bundler.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Distributing Bundles
|
||||||
|
|
||||||
|
### Transfer Methods
|
||||||
|
|
||||||
|
#### 1. Physical Media (Highest Security)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy to USB drive
|
||||||
|
cp /var/stellaops/bundles/risk-bundle-2025-12-17.tar.gz /media/usb/stellaops/
|
||||||
|
|
||||||
|
# Verify checksum
|
||||||
|
sha256sum /media/usb/stellaops/risk-bundle-2025-12-17.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Secure File Transfer (Network Isolation)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SCP over dedicated management network
|
||||||
|
scp /var/stellaops/bundles/risk-bundle-2025-12-17.tar.gz \
|
||||||
|
admin@airgap-gateway.internal:/incoming/
|
||||||
|
|
||||||
|
# Verify after transfer
|
||||||
|
ssh admin@airgap-gateway.internal \
|
||||||
|
"sha256sum /incoming/risk-bundle-2025-12-17.tar.gz"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Offline Bundle Repository (CD/DVD)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Burn to CD/DVD (for regulated industries)
|
||||||
|
growisofs -Z /dev/sr0 \
|
||||||
|
-R -J -joliet-long \
|
||||||
|
-V "StellaOps Risk Bundle 2025-12-17" \
|
||||||
|
/var/stellaops/bundles/risk-bundle-2025-12-17.tar.gz
|
||||||
|
|
||||||
|
# Verify disc
|
||||||
|
md5sum /dev/sr0 > risk-bundle-2025-12-17.md5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage Recommendations
|
||||||
|
|
||||||
|
**Bundle Retention**:
|
||||||
|
- **Online bundler**: Keep last 90 days (rolling cleanup)
|
||||||
|
- **Air-gapped system**: Keep last 30 days minimum (for rollback)
|
||||||
|
|
||||||
|
**Naming Convention**:
|
||||||
|
- Pattern: `risk-bundle-YYYY-MM-DD.tar.gz`
|
||||||
|
- Example: `risk-bundle-2025-12-17.tar.gz`
|
||||||
|
|
||||||
|
**Directory Structure** (air-gapped system):
|
||||||
|
```
|
||||||
|
/opt/stellaops/bundles/
|
||||||
|
├── incoming/ # Transfer staging area
|
||||||
|
├── verified/ # Verified, ready to import
|
||||||
|
├── imported/ # Successfully imported (archive)
|
||||||
|
└── failed/ # Failed verification/import (quarantine)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Importing Bundles (Air-Gapped System)
|
||||||
|
|
||||||
|
### Pre-Import Verification
|
||||||
|
|
||||||
|
**Step 1: Transfer to Verified Directory**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Transfer from incoming to verified (manual approval gate)
|
||||||
|
sudo mv /opt/stellaops/bundles/incoming/risk-bundle-2025-12-17.tar.gz \
|
||||||
|
/opt/stellaops/bundles/verified/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify Bundle Integrity**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Extract bundle
|
||||||
|
cd /opt/stellaops/bundles/verified
|
||||||
|
tar -xzf risk-bundle-2025-12-17.tar.gz
|
||||||
|
|
||||||
|
# Verify checksums
|
||||||
|
cd risk-bundle-2025-12-17
|
||||||
|
sha256sum -c signatures/bundle.sha256sums
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# epss/epss_scores-2025-12-17.csv.zst: OK
|
||||||
|
# epss/epss_metadata.json: OK
|
||||||
|
# kev/kev-catalog.json: OK
|
||||||
|
# manifest.json: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify DSSE Signature (if signed)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stellaops-bundler verify-signature \
|
||||||
|
--manifest manifest.json \
|
||||||
|
--signature signatures/bundle.dsse.json \
|
||||||
|
--trusted-keys /etc/stellaops/trusted-keys.json
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# ✓ Signature valid
|
||||||
|
# ✓ Key ID: stellaops-bundler-2025
|
||||||
|
# ✓ Signed at: 2025-12-17T01:05:00Z
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Procedure
|
||||||
|
|
||||||
|
**Step 4: Import Bundle**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Import using stellaops CLI
|
||||||
|
stellaops offline import \
|
||||||
|
--bundle /opt/stellaops/bundles/verified/risk-bundle-2025-12-17.tar.gz \
|
||||||
|
--verify \
|
||||||
|
--dry-run
|
||||||
|
|
||||||
|
# Review dry-run output, then execute
|
||||||
|
stellaops offline import \
|
||||||
|
--bundle /opt/stellaops/bundles/verified/risk-bundle-2025-12-17.tar.gz \
|
||||||
|
--verify
|
||||||
|
```
|
||||||
|
|
||||||
|
**Import Output**:
|
||||||
|
```
|
||||||
|
Importing risk bundle: risk-bundle-2025-12-17
|
||||||
|
✓ Manifest validated
|
||||||
|
✓ Checksums verified
|
||||||
|
✓ Signature verified
|
||||||
|
|
||||||
|
Importing EPSS data...
|
||||||
|
Model Date: 2025-12-17
|
||||||
|
Row Count: 231,417
|
||||||
|
✓ epss_import_runs created (import_run_id: 550e8400-...)
|
||||||
|
✓ epss_scores inserted (231,417 rows, 23.4s)
|
||||||
|
✓ epss_changes computed (12,345 changes, 8.1s)
|
||||||
|
✓ epss_current upserted (231,417 rows, 5.2s)
|
||||||
|
✓ Event emitted: epss.updated
|
||||||
|
|
||||||
|
Importing KEV catalog...
|
||||||
|
Known Exploited Count: 1,247
|
||||||
|
✓ kev_catalog updated
|
||||||
|
|
||||||
|
Import completed successfully in 41.2s
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Verify Import**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check EPSS status
|
||||||
|
stellaops epss status
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# EPSS Status:
|
||||||
|
# Latest Model Date: 2025-12-17
|
||||||
|
# Source: bundle://risk-bundle-2025-12-17
|
||||||
|
# CVE Count: 231,417
|
||||||
|
# Staleness: FRESH (0 days)
|
||||||
|
# Import Time: 2025-12-17T10:30:00Z
|
||||||
|
|
||||||
|
# Query specific CVE to verify
|
||||||
|
stellaops epss get CVE-2024-12345
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# CVE-2024-12345
|
||||||
|
# Score: 0.42357
|
||||||
|
# Percentile: 88.2th
|
||||||
|
# Model Date: 2025-12-17
|
||||||
|
# Source: bundle://risk-bundle-2025-12-17
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 6: Archive Imported Bundle**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Move to imported archive
|
||||||
|
sudo mv /opt/stellaops/bundles/verified/risk-bundle-2025-12-17.tar.gz \
|
||||||
|
/opt/stellaops/bundles/imported/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automation (Air-Gapped System)
|
||||||
|
|
||||||
|
### Automated Import on Arrival
|
||||||
|
|
||||||
|
**Script**: `/opt/stellaops/scripts/auto-import-bundle.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INCOMING_DIR="/opt/stellaops/bundles/incoming"
|
||||||
|
VERIFIED_DIR="/opt/stellaops/bundles/verified"
|
||||||
|
IMPORTED_DIR="/opt/stellaops/bundles/imported"
|
||||||
|
FAILED_DIR="/opt/stellaops/bundles/failed"
|
||||||
|
LOG_FILE="/var/log/stellaops/auto-import.log"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date -Iseconds)] $*" | tee -a "${LOG_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Watch for new bundles in incoming/
|
||||||
|
for bundle in "${INCOMING_DIR}"/risk-bundle-*.tar.gz; do
|
||||||
|
[ -f "${bundle}" ] || continue
|
||||||
|
|
||||||
|
BUNDLE_NAME=$(basename "${bundle}")
|
||||||
|
log "Detected new bundle: ${BUNDLE_NAME}"
|
||||||
|
|
||||||
|
# Extract
|
||||||
|
EXTRACT_DIR="${VERIFIED_DIR}/${BUNDLE_NAME%.tar.gz}"
|
||||||
|
mkdir -p "${EXTRACT_DIR}"
|
||||||
|
tar -xzf "${bundle}" -C "${VERIFIED_DIR}"
|
||||||
|
|
||||||
|
# Verify checksums
|
||||||
|
if ! (cd "${EXTRACT_DIR}" && sha256sum -c signatures/bundle.sha256sums > /dev/null 2>&1); then
|
||||||
|
log "ERROR: Checksum verification failed for ${BUNDLE_NAME}"
|
||||||
|
mv "${bundle}" "${FAILED_DIR}/"
|
||||||
|
rm -rf "${EXTRACT_DIR}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Checksum verification passed"
|
||||||
|
|
||||||
|
# Verify signature (if present)
|
||||||
|
if [ -f "${EXTRACT_DIR}/signatures/bundle.dsse.json" ]; then
|
||||||
|
if ! stellaops-bundler verify-signature \
|
||||||
|
--manifest "${EXTRACT_DIR}/manifest.json" \
|
||||||
|
--signature "${EXTRACT_DIR}/signatures/bundle.dsse.json" \
|
||||||
|
--trusted-keys /etc/stellaops/trusted-keys.json > /dev/null 2>&1; then
|
||||||
|
log "ERROR: Signature verification failed for ${BUNDLE_NAME}"
|
||||||
|
mv "${bundle}" "${FAILED_DIR}/"
|
||||||
|
rm -rf "${EXTRACT_DIR}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
log "Signature verification passed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import
|
||||||
|
if stellaops offline import --bundle "${bundle}" --verify >> "${LOG_FILE}" 2>&1; then
|
||||||
|
log "Import successful for ${BUNDLE_NAME}"
|
||||||
|
mv "${bundle}" "${IMPORTED_DIR}/"
|
||||||
|
rm -rf "${EXTRACT_DIR}"
|
||||||
|
else
|
||||||
|
log "ERROR: Import failed for ${BUNDLE_NAME}"
|
||||||
|
mv "${bundle}" "${FAILED_DIR}/"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
**Systemd Service**: `/etc/systemd/system/stellaops-bundle-watcher.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=StellaOps Bundle Auto-Import Watcher
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/inotifywait -m -e close_write --format '%w%f' /opt/stellaops/bundles/incoming | \
|
||||||
|
while read file; do /opt/stellaops/scripts/auto-import-bundle.sh; done
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
User=stellaops
|
||||||
|
Group=stellaops
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enable Service**:
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable stellaops-bundle-watcher
|
||||||
|
sudo systemctl start stellaops-bundle-watcher
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Staleness Handling
|
||||||
|
|
||||||
|
### Staleness Thresholds
|
||||||
|
|
||||||
|
| Days Since Model Date | Status | Action |
|
||||||
|
|-----------------------|--------|--------|
|
||||||
|
| 0-1 | FRESH | Normal operation |
|
||||||
|
| 2-7 | ACCEPTABLE | Continue, low-priority alert |
|
||||||
|
| 8-14 | STALE | Alert, plan bundle import |
|
||||||
|
| 15+ | VERY_STALE | Fallback to CVSS-only, urgent alert |
|
||||||
|
|
||||||
|
### Monitoring Staleness
|
||||||
|
|
||||||
|
**SQL Query**:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM concelier.epss_model_staleness;
|
||||||
|
|
||||||
|
-- Output:
|
||||||
|
-- latest_model_date | latest_import_at | days_stale | staleness_status
|
||||||
|
-- 2025-12-10 | 2025-12-10 10:30:00+00 | 7 | ACCEPTABLE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prometheus Metric**:
|
||||||
|
```promql
|
||||||
|
epss_model_staleness_days{instance="airgap-prod"}
|
||||||
|
|
||||||
|
# Alert rule:
|
||||||
|
- alert: EpssDataStale
|
||||||
|
expr: epss_model_staleness_days > 7
|
||||||
|
for: 1h
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "EPSS data is stale ({{ $value }} days old)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fallback Behavior
|
||||||
|
|
||||||
|
When EPSS data is VERY_STALE (>14 days):
|
||||||
|
|
||||||
|
**Automatic Fallback**:
|
||||||
|
- Scanner: Skip EPSS evidence, log warning
|
||||||
|
- Policy: Use CVSS-only scoring (no EPSS bonus)
|
||||||
|
- Notifications: Disabled EPSS-based alerts
|
||||||
|
- UI: Show staleness banner, disable EPSS filters
|
||||||
|
|
||||||
|
**Manual Override** (force continue using stale data):
|
||||||
|
```yaml
|
||||||
|
# etc/scanner.yaml
|
||||||
|
scanner:
|
||||||
|
epss:
|
||||||
|
staleness_policy: continue # Options: fallback, continue, error
|
||||||
|
max_staleness_days: 30 # Override 14-day default
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Bundle Import Failed: Checksum Mismatch
|
||||||
|
|
||||||
|
**Symptom**:
|
||||||
|
```
|
||||||
|
ERROR: Checksum verification failed
|
||||||
|
epss/epss_scores-2025-12-17.csv.zst: FAILED
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagnosis**:
|
||||||
|
1. Verify bundle was not corrupted during transfer:
|
||||||
|
```bash
|
||||||
|
# Compare with original
|
||||||
|
sha256sum risk-bundle-2025-12-17.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Re-transfer bundle from source
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
- Delete corrupted bundle: `rm risk-bundle-2025-12-17.tar.gz`
|
||||||
|
- Re-download/re-transfer from bundler system
|
||||||
|
|
||||||
|
### Bundle Import Failed: Signature Invalid
|
||||||
|
|
||||||
|
**Symptom**:
|
||||||
|
```
|
||||||
|
ERROR: Signature verification failed
|
||||||
|
Invalid signature or untrusted key
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagnosis**:
|
||||||
|
1. Check trusted keys configured:
|
||||||
|
```bash
|
||||||
|
cat /etc/stellaops/trusted-keys.json
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify key ID in bundle signature matches:
|
||||||
|
```bash
|
||||||
|
jq '.signature.key_id' manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
- Update trusted keys file with current bundler public key
|
||||||
|
- Or: Skip signature verification (if signatures optional):
|
||||||
|
```bash
|
||||||
|
stellaops offline import --bundle risk-bundle-2025-12-17.tar.gz --skip-signature-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
### No EPSS Data After Import
|
||||||
|
|
||||||
|
**Symptom**:
|
||||||
|
- Import succeeded, but `stellaops epss status` shows "No EPSS data"
|
||||||
|
|
||||||
|
**Diagnosis**:
|
||||||
|
```sql
|
||||||
|
-- Check import runs
|
||||||
|
SELECT * FROM concelier.epss_import_runs ORDER BY created_at DESC LIMIT 1;
|
||||||
|
|
||||||
|
-- Check epss_current count
|
||||||
|
SELECT COUNT(*) FROM concelier.epss_current;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
1. If import_runs shows FAILED status:
|
||||||
|
- Check error column: `SELECT error FROM concelier.epss_import_runs WHERE status = 'FAILED'`
|
||||||
|
- Re-run import with verbose logging
|
||||||
|
|
||||||
|
2. If epss_current is empty:
|
||||||
|
- Manually trigger upsert:
|
||||||
|
```sql
|
||||||
|
-- Re-run upsert for latest model_date
|
||||||
|
-- (This SQL is safe to re-run)
|
||||||
|
INSERT INTO concelier.epss_current (cve_id, epss_score, percentile, model_date, import_run_id, updated_at)
|
||||||
|
SELECT s.cve_id, s.epss_score, s.percentile, s.model_date, s.import_run_id, NOW()
|
||||||
|
FROM concelier.epss_scores s
|
||||||
|
WHERE s.model_date = (SELECT MAX(model_date) FROM concelier.epss_import_runs WHERE status = 'SUCCEEDED')
|
||||||
|
ON CONFLICT (cve_id) DO UPDATE SET
|
||||||
|
epss_score = EXCLUDED.epss_score,
|
||||||
|
percentile = EXCLUDED.percentile,
|
||||||
|
model_date = EXCLUDED.model_date,
|
||||||
|
import_run_id = EXCLUDED.import_run_id,
|
||||||
|
updated_at = NOW();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Weekly Bundle Import Cadence
|
||||||
|
|
||||||
|
**Recommended Schedule**:
|
||||||
|
- **Minimum**: Weekly (every Monday)
|
||||||
|
- **Preferred**: Bi-weekly (Monday & Thursday)
|
||||||
|
- **Ideal**: Daily (if transfer logistics allow)
|
||||||
|
|
||||||
|
### 2. Bundle Verification Checklist
|
||||||
|
|
||||||
|
Before importing:
|
||||||
|
- [ ] Checksum verification passed
|
||||||
|
- [ ] Signature verification passed (if signed)
|
||||||
|
- [ ] Model date within acceptable staleness window
|
||||||
|
- [ ] Disk space available (estimate: 500MB per bundle)
|
||||||
|
- [ ] Backup current EPSS data (for rollback)
|
||||||
|
|
||||||
|
### 3. Rollback Plan
|
||||||
|
|
||||||
|
If new bundle causes issues:
|
||||||
|
```bash
|
||||||
|
# 1. Identify problematic import_run_id
|
||||||
|
SELECT import_run_id, model_date, status
|
||||||
|
FROM concelier.epss_import_runs
|
||||||
|
ORDER BY created_at DESC LIMIT 5;
|
||||||
|
|
||||||
|
# 2. Delete problematic import (cascades to epss_scores, epss_changes)
|
||||||
|
DELETE FROM concelier.epss_import_runs
|
||||||
|
WHERE import_run_id = '550e8400-...';
|
||||||
|
|
||||||
|
# 3. Restore epss_current from previous day
|
||||||
|
-- (Upsert from previous model_date as shown in troubleshooting)
|
||||||
|
|
||||||
|
# 4. Verify rollback
|
||||||
|
stellaops epss status
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Audit Trail
|
||||||
|
|
||||||
|
Log all bundle imports for compliance:
|
||||||
|
|
||||||
|
**Audit Log Format** (`/var/log/stellaops/bundle-audit.log`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2025-12-17T10:30:00Z",
|
||||||
|
"action": "import",
|
||||||
|
"bundle_id": "risk-bundle-2025-12-17",
|
||||||
|
"bundle_sha256": "abc123...",
|
||||||
|
"imported_by": "admin@example.com",
|
||||||
|
"import_run_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"result": "SUCCESS",
|
||||||
|
"row_count": 231417,
|
||||||
|
"duration_seconds": 41.2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Bundle Creation Tools
|
||||||
|
|
||||||
|
### stellaops-bundler CLI Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create EPSS metadata
|
||||||
|
stellaops-bundler epss metadata \
|
||||||
|
--file epss_scores-2025-12-17.csv.zst \
|
||||||
|
--model-date 2025-12-17 \
|
||||||
|
--output epss_metadata.json
|
||||||
|
|
||||||
|
# Create manifest
|
||||||
|
stellaops-bundler manifest create \
|
||||||
|
--bundle-dir risk-bundle-2025-12-17 \
|
||||||
|
--bundle-id risk-bundle-2025-12-17 \
|
||||||
|
--output manifest.json
|
||||||
|
|
||||||
|
# Sign bundle
|
||||||
|
stellaops-bundler sign \
|
||||||
|
--manifest manifest.json \
|
||||||
|
--key /path/to/signing-key.pem \
|
||||||
|
--output bundle.dsse.json
|
||||||
|
|
||||||
|
# Verify bundle
|
||||||
|
stellaops-bundler verify risk-bundle-2025-12-17.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Bundle Scripts
|
||||||
|
|
||||||
|
Example for creating weekly bundles (7-day snapshots):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# create-weekly-bundle.sh
|
||||||
|
|
||||||
|
WEEK_START=$(date -u -d "last monday" +%Y-%m-%d)
|
||||||
|
WEEK_END=$(date -u +%Y-%m-%d)
|
||||||
|
BUNDLE_ID="risk-bundle-weekly-${WEEK_START}"
|
||||||
|
|
||||||
|
echo "Creating weekly bundle: ${BUNDLE_ID}"
|
||||||
|
|
||||||
|
for day in $(seq 0 6); do
|
||||||
|
CURRENT_DATE=$(date -u -d "${WEEK_START} + ${day} days" +%Y-%m-%d)
|
||||||
|
# Fetch EPSS for each day...
|
||||||
|
curl -sL "https://epss.empiricalsecurity.com/epss_scores-${CURRENT_DATE}.csv.gz" \
|
||||||
|
-o "epss/epss_scores-${CURRENT_DATE}.csv.gz"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Compress and bundle...
|
||||||
|
tar -czf "${BUNDLE_ID}.tar.gz" epss/ kev/ manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-12-17
|
||||||
|
**Version**: 1.0
|
||||||
|
**Maintainer**: StellaOps Operations Team
|
||||||
@@ -18,13 +18,20 @@
|
|||||||
- Expanded tests for DSSE, TUF, Merkle helpers.
|
- Expanded tests for DSSE, TUF, Merkle helpers.
|
||||||
- Added trust store + root rotation policy (dual approval) and import validator that coordinates DSSE/TUF/Merkle/rotation checks.
|
- Added trust store + root rotation policy (dual approval) and import validator that coordinates DSSE/TUF/Merkle/rotation checks.
|
||||||
|
|
||||||
|
## Updates (2025-12-15)
|
||||||
|
- Added monotonicity enforcement primitives under `src/AirGap/StellaOps.AirGap.Importer/Versioning/` (`BundleVersion`, `IVersionMonotonicityChecker`, `IBundleVersionStore`).
|
||||||
|
- Added file-based quarantine service under `src/AirGap/StellaOps.AirGap.Importer/Quarantine/` (`IQuarantineService`, `FileSystemQuarantineService`, `QuarantineOptions`).
|
||||||
|
- Updated `ImportValidator` to include monotonicity checks, force-activate support (requires reason), and quarantine on validation failures.
|
||||||
|
- Added Postgres-backed bundle version tracking in `src/AirGap/StellaOps.AirGap.Storage.Postgres/Repositories/PostgresBundleVersionStore.cs` and registration via `src/AirGap/StellaOps.AirGap.Storage.Postgres/ServiceCollectionExtensions.cs`.
|
||||||
|
- Updated tests in `tests/AirGap/StellaOps.AirGap.Importer.Tests` to cover versioning/quarantine and the new import validator behavior.
|
||||||
|
|
||||||
## Next implementation hooks
|
## Next implementation hooks
|
||||||
- Replace placeholder plan with actual DSSE + TUF verifiers; keep step ordering stable.
|
- Replace placeholder plan with actual DSSE + TUF verifiers; keep step ordering stable.
|
||||||
- Feed trust roots from sealed-mode config and Evidence Locker bundles (once available) before allowing imports.
|
- Feed trust roots from sealed-mode config and Evidence Locker bundles (once available) before allowing imports.
|
||||||
- Record audit trail for each plan step (success/failure) and a Merkle root of staged content.
|
- Record audit trail for each plan step (success/failure) and a Merkle root of staged content.
|
||||||
|
|
||||||
## Determinism/air-gap posture
|
## Determinism/air-gap posture
|
||||||
- No network dependencies; only BCL used.
|
- No network dependencies; BCL + `Microsoft.Extensions.*` only.
|
||||||
- Tests use cached local NuGet feed (`local-nugets/`).
|
- Tests use cached local NuGet feed (`local-nugets/`).
|
||||||
- Plan steps are ordered list; do not reorder without bumping downstream replay expectations.
|
- Plan steps are ordered list; do not reorder without bumping downstream replay expectations.
|
||||||
|
|
||||||
|
|||||||
213
docs/airgap/offline-bundle-format.md
Normal file
213
docs/airgap/offline-bundle-format.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# Offline Bundle Format (.stella.bundle.tgz)
|
||||||
|
|
||||||
|
> Sprint: SPRINT_3603_0001_0001
|
||||||
|
> Module: ExportCenter
|
||||||
|
|
||||||
|
This document describes the `.stella.bundle.tgz` format for portable, signed, verifiable evidence packages.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The offline bundle is a self-contained archive containing all evidence and artifacts needed for offline triage of security findings. Bundles are:
|
||||||
|
|
||||||
|
- **Portable**: Single file that can be transferred to air-gapped environments
|
||||||
|
- **Signed**: DSSE-signed manifest for authenticity verification
|
||||||
|
- **Verifiable**: Content-addressable with SHA-256 hashes for integrity
|
||||||
|
- **Complete**: Contains all data needed for offline decision-making
|
||||||
|
|
||||||
|
## File Format
|
||||||
|
|
||||||
|
```
|
||||||
|
{alert-id}.stella.bundle.tgz
|
||||||
|
├── manifest.json # Bundle manifest (DSSE-signed)
|
||||||
|
├── metadata/
|
||||||
|
│ ├── alert.json # Alert metadata snapshot
|
||||||
|
│ └── generation-info.json # Bundle generation metadata
|
||||||
|
├── evidence/
|
||||||
|
│ ├── reachability-proof.json # Call-graph reachability evidence
|
||||||
|
│ ├── callstack.json # Exploitability call stacks
|
||||||
|
│ └── provenance.json # Build provenance attestations
|
||||||
|
├── vex/
|
||||||
|
│ ├── decisions.ndjson # VEX decision history (NDJSON)
|
||||||
|
│ └── current-status.json # Current VEX status
|
||||||
|
├── sbom/
|
||||||
|
│ ├── current.cdx.json # Current SBOM slice (CycloneDX)
|
||||||
|
│ └── baseline.cdx.json # Baseline SBOM for diff
|
||||||
|
├── diff/
|
||||||
|
│ └── sbom-delta.json # SBOM delta changes
|
||||||
|
└── attestations/
|
||||||
|
├── bundle.dsse.json # DSSE envelope for bundle
|
||||||
|
└── evidence.dsse.json # Evidence attestation chain
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manifest Schema
|
||||||
|
|
||||||
|
The `manifest.json` file follows this schema:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bundle_format_version": "1.0.0",
|
||||||
|
"bundle_id": "abc123def456...",
|
||||||
|
"alert_id": "alert-789",
|
||||||
|
"created_at": "2024-12-15T10:00:00Z",
|
||||||
|
"created_by": "user@example.com",
|
||||||
|
"stellaops_version": "1.5.0",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"path": "metadata/alert.json",
|
||||||
|
"hash": "sha256:...",
|
||||||
|
"size": 1234,
|
||||||
|
"content_type": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"root_hash": "sha256:...",
|
||||||
|
"signature": {
|
||||||
|
"algorithm": "ES256",
|
||||||
|
"key_id": "signing-key-001",
|
||||||
|
"value": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manifest Fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `bundle_format_version` | string | Yes | Format version (semver) |
|
||||||
|
| `bundle_id` | string | Yes | Unique bundle identifier |
|
||||||
|
| `alert_id` | string | Yes | Source alert identifier |
|
||||||
|
| `created_at` | ISO 8601 | Yes | Bundle creation timestamp (UTC) |
|
||||||
|
| `created_by` | string | Yes | Actor who created the bundle |
|
||||||
|
| `stellaops_version` | string | Yes | StellaOps version that created bundle |
|
||||||
|
| `entries` | array | Yes | List of content entries with hashes |
|
||||||
|
| `root_hash` | string | Yes | Merkle root of all entry hashes |
|
||||||
|
| `signature` | object | No | DSSE signature (if signed) |
|
||||||
|
|
||||||
|
## Entry Schema
|
||||||
|
|
||||||
|
Each entry in the manifest:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "evidence/reachability-proof.json",
|
||||||
|
"hash": "sha256:abc123...",
|
||||||
|
"size": 2048,
|
||||||
|
"content_type": "application/json",
|
||||||
|
"compression": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DSSE Signing
|
||||||
|
|
||||||
|
Bundles support DSSE (Dead Simple Signing Envelope) signing:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payloadType": "application/vnd.stellaops.bundle.manifest+json",
|
||||||
|
"payload": "<base64-encoded manifest>",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"keyid": "signing-key-001",
|
||||||
|
"sig": "<base64-encoded signature>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creation
|
||||||
|
|
||||||
|
### API Endpoint
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/alerts/{alertId}/bundle
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
|
Response: application/gzip
|
||||||
|
Content-Disposition: attachment; filename="alert-123.stella.bundle.tgz"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var packager = services.GetRequiredService<IOfflineBundlePackager>();
|
||||||
|
|
||||||
|
var result = await packager.CreateBundleAsync(new BundleRequest
|
||||||
|
{
|
||||||
|
AlertId = "alert-123",
|
||||||
|
ActorId = "user@example.com",
|
||||||
|
IncludeVexHistory = true,
|
||||||
|
IncludeSbomSlice = true
|
||||||
|
});
|
||||||
|
|
||||||
|
// result.Content contains the tarball stream
|
||||||
|
// result.ManifestHash contains the verification hash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### API Endpoint
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/alerts/{alertId}/bundle/verify
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"bundle_hash": "sha256:abc123...",
|
||||||
|
"signature": "<optional DSSE signature>"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"is_valid": true,
|
||||||
|
"hash_valid": true,
|
||||||
|
"chain_valid": true,
|
||||||
|
"signature_valid": true,
|
||||||
|
"verified_at": "2024-12-15T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var verification = await packager.VerifyBundleAsync(
|
||||||
|
bundlePath: "/path/to/bundle.stella.bundle.tgz",
|
||||||
|
expectedHash: "sha256:abc123...");
|
||||||
|
|
||||||
|
if (!verification.IsValid)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Verification failed: {string.Join(", ", verification.Errors)}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export bundle
|
||||||
|
stellaops alert bundle export --alert-id alert-123 --output ./bundles/
|
||||||
|
|
||||||
|
# Verify bundle
|
||||||
|
stellaops alert bundle verify --file ./bundles/alert-123.stella.bundle.tgz
|
||||||
|
|
||||||
|
# Import bundle (air-gapped instance)
|
||||||
|
stellaops alert bundle import --file ./bundles/alert-123.stella.bundle.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Hash Verification**: Always verify bundle hash before processing
|
||||||
|
2. **Signature Validation**: Verify DSSE signature if present
|
||||||
|
3. **Content Validation**: Validate JSON schemas after extraction
|
||||||
|
4. **Size Limits**: Enforce maximum bundle size limits (default: 100MB)
|
||||||
|
5. **Path Traversal**: Tarball extraction must prevent path traversal attacks
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
| Format Version | Changes | Min StellaOps Version |
|
||||||
|
|----------------|---------|----------------------|
|
||||||
|
| 1.0.0 | Initial format | 1.0.0 |
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Evidence Bundle Envelope](./evidence-bundle-envelope.md)
|
||||||
|
- [DSSE Signing Guide](./dsse-signing.md)
|
||||||
|
- [Offline Kit Guide](../10_OFFLINE_KIT.md)
|
||||||
|
- [API Reference](../api/evidence-decision-api.openapi.yaml)
|
||||||
415
docs/airgap/proof-chain-verification.md
Normal file
415
docs/airgap/proof-chain-verification.md
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# Proof Chain Verification in Air-Gap Mode
|
||||||
|
|
||||||
|
> **Version**: 1.0.0
|
||||||
|
> **Last Updated**: 2025-12-17
|
||||||
|
> **Related**: [Proof Chain API](../api/proofs.md), [Key Rotation Runbook](../operations/key-rotation-runbook.md)
|
||||||
|
|
||||||
|
This document describes how to verify proof chains in air-gapped (offline) environments where Rekor transparency log access is unavailable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Proof chains in StellaOps consist of cryptographically-linked attestations:
|
||||||
|
1. **Evidence statements** - Raw vulnerability findings
|
||||||
|
2. **Reasoning statements** - Policy evaluation traces
|
||||||
|
3. **VEX verdict statements** - Final vulnerability status determinations
|
||||||
|
4. **Proof spine** - Merkle tree aggregating all components
|
||||||
|
|
||||||
|
In online mode, proof chains include Rekor inclusion proofs for transparency. In air-gap mode, verification proceeds without Rekor but maintains cryptographic integrity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Levels
|
||||||
|
|
||||||
|
### Level 1: Content-Addressed ID Verification
|
||||||
|
Verifies that content-addressed IDs match payload hashes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify a proof bundle ID
|
||||||
|
stellaops proof verify --offline \
|
||||||
|
--proof-bundle sha256:1a2b3c4d... \
|
||||||
|
--level content-id
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# ✓ Content-addressed ID verified
|
||||||
|
# ✓ Payload hash: sha256:1a2b3c4d...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 2: DSSE Signature Verification
|
||||||
|
Verifies DSSE envelope signatures against trust anchors.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify signatures with local trust anchors
|
||||||
|
stellaops proof verify --offline \
|
||||||
|
--proof-bundle sha256:1a2b3c4d... \
|
||||||
|
--anchor-file /path/to/trust-anchors.json \
|
||||||
|
--level signature
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# ✓ DSSE signature valid
|
||||||
|
# ✓ Signer: key-2025-prod
|
||||||
|
# ✓ Trust anchor: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 3: Merkle Path Verification
|
||||||
|
Verifies the proof spine merkle tree structure.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify merkle paths
|
||||||
|
stellaops proof verify --offline \
|
||||||
|
--proof-bundle sha256:1a2b3c4d... \
|
||||||
|
--level merkle
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# ✓ Merkle root verified
|
||||||
|
# ✓ Evidence paths: 3/3 valid
|
||||||
|
# ✓ Reasoning path: valid
|
||||||
|
# ✓ VEX verdict path: valid
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 4: Full Verification (Offline)
|
||||||
|
Performs all verification steps except Rekor.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full offline verification
|
||||||
|
stellaops proof verify --offline \
|
||||||
|
--proof-bundle sha256:1a2b3c4d... \
|
||||||
|
--anchor-file /path/to/trust-anchors.json
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# Proof Chain Verification
|
||||||
|
# ═══════════════════════
|
||||||
|
# ✓ Content-addressed IDs verified
|
||||||
|
# ✓ DSSE signatures verified (3 envelopes)
|
||||||
|
# ✓ Merkle paths verified
|
||||||
|
# ⊘ Rekor verification skipped (offline mode)
|
||||||
|
#
|
||||||
|
# Overall: VERIFIED (offline)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trust Anchor Distribution
|
||||||
|
|
||||||
|
In air-gap environments, trust anchors must be distributed out-of-band.
|
||||||
|
|
||||||
|
### Export Trust Anchors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the online system, export trust anchors
|
||||||
|
stellaops anchor export --format json > trust-anchors.json
|
||||||
|
|
||||||
|
# Verify export integrity
|
||||||
|
sha256sum trust-anchors.json > trust-anchors.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trust Anchor File Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"exportedAt": "2025-12-17T00:00:00Z",
|
||||||
|
"anchors": [
|
||||||
|
{
|
||||||
|
"trustAnchorId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"purlPattern": "pkg:*",
|
||||||
|
"allowedKeyids": ["key-2024-prod", "key-2025-prod"],
|
||||||
|
"allowedPredicateTypes": [
|
||||||
|
"evidence.stella/v1",
|
||||||
|
"reasoning.stella/v1",
|
||||||
|
"cdx-vex.stella/v1",
|
||||||
|
"proofspine.stella/v1"
|
||||||
|
],
|
||||||
|
"revokedKeys": ["key-2023-prod"],
|
||||||
|
"keyMaterial": {
|
||||||
|
"key-2024-prod": {
|
||||||
|
"algorithm": "ECDSA-P256",
|
||||||
|
"publicKey": "-----BEGIN PUBLIC KEY-----\n..."
|
||||||
|
},
|
||||||
|
"key-2025-prod": {
|
||||||
|
"algorithm": "ECDSA-P256",
|
||||||
|
"publicKey": "-----BEGIN PUBLIC KEY-----\n..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Trust Anchors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the air-gapped system
|
||||||
|
stellaops anchor import --file trust-anchors.json
|
||||||
|
|
||||||
|
# Verify import
|
||||||
|
stellaops anchor list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proof Bundle Distribution
|
||||||
|
|
||||||
|
### Export Proof Bundles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export a proof bundle for offline transfer
|
||||||
|
stellaops proof export \
|
||||||
|
--entry sha256:abc123:pkg:npm/lodash@4.17.21 \
|
||||||
|
--output proof-bundle.zip
|
||||||
|
|
||||||
|
# Bundle contents:
|
||||||
|
# proof-bundle.zip
|
||||||
|
# ├── proof-spine.json # The proof spine
|
||||||
|
# ├── evidence/ # Evidence statements
|
||||||
|
# │ ├── sha256_e1.json
|
||||||
|
# │ └── sha256_e2.json
|
||||||
|
# ├── reasoning.json # Reasoning statement
|
||||||
|
# ├── vex-verdict.json # VEX verdict statement
|
||||||
|
# ├── envelopes/ # DSSE envelopes
|
||||||
|
# │ ├── evidence-e1.dsse
|
||||||
|
# │ ├── evidence-e2.dsse
|
||||||
|
# │ ├── reasoning.dsse
|
||||||
|
# │ ├── vex-verdict.dsse
|
||||||
|
# │ └── proof-spine.dsse
|
||||||
|
# └── VERIFY.md # Verification instructions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Exported Bundle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the air-gapped system
|
||||||
|
stellaops proof verify --offline \
|
||||||
|
--bundle-file proof-bundle.zip \
|
||||||
|
--anchor-file trust-anchors.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Batch Verification
|
||||||
|
|
||||||
|
For audits, verify multiple proof bundles efficiently:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a verification manifest
|
||||||
|
cat > verify-manifest.json << 'EOF'
|
||||||
|
{
|
||||||
|
"bundles": [
|
||||||
|
"sha256:1a2b3c4d...",
|
||||||
|
"sha256:5e6f7g8h...",
|
||||||
|
"sha256:9i0j1k2l..."
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"checkRekor": false,
|
||||||
|
"failFast": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Run batch verification
|
||||||
|
stellaops proof verify-batch \
|
||||||
|
--manifest verify-manifest.json \
|
||||||
|
--anchor-file trust-anchors.json \
|
||||||
|
--output verification-report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification Report Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"verifiedAt": "2025-12-17T10:00:00Z",
|
||||||
|
"mode": "offline",
|
||||||
|
"anchorsUsed": ["550e8400..."],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"proofBundleId": "sha256:1a2b3c4d...",
|
||||||
|
"verified": true,
|
||||||
|
"checks": {
|
||||||
|
"contentId": true,
|
||||||
|
"signature": true,
|
||||||
|
"merklePath": true,
|
||||||
|
"rekorInclusion": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total": 3,
|
||||||
|
"verified": 3,
|
||||||
|
"failed": 0,
|
||||||
|
"skipped": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Rotation in Air-Gap Mode
|
||||||
|
|
||||||
|
When keys are rotated, trust anchor updates must be distributed:
|
||||||
|
|
||||||
|
### 1. Export Updated Anchors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On online system after key rotation
|
||||||
|
stellaops anchor export --since 2025-01-01 > anchor-update.json
|
||||||
|
sha256sum anchor-update.json > anchor-update.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Verify and Import Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On air-gapped system
|
||||||
|
sha256sum -c anchor-update.sha256
|
||||||
|
stellaops anchor import --file anchor-update.json --merge
|
||||||
|
|
||||||
|
# Verify key history
|
||||||
|
stellaops anchor show --anchor-id 550e8400... --show-history
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Temporal Verification
|
||||||
|
|
||||||
|
When verifying old proofs after key rotation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify proof signed with now-revoked key
|
||||||
|
stellaops proof verify --offline \
|
||||||
|
--proof-bundle sha256:old-proof... \
|
||||||
|
--anchor-file trust-anchors.json \
|
||||||
|
--at-time "2024-06-15T12:00:00Z"
|
||||||
|
|
||||||
|
# The verification uses key validity at the specified time
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Verification (No CLI)
|
||||||
|
|
||||||
|
For environments without the StellaOps CLI, manual verification is possible:
|
||||||
|
|
||||||
|
### 1. Verify Content-Addressed ID
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Extract payload from DSSE envelope
|
||||||
|
jq -r '.payload' proof-spine.dsse | base64 -d > payload.json
|
||||||
|
|
||||||
|
# Compute hash
|
||||||
|
sha256sum payload.json
|
||||||
|
# Compare with proof bundle ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Verify DSSE Signature
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||||
|
|
||||||
|
def verify_dsse(envelope_path, public_key_pem):
|
||||||
|
"""Verify a DSSE envelope signature."""
|
||||||
|
with open(envelope_path) as f:
|
||||||
|
envelope = json.load(f)
|
||||||
|
|
||||||
|
payload_type = envelope['payloadType']
|
||||||
|
payload = base64.b64decode(envelope['payload'])
|
||||||
|
|
||||||
|
# Build PAE (Pre-Authentication Encoding)
|
||||||
|
pae = f"DSSEv1 {len(payload_type)} {payload_type} {len(payload)} ".encode() + payload
|
||||||
|
|
||||||
|
public_key = load_pem_public_key(public_key_pem.encode())
|
||||||
|
|
||||||
|
for sig in envelope['signatures']:
|
||||||
|
signature = base64.b64decode(sig['sig'])
|
||||||
|
try:
|
||||||
|
public_key.verify(signature, pae, ec.ECDSA(hashes.SHA256()))
|
||||||
|
print(f"✓ Signature valid for keyid: {sig['keyid']}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Signature invalid: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verify Merkle Path
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def verify_merkle_path(leaf_hash, path, root_hash, leaf_index):
|
||||||
|
"""Verify a Merkle inclusion path."""
|
||||||
|
current = bytes.fromhex(leaf_hash)
|
||||||
|
index = leaf_index
|
||||||
|
|
||||||
|
for sibling in path:
|
||||||
|
sibling_bytes = bytes.fromhex(sibling)
|
||||||
|
if index % 2 == 0:
|
||||||
|
# Current is left child
|
||||||
|
combined = current + sibling_bytes
|
||||||
|
else:
|
||||||
|
# Current is right child
|
||||||
|
combined = sibling_bytes + current
|
||||||
|
current = hashlib.sha256(combined).digest()
|
||||||
|
index //= 2
|
||||||
|
|
||||||
|
computed_root = current.hex()
|
||||||
|
if computed_root == root_hash:
|
||||||
|
print("✓ Merkle path verified")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"✗ Merkle root mismatch: {computed_root} != {root_hash}")
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
Offline verification uses the same exit codes as online:
|
||||||
|
|
||||||
|
| Code | Meaning | CI/CD Action |
|
||||||
|
|------|---------|--------------|
|
||||||
|
| 0 | Verification passed | Proceed |
|
||||||
|
| 1 | Verification failed | Block |
|
||||||
|
| 2 | System error | Retry/investigate |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Missing Trust Anchor
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: No trust anchor found for keyid "key-2025-prod"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Import updated trust anchors from online system.
|
||||||
|
|
||||||
|
### Key Not Valid at Time
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Key "key-2024-prod" was revoked at 2024-12-01, before proof signature at 2025-01-15
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: This indicates the proof was signed after key revocation. Investigate the signature timestamp.
|
||||||
|
|
||||||
|
### Merkle Path Invalid
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Merkle path verification failed for evidence sha256:e1...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: The proof bundle may be corrupted. Re-export from online system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Proof Chain API Reference](../api/proofs.md)
|
||||||
|
- [Key Rotation Runbook](../operations/key-rotation-runbook.md)
|
||||||
|
- [Portable Evidence Bundle Verification](portable-evidence-bundle-verification.md)
|
||||||
|
- [Offline Bundle Format](offline-bundle-format.md)
|
||||||
368
docs/airgap/reachability-drift-airgap-workflows.md
Normal file
368
docs/airgap/reachability-drift-airgap-workflows.md
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
# Reachability Drift Air-Gap Workflows
|
||||||
|
|
||||||
|
**Sprint:** SPRINT_3600_0001_0001
|
||||||
|
**Task:** RDRIFT-MASTER-0006 - Document air-gap workflows for reachability drift
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Reachability Drift Detection can operate in fully air-gapped environments using offline bundles. This document describes the workflows for running reachability drift analysis without network connectivity, building on the Smart-Diff air-gap patterns.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Offline Kit** - Downloaded and verified (`stellaops offline kit download`)
|
||||||
|
2. **Feed Snapshots** - Pre-staged vulnerability feeds and surfaces
|
||||||
|
3. **Call Graph Cache** - Pre-extracted call graphs for target artifacts
|
||||||
|
4. **Vulnerability Surface Bundles** - Pre-computed trigger method mappings
|
||||||
|
|
||||||
|
## Key Differences from Online Mode
|
||||||
|
|
||||||
|
| Aspect | Online Mode | Air-Gap Mode |
|
||||||
|
|--------|-------------|--------------|
|
||||||
|
| Surface Queries | Real-time API | Local bundle lookup |
|
||||||
|
| Call Graph Extraction | On-demand | Pre-computed + cached |
|
||||||
|
| Graph Diff | Direct comparison | Bundle-to-bundle |
|
||||||
|
| Attestation | Online transparency log | Offline DSSE bundle |
|
||||||
|
| Metrics | Telemetry enabled | Local-only metrics |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow 1: Offline Reachability Drift Analysis
|
||||||
|
|
||||||
|
### Step 1: Prepare Offline Bundle with Call Graphs
|
||||||
|
|
||||||
|
On a connected machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download offline kit with reachability bundles
|
||||||
|
stellaops offline kit download \
|
||||||
|
--output /path/to/offline-bundle \
|
||||||
|
--include-feeds nvd,osv,epss \
|
||||||
|
--include-surfaces \
|
||||||
|
--feed-date 2025-01-15
|
||||||
|
|
||||||
|
# Pre-extract call graphs for known artifacts
|
||||||
|
stellaops callgraph extract \
|
||||||
|
--artifact registry.example.com/app:v1 \
|
||||||
|
--artifact registry.example.com/app:v2 \
|
||||||
|
--output /path/to/offline-bundle/callgraphs \
|
||||||
|
--languages dotnet,nodejs,java,go,python
|
||||||
|
|
||||||
|
# Include vulnerability surface bundles
|
||||||
|
stellaops surfaces export \
|
||||||
|
--cve-list /path/to/known-cves.txt \
|
||||||
|
--output /path/to/offline-bundle/surfaces \
|
||||||
|
--format ndjson
|
||||||
|
|
||||||
|
# Package for transfer
|
||||||
|
stellaops offline kit package \
|
||||||
|
--input /path/to/offline-bundle \
|
||||||
|
--output stellaops-reach-offline-2025-01-15.tar.gz \
|
||||||
|
--sign
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Transfer to Air-Gapped Environment
|
||||||
|
|
||||||
|
Transfer the bundle using approved media:
|
||||||
|
- USB drive (scanned and approved)
|
||||||
|
- Optical media (DVD/Blu-ray)
|
||||||
|
- Data diode
|
||||||
|
|
||||||
|
### Step 3: Import Bundle
|
||||||
|
|
||||||
|
On the air-gapped machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify bundle signature
|
||||||
|
stellaops offline kit verify \
|
||||||
|
--input stellaops-reach-offline-2025-01-15.tar.gz \
|
||||||
|
--public-key /path/to/signing-key.pub
|
||||||
|
|
||||||
|
# Extract and configure
|
||||||
|
stellaops offline kit import \
|
||||||
|
--input stellaops-reach-offline-2025-01-15.tar.gz \
|
||||||
|
--data-dir /opt/stellaops/data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Run Reachability Drift Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set offline mode
|
||||||
|
export STELLAOPS_OFFLINE=true
|
||||||
|
export STELLAOPS_DATA_DIR=/opt/stellaops/data
|
||||||
|
export STELLAOPS_SURFACES_DIR=/opt/stellaops/data/surfaces
|
||||||
|
export STELLAOPS_CALLGRAPH_CACHE=/opt/stellaops/data/callgraphs
|
||||||
|
|
||||||
|
# Run reachability drift
|
||||||
|
stellaops reach-drift \
|
||||||
|
--base-scan scan-v1.json \
|
||||||
|
--current-scan scan-v2.json \
|
||||||
|
--base-callgraph callgraph-v1.json \
|
||||||
|
--current-callgraph callgraph-v2.json \
|
||||||
|
--output drift-report.json \
|
||||||
|
--format json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow 2: Pre-Computed Drift Export
|
||||||
|
|
||||||
|
For environments that cannot run the full analysis, pre-compute drift results on a connected machine and export them for review.
|
||||||
|
|
||||||
|
### Step 1: Pre-Compute Drift Results
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On connected machine: compute drift
|
||||||
|
stellaops reach-drift \
|
||||||
|
--base-scan scan-v1.json \
|
||||||
|
--current-scan scan-v2.json \
|
||||||
|
--output drift-results.json \
|
||||||
|
--include-witnesses \
|
||||||
|
--include-paths
|
||||||
|
|
||||||
|
# Generate offline viewer bundle
|
||||||
|
stellaops offline viewer export \
|
||||||
|
--drift-report drift-results.json \
|
||||||
|
--output drift-viewer-bundle.html \
|
||||||
|
--self-contained
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Transfer and Review
|
||||||
|
|
||||||
|
The self-contained HTML viewer can be opened in any browser on the air-gapped machine without additional dependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow 3: Incremental Call Graph Updates
|
||||||
|
|
||||||
|
For environments that need to update call graphs without full re-extraction.
|
||||||
|
|
||||||
|
### Step 1: Export Graph Delta
|
||||||
|
|
||||||
|
On connected machine after code changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Extract delta since last snapshot
|
||||||
|
stellaops callgraph delta \
|
||||||
|
--base-snapshot callgraph-v1.json \
|
||||||
|
--current-source /path/to/code \
|
||||||
|
--output graph-delta.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Apply Delta in Air-Gap
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Merge delta into existing graph
|
||||||
|
stellaops callgraph merge \
|
||||||
|
--base /opt/stellaops/data/callgraphs/app-v1.json \
|
||||||
|
--delta graph-delta.json \
|
||||||
|
--output /opt/stellaops/data/callgraphs/app-v2.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bundle Contents
|
||||||
|
|
||||||
|
### Call Graph Bundle Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
callgraphs/
|
||||||
|
├── manifest.json # Bundle metadata
|
||||||
|
├── checksums.sha256 # Content hashes
|
||||||
|
├── app-v1/
|
||||||
|
│ ├── snapshot.json # CallGraphSnapshot
|
||||||
|
│ ├── entrypoints.json # Entrypoint index
|
||||||
|
│ └── sinks.json # Sink index
|
||||||
|
└── app-v2/
|
||||||
|
├── snapshot.json
|
||||||
|
├── entrypoints.json
|
||||||
|
└── sinks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Surface Bundle Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
surfaces/
|
||||||
|
├── manifest.json # Bundle metadata
|
||||||
|
├── checksums.sha256 # Content hashes
|
||||||
|
├── by-cve/
|
||||||
|
│ ├── CVE-2024-1234.json # Surface + triggers
|
||||||
|
│ └── CVE-2024-5678.json
|
||||||
|
└── by-package/
|
||||||
|
├── nuget/
|
||||||
|
│ └── Newtonsoft.Json/
|
||||||
|
│ └── surfaces.ndjson
|
||||||
|
└── npm/
|
||||||
|
└── lodash/
|
||||||
|
└── surfaces.ndjson
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offline Surface Query
|
||||||
|
|
||||||
|
When running in air-gap mode, the surface query service automatically uses local bundles:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Configuration for air-gap mode
|
||||||
|
services.AddSingleton<ISurfaceQueryService>(sp =>
|
||||||
|
{
|
||||||
|
var options = sp.GetRequiredService<IOptions<AirGapOptions>>().Value;
|
||||||
|
|
||||||
|
if (options.Enabled)
|
||||||
|
{
|
||||||
|
return new OfflineSurfaceQueryService(
|
||||||
|
options.SurfacesBundlePath,
|
||||||
|
sp.GetRequiredService<ILogger<OfflineSurfaceQueryService>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sp.GetRequiredService<OnlineSurfaceQueryService>();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attestation in Air-Gap Mode
|
||||||
|
|
||||||
|
Reachability drift results can be attested even in offline mode using pre-provisioned signing keys:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sign drift results with offline key
|
||||||
|
stellaops attest sign \
|
||||||
|
--input drift-results.json \
|
||||||
|
--predicate-type https://stellaops.io/attestation/reachability-drift/v1 \
|
||||||
|
--key /opt/stellaops/keys/signing-key.pem \
|
||||||
|
--output drift-attestation.dsse.json
|
||||||
|
|
||||||
|
# Verify attestation (offline)
|
||||||
|
stellaops attest verify \
|
||||||
|
--input drift-attestation.dsse.json \
|
||||||
|
--trust-root /opt/stellaops/keys/trust-root.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Staleness Considerations
|
||||||
|
|
||||||
|
### Call Graph Freshness
|
||||||
|
|
||||||
|
Call graphs should be re-extracted when:
|
||||||
|
- Source code changes significantly
|
||||||
|
- Dependencies are updated
|
||||||
|
- Framework versions change
|
||||||
|
|
||||||
|
Maximum recommended staleness: **7 days** for active development, **30 days** for stable releases.
|
||||||
|
|
||||||
|
### Surface Bundle Freshness
|
||||||
|
|
||||||
|
Surface bundles should be updated when:
|
||||||
|
- New CVEs are published
|
||||||
|
- Vulnerability details are refined
|
||||||
|
- Trigger methods are updated
|
||||||
|
|
||||||
|
Maximum recommended staleness: **24 hours** for high-security environments, **7 days** for standard environments.
|
||||||
|
|
||||||
|
### Staleness Indicators
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check bundle freshness
|
||||||
|
stellaops offline status \
|
||||||
|
--data-dir /opt/stellaops/data
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# Bundle Type | Last Updated | Age | Status
|
||||||
|
# -----------------|---------------------|--------|--------
|
||||||
|
# NVD Feed | 2025-01-15T00:00:00 | 3 days | OK
|
||||||
|
# OSV Feed | 2025-01-15T00:00:00 | 3 days | OK
|
||||||
|
# Surfaces | 2025-01-14T12:00:00 | 4 days | WARNING
|
||||||
|
# Call Graphs (v1) | 2025-01-10T08:00:00 | 8 days | STALE
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Determinism Requirements
|
||||||
|
|
||||||
|
All offline workflows must produce deterministic results:
|
||||||
|
|
||||||
|
1. **Call Graph Extraction** - Same source produces identical graph hash
|
||||||
|
2. **Drift Detection** - Same inputs produce identical drift report
|
||||||
|
3. **Path Witnesses** - Same reachability query produces identical paths
|
||||||
|
4. **Attestation** - Signature over canonical JSON (sorted keys, no whitespace)
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify determinism
|
||||||
|
stellaops reach-drift \
|
||||||
|
--base-scan scan-v1.json \
|
||||||
|
--current-scan scan-v2.json \
|
||||||
|
--output drift-1.json
|
||||||
|
|
||||||
|
stellaops reach-drift \
|
||||||
|
--base-scan scan-v1.json \
|
||||||
|
--current-scan scan-v2.json \
|
||||||
|
--output drift-2.json
|
||||||
|
|
||||||
|
# Must be identical
|
||||||
|
diff drift-1.json drift-2.json
|
||||||
|
# (no output = identical)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Missing Surface Data
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: No surface found for CVE-2024-1234 in package pkg:nuget/Newtonsoft.Json@12.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resolution:** Update surface bundle or fall back to package-API-level reachability:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stellaops reach-drift \
|
||||||
|
--fallback-mode package-api \
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Call Graph Extraction Failure
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Failed to extract call graph - missing language support for 'rust'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resolution:** Pre-extract call graphs on a machine with required tooling, or skip unsupported languages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stellaops callgraph extract \
|
||||||
|
--skip-unsupported \
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bundle Signature Verification Failure
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Bundle signature invalid - public key mismatch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resolution:** Ensure correct public key is used, or re-download bundle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List available trust roots
|
||||||
|
stellaops offline trust-roots list
|
||||||
|
|
||||||
|
# Import new trust root (requires approval)
|
||||||
|
stellaops offline trust-roots import \
|
||||||
|
--key new-signing-key.pub \
|
||||||
|
--fingerprint <expected-fingerprint>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Smart-Diff Air-Gap Workflows](smart-diff-airgap-workflows.md)
|
||||||
|
- [Offline Bundle Format](offline-bundle-format.md)
|
||||||
|
- [Air-Gap Operations](operations.md)
|
||||||
|
- [Staleness and Time](staleness-and-time.md)
|
||||||
|
- [Sealing and Egress](sealing-and-egress.md)
|
||||||
39
docs/airgap/runbooks/quarantine-investigation.md
Normal file
39
docs/airgap/runbooks/quarantine-investigation.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# AirGap Quarantine Investigation Runbook
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Quarantine preserves failed bundle imports for offline forensic analysis. It keeps the original bundle and the verification context (reason + logs) so operators can diagnose tampering, trust-root drift, or packaging issues without re-running in an online environment.
|
||||||
|
|
||||||
|
## Location & Structure
|
||||||
|
Default root: `/updates/quarantine`
|
||||||
|
|
||||||
|
Per-tenant layout:
|
||||||
|
`/updates/quarantine/<tenantId>/<timestamp>-<reason>-<id>/`
|
||||||
|
|
||||||
|
Removal staging:
|
||||||
|
`/updates/quarantine/<tenantId>/.removed/<quarantineId>/`
|
||||||
|
|
||||||
|
## Files in a quarantine entry
|
||||||
|
- `bundle.tar.zst` - the original bundle as provided
|
||||||
|
- `manifest.json` - bundle manifest (when available)
|
||||||
|
- `verification.log` - validation step output (TUF/DSSE/Merkle/rotation/monotonicity, etc.)
|
||||||
|
- `failure-reason.txt` - human-readable failure summary (reason + timestamp + metadata)
|
||||||
|
- `quarantine.json` - structured metadata for listing/automation
|
||||||
|
|
||||||
|
## Investigation steps (offline)
|
||||||
|
1. Identify the tenant and locate the quarantine root on the importer host.
|
||||||
|
2. Pick the newest quarantine entry for the tenant (timestamp prefix).
|
||||||
|
3. Read `failure-reason.txt` first to capture the top-level reason and metadata.
|
||||||
|
4. Review `verification.log` for the precise failing step.
|
||||||
|
5. If needed, extract and inspect `bundle.tar.zst` in an isolated workspace (no network).
|
||||||
|
6. Decide whether the entry should be retained (for audit) or removed after investigation.
|
||||||
|
|
||||||
|
## Removal & Retention
|
||||||
|
- Removal requires a human-provided reason (audit trail). Implementations should use the quarantine service’s remove operation which moves entries under `.removed/`.
|
||||||
|
- Retention and quota controls are configured via `AirGap:Quarantine` settings (root, TTL, max size); TTL cleanup can remove entries older than the retention period.
|
||||||
|
|
||||||
|
## Common failure categories
|
||||||
|
- `tuf:*` - invalid/expired metadata or snapshot hash mismatch
|
||||||
|
- `dsse:*` - signature invalid or trust root mismatch
|
||||||
|
- `merkle-*` - payload entry set invalid or empty
|
||||||
|
- `rotation:*` - root rotation policy failure (dual approval, no-op rotation, etc.)
|
||||||
|
- `version-non-monotonic:*` - rollback prevention triggered (force activation requires a justification)
|
||||||
287
docs/airgap/smart-diff-airgap-workflows.md
Normal file
287
docs/airgap/smart-diff-airgap-workflows.md
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
# Smart-Diff Air-Gap Workflows
|
||||||
|
|
||||||
|
**Sprint:** SPRINT_3500_0001_0001
|
||||||
|
**Task:** SDIFF-MASTER-0006 - Document air-gap workflows for smart-diff
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Smart-Diff can operate in fully air-gapped environments using offline bundles. This document describes the workflows for running smart-diff analysis without network connectivity.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Offline Kit** - Downloaded and verified (`stellaops offline kit download`)
|
||||||
|
2. **Feed Snapshots** - Pre-staged vulnerability feeds
|
||||||
|
3. **SBOM Cache** - Pre-generated SBOMs for target artifacts
|
||||||
|
|
||||||
|
## Workflow 1: Offline Smart-Diff Analysis
|
||||||
|
|
||||||
|
### Step 1: Prepare Offline Bundle
|
||||||
|
|
||||||
|
On a connected machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download offline kit with feeds
|
||||||
|
stellaops offline kit download \
|
||||||
|
--output /path/to/offline-bundle \
|
||||||
|
--include-feeds nvd,osv,epss \
|
||||||
|
--feed-date 2025-01-15
|
||||||
|
|
||||||
|
# Include SBOMs for known artifacts
|
||||||
|
stellaops offline sbom generate \
|
||||||
|
--artifact registry.example.com/app:v1 \
|
||||||
|
--artifact registry.example.com/app:v2 \
|
||||||
|
--output /path/to/offline-bundle/sboms
|
||||||
|
|
||||||
|
# Package for transfer
|
||||||
|
stellaops offline kit package \
|
||||||
|
--input /path/to/offline-bundle \
|
||||||
|
--output stellaops-offline-2025-01-15.tar.gz \
|
||||||
|
--sign
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Transfer to Air-Gapped Environment
|
||||||
|
|
||||||
|
Transfer the bundle using approved media:
|
||||||
|
- USB drive (scanned and approved)
|
||||||
|
- Optical media (DVD/Blu-ray)
|
||||||
|
- Data diode
|
||||||
|
|
||||||
|
### Step 3: Import Bundle
|
||||||
|
|
||||||
|
On the air-gapped machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify bundle signature
|
||||||
|
stellaops offline kit verify \
|
||||||
|
--input stellaops-offline-2025-01-15.tar.gz \
|
||||||
|
--public-key /path/to/signing-key.pub
|
||||||
|
|
||||||
|
# Extract and configure
|
||||||
|
stellaops offline kit import \
|
||||||
|
--input stellaops-offline-2025-01-15.tar.gz \
|
||||||
|
--data-dir /opt/stellaops/data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Run Smart-Diff
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set offline mode
|
||||||
|
export STELLAOPS_OFFLINE=true
|
||||||
|
export STELLAOPS_DATA_DIR=/opt/stellaops/data
|
||||||
|
|
||||||
|
# Run smart-diff
|
||||||
|
stellaops smart-diff \
|
||||||
|
--base sbom:app-v1.json \
|
||||||
|
--target sbom:app-v2.json \
|
||||||
|
--output smart-diff-report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow 2: Pre-Computed Smart-Diff Export
|
||||||
|
|
||||||
|
For environments where even running analysis tools is restricted.
|
||||||
|
|
||||||
|
### Step 1: Prepare Artifacts (Connected Machine)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate SBOMs
|
||||||
|
stellaops sbom generate --artifact app:v1 --output app-v1-sbom.json
|
||||||
|
stellaops sbom generate --artifact app:v2 --output app-v2-sbom.json
|
||||||
|
|
||||||
|
# Run smart-diff with full proof bundle
|
||||||
|
stellaops smart-diff \
|
||||||
|
--base app-v1-sbom.json \
|
||||||
|
--target app-v2-sbom.json \
|
||||||
|
--output-dir ./smart-diff-export \
|
||||||
|
--include-proofs \
|
||||||
|
--include-evidence \
|
||||||
|
--format bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Verify Export Contents
|
||||||
|
|
||||||
|
The export bundle contains:
|
||||||
|
```
|
||||||
|
smart-diff-export/
|
||||||
|
├── manifest.json # Signed manifest
|
||||||
|
├── base-sbom.json # Base SBOM (hash verified)
|
||||||
|
├── target-sbom.json # Target SBOM (hash verified)
|
||||||
|
├── diff-results.json # Smart-diff findings
|
||||||
|
├── sarif-report.json # SARIF formatted output
|
||||||
|
├── proofs/
|
||||||
|
│ ├── ledger.json # Proof ledger
|
||||||
|
│ └── nodes/ # Individual proof nodes
|
||||||
|
├── evidence/
|
||||||
|
│ ├── reachability.json # Reachability evidence
|
||||||
|
│ ├── vex-statements.json # VEX statements
|
||||||
|
│ └── hardening.json # Binary hardening data
|
||||||
|
└── signature.dsse # DSSE envelope
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Import and Verify (Air-Gapped Machine)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify bundle integrity
|
||||||
|
stellaops verify-bundle \
|
||||||
|
--input smart-diff-export \
|
||||||
|
--public-key /path/to/trusted-key.pub
|
||||||
|
|
||||||
|
# View results
|
||||||
|
stellaops smart-diff show \
|
||||||
|
--bundle smart-diff-export \
|
||||||
|
--format table
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow 3: Incremental Feed Updates
|
||||||
|
|
||||||
|
### Step 1: Generate Delta Feed
|
||||||
|
|
||||||
|
On connected machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate delta since last sync
|
||||||
|
stellaops offline feed delta \
|
||||||
|
--since 2025-01-10 \
|
||||||
|
--output feed-delta-2025-01-15.tar.gz \
|
||||||
|
--sign
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Apply Delta (Air-Gapped)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Import delta
|
||||||
|
stellaops offline feed apply \
|
||||||
|
--input feed-delta-2025-01-15.tar.gz \
|
||||||
|
--verify
|
||||||
|
|
||||||
|
# Trigger score replay for affected scans
|
||||||
|
stellaops score replay-all \
|
||||||
|
--trigger feed-update \
|
||||||
|
--dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `STELLAOPS_OFFLINE` | Enable offline mode | `false` |
|
||||||
|
| `STELLAOPS_DATA_DIR` | Local data directory | `~/.stellaops` |
|
||||||
|
| `STELLAOPS_FEED_DIR` | Feed snapshot directory | `$DATA_DIR/feeds` |
|
||||||
|
| `STELLAOPS_SBOM_CACHE` | SBOM cache directory | `$DATA_DIR/sboms` |
|
||||||
|
| `STELLAOPS_SKIP_NETWORK` | Block network requests | `false` |
|
||||||
|
| `STELLAOPS_REQUIRE_SIGNATURES` | Require signed data | `true` |
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ~/.stellaops/config.yaml
|
||||||
|
offline:
|
||||||
|
enabled: true
|
||||||
|
data_dir: /opt/stellaops/data
|
||||||
|
require_signatures: true
|
||||||
|
|
||||||
|
feeds:
|
||||||
|
source: local
|
||||||
|
path: /opt/stellaops/data/feeds
|
||||||
|
|
||||||
|
sbom:
|
||||||
|
cache_dir: /opt/stellaops/data/sboms
|
||||||
|
|
||||||
|
network:
|
||||||
|
allow_list: [] # Empty = no network
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Verify Feed Freshness
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check feed dates
|
||||||
|
stellaops offline status
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# Feed Status (Offline Mode)
|
||||||
|
# ─────────────────────────────
|
||||||
|
# NVD: 2025-01-15 (2 days old)
|
||||||
|
# OSV: 2025-01-15 (2 days old)
|
||||||
|
# EPSS: 2025-01-14 (3 days old)
|
||||||
|
# KEV: 2025-01-15 (2 days old)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Proof Integrity
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify smart-diff proofs
|
||||||
|
stellaops smart-diff verify \
|
||||||
|
--input smart-diff-report.json \
|
||||||
|
--proof-bundle ./proofs
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# ✓ Manifest hash verified
|
||||||
|
# ✓ All proof nodes valid
|
||||||
|
# ✓ Root hash matches: sha256:abc123...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Determinism Guarantees
|
||||||
|
|
||||||
|
Offline smart-diff maintains determinism by:
|
||||||
|
|
||||||
|
1. **Content-addressed feeds** - Same feed hash = same results
|
||||||
|
2. **Frozen timestamps** - All timestamps use manifest creation time
|
||||||
|
3. **No network randomness** - No external API calls
|
||||||
|
4. **Stable sorting** - Deterministic output ordering
|
||||||
|
|
||||||
|
### Reproducibility Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run twice and compare
|
||||||
|
stellaops smart-diff --base a.json --target b.json --output run1.json
|
||||||
|
stellaops smart-diff --base a.json --target b.json --output run2.json
|
||||||
|
|
||||||
|
# Compare hashes
|
||||||
|
sha256sum run1.json run2.json
|
||||||
|
# abc123... run1.json
|
||||||
|
# abc123... run2.json (identical)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error: Feed not found
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Feed 'nvd' not found in offline data directory
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Ensure feed was included in offline kit:
|
||||||
|
```bash
|
||||||
|
stellaops offline kit status
|
||||||
|
ls $STELLAOPS_FEED_DIR/nvd/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: Network request blocked
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Network request blocked in offline mode: api.osv.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** This is expected behavior. Ensure all required data is in offline bundle.
|
||||||
|
|
||||||
|
### Error: Signature verification failed
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Bundle signature verification failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Ensure correct public key is configured:
|
||||||
|
```bash
|
||||||
|
stellaops offline kit verify \
|
||||||
|
--input bundle.tar.gz \
|
||||||
|
--public-key /path/to/correct-key.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Offline Kit Guide](../10_OFFLINE_KIT.md)
|
||||||
|
- [Determinism Requirements](../product-advisories/14-Dec-2025%20-%20Determinism%20and%20Reproducibility%20Technical%20Reference.md)
|
||||||
|
- [Smart-Diff API](../api/scanner-api.md)
|
||||||
366
docs/airgap/triage-airgap-workflows.md
Normal file
366
docs/airgap/triage-airgap-workflows.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# Triage Air-Gap Workflows
|
||||||
|
|
||||||
|
**Sprint:** SPRINT_3600_0001_0001
|
||||||
|
**Task:** TRI-MASTER-0006 - Document air-gap triage workflows
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes how to perform vulnerability triage in fully air-gapped environments. The triage workflow supports offline evidence bundles, decision capture, and replay token generation.
|
||||||
|
|
||||||
|
## Workflow 1: Offline Triage with Evidence Bundles
|
||||||
|
|
||||||
|
### Step 1: Export Evidence Bundle (Connected Machine)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export triage bundle for specific findings
|
||||||
|
stellaops triage export \
|
||||||
|
--scan-id scan-12345678 \
|
||||||
|
--findings CVE-2024-1234,CVE-2024-5678 \
|
||||||
|
--include-evidence \
|
||||||
|
--include-graph \
|
||||||
|
--output triage-bundle.stella.bundle.tgz
|
||||||
|
|
||||||
|
# Export entire scan for offline review
|
||||||
|
stellaops triage export \
|
||||||
|
--scan-id scan-12345678 \
|
||||||
|
--all-findings \
|
||||||
|
--output full-triage-bundle.stella.bundle.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Bundle Contents
|
||||||
|
|
||||||
|
The `.stella.bundle.tgz` archive contains:
|
||||||
|
|
||||||
|
```
|
||||||
|
triage-bundle.stella.bundle.tgz/
|
||||||
|
├── manifest.json # Signed bundle manifest
|
||||||
|
├── findings/
|
||||||
|
│ ├── index.json # Finding list with IDs
|
||||||
|
│ ├── CVE-2024-1234.json # Finding details
|
||||||
|
│ └── CVE-2024-5678.json
|
||||||
|
├── evidence/
|
||||||
|
│ ├── reachability/ # Reachability proofs
|
||||||
|
│ ├── callstack/ # Call stack snippets
|
||||||
|
│ ├── vex/ # VEX/CSAF statements
|
||||||
|
│ └── provenance/ # Provenance data
|
||||||
|
├── graph/
|
||||||
|
│ ├── nodes.ndjson # Dependency graph nodes
|
||||||
|
│ └── edges.ndjson # Graph edges
|
||||||
|
├── feeds/
|
||||||
|
│ └── snapshot.json # Feed snapshot metadata
|
||||||
|
└── signature.dsse # DSSE envelope
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Transfer to Air-Gapped Environment
|
||||||
|
|
||||||
|
Transfer using approved methods:
|
||||||
|
- USB media (security scanned)
|
||||||
|
- Optical media
|
||||||
|
- Data diode
|
||||||
|
|
||||||
|
### Step 4: Import and Verify
|
||||||
|
|
||||||
|
On the air-gapped machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify bundle integrity
|
||||||
|
stellaops triage verify-bundle \
|
||||||
|
--input triage-bundle.stella.bundle.tgz \
|
||||||
|
--public-key /path/to/signing-key.pub
|
||||||
|
|
||||||
|
# Import for offline triage
|
||||||
|
stellaops triage import \
|
||||||
|
--input triage-bundle.stella.bundle.tgz \
|
||||||
|
--workspace /opt/stellaops/triage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Perform Offline Triage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List findings in bundle
|
||||||
|
stellaops triage list \
|
||||||
|
--workspace /opt/stellaops/triage
|
||||||
|
|
||||||
|
# View finding with evidence
|
||||||
|
stellaops triage show CVE-2024-1234 \
|
||||||
|
--workspace /opt/stellaops/triage \
|
||||||
|
--show-evidence
|
||||||
|
|
||||||
|
# Make triage decision
|
||||||
|
stellaops triage decide CVE-2024-1234 \
|
||||||
|
--workspace /opt/stellaops/triage \
|
||||||
|
--status not_affected \
|
||||||
|
--justification "Code path is unreachable due to config gating" \
|
||||||
|
--reviewer "security-team"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Export Decisions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export decisions for sync back
|
||||||
|
stellaops triage export-decisions \
|
||||||
|
--workspace /opt/stellaops/triage \
|
||||||
|
--output decisions-2025-01-15.json \
|
||||||
|
--sign
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Sync Decisions (Connected Machine)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Import and apply decisions
|
||||||
|
stellaops triage import-decisions \
|
||||||
|
--input decisions-2025-01-15.json \
|
||||||
|
--verify \
|
||||||
|
--apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow 2: Batch Offline Triage
|
||||||
|
|
||||||
|
For high-volume environments.
|
||||||
|
|
||||||
|
### Step 1: Export Batch Bundle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export all untriaged findings
|
||||||
|
stellaops triage export-batch \
|
||||||
|
--query "status=untriaged AND priority>=0.7" \
|
||||||
|
--limit 100 \
|
||||||
|
--output batch-triage-2025-01-15.stella.bundle.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Offline Batch Processing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive batch triage
|
||||||
|
stellaops triage batch \
|
||||||
|
--workspace /opt/stellaops/triage \
|
||||||
|
--input batch-triage-2025-01-15.stella.bundle.tgz
|
||||||
|
|
||||||
|
# Keyboard shortcuts enabled:
|
||||||
|
# j/k - Next/Previous finding
|
||||||
|
# a - Accept (affected)
|
||||||
|
# n - Not affected
|
||||||
|
# w - Will not fix
|
||||||
|
# f - False positive
|
||||||
|
# u - Undo last decision
|
||||||
|
# q - Quit (saves progress)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Export and Sync
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export batch decisions
|
||||||
|
stellaops triage export-decisions \
|
||||||
|
--workspace /opt/stellaops/triage \
|
||||||
|
--format json \
|
||||||
|
--sign \
|
||||||
|
--output batch-decisions.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow 3: Evidence-First Offline Review
|
||||||
|
|
||||||
|
### Step 1: Pre-compute Evidence
|
||||||
|
|
||||||
|
On connected machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate evidence for all high-priority findings
|
||||||
|
stellaops evidence generate \
|
||||||
|
--scan-id scan-12345678 \
|
||||||
|
--priority-min 0.7 \
|
||||||
|
--output-dir ./evidence-pack
|
||||||
|
|
||||||
|
# Include:
|
||||||
|
# - Reachability analysis
|
||||||
|
# - Call stack traces
|
||||||
|
# - VEX lookups
|
||||||
|
# - Dependency graph snippets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Package with Findings
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stellaops triage package \
|
||||||
|
--scan-id scan-12345678 \
|
||||||
|
--evidence-dir ./evidence-pack \
|
||||||
|
--output evidence-triage.stella.bundle.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Offline Review with Evidence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Evidence-first view
|
||||||
|
stellaops triage show CVE-2024-1234 \
|
||||||
|
--workspace /opt/stellaops/triage \
|
||||||
|
--evidence-first
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# CVE-2024-1234 · lodash@4.17.20
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
#
|
||||||
|
# EVIDENCE SUMMARY
|
||||||
|
# ────────────────
|
||||||
|
# Reachability: EXECUTED (tier 2/3)
|
||||||
|
# └─ main.js:42 → utils.js:15 → lodash/merge
|
||||||
|
#
|
||||||
|
# Call Stack:
|
||||||
|
# 1. main.js:42 handleRequest()
|
||||||
|
# 2. utils.js:15 mergeConfig()
|
||||||
|
# 3. lodash:merge <vulnerable>
|
||||||
|
#
|
||||||
|
# VEX Status: No statement found
|
||||||
|
# EPSS: 0.45 (Medium)
|
||||||
|
# KEV: No
|
||||||
|
#
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Press [a]ffected, [n]ot affected, [s]kip...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `STELLAOPS_OFFLINE` | Enable offline mode | `false` |
|
||||||
|
| `STELLAOPS_TRIAGE_WORKSPACE` | Triage workspace path | `~/.stellaops/triage` |
|
||||||
|
| `STELLAOPS_BUNDLE_VERIFY` | Verify bundle signatures | `true` |
|
||||||
|
| `STELLAOPS_DECISION_SIGN` | Sign exported decisions | `true` |
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ~/.stellaops/triage.yaml
|
||||||
|
offline:
|
||||||
|
enabled: true
|
||||||
|
workspace: /opt/stellaops/triage
|
||||||
|
bundle_verify: true
|
||||||
|
|
||||||
|
decisions:
|
||||||
|
require_justification: true
|
||||||
|
sign_exports: true
|
||||||
|
|
||||||
|
keyboard:
|
||||||
|
enabled: true
|
||||||
|
vim_mode: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bundle Format Specification
|
||||||
|
|
||||||
|
### manifest.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"type": "triage-bundle",
|
||||||
|
"created_at": "2025-01-15T10:00:00Z",
|
||||||
|
"scan_id": "scan-12345678",
|
||||||
|
"finding_count": 25,
|
||||||
|
"feed_snapshot": "sha256:abc123...",
|
||||||
|
"graph_revision": "sha256:def456...",
|
||||||
|
"signatures": {
|
||||||
|
"manifest": "sha256:ghi789...",
|
||||||
|
"dsse_envelope": "signature.dsse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"finding_id": "finding-12345678",
|
||||||
|
"vuln_key": "CVE-2024-1234:pkg:npm/lodash@4.17.20",
|
||||||
|
"status": "not_affected",
|
||||||
|
"justification": "Code path gated by feature flag",
|
||||||
|
"reviewer": "security-team",
|
||||||
|
"decided_at": "2025-01-15T14:30:00Z",
|
||||||
|
"replay_token": "rt_abc123...",
|
||||||
|
"evidence_refs": [
|
||||||
|
"evidence/reachability/CVE-2024-1234.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Replay Tokens
|
||||||
|
|
||||||
|
Each decision generates a replay token for audit trail:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View replay token
|
||||||
|
stellaops triage show-token rt_abc123...
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# Replay Token: rt_abc123...
|
||||||
|
# ─────────────────────────────
|
||||||
|
# Finding: CVE-2024-1234
|
||||||
|
# Decision: not_affected
|
||||||
|
# Evidence Hash: sha256:xyz789...
|
||||||
|
# Feed Snapshot: sha256:abc123...
|
||||||
|
# Decided: 2025-01-15T14:30:00Z
|
||||||
|
# Reviewer: security-team
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stellaops triage verify-token rt_abc123... \
|
||||||
|
--public-key /path/to/key.pub
|
||||||
|
|
||||||
|
# ✓ Token signature valid
|
||||||
|
# ✓ Evidence hash matches
|
||||||
|
# ✓ Feed snapshot verified
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error: Bundle signature invalid
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Bundle signature verification failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Ensure the correct public key is used:
|
||||||
|
```bash
|
||||||
|
stellaops triage verify-bundle \
|
||||||
|
--input bundle.tgz \
|
||||||
|
--public-key /path/to/correct-key.pub \
|
||||||
|
--verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: Evidence not found
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Evidence for CVE-2024-1234 not included in bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Re-export with evidence:
|
||||||
|
```bash
|
||||||
|
stellaops triage export \
|
||||||
|
--scan-id scan-12345678 \
|
||||||
|
--findings CVE-2024-1234 \
|
||||||
|
--include-evidence \
|
||||||
|
--output bundle.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: Decision sync conflict
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Finding CVE-2024-1234 has newer decision on server
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Review and resolve:
|
||||||
|
```bash
|
||||||
|
stellaops triage import-decisions \
|
||||||
|
--input decisions.json \
|
||||||
|
--conflict-mode review
|
||||||
|
|
||||||
|
# Options: keep-local, keep-server, newest, review
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Offline Kit Guide](../10_OFFLINE_KIT.md)
|
||||||
|
- [Triage API Reference](../api/triage-api.md)
|
||||||
|
- [Keyboard Shortcuts](../ui/keyboard-shortcuts.md)
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
The Aggregation-Only Contract (AOC) guard library enforces the canonical ingestion
|
The Aggregation-Only Contract (AOC) guard library enforces the canonical ingestion
|
||||||
rules described in `docs/ingestion/aggregation-only-contract.md`. Service owners
|
rules described in `docs/ingestion/aggregation-only-contract.md`. Service owners
|
||||||
should use the guard whenever raw advisory or VEX payloads are accepted so that
|
should use the guard whenever raw advisory or VEX payloads are accepted so that
|
||||||
forbidden fields are rejected long before they reach MongoDB.
|
forbidden fields are rejected long before they reach PostgreSQL.
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
|
|||||||
434
docs/api/evidence-decision-api.openapi.yaml
Normal file
434
docs/api/evidence-decision-api.openapi.yaml
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: StellaOps Evidence & Decision API
|
||||||
|
description: |
|
||||||
|
REST API for evidence retrieval and decision recording.
|
||||||
|
Sprint: SPRINT_3602_0001_0001
|
||||||
|
version: 1.0.0
|
||||||
|
license:
|
||||||
|
name: AGPL-3.0-or-later
|
||||||
|
url: https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: /v1
|
||||||
|
description: API v1
|
||||||
|
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/alerts:
|
||||||
|
get:
|
||||||
|
operationId: listAlerts
|
||||||
|
summary: List alerts with filtering and pagination
|
||||||
|
tags:
|
||||||
|
- Alerts
|
||||||
|
parameters:
|
||||||
|
- name: band
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [critical, high, medium, low, info]
|
||||||
|
- name: severity
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: status
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [open, acknowledged, resolved, suppressed]
|
||||||
|
- name: artifactId
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: vulnId
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: componentPurl
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 50
|
||||||
|
maximum: 500
|
||||||
|
- name: offset
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 0
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Alert list
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AlertListResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
|
||||||
|
/alerts/{alertId}:
|
||||||
|
get:
|
||||||
|
operationId: getAlert
|
||||||
|
summary: Get alert details
|
||||||
|
tags:
|
||||||
|
- Alerts
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/alertId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Alert details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AlertSummary'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
/alerts/{alertId}/evidence:
|
||||||
|
get:
|
||||||
|
operationId: getAlertEvidence
|
||||||
|
summary: Get evidence bundle for an alert
|
||||||
|
tags:
|
||||||
|
- Evidence
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/alertId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Evidence payload
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/EvidencePayloadResponse'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
/alerts/{alertId}/decisions:
|
||||||
|
post:
|
||||||
|
operationId: recordDecision
|
||||||
|
summary: Record a decision for an alert
|
||||||
|
tags:
|
||||||
|
- Decisions
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/alertId'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DecisionRequest'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Decision recorded
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DecisionResponse'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
|
||||||
|
/alerts/{alertId}/audit:
|
||||||
|
get:
|
||||||
|
operationId: getAlertAudit
|
||||||
|
summary: Get audit timeline for an alert
|
||||||
|
tags:
|
||||||
|
- Audit
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/alertId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Audit timeline
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuditTimelineResponse'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
/alerts/{alertId}/bundle:
|
||||||
|
get:
|
||||||
|
operationId: downloadAlertBundle
|
||||||
|
summary: Download evidence bundle as tar.gz
|
||||||
|
tags:
|
||||||
|
- Bundles
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/alertId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Evidence bundle file
|
||||||
|
content:
|
||||||
|
application/gzip:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
/alerts/{alertId}/bundle/verify:
|
||||||
|
post:
|
||||||
|
operationId: verifyAlertBundle
|
||||||
|
summary: Verify evidence bundle integrity
|
||||||
|
tags:
|
||||||
|
- Bundles
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/alertId'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BundleVerificationRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Verification result
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BundleVerificationResponse'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
bearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
alertId:
|
||||||
|
name: alertId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Alert identifier
|
||||||
|
|
||||||
|
responses:
|
||||||
|
BadRequest:
|
||||||
|
description: Bad request
|
||||||
|
content:
|
||||||
|
application/problem+json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ProblemDetails'
|
||||||
|
Unauthorized:
|
||||||
|
description: Unauthorized
|
||||||
|
NotFound:
|
||||||
|
description: Resource not found
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
AlertListResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- items
|
||||||
|
- total_count
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/AlertSummary'
|
||||||
|
total_count:
|
||||||
|
type: integer
|
||||||
|
next_page_token:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
AlertSummary:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- alert_id
|
||||||
|
- artifact_id
|
||||||
|
- vuln_id
|
||||||
|
- severity
|
||||||
|
- band
|
||||||
|
- status
|
||||||
|
- created_at
|
||||||
|
properties:
|
||||||
|
alert_id:
|
||||||
|
type: string
|
||||||
|
artifact_id:
|
||||||
|
type: string
|
||||||
|
vuln_id:
|
||||||
|
type: string
|
||||||
|
component_purl:
|
||||||
|
type: string
|
||||||
|
severity:
|
||||||
|
type: string
|
||||||
|
band:
|
||||||
|
type: string
|
||||||
|
enum: [critical, high, medium, low, info]
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [open, acknowledged, resolved, suppressed]
|
||||||
|
score:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
decision_count:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
EvidencePayloadResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- alert_id
|
||||||
|
properties:
|
||||||
|
alert_id:
|
||||||
|
type: string
|
||||||
|
reachability:
|
||||||
|
$ref: '#/components/schemas/EvidenceSection'
|
||||||
|
callstack:
|
||||||
|
$ref: '#/components/schemas/EvidenceSection'
|
||||||
|
vex:
|
||||||
|
$ref: '#/components/schemas/EvidenceSection'
|
||||||
|
|
||||||
|
EvidenceSection:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
hash:
|
||||||
|
type: string
|
||||||
|
source:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
DecisionRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- decision
|
||||||
|
- rationale
|
||||||
|
properties:
|
||||||
|
decision:
|
||||||
|
type: string
|
||||||
|
enum: [accept_risk, mitigate, suppress, escalate]
|
||||||
|
rationale:
|
||||||
|
type: string
|
||||||
|
minLength: 10
|
||||||
|
maxLength: 2000
|
||||||
|
justification_code:
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
|
||||||
|
DecisionResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- decision_id
|
||||||
|
- alert_id
|
||||||
|
- decision
|
||||||
|
- recorded_at
|
||||||
|
properties:
|
||||||
|
decision_id:
|
||||||
|
type: string
|
||||||
|
alert_id:
|
||||||
|
type: string
|
||||||
|
decision:
|
||||||
|
type: string
|
||||||
|
rationale:
|
||||||
|
type: string
|
||||||
|
recorded_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
recorded_by:
|
||||||
|
type: string
|
||||||
|
replay_token:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
AuditTimelineResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- alert_id
|
||||||
|
- events
|
||||||
|
- total_count
|
||||||
|
properties:
|
||||||
|
alert_id:
|
||||||
|
type: string
|
||||||
|
events:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/AuditEvent'
|
||||||
|
total_count:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
AuditEvent:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- event_id
|
||||||
|
- event_type
|
||||||
|
- timestamp
|
||||||
|
properties:
|
||||||
|
event_id:
|
||||||
|
type: string
|
||||||
|
event_type:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
actor:
|
||||||
|
type: string
|
||||||
|
details:
|
||||||
|
type: object
|
||||||
|
replay_token:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
BundleVerificationRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- bundle_hash
|
||||||
|
properties:
|
||||||
|
bundle_hash:
|
||||||
|
type: string
|
||||||
|
description: SHA-256 hash of the bundle
|
||||||
|
signature:
|
||||||
|
type: string
|
||||||
|
description: Optional DSSE signature
|
||||||
|
|
||||||
|
BundleVerificationResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- alert_id
|
||||||
|
- is_valid
|
||||||
|
- verified_at
|
||||||
|
properties:
|
||||||
|
alert_id:
|
||||||
|
type: string
|
||||||
|
is_valid:
|
||||||
|
type: boolean
|
||||||
|
verified_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
signature_valid:
|
||||||
|
type: boolean
|
||||||
|
hash_valid:
|
||||||
|
type: boolean
|
||||||
|
chain_valid:
|
||||||
|
type: boolean
|
||||||
|
errors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
ProblemDetails:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
detail:
|
||||||
|
type: string
|
||||||
|
instance:
|
||||||
|
type: string
|
||||||
102
docs/api/orchestrator-first-signal.md
Normal file
102
docs/api/orchestrator-first-signal.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Orchestrator · First Signal API
|
||||||
|
|
||||||
|
Provides a fast “first meaningful signal” for a run (TTFS), with caching and ETag-based conditional requests.
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
`GET /api/v1/orchestrator/runs/{runId}/first-signal`
|
||||||
|
|
||||||
|
### Required headers
|
||||||
|
- `X-Tenant-Id`: tenant identifier (string)
|
||||||
|
|
||||||
|
### Optional headers
|
||||||
|
- `If-None-Match`: weak ETag from a previous 200 response (supports multiple values)
|
||||||
|
|
||||||
|
## Responses
|
||||||
|
|
||||||
|
### 200 OK
|
||||||
|
Returns the first signal payload and a weak ETag.
|
||||||
|
|
||||||
|
Response headers:
|
||||||
|
- `ETag`: weak ETag (for `If-None-Match`)
|
||||||
|
- `Cache-Control: private, max-age=60`
|
||||||
|
- `Cache-Status: hit|miss`
|
||||||
|
- `X-FirstSignal-Source: snapshot|cold_start` (best-effort diagnostics)
|
||||||
|
|
||||||
|
Body (`application/json`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||||
|
"firstSignal": {
|
||||||
|
"type": "started",
|
||||||
|
"stage": "unknown",
|
||||||
|
"step": null,
|
||||||
|
"message": "Run started",
|
||||||
|
"at": "2025-12-15T12:00:10+00:00",
|
||||||
|
"artifact": { "kind": "run", "range": null }
|
||||||
|
},
|
||||||
|
"summaryEtag": "W/\"...\""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 204 No Content
|
||||||
|
Run exists but no signal is available yet (e.g., run has no jobs).
|
||||||
|
|
||||||
|
### 304 Not Modified
|
||||||
|
Returned when `If-None-Match` matches the current ETag.
|
||||||
|
|
||||||
|
### 404 Not Found
|
||||||
|
Run does not exist for the resolved tenant.
|
||||||
|
|
||||||
|
### 400 Bad Request
|
||||||
|
Missing/invalid tenant header or invalid parameters.
|
||||||
|
|
||||||
|
## ETag semantics
|
||||||
|
- Weak ETags are computed from a deterministic, canonical hash of the stable signal content.
|
||||||
|
- Per-request diagnostics (e.g., cache hit/miss) are intentionally excluded from the ETag material.
|
||||||
|
|
||||||
|
## Streaming (SSE)
|
||||||
|
The run stream emits `first_signal` events when the signal changes:
|
||||||
|
|
||||||
|
`GET /api/v1/orchestrator/stream/runs/{runId}`
|
||||||
|
|
||||||
|
Event type:
|
||||||
|
- `first_signal`
|
||||||
|
|
||||||
|
Payload shape:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||||
|
"etag": "W/\"...\"",
|
||||||
|
"signal": { "version": "1.0", "signalId": "...", "jobId": "...", "timestamp": "...", "kind": 1, "phase": 6, "scope": { "type": "run", "id": "..." }, "summary": "...", "etaSeconds": null, "lastKnownOutcome": null, "nextActions": null, "diagnostics": { "cacheHit": false, "source": "cold_start", "correlationId": "" } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
`appsettings.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"FirstSignal": {
|
||||||
|
"Cache": {
|
||||||
|
"Backend": "inmemory",
|
||||||
|
"TtlSeconds": 86400,
|
||||||
|
"SlidingExpiration": true,
|
||||||
|
"KeyPrefix": "orchestrator:first_signal:"
|
||||||
|
},
|
||||||
|
"ColdPath": {
|
||||||
|
"TimeoutMs": 3000
|
||||||
|
},
|
||||||
|
"SnapshotWriter": {
|
||||||
|
"Enabled": false,
|
||||||
|
"TenantId": null,
|
||||||
|
"PollIntervalSeconds": 10,
|
||||||
|
"MaxRunsPerTick": 50,
|
||||||
|
"LookbackMinutes": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messaging": {
|
||||||
|
"transport": "inmemory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
622
docs/api/proofs-openapi.yaml
Normal file
622
docs/api/proofs-openapi.yaml
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: StellaOps Proof Chain API
|
||||||
|
version: 1.0.0
|
||||||
|
description: |
|
||||||
|
API for proof chain operations including proof spine creation, verification receipts,
|
||||||
|
VEX attestations, and trust anchor management.
|
||||||
|
|
||||||
|
The proof chain provides cryptographic evidence linking SBOM entries to vulnerability
|
||||||
|
assessments through attestable DSSE envelopes.
|
||||||
|
|
||||||
|
license:
|
||||||
|
name: AGPL-3.0-or-later
|
||||||
|
url: https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: https://api.stellaops.dev/v1
|
||||||
|
description: Production API
|
||||||
|
- url: http://localhost:5000/v1
|
||||||
|
description: Local development
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Proofs
|
||||||
|
description: Proof spine and receipt operations
|
||||||
|
- name: Anchors
|
||||||
|
description: Trust anchor management
|
||||||
|
- name: Verify
|
||||||
|
description: Proof verification endpoints
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/proofs/{entry}/spine:
|
||||||
|
post:
|
||||||
|
operationId: createProofSpine
|
||||||
|
summary: Create proof spine for SBOM entry
|
||||||
|
description: |
|
||||||
|
Assembles a merkle-rooted proof spine from evidence, reasoning, and VEX verdict
|
||||||
|
for an SBOM entry. Returns a content-addressed proof bundle ID.
|
||||||
|
tags: [Proofs]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
- mtls: []
|
||||||
|
parameters:
|
||||||
|
- name: entry
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}:pkg:.+'
|
||||||
|
description: SBOMEntryID in format sha256:<hash>:pkg:<purl>
|
||||||
|
example: "sha256:abc123...def:pkg:npm/lodash@4.17.21"
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CreateSpineRequest'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Proof spine created successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CreateSpineResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'422':
|
||||||
|
$ref: '#/components/responses/ValidationError'
|
||||||
|
|
||||||
|
get:
|
||||||
|
operationId: getProofSpine
|
||||||
|
summary: Get proof spine for SBOM entry
|
||||||
|
description: Retrieves the existing proof spine for an SBOM entry.
|
||||||
|
tags: [Proofs]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: entry
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}:pkg:.+'
|
||||||
|
description: SBOMEntryID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Proof spine retrieved
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ProofSpineDto'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
/proofs/{entry}/receipt:
|
||||||
|
get:
|
||||||
|
operationId: getProofReceipt
|
||||||
|
summary: Get verification receipt
|
||||||
|
description: |
|
||||||
|
Retrieves a verification receipt for the SBOM entry's proof spine.
|
||||||
|
The receipt includes merkle proof paths and signature verification status.
|
||||||
|
tags: [Proofs]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: entry
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}:pkg:.+'
|
||||||
|
description: SBOMEntryID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Verification receipt
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/VerificationReceiptDto'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
/proofs/{entry}/vex:
|
||||||
|
get:
|
||||||
|
operationId: getProofVex
|
||||||
|
summary: Get VEX attestation for entry
|
||||||
|
description: Retrieves the VEX verdict attestation for the SBOM entry.
|
||||||
|
tags: [Proofs]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: entry
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}:pkg:.+'
|
||||||
|
description: SBOMEntryID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: VEX attestation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/VexAttestationDto'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
/anchors:
|
||||||
|
get:
|
||||||
|
operationId: listAnchors
|
||||||
|
summary: List trust anchors
|
||||||
|
description: Lists all configured trust anchors with their status.
|
||||||
|
tags: [Anchors]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of trust anchors
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
anchors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/TrustAnchorDto'
|
||||||
|
|
||||||
|
post:
|
||||||
|
operationId: createAnchor
|
||||||
|
summary: Create trust anchor
|
||||||
|
description: Creates a new trust anchor with the specified public key.
|
||||||
|
tags: [Anchors]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CreateAnchorRequest'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Trust anchor created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TrustAnchorDto'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'409':
|
||||||
|
description: Anchor already exists
|
||||||
|
|
||||||
|
/anchors/{anchorId}:
|
||||||
|
get:
|
||||||
|
operationId: getAnchor
|
||||||
|
summary: Get trust anchor
|
||||||
|
description: Retrieves a specific trust anchor by ID.
|
||||||
|
tags: [Anchors]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: anchorId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Trust anchor ID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Trust anchor details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TrustAnchorDto'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
delete:
|
||||||
|
operationId: deleteAnchor
|
||||||
|
summary: Delete trust anchor
|
||||||
|
description: Deletes a trust anchor (soft delete, marks as revoked).
|
||||||
|
tags: [Anchors]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: anchorId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Trust anchor ID
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Anchor deleted
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
|
/verify:
|
||||||
|
post:
|
||||||
|
operationId: verifyProofBundle
|
||||||
|
summary: Verify proof bundle
|
||||||
|
description: |
|
||||||
|
Performs full verification of a proof bundle including:
|
||||||
|
- DSSE signature verification
|
||||||
|
- Content-addressed ID recomputation
|
||||||
|
- Merkle path verification
|
||||||
|
- Optional Rekor inclusion proof verification
|
||||||
|
tags: [Verify]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/VerifyRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Verification result
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/VerificationResultDto'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
|
||||||
|
/verify/batch:
|
||||||
|
post:
|
||||||
|
operationId: verifyBatch
|
||||||
|
summary: Verify multiple proof bundles
|
||||||
|
description: Performs batch verification of multiple proof bundles.
|
||||||
|
tags: [Verify]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- bundles
|
||||||
|
properties:
|
||||||
|
bundles:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/VerifyRequest'
|
||||||
|
maxItems: 100
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Batch verification results
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
results:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/VerificationResultDto'
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
bearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
description: Authority-issued OpToken
|
||||||
|
mtls:
|
||||||
|
type: mutualTLS
|
||||||
|
description: Mutual TLS with client certificate
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
CreateSpineRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- evidenceIds
|
||||||
|
- reasoningId
|
||||||
|
- vexVerdictId
|
||||||
|
- policyVersion
|
||||||
|
properties:
|
||||||
|
evidenceIds:
|
||||||
|
type: array
|
||||||
|
description: Content-addressed IDs of evidence statements
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}$'
|
||||||
|
minItems: 1
|
||||||
|
example: ["sha256:e7f8a9b0c1d2..."]
|
||||||
|
reasoningId:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}$'
|
||||||
|
description: Content-addressed ID of reasoning statement
|
||||||
|
example: "sha256:f0e1d2c3b4a5..."
|
||||||
|
vexVerdictId:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}$'
|
||||||
|
description: Content-addressed ID of VEX verdict statement
|
||||||
|
example: "sha256:d4c5b6a7e8f9..."
|
||||||
|
policyVersion:
|
||||||
|
type: string
|
||||||
|
pattern: '^v[0-9]+\.[0-9]+\.[0-9]+$'
|
||||||
|
description: Version of the policy used
|
||||||
|
example: "v1.2.3"
|
||||||
|
|
||||||
|
CreateSpineResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- proofBundleId
|
||||||
|
properties:
|
||||||
|
proofBundleId:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}$'
|
||||||
|
description: Content-addressed ID of the created proof bundle (merkle root)
|
||||||
|
example: "sha256:1a2b3c4d5e6f..."
|
||||||
|
receiptUrl:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
description: URL to retrieve the verification receipt
|
||||||
|
example: "/proofs/sha256:abc:pkg:npm/lodash@4.17.21/receipt"
|
||||||
|
|
||||||
|
ProofSpineDto:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- sbomEntryId
|
||||||
|
- proofBundleId
|
||||||
|
- evidenceIds
|
||||||
|
- reasoningId
|
||||||
|
- vexVerdictId
|
||||||
|
- policyVersion
|
||||||
|
- createdAt
|
||||||
|
properties:
|
||||||
|
sbomEntryId:
|
||||||
|
type: string
|
||||||
|
description: The SBOM entry this spine covers
|
||||||
|
proofBundleId:
|
||||||
|
type: string
|
||||||
|
description: Merkle root hash of the proof bundle
|
||||||
|
evidenceIds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Sorted list of evidence IDs
|
||||||
|
reasoningId:
|
||||||
|
type: string
|
||||||
|
description: Reasoning statement ID
|
||||||
|
vexVerdictId:
|
||||||
|
type: string
|
||||||
|
description: VEX verdict statement ID
|
||||||
|
policyVersion:
|
||||||
|
type: string
|
||||||
|
description: Policy version used
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Creation timestamp (UTC ISO-8601)
|
||||||
|
|
||||||
|
VerificationReceiptDto:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- graphRevisionId
|
||||||
|
- findingKey
|
||||||
|
- decision
|
||||||
|
- createdAt
|
||||||
|
- verified
|
||||||
|
properties:
|
||||||
|
graphRevisionId:
|
||||||
|
type: string
|
||||||
|
description: Graph revision ID this receipt was computed from
|
||||||
|
findingKey:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sbomEntryId:
|
||||||
|
type: string
|
||||||
|
vulnerabilityId:
|
||||||
|
type: string
|
||||||
|
rule:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
decision:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
verdict:
|
||||||
|
type: string
|
||||||
|
enum: [pass, fail, warn, skip]
|
||||||
|
severity:
|
||||||
|
type: string
|
||||||
|
reasoning:
|
||||||
|
type: string
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
verified:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the receipt signature verified correctly
|
||||||
|
|
||||||
|
VexAttestationDto:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- sbomEntryId
|
||||||
|
- vulnerabilityId
|
||||||
|
- status
|
||||||
|
- vexVerdictId
|
||||||
|
properties:
|
||||||
|
sbomEntryId:
|
||||||
|
type: string
|
||||||
|
vulnerabilityId:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [not_affected, affected, fixed, under_investigation]
|
||||||
|
justification:
|
||||||
|
type: string
|
||||||
|
policyVersion:
|
||||||
|
type: string
|
||||||
|
reasoningId:
|
||||||
|
type: string
|
||||||
|
vexVerdictId:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
TrustAnchorDto:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- keyId
|
||||||
|
- algorithm
|
||||||
|
- status
|
||||||
|
- createdAt
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Unique anchor identifier
|
||||||
|
keyId:
|
||||||
|
type: string
|
||||||
|
description: Key identifier (fingerprint)
|
||||||
|
algorithm:
|
||||||
|
type: string
|
||||||
|
enum: [ECDSA-P256, Ed25519, RSA-2048, RSA-4096]
|
||||||
|
description: Signing algorithm
|
||||||
|
publicKey:
|
||||||
|
type: string
|
||||||
|
description: PEM-encoded public key
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [active, revoked, expired]
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
revokedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
CreateAnchorRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- keyId
|
||||||
|
- algorithm
|
||||||
|
- publicKey
|
||||||
|
properties:
|
||||||
|
keyId:
|
||||||
|
type: string
|
||||||
|
description: Key identifier
|
||||||
|
algorithm:
|
||||||
|
type: string
|
||||||
|
enum: [ECDSA-P256, Ed25519, RSA-2048, RSA-4096]
|
||||||
|
publicKey:
|
||||||
|
type: string
|
||||||
|
description: PEM-encoded public key
|
||||||
|
|
||||||
|
VerifyRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- proofBundleId
|
||||||
|
properties:
|
||||||
|
proofBundleId:
|
||||||
|
type: string
|
||||||
|
pattern: '^sha256:[a-f0-9]{64}$'
|
||||||
|
description: The proof bundle ID to verify
|
||||||
|
checkRekor:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: Whether to verify Rekor inclusion proofs
|
||||||
|
anchorIds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Specific trust anchors to use for verification
|
||||||
|
|
||||||
|
VerificationResultDto:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- proofBundleId
|
||||||
|
- verified
|
||||||
|
- checks
|
||||||
|
properties:
|
||||||
|
proofBundleId:
|
||||||
|
type: string
|
||||||
|
verified:
|
||||||
|
type: boolean
|
||||||
|
description: Overall verification result
|
||||||
|
checks:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
signatureValid:
|
||||||
|
type: boolean
|
||||||
|
description: DSSE signature verification passed
|
||||||
|
idRecomputed:
|
||||||
|
type: boolean
|
||||||
|
description: Content-addressed IDs recomputed correctly
|
||||||
|
merklePathValid:
|
||||||
|
type: boolean
|
||||||
|
description: Merkle path verification passed
|
||||||
|
rekorInclusionValid:
|
||||||
|
type: boolean
|
||||||
|
description: Rekor inclusion proof verified (if checked)
|
||||||
|
errors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Error messages if verification failed
|
||||||
|
verifiedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
responses:
|
||||||
|
BadRequest:
|
||||||
|
description: Invalid request
|
||||||
|
content:
|
||||||
|
application/problem+json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
detail:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
example: 400
|
||||||
|
|
||||||
|
NotFound:
|
||||||
|
description: Resource not found
|
||||||
|
content:
|
||||||
|
application/problem+json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
detail:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
example: 404
|
||||||
|
|
||||||
|
ValidationError:
|
||||||
|
description: Validation error
|
||||||
|
content:
|
||||||
|
application/problem+json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
detail:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
example: 422
|
||||||
|
errors:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
333
docs/api/proofs.md
Normal file
333
docs/api/proofs.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
# Proof Chain API Reference
|
||||||
|
|
||||||
|
> **Version**: 1.0.0
|
||||||
|
> **OpenAPI Spec**: [`proofs-openapi.yaml`](./proofs-openapi.yaml)
|
||||||
|
|
||||||
|
The Proof Chain API provides endpoints for creating and verifying cryptographic proof bundles that link SBOM entries to vulnerability assessments through attestable DSSE envelopes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The proof chain creates an auditable, cryptographically-verifiable trail from vulnerability evidence through policy reasoning to VEX verdicts. Each component is signed with DSSE envelopes and aggregated into a merkle-rooted proof spine.
|
||||||
|
|
||||||
|
### Proof Chain Components
|
||||||
|
|
||||||
|
| Component | Predicate Type | Purpose |
|
||||||
|
|-----------|----------------|---------|
|
||||||
|
| **Evidence** | `evidence.stella/v1` | Raw findings from scanners/feeds |
|
||||||
|
| **Reasoning** | `reasoning.stella/v1` | Policy evaluation trace |
|
||||||
|
| **VEX Verdict** | `cdx-vex.stella/v1` | Final VEX status determination |
|
||||||
|
| **Proof Spine** | `proofspine.stella/v1` | Merkle aggregation of all components |
|
||||||
|
| **Verdict Receipt** | `verdict.stella/v1` | Human-readable verification receipt |
|
||||||
|
|
||||||
|
### Content-Addressed IDs
|
||||||
|
|
||||||
|
All proof chain components use content-addressed identifiers:
|
||||||
|
|
||||||
|
```
|
||||||
|
Format: sha256:<64-hex-chars>
|
||||||
|
Example: sha256:e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6...
|
||||||
|
```
|
||||||
|
|
||||||
|
IDs are computed by:
|
||||||
|
1. Canonicalizing the JSON payload (RFC 8785/JCS)
|
||||||
|
2. Computing SHA-256 hash
|
||||||
|
3. Prefixing with `sha256:`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All endpoints require authentication via:
|
||||||
|
|
||||||
|
- **Bearer Token**: Authority-issued OpToken with appropriate scopes
|
||||||
|
- **mTLS**: Mutual TLS with client certificate (service-to-service)
|
||||||
|
|
||||||
|
Required scopes:
|
||||||
|
- `proofs.read` - Read proof bundles and receipts
|
||||||
|
- `proofs.write` - Create proof spines
|
||||||
|
- `anchors.manage` - Manage trust anchors
|
||||||
|
- `proofs.verify` - Perform verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### Proofs
|
||||||
|
|
||||||
|
#### POST /proofs/{entry}/spine
|
||||||
|
|
||||||
|
Create a proof spine for an SBOM entry.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `entry` (path, required): SBOMEntryID in format `sha256:<hash>:pkg:<purl>`
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"evidenceIds": ["sha256:e7f8a9b0..."],
|
||||||
|
"reasoningId": "sha256:f0e1d2c3...",
|
||||||
|
"vexVerdictId": "sha256:d4c5b6a7...",
|
||||||
|
"policyVersion": "v1.2.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201 Created):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"proofBundleId": "sha256:1a2b3c4d...",
|
||||||
|
"receiptUrl": "/proofs/sha256:abc:pkg:npm/lodash@4.17.21/receipt"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `400 Bad Request`: Invalid SBOM entry ID format
|
||||||
|
- `404 Not Found`: Evidence, reasoning, or VEX verdict not found
|
||||||
|
- `422 Unprocessable Entity`: Validation error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### GET /proofs/{entry}/spine
|
||||||
|
|
||||||
|
Get the proof spine for an SBOM entry.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `entry` (path, required): SBOMEntryID
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
|
||||||
|
"proofBundleId": "sha256:1a2b3c4d...",
|
||||||
|
"evidenceIds": ["sha256:e7f8a9b0..."],
|
||||||
|
"reasoningId": "sha256:f0e1d2c3...",
|
||||||
|
"vexVerdictId": "sha256:d4c5b6a7...",
|
||||||
|
"policyVersion": "v1.2.3",
|
||||||
|
"createdAt": "2025-12-17T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### GET /proofs/{entry}/receipt
|
||||||
|
|
||||||
|
Get the verification receipt for an SBOM entry's proof spine.
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"graphRevisionId": "grv_sha256:9f8e7d6c...",
|
||||||
|
"findingKey": {
|
||||||
|
"sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
|
||||||
|
"vulnerabilityId": "CVE-2025-1234"
|
||||||
|
},
|
||||||
|
"rule": {
|
||||||
|
"id": "critical-vuln-block",
|
||||||
|
"version": "v1.0.0"
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"verdict": "pass",
|
||||||
|
"severity": "none",
|
||||||
|
"reasoning": "Not affected - vulnerable code not present"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-17T10:00:00Z",
|
||||||
|
"verified": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### GET /proofs/{entry}/vex
|
||||||
|
|
||||||
|
Get the VEX attestation for an SBOM entry.
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
|
||||||
|
"vulnerabilityId": "CVE-2025-1234",
|
||||||
|
"status": "not_affected",
|
||||||
|
"justification": "vulnerable_code_not_present",
|
||||||
|
"policyVersion": "v1.2.3",
|
||||||
|
"reasoningId": "sha256:f0e1d2c3...",
|
||||||
|
"vexVerdictId": "sha256:d4c5b6a7..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Trust Anchors
|
||||||
|
|
||||||
|
#### GET /anchors
|
||||||
|
|
||||||
|
List all configured trust anchors.
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"anchors": [
|
||||||
|
{
|
||||||
|
"id": "anchor-001",
|
||||||
|
"keyId": "sha256:abc123...",
|
||||||
|
"algorithm": "ECDSA-P256",
|
||||||
|
"status": "active",
|
||||||
|
"createdAt": "2025-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### POST /anchors
|
||||||
|
|
||||||
|
Create a new trust anchor.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"keyId": "sha256:abc123...",
|
||||||
|
"algorithm": "ECDSA-P256",
|
||||||
|
"publicKey": "-----BEGIN PUBLIC KEY-----\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201 Created):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "anchor-002",
|
||||||
|
"keyId": "sha256:abc123...",
|
||||||
|
"algorithm": "ECDSA-P256",
|
||||||
|
"status": "active",
|
||||||
|
"createdAt": "2025-12-17T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### DELETE /anchors/{anchorId}
|
||||||
|
|
||||||
|
Delete (revoke) a trust anchor.
|
||||||
|
|
||||||
|
**Response:** `204 No Content`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
#### POST /verify
|
||||||
|
|
||||||
|
Perform full verification of a proof bundle.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"proofBundleId": "sha256:1a2b3c4d...",
|
||||||
|
"checkRekor": true,
|
||||||
|
"anchorIds": ["anchor-001"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"proofBundleId": "sha256:1a2b3c4d...",
|
||||||
|
"verified": true,
|
||||||
|
"checks": {
|
||||||
|
"signatureValid": true,
|
||||||
|
"idRecomputed": true,
|
||||||
|
"merklePathValid": true,
|
||||||
|
"rekorInclusionValid": true
|
||||||
|
},
|
||||||
|
"errors": [],
|
||||||
|
"verifiedAt": "2025-12-17T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verification Steps:**
|
||||||
|
1. **Signature Verification**: Verify DSSE envelope signatures against trust anchors
|
||||||
|
2. **ID Recomputation**: Recompute content-addressed IDs and compare
|
||||||
|
3. **Merkle Path Verification**: Verify proof bundle merkle tree construction
|
||||||
|
4. **Rekor Inclusion**: Verify transparency log inclusion proof (if enabled)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### POST /verify/batch
|
||||||
|
|
||||||
|
Verify multiple proof bundles in a single request.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bundles": [
|
||||||
|
{ "proofBundleId": "sha256:1a2b3c4d...", "checkRekor": true },
|
||||||
|
{ "proofBundleId": "sha256:5e6f7g8h...", "checkRekor": false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{ "proofBundleId": "sha256:1a2b3c4d...", "verified": true, "checks": {...} },
|
||||||
|
{ "proofBundleId": "sha256:5e6f7g8h...", "verified": false, "errors": ["..."] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All errors follow RFC 7807 Problem Details format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Validation Error",
|
||||||
|
"detail": "Evidence ID sha256:abc... not found",
|
||||||
|
"status": 422,
|
||||||
|
"errors": {
|
||||||
|
"evidenceIds[0]": ["Evidence not found"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| 400 | Invalid request format or parameters |
|
||||||
|
| 401 | Authentication required |
|
||||||
|
| 403 | Insufficient permissions |
|
||||||
|
| 404 | Resource not found |
|
||||||
|
| 409 | Conflict (e.g., anchor already exists) |
|
||||||
|
| 422 | Validation error |
|
||||||
|
| 500 | Internal server error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offline Verification
|
||||||
|
|
||||||
|
For air-gapped environments, verification can be performed without Rekor:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"proofBundleId": "sha256:1a2b3c4d...",
|
||||||
|
"checkRekor": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This skips Rekor inclusion proof verification but still performs:
|
||||||
|
- DSSE signature verification
|
||||||
|
- Content-addressed ID recomputation
|
||||||
|
- Merkle path verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Proof Chain Predicates](../modules/attestor/architecture.md#predicate-types) - DSSE predicate type specifications
|
||||||
|
- [Content-Addressed IDs](../modules/attestor/architecture.md#content-addressed-identifier-formats) - ID generation rules
|
||||||
|
- [Attestor Architecture](../modules/attestor/architecture.md) - Full attestor module documentation
|
||||||
682
docs/api/scanner-score-proofs-api.md
Normal file
682
docs/api/scanner-score-proofs-api.md
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
# Scanner WebService API — Score Proofs & Reachability Extensions
|
||||||
|
|
||||||
|
**Version**: 2.0
|
||||||
|
**Base URL**: `/api/v1/scanner`
|
||||||
|
**Authentication**: Bearer token (OpTok with DPoP/mTLS)
|
||||||
|
**Sprint**: SPRINT_3500_0002_0003, SPRINT_3500_0003_0003
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document specifies API extensions to `Scanner.WebService` for:
|
||||||
|
1. Scan manifests and deterministic replay
|
||||||
|
2. Proof bundles (score proofs + reachability evidence)
|
||||||
|
3. Call-graph ingestion and reachability analysis
|
||||||
|
4. Unknowns management
|
||||||
|
|
||||||
|
**Design Principles**:
|
||||||
|
- All endpoints return canonical JSON (deterministic serialization)
|
||||||
|
- Idempotency via `Content-Digest` headers (SHA-256)
|
||||||
|
- DSSE signatures returned for all proof artifacts
|
||||||
|
- Offline-first (bundles downloadable for air-gap verification)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### 1. Create Scan with Manifest
|
||||||
|
|
||||||
|
**POST** `/api/v1/scanner/scans`
|
||||||
|
|
||||||
|
**Description**: Creates a new scan with deterministic manifest.
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"artifactDigest": "sha256:abc123...",
|
||||||
|
"artifactPurl": "pkg:oci/myapp@sha256:abc123...",
|
||||||
|
"scannerVersion": "1.0.0",
|
||||||
|
"workerVersion": "1.0.0",
|
||||||
|
"concelierSnapshotHash": "sha256:feed123...",
|
||||||
|
"excititorSnapshotHash": "sha256:vex456...",
|
||||||
|
"latticePolicyHash": "sha256:policy789...",
|
||||||
|
"deterministic": true,
|
||||||
|
"seed": "AQIDBA==", // base64-encoded 32 bytes
|
||||||
|
"knobs": {
|
||||||
|
"maxDepth": "10",
|
||||||
|
"indirectCallResolution": "conservative"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response** (201 Created):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scanId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"manifestHash": "sha256:manifest123...",
|
||||||
|
"createdAt": "2025-12-17T12:00:00Z",
|
||||||
|
"_links": {
|
||||||
|
"self": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"manifest": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/manifest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Headers**:
|
||||||
|
- `Content-Digest`: `sha256=<base64-hash>` (idempotency key)
|
||||||
|
- `Location`: `/api/v1/scanner/scans/{scanId}`
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `400 Bad Request` — Invalid manifest (missing required fields)
|
||||||
|
- `409 Conflict` — Scan with same `manifestHash` already exists
|
||||||
|
- `422 Unprocessable Entity` — Snapshot hashes not found in Concelier/Excititor
|
||||||
|
|
||||||
|
**Idempotency**: Requests with same `Content-Digest` return existing scan (no duplicate creation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Retrieve Scan Manifest
|
||||||
|
|
||||||
|
**GET** `/api/v1/scanner/scans/{scanId}/manifest`
|
||||||
|
|
||||||
|
**Description**: Retrieves the canonical JSON manifest with DSSE signature.
|
||||||
|
|
||||||
|
**Response** (200 OK):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"manifest": {
|
||||||
|
"scanId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"createdAtUtc": "2025-12-17T12:00:00Z",
|
||||||
|
"artifactDigest": "sha256:abc123...",
|
||||||
|
"artifactPurl": "pkg:oci/myapp@sha256:abc123...",
|
||||||
|
"scannerVersion": "1.0.0",
|
||||||
|
"workerVersion": "1.0.0",
|
||||||
|
"concelierSnapshotHash": "sha256:feed123...",
|
||||||
|
"excititorSnapshotHash": "sha256:vex456...",
|
||||||
|
"latticePolicyHash": "sha256:policy789...",
|
||||||
|
"deterministic": true,
|
||||||
|
"seed": "AQIDBA==",
|
||||||
|
"knobs": {
|
||||||
|
"maxDepth": "10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manifestHash": "sha256:manifest123...",
|
||||||
|
"dsseEnvelope": {
|
||||||
|
"payloadType": "application/vnd.stellaops.scan-manifest.v1+json",
|
||||||
|
"payload": "eyJzY2FuSWQiOiIuLi4ifQ==", // base64 canonical JSON
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"keyid": "ecdsa-p256-key-001",
|
||||||
|
"sig": "MEUCIQDx..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Headers**:
|
||||||
|
- `Content-Type`: `application/json`
|
||||||
|
- `ETag`: `"<manifestHash>"`
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `404 Not Found` — Scan ID not found
|
||||||
|
|
||||||
|
**Caching**: `ETag` supports conditional `If-None-Match` requests (304 Not Modified).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Replay Score Computation
|
||||||
|
|
||||||
|
**POST** `/api/v1/scanner/scans/{scanId}/score/replay`
|
||||||
|
|
||||||
|
**Description**: Recomputes score proofs from manifest without rescanning binaries. Used when feeds/policies change.
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"overrides": {
|
||||||
|
"concelierSnapshotHash": "sha256:newfeed...", // Optional: use different feed
|
||||||
|
"excititorSnapshotHash": "sha256:newvex...", // Optional: use different VEX
|
||||||
|
"latticePolicyHash": "sha256:newpolicy..." // Optional: use different policy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response** (200 OK):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scanId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"replayedAt": "2025-12-17T13:00:00Z",
|
||||||
|
"scoreProof": {
|
||||||
|
"rootHash": "sha256:proof123...",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "input-1",
|
||||||
|
"kind": "Input",
|
||||||
|
"ruleId": "inputs.v1",
|
||||||
|
"delta": 0.0,
|
||||||
|
"total": 0.0,
|
||||||
|
"nodeHash": "sha256:node1..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "delta-cvss",
|
||||||
|
"kind": "Delta",
|
||||||
|
"ruleId": "score.cvss_base.weighted",
|
||||||
|
"parentIds": ["input-1"],
|
||||||
|
"evidenceRefs": ["cvss:9.1"],
|
||||||
|
"delta": 0.50,
|
||||||
|
"total": 0.50,
|
||||||
|
"nodeHash": "sha256:node2..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"proofBundleUri": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/proofs/sha256:proof123...",
|
||||||
|
"_links": {
|
||||||
|
"bundle": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/proofs/sha256:proof123..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `404 Not Found` — Scan ID not found
|
||||||
|
- `422 Unprocessable Entity` — Override snapshot not found
|
||||||
|
|
||||||
|
**Use Case**: Nightly rescore job when Concelier publishes new advisory snapshot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Upload Call-Graph
|
||||||
|
|
||||||
|
**POST** `/api/v1/scanner/scans/{scanId}/callgraphs`
|
||||||
|
|
||||||
|
**Description**: Uploads call-graph extracted by language-specific workers (.NET, Java, etc.).
|
||||||
|
|
||||||
|
**Request Body** (`application/json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": "stella.callgraph.v1",
|
||||||
|
"language": "dotnet",
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"artifactKey": "MyApp.WebApi.dll",
|
||||||
|
"kind": "assembly",
|
||||||
|
"sha256": "sha256:artifact123..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"nodeId": "sha256:node1...",
|
||||||
|
"artifactKey": "MyApp.WebApi.dll",
|
||||||
|
"symbolKey": "MyApp.Controllers.OrdersController::Get(System.Guid)",
|
||||||
|
"visibility": "public",
|
||||||
|
"isEntrypointCandidate": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "sha256:node1...",
|
||||||
|
"to": "sha256:node2...",
|
||||||
|
"kind": "static",
|
||||||
|
"reason": "direct_call",
|
||||||
|
"weight": 1.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entrypoints": [
|
||||||
|
{
|
||||||
|
"nodeId": "sha256:node1...",
|
||||||
|
"kind": "http",
|
||||||
|
"route": "/api/orders/{id}",
|
||||||
|
"framework": "aspnetcore"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Headers**:
|
||||||
|
- `Content-Digest`: `sha256=<hash>` (idempotency)
|
||||||
|
|
||||||
|
**Response** (202 Accepted):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scanId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"callGraphDigest": "sha256:cg123...",
|
||||||
|
"nodesCount": 1234,
|
||||||
|
"edgesCount": 5678,
|
||||||
|
"entrypointsCount": 12,
|
||||||
|
"status": "accepted",
|
||||||
|
"_links": {
|
||||||
|
"reachability": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/reachability/compute"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `400 Bad Request` — Invalid call-graph schema
|
||||||
|
- `404 Not Found` — Scan ID not found
|
||||||
|
- `413 Payload Too Large` — Call-graph >100MB
|
||||||
|
|
||||||
|
**Idempotency**: Same `Content-Digest` → returns existing call-graph.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Compute Reachability
|
||||||
|
|
||||||
|
**POST** `/api/v1/scanner/scans/{scanId}/reachability/compute`
|
||||||
|
|
||||||
|
**Description**: Triggers reachability analysis for uploaded call-graph + SBOM + vulnerabilities.
|
||||||
|
|
||||||
|
**Request Body**: Empty (uses existing scan data)
|
||||||
|
|
||||||
|
**Response** (202 Accepted):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scanId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"jobId": "reachability-job-001",
|
||||||
|
"status": "queued",
|
||||||
|
"estimatedDuration": "30s",
|
||||||
|
"_links": {
|
||||||
|
"status": "/api/v1/scanner/jobs/reachability-job-001",
|
||||||
|
"results": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000/reachability/findings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Polling**: Use `GET /api/v1/scanner/jobs/{jobId}` to check status.
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `404 Not Found` — Scan ID not found
|
||||||
|
- `422 Unprocessable Entity` — Call-graph not uploaded yet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Get Reachability Findings
|
||||||
|
|
||||||
|
**GET** `/api/v1/scanner/scans/{scanId}/reachability/findings`
|
||||||
|
|
||||||
|
**Description**: Retrieves reachability verdicts for all vulnerabilities.
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `status` (optional): Filter by `REACHABLE`, `UNREACHABLE`, `POSSIBLY_REACHABLE`, `UNKNOWN`
|
||||||
|
- `cveId` (optional): Filter by CVE ID
|
||||||
|
|
||||||
|
**Response** (200 OK):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scanId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"computedAt": "2025-12-17T12:30:00Z",
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"cveId": "CVE-2024-1234",
|
||||||
|
"purl": "pkg:npm/lodash@4.17.20",
|
||||||
|
"status": "REACHABLE_STATIC",
|
||||||
|
"confidence": 0.70,
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"nodeId": "sha256:entrypoint...",
|
||||||
|
"symbolKey": "MyApp.Controllers.OrdersController::Get(System.Guid)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "sha256:intermediate...",
|
||||||
|
"symbolKey": "MyApp.Services.OrderService::Process(Order)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "sha256:vuln...",
|
||||||
|
"symbolKey": "Lodash.merge(Object, Object)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"evidence": {
|
||||||
|
"pathLength": 3,
|
||||||
|
"staticEdgesOnly": true,
|
||||||
|
"runtimeConfirmed": false
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"explain": "/api/v1/scanner/scans/{scanId}/reachability/explain?cve=CVE-2024-1234&purl=pkg:npm/lodash@4.17.20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total": 45,
|
||||||
|
"reachable": 3,
|
||||||
|
"unreachable": 38,
|
||||||
|
"possiblyReachable": 4,
|
||||||
|
"unknown": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `404 Not Found` — Scan ID not found or reachability not computed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Explain Reachability
|
||||||
|
|
||||||
|
**GET** `/api/v1/scanner/scans/{scanId}/reachability/explain`
|
||||||
|
|
||||||
|
**Description**: Provides detailed explanation for a reachability verdict.
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `cve` (required): CVE ID
|
||||||
|
- `purl` (required): Package URL
|
||||||
|
|
||||||
|
**Response** (200 OK):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cveId": "CVE-2024-1234",
|
||||||
|
"purl": "pkg:npm/lodash@4.17.20",
|
||||||
|
"status": "REACHABLE_STATIC",
|
||||||
|
"confidence": 0.70,
|
||||||
|
"explanation": {
|
||||||
|
"shortestPath": [
|
||||||
|
{
|
||||||
|
"depth": 0,
|
||||||
|
"nodeId": "sha256:entry...",
|
||||||
|
"symbolKey": "MyApp.Controllers.OrdersController::Get(System.Guid)",
|
||||||
|
"entrypointKind": "http",
|
||||||
|
"route": "/api/orders/{id}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depth": 1,
|
||||||
|
"nodeId": "sha256:inter...",
|
||||||
|
"symbolKey": "MyApp.Services.OrderService::Process(Order)",
|
||||||
|
"edgeKind": "static",
|
||||||
|
"edgeReason": "direct_call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depth": 2,
|
||||||
|
"nodeId": "sha256:vuln...",
|
||||||
|
"symbolKey": "Lodash.merge(Object, Object)",
|
||||||
|
"edgeKind": "static",
|
||||||
|
"edgeReason": "direct_call",
|
||||||
|
"vulnerableFunction": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"whyReachable": [
|
||||||
|
"Static call path exists from HTTP entrypoint /api/orders/{id}",
|
||||||
|
"All edges are statically proven (no heuristics)",
|
||||||
|
"Vulnerable function Lodash.merge() is directly invoked"
|
||||||
|
],
|
||||||
|
"confidenceFactors": {
|
||||||
|
"staticPathExists": 0.50,
|
||||||
|
"noHeuristicEdges": 0.20,
|
||||||
|
"runtimeConfirmed": 0.00
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alternativePaths": 2, // Number of other paths found
|
||||||
|
"_links": {
|
||||||
|
"callGraph": "/api/v1/scanner/scans/{scanId}/callgraphs/sha256:cg123.../graph.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `404 Not Found` — Scan, CVE, or PURL not found
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Fetch Proof Bundle
|
||||||
|
|
||||||
|
**GET** `/api/v1/scanner/scans/{scanId}/proofs/{rootHash}`
|
||||||
|
|
||||||
|
**Description**: Downloads proof bundle zip archive for offline verification.
|
||||||
|
|
||||||
|
**Path Parameters**:
|
||||||
|
- `rootHash`: Proof root hash (e.g., `sha256:proof123...`)
|
||||||
|
|
||||||
|
**Response** (200 OK):
|
||||||
|
|
||||||
|
**Headers**:
|
||||||
|
- `Content-Type`: `application/zip`
|
||||||
|
- `Content-Disposition`: `attachment; filename="proof-{scanId}-{rootHash}.zip"`
|
||||||
|
- `X-Proof-Root-Hash`: `{rootHash}`
|
||||||
|
- `X-Manifest-Hash`: `{manifestHash}`
|
||||||
|
|
||||||
|
**Body**: Binary zip archive containing:
|
||||||
|
- `manifest.json` — Canonical scan manifest
|
||||||
|
- `manifest.dsse.json` — DSSE signature of manifest
|
||||||
|
- `score_proof.json` — Proof ledger (array of ProofNodes)
|
||||||
|
- `proof_root.dsse.json` — DSSE signature of proof root
|
||||||
|
- `meta.json` — Metadata (created timestamp, etc.)
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `404 Not Found` — Scan or proof root hash not found
|
||||||
|
|
||||||
|
**Use Case**: Air-gap verification (`stella proof verify --bundle proof.zip`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. List Unknowns
|
||||||
|
|
||||||
|
**GET** `/api/v1/scanner/unknowns`
|
||||||
|
|
||||||
|
**Description**: Lists unknowns (missing evidence) ranked by priority.
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `band` (optional): Filter by `HOT`, `WARM`, `COLD`
|
||||||
|
- `limit` (optional): Max results (default: 100, max: 1000)
|
||||||
|
- `offset` (optional): Pagination offset
|
||||||
|
|
||||||
|
**Response** (200 OK):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"unknowns": [
|
||||||
|
{
|
||||||
|
"unknownId": "unk-001",
|
||||||
|
"pkgId": "pkg:npm/lodash",
|
||||||
|
"pkgVersion": "4.17.20",
|
||||||
|
"digestAnchor": "sha256:...",
|
||||||
|
"reasons": ["missing_vex", "ambiguous_version"],
|
||||||
|
"score": 0.72,
|
||||||
|
"band": "HOT",
|
||||||
|
"popularity": 0.85,
|
||||||
|
"potentialExploit": 0.60,
|
||||||
|
"uncertainty": 0.75,
|
||||||
|
"evidence": {
|
||||||
|
"deployments": 42,
|
||||||
|
"epss": 0.58,
|
||||||
|
"kev": false
|
||||||
|
},
|
||||||
|
"createdAt": "2025-12-15T10:00:00Z",
|
||||||
|
"_links": {
|
||||||
|
"escalate": "/api/v1/scanner/unknowns/unk-001/escalate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"total": 156,
|
||||||
|
"limit": 100,
|
||||||
|
"offset": 0,
|
||||||
|
"next": "/api/v1/scanner/unknowns?band=HOT&limit=100&offset=100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `400 Bad Request` — Invalid band value
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Escalate Unknown to Rescan
|
||||||
|
|
||||||
|
**POST** `/api/v1/scanner/unknowns/{unknownId}/escalate`
|
||||||
|
|
||||||
|
**Description**: Escalates an unknown to trigger immediate rescan/re-analysis.
|
||||||
|
|
||||||
|
**Request Body**: Empty
|
||||||
|
|
||||||
|
**Response** (202 Accepted):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"unknownId": "unk-001",
|
||||||
|
"escalatedAt": "2025-12-17T12:00:00Z",
|
||||||
|
"rescanJobId": "rescan-job-001",
|
||||||
|
"status": "queued",
|
||||||
|
"_links": {
|
||||||
|
"job": "/api/v1/scanner/jobs/rescan-job-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
- `404 Not Found` — Unknown ID not found
|
||||||
|
- `409 Conflict` — Unknown already escalated (rescan in progress)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### ScanManifest
|
||||||
|
|
||||||
|
See `src/__Libraries/StellaOps.Scanner.Core/Models/ScanManifest.cs` for full definition.
|
||||||
|
|
||||||
|
### ProofNode
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ProofNode {
|
||||||
|
id: string;
|
||||||
|
kind: "Input" | "Transform" | "Delta" | "Score";
|
||||||
|
ruleId: string;
|
||||||
|
parentIds: string[];
|
||||||
|
evidenceRefs: string[];
|
||||||
|
delta: number;
|
||||||
|
total: number;
|
||||||
|
actor: string;
|
||||||
|
tsUtc: string; // ISO 8601
|
||||||
|
seed: string; // base64
|
||||||
|
nodeHash: string; // sha256:...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DsseEnvelope
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DsseEnvelope {
|
||||||
|
payloadType: string;
|
||||||
|
payload: string; // base64 canonical JSON
|
||||||
|
signatures: DsseSignature[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DsseSignature {
|
||||||
|
keyid: string;
|
||||||
|
sig: string; // base64
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ReachabilityStatus
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum ReachabilityStatus {
|
||||||
|
UNREACHABLE = "UNREACHABLE",
|
||||||
|
POSSIBLY_REACHABLE = "POSSIBLY_REACHABLE",
|
||||||
|
REACHABLE_STATIC = "REACHABLE_STATIC",
|
||||||
|
REACHABLE_PROVEN = "REACHABLE_PROVEN",
|
||||||
|
UNKNOWN = "UNKNOWN"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
All errors follow RFC 7807 (Problem Details):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "https://stella-ops.org/errors/scan-not-found",
|
||||||
|
"title": "Scan Not Found",
|
||||||
|
"status": 404,
|
||||||
|
"detail": "Scan ID '550e8400-e29b-41d4-a716-446655440000' does not exist.",
|
||||||
|
"instance": "/api/v1/scanner/scans/550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"traceId": "trace-001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
| Type | Status | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `scan-not-found` | 404 | Scan ID not found |
|
||||||
|
| `invalid-manifest` | 400 | Manifest validation failed |
|
||||||
|
| `duplicate-scan` | 409 | Scan with same manifest hash exists |
|
||||||
|
| `snapshot-not-found` | 422 | Concelier/Excititor snapshot not found |
|
||||||
|
| `callgraph-not-uploaded` | 422 | Call-graph required before reachability |
|
||||||
|
| `payload-too-large` | 413 | Request body exceeds size limit |
|
||||||
|
| `proof-not-found` | 404 | Proof root hash not found |
|
||||||
|
| `unknown-not-found` | 404 | Unknown ID not found |
|
||||||
|
| `escalation-conflict` | 409 | Unknown already escalated |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
**Limits**:
|
||||||
|
- `POST /scans`: 100 requests/hour per tenant
|
||||||
|
- `POST /scans/{id}/score/replay`: 1000 requests/hour per tenant
|
||||||
|
- `POST /callgraphs`: 100 requests/hour per tenant
|
||||||
|
- `POST /reachability/compute`: 100 requests/hour per tenant
|
||||||
|
- `GET` endpoints: 10,000 requests/hour per tenant
|
||||||
|
|
||||||
|
**Headers**:
|
||||||
|
- `X-RateLimit-Limit`: Maximum requests per window
|
||||||
|
- `X-RateLimit-Remaining`: Remaining requests
|
||||||
|
- `X-RateLimit-Reset`: Unix timestamp when limit resets
|
||||||
|
|
||||||
|
**Error** (429 Too Many Requests):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "https://stella-ops.org/errors/rate-limit-exceeded",
|
||||||
|
"title": "Rate Limit Exceeded",
|
||||||
|
"status": 429,
|
||||||
|
"detail": "Exceeded 100 requests/hour for POST /scans. Retry after 1234567890.",
|
||||||
|
"retryAfter": 1234567890
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Webhooks (Future)
|
||||||
|
|
||||||
|
**Planned for Sprint 3500.0004.0003**:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/scanner/webhooks
|
||||||
|
Register webhook for events: scan.completed, reachability.computed, unknown.escalated
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OpenAPI Specification
|
||||||
|
|
||||||
|
**File**: `src/Api/StellaOps.Api.OpenApi/scanner/openapi.yaml`
|
||||||
|
|
||||||
|
Update with new endpoints (Sprint 3500.0002.0003).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `SPRINT_3500_0002_0001_score_proofs_foundations.md` — Implementation sprint
|
||||||
|
- `SPRINT_3500_0002_0003_proof_replay_api.md` — API implementation sprint
|
||||||
|
- `SPRINT_3500_0003_0003_graph_attestations_rekor.md` — Reachability API sprint
|
||||||
|
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` — API contracts section
|
||||||
|
- `docs/db/schemas/scanner_schema_specification.md` — Database schema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-12-17
|
||||||
|
**API Version**: 2.0
|
||||||
|
**Next Review**: Sprint 3500.0004.0001 (CLI integration)
|
||||||
282
docs/api/score-replay-api.md
Normal file
282
docs/api/score-replay-api.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# Score Replay API Reference
|
||||||
|
|
||||||
|
**Sprint:** SPRINT_3401_0002_0001
|
||||||
|
**Task:** SCORE-REPLAY-014 - Update scanner API docs with replay endpoint
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Score Replay API enables deterministic re-scoring of scans using historical manifests. This is essential for auditing, compliance verification, and investigating how scores change with updated advisory feeds.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/v1/score
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All endpoints require Bearer token authentication:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Required scope: `scanner:replay:read` for GET, `scanner:replay:write` for POST
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### Replay Score
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/score/replay
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-scores a scan using the original manifest with an optionally different feed snapshot.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scanId": "scan-12345678-abcd",
|
||||||
|
"feedSnapshotHash": "sha256:abc123...",
|
||||||
|
"policyVersion": "1.0.0",
|
||||||
|
"dryRun": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `scanId` | string | Yes | Original scan ID to replay |
|
||||||
|
| `feedSnapshotHash` | string | No | Feed snapshot to use (defaults to current) |
|
||||||
|
| `policyVersion` | string | No | Policy version (defaults to original) |
|
||||||
|
| `dryRun` | boolean | No | If true, calculates but doesn't persist |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"replayId": "replay-87654321-dcba",
|
||||||
|
"originalScanId": "scan-12345678-abcd",
|
||||||
|
"status": "completed",
|
||||||
|
"feedSnapshotHash": "sha256:abc123...",
|
||||||
|
"policyVersion": "1.0.0",
|
||||||
|
"originalManifestHash": "sha256:def456...",
|
||||||
|
"replayedManifestHash": "sha256:ghi789...",
|
||||||
|
"scoreDelta": {
|
||||||
|
"originalScore": 7.5,
|
||||||
|
"replayedScore": 6.8,
|
||||||
|
"delta": -0.7
|
||||||
|
},
|
||||||
|
"findingsDelta": {
|
||||||
|
"added": 2,
|
||||||
|
"removed": 5,
|
||||||
|
"rescored": 12,
|
||||||
|
"unchanged": 45
|
||||||
|
},
|
||||||
|
"proofBundleRef": "proofs/replays/replay-87654321/bundle.zip",
|
||||||
|
"duration": {
|
||||||
|
"ms": 1250
|
||||||
|
},
|
||||||
|
"createdAt": "2025-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Replay with latest feed
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"scanId": "scan-12345678-abcd"}' \
|
||||||
|
"https://scanner.example.com/api/v1/score/replay"
|
||||||
|
|
||||||
|
# Replay with specific feed snapshot
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"scanId": "scan-12345678-abcd",
|
||||||
|
"feedSnapshotHash": "sha256:abc123..."
|
||||||
|
}' \
|
||||||
|
"https://scanner.example.com/api/v1/score/replay"
|
||||||
|
|
||||||
|
# Dry run (preview only)
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"scanId": "scan-12345678-abcd",
|
||||||
|
"dryRun": true
|
||||||
|
}' \
|
||||||
|
"https://scanner.example.com/api/v1/score/replay"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Replay History
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/score/replays
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns history of score replays.
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `scanId` | string | - | Filter by original scan |
|
||||||
|
| `page` | int | 1 | Page number |
|
||||||
|
| `pageSize` | int | 50 | Items per page |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"replayId": "replay-87654321-dcba",
|
||||||
|
"originalScanId": "scan-12345678-abcd",
|
||||||
|
"triggerType": "manual",
|
||||||
|
"scoreDelta": -0.7,
|
||||||
|
"findingsAdded": 2,
|
||||||
|
"findingsRemoved": 5,
|
||||||
|
"createdAt": "2025-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 50,
|
||||||
|
"totalItems": 12,
|
||||||
|
"totalPages": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Replay Details
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/score/replays/{replayId}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns detailed information about a specific replay.
|
||||||
|
|
||||||
|
### Get Scan Manifest
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/scans/{scanId}/manifest
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the scan manifest containing all input hashes.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"manifestId": "manifest-12345678",
|
||||||
|
"scanId": "scan-12345678-abcd",
|
||||||
|
"manifestHash": "sha256:def456...",
|
||||||
|
"sbomHash": "sha256:aaa111...",
|
||||||
|
"rulesHash": "sha256:bbb222...",
|
||||||
|
"feedHash": "sha256:ccc333...",
|
||||||
|
"policyHash": "sha256:ddd444...",
|
||||||
|
"scannerVersion": "1.0.0",
|
||||||
|
"createdAt": "2025-01-15T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Proof Bundle
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/scans/{scanId}/proof-bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
Downloads the proof bundle (ZIP archive) for a scan.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Returns `application/zip` with the proof bundle containing:
|
||||||
|
- `manifest.json` - Signed scan manifest
|
||||||
|
- `ledger.json` - Proof ledger nodes
|
||||||
|
- `sbom.json` - Input SBOM (hash-verified)
|
||||||
|
- `findings.json` - Scored findings
|
||||||
|
- `signature.dsse` - DSSE envelope
|
||||||
|
|
||||||
|
## Scheduled Replay
|
||||||
|
|
||||||
|
Scans can be automatically replayed when feed snapshots change.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/scanner.yaml
|
||||||
|
score_replay:
|
||||||
|
enabled: true
|
||||||
|
schedule: "0 4 * * *" # Daily at 4 AM UTC
|
||||||
|
max_age_days: 30 # Only replay scans from last 30 days
|
||||||
|
notify_on_delta: true # Send notification if scores change
|
||||||
|
delta_threshold: 0.5 # Only notify if delta > threshold
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trigger Types
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `manual` | User-initiated via API |
|
||||||
|
| `feed_update` | Triggered by new feed snapshot |
|
||||||
|
| `policy_change` | Triggered by policy version change |
|
||||||
|
| `scheduled` | Triggered by scheduled job |
|
||||||
|
|
||||||
|
## Determinism Guarantees
|
||||||
|
|
||||||
|
Score replay guarantees deterministic results when:
|
||||||
|
|
||||||
|
1. **Same manifest hash** - All inputs are identical
|
||||||
|
2. **Same scanner version** - Scoring algorithm unchanged
|
||||||
|
3. **Same policy version** - Policy rules unchanged
|
||||||
|
|
||||||
|
### Manifest Contents
|
||||||
|
|
||||||
|
The manifest captures:
|
||||||
|
- SBOM content hash
|
||||||
|
- Rules snapshot hash
|
||||||
|
- Advisory feed snapshot hash
|
||||||
|
- Policy configuration hash
|
||||||
|
- Scanner version
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify replay determinism
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"https://scanner.example.com/api/v1/scans/{scanId}/manifest" \
|
||||||
|
| jq '.manifestHash'
|
||||||
|
|
||||||
|
# Compare with replay
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"https://scanner.example.com/api/v1/score/replays/{replayId}" \
|
||||||
|
| jq '.replayedManifestHash'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
| Status | Code | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| 400 | `INVALID_SCAN_ID` | Scan ID not found |
|
||||||
|
| 400 | `INVALID_FEED_SNAPSHOT` | Feed snapshot not found |
|
||||||
|
| 400 | `MANIFEST_NOT_FOUND` | Scan manifest missing |
|
||||||
|
| 401 | `UNAUTHORIZED` | Invalid token |
|
||||||
|
| 403 | `FORBIDDEN` | Insufficient permissions |
|
||||||
|
| 409 | `REPLAY_IN_PROGRESS` | Replay already running for scan |
|
||||||
|
| 429 | `RATE_LIMITED` | Too many requests |
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- POST replay: 10 requests/minute
|
||||||
|
- GET replays: 100 requests/minute
|
||||||
|
- GET manifest: 100 requests/minute
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Proof Bundle Format](./proof-bundle-format.md)
|
||||||
|
- [Scanner Architecture](../modules/scanner/architecture.md)
|
||||||
|
- [Determinism Requirements](../product-advisories/14-Dec-2025%20-%20Determinism%20and%20Reproducibility%20Technical%20Reference.md)
|
||||||
325
docs/api/smart-diff-types.md
Normal file
325
docs/api/smart-diff-types.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# Smart-Diff API Types
|
||||||
|
|
||||||
|
> Sprint: SPRINT_3500_0002_0001
|
||||||
|
> Module: Scanner, Policy, Attestor
|
||||||
|
|
||||||
|
This document describes the Smart-Diff types exposed through APIs.
|
||||||
|
|
||||||
|
## Smart-Diff Predicate
|
||||||
|
|
||||||
|
The Smart-Diff predicate is a DSSE-signed attestation describing differential analysis between two scans.
|
||||||
|
|
||||||
|
### Predicate Type URI
|
||||||
|
|
||||||
|
```
|
||||||
|
stellaops.dev/predicates/smart-diff@v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenAPI Schema Fragment
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SmartDiffPredicate:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- schemaVersion
|
||||||
|
- baseImage
|
||||||
|
- targetImage
|
||||||
|
- diff
|
||||||
|
- reachabilityGate
|
||||||
|
- scanner
|
||||||
|
properties:
|
||||||
|
schemaVersion:
|
||||||
|
type: string
|
||||||
|
pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||||
|
example: "1.0.0"
|
||||||
|
description: Schema version (semver)
|
||||||
|
baseImage:
|
||||||
|
$ref: '#/components/schemas/ImageReference'
|
||||||
|
targetImage:
|
||||||
|
$ref: '#/components/schemas/ImageReference'
|
||||||
|
diff:
|
||||||
|
$ref: '#/components/schemas/DiffPayload'
|
||||||
|
reachabilityGate:
|
||||||
|
$ref: '#/components/schemas/ReachabilityGate'
|
||||||
|
scanner:
|
||||||
|
$ref: '#/components/schemas/ScannerInfo'
|
||||||
|
context:
|
||||||
|
$ref: '#/components/schemas/RuntimeContext'
|
||||||
|
suppressedCount:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
description: Number of findings suppressed by pre-filters
|
||||||
|
materialChanges:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/MaterialChange'
|
||||||
|
|
||||||
|
ImageReference:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- digest
|
||||||
|
properties:
|
||||||
|
digest:
|
||||||
|
type: string
|
||||||
|
pattern: "^sha256:[a-f0-9]{64}$"
|
||||||
|
example: "sha256:abc123..."
|
||||||
|
repository:
|
||||||
|
type: string
|
||||||
|
example: "ghcr.io/org/image"
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
example: "v1.2.3"
|
||||||
|
|
||||||
|
DiffPayload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- added
|
||||||
|
- removed
|
||||||
|
- modified
|
||||||
|
properties:
|
||||||
|
added:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/DiffEntry'
|
||||||
|
description: New vulnerabilities in target
|
||||||
|
removed:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/DiffEntry'
|
||||||
|
description: Vulnerabilities fixed in target
|
||||||
|
modified:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/DiffEntry'
|
||||||
|
description: Changed vulnerability status
|
||||||
|
|
||||||
|
DiffEntry:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- vulnId
|
||||||
|
- componentPurl
|
||||||
|
properties:
|
||||||
|
vulnId:
|
||||||
|
type: string
|
||||||
|
example: "CVE-2024-1234"
|
||||||
|
componentPurl:
|
||||||
|
type: string
|
||||||
|
example: "pkg:npm/lodash@4.17.21"
|
||||||
|
severity:
|
||||||
|
type: string
|
||||||
|
enum: [CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN]
|
||||||
|
changeType:
|
||||||
|
type: string
|
||||||
|
enum: [added, removed, severity_changed, status_changed]
|
||||||
|
|
||||||
|
ReachabilityGate:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- class
|
||||||
|
- isSinkReachable
|
||||||
|
- isEntryReachable
|
||||||
|
properties:
|
||||||
|
class:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
maximum: 7
|
||||||
|
description: |
|
||||||
|
3-bit reachability class:
|
||||||
|
- Bit 0: Entry point reachable
|
||||||
|
- Bit 1: Sink reachable
|
||||||
|
- Bit 2: Direct path exists
|
||||||
|
isSinkReachable:
|
||||||
|
type: boolean
|
||||||
|
description: Whether a sensitive sink is reachable
|
||||||
|
isEntryReachable:
|
||||||
|
type: boolean
|
||||||
|
description: Whether an entry point is reachable
|
||||||
|
sinkCategory:
|
||||||
|
type: string
|
||||||
|
enum: [file, network, crypto, command, sql, ldap, xpath, ssrf, log, deserialization, reflection]
|
||||||
|
description: Category of the matched sink
|
||||||
|
|
||||||
|
ScannerInfo:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- version
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: "stellaops-scanner"
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
example: "1.5.0"
|
||||||
|
commit:
|
||||||
|
type: string
|
||||||
|
example: "abc123"
|
||||||
|
|
||||||
|
RuntimeContext:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
description: Optional runtime context for the scan
|
||||||
|
example:
|
||||||
|
env: "production"
|
||||||
|
namespace: "default"
|
||||||
|
cluster: "us-east-1"
|
||||||
|
|
||||||
|
MaterialChange:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: [file, package, config]
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
hash:
|
||||||
|
type: string
|
||||||
|
changeKind:
|
||||||
|
type: string
|
||||||
|
enum: [added, removed, modified]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reachability Gate Classes
|
||||||
|
|
||||||
|
| Class | Entry | Sink | Direct | Description |
|
||||||
|
|-------|-------|------|--------|-------------|
|
||||||
|
| 0 | ❌ | ❌ | ❌ | Not reachable |
|
||||||
|
| 1 | ✅ | ❌ | ❌ | Entry point only |
|
||||||
|
| 2 | ❌ | ✅ | ❌ | Sink only |
|
||||||
|
| 3 | ✅ | ✅ | ❌ | Both, no direct path |
|
||||||
|
| 4 | ❌ | ❌ | ✅ | Direct path, no endpoints |
|
||||||
|
| 5 | ✅ | ❌ | ✅ | Entry + direct |
|
||||||
|
| 6 | ❌ | ✅ | ✅ | Sink + direct |
|
||||||
|
| 7 | ✅ | ✅ | ✅ | Full reachability confirmed |
|
||||||
|
|
||||||
|
## Sink Categories
|
||||||
|
|
||||||
|
| Category | Description | Examples |
|
||||||
|
|----------|-------------|----------|
|
||||||
|
| `file` | File system operations | `File.Open`, `fopen` |
|
||||||
|
| `network` | Network I/O | `HttpClient`, `socket` |
|
||||||
|
| `crypto` | Cryptographic operations | `SHA256`, `AES` |
|
||||||
|
| `command` | Command execution | `Process.Start`, `exec` |
|
||||||
|
| `sql` | SQL queries | `SqlCommand`, query builders |
|
||||||
|
| `ldap` | LDAP operations | `DirectoryEntry` |
|
||||||
|
| `xpath` | XPath queries | `XPathNavigator` |
|
||||||
|
| `ssrf` | Server-side request forgery | HTTP clients with user input |
|
||||||
|
| `log` | Logging operations | `ILogger`, `Console.Write` |
|
||||||
|
| `deserialization` | Deserialization | `JsonSerializer`, `BinaryFormatter` |
|
||||||
|
| `reflection` | Reflection operations | `Type.GetType`, `Assembly.Load` |
|
||||||
|
|
||||||
|
## Suppression Rules
|
||||||
|
|
||||||
|
### OpenAPI Schema Fragment
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SuppressionRule:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- type
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Unique rule identifier
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- cve_pattern
|
||||||
|
- purl_pattern
|
||||||
|
- severity_below
|
||||||
|
- patch_churn
|
||||||
|
- sink_category
|
||||||
|
- reachability_class
|
||||||
|
pattern:
|
||||||
|
type: string
|
||||||
|
description: Regex pattern (for pattern rules)
|
||||||
|
threshold:
|
||||||
|
type: string
|
||||||
|
description: Threshold value (for severity/class rules)
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
|
description: Human-readable reason for suppression
|
||||||
|
expires:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Optional expiration timestamp
|
||||||
|
|
||||||
|
SuppressionResult:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
suppressed:
|
||||||
|
type: boolean
|
||||||
|
matchedRuleId:
|
||||||
|
type: string
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Creating a Smart-Diff Predicate
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var predicate = new SmartDiffPredicate
|
||||||
|
{
|
||||||
|
SchemaVersion = "1.0.0",
|
||||||
|
BaseImage = new ImageReference
|
||||||
|
{
|
||||||
|
Digest = "sha256:abc123...",
|
||||||
|
Repository = "ghcr.io/org/image",
|
||||||
|
Tag = "v1.0.0"
|
||||||
|
},
|
||||||
|
TargetImage = new ImageReference
|
||||||
|
{
|
||||||
|
Digest = "sha256:def456...",
|
||||||
|
Repository = "ghcr.io/org/image",
|
||||||
|
Tag = "v1.1.0"
|
||||||
|
},
|
||||||
|
Diff = new DiffPayload
|
||||||
|
{
|
||||||
|
Added = [new DiffEntry { VulnId = "CVE-2024-1234", ... }],
|
||||||
|
Removed = [],
|
||||||
|
Modified = []
|
||||||
|
},
|
||||||
|
ReachabilityGate = new ReachabilityGate
|
||||||
|
{
|
||||||
|
Class = 7,
|
||||||
|
IsSinkReachable = true,
|
||||||
|
IsEntryReachable = true,
|
||||||
|
SinkCategory = SinkCategory.Network
|
||||||
|
},
|
||||||
|
Scanner = new ScannerInfo
|
||||||
|
{
|
||||||
|
Name = "stellaops-scanner",
|
||||||
|
Version = "1.5.0"
|
||||||
|
},
|
||||||
|
SuppressedCount = 5
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Evaluating Suppression Rules
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var evaluator = services.GetRequiredService<ISuppressionRuleEvaluator>();
|
||||||
|
|
||||||
|
var result = await evaluator.EvaluateAsync(finding, rules);
|
||||||
|
|
||||||
|
if (result.Suppressed)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Finding {VulnId} suppressed by rule {RuleId}: {Reason}",
|
||||||
|
finding.VulnId,
|
||||||
|
result.MatchedRuleId,
|
||||||
|
result.Reason);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Smart-Diff Technical Reference](../product-advisories/14-Dec-2025%20-%20Smart-Diff%20Technical%20Reference.md)
|
||||||
|
- [Scanner Architecture](../modules/scanner/architecture.md)
|
||||||
|
- [Policy Architecture](../modules/policy/architecture.md)
|
||||||
334
docs/api/triage.contract.v1.md
Normal file
334
docs/api/triage.contract.v1.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# Stella Ops Triage API Contract v1
|
||||||
|
|
||||||
|
Base path: `/api/triage/v1`
|
||||||
|
|
||||||
|
This contract is served by `scanner.webservice` (or a dedicated triage facade that reads scanner-owned tables).
|
||||||
|
All risk/lattice outputs originate from `scanner.webservice`.
|
||||||
|
|
||||||
|
Key requirements:
|
||||||
|
- Deterministic outputs (policyId + policyVersion + inputsHash).
|
||||||
|
- Proof-linking (chips reference evidenceIds).
|
||||||
|
- `concelier` and `excititor` preserve prune source: API surfaces source chains via `sourceRefs`.
|
||||||
|
|
||||||
|
## 0. Conventions
|
||||||
|
|
||||||
|
### 0.1 Identifiers
|
||||||
|
- `caseId` == `findingId` (UUID). A case is a finding scoped to an asset/environment.
|
||||||
|
- Hashes are hex strings.
|
||||||
|
|
||||||
|
### 0.2 Caching
|
||||||
|
- GET endpoints SHOULD return `ETag`.
|
||||||
|
- Clients SHOULD send `If-None-Match`.
|
||||||
|
|
||||||
|
### 0.3 Errors
|
||||||
|
Standard error envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "string",
|
||||||
|
"message": "string",
|
||||||
|
"details": { "any": "json" },
|
||||||
|
"traceId": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common codes:
|
||||||
|
|
||||||
|
* `not_found`
|
||||||
|
* `validation_error`
|
||||||
|
* `conflict`
|
||||||
|
* `unauthorized`
|
||||||
|
* `forbidden`
|
||||||
|
* `rate_limited`
|
||||||
|
|
||||||
|
## 1. Findings Table
|
||||||
|
|
||||||
|
### 1.1 List findings
|
||||||
|
|
||||||
|
`GET /findings`
|
||||||
|
|
||||||
|
Query params:
|
||||||
|
|
||||||
|
* `showMuted` (bool, default false)
|
||||||
|
* `lane` (optional, enum)
|
||||||
|
* `search` (optional string; searches asset, purl, cveId)
|
||||||
|
* `page` (int, default 1)
|
||||||
|
* `pageSize` (int, default 50; max 200)
|
||||||
|
* `sort` (optional: `updatedAt`, `score`, `lane`)
|
||||||
|
* `order` (optional: `asc|desc`)
|
||||||
|
|
||||||
|
Response 200:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 50,
|
||||||
|
"total": 12345,
|
||||||
|
"mutedCounts": { "reach": 1904, "vex": 513, "compensated": 18 },
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"lane": "BLOCKED",
|
||||||
|
"verdict": "BLOCK",
|
||||||
|
"score": 87,
|
||||||
|
"reachable": "YES",
|
||||||
|
"vex": "affected",
|
||||||
|
"exploit": "YES",
|
||||||
|
"asset": "prod/api-gateway:1.2.3",
|
||||||
|
"updatedAt": "2025-12-16T01:02:03Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Case Narrative
|
||||||
|
|
||||||
|
### 2.1 Get case header
|
||||||
|
|
||||||
|
`GET /cases/{caseId}`
|
||||||
|
|
||||||
|
Response 200:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"verdict": "BLOCK",
|
||||||
|
"lane": "BLOCKED",
|
||||||
|
"score": 87,
|
||||||
|
"policyId": "prod-strict",
|
||||||
|
"policyVersion": "2025.12.14",
|
||||||
|
"inputsHash": "hex",
|
||||||
|
"why": "Reachable path observed; exploit signal present; prod-strict blocks.",
|
||||||
|
"chips": [
|
||||||
|
{ "key": "reachability", "label": "Reachability", "value": "Reachable (92%)", "evidenceIds": ["uuid"] },
|
||||||
|
{ "key": "vex", "label": "VEX", "value": "affected", "evidenceIds": ["uuid"] },
|
||||||
|
{ "key": "gate", "label": "Gate", "value": "BLOCKED by prod-strict", "evidenceIds": ["uuid"] }
|
||||||
|
],
|
||||||
|
"sourceRefs": [
|
||||||
|
{
|
||||||
|
"domain": "concelier",
|
||||||
|
"kind": "cve_record",
|
||||||
|
"ref": "concelier:osv:...",
|
||||||
|
"pruned": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "excititor",
|
||||||
|
"kind": "effective_vex",
|
||||||
|
"ref": "excititor:openvex:...",
|
||||||
|
"pruned": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updatedAt": "2025-12-16T01:02:03Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
* `sourceRefs` provides preserved provenance chains (including pruned markers when applicable).
|
||||||
|
|
||||||
|
## 3. Evidence
|
||||||
|
|
||||||
|
### 3.1 List evidence for case
|
||||||
|
|
||||||
|
`GET /cases/{caseId}/evidence`
|
||||||
|
|
||||||
|
Response 200:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"caseId": "uuid",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"type": "VEX_DOC",
|
||||||
|
"title": "Vendor OpenVEX assertion",
|
||||||
|
"issuer": "vendor.example",
|
||||||
|
"signed": true,
|
||||||
|
"signedBy": "CN=Vendor VEX Signer",
|
||||||
|
"contentHash": "hex",
|
||||||
|
"createdAt": "2025-12-15T22:10:00Z",
|
||||||
|
"previewUrl": "/api/triage/v1/evidence/uuid/preview",
|
||||||
|
"rawUrl": "/api/triage/v1/evidence/uuid/raw"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Get raw evidence object
|
||||||
|
|
||||||
|
`GET /evidence/{evidenceId}/raw`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* `application/json` for JSON evidence
|
||||||
|
* `application/octet-stream` for binary
|
||||||
|
* MUST include `Content-SHA256` header (hex) when possible.
|
||||||
|
|
||||||
|
### 3.3 Preview evidence object
|
||||||
|
|
||||||
|
`GET /evidence/{evidenceId}/preview`
|
||||||
|
|
||||||
|
Returns a compact representation safe for UI preview.
|
||||||
|
|
||||||
|
## 4. Decisions
|
||||||
|
|
||||||
|
### 4.1 Create decision
|
||||||
|
|
||||||
|
`POST /decisions`
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"caseId": "uuid",
|
||||||
|
"kind": "MUTE_REACH",
|
||||||
|
"reasonCode": "NON_REACHABLE",
|
||||||
|
"note": "No entry path in this env; reviewed runtime traces.",
|
||||||
|
"ttl": "2026-01-16T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response 201:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"decision": {
|
||||||
|
"id": "uuid",
|
||||||
|
"kind": "MUTE_REACH",
|
||||||
|
"reasonCode": "NON_REACHABLE",
|
||||||
|
"note": "No entry path in this env; reviewed runtime traces.",
|
||||||
|
"ttl": "2026-01-16T00:00:00Z",
|
||||||
|
"actor": { "subject": "user:abc", "display": "Vlad" },
|
||||||
|
"createdAt": "2025-12-16T01:10:00Z",
|
||||||
|
"signatureRef": "dsse:rekor:uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Server signs decisions (DSSE) and persists signature reference.
|
||||||
|
* Creating a decision MUST create a `Snapshot` with trigger `DECISION`.
|
||||||
|
|
||||||
|
### 4.2 Revoke decision
|
||||||
|
|
||||||
|
`POST /decisions/{decisionId}/revoke`
|
||||||
|
|
||||||
|
Body (optional):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "reason": "Mistake; reachability now observed." }
|
||||||
|
```
|
||||||
|
|
||||||
|
Response 200:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "revokedAt": "2025-12-16T02:00:00Z", "signatureRef": "dsse:rekor:uuid" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Snapshots & Smart-Diff
|
||||||
|
|
||||||
|
### 5.1 List snapshots
|
||||||
|
|
||||||
|
`GET /cases/{caseId}/snapshots`
|
||||||
|
|
||||||
|
Response 200:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"caseId": "uuid",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"trigger": "POLICY_UPDATE",
|
||||||
|
"changedAt": "2025-12-16T00:00:00Z",
|
||||||
|
"fromInputsHash": "hex",
|
||||||
|
"toInputsHash": "hex",
|
||||||
|
"summary": "Policy version changed; gate threshold crossed."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Smart-Diff between two snapshots
|
||||||
|
|
||||||
|
`GET /cases/{caseId}/smart-diff?from={inputsHashA}&to={inputsHashB}`
|
||||||
|
|
||||||
|
Response 200:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fromInputsHash": "hex",
|
||||||
|
"toInputsHash": "hex",
|
||||||
|
"inputsChanged": [
|
||||||
|
{ "key": "policyVersion", "before": "2025.12.14", "after": "2025.12.16", "evidenceIds": ["uuid"] }
|
||||||
|
],
|
||||||
|
"outputsChanged": [
|
||||||
|
{ "key": "verdict", "before": "SHIP", "after": "BLOCK", "evidenceIds": ["uuid"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Export Evidence Bundle
|
||||||
|
|
||||||
|
### 6.1 Start export
|
||||||
|
|
||||||
|
`POST /cases/{caseId}/export`
|
||||||
|
|
||||||
|
Response 202:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"exportId": "uuid",
|
||||||
|
"status": "QUEUED"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Poll export
|
||||||
|
|
||||||
|
`GET /exports/{exportId}`
|
||||||
|
|
||||||
|
Response 200:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"exportId": "uuid",
|
||||||
|
"status": "READY",
|
||||||
|
"downloadUrl": "/api/triage/v1/exports/uuid/download"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Download bundle
|
||||||
|
|
||||||
|
`GET /exports/{exportId}/download`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* `application/zip`
|
||||||
|
* DSSE envelope embedded (or alongside in zip)
|
||||||
|
* bundle contains replay manifest, artifacts, risk result, snapshots
|
||||||
|
|
||||||
|
## 7. Events (Notify.WebService integration)
|
||||||
|
|
||||||
|
These are emitted by `notify.webservice` when scanner outputs change.
|
||||||
|
|
||||||
|
* `first_signal`
|
||||||
|
* fired on first actionable detection for an asset/environment
|
||||||
|
* `risk_changed`
|
||||||
|
* fired when verdict/lane changes or thresholds crossed
|
||||||
|
* `gate_blocked`
|
||||||
|
* fired when CI gate blocks
|
||||||
|
|
||||||
|
Event payload includes:
|
||||||
|
|
||||||
|
* caseId
|
||||||
|
* old/new verdict/lane/score (for changed events)
|
||||||
|
* inputsHash
|
||||||
|
* links to `/cases/{caseId}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Target Platform**: .NET 10, PostgreSQL >= 16
|
||||||
334
docs/api/unknowns-api.md
Normal file
334
docs/api/unknowns-api.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# Unknowns API Reference
|
||||||
|
|
||||||
|
**Sprint:** SPRINT_3600_0002_0001
|
||||||
|
**Task:** UNK-RANK-011 - Update unknowns API documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Unknowns API provides access to items that could not be fully classified due to missing evidence, ambiguous data, or incomplete intelligence. Unknowns are ranked by blast radius, exploit pressure, and containment signals.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/v1/unknowns
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All endpoints require Bearer token authentication:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Required scope: `scanner:unknowns:read`
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### List Unknowns
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/unknowns
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns paginated list of unknowns, optionally sorted by score.
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `sort` | string | `score` | Sort field: `score`, `created_at`, `blast_dependents` |
|
||||||
|
| `order` | string | `desc` | Sort order: `asc`, `desc` |
|
||||||
|
| `page` | int | 1 | Page number (1-indexed) |
|
||||||
|
| `pageSize` | int | 50 | Items per page (max 200) |
|
||||||
|
| `artifact` | string | - | Filter by artifact digest |
|
||||||
|
| `reason` | string | - | Filter by reason code |
|
||||||
|
| `minScore` | float | - | Minimum score threshold (0-1) |
|
||||||
|
| `maxScore` | float | - | Maximum score threshold (0-1) |
|
||||||
|
| `kev` | bool | - | Filter by KEV status |
|
||||||
|
| `seccomp` | string | - | Filter by seccomp state: `enforced`, `permissive`, `unknown` |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "unk-12345678-abcd-1234-5678-abcdef123456",
|
||||||
|
"artifactDigest": "sha256:abc123...",
|
||||||
|
"artifactPurl": "pkg:oci/myapp@sha256:abc123",
|
||||||
|
"reasons": ["missing_vex", "ambiguous_indirect_call"],
|
||||||
|
"blastRadius": {
|
||||||
|
"dependents": 15,
|
||||||
|
"netFacing": true,
|
||||||
|
"privilege": "user"
|
||||||
|
},
|
||||||
|
"evidenceScarcity": 0.7,
|
||||||
|
"exploitPressure": {
|
||||||
|
"epss": 0.45,
|
||||||
|
"kev": false
|
||||||
|
},
|
||||||
|
"containment": {
|
||||||
|
"seccomp": "enforced",
|
||||||
|
"fs": "ro"
|
||||||
|
},
|
||||||
|
"score": 0.62,
|
||||||
|
"proofRef": "proofs/unknowns/unk-12345678/tree.json",
|
||||||
|
"createdAt": "2025-01-15T10:30:00Z",
|
||||||
|
"updatedAt": "2025-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 50,
|
||||||
|
"totalItems": 142,
|
||||||
|
"totalPages": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get top 10 highest-scored unknowns
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"https://scanner.example.com/api/v1/unknowns?sort=score&order=desc&pageSize=10"
|
||||||
|
|
||||||
|
# Filter by KEV and minimum score
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"https://scanner.example.com/api/v1/unknowns?kev=true&minScore=0.5"
|
||||||
|
|
||||||
|
# Filter by artifact
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
"https://scanner.example.com/api/v1/unknowns?artifact=sha256:abc123"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Unknown by ID
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/unknowns/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns detailed information about a specific unknown.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "unk-12345678-abcd-1234-5678-abcdef123456",
|
||||||
|
"artifactDigest": "sha256:abc123...",
|
||||||
|
"artifactPurl": "pkg:oci/myapp@sha256:abc123",
|
||||||
|
"reasons": ["missing_vex", "ambiguous_indirect_call"],
|
||||||
|
"reasonDetails": [
|
||||||
|
{
|
||||||
|
"code": "missing_vex",
|
||||||
|
"message": "No VEX statement found for CVE-2024-1234",
|
||||||
|
"component": "pkg:npm/lodash@4.17.20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ambiguous_indirect_call",
|
||||||
|
"message": "Indirect call target could not be resolved",
|
||||||
|
"location": "src/utils.js:42"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"blastRadius": {
|
||||||
|
"dependents": 15,
|
||||||
|
"netFacing": true,
|
||||||
|
"privilege": "user"
|
||||||
|
},
|
||||||
|
"evidenceScarcity": 0.7,
|
||||||
|
"exploitPressure": {
|
||||||
|
"epss": 0.45,
|
||||||
|
"kev": false
|
||||||
|
},
|
||||||
|
"containment": {
|
||||||
|
"seccomp": "enforced",
|
||||||
|
"fs": "ro"
|
||||||
|
},
|
||||||
|
"score": 0.62,
|
||||||
|
"scoreBreakdown": {
|
||||||
|
"blastComponent": 0.35,
|
||||||
|
"scarcityComponent": 0.21,
|
||||||
|
"pressureComponent": 0.26,
|
||||||
|
"containmentDeduction": -0.20
|
||||||
|
},
|
||||||
|
"proofRef": "proofs/unknowns/unk-12345678/tree.json",
|
||||||
|
"createdAt": "2025-01-15T10:30:00Z",
|
||||||
|
"updatedAt": "2025-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Unknown Proof
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/unknowns/{id}/proof
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the proof tree explaining the ranking decision.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"unknownId": "unk-12345678-abcd-1234-5678-abcdef123456",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "input",
|
||||||
|
"hash": "sha256:abc...",
|
||||||
|
"data": {
|
||||||
|
"reasons": ["missing_vex"],
|
||||||
|
"evidenceScarcity": 0.7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "delta",
|
||||||
|
"hash": "sha256:def...",
|
||||||
|
"factor": "blast_radius",
|
||||||
|
"contribution": 0.35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "delta",
|
||||||
|
"hash": "sha256:ghi...",
|
||||||
|
"factor": "containment_seccomp",
|
||||||
|
"contribution": -0.10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "score",
|
||||||
|
"hash": "sha256:jkl...",
|
||||||
|
"finalScore": 0.62
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rootHash": "sha256:mno..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Get Unknowns
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/unknowns/batch
|
||||||
|
```
|
||||||
|
|
||||||
|
Get multiple unknowns by ID in a single request.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": [
|
||||||
|
"unk-12345678-abcd-1234-5678-abcdef123456",
|
||||||
|
"unk-87654321-dcba-4321-8765-654321fedcba"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Same format as list response with matching items.
|
||||||
|
|
||||||
|
### Get Unknowns Summary
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/unknowns/summary
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns aggregate statistics about unknowns.
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `artifact` | string | Filter by artifact digest |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"totalCount": 142,
|
||||||
|
"byReason": {
|
||||||
|
"missing_vex": 45,
|
||||||
|
"ambiguous_indirect_call": 32,
|
||||||
|
"incomplete_sbom": 28,
|
||||||
|
"unknown_platform": 15,
|
||||||
|
"other": 22
|
||||||
|
},
|
||||||
|
"byScoreBucket": {
|
||||||
|
"critical": 12, // score >= 0.8
|
||||||
|
"high": 35, // 0.6 <= score < 0.8
|
||||||
|
"medium": 48, // 0.4 <= score < 0.6
|
||||||
|
"low": 47 // score < 0.4
|
||||||
|
},
|
||||||
|
"byContainment": {
|
||||||
|
"enforced": 45,
|
||||||
|
"permissive": 32,
|
||||||
|
"unknown": 65
|
||||||
|
},
|
||||||
|
"kevCount": 8,
|
||||||
|
"avgScore": 0.52
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reason Codes
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `missing_vex` | No VEX statement for vulnerability |
|
||||||
|
| `ambiguous_indirect_call` | Indirect call target unresolved |
|
||||||
|
| `incomplete_sbom` | SBOM missing component data |
|
||||||
|
| `unknown_platform` | Platform not recognized |
|
||||||
|
| `missing_advisory` | No advisory data for CVE |
|
||||||
|
| `conflicting_evidence` | Multiple conflicting data sources |
|
||||||
|
| `stale_data` | Data exceeds freshness threshold |
|
||||||
|
|
||||||
|
## Score Calculation
|
||||||
|
|
||||||
|
The unknown score is calculated as:
|
||||||
|
|
||||||
|
```
|
||||||
|
score = 0.60 × blast + 0.30 × scarcity + 0.30 × pressure + containment_deduction
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
- `blast` = normalized blast radius (0-1)
|
||||||
|
- `scarcity` = evidence scarcity factor (0-1)
|
||||||
|
- `pressure` = exploit pressure (EPSS + KEV factor)
|
||||||
|
- `containment_deduction` = -0.10 for enforced seccomp, -0.10 for read-only FS
|
||||||
|
|
||||||
|
### Blast Radius Normalization
|
||||||
|
|
||||||
|
```
|
||||||
|
dependents_normalized = min(dependents / 50, 1.0)
|
||||||
|
net_factor = 0.5 if net_facing else 0.0
|
||||||
|
priv_factor = 0.5 if privilege == "root" else 0.0
|
||||||
|
blast = min((dependents_normalized + net_factor + priv_factor) / 2, 1.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exploit Pressure
|
||||||
|
|
||||||
|
```
|
||||||
|
epss_normalized = epss ?? 0.35 // Default if unknown
|
||||||
|
kev_factor = 0.30 if kev else 0.0
|
||||||
|
pressure = min(epss_normalized + kev_factor, 1.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
| Status | Code | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| 400 | `INVALID_PARAMETER` | Invalid query parameter |
|
||||||
|
| 401 | `UNAUTHORIZED` | Missing or invalid token |
|
||||||
|
| 403 | `FORBIDDEN` | Insufficient permissions |
|
||||||
|
| 404 | `NOT_FOUND` | Unknown not found |
|
||||||
|
| 429 | `RATE_LIMITED` | Too many requests |
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- List: 100 requests/minute
|
||||||
|
- Get by ID: 300 requests/minute
|
||||||
|
- Summary: 60 requests/minute
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Unknowns Ranking Technical Reference](../product-advisories/14-Dec-2025%20-%20Triage%20and%20Unknowns%20Technical%20Reference.md)
|
||||||
|
- [Scanner Architecture](../modules/scanner/architecture.md)
|
||||||
|
- [Proof Bundle Format](../api/proof-bundle-format.md)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user