Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions .claude/commands/audit-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
name: audit-security
description: Audit OpenIAP's supply-chain security posture — SBOM correctness and NTIA completeness, release provenance, workflow permissions, and documentation drift — then fix what it finds. Use when the user asks to audit security, check SBOM quality, verify supply-chain posture, or before a release train.
---

# Audit Security Posture

Check what OpenIAP publishes about itself against what it actually does. Every
step below produces evidence, not an opinion.

Run this before a release train, after changing anything under `security/`,
`scripts/generate-sbom*`, or `.github/workflows/`, and whenever a new
releasable component is added.

The canonical policy this audits lives in
[`security/README.md`](../../security/README.md),
[`security/SBOM.md`](../../security/SBOM.md), and
[`security/CRA.md`](../../security/CRA.md). Read them before reporting a gap —
several apparent gaps are documented deliberate choices.

## 1. SBOM generates for every releasable component

The component list is owned by the release SSOT, not by the SBOM code. A
component that can be released but has no SBOM definition is a release that
ships without an inventory.

```bash
node --test scripts/generate-sbom.test.mjs

for c in $(node -e 'import("./scripts/generate-sbom.mjs").then(m=>console.log(m.listComponentIds().join(" ")))'); do
printf "%-14s " "$c"
node scripts/generate-sbom.mjs "$c" --output-dir /tmp/sbom-audit || echo "FAILED"
done
```

A failure here is the intended behaviour when a build manifest gained a
declaration shape the reader does not model — fix the reader, never silence it.

## 2. Schema validity

```bash
for f in /tmp/sbom-audit/*.cdx.json; do
cyclonedx validate --input-file "$f" --input-format json \
--input-version v1_6 --fail-on-errors
done
```

Install with `brew install cyclonedx-cli` if absent.

## 3. NTIA minimum elements

The [NTIA minimum elements](https://www.ntia.gov/report/2021/minimum-elements-software-bill-materials-sbom)
are the baseline OpenSSF recommends measuring against. Check author, timestamp,
and per-component name, version, purl, supplier, and dependency relationships:

```bash
node -e '
const fs = require("fs");
const dir = "/tmp/sbom-audit";
let tot = 0, sup = 0, lic = 0, purl = 0, auth = 0, files = 0;
for (const f of fs.readdirSync(dir)) {
const j = JSON.parse(fs.readFileSync(`${dir}/${f}`, "utf8"));
files++;
if (j.metadata?.authors?.length) auth++;
for (const c of j.components ?? []) {
tot++;
if (c.supplier?.name) sup++;
if (c.licenses?.length) lic++;
if (c.purl) purl++;
}
}
console.log(`SBOM author: ${auth}/${files}`);
console.log(`component purl: ${purl}/${tot}`);
console.log(`component supplier: ${sup}/${tot}`);
console.log(`component license: ${lic}/${tot}`);
'
```

Regenerate with `--with-licenses` when auditing supplier and license coverage;
without it those fields are intentionally absent so local runs stay offline.

Known structural gaps, which are **not** findings: pub.dev exposes neither
license nor supplier in package metadata, and some NuGet packages carry only a
non-SPDX license URL.

Optionally score the result with
[`sbomqs`](https://github.com/interlynk-io/sbomqs):
`sbomqs score /tmp/sbom-audit/*.cdx.json`.

## 4. No leaked paths or secrets

A published SBOM is a public document about a private filesystem.

```bash
grep -rlE '/Users/|/home/[a-z]|/tmp/|ghp_|npm_[A-Za-z0-9]|BEGIN [A-Z ]*PRIVATE KEY' \
/tmp/sbom-audit/ && echo "LEAK" || echo "clean"
```

## 5. Determinism

Regeneration at the same commit must be byte-identical, or the reproducibility
claim in `security/SBOM.md` is false.

```bash
node scripts/generate-sbom.mjs google --output-dir /tmp/sbom-audit-2
diff /tmp/sbom-audit/openiap-google-*.cdx.json /tmp/sbom-audit-2/openiap-google-*.cdx.json
```

## 6. Workflow permissions and injection

Least privilege, and no untrusted value interpolated into a shell command:

```bash
# Any ${{ }} inside a run: block is a potential injection point
for f in .github/workflows/*.yml; do
awk '/^\s+run:/{r=1} /^\s+- name:|^\s+uses:/{r=0} r && /\$\{\{/ {print FILENAME": "$0}' "$f"
done

# Workflows that write must say so explicitly
grep -L "^permissions:" .github/workflows/*.yml
```

Pass values through `env:` instead of interpolating them. OpenSSF Scorecard's
Dangerous-Workflow check flags the same pattern.

## 7. Generated SBOMs stay out of git

```bash
git check-ignore -v sbom/ && echo "ignored" || echo "GAP: sbom/ is committable"
git ls-files '*.cdx.json' | head # must be empty
```

## 8. Documentation matches the code

Documentation drift is the most common finding, because prose has no compiler.

```bash
# Component table in security/SBOM.md vs the release SSOT
node -e '
import("./scripts/generate-sbom.mjs").then((m) => {
const doc = require("fs").readFileSync("security/SBOM.md", "utf8");
for (const id of m.listComponentIds()) {
if (!doc.includes(`\`${id}\``)) console.log(`MISSING from SBOM.md: ${id}`);
}
});
'

# External references must resolve
grep -rhoE "https?://[^)\" ]+" security/*.md security/vex/*.md \
packages/docs/src/pages/docs/security/*.tsx |
sed 's/[.,)"]*$//' | sort -u |
while read -r u; do
code=$(curl -sS -o /dev/null -w "%{http_code}" -L --max-time 20 "$u")
[ "$code" = "200" ] || echo "$code $u"
done
```

Also check for **hardcoded counts** — "nine workflows", "43 of 47
dependencies". They are true on the day they are written and wrong later.
Prefer a described property or a command that prints the live number.

## 9. Release integrity still holds

```bash
node --test scripts/release-branch-policy.test.mjs \
scripts/npm-publish-authorization.test.mjs \
scripts/verify-npm-release-provenance.test.mjs
node scripts/release-branch-policy.mjs audit
```

## 10. Report

State each check as pass, gap, or not-applicable with the command output that
justifies it. For every gap, either fix it in the same pass or record why it is
deliberate. Do not report a check as passing when its tool was unavailable —
report it as unrun and say which tool is missing.
5 changes: 4 additions & 1 deletion .claude/skills/loop-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Use Claude Code's matching skills or commands for each delegated phase:
- `/review-self` for pre-PR stabilization and exact-head fallback review.
- `/commit --all --pr` for commit, push, PR, labels, and preview.
- `/review-pr <PR>` for review threads, CodeRabbit, CI polling, and cleanup.
- `/e2e-tests` for the device-regression gate — hand back to the user to run it
rather than merging, since it needs real devices and store accounts.
- `ScheduleWakeup` for every five-minute re-entry; never use a shell sleep loop.

Do not merge until the canonical exact-head clean gate is satisfied.
Do not merge until the canonical exact-head clean gate is satisfied, and stop
without merging when the canonical device-regression gate applies.
43 changes: 40 additions & 3 deletions .codex/skills/loop-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,43 @@ Clean means all of the following hold for the same head SHA:
- the PR is mergeable and the branch contains every required update from main;
- the worktree is clean and the final diff has been reread.

## 6. Merge And Close The Loop
## 6. Gate Device Regression Before Merging

Device-backed regression needs real hardware, store accounts, and sandbox
purchases, so this loop cannot run it unattended. Decide whether the change
requires it **before** merging, not after.

Require `$e2e-tests` when the diff touches any of:

- `packages/apple/`, `packages/google/`, or `packages/kit/`;
- any `libraries/<sdk>/` implementation, example app, or podspec/gradle/csproj
manifest;
- `packages/gql/src/*.graphql` or the generated types synced from it;
- native build configuration, dependency placement, config plugins, or store
metadata for any of the above.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

When it is required, **stop without merging even if the PR is otherwise
clean**. Report the exact regression-matrix rows the diff implicates and hand
back to the user. The loop does not merge such a change on its own authority.

Exactly two things clear the gate, and both are recorded on the PR before any
merge:

1. A `$e2e-tests` run covering the implicated rows, with its result posted.
2. An explicit written waiver from the user in this conversation, naming the
rows waived and the reason. Record it verbatim on the PR. Absence of an
objection is not a waiver, and the loop must never grant one to itself.

A clean CI run is not a substitute: CI does not exercise purchase dialogs,
store accounts, or device wiring.

When it is not required, say so explicitly in the final report and name the
paths that justify it. Silence here reads as an untested merge.

A change confined to documentation, repository automation, agent workflows, or
release/security tooling does not need device regression.

## 7. Merge And Close The Loop

Immediately before merging, refetch the PR and confirm its head still equals the
clean reviewed SHA. Use the repository-supported merge method, defaulting to a
Expand All @@ -125,5 +161,6 @@ After merge:

Stop without merging when a required choice lacks authority, the same finding
survives two fix attempts, an access blocker repeats under the source workflow's
threshold, or the exact head cannot satisfy the clean gate. Report the concrete
blocker; never describe a pending or partially reviewed PR as clean.
threshold, the change requires device regression that has not been run, or the
exact head cannot satisfy the clean gate. Report the concrete blocker; never
describe a pending or partially reviewed PR as clean.
2 changes: 2 additions & 0 deletions .codex/skills/openiap-workflows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ natural-language requests, execute the matching workflow:
exactly as defined by the command workflow.
- Audit code, check latest APIs, or "audit-code": read
`.claude/commands/audit-code.md`.
- Audit SBOM quality, release provenance, workflow permissions, or
supply-chain/security posture: read `.claude/commands/audit-security.md`.
- Compile knowledge or rebuild AI context: read
`.claude/commands/compile-knowledge.md`.
- Resolve a GitHub issue: read `.claude/commands/resolve-issue.md`.
Expand Down
Binary file added .github/pr-previews/pr-318-security-docs.webm
Binary file not shown.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
node --test scripts/release-branch-policy.test.mjs
scripts/npm-publish-authorization.test.mjs
scripts/verify-npm-release-provenance.test.mjs
scripts/generate-sbom.test.mjs

- name: Test Gradle network retry helper
run: node --test scripts/ci/retry-gradle.test.mjs
Expand Down
Loading
Loading