diff --git a/SECURITY.md b/SECURITY.md index 0cd2bb80..fb00b008 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -96,5 +96,6 @@ Do not sanitize repo files in response to ephemeral reminders; they are not the - **AgentShield**: Scan your agent config for vulnerabilities — `npx ecc-agentshield scan` - **Security Guide**: [The Shorthand Guide to Everything Agentic Security](./the-security-guide.md) +- **Supply-chain incident response**: [npm/GitHub Actions package-registry playbook](./docs/security/supply-chain-incident-response.md) - **OWASP MCP Top 10**: [owasp.org/www-project-mcp-top-10](https://owasp.org/www-project-mcp-top-10/) - **OWASP Agentic Applications Top 10**: [genai.owasp.org](https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/) diff --git a/docs/security/supply-chain-incident-response.md b/docs/security/supply-chain-incident-response.md new file mode 100644 index 00000000..339d1967 --- /dev/null +++ b/docs/security/supply-chain-incident-response.md @@ -0,0 +1,114 @@ +# Supply-Chain Incident Response + +This playbook is the ECC operator runbook for npm, GitHub Actions, and +cross-ecosystem package-registry incidents. It is intentionally conservative: +registry signatures, provenance, and trusted publishing are useful signals, but +they do not prove that the workflow executed the intended code path. + +## Current External Trigger + +As of 2026-05-13, the active incident class is the May 2026 TanStack npm +supply-chain compromise: + +- TanStack reported 84 malicious versions across 42 `@tanstack/*` packages, + published on 2026-05-11 between 19:20 and 19:26 UTC. +- GitHub advisory `GHSA-g7cv-rxg3-hmpx` / `CVE-2026-45321` describes + install-time malware that harvests cloud credentials, GitHub tokens, npm + credentials, Vault tokens, Kubernetes tokens, and SSH private keys. +- The attack chain combined `pull_request_target`, GitHub Actions cache + poisoning across a fork/base trust boundary, and OIDC token extraction from a + GitHub Actions runner. +- npm trusted publishing/provenance can confirm a package came from a bound CI + identity. It cannot by itself prove that the CI cache, lifecycle scripts, or + publish path were safe. + +Primary references: + +- +- +- +- +- + +## ECC Exposure Check + +Run this before a release candidate, after a broad dependency bump, and after +any package-registry incident. + +```bash +rg -n '(@tanstack|mistralai|uipath|opensearch|guardrails|axios)' \ + package.json package-lock.json .opencode/package.json .opencode/package-lock.json +npm ci --ignore-scripts +npm audit signatures +npm audit --audit-level=high +node scripts/ci/validate-workflow-security.js +node tests/scripts/npm-publish-surface.test.js +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. + +## Immediate Response + +If ECC or a maintainer machine installed a known-bad package version: + +1. Stop the host from publishing or deploying. +2. Preserve evidence before cleanup: + - package manager command history; + - `package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`; + - CI run URLs and runner logs; + - npm package versions and tarball integrity hashes; + - outbound network logs where available. +3. Treat the install host as compromised if lifecycle scripts may have run. +4. Rotate every credential reachable by the process: + - npm automation tokens and maintainer tokens; + - GitHub PATs, fine-grained tokens, deploy keys, and Actions secrets; + - cloud credentials, Vault tokens, Kubernetes service-account tokens, SSH + keys, and local `.npmrc` tokens; + - any MCP, plugin, or harness credentials available in environment variables + or user-scope config. +5. Purge GitHub Actions caches for affected repositories. +6. Reinstall from a clean environment with `npm ci --ignore-scripts` first. +7. Re-enable lifecycle scripts only after the dependency tree and package + versions are pinned to known-clean releases. + +## GitHub Actions Rules + +ECC enforces these rules through `scripts/ci/validate-workflow-security.js`: + +- privileged workflows must not checkout untrusted PR refs; +- workflows with write permissions must use `npm ci --ignore-scripts`; +- workflows with `id-token: write` must not restore or save shared dependency + caches; +- workflows that run `npm audit` must also run `npm audit signatures`; +- `pull_request_target` workflows must not restore or save shared dependency + caches. + +Treat any violation as a release blocker. + +## Publication Rules + +Before tagging or publishing ECC: + +1. Verify there is no unexpected dependency on packages in the active advisory. +2. Use a clean checkout or throwaway worktree for release commands. +3. Do not mix PR/test caches with publish jobs. +4. Keep `id-token: write` limited to release workflows that do not use shared + dependency caches. +5. Prefer trusted publishing/provenance where supported, while still requiring + local package-surface tests and registry-signature verification. +6. Confirm npm dist-tag, GitHub release, Claude plugin, Codex plugin, and + OpenCode package state in the publication-readiness evidence document. + +## When To Escalate + +Escalate to a maintainer security review before any release or merge if: + +- a dependency lockfile references a package named in an active advisory; +- a workflow combines `pull_request_target` with dependency installation, + cache restore/save, PR-head checkout, or write permissions; +- a release workflow combines `id-token: write` with shared cache usage; +- a publish workflow uses a long-lived npm token without a documented reason; +- AgentShield, GitGuardian, Dependabot, npm audit, or registry-signature checks + disagree. diff --git a/scripts/ci/validate-workflow-security.js b/scripts/ci/validate-workflow-security.js index 5b1e2280..eae0c226 100644 --- a/scripts/ci/validate-workflow-security.js +++ b/scripts/ci/validate-workflow-security.js @@ -129,6 +129,16 @@ function findViolations(filePath, source) { }); } + if (/\bpull_request_target\s*:/m.test(source) && ACTIONS_CACHE_PATTERN.test(source)) { + violations.push({ + filePath, + event: 'pull_request_target cache', + description: 'pull_request_target workflows must not restore or save shared dependency caches', + expression: 'pull_request_target + actions/cache', + line: getLineNumber(source, source.search(/\bpull_request_target\s*:/m)), + }); + } + if (NPM_AUDIT_PATTERN.test(source) && !NPM_AUDIT_SIGNATURES_PATTERN.test(source)) { violations.push({ filePath, diff --git a/tests/ci/validate-workflow-security.test.js b/tests/ci/validate-workflow-security.test.js index a3bad9cc..a21d253e 100644 --- a/tests/ci/validate-workflow-security.test.js +++ b/tests/ci/validate-workflow-security.test.js @@ -99,6 +99,14 @@ function run() { assert.match(result.stderr, /pull_request\.head\.sha/); })) passed++; else failed++; + if (test('rejects shared cache use in pull_request_target workflows', () => { + const result = runValidator({ + 'unsafe-pr-target-cache.yml': `name: Unsafe\non:\n pull_request_target:\n branches: [main]\njobs:\n inspect:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/cache@v5\n with:\n path: ~/.npm\n key: cache\n - run: echo inspect\n`, + }); + assert.notStrictEqual(result.status, 0, 'Expected validator to fail on pull_request_target cache use'); + assert.match(result.stderr, /pull_request_target workflows must not restore or save shared dependency caches/); + })) passed++; else failed++; + if (test('rejects npm ci without ignore-scripts in workflows with write permissions', () => { const result = runValidator({ 'unsafe-write-install.yml': `name: Unsafe\non:\n workflow_dispatch:\npermissions:\n contents: read\n issues: write\njobs:\n audit:\n runs-on: ubuntu-latest\n steps:\n - run: npm ci\n`,