Skip to content

Commit b7931ea

Browse files
SecAI-Hubclaude
andcommitted
M52: Better release verification UX — Makefile, release manifest, structured verify output
Add repo-root Makefile with developer targets (verify-release, test, shellcheck, lint). Add RELEASE_MANIFEST.json generation to release CI pipeline (image digest, binaries with SHA256, SBOMs, provenance, checksums, build metadata — transitively signed via SHA256SUMS). Enhance verify-release.sh with --json and --report flags for machine-readable and human-readable structured output while preserving backward compatibility. Wire audit-quick-path.md to verify-release.sh with Make target reference. Update sample-release-bundle.md with manifest documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 46fbf31 commit b7931ea

7 files changed

Lines changed: 430 additions & 41 deletions

File tree

.github/workflows/release.yml

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,101 @@ jobs:
108108
path: dist/
109109
merge-multiple: true
110110

111+
- name: Record release image digest
112+
run: |
113+
IMAGE_REF="ghcr.io/${{ github.repository }}"
114+
TAG="${{ github.ref_name }}"
115+
DIGEST=$(skopeo inspect "docker://${IMAGE_REF}:${TAG}" 2>/dev/null | jq -r '.Digest' || echo "")
116+
if [ -n "$DIGEST" ] && [ "$DIGEST" != "null" ]; then
117+
echo "${DIGEST}" > dist/IMAGE_DIGEST
118+
echo "${IMAGE_REF}@${DIGEST}" > dist/IMAGE_REF_PINNED
119+
echo "## Install with digest pinning" >> "$GITHUB_STEP_SUMMARY"
120+
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
121+
echo "sudo bash secai-bootstrap.sh --digest ${DIGEST}" >> "$GITHUB_STEP_SUMMARY"
122+
echo '```' >> "$GITHUB_STEP_SUMMARY"
123+
else
124+
echo "WARNING: Could not extract image digest for tag ${TAG}"
125+
echo "unknown" > dist/IMAGE_DIGEST
126+
fi
127+
128+
- name: Generate release manifest
129+
run: |
130+
cd dist
131+
132+
# Collect binary names + SHA256 hashes
133+
BINARIES_JSON="[]"
134+
for bin in *-linux-*; do
135+
[ -f "$bin" ] || continue
136+
HASH=$(sha256sum "$bin" | awk '{print $1}')
137+
BINARIES_JSON=$(echo "$BINARIES_JSON" | jq \
138+
--arg name "$bin" --arg sha256 "$HASH" \
139+
'. + [{"name": $name, "sha256": $sha256}]')
140+
done
141+
142+
# Collect SBOM filenames
143+
SBOMS_JSON="[]"
144+
for sbom in *-sbom.cdx.json; do
145+
[ -f "$sbom" ] || continue
146+
SBOMS_JSON=$(echo "$SBOMS_JSON" | jq \
147+
--arg name "$sbom" \
148+
'. + [$name]')
149+
done
150+
151+
# Read image digest
152+
IMAGE_DIGEST="unknown"
153+
if [ -f IMAGE_DIGEST ]; then
154+
IMAGE_DIGEST=$(cat IMAGE_DIGEST)
155+
fi
156+
IMAGE_REF_PINNED=""
157+
if [ -f IMAGE_REF_PINNED ]; then
158+
IMAGE_REF_PINNED=$(cat IMAGE_REF_PINNED)
159+
fi
160+
161+
# Build manifest JSON
162+
jq -n \
163+
--arg schema_version "1" \
164+
--arg tag "${{ github.ref_name }}" \
165+
--arg image_ref "ghcr.io/${{ github.repository }}" \
166+
--arg image_digest "$IMAGE_DIGEST" \
167+
--arg image_ref_pinned "$IMAGE_REF_PINNED" \
168+
--argjson binaries "$BINARIES_JSON" \
169+
--argjson sboms "$SBOMS_JSON" \
170+
--arg provenance_type "https://slsa.dev/provenance/v1" \
171+
--arg checksum_file "SHA256SUMS" \
172+
--arg signature_file "SHA256SUMS.sig" \
173+
--arg commit_sha "${{ github.sha }}" \
174+
--arg workflow_run "${{ github.run_id }}" \
175+
--arg workflow_ref "${{ github.workflow_ref }}" \
176+
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
177+
'{
178+
schema_version: $schema_version,
179+
tag: $tag,
180+
image: {
181+
ref: $image_ref,
182+
digest: $image_digest,
183+
ref_pinned: $image_ref_pinned
184+
},
185+
binaries: $binaries,
186+
sboms: $sboms,
187+
provenance: {
188+
type: $provenance_type,
189+
attested: true
190+
},
191+
checksums: {
192+
file: $checksum_file,
193+
signature: $signature_file
194+
},
195+
build: {
196+
commit_sha: $commit_sha,
197+
workflow_run: $workflow_run,
198+
workflow_ref: $workflow_ref,
199+
timestamp: $timestamp
200+
}
201+
}' > RELEASE_MANIFEST.json
202+
203+
echo "--- RELEASE_MANIFEST.json ---"
204+
cat RELEASE_MANIFEST.json
205+
111206
- name: Generate SHA256 checksums
112207
run: |
113208
cd dist
@@ -141,23 +236,6 @@ jobs:
141236
env:
142237
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
143238

144-
- name: Record release image digest
145-
run: |
146-
IMAGE_REF="ghcr.io/${{ github.repository }}"
147-
TAG="${{ github.ref_name }}"
148-
DIGEST=$(skopeo inspect "docker://${IMAGE_REF}:${TAG}" 2>/dev/null | jq -r '.Digest' || echo "")
149-
if [ -n "$DIGEST" ] && [ "$DIGEST" != "null" ]; then
150-
echo "${DIGEST}" > dist/IMAGE_DIGEST
151-
echo "${IMAGE_REF}@${DIGEST}" > dist/IMAGE_REF_PINNED
152-
echo "## Install with digest pinning" >> "$GITHUB_STEP_SUMMARY"
153-
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
154-
echo "sudo bash secai-bootstrap.sh --digest ${DIGEST}" >> "$GITHUB_STEP_SUMMARY"
155-
echo '```' >> "$GITHUB_STEP_SUMMARY"
156-
else
157-
echo "WARNING: Could not extract image digest for tag ${TAG}"
158-
echo "unknown" > dist/IMAGE_DIGEST
159-
fi
160-
161239
- name: Create GitHub Release
162240
if: ${{ !inputs.dry_run }}
163241
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
@@ -169,5 +247,6 @@ jobs:
169247
dist/SHA256SUMS.sig
170248
dist/IMAGE_DIGEST
171249
dist/IMAGE_REF_PINNED
250+
dist/RELEASE_MANIFEST.json
172251
generate_release_notes: true
173252
fail_on_unmatched_files: false

Makefile

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# SecAI OS — Developer Targets
2+
# Usage: make help
3+
#
4+
# Thin wrappers around existing CI / verification commands.
5+
# No build targets — image builds are handled by BlueBuild + release.yml.
6+
7+
.DEFAULT_GOAL := help
8+
SHELL := /usr/bin/env bash
9+
10+
# ---------------------------------------------------------------------------
11+
# Configuration (override via environment or make args)
12+
# ---------------------------------------------------------------------------
13+
IMAGE ?= ghcr.io/secai-hub/secai_os:latest
14+
15+
GO_SERVICES := airlock registry tool-firewall gpu-integrity-watch mcp-firewall \
16+
policy-engine runtime-attestor integrity-monitor incident-recorder
17+
18+
SCRIPTS_LIBEXEC := $(wildcard files/system/usr/libexec/secure-ai/*.sh)
19+
SCRIPTS_FILES := $(wildcard files/scripts/*.sh)
20+
21+
# ---------------------------------------------------------------------------
22+
# Targets
23+
# ---------------------------------------------------------------------------
24+
25+
.PHONY: help
26+
help: ## Show available targets
27+
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \
28+
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
29+
30+
.PHONY: verify-release
31+
verify-release: ## Verify a release image (IMAGE=ghcr.io/...)
32+
@files/scripts/verify-release.sh "$(IMAGE)"
33+
34+
.PHONY: test
35+
test: test-go test-python ## Run all tests (Go + Python)
36+
37+
.PHONY: test-go
38+
test-go: ## Run Go service tests (all 9 services, -race)
39+
@for svc in $(GO_SERVICES); do \
40+
echo "=== $${svc} ===" ; \
41+
(cd services/$${svc} && go test -v -race -count=1 ./...) || exit 1 ; \
42+
done
43+
44+
.PHONY: test-python
45+
test-python: ## Run Python tests (pytest tests/ -v)
46+
PYTHONPATH=services python -m pytest tests/ -v
47+
48+
.PHONY: shellcheck
49+
shellcheck: ## Lint all shell scripts with shellcheck
50+
shellcheck -s bash $(SCRIPTS_LIBEXEC) $(SCRIPTS_FILES)
51+
52+
.PHONY: lint
53+
lint: shellcheck ## Combined lint (shellcheck + ruff + go vet)
54+
ruff check services/ tests/ --select E,F,W --ignore E501,E402
55+
@for svc in $(GO_SERVICES); do \
56+
echo "--- vet: $${svc} ---" ; \
57+
(cd services/$${svc} && go vet ./...) || exit 1 ; \
58+
done

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ Every model passes through the same fully automatic pipeline:
158158
| **Updates** | Cosign-verified rpm-ostree, staged workflow, greenboot auto-rollback |
159159
| **Supply Chain** | Per-service CycloneDX SBOMs, SLSA3 provenance attestation, cosign-signed checksums |
160160

161-
See [docs/threat-model.md](docs/threat-model.md) for threat classes, residual risks, and security invariants. See [docs/security-status.md](docs/security-status.md) for implementation status of all 51 milestones.
161+
See [docs/threat-model.md](docs/threat-model.md) for threat classes, residual risks, and security invariants. See [docs/security-status.md](docs/security-status.md) for implementation status of all 52 milestones.
162162

163163
### Verify Image Signatures
164164

@@ -241,7 +241,7 @@ All CI jobs are defined in [`.github/workflows/ci.yml`](.github/workflows/ci.yml
241241
| [Threat Model](docs/threat-model.md) | Threat classes, invariants, residual risks |
242242
| [API Reference](docs/api.md) | HTTP API for all services |
243243
| [Policy Schema](docs/policy-schema.md) | Full policy.yaml schema reference |
244-
| [Security Status](docs/security-status.md) | Implementation status of all 50 milestones |
244+
| [Security Status](docs/security-status.md) | Implementation status of all 52 milestones |
245245
| [Test Matrix](docs/test-matrix.md) | Test coverage: 1,141 tests across Go and Python (see [test-counts.json](docs/test-counts.json)) |
246246
| [Compatibility Matrix](docs/compatibility-matrix.md) | GPU, VM, and hardware support |
247247
| [Security Test Matrix](docs/security-test-matrix.md) | Security feature test coverage |
@@ -378,7 +378,7 @@ See [docs/test-matrix.md](docs/test-matrix.md) for full breakdown.
378378
## Roadmap
379379

380380
<details>
381-
<summary>All 51 project milestones (click to expand)</summary>
381+
<summary>All 52 project milestones (click to expand)</summary>
382382

383383
- [x] **Milestone 0** -- Threat model, dataflow, invariants, policy files
384384
- [x] **Milestone 1** -- Bootable OS, encrypted vault, GPU drivers
@@ -432,6 +432,7 @@ See [docs/test-matrix.md](docs/test-matrix.md) for full breakdown.
432432
- [x] **Milestone 49** -- Signed-first install path: bootstrap script configures signing policy before first rebase (eliminates unverified transport), digest-pinned install flow (CI publishes digests in build summary + release assets), first-boot setup wizard (interactive integrity verification + vault + TPM2 + health check), recovery/dev path separated into dedicated doc
433433
- [x] **Milestone 50** -- Production operations package: backup/restore scripts (full/config/logs/keys categories, age/gpg encryption, SHA256 manifest, LUKS header backup/restore), rollback decision matrix (Greenboot auto-rollback + manual criteria), 5 break-glass recovery procedures, formal data retention policy (7 data classes, disk capacity thresholds)
434434
- [x] **Milestone 51** -- Stronger observability: unified appliance health dashboard (trusted/degraded/recovery_required), live SLO compliance monitoring (uptime + P95 latency tracking), webhook alerting hooks for containment events, forensic bundle export via UI + CLI (secai-forensic.sh), recovery ceremony endpoints wired
435+
- [x] **Milestone 52** -- Better release verification UX: repo-root Makefile (verify-release, test, shellcheck, lint), RELEASE_MANIFEST.json in release CI (image digest, binaries, SBOMs, provenance, checksums, build metadata), verify-release.sh --json and --report flags, audit-quick-path wired to verify-release.sh
435436

436437
</details>
437438

docs/audit-quick-path.md

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,37 @@ cosign verify-blob \
230230
sha256sum -c SHA256SUMS
231231
```
232232

233+
### Automated Release Verification
234+
235+
For a single-command verification of all supply-chain artifacts, use the `verify-release.sh` script:
236+
237+
```bash
238+
# Download release artifacts
239+
mkdir release && cd release
240+
gh release download v1.0.0 -R SecAI-Hub/SecAI_OS
241+
242+
# Place cosign.pub (or set COSIGN_PUB_KEY)
243+
cp /path/to/cosign.pub .
244+
245+
# Run full verification (colored terminal output)
246+
../files/scripts/verify-release.sh ghcr.io/secai-hub/secai_os:v1.0.0
247+
248+
# Generate a human-readable report file
249+
../files/scripts/verify-release.sh --report verification-report.txt \
250+
ghcr.io/secai-hub/secai_os:v1.0.0
251+
252+
# Machine-readable JSON output (for CI pipelines or tooling)
253+
../files/scripts/verify-release.sh --json ghcr.io/secai-hub/secai_os:v1.0.0
254+
```
255+
256+
The script checks cosign image signature, CycloneDX SBOM attestation, SLSA3 provenance attestation, and SHA256 checksums. See `files/scripts/verify-release.sh --help` for configuration options.
257+
258+
Or via Make:
259+
260+
```bash
261+
make verify-release IMAGE=ghcr.io/secai-hub/secai_os:v1.0.0
262+
```
263+
233264
### Forensic Bundle Integrity
234265

235266
Export and verify a forensic bundle from a running appliance:
@@ -382,24 +413,33 @@ Run this all-in-one script to validate the test suite and artifact structure fro
382413
set -e
383414
echo "=== M5 Audit Quick Validation ==="
384415

385-
echo "[1/5] M5 acceptance suite..."
416+
echo "[1/6] M5 acceptance suite..."
386417
PYTHONPATH=services python -m pytest tests/test_m5_acceptance.py -v --tb=short
387418

388-
echo "[2/5] Adversarial tests..."
419+
echo "[2/6] Adversarial tests..."
389420
PYTHONPATH=services python -m pytest tests/test_adversarial.py -v --tb=short
390421

391-
echo "[3/5] Incident recorder recovery/forensic tests..."
422+
echo "[3/6] Incident recorder recovery/forensic tests..."
392423
(cd services/incident-recorder && go test -v -race -run "TestRecovery|TestEscalation|TestForensic|TestLatched" ./...)
393424

394-
echo "[4/5] MCP firewall adversarial tests..."
425+
echo "[4/6] MCP firewall adversarial tests..."
395426
(cd services/mcp-firewall && go test -v -race -run TestAdversarial ./...)
396427

397-
echo "[5/5] All Go service tests..."
428+
echo "[5/6] All Go service tests..."
398429
for svc in airlock registry tool-firewall gpu-integrity-watch mcp-firewall \
399430
policy-engine runtime-attestor integrity-monitor incident-recorder; do
400431
echo "--- ${svc} ---"
401432
(cd services/${svc} && go test -race -count=1 ./...)
402433
done
403434

435+
echo "[6/6] Release artifact verification..."
436+
if [ -f SHA256SUMS ]; then
437+
files/scripts/verify-release.sh --report /tmp/secai-verify-report.txt \
438+
ghcr.io/secai-hub/secai_os:latest
439+
echo "Report: /tmp/secai-verify-report.txt"
440+
else
441+
echo "SKIP: No release artifacts found (download with 'gh release download')"
442+
fi
443+
404444
echo "=== All M5 checks passed ==="
405445
```

docs/sample-release-bundle.md

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ v1.0.0/
4747
diffusion-worker-sbom.cdx.json
4848
search-mediator-sbom.cdx.json
4949
50+
# Release manifest (machine-readable)
51+
RELEASE_MANIFEST.json # structured JSON: image, binaries, SBOMs, provenance, build metadata
52+
5053
# Checksums and signature
51-
SHA256SUMS # sha256sum of every artifact above
54+
SHA256SUMS # sha256sum of every artifact above (includes RELEASE_MANIFEST.json)
5255
SHA256SUMS.sig # cosign detached signature over SHA256SUMS
5356
```
5457

@@ -81,6 +84,59 @@ cosign sign-blob --yes \
8184
SHA256SUMS
8285
```
8386

87+
## Release Manifest
88+
89+
`RELEASE_MANIFEST.json` is a structured JSON file that catalogues every artifact in the release bundle. It is included in `SHA256SUMS` and is therefore transitively signed.
90+
91+
Example structure:
92+
93+
```json
94+
{
95+
"schema_version": "1",
96+
"tag": "v1.0.0",
97+
"image": {
98+
"ref": "ghcr.io/secai-hub/secai_os",
99+
"digest": "sha256:a1b2c3d4e5f6...",
100+
"ref_pinned": "ghcr.io/secai-hub/secai_os@sha256:a1b2c3d4e5f6..."
101+
},
102+
"binaries": [
103+
{"name": "airlock-linux-amd64", "sha256": "e3b0c44298fc..."},
104+
{"name": "airlock-linux-arm64", "sha256": "7d865e959b24..."}
105+
],
106+
"sboms": [
107+
"airlock-sbom.cdx.json",
108+
"registry-sbom.cdx.json"
109+
],
110+
"provenance": {
111+
"type": "https://slsa.dev/provenance/v1",
112+
"attested": true
113+
},
114+
"checksums": {
115+
"file": "SHA256SUMS",
116+
"signature": "SHA256SUMS.sig"
117+
},
118+
"build": {
119+
"commit_sha": "abc123def456...",
120+
"workflow_run": "12345678",
121+
"workflow_ref": "SecAI-Hub/SecAI_OS/.github/workflows/release.yml@refs/tags/v1.0.0",
122+
"timestamp": "2026-03-15T12:00:00Z"
123+
}
124+
}
125+
```
126+
127+
Use `jq` to inspect specific fields:
128+
129+
```bash
130+
# Image digest
131+
jq -r '.image.digest' RELEASE_MANIFEST.json
132+
133+
# List all binaries with their hashes
134+
jq '.binaries[] | .name + " " + .sha256' RELEASE_MANIFEST.json
135+
136+
# Build commit
137+
jq -r '.build.commit_sha' RELEASE_MANIFEST.json
138+
```
139+
84140
## SBOM Files
85141

86142
Each service gets a CycloneDX JSON SBOM generated by [Syft](https://github.com/anchore/syft). These SBOMs list all direct and transitive dependencies for the service.
@@ -228,8 +284,17 @@ gh release download v1.0.0 -R SecAI-Hub/SecAI_OS
228284
# Place cosign.pub in the directory (or set COSIGN_PUB_KEY)
229285
cp /path/to/cosign.pub .
230286

231-
# Run full verification
287+
# Run full verification (colored terminal output)
232288
../files/scripts/verify-release.sh ghcr.io/secai-hub/secai_os:v1.0.0
289+
290+
# Save a human-readable report
291+
../files/scripts/verify-release.sh --report report.txt ghcr.io/secai-hub/secai_os:v1.0.0
292+
293+
# Machine-readable JSON (for CI pipelines or tooling)
294+
../files/scripts/verify-release.sh --json ghcr.io/secai-hub/secai_os:v1.0.0
295+
296+
# Or via Make target from the repo root
297+
make verify-release IMAGE=ghcr.io/secai-hub/secai_os:v1.0.0
233298
```
234299

235300
The script prints PASS/FAIL for each step and exits non-zero if any check fails. See `--help` for configuration options.

0 commit comments

Comments
 (0)