aiwg 2026.5.3 → 2026.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -1
- package/agentic/code/frameworks/security-engineering/manifest.json +2 -2
- package/agentic/code/frameworks/security-engineering/rules/RULES-INDEX.md +19 -4
- package/agentic/code/frameworks/security-engineering/rules/ci-action-pinning.md +145 -0
- package/agentic/code/frameworks/security-engineering/rules/dependency-source-policy.md +177 -0
- package/agentic/code/frameworks/security-engineering/skills/bun-release-age-gate/SKILL.md +193 -0
- package/agentic/code/frameworks/security-engineering/skills/ci-workflow-audit/SKILL.md +266 -0
- package/agentic/code/frameworks/security-engineering/skills/pnpm-release-age-gate/SKILL.md +218 -0
- package/agentic/code/frameworks/security-engineering/skills/security-engineering-quickref/SKILL.md +9 -0
- package/agentic/code/frameworks/security-engineering/skills/yarn-release-age-gate/SKILL.md +183 -0
- package/agentic/code/frameworks/security-engineering/templates/README.md +35 -0
- package/agentic/code/frameworks/security-engineering/templates/security-md-template.md +192 -0
- package/dist/src/mcp/cli.mjs +6 -0
- package/dist/src/mcp/helpers.mjs +232 -0
- package/dist/src/mcp/server.mjs +41 -10
- package/dist/src/mcp/tools/command-run.mjs +101 -0
- package/dist/src/mcp/tools/discovery.mjs +291 -0
- package/dist/src/mcp/tools/subsystems.mjs +609 -0
- package/package.json +1 -1
- package/tools/agents/providers/hermes.mjs +322 -41
- package/tools/verify-hermes-citations.mjs +190 -0
package/CLAUDE.md
CHANGED
|
@@ -87,7 +87,7 @@ All 10 providers receive all 4 artifact types (agents, commands, skills, rules).
|
|
|
87
87
|
| Warp Terminal | `.warp/agents/` + WARP.md | `.warp/commands/` | `.warp/skills/` + `.agents/skills/` | `.warp/rules/` | `aiwg use sdlc --provider warp` |
|
|
88
88
|
| Windsurf | AGENTS.md | `.windsurf/workflows/` | `.windsurf/skills/` + `.agents/skills/` | `.windsurf/rules/` | `aiwg use sdlc --provider windsurf` |
|
|
89
89
|
| OpenClaw | `~/.openclaw/agents/` | `~/.openclaw/commands/` | `~/.openclaw/skills/` + `.agents/skills/` | `~/.openclaw/rules/` | `aiwg use sdlc --provider openclaw` |
|
|
90
|
-
| Hermes | AGENTS.md |
|
|
90
|
+
| Hermes | AGENTS.md + .hermes.md | via MCP `command-run` | `~/.hermes/skills/` (kernel) + `~/.hermes/skills/.aiwg/` (standard) | inlined in AGENTS.md + MCP `rule-list`/`rule-show` | `aiwg use sdlc --provider hermes` |
|
|
91
91
|
|
|
92
92
|
**Special cases:**
|
|
93
93
|
- **Codex**: Commands and skills deploy to home directory (`~/.codex/prompts/`, `~/.codex/skills/`) for user-level availability across all projects. `.codex/commands/` also deploys at project scope for operator visibility, but Codex commands are a static built-in enum in codex-rs and these files are not auto-scanned by the loader; AGENTS.md is the discovery bridge per ADR-1. Reference: `src/smiths/platform-paths.ts:23`.
|
|
@@ -95,6 +95,7 @@ All 10 providers receive all 4 artifact types (agents, commands, skills, rules).
|
|
|
95
95
|
- **Warp**: Agents and commands are also aggregated into `WARP.md` for single-file context loading
|
|
96
96
|
- **Windsurf**: Agents are aggregated into `AGENTS.md` at project root
|
|
97
97
|
- **OpenClaw**: All artifacts deploy to home directory (`~/.openclaw/`). First provider to support behaviors (`~/.openclaw/behaviors/`)
|
|
98
|
+
- **Hermes**: MCP sidecar architecture (`Hermes → MCP → AIWG`). Commands reach AIWG via `mcp_aiwg_command_run` (allow-listed). Standard skills (~385) live under `~/.hermes/skills/.aiwg/` and are recursively discovered; kernel skills (~9) live at the top level and are protected from the Curator (v0.12.0+) via the `.bundled_manifest`. The MCP server has ~12 core tools by default; ~45 additional via `AIWG_MCP_TOOLSETS=<csv>`. See `docs/integrations/hermes-quickstart.md`.
|
|
98
99
|
|
|
99
100
|
## Writing Principles
|
|
100
101
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Security Engineering Rules Index
|
|
2
2
|
|
|
3
|
-
Applied-security enforcement rules for cryptographic primitive choices, chain-of-trust integrity, secret handling, and related design-time concerns. Deployed when the `security-engineering` framework is installed.
|
|
3
|
+
Applied-security enforcement rules for cryptographic primitive choices, chain-of-trust integrity, secret handling, supply-chain pinning, dependency-source policy, and related design-time concerns. Deployed when the `security-engineering` framework is installed.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## Tier 1 Rules (
|
|
7
|
+
## Tier 1 Rules (6 rules — applied cryptography + supply chain)
|
|
8
8
|
|
|
9
9
|
### HIGH
|
|
10
10
|
|
|
@@ -32,6 +32,18 @@ Applied-security enforcement rules for cryptographic primitive choices, chain-of
|
|
|
32
32
|
**Maps to review finding**: H6
|
|
33
33
|
**Full rule**: @$AIWG_ROOT/agentic/code/frameworks/security-engineering/rules/crypto-flag-verification.md
|
|
34
34
|
|
|
35
|
+
#### ci-action-pinning
|
|
36
|
+
**Summary**: Every CI workflow `uses:` reference MUST be a 40-character commit SHA (not a tag); every `container:`/`image:` reference MUST be `<name>:<tag>@sha256:<digest>`. Tools downloaded via `curl | sh` must record an observed-SHA log and support strict-mode SHA enforcement. Floating tags expose CI to silent supply-chain attacks (Shai-Hulud-class worm propagation). Maintain a pin manifest (`ci/digests.txt` or equivalent) as source of truth for diffs.
|
|
37
|
+
**When to apply**: Any workflow file under `.github/workflows/`, `.gitea/workflows/`, or equivalent; any tool-install step in CI
|
|
38
|
+
**Maps to issue**: #1293 (B3 / Mini Shai-Hulud)
|
|
39
|
+
**Full rule**: @$AIWG_ROOT/agentic/code/frameworks/security-engineering/rules/ci-action-pinning.md
|
|
40
|
+
|
|
41
|
+
#### dependency-source-policy
|
|
42
|
+
**Summary**: Non-registry dependency sources (`git+`, `github:`, raw tarball URLs, `file:`, `link:`) are PROHIBITED — they bypass registry signature verification and can execute arbitrary code at install time via `prepare` scripts (Mini Shai-Hulud's primary propagation vector). Policy applies to `package.json` AND transitive lockfile entries. Exceptions require an allowlist entry with owner, reason, review_date, and explicit risk acceptance. pnpm workspaces must set `blockExoticSubdeps: true` for workspace-scope enforcement.
|
|
43
|
+
**When to apply**: Any change to `package.json`, `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, or `bun.lockb`; CI lint should run on every push
|
|
44
|
+
**Maps to issue**: #1297 (Mini Shai-Hulud follow-up)
|
|
45
|
+
**Full rule**: @$AIWG_ROOT/agentic/code/frameworks/security-engineering/rules/dependency-source-policy.md
|
|
46
|
+
|
|
35
47
|
---
|
|
36
48
|
|
|
37
49
|
## Quick Reference by Context
|
|
@@ -43,7 +55,10 @@ Applied-security enforcement rules for cryptographic primitive choices, chain-of
|
|
|
43
55
|
| **Multi-key systems** | no-key-reuse-across-purposes, no-adhoc-kdf |
|
|
44
56
|
| **Password handling** | no-adhoc-kdf (Argon2id/PBKDF2 ≥600k) |
|
|
45
57
|
| **CLI crypto invocations** | crypto-flag-verification, no-unauthenticated-encryption |
|
|
46
|
-
| **
|
|
58
|
+
| **CI workflow review** | ci-action-pinning |
|
|
59
|
+
| **Container image references** | ci-action-pinning |
|
|
60
|
+
| **package.json / lockfile review** | dependency-source-policy |
|
|
61
|
+
| **Reviewing cryptographic decisions** | All four crypto rules in sequence |
|
|
47
62
|
|
|
48
63
|
---
|
|
49
64
|
|
|
@@ -53,5 +68,5 @@ Future Tier 2 rules will cover authentication-factor architecture, degraded-mode
|
|
|
53
68
|
|
|
54
69
|
---
|
|
55
70
|
|
|
56
|
-
*Generated from security-engineering framework —
|
|
71
|
+
*Generated from security-engineering framework — 6 rules in Tier 1*
|
|
57
72
|
*Full rule files: @$AIWG_ROOT/agentic/code/frameworks/security-engineering/rules/*
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# CI Action and Container Pinning
|
|
2
|
+
|
|
3
|
+
**Enforcement Level**: HIGH
|
|
4
|
+
**Scope**: All CI workflow files (`.github/workflows/`, `.gitea/workflows/`, equivalent) and any `container:` references therein
|
|
5
|
+
**Issue**: #1293
|
|
6
|
+
|
|
7
|
+
## Principle
|
|
8
|
+
|
|
9
|
+
CI workflows execute third-party code with broad permissions on every push. A workflow that references `actions/checkout@v4` or `node:24` resolves the reference at run time — the bytes that execute today are not guaranteed to be the bytes that execute tomorrow. Anyone who can move the `v4` tag (a GitHub Action maintainer compromise, a registry account takeover, a published-but-malicious update) silently runs new code in your CI. Pinning by immutable digest closes this attack surface.
|
|
10
|
+
|
|
11
|
+
This is the same standard AIWG applies to its own CI per [`dev-idempotent-builds.md`](.claude/rules/dev-idempotent-builds.md) rule 2 ("no `:latest` in production"). B3 extends the standard to user projects as a deployable rule.
|
|
12
|
+
|
|
13
|
+
## Mandatory Rules
|
|
14
|
+
|
|
15
|
+
### Rule 1: GitHub Actions references pinned by 40-char commit SHA
|
|
16
|
+
|
|
17
|
+
Every `uses:` reference in a workflow file MUST point at a 40-character commit SHA, not a tag or branch.
|
|
18
|
+
|
|
19
|
+
**FORBIDDEN**:
|
|
20
|
+
```yaml
|
|
21
|
+
- uses: actions/checkout@v4 # tag — can move
|
|
22
|
+
- uses: actions/checkout@main # branch — definitely moves
|
|
23
|
+
- uses: actions/checkout@v4.3.1 # version tag — usually immutable but a compromised account can re-point it
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**REQUIRED**:
|
|
27
|
+
```yaml
|
|
28
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The trailing comment is non-optional. It preserves human-readable diffability without compromising the immutability of the SHA pin.
|
|
32
|
+
|
|
33
|
+
### Rule 2: Container images pinned by sha256 digest
|
|
34
|
+
|
|
35
|
+
Every `container:` or `image:` reference in a workflow MUST be `<name>:<tag>@sha256:<digest>`.
|
|
36
|
+
|
|
37
|
+
**FORBIDDEN**:
|
|
38
|
+
```yaml
|
|
39
|
+
container: node:24
|
|
40
|
+
container: node:latest
|
|
41
|
+
container: node:24-bookworm
|
|
42
|
+
container: ghcr.io/owner/image:v2.1.0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**REQUIRED**:
|
|
46
|
+
```yaml
|
|
47
|
+
container: node:24@sha256:050bf2bbe33c1d6754e060bec89378a79ed831f04a7bb1a53fe45e997df7b3bb # 24.15.0
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The `<name>:<tag>` prefix is retained for human readability; the digest is the trust anchor. Trailing comment captures the resolved version for diff context.
|
|
51
|
+
|
|
52
|
+
### Rule 3: Pin manifest at `ci/digests.txt` (or equivalent)
|
|
53
|
+
|
|
54
|
+
Every project that enforces this rule MUST maintain a pin manifest listing each pinned reference, its resolved version, the date pinned, and the rationale. The manifest is the source of truth for diffs — a SHA change without a corresponding manifest entry is a red flag.
|
|
55
|
+
|
|
56
|
+
Reference manifest format: see AIWG's own [`ci/digests.txt`](https://git.integrolabs.net/roctinam/aiwg/src/branch/main/ci/digests.txt).
|
|
57
|
+
|
|
58
|
+
Minimum row format:
|
|
59
|
+
```
|
|
60
|
+
<kind> <name> <pin> <resolved-version> <date-pinned> <update-rationale>
|
|
61
|
+
container node:24 sha256:050bf2bbe33c1d6754e060bec89378a79ed831f04a7bb1a53fe45e997df7b3bb 24.15.0 2026-05-12 initial pin
|
|
62
|
+
action actions/checkout 93cb6efe18208431cddfb8368fd83d5badbf9bfd v5.0.1 2026-05-12 initial pin
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Rule 4: Pin bumps go through review
|
|
66
|
+
|
|
67
|
+
Every pin update is a reviewable change. The commit should:
|
|
68
|
+
- Reference the issue or advisory motivating the bump (CVE, needed feature, scheduled rotation)
|
|
69
|
+
- Update the manifest row in the same commit as the workflow file edit
|
|
70
|
+
- Include `chore(ci): bump <name> pin to <version> (refs #<issue>)` style commit message
|
|
71
|
+
|
|
72
|
+
Workflow diffs that change a digest without a corresponding manifest update fail review.
|
|
73
|
+
|
|
74
|
+
### Rule 5: Standalone tools pinned by version + checksum
|
|
75
|
+
|
|
76
|
+
Tools downloaded by `curl | sh` or equivalent in CI (e.g., `syft install.sh`, ad-hoc binary releases) MUST pin a specific version AND record the installer's content hash. The version pin gives reproducibility; the content hash detects upstream tampering.
|
|
77
|
+
|
|
78
|
+
**Acceptable pattern** (strict-mode opt-in, with observed-SHA logging):
|
|
79
|
+
```yaml
|
|
80
|
+
- name: Install syft
|
|
81
|
+
run: |
|
|
82
|
+
VERSION=v1.18.0
|
|
83
|
+
EXPECTED_INSTALL_SHA="" # populate after first run; logs `observed` SHA
|
|
84
|
+
SCRIPT=$(curl -fsSL "https://raw.githubusercontent.com/anchore/syft/${VERSION}/install.sh")
|
|
85
|
+
OBSERVED_SHA=$(printf '%s' "$SCRIPT" | sha256sum | awk '{print $1}')
|
|
86
|
+
echo "observed install-script SHA: ${OBSERVED_SHA}"
|
|
87
|
+
if [ -n "$EXPECTED_INSTALL_SHA" ] && [ "$OBSERVED_SHA" != "$EXPECTED_INSTALL_SHA" ]; then
|
|
88
|
+
echo "✗ install-script SHA drift — refusing to execute"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
printf '%s' "$SCRIPT" | sh -s -- -b /usr/local/bin "$VERSION"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Detection Patterns
|
|
95
|
+
|
|
96
|
+
CI lint should flag the following:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Floating action tags
|
|
100
|
+
grep -rE '^\s*-\s*uses:\s*[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+@(v?[0-9]+(\.[0-9]+)*|main|master|latest)\s*(#.*)?$' \
|
|
101
|
+
.github/workflows/ .gitea/workflows/ 2>/dev/null
|
|
102
|
+
|
|
103
|
+
# Unpinned container images
|
|
104
|
+
grep -rE '^\s*(container|image):\s*[a-zA-Z0-9._/-]+:[a-zA-Z0-9._-]+\s*(#.*)?$' \
|
|
105
|
+
.github/workflows/ .gitea/workflows/ 2>/dev/null \
|
|
106
|
+
| grep -v 'sha256:'
|
|
107
|
+
|
|
108
|
+
# Bare :latest anywhere
|
|
109
|
+
grep -rnE ':latest\b' .github/workflows/ .gitea/workflows/ 2>/dev/null
|
|
110
|
+
|
|
111
|
+
# Curl-pipe-shell installers without a content hash check
|
|
112
|
+
grep -rnE 'curl[^|]+\|\s*(bash|sh)' .github/workflows/ .gitea/workflows/ 2>/dev/null
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Wire these into a `npm run lint:ci-pins` script (or equivalent) and call it from the CI workflow itself so the rule is self-enforcing.
|
|
116
|
+
|
|
117
|
+
## Acceptable Exceptions
|
|
118
|
+
|
|
119
|
+
The following are NOT subject to this rule:
|
|
120
|
+
|
|
121
|
+
1. **Dockerfile `FROM` instructions in build stages** that are NOT consumed by CI directly — those are governed by the project's image-build pipeline (which should follow the same standard, but the rule lives in the Dockerfile review, not the workflow review).
|
|
122
|
+
2. **Ephemeral self-hosted runners** that pull pre-baked AMIs/images with their own attestation chain — the chain-of-trust is at the AMI level, not the workflow level.
|
|
123
|
+
3. **Reusable workflows from the same repository** (`uses: ./.github/workflows/foo.yml`) — these are pinned to the repository state at the calling commit.
|
|
124
|
+
4. **Build-arg substitutions for the same digest** — `FROM ${BASE_IMAGE}` where `BASE_IMAGE` is built from a digest-pinned reference one level up.
|
|
125
|
+
|
|
126
|
+
Document any other exception in a code comment adjacent to the unpinned reference, with reasoning and an issue link tracking eventual migration to a pin.
|
|
127
|
+
|
|
128
|
+
## Rationale
|
|
129
|
+
|
|
130
|
+
The mutable-tag attack surface is one of the largest unaddressed components of npm-ecosystem supply-chain risk as of 2026. The recent Shai-Hulud worm campaign demonstrated that compromised maintainer accounts can push malicious updates that propagate via floating version tags within hours. Workflows pinned by SHA stop that propagation at the boundary of every individual project that pins.
|
|
131
|
+
|
|
132
|
+
Pin-bump cost is low (one curl/git ls-remote command); incident cost from an unpinned dependency executing in CI with secret access is high. The asymmetry justifies the rule.
|
|
133
|
+
|
|
134
|
+
## References
|
|
135
|
+
|
|
136
|
+
- [`dev-idempotent-builds.md`](.claude/rules/dev-idempotent-builds.md) — base rule on reproducible builds
|
|
137
|
+
- AIWG's own pin manifest: `ci/digests.txt`
|
|
138
|
+
- Defenses brief C7 (action pinning) and C8 (container pinning)
|
|
139
|
+
- Shai-Hulud worm post-mortem references
|
|
140
|
+
- GitHub Actions documentation: pinning third-party actions to a full-length commit SHA
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
**Rule Status**: ACTIVE
|
|
145
|
+
**Last Updated**: 2026-05-13
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Dependency Source Policy
|
|
2
|
+
|
|
3
|
+
**Enforcement Level**: HIGH
|
|
4
|
+
**Scope**: `package.json` and lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`) — direct AND transitive entries
|
|
5
|
+
**Issue**: #1297
|
|
6
|
+
|
|
7
|
+
## Principle
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Mandatory Rules
|
|
16
|
+
|
|
17
|
+
### Rule 1: Six dep-source patterns are forbidden by default
|
|
18
|
+
|
|
19
|
+
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:
|
|
20
|
+
|
|
21
|
+
| Pattern | Example | Reason |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `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 |
|
|
24
|
+
| `git://` scheme | `"foo": "git://github.com/owner/foo.git"` | Same as above |
|
|
25
|
+
| `github:` shorthand | `"foo": "github:owner/foo"` | Same as above; npm expands to git+ |
|
|
26
|
+
| Non-registry tarball | `"foo": "https://example.com/foo-1.0.0.tgz"` | Tarball can contain any payload and lifecycle scripts; bypasses registry signature verification |
|
|
27
|
+
| `file:` path | `"foo": "file:./vendor/foo"` | Local-path deps bypass dep-resolution review and lockfile signature checks |
|
|
28
|
+
| `link:` symlink | `"foo": "link:./packages/foo"` | Follows the symlink target wherever it points; same gaps as `file:` |
|
|
29
|
+
|
|
30
|
+
Registry-hosted patterns that are ALWAYS acceptable:
|
|
31
|
+
|
|
32
|
+
- `"foo": "^1.2.3"` — registry semver range
|
|
33
|
+
- `"foo": "1.2.3"` — exact registry version
|
|
34
|
+
- `"foo": "npm:@scope/foo@^1.2.3"` — aliased registry dep
|
|
35
|
+
- Lockfile `"resolved": "https://registry.npmjs.org/..."` — registry-served tarball
|
|
36
|
+
|
|
37
|
+
### Rule 2: Exceptions require an allowlist entry
|
|
38
|
+
|
|
39
|
+
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:
|
|
40
|
+
|
|
41
|
+
- **Owner** — who took the risk-acceptance decision
|
|
42
|
+
- **Reason** — why a registry version isn't viable today
|
|
43
|
+
- **Expiry / review date** — when this exception must be re-evaluated (max 1 year)
|
|
44
|
+
- **Risk acceptance** — explicit statement that the operator accepts the dep-source bypass
|
|
45
|
+
|
|
46
|
+
Allowlist file format (committed at `.aiwg/security/dep-source-allowlist.yaml` or equivalent):
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
allowlist:
|
|
50
|
+
- dep: "foo"
|
|
51
|
+
source: "git+https://github.com/owner/foo.git#sha-or-tag"
|
|
52
|
+
owner: "alice@example.org"
|
|
53
|
+
reason: "Upstream PR #123 not yet merged; we need fix from main for issue X"
|
|
54
|
+
review_date: "2026-08-01"
|
|
55
|
+
risk_acceptance: "Source pinned to commit SHA; reviewed prepare script line-by-line; no shell exec in install path"
|
|
56
|
+
- dep: "@internal/shared"
|
|
57
|
+
source: "file:./packages/shared"
|
|
58
|
+
owner: "bob@example.org"
|
|
59
|
+
reason: "Monorepo-internal package; not published to registry"
|
|
60
|
+
review_date: "2027-01-01"
|
|
61
|
+
risk_acceptance: "Source within same security boundary as host package; no external code path"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Allowlist entries beyond their `review_date` are treated as policy violations.
|
|
65
|
+
|
|
66
|
+
### Rule 3: Lockfile entries are policy targets
|
|
67
|
+
|
|
68
|
+
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:
|
|
69
|
+
|
|
70
|
+
- `"resolved": "git+..."` in `package-lock.json`
|
|
71
|
+
- `tarball: ...` URLs in `pnpm-lock.yaml` that don't match a registry origin
|
|
72
|
+
- `resolution: ` blocks in `yarn.lock`
|
|
73
|
+
- Binary lockfile (`bun.lockb`) — extract via `bun pm ls --all` and pattern-match the output
|
|
74
|
+
|
|
75
|
+
### Rule 4: pnpm workspaces enable `blockExoticSubdeps`
|
|
76
|
+
|
|
77
|
+
When pnpm is the package manager, the workspace MUST set `blockExoticSubdeps: true` (in `pnpm-workspace.yaml`) or the per-repo equivalent in `.npmrc`:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# pnpm-workspace.yaml
|
|
81
|
+
packages:
|
|
82
|
+
- 'packages/*'
|
|
83
|
+
blockExoticSubdeps: true
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
### Rule 5: Failure messages point to remediation
|
|
89
|
+
|
|
90
|
+
When CI rejects a violation, the failure message MUST:
|
|
91
|
+
|
|
92
|
+
1. State which dep and which source pattern matched
|
|
93
|
+
2. Reference the operator's choices: switch to a registry version, request an allowlist entry, or accept install-time compromise risk
|
|
94
|
+
3. Link to this rule (or the project's adaptation of it) so the receiving developer can read the policy
|
|
95
|
+
|
|
96
|
+
Reference failure-message format (from AIWG's own `tools/lint/dep-source.mjs`):
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
✗ Dependency source policy violation
|
|
100
|
+
|
|
101
|
+
foo (in package.json)
|
|
102
|
+
source: git+https://github.com/owner/foo.git
|
|
103
|
+
pattern: git+ scheme
|
|
104
|
+
|
|
105
|
+
This dependency source bypasses registry signature verification and
|
|
106
|
+
executes arbitrary code at install time via the prepare script.
|
|
107
|
+
|
|
108
|
+
Options:
|
|
109
|
+
1. Switch to a registry-published version: npm install foo@<version>
|
|
110
|
+
2. Add an allowlist entry: see .aiwg/security/dep-source-allowlist.yaml
|
|
111
|
+
3. Read the policy: docs/contributing/dependency-sources.md
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Detection Patterns
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# package.json — direct deps with exotic sources
|
|
118
|
+
node -p "
|
|
119
|
+
const p = require('./package.json');
|
|
120
|
+
const all = {...p.dependencies, ...p.devDependencies, ...p.optionalDependencies, ...p.peerDependencies};
|
|
121
|
+
for (const [name, source] of Object.entries(all || {})) {
|
|
122
|
+
if (/^(git\+|git:|github:|file:|link:)/.test(source) ||
|
|
123
|
+
(/^https?:\/\//.test(source) && /\.tgz/.test(source))) {
|
|
124
|
+
console.log(name, '=', source);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
"
|
|
128
|
+
|
|
129
|
+
# package-lock.json — transitive entries with exotic resolved URLs
|
|
130
|
+
node -p "
|
|
131
|
+
const lock = require('./package-lock.json');
|
|
132
|
+
function walk(pkgs, prefix = '') {
|
|
133
|
+
for (const [path, entry] of Object.entries(pkgs || {})) {
|
|
134
|
+
if (entry.resolved && /^(git\+|git:)/.test(entry.resolved)) {
|
|
135
|
+
console.log(prefix + path, '=', entry.resolved);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
walk(lock.packages);
|
|
140
|
+
"
|
|
141
|
+
|
|
142
|
+
# pnpm-lock.yaml — exotic resolution blocks
|
|
143
|
+
grep -E 'resolution:\s*\{(git|tarball:.*[^.]https?://[^/]+/[^.]*\.tgz)' pnpm-lock.yaml 2>/dev/null
|
|
144
|
+
|
|
145
|
+
# yarn.lock — exotic resolved entries
|
|
146
|
+
grep -E '"?resolved"?\s*"?:?\s*"?(git\+|git:)' yarn.lock 2>/dev/null
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
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.
|
|
150
|
+
|
|
151
|
+
## Acceptable Exceptions
|
|
152
|
+
|
|
153
|
+
Three exception categories beyond explicit allowlist entries:
|
|
154
|
+
|
|
155
|
+
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.
|
|
156
|
+
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.
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
## Rationale
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
## References
|
|
166
|
+
|
|
167
|
+
- [`ci-action-pinning`](ci-action-pinning.md) — CI execution-environment trust (companion rule)
|
|
168
|
+
- AIWG's lint implementation: [`tools/lint/dep-source.mjs`](https://git.integrolabs.net/roctinam/aiwg/src/branch/main/tools/lint/dep-source.mjs)
|
|
169
|
+
- AIWG's contributor doc: [`docs/contributing/dependency-sources.md`](https://git.integrolabs.net/roctinam/aiwg/src/branch/main/docs/contributing/dependency-sources.md)
|
|
170
|
+
- ADR: `.aiwg/architecture/adr-dep-source-policy.md` (in AIWG's own repo)
|
|
171
|
+
- pnpm `blockExoticSubdeps`: <https://pnpm.io/settings#blockexoticsubdeps>
|
|
172
|
+
- Mini Shai-Hulud post-mortem references
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
**Rule Status**: ACTIVE
|
|
177
|
+
**Last Updated**: 2026-05-13
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
namespace: aiwg
|
|
3
|
+
name: bun-release-age-gate
|
|
4
|
+
platforms: [all]
|
|
5
|
+
description: Configure Bun's install.minimumReleaseAge gate (7-day default, 10-day high-sensitivity) for JavaScript projects on Bun. Includes Corepack-equivalent version detection and lockfile-caveat warning.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# bun-release-age-gate
|
|
9
|
+
|
|
10
|
+
Use this skill when a user has chosen Bun as their JavaScript runtime
|
|
11
|
+
and package manager and wants release-age-gate hardening parallel to
|
|
12
|
+
what `npm-release-age-gate`, `pnpm-release-age-gate`, and
|
|
13
|
+
`yarn-release-age-gate` provide for their respective ecosystems.
|
|
14
|
+
|
|
15
|
+
## Triggers
|
|
16
|
+
|
|
17
|
+
- "bun release age gate"
|
|
18
|
+
- "bun minimumReleaseAge"
|
|
19
|
+
- "bun supply chain hardening"
|
|
20
|
+
- "bun install hardening"
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- Bun v1.1.30+ installed (`bun --version`)
|
|
25
|
+
- `package.json` exists at repo root
|
|
26
|
+
- `bunfig.toml` exists (or will be created) — Bun's config file
|
|
27
|
+
|
|
28
|
+
If Bun is below v1.1.30, the skill should refuse to proceed:
|
|
29
|
+
`install.minimumReleaseAge` was introduced in v1.1.30 and earlier
|
|
30
|
+
versions silently ignore it.
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Add the gate to `bunfig.toml` at repo root:
|
|
35
|
+
|
|
36
|
+
```toml
|
|
37
|
+
[install]
|
|
38
|
+
# Refuse dependency versions published less than 7 days ago.
|
|
39
|
+
# Bun interprets the value in SECONDS. 604800 = 7 days; 864000 = 10 days.
|
|
40
|
+
# Defends against newly-published malicious versions.
|
|
41
|
+
minimumReleaseAge = 604800
|
|
42
|
+
|
|
43
|
+
# Optional: reject git+ and tarball-URL sources during install
|
|
44
|
+
# (Bun's equivalent of pnpm's blockExoticSubdeps — check current
|
|
45
|
+
# version's docs for the exact key name; the API is evolving)
|
|
46
|
+
# safeRegistry = true
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Unit conversion reference
|
|
50
|
+
|
|
51
|
+
Bun uses **seconds** for `install.minimumReleaseAge`:
|
|
52
|
+
|
|
53
|
+
| Days | Seconds value |
|
|
54
|
+
|---|---|
|
|
55
|
+
| 1 | `86400` |
|
|
56
|
+
| 7 (recommended default) | `604800` |
|
|
57
|
+
| 10 (high-sensitivity profile) | `864000` |
|
|
58
|
+
| 14 | `1209600` |
|
|
59
|
+
| 30 | `2592000` |
|
|
60
|
+
|
|
61
|
+
This differs from npm (**days**), pnpm (**minutes**), and Yarn
|
|
62
|
+
(**duration string** like `7d`). Document the unit inline when
|
|
63
|
+
committing the config so future readers don't misread it.
|
|
64
|
+
|
|
65
|
+
## Lockfile caveat
|
|
66
|
+
|
|
67
|
+
The gate is checked at resolution time. If `bun.lockb` was generated
|
|
68
|
+
without the gate, the gate applies on the NEXT resolution pass — not
|
|
69
|
+
retroactively against the existing lockfile.
|
|
70
|
+
|
|
71
|
+
To apply the gate retroactively:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Force re-resolution
|
|
75
|
+
rm bun.lockb
|
|
76
|
+
bun install
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This is destructive to existing pins. Coordinate before running.
|
|
80
|
+
|
|
81
|
+
## Version detection (Corepack-equivalent)
|
|
82
|
+
|
|
83
|
+
Bun is not currently in the Corepack supported set, but version
|
|
84
|
+
pinning is still important. The `packageManager` field in
|
|
85
|
+
`package.json` can pin Bun:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node -p "require('./package.json').packageManager"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If the value is `bun@<version>`, that's the pinned version (CI
|
|
92
|
+
should use this). If empty, recommend setting it:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"packageManager": "bun@1.1.30"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The skill should:
|
|
101
|
+
|
|
102
|
+
1. Confirm pinned version is ≥ v1.1.30 (else flag — gate is silently ignored)
|
|
103
|
+
2. Document the pinned version
|
|
104
|
+
3. If unpinned, suggest pinning before applying the gate
|
|
105
|
+
|
|
106
|
+
## Override policy
|
|
107
|
+
|
|
108
|
+
Genuine emergency overrides:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Bypass the gate for a single install (rare)
|
|
112
|
+
bun add <pkg> --minimum-release-age=0
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Document every override with reason + sunset date.
|
|
116
|
+
|
|
117
|
+
## CI integration
|
|
118
|
+
|
|
119
|
+
Add a verification step to the publish/build workflow:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
- name: Verify Bun gate active
|
|
123
|
+
run: |
|
|
124
|
+
set -euo pipefail
|
|
125
|
+
GATE=$(grep -E '^\s*minimumReleaseAge\s*=' bunfig.toml | awk -F'=' '{print $2}' | tr -d ' ')
|
|
126
|
+
if [ -z "$GATE" ] || [ "$GATE" -lt 604800 ]; then
|
|
127
|
+
echo "✗ Bun install.minimumReleaseAge not configured to baseline (≥604800 = 7 days)"
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
echo "✓ Bun install.minimumReleaseAge = $GATE seconds"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## What to inspect during review
|
|
134
|
+
|
|
135
|
+
- `bunfig.toml` for `[install] minimumReleaseAge`
|
|
136
|
+
- `package.json` `packageManager` field for Bun version pin
|
|
137
|
+
- CI workflow has the verification step above
|
|
138
|
+
- `bun.lockb` was generated AFTER the gate was committed (timestamp check)
|
|
139
|
+
|
|
140
|
+
## Output format
|
|
141
|
+
|
|
142
|
+
When auditing an existing Bun project, produce a structured report
|
|
143
|
+
at `.aiwg/security/working/bun-release-age-audit.md`:
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
# Bun Release-Age Gate Audit
|
|
147
|
+
|
|
148
|
+
**Bun version**: <version> (Pinned in package.json: yes/no)
|
|
149
|
+
**Gate active**: yes (604800s) / yes (864000s) / yes (custom: <value>) / no
|
|
150
|
+
|
|
151
|
+
## Findings
|
|
152
|
+
|
|
153
|
+
### <severity> — <description>
|
|
154
|
+
|
|
155
|
+
- File: <path>
|
|
156
|
+
- Issue: <what's wrong>
|
|
157
|
+
- Fix: <exact change>
|
|
158
|
+
|
|
159
|
+
## Clean Checks
|
|
160
|
+
|
|
161
|
+
- ...
|
|
162
|
+
|
|
163
|
+
## Recommendations
|
|
164
|
+
|
|
165
|
+
- ...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Bun-specific notes
|
|
169
|
+
|
|
170
|
+
Bun's install API is younger than npm/pnpm/Yarn — settings names have
|
|
171
|
+
changed across versions. The skill should:
|
|
172
|
+
|
|
173
|
+
- Check the installed Bun version's docs for the canonical setting name
|
|
174
|
+
- If `install.minimumReleaseAge` isn't recognized by the installed
|
|
175
|
+
version, fall back to checking for older keys (`minReleaseAge`,
|
|
176
|
+
`releaseAge`) and recommend upgrade
|
|
177
|
+
|
|
178
|
+
When in doubt, run `bun pm --help` and `bun install --help` to
|
|
179
|
+
discover the current supported flags.
|
|
180
|
+
|
|
181
|
+
## See Also
|
|
182
|
+
|
|
183
|
+
- [`npm-release-age-gate` skill](../npm-release-age-gate/SKILL.md) — npm equivalent
|
|
184
|
+
- [`pnpm-release-age-gate` skill](../pnpm-release-age-gate/SKILL.md) — pnpm equivalent
|
|
185
|
+
- [`yarn-release-age-gate` skill](../yarn-release-age-gate/SKILL.md) — Yarn equivalent
|
|
186
|
+
- [`npm-supply-chain-audit` skill](../npm-supply-chain-audit/SKILL.md) — companion audit
|
|
187
|
+
- [`supply-chain-hardening-quickstart` skill](../supply-chain-hardening-quickstart/SKILL.md) — orchestrator
|
|
188
|
+
|
|
189
|
+
## References
|
|
190
|
+
|
|
191
|
+
- Bun install settings: <https://bun.com/docs/runtime/bunfig#install>
|
|
192
|
+
- Bun `bunfig.toml` reference: <https://bun.com/docs/runtime/bunfig>
|
|
193
|
+
- Bun release notes for `minimumReleaseAge` introduction: check Bun changelog for v1.1.30
|