aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
178 lines (129 loc) • 9.48 kB
Markdown
# Dependency Source Policy
**Enforcement Level**: HIGH
**Scope**: `package.json` and lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`) — direct AND transitive entries
**Issue**: #1297
## Principle
Non-registry dependency sources (`git+`, `github:`, raw tarball URLs, `file:`, `link:`) bypass registry signature verification AND can execute arbitrary code during install via lifecycle scripts (especially `prepare`). The May 2026 Mini Shai-Hulud campaign's primary propagation vector was an `optionalDependencies` entry sourced from `git+https://...attacker-repo` whose `prepare` script ran during every `npm install` of any project that pulled in the affected package.
Registry-hosted tarballs (`registry.npmjs.org/.../foo.tgz`, `registry.yarnpkg.com/...`, `npm.pkg.github.com/...`) are fine — they're the normal `resolved` URL format `npm install` emits for every registry-published dep. The policy only flags non-registry sources.
Companion to [`ci-action-pinning`](ci-action-pinning.md) (CI execution-environment trust) and the per-package-manager release-age-gate skills (which gate freshly-published registry versions). Together they cover the three primary supply-chain attack vectors: CI environment poisoning, registry-version poisoning, and direct dep-source injection.
## Mandatory Rules
### Rule 1: Six dep-source patterns are forbidden by default
Every dep in `package.json` AND every entry in the lockfile (including transitive) MUST be a registry-published version reference. The following patterns are blocked:
| Pattern | Example | Reason |
|---|---|---|
| `git+*` scheme | `"foo": "git+https://github.com/owner/foo.git"` | npm clones the repo and runs its `prepare` script — arbitrary code execution at install time |
| `git://` scheme | `"foo": "git://github.com/owner/foo.git"` | Same as above |
| `github:` shorthand | `"foo": "github:owner/foo"` | Same as above; npm expands to git+ |
| Non-registry tarball | `"foo": "https://example.com/foo-1.0.0.tgz"` | Tarball can contain any payload and lifecycle scripts; bypasses registry signature verification |
| `file:` path | `"foo": "file:./vendor/foo"` | Local-path deps bypass dep-resolution review and lockfile signature checks |
| `link:` symlink | `"foo": "link:./packages/foo"` | Follows the symlink target wherever it points; same gaps as `file:` |
Registry-hosted patterns that are ALWAYS acceptable:
- `"foo": "^1.2.3"` — registry semver range
- `"foo": "1.2.3"` — exact registry version
- `"foo": "npm:@scope/foo@^1.2.3"` — aliased registry dep
- Lockfile `"resolved": "https://registry.npmjs.org/..."` — registry-served tarball
### Rule 2: Exceptions require an allowlist entry
If a legitimate non-registry source exists (e.g., temporary fork pending upstream merge, monorepo-internal `file:` link with a known security boundary), it MUST be allowlisted with:
- **Owner** — who took the risk-acceptance decision
- **Reason** — why a registry version isn't viable today
- **Expiry / review date** — when this exception must be re-evaluated (max 1 year)
- **Risk acceptance** — explicit statement that the operator accepts the dep-source bypass
Allowlist file format (committed at `.aiwg/security/dep-source-allowlist.yaml` or equivalent):
```yaml
allowlist:
- dep: "foo"
source: "git+https://github.com/owner/foo.git#sha-or-tag"
owner: "alice@example.org"
reason: "Upstream PR #123 not yet merged; we need fix from main for issue X"
review_date: "2026-08-01"
risk_acceptance: "Source pinned to commit SHA; reviewed prepare script line-by-line; no shell exec in install path"
- dep: "@internal/shared"
source: "file:./packages/shared"
owner: "bob@example.org"
reason: "Monorepo-internal package; not published to registry"
review_date: "2027-01-01"
risk_acceptance: "Source within same security boundary as host package; no external code path"
```
Allowlist entries beyond their `review_date` are treated as policy violations.
### Rule 3: Lockfile entries are policy targets
Direct deps in `package.json` are easy to spot. Transitive entries in the lockfile are the actual attack surface — Mini Shai-Hulud propagated via transitive `optionalDependencies` with exotic sources. The CI lint MUST scan the lockfile for the same patterns, including:
- `"resolved": "git+..."` in `package-lock.json`
- `tarball: ...` URLs in `pnpm-lock.yaml` that don't match a registry origin
- `resolution: ` blocks in `yarn.lock`
- Binary lockfile (`bun.lockb`) — extract via `bun pm ls --all` and pattern-match the output
### Rule 4: pnpm workspaces enable `blockExoticSubdeps`
When pnpm is the package manager, the workspace MUST set `blockExoticSubdeps: true` (in `pnpm-workspace.yaml`) or the per-repo equivalent in `.npmrc`:
```yaml
# pnpm-workspace.yaml
packages:
- 'packages/*'
blockExoticSubdeps: true
```
This is the pnpm advantage over npm: workspace-scope enforcement that npm has no equivalent for. The `pnpm-release-age-gate` skill documents the wiring.
### Rule 5: Failure messages point to remediation
When CI rejects a violation, the failure message MUST:
1. State which dep and which source pattern matched
2. Reference the operator's choices: switch to a registry version, request an allowlist entry, or accept install-time compromise risk
3. Link to this rule (or the project's adaptation of it) so the receiving developer can read the policy
Reference failure-message format (from AIWG's own `tools/lint/dep-source.mjs`):
```
✗ Dependency source policy violation
foo (in package.json)
source: git+https://github.com/owner/foo.git
pattern: git+ scheme
This dependency source bypasses registry signature verification and
executes arbitrary code at install time via the prepare script.
Options:
1. Switch to a registry-published version: npm install foo@<version>
2. Add an allowlist entry: see .aiwg/security/dep-source-allowlist.yaml
3. Read the policy: docs/contributing/dependency-sources.md
```
## Detection Patterns
```bash
# package.json — direct deps with exotic sources
node -p "
const p = require('./package.json');
const all = {...p.dependencies, ...p.devDependencies, ...p.optionalDependencies, ...p.peerDependencies};
for (const [name, source] of Object.entries(all || {})) {
if (/^(git\+|git:|github:|file:|link:)/.test(source) ||
(/^https?:\/\//.test(source) && /\.tgz/.test(source))) {
console.log(name, '=', source);
}
}
"
# package-lock.json — transitive entries with exotic resolved URLs
node -p "
const lock = require('./package-lock.json');
function walk(pkgs, prefix = '') {
for (const [path, entry] of Object.entries(pkgs || {})) {
if (entry.resolved && /^(git\+|git:)/.test(entry.resolved)) {
console.log(prefix + path, '=', entry.resolved);
}
}
}
walk(lock.packages);
"
# pnpm-lock.yaml — exotic resolution blocks
grep -E 'resolution:\s*\{(git|tarball:.*[^.]https?://[^/]+/[^.]*\.tgz)' pnpm-lock.yaml 2>/dev/null
# yarn.lock — exotic resolved entries
grep -E '"?resolved"?\s*"?:?\s*"?(git\+|git:)' yarn.lock 2>/dev/null
```
Reference implementation: AIWG's own [`tools/lint/dep-source.mjs`](https://git.integrolabs.net/roctinam/aiwg/src/branch/main/tools/lint/dep-source.mjs) — runs `npm run lint:dep-sources` and exits non-zero on any violation.
## Acceptable Exceptions
Three exception categories beyond explicit allowlist entries:
1. **Monorepo workspace links** — `file:` or `link:` deps between packages in the same `pnpm-workspace.yaml` / `lerna.json` / `nx.json` / `package.json workspaces` configuration. These are within the same security boundary; flag as INFO only.
2. **Lockfile resolved-URL prefixes that look exotic but ARE registry URLs** — e.g., `https://registry.npmjs.org/...` matches `^https://` but is a registry origin. The detection logic above already excludes these correctly by checking the host.
3. **Dev-only deps for build tooling that consume only `package.json`-declared scripts** — debatable; recommend treating as standard policy violations and using allowlist entries for the temporary cases.
## Rationale
Mini Shai-Hulud demonstrated that a single transitive `optionalDependencies` entry from a compromised maintainer can propagate a `prepare`-script payload to every downstream `npm install`. The registry's "this version was published by an authenticated maintainer" guarantee is the foundation of `npm audit signatures`; non-registry sources bypass that guarantee entirely.
Cost of compliance is low (most deps are already registry-published). Cost of a single exotic-source incident is high (full secret-rotation cycle, downstream user trust loss, potential CVE assignment). The asymmetry justifies the rule.
## References
- [`ci-action-pinning`](ci-action-pinning.md) — CI execution-environment trust (companion rule)
- AIWG's lint implementation: [`tools/lint/dep-source.mjs`](https://git.integrolabs.net/roctinam/aiwg/src/branch/main/tools/lint/dep-source.mjs)
- AIWG's contributor doc: [`docs/contributing/dependency-sources.md`](https://git.integrolabs.net/roctinam/aiwg/src/branch/main/docs/contributing/dependency-sources.md)
- ADR: `.aiwg/architecture/adr-dep-source-policy.md` (in AIWG's own repo)
- pnpm `blockExoticSubdeps`: <https://pnpm.io/settings#blockexoticsubdeps>
- Mini Shai-Hulud post-mortem references
---
**Rule Status**: ACTIVE
**Last Updated**: 2026-05-13