mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
0181fb85c2
After ADR-122 + ADR-123 expanded ruflo's published-package surface
(@claude-flow/browser, ruflo-graph-intelligence) the supply-chain attack
surface grew alongside it. This commit puts five layers of defence on
every PR + push that touches package.json, lockfiles, or the audit
itself.
New files:
.github/supply-chain/allowed-deps.json
Whitelist of direct deps per audited package (ruflo-graph-intelligence,
@claude-flow/browser). New top-level deps require an explicit edit here.
Also lists policies.rejectInRegistry (typosquat block-list — ruflo-graph-
intellligence, sublinear-time-solvor, agent-browse, etc.) and
publisherTrust.criticalUpstreamPackages (upstream identities snapshotted
in CI logs so maintainer takeovers are visible).
.github/supply-chain/accepted-findings.json
Triaged findings the audit currently tolerates. Each entry cites a
tracking issue or ADR and carries an expiresAt date so the audit
re-fails after triage window closes. CODEOWNER review required to
edit.
.github/supply-chain/README.md
Operator guide: how to run the audit locally, how to triage new
findings, how to add a new package to coverage.
.github/CODEOWNERS
Workflows / lockfiles / plugin manifests / supply-chain config now
require explicit maintainer review. Pairs with the automated audit.
scripts/audit-supply-chain.mjs
Five-layer audit:
[1/5] CVE — npm audit --audit-level=high, fail on HIGH/CRITICAL in
DIRECT deps unless accepted
[2/5] Lockfile integrity — every downloaded entry must carry
SHA-512 in `integrity` (skip pnpm-shadow + workspace-link
entries by construction)
[3/5] Top-level allowlist — package.json deps must match
allowed-deps.json
[4/5] Typosquat reject — names matching policies.rejectInRegistry
fail regardless of upstream status
[5/5] Publisher trust — npm view maintainers, log-only (informational)
scripts/__tests__/audit-supply-chain.test.mjs
Smoke test for the audit itself (full-run / --json / each --scope).
Workflow additions in .github/workflows/v3-ci.yml:
supply-chain-audit job — runs the audit + the smoke test on every PR
and push that touches package.json, lockfile, allowed-deps,
accepted-findings, or the audit script.
dependency-review job — actions/dependency-review-action@v4 (PR-only)
flags any new vulnerable dep added by the PR vs base, fails on
`high` severity, denies known-bad licenses (AGPL/GPL/SSPL) and
blocks specific compromised versions (event-stream, flatmap-stream,
ua-parser-js@0.7.29 / 0.8.0 / 1.0.0, colors@1.4.1, faker@6.6.6).
Path filter expanded to include **/package.json, **/package-lock.json,
**/pnpm-lock.yaml, .github/supply-chain/**, scripts/audit-supply-chain.mjs
Dependabot additions in .github/dependabot.yml:
Per-plugin npm stanzas for ruflo-graph-intelligence and
@claude-flow/browser. Major-version bumps are blocked from auto-PR
(they require allowlist re-approval); patch + minor auto-PR'd.
One real-world finding the audit caught on first run + how it was
triaged (recorded in accepted-findings.json):
- HIGH CVE on @claude-flow/browser → agentic-flow (via @xenova/
transformers, agentdb). Already tracked in ADR-121 Phase 4
(Xenova retirement migration). Marked accepted with expiresAt
2026-08-19 so the audit re-fails if the migration hasn't
landed by then.
Audit run on this branch: PASS (0 unaccepted CVE / 0 lockfile issue
/ 0 allowlist violation / 0 typosquat / 5 publisher-trust entries
logged). Audit smoke test: 5/5 passing.
Co-Authored-By: RuFlo <ruv@ruv.net>
90 lines
2.7 KiB
JavaScript
90 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Smoke test for scripts/audit-supply-chain.mjs.
|
|
*
|
|
* Runs the audit and confirms:
|
|
* - It exits 0 (current accepted-findings keep the bar)
|
|
* - The five layers all execute
|
|
* - JSON output is parseable
|
|
*
|
|
* Run via: node scripts/__tests__/audit-supply-chain.test.mjs
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import assert from 'node:assert/strict';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO_ROOT = join(__dirname, '..', '..');
|
|
const SCRIPT = join(REPO_ROOT, 'scripts', 'audit-supply-chain.mjs');
|
|
|
|
function run(args = []) {
|
|
try {
|
|
return {
|
|
stdout: execFileSync('node', [SCRIPT, ...args], { encoding: 'utf8', cwd: REPO_ROOT }),
|
|
code: 0,
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
stdout: err.stdout?.toString() ?? '',
|
|
stderr: err.stderr?.toString() ?? '',
|
|
code: err.status ?? 1,
|
|
};
|
|
}
|
|
}
|
|
|
|
console.log('test: full audit returns 0 on current accepted state');
|
|
{
|
|
const r = run();
|
|
assert.equal(r.code, 0, `expected exit 0, got ${r.code}. stdout:\n${r.stdout}`);
|
|
assert.match(r.stdout, /\[1\/5\] CVE audit/);
|
|
assert.match(r.stdout, /\[2\/5\] Lockfile integrity/);
|
|
assert.match(r.stdout, /\[3\/5\] Top-level allowlist/);
|
|
assert.match(r.stdout, /\[4\/5\] Typosquat reject/);
|
|
assert.match(r.stdout, /\[5\/5\] Publisher trust snapshot/);
|
|
assert.match(r.stdout, /OK: no hard-fail findings\./);
|
|
console.log(' pass');
|
|
}
|
|
|
|
console.log('test: --json output is parseable');
|
|
{
|
|
const r = run(['--json']);
|
|
assert.equal(r.code, 0, `expected exit 0, got ${r.code}`);
|
|
const parsed = JSON.parse(r.stdout);
|
|
assert.ok(Array.isArray(parsed.cve));
|
|
assert.ok(Array.isArray(parsed.lockfile));
|
|
assert.ok(Array.isArray(parsed.allowlist));
|
|
assert.ok(Array.isArray(parsed.typosquat));
|
|
assert.ok(Array.isArray(parsed.publisherTrust));
|
|
console.log(' pass');
|
|
}
|
|
|
|
console.log('test: --scope cve runs only the CVE pass');
|
|
{
|
|
const r = run(['--scope', 'cve']);
|
|
assert.equal(r.code, 0);
|
|
assert.match(r.stdout, /\[1\/5\] CVE audit/);
|
|
assert.doesNotMatch(r.stdout, /\[2\/5\] Lockfile integrity/);
|
|
console.log(' pass');
|
|
}
|
|
|
|
console.log('test: --scope allowlist runs only the allowlist pass');
|
|
{
|
|
const r = run(['--scope', 'allowlist']);
|
|
assert.equal(r.code, 0);
|
|
assert.match(r.stdout, /\[3\/5\] Top-level allowlist/);
|
|
assert.doesNotMatch(r.stdout, /\[1\/5\] CVE audit/);
|
|
console.log(' pass');
|
|
}
|
|
|
|
console.log('test: --scope typosquat runs only the typosquat pass');
|
|
{
|
|
const r = run(['--scope', 'typosquat']);
|
|
assert.equal(r.code, 0);
|
|
assert.match(r.stdout, /\[4\/5\] Typosquat reject/);
|
|
console.log(' pass');
|
|
}
|
|
|
|
console.log('\nall supply-chain audit tests passed');
|