supply-chain-guardrail 1.0.0
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/.github/workflows/guardrail.yml +37 -0
- package/INSTALL.md +265 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/src/commands/audit-tokens.js +111 -0
- package/dist/src/commands/incident.js +350 -0
- package/dist/src/commands/ioc.js +199 -0
- package/dist/src/commands/monitor.js +361 -0
- package/dist/src/commands/scan.js +794 -0
- package/dist/src/commands/verify.js +91 -0
- package/dist/src/core/baseline.js +313 -0
- package/dist/src/core/npm-feed.js +54 -0
- package/dist/src/core/provenance.js +600 -0
- package/dist/src/core/script-analyzer.js +372 -0
- package/dist/src/core/token-scanner.js +364 -0
- package/dist/src/index.js +250 -0
- package/dist/src/integrations/github-actions.js +236 -0
- package/dist/src/integrations/sarif.js +110 -0
- package/dist/src/integrations/slack.js +48 -0
- package/dist/src/types/index.js +2 -0
- package/dist/src/utils/lockfile.js +505 -0
- package/dist/src/utils/registry.js +125 -0
- package/guardrail.config.json +71 -0
- package/package.json +59 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: guardrail
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
- master
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
guardrail:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
actions: read
|
|
15
|
+
security-events: write
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout
|
|
19
|
+
uses: actions/checkout@v5
|
|
20
|
+
|
|
21
|
+
- name: Set up Node.js
|
|
22
|
+
uses: actions/setup-node@v5
|
|
23
|
+
with:
|
|
24
|
+
node-version: '22'
|
|
25
|
+
cache: npm
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies and build GuardRail
|
|
28
|
+
run: npm ci && npm run build
|
|
29
|
+
|
|
30
|
+
- name: Run GuardRail scan
|
|
31
|
+
run: node dist/src/index.js scan --fail-fast --sarif guardrail.sarif
|
|
32
|
+
|
|
33
|
+
- name: Upload GuardRail SARIF
|
|
34
|
+
if: always()
|
|
35
|
+
uses: github/codeql-action/upload-sarif@v4
|
|
36
|
+
with:
|
|
37
|
+
sarif_file: guardrail.sarif
|
package/INSTALL.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# GuardRail installation and rollout
|
|
2
|
+
|
|
3
|
+
This is the shortest path from download to a working first scan.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js 20 or newer
|
|
8
|
+
- npm 10 or newer
|
|
9
|
+
- internet access for `verify` and `monitor`
|
|
10
|
+
- a GitHub token only if you want `incident` to read GitHub Actions logs
|
|
11
|
+
|
|
12
|
+
## Path 1: developer machine
|
|
13
|
+
|
|
14
|
+
### 1. Get the code
|
|
15
|
+
|
|
16
|
+
Clone the repository or unpack the release archive.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
git clone https://github.com/guardrail-security/guardrail.git
|
|
20
|
+
cd guardrail
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Install dependencies
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 3. Build the CLI
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm run build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 4. Create or review config
|
|
36
|
+
|
|
37
|
+
Copy the example file if you want a clean starting point.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cp guardrail.config.json your-project/guardrail.config.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
At minimum, adjust:
|
|
44
|
+
|
|
45
|
+
- `scan.trustedPackages`
|
|
46
|
+
- `tokenPolicy.staleAfterDays`
|
|
47
|
+
- `github.owner`, `github.repo`, and `github.tokenEnvVar` if you plan to use `incident`
|
|
48
|
+
- notification settings if you want live alerts
|
|
49
|
+
|
|
50
|
+
### 5. Run the first scan against a real Node project
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd /path/to/your-node-project
|
|
54
|
+
npx /path/to/guardrail/dist/src/index.js scan --fail-fast --sarif guardrail.sarif
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If you installed GuardRail globally instead:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
guardrail scan --fail-fast --sarif guardrail.sarif
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 6. Review the first-run baseline
|
|
64
|
+
|
|
65
|
+
The first scan writes a signed baseline under `.guardrail/` by default.
|
|
66
|
+
|
|
67
|
+
Files created there:
|
|
68
|
+
|
|
69
|
+
- `.guardrail/baseline.json`
|
|
70
|
+
- `.guardrail/baseline-private.pem`
|
|
71
|
+
- `.guardrail/baseline-public.pem`
|
|
72
|
+
|
|
73
|
+
Commit the baseline file if you want the project baseline to travel with the repository. Do **not** commit the private key unless you intentionally want a shared signing identity. In most teams, keep the private key in CI or a secure developer secret store.
|
|
74
|
+
|
|
75
|
+
### 7. Add local pre-commit protection
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
guardrail scan --install-pre-commit
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That writes `.git/hooks/pre-commit` so risky dependency changes fail before commit.
|
|
82
|
+
|
|
83
|
+
## Path 2: use GuardRail without global install
|
|
84
|
+
|
|
85
|
+
From any Node project:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx guardrail-security scan
|
|
89
|
+
npx guardrail-security audit-tokens
|
|
90
|
+
npx guardrail-security verify axios@1.14.0
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Path 3: GitHub Actions rollout
|
|
94
|
+
|
|
95
|
+
### 1. Add the workflow file
|
|
96
|
+
|
|
97
|
+
Copy `.github/workflows/guardrail.yml` into the target repository.
|
|
98
|
+
|
|
99
|
+
Or generate it from GuardRail itself:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
guardrail scan --generate-workflow
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 2. Make sure the workflow can upload SARIF
|
|
106
|
+
|
|
107
|
+
The generated workflow already includes these permissions:
|
|
108
|
+
|
|
109
|
+
- `contents: read`
|
|
110
|
+
- `actions: read`
|
|
111
|
+
- `security-events: write`
|
|
112
|
+
- `id-token: write`
|
|
113
|
+
|
|
114
|
+
### 3. Keep dependency installation script-safe in CI
|
|
115
|
+
|
|
116
|
+
The workflow intentionally installs project dependencies with:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm ci --ignore-scripts
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
That lets GuardRail inspect lifecycle hooks before the hooks execute.
|
|
123
|
+
|
|
124
|
+
### 4. Decide whether GuardRail should fail the build
|
|
125
|
+
|
|
126
|
+
Default fail-fast usage:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
guardrail scan --fail-fast --sarif guardrail.sarif
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Tune the policy in `guardrail.config.json`:
|
|
133
|
+
|
|
134
|
+
```jsonc
|
|
135
|
+
{
|
|
136
|
+
"scan": {
|
|
137
|
+
"riskThreshold": 70,
|
|
138
|
+
"failOnSeverity": "high"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 5. Add GitHub token support for incident response
|
|
144
|
+
|
|
145
|
+
If you want `guardrail incident` to inspect workflow logs, add a token with Actions read access.
|
|
146
|
+
|
|
147
|
+
Example repository secret setup:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
gh secret set GITHUB_TOKEN < ./github-token.txt
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Then set in config:
|
|
154
|
+
|
|
155
|
+
```jsonc
|
|
156
|
+
{
|
|
157
|
+
"github": {
|
|
158
|
+
"owner": "your-org",
|
|
159
|
+
"repo": "your-repo",
|
|
160
|
+
"tokenEnvVar": "GITHUB_TOKEN"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Optional: live monitoring
|
|
166
|
+
|
|
167
|
+
### Slack webhook
|
|
168
|
+
|
|
169
|
+
```jsonc
|
|
170
|
+
{
|
|
171
|
+
"monitor": {
|
|
172
|
+
"packages": ["axios", "react"],
|
|
173
|
+
"slackWebhook": "https://hooks.slack.com/services/..."
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Run:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
guardrail monitor
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Generic webhook
|
|
185
|
+
|
|
186
|
+
```jsonc
|
|
187
|
+
{
|
|
188
|
+
"monitor": {
|
|
189
|
+
"packages": ["axios"],
|
|
190
|
+
"webhook": "https://your-webhook.example/guardrail"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Email via SMTP
|
|
196
|
+
|
|
197
|
+
```jsonc
|
|
198
|
+
{
|
|
199
|
+
"monitor": {
|
|
200
|
+
"packages": ["axios"],
|
|
201
|
+
"email": {
|
|
202
|
+
"host": "smtp.example.com",
|
|
203
|
+
"port": 587,
|
|
204
|
+
"secure": false,
|
|
205
|
+
"username": "smtp-user",
|
|
206
|
+
"password": "smtp-password",
|
|
207
|
+
"from": "guardrail@example.com",
|
|
208
|
+
"to": ["secops@example.com"]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Recommended rollout order
|
|
215
|
+
|
|
216
|
+
1. run `guardrail scan` on a clean branch
|
|
217
|
+
2. review and commit the baseline policy files
|
|
218
|
+
3. add `guardrail audit-tokens` to maintainer and CI environments
|
|
219
|
+
4. enable the GitHub Actions workflow
|
|
220
|
+
5. turn on `--fail-fast`
|
|
221
|
+
6. enable live monitoring for the packages you trust most
|
|
222
|
+
7. use `guardrail incident` for every supply chain advisory that overlaps your dependency tree
|
|
223
|
+
|
|
224
|
+
## Five-minute first scan
|
|
225
|
+
|
|
226
|
+
Assuming GuardRail is already downloaded:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
cd /path/to/guardrail
|
|
230
|
+
npm install
|
|
231
|
+
npm run build
|
|
232
|
+
cd /path/to/your-node-project
|
|
233
|
+
cp /path/to/guardrail/guardrail.config.json .
|
|
234
|
+
node /path/to/guardrail/dist/src/index.js scan --fail-fast --sarif guardrail.sarif
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
That gets you:
|
|
238
|
+
|
|
239
|
+
- a signed baseline
|
|
240
|
+
- dependency mutation analysis
|
|
241
|
+
- ghost dependency checks
|
|
242
|
+
- lifecycle script inventory and scoring
|
|
243
|
+
- a SARIF file for GitHub code scanning
|
|
244
|
+
|
|
245
|
+
## Troubleshooting
|
|
246
|
+
|
|
247
|
+
### No lockfile found
|
|
248
|
+
|
|
249
|
+
GuardRail can still scan installed packages from `node_modules`, but it gets stronger when a lockfile exists.
|
|
250
|
+
|
|
251
|
+
### `verify` says `fetch failed`
|
|
252
|
+
|
|
253
|
+
The command needs network access to the npm registry and usually the source host.
|
|
254
|
+
|
|
255
|
+
### GitHub log scan says skipped
|
|
256
|
+
|
|
257
|
+
Set `github.owner`, `github.repo`, and a token through `github.tokenEnvVar` or `--github-token`.
|
|
258
|
+
|
|
259
|
+
### SMTP alerts fail
|
|
260
|
+
|
|
261
|
+
Check host, port, auth mode, firewall, and whether the SMTP server accepts `AUTH PLAIN`.
|
|
262
|
+
|
|
263
|
+
### False positives on generated build artifacts
|
|
264
|
+
|
|
265
|
+
This usually affects `verify`, not `scan`. Generated tarball files that are intentionally not committed to source will produce `partial` instead of `verified`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GuardRail Security Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# GuardRail
|
|
2
|
+
|
|
3
|
+
GuardRail is a zero-dependency npm supply chain security tool that detects attacks before any advisory exists. It was built in response to the March 2026 axios supply chain compromise (GHSA-fw8c-xr5c-95f9), where a trusted package silently added a malicious dependency that ran a postinstall hook to deploy a RAT. No CVE existed at the time of the attack. Traditional audit tools were blind to it. GuardRail catches this class of attack -- and others like it -- by analyzing behavioral signals rather than waiting for someone to file an advisory.
|
|
4
|
+
|
|
5
|
+
The core insight: no existing tool catches "ghost dependencies" -- packages added to `package.json` that are never imported in source code. These packages exist solely to run their postinstall hooks. GuardRail cross-references every declared dependency against the actual import graph of the package source, and flags any dependency that is declared but never used. Combined with behavioral scoring of install scripts, baseline drift detection, and IOC matching, GuardRail provides defense-in-depth for any npm project.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g supply-chain-guardrail
|
|
11
|
+
cd your-project
|
|
12
|
+
guardrail scan
|
|
13
|
+
guardrail ioc list
|
|
14
|
+
guardrail audit-tokens
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## How Detection Works
|
|
18
|
+
|
|
19
|
+
### Ghost Dependency Detection
|
|
20
|
+
|
|
21
|
+
GuardRail builds an import map by statically analyzing all source files in a package (`.js`, `.ts`, `.mjs`, `.cjs`, and related extensions). It extracts every `require()` call and `import` statement, then cross-references those against the dependencies declared in `package.json`. If a dependency is declared but never imported by any source file, it is flagged as a ghost dependency. This is the single most reliable signal for postinstall dropper attacks, because the malicious package does not need to be imported -- it only needs to be installed.
|
|
22
|
+
|
|
23
|
+
### Script Behavioral Scoring
|
|
24
|
+
|
|
25
|
+
Every lifecycle script (`preinstall`, `install`, `postinstall`, `prepare`, `prepublish`) across the full dependency tree is analyzed against a weighted multi-rule system. Each rule detects a specific behavioral pattern: network calls, process spawning, file writes, base64/hex payloads, eval usage, self-delete behavior, persistence paths, and shell launchers. Individual rule matches produce a base score, and compound bonuses are added when multiple dangerous patterns appear together (for example, a script that both fetches from a URL and spawns a child process scores higher than either signal alone). The final score is compared against a configurable threshold to determine severity.
|
|
26
|
+
|
|
27
|
+
### Axios Blind Spot (OIDC + Token Coexistence)
|
|
28
|
+
|
|
29
|
+
The axios attack succeeded in part because the compromised maintainer account still had static npm publish tokens alongside OIDC trusted publishing. GuardRail scans for this exact configuration: if a project uses OIDC trusted publishing but static publish tokens also exist (in `.npmrc`, environment variables, or CI workflow files), it flags the mixed-mode setup as a critical risk. An attacker who compromises a static token can publish releases that bypass the trusted-publisher pipeline entirely.
|
|
30
|
+
|
|
31
|
+
### Baseline Drift
|
|
32
|
+
|
|
33
|
+
GuardRail creates an Ed25519-signed snapshot of every package's dependency set, lifecycle scripts, and manifest contents. On subsequent scans, it compares the current state against the signed baseline. Any change -- new dependencies, modified scripts, altered metadata -- is detected and reported. Because the baseline is cryptographically signed, an attacker cannot silently modify it. The `--update-baseline` flag lets you intentionally advance the baseline after reviewing changes.
|
|
34
|
+
|
|
35
|
+
### Advisory-Independent
|
|
36
|
+
|
|
37
|
+
GuardRail does not depend on CVE databases or advisory feeds. All detection is based on behavioral analysis and structural comparison. This means GuardRail catches zero-day supply chain attacks the moment they appear in a package release, not days or weeks later when an advisory is published. The built-in IOC database provides an additional layer for known threats, but the core detection engine works without it.
|
|
38
|
+
|
|
39
|
+
## CLI Reference
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
|---------|-------------|
|
|
43
|
+
| `guardrail scan` | Full project scan: ghost dependencies, script scoring, baseline drift, IOC matching |
|
|
44
|
+
| `guardrail monitor` | Watch the npm change feed for new releases of your dependencies |
|
|
45
|
+
| `guardrail audit-tokens` | Find static publish tokens and mixed-mode publishing risk |
|
|
46
|
+
| `guardrail verify <package@version>` | Verify a specific package version against registry metadata and provenance signals |
|
|
47
|
+
| `guardrail incident <package@version> --from ISO --to ISO` | Build an incident checklist and optionally scan GitHub Actions logs |
|
|
48
|
+
| `guardrail ioc list\|add\|remove\|check` | Manage the local IOC (Indicators of Compromise) database |
|
|
49
|
+
|
|
50
|
+
### scan
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
guardrail scan [options]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Flag | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `--root-dir <path>` | Project root directory (default: current directory) |
|
|
59
|
+
| `--fail-fast` | Exit with non-zero status on first high-severity finding |
|
|
60
|
+
| `--threshold <number>` | Script risk score threshold (default: from config or 70) |
|
|
61
|
+
| `--sarif <path>` | Write findings in SARIF format for GitHub Code Scanning |
|
|
62
|
+
| `--update-baseline` | Update the signed baseline after scan |
|
|
63
|
+
| `--generate-workflow` | Generate a GitHub Actions workflow file |
|
|
64
|
+
| `--install-pre-commit` | Install a Git pre-commit hook that runs GuardRail |
|
|
65
|
+
| `--output <path>` | Write scan results to a file |
|
|
66
|
+
| `--json` | Output results as JSON |
|
|
67
|
+
| `--quiet` | Suppress non-essential output |
|
|
68
|
+
|
|
69
|
+
### monitor
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
guardrail monitor [options]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Flag | Description |
|
|
76
|
+
|------|-------------|
|
|
77
|
+
| `--root-dir <path>` | Project root directory |
|
|
78
|
+
| `--interval-ms <ms>` | Poll interval in milliseconds |
|
|
79
|
+
| `--slack-webhook <url>` | Slack webhook URL for alerts |
|
|
80
|
+
| `--webhook <url>` | Generic webhook URL for alerts |
|
|
81
|
+
| `--once` | Run one check and exit instead of continuous monitoring |
|
|
82
|
+
|
|
83
|
+
### audit-tokens
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
guardrail audit-tokens [options]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Flag | Description |
|
|
90
|
+
|------|-------------|
|
|
91
|
+
| `--root-dir <path>` | Project root directory |
|
|
92
|
+
| `--revoke-stale` | Suggest revocation of stale tokens |
|
|
93
|
+
| `--stale-after-days <days>` | Number of days after which a token is considered stale |
|
|
94
|
+
|
|
95
|
+
### verify
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
guardrail verify <package@version> [options]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Flag | Description |
|
|
102
|
+
|------|-------------|
|
|
103
|
+
| `--root-dir <path>` | Project root directory |
|
|
104
|
+
| `--fail-fast` | Exit with non-zero status on verification failure |
|
|
105
|
+
|
|
106
|
+
### incident
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
guardrail incident <package@version> --from <ISO> --to <ISO> [options]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Flag | Description |
|
|
113
|
+
|------|-------------|
|
|
114
|
+
| `--from <ISO>` | Start of the incident time window (required) |
|
|
115
|
+
| `--to <ISO>` | End of the incident time window (required) |
|
|
116
|
+
| `--github-owner <owner>` | GitHub repository owner for Actions log scanning |
|
|
117
|
+
| `--github-repo <repo>` | GitHub repository name |
|
|
118
|
+
| `--github-token <token>` | GitHub personal access token |
|
|
119
|
+
|
|
120
|
+
### ioc
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
guardrail ioc <list|add|remove|check> [options]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Flag | Description |
|
|
127
|
+
|------|-------------|
|
|
128
|
+
| `--reason <text>` | Reason for adding an IOC entry |
|
|
129
|
+
| `--advisory <id>` | Advisory identifier (GHSA, CVE) |
|
|
130
|
+
|
|
131
|
+
## IOC Database
|
|
132
|
+
|
|
133
|
+
GuardRail maintains a database of known malicious packages (Indicators of Compromise). The database has two layers: built-in entries that ship with GuardRail, and custom entries that you manage per-project via `guardrail.config.json`.
|
|
134
|
+
|
|
135
|
+
### List all IOCs
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
guardrail ioc list
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Shows both built-in and custom IOC entries with their reasons and advisory links.
|
|
142
|
+
|
|
143
|
+
### Add a custom IOC
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
guardrail ioc add evil-package --reason "Exfiltrates env vars via postinstall" --advisory "GHSA-xxxx-xxxx-xxxx"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Adds a package to the custom IOC list in your project config. Future scans will flag this package if it appears in any dependency tree.
|
|
150
|
+
|
|
151
|
+
### Check a specific package
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
guardrail ioc check suspicious-package
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Checks whether a package name matches any built-in or custom IOC entry.
|
|
158
|
+
|
|
159
|
+
### Remove a custom IOC
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
guardrail ioc remove evil-package
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Removes a package from the custom IOC list. Built-in IOCs cannot be removed.
|
|
166
|
+
|
|
167
|
+
## Contributing IOCs
|
|
168
|
+
|
|
169
|
+
The community can contribute new malicious package entries to the built-in IOC database. To submit a new IOC:
|
|
170
|
+
|
|
171
|
+
1. Open a GitHub issue using the "New IOC Submission" template.
|
|
172
|
+
2. Provide the package name, affected versions, advisory link (GHSA or CVE), and a description of the malicious behavior.
|
|
173
|
+
3. Alternatively, submit a pull request that adds the entry directly to the `BUILTIN_IOCS` array in `src/commands/ioc.ts` and the `builtinIocs` object in `src/commands/scan.ts`.
|
|
174
|
+
|
|
175
|
+
All submissions require evidence: a published advisory or a reproducible technical analysis demonstrating malicious behavior.
|
|
176
|
+
|
|
177
|
+
## CI/CD Integration
|
|
178
|
+
|
|
179
|
+
GuardRail integrates into your CI/CD pipeline in three ways:
|
|
180
|
+
|
|
181
|
+
- **GitHub Actions workflow**: Run `guardrail scan --generate-workflow` to create a `.github/workflows/guardrail.yml` file that runs on every push and pull request.
|
|
182
|
+
- **SARIF output**: Run `guardrail scan --sarif guardrail.sarif` to produce SARIF output compatible with GitHub Code Scanning. Upload the SARIF file using the `github/codeql-action/upload-sarif` action to see GuardRail findings directly in the Security tab.
|
|
183
|
+
- **Pre-commit hook**: Run `guardrail scan --install-pre-commit` to install a Git hook that runs GuardRail before every commit, blocking commits that introduce high-severity findings.
|
|
184
|
+
|
|
185
|
+
## Configuration
|
|
186
|
+
|
|
187
|
+
GuardRail reads `guardrail.config.json` from the project root. The file supports JSON-with-comments (lines starting with `//` and `/* ... */` blocks are stripped before parsing).
|
|
188
|
+
|
|
189
|
+
```jsonc
|
|
190
|
+
{
|
|
191
|
+
"scan": {
|
|
192
|
+
"riskThreshold": 70,
|
|
193
|
+
"failOnSeverity": "high",
|
|
194
|
+
"trustedPackages": ["axios"]
|
|
195
|
+
},
|
|
196
|
+
"tokenPolicy": {
|
|
197
|
+
"staleAfterDays": 30,
|
|
198
|
+
"mixedModeAllowed": false
|
|
199
|
+
},
|
|
200
|
+
"github": {
|
|
201
|
+
"owner": "your-org",
|
|
202
|
+
"repo": "your-repo",
|
|
203
|
+
"tokenEnvVar": "GITHUB_TOKEN"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Key configuration fields:
|
|
209
|
+
|
|
210
|
+
| Field | Description |
|
|
211
|
+
|-------|-------------|
|
|
212
|
+
| `baseline.directory` | Directory for baseline data and signing keys |
|
|
213
|
+
| `baseline.path` | Signed baseline file path |
|
|
214
|
+
| `baseline.privateKeyPath` | Ed25519 private signing key path |
|
|
215
|
+
| `baseline.publicKeyPath` | Ed25519 public verification key path |
|
|
216
|
+
| `scan.riskThreshold` | Script risk score threshold for blocking |
|
|
217
|
+
| `scan.failOnSeverity` | Minimum severity that fails CI in `--fail-fast` mode |
|
|
218
|
+
| `scan.trustedPackages` | Packages whose mutations are treated as especially sensitive |
|
|
219
|
+
| `scan.ignoreDirs` | Directories to skip when building import graphs |
|
|
220
|
+
| `scan.maxScriptFileBytes` | Maximum script file size to analyze |
|
|
221
|
+
| `tokenPolicy.staleAfterDays` | Days after which a token is considered stale |
|
|
222
|
+
| `tokenPolicy.mixedModeAllowed` | Allow OIDC + static token coexistence |
|
|
223
|
+
| `monitor.packages` | Explicit package watch list |
|
|
224
|
+
| `monitor.pollIntervalMs` | npm change feed poll interval |
|
|
225
|
+
| `monitor.slackWebhook` | Default Slack webhook for alerts |
|
|
226
|
+
| `monitor.webhook` | Generic webhook for alerts |
|
|
227
|
+
| `github.owner` / `github.repo` / `github.tokenEnvVar` | Defaults for incident log scanning |
|
|
228
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runAuditTokens = runAuditTokens;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const token_scanner_1 = require("../core/token-scanner");
|
|
40
|
+
const baseline_1 = require("../core/baseline");
|
|
41
|
+
async function runAuditTokens(options, config) {
|
|
42
|
+
const rootDir = path.resolve(options.rootDir);
|
|
43
|
+
const result = await (0, token_scanner_1.scanTokenExposure)(rootDir, config);
|
|
44
|
+
let revoked = [];
|
|
45
|
+
if (options.revokeStale) {
|
|
46
|
+
revoked = (0, token_scanner_1.revokeSuggestedTokens)(result.findings, options.staleAfterDays ?? config.tokenPolicy?.staleAfterDays ?? 30);
|
|
47
|
+
}
|
|
48
|
+
if (options.output) {
|
|
49
|
+
fs.writeFileSync(path.resolve(rootDir, options.output), `${JSON.stringify({ ...result, revoked }, null, 2)}\n`);
|
|
50
|
+
}
|
|
51
|
+
if (options.json) {
|
|
52
|
+
console.log(JSON.stringify({ ...result, revoked }, null, 2));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
printHumanReadable(result, revoked);
|
|
56
|
+
}
|
|
57
|
+
const highest = result.issues.reduce((max, issue) => Math.max(max, (0, baseline_1.severityToNumber)(issue.severity)), 1);
|
|
58
|
+
return highest >= (0, baseline_1.severityToNumber)('high') ? 1 : 0;
|
|
59
|
+
}
|
|
60
|
+
function printHumanReadable(result, revoked) {
|
|
61
|
+
console.log('GuardRail token audit');
|
|
62
|
+
console.log(`root: ${result.rootDir}`);
|
|
63
|
+
console.log(`oidc trusted publishing detected: ${result.oidcTrustedPublishingDetected ? 'yes' : 'no'}`);
|
|
64
|
+
console.log(`self-hosted runners detected: ${result.selfHostedRunnerDetected ? 'yes' : 'no'}`);
|
|
65
|
+
console.log(`static publish tokens found: ${result.staticPublishTokensFound ? 'yes' : 'no'}`);
|
|
66
|
+
console.log(`mixed mode risk: ${result.mixedModeRisk ? 'yes' : 'no'}`);
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log('Discovered credentials:');
|
|
69
|
+
if (result.findings.length === 0) {
|
|
70
|
+
console.log('- none');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (const finding of result.findings) {
|
|
74
|
+
console.log(`- ${finding.sourceType} ${finding.sourcePath ?? finding.envVar ?? 'unknown'} -> ${finding.tokenKind} ${finding.tokenPreview}`);
|
|
75
|
+
if (finding.note) {
|
|
76
|
+
console.log(` note: ${finding.note}`);
|
|
77
|
+
}
|
|
78
|
+
if (finding.expiresAt) {
|
|
79
|
+
console.log(` expires: ${finding.expiresAt}`);
|
|
80
|
+
}
|
|
81
|
+
if (finding.id) {
|
|
82
|
+
console.log(` revoke: npm token revoke ${finding.id}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log('Findings:');
|
|
88
|
+
if (result.issues.length === 0) {
|
|
89
|
+
console.log('- no token hygiene issues detected');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
for (const issue of result.issues) {
|
|
93
|
+
console.log(`- [${issue.severity}] ${issue.code}: ${issue.title}`);
|
|
94
|
+
console.log(` ${issue.description}`);
|
|
95
|
+
if (issue.recommendation) {
|
|
96
|
+
console.log(` action: ${issue.recommendation}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (result.suggestedRevocations.length > 0) {
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('Suggested revocations:');
|
|
103
|
+
for (const command of result.suggestedRevocations) {
|
|
104
|
+
console.log(`- ${command}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (revoked.length > 0) {
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(`Revoked tokens: ${revoked.join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
}
|