diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 7bac3314..474dea0b 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -25,3 +25,19 @@ jobs: pip install --require-hashes -r requirements.lock - name: Run pip-audit run: pip-audit -r requirements.lock --ignore-vuln GHSA-wj6h-64fc-37mp --ignore-vuln CVE-2025-62727 + # Entropy audit L5: pip-audit answers "are there known CVEs today". An + # SBOM answers "which exact versions were in this build", which is the + # question that arises when a vulnerability is disclosed later against a + # release already in the wild. + - name: Generate CycloneDX SBOM + run: | + python -m pip install cyclonedx-bom + mkdir -p security-artifacts + cyclonedx-py requirements requirements.lock \ + --of json --output-reproducible \ + -o security-artifacts/sbom.cdx.json + - uses: actions/upload-artifact@v4 + with: + name: seedpass-sbom + path: security-artifacts/ + if-no-files-found: error diff --git a/TODO.md b/TODO.md index 9bb1d02c..e83c7789 100644 --- a/TODO.md +++ b/TODO.md @@ -41,10 +41,18 @@ fixes land behind a new `gen_version` field; v1 output must stay byte-identical. site first"). Never bulk-migrate. Until this ships, existing entries stay on v1 by design. - [ ] **L2** — master seed is `os.urandom(32)` reduced to a hardcoded 12-word (128-bit) mnemonic while derived seeds default to 24 words. Not a defect; offer 24 words at profile creation. -- [ ] **L4** — `torch/`: predictable temp filename (`services/memory/index.js:60`), a `Math.random` - value misnamed `nonce` (`relay-health.mjs:95`), and three stale `_backups/` code copies that - pollute security greps. -- [ ] **L5** — no CycloneDX SBOM; folds into the supply-chain item below. +- [x] **L4 (code fixes done)** — `torch/` temp filename now uses `randomBytes(8)`; the + `Math.random` value misnamed `nonce` in `relay-health.mjs` is now `probeSuffix` from + `randomBytes(6)`. +- [ ] **⚠️ Decision needed: `torch/_backups/` holds 5240 tracked files** across six dated + snapshots. Untracking is a large deletion, so it is not an audit side effect. Costs today: + duplicates every security grep hit, and bare `node --test` in `torch/` discovers them and + fails 18 tests from stale snapshot code (`npm test` uses explicit paths, so CI is unaffected). + Separately: `torch/` has no `test/` directory in the repo, so its `npm test` cannot run as + written. +- [x] **L5 (done)** — `dependency-audit.yml` generates and uploads a CycloneDX SBOM from + `requirements.lock`. Verified with the real tool: CycloneDX 1.6, 96 components, crypto chain + recorded at exact versions. L3 (broad `except Exception` around crypto — these re-raise, so no silent fallback) overlaps the Robustness item further down. diff --git a/docs/entropy_audit_2026-07-31.md b/docs/entropy_audit_2026-07-31.md index 03008c3c..1a9a86e0 100644 --- a/docs/entropy_audit_2026-07-31.md +++ b/docs/entropy_audit_2026-07-31.md @@ -352,7 +352,27 @@ should be narrowed to the specific exceptions each path can raise. --- -### L4 — `torch/` subsystem +### L4 — `torch/` subsystem — **PARTLY DONE 2026-07-31** + +Fixed: the temp filename in `services/memory/index.js` now uses +`randomBytes(8)` instead of `Date.now()` + `Math.random()`, and the +`Math.random`-derived value in `relay-health.mjs` that was called a `nonce` is +renamed `probeSuffix` and drawn from `randomBytes(6)` — it is a public probe +tag, but the name invited reuse of the pattern somewhere it would matter. + +**Still open, and it needs a decision:** `torch/_backups/` holds **5240 tracked +files** across six dated snapshots. Untracking them is a large deletion, so it +is not something to do as a side effect of an entropy audit. Two concrete costs +today: they duplicate every security-relevant grep hit, and bare `node --test` +in `torch/` discovers them and fails 18 tests from stale snapshot code. (The +`npm test` script uses explicit paths, so CI is unaffected.) + +Separately noted: `torch/` has **no `test/` directory in the repo at all**, so +`npm test` there cannot run as written. Out of scope here, but worth knowing. + +The original note follows. + +### L4 (original) — `torch/` subsystem - `torch/src/services/memory/index.js:60` builds a temp filename from `Date.now()` + `Math.random()` before an atomic rename. Predictable name; @@ -368,7 +388,20 @@ should be narrowed to the specific exceptions each path can raise. --- -### L5 — No SBOM +### L5 — No SBOM — **DONE 2026-07-31** + +`dependency-audit.yml` now generates a CycloneDX SBOM from `requirements.lock` +and uploads it. Verified locally with the real tool rather than assumed: the +first flag spelling I wrote (`--output-format` / `--outfile`) does not exist in +`cyclonedx-py` and would have failed in CI. The working form is +`--of json --output-reproducible -o `, which emits CycloneDX 1.6 with 96 +components and records the crypto chain at exact versions — `cryptography@46.0.5`, +`bip-utils@2.9.3`, `coincurve@21.0.0`, `pynacl@1.6.2`, `argon2-cffi@25.1.0`, +`bcrypt@4.3.0`, `pycryptodome@3.23.0`. + +The original note follows. + +### L5 (original) — No SBOM `dependency-audit.yml` and `release-integrity.yml` cover part of §11/§12, but no workflow emits a dependency manifest artifact. Add CycloneDX generation diff --git a/torch/src/relay-health.mjs b/torch/src/relay-health.mjs index 3e4f921f..37e0ba0b 100644 --- a/torch/src/relay-health.mjs +++ b/torch/src/relay-health.mjs @@ -1,3 +1,4 @@ +import { randomBytes } from 'node:crypto'; import fs from 'node:fs/promises'; import path from 'node:path'; import WebSocket from 'ws'; @@ -92,8 +93,11 @@ async function probePublishRead(relayUrl, namespace, timeoutMs) { const sk = generateSecretKey(); const pk = getPublicKey(sk); const createdAt = Math.floor(Date.now() / MS_PER_SECOND); - const nonce = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`; - const probeTag = `${namespace}-health-probe-${nonce}`; + // Named "suffix", not "nonce" (entropy audit L4): this is a public probe tag + // used to correlate one publish with its readback, not a cryptographic nonce. + // The old name invited someone to reuse this pattern where it would matter. + const probeSuffix = randomBytes(6).toString('hex'); + const probeTag = `${namespace}-health-probe-${probeSuffix}`; const event = finalizeEvent({ kind: PROBE_KIND, created_at: createdAt, @@ -101,10 +105,10 @@ async function probePublishRead(relayUrl, namespace, timeoutMs) { ['t', `${namespace}-relay-health`], ['d', probeTag], ], - content: JSON.stringify({ type: 'relay_health_probe', nonce, namespace, pubkey: pk }), + content: JSON.stringify({ type: 'relay_health_probe', probeSuffix, namespace, pubkey: pk }), }, sk); - const subId = `probe-${nonce}`; + const subId = `probe-${probeSuffix}`; return withTimeout(new Promise((resolve, reject) => { let okAck = false; diff --git a/torch/src/services/memory/index.js b/torch/src/services/memory/index.js index 46b42822..f09afbec 100644 --- a/torch/src/services/memory/index.js +++ b/torch/src/services/memory/index.js @@ -1,3 +1,5 @@ +import { randomBytes } from 'node:crypto'; + import { createMemoryCache } from './cache.js'; import { ingestMemoryWindow } from './ingestor.js'; import { listPruneCandidates, selectPrunableMemories } from './pruner.js'; @@ -57,7 +59,10 @@ async function performSave(store) { const dir = path.dirname(MEMORY_FILE_PATH); await fs.promises.mkdir(dir, { recursive: true }); const entries = [...store.entries()]; - const tmpPath = `${MEMORY_FILE_PATH}.${Date.now()}.${Math.random()}.tmp`; + // Unguessable temp name (entropy audit L4). Date.now()+Math.random() is + // predictable enough to pre-create or symlink from a shared directory + // between this line and the rename below. + const tmpPath = `${MEMORY_FILE_PATH}.${randomBytes(8).toString('hex')}.tmp`; await fs.promises.writeFile(tmpPath, JSON.stringify(entries, null, 2), 'utf8'); await fs.promises.rename(tmpPath, MEMORY_FILE_PATH); lastSaveTime = Date.now();