diff --git a/.github/workflows/supply-chain-watch.yml b/.github/workflows/supply-chain-watch.yml new file mode 100644 index 00000000..0bf238f1 --- /dev/null +++ b/.github/workflows/supply-chain-watch.yml @@ -0,0 +1,57 @@ +name: Supply-Chain Watch + +on: + schedule: + - cron: '17 */6 * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + ioc-watch: + name: IOC watch + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: '20.x' + + - name: Install dependencies without lifecycle scripts + run: npm ci --ignore-scripts + + - name: Verify registry signatures and advisories + run: | + npm audit signatures + npm audit --audit-level=high + + - name: Validate IOC scanner fixtures + run: node tests/ci/scan-supply-chain-iocs.test.js + + - name: Generate IOC report + run: | + mkdir -p artifacts + node scripts/ci/scan-supply-chain-iocs.js --json > artifacts/supply-chain-ioc-report.json + + - name: Validate workflow hardening rules + run: node scripts/ci/validate-workflow-security.js + + - name: Upload IOC report + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: supply-chain-ioc-report + path: artifacts/supply-chain-ioc-report.json + retention-days: 14 diff --git a/docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-15.md b/docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-15.md index 2442b1e2..c1485589 100644 --- a/docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-15.md +++ b/docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-15.md @@ -13,7 +13,7 @@ clean checkout. | Issue queue | Current | 0 open issues across checked repos | | Discussions | Current | 58 main-repo discussions; 0 need maintainer touch; 0 answerable discussions missing accepted answers | | Local worktree | Current with caveat | `main...origin/main`; unrelated `?? docs/drafts/` ignored | -| Security sweep | Current with follow-up | IOC scan, audits, and package-manager hardening completed | +| Security sweep | Current with follow-up | IOC scan, audits, package-manager hardening, and scheduled watch workflow completed | | Linear roadmap | Current with follow-up | `ECC Platform Roadmap`, ITO-44 through ITO-59 | | ECC 2.0 publication | Not complete | Release, npm, plugin, and announcement gates pending | | AgentShield enterprise depth | In progress | AgentShield #86 merged; live IOC loop still pending | @@ -29,7 +29,7 @@ Run these from `everything-claude-code` unless a row says otherwise. | Platform audit | `node scripts/platform-audit.js --json --allow-untracked docs/drafts/` | `ready: true`; open PRs 0/20; open issues 0/20; discussions needing maintainer touch 0; answerable discussions missing accepted answers 0; blocking dirty files 0 | | Discussion audit | `node scripts/discussion-audit.js --json --repo affaan-m/everything-claude-code` | `ready: true`; 58 discussions sampled; 0 need maintainer touch; 0 answerable discussions missing accepted answers | | Main repo status | `git status --short --branch` | `## main...origin/main`; `?? docs/drafts/` remains unrelated | -| Main commit | `git rev-parse HEAD` | `c0f8c3bc813360f29e9f2b66bcae7e977cd03327` | +| Main commit | `git rev-parse HEAD` | `6887f2952d193cff10b3eb79af7765555d8ca9f5` | | Main repo PRs/issues | GitHub connector and `gh` readback | 0 open PRs; 0 open issues | | AgentShield PRs/issues | GitHub connector and `gh` readback | 0 open PRs; 0 open issues | | ECC Tools PRs/issues | Local `gh pr list` and `gh issue list` | 0 open PRs; 0 open issues | @@ -37,7 +37,8 @@ Run these from `everything-claude-code` unless a row says otherwise. | Supply-chain IOC scan | `node scripts/ci/scan-supply-chain-iocs.js --root --home` | Passed; 1241 files inspected | | IOC unit tests | `node tests/ci/scan-supply-chain-iocs.test.js` | 15/15 passed | | Dead-man switch persistence sweep | Process, LaunchAgent, and known payload filename sweep for Mini Shai-Hulud markers | No matches | -| Workflow security gate | `node scripts/ci/validate-workflow-security.js` | Passed; 7 workflow files inspected | +| Workflow security gate | `node scripts/ci/validate-workflow-security.js` | Passed; 8 workflow files inspected | +| Supply-chain watch workflow | `.github/workflows/supply-chain-watch.yml` | Scheduled every 6 hours; emits `supply-chain-ioc-report.json` | | npm signatures and audit | `npm audit signatures && npm audit --audit-level=moderate` in main, AgentShield, ECC Tools | 0 vulnerabilities in each checked package | ## Prompt-To-Artifact Checklist @@ -94,9 +95,9 @@ Still-open lanes: should not spend more time closing nonexistent PRs/issues. - The discussion queue is current and repeatable through `discussion:audit`. ITO-59 remains open only for recurring Linear/status synchronization. -- The Mini Shai-Hulud/TanStack protection pass is strong enough for current - local protection, but ITO-57 remains open until incident response and IOC - updates become a durable workflow. +- The Mini Shai-Hulud/TanStack protection pass now has a durable scheduled + watch workflow. ITO-57 remains open for advisory-source refresh automation + and Linear status synchronization. - The release is still blocked by publication, package, plugin, billing, and announcement gates. Passing `platform:audit` alone is not proof that ECC 2.0 is publishable. @@ -107,7 +108,7 @@ Still-open lanes: markdown artifact. 2. Run `platform:audit` and `discussion:audit` from the final release commit before recording publication evidence. -3. Continue ITO-57 by turning emergency hardening into documented incident - response and scanner update workflow. +3. Continue ITO-57 by adding advisory-source refresh automation and Linear + status synchronization for the scheduled supply-chain watch. 4. Resume release/publication lanes ITO-45, ITO-46, and ITO-56 only after the readiness dashboard can be refreshed from commands. diff --git a/docs/security/supply-chain-incident-response.md b/docs/security/supply-chain-incident-response.md index efab8231..3d2aad6f 100644 --- a/docs/security/supply-chain-incident-response.md +++ b/docs/security/supply-chain-incident-response.md @@ -81,6 +81,21 @@ node tests/run-all.js If a search hit appears only in documentation examples, note it in the release evidence but do not rotate credentials for a docs-only reference. +## Durable Watch Workflow + +ECC also runs `.github/workflows/supply-chain-watch.yml` every six hours and on +manual dispatch. The workflow is read-only, disables checkout credential +persistence, installs with `npm ci --ignore-scripts`, verifies npm registry +signatures, runs the IOC scanner fixtures, emits +`supply-chain-ioc-report.json`, and re-validates GitHub Actions hardening rules. + +Treat a failed scheduled watch as a release blocker until an operator confirms +whether the failure is a newly reported advisory, a stale scanner fixture, a +registry-signature issue, or a workflow hardening regression. If the scanner +needs new indicators, update `scripts/ci/scan-supply-chain-iocs.js`, add fixture +coverage in `tests/ci/scan-supply-chain-iocs.test.js`, refresh this runbook, and +attach the latest JSON artifact to the release evidence. + ## Immediate Response If ECC or a maintainer machine installed a known-bad package version: diff --git a/tests/ci/supply-chain-watch-workflow.test.js b/tests/ci/supply-chain-watch-workflow.test.js new file mode 100644 index 00000000..f4946ff6 --- /dev/null +++ b/tests/ci/supply-chain-watch-workflow.test.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/** + * Validate the scheduled supply-chain watch workflow contract. + */ + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const WORKFLOW_PATH = path.join( + __dirname, + '..', + '..', + '.github', + 'workflows', + 'supply-chain-watch.yml', +); + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + return true; + } catch (error) { + console.log(` ✗ ${name}`); + console.log(` Error: ${error.message}`); + return false; + } +} + +function run() { + console.log('\n=== Testing supply-chain watch workflow ===\n'); + + const source = fs.readFileSync(WORKFLOW_PATH, 'utf8'); + let passed = 0; + let failed = 0; + + if (test('runs on schedule and manual dispatch', () => { + assert.match(source, /schedule:\r?\n\s+- cron: '17 \*\/6 \* \* \*'/); + assert.match(source, /workflow_dispatch:/); + })) passed++; else failed++; + + if (test('uses read-only permissions and non-persisting checkout credentials', () => { + assert.match(source, /permissions:\r?\n\s+contents: read/); + assert.doesNotMatch(source, /^\s+[A-Za-z-]+:\s*write\b/m); + assert.match(source, /uses: actions\/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd/); + assert.match(source, /persist-credentials: false/); + assert.doesNotMatch(source, /id-token:\s*write/); + assert.doesNotMatch(source, /actions\/cache@/); + })) passed++; else failed++; + + if (test('installs without lifecycle scripts and verifies registry signatures', () => { + assert.match(source, /npm ci --ignore-scripts/); + assert.match(source, /npm audit signatures/); + assert.match(source, /npm audit --audit-level=high/); + })) passed++; else failed++; + + if (test('runs IOC fixtures, emits JSON report, and uploads the artifact', () => { + assert.match(source, /node tests\/ci\/scan-supply-chain-iocs\.test\.js/); + assert.match(source, /node scripts\/ci\/scan-supply-chain-iocs\.js --json > artifacts\/supply-chain-ioc-report\.json/); + assert.match(source, /node scripts\/ci\/validate-workflow-security\.js/); + assert.match(source, /uses: actions\/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a/); + assert.match(source, /name: supply-chain-ioc-report/); + assert.match(source, /retention-days: 14/); + })) passed++; else failed++; + + console.log(`\nPassed: ${passed}`); + console.log(`Failed: ${failed}`); + + process.exit(failed > 0 ? 1 : 0); +} + +run();