chain-audit 0.6.2 → 0.6.5
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/README.md +86 -61
- package/package.json +7 -12
- package/src/analyzer.js +340 -24
- package/src/collector.js +0 -12
- package/src/config.js +3 -3
- package/src/formatters.js +79 -7
- package/src/index.js +24 -17
package/README.md
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**Zero-dependency heuristic scanner CLI to detect supply chain attacks in `node_modules`.**
|
|
9
|
+
|
|
10
|
+
> **Disclaimer:** chain-audit is a **heuristic scanner** that searches for suspicious patterns in code. It does **not** detect 100% confirmed attacks, but rather flags potentially suspicious behavior that requires **human analysis**. The tool may produce **false positives** (flagging legitimate code as suspicious) and **false negatives** (missing real attacks). It's up to you to review and determine whether findings are actually suspicious or legitimate. Always investigate findings before taking action.
|
|
11
|
+
> Licensed under **MIT License**, provided "AS IS" without warranty of any kind.
|
|
9
12
|
|
|
10
13
|
---
|
|
11
14
|
|
|
@@ -15,14 +18,17 @@
|
|
|
15
18
|
|---------|-------------|-----------|
|
|
16
19
|
| Detects known CVEs | ❌ | ✅ |
|
|
17
20
|
| Detects malicious install scripts | ✅ | ❌ |
|
|
18
|
-
| Detects
|
|
19
|
-
| Detects
|
|
20
|
-
| Detects
|
|
21
|
+
| Detects network access in scripts | ✅ | ❌ |
|
|
22
|
+
| Detects env exfiltration attempts | ✅ | ❌ |
|
|
23
|
+
| Detects executable files | ✅ | ❌ |
|
|
24
|
+
| Detects native binaries | ✅ | ❌ |
|
|
25
|
+
| Detects corrupted package.json | ✅ | ❌ |
|
|
26
|
+
| Detects metadata anomalies | ✅ | ❌ |
|
|
21
27
|
| Zero dependencies | ✅ | N/A |
|
|
22
28
|
| Works offline | ✅ | ❌ |
|
|
23
|
-
| SARIF output (GitHub integration) | ✅ | ❌ |
|
|
29
|
+
| SARIF output (GitHub integration) [experimental] | ✅ | ❌ |
|
|
24
30
|
|
|
25
|
-
**Use both together** – `npm audit` for known vulnerabilities, `chain-audit` for detecting novel attacks.
|
|
31
|
+
**Use both together** – `npm audit` for known vulnerabilities, `chain-audit` (heuristic scanner) for detecting novel attacks and suspicious patterns.
|
|
26
32
|
|
|
27
33
|
## Installation
|
|
28
34
|
|
|
@@ -69,7 +75,10 @@ bun build src/index.js --compile --outfile chain-audit
|
|
|
69
75
|
## Quick Start
|
|
70
76
|
|
|
71
77
|
```bash
|
|
72
|
-
#
|
|
78
|
+
# Recommended: Thorough scan with detailed analysis
|
|
79
|
+
chain-audit --scan-code --detailed
|
|
80
|
+
|
|
81
|
+
# Scan current project (basic scan)
|
|
73
82
|
chain-audit
|
|
74
83
|
|
|
75
84
|
# Fail CI on high severity issues
|
|
@@ -84,7 +93,7 @@ chain-audit --severity critical,high --fail-on high
|
|
|
84
93
|
# JSON output for processing
|
|
85
94
|
chain-audit --json
|
|
86
95
|
|
|
87
|
-
# SARIF output for GitHub Code Scanning
|
|
96
|
+
# SARIF output for GitHub Code Scanning (experimental)
|
|
88
97
|
chain-audit --sarif > results.sarif
|
|
89
98
|
|
|
90
99
|
# Deep code analysis (slower but more thorough)
|
|
@@ -97,7 +106,7 @@ chain-audit --detailed --scan-code
|
|
|
97
106
|
chain-audit --detailed --json --scan-code
|
|
98
107
|
|
|
99
108
|
# Ignore specific packages and rules
|
|
100
|
-
chain-audit --ignore-packages "@types/*" --ignore-rules native_binary
|
|
109
|
+
chain-audit --ignore-packages "@types/*" --ignore-rules native_binary,executable_files
|
|
101
110
|
|
|
102
111
|
# Additional structure integrity checks
|
|
103
112
|
chain-audit --verify-integrity --fail-on high
|
|
@@ -120,7 +129,7 @@ chain-audit --check-typosquatting
|
|
|
120
129
|
| `-l, --lock <path>` | Path to lockfile (auto-detects npm, yarn, pnpm, bun) |
|
|
121
130
|
| `-c, --config <path>` | Path to config file (auto-detects if not specified) |
|
|
122
131
|
| `--json` | Output as JSON |
|
|
123
|
-
| `--sarif` | Output as SARIF (for GitHub Code Scanning) |
|
|
132
|
+
| `--sarif` | Output as SARIF (for GitHub Code Scanning) [experimental] |
|
|
124
133
|
| `-s, --severity <levels>` | Show only specified severity levels (comma-separated, e.g., `critical,high`) |
|
|
125
134
|
| `--fail-on <level>` | Exit 1 if max severity >= level |
|
|
126
135
|
| `--scan-code` | Deep scan JS files for suspicious patterns |
|
|
@@ -131,7 +140,7 @@ chain-audit --check-typosquatting
|
|
|
131
140
|
| `-f, --force` | Force overwrite existing config file (use with `--init`) |
|
|
132
141
|
| **Filtering Options** | |
|
|
133
142
|
| `-I, --ignore-packages <list>` | Ignore packages (comma-separated, supports globs, e.g., `@types/*,lodash`) |
|
|
134
|
-
| `-R, --ignore-rules <list>` | Ignore rule IDs (comma-separated, e.g., `native_binary,install_script`) |
|
|
143
|
+
| `-R, --ignore-rules <list>` | Ignore rule IDs (comma-separated, e.g., `native_binary,executable_files,install_script`) |
|
|
135
144
|
| `-T, --trust-packages <list>` | Trust packages (comma-separated, supports globs, e.g., `esbuild,@swc/*`) |
|
|
136
145
|
| **Scan Options** | |
|
|
137
146
|
| `--max-file-size <bytes>` | Max file size to scan (default: 1048576 = 1MB) |
|
|
@@ -145,11 +154,11 @@ chain-audit --check-typosquatting
|
|
|
145
154
|
|
|
146
155
|
| Level | Description | Example |
|
|
147
156
|
|-------|-------------|---------|
|
|
148
|
-
| `critical` | Highly likely malicious | Obfuscated code with network access,
|
|
157
|
+
| `critical` | Highly likely malicious | Obfuscated code with network access, version mismatch |
|
|
149
158
|
| `high` | Strong attack indicators | Suspicious install scripts with network/exec, typosquatting |
|
|
150
159
|
| `medium` | Warrants investigation | Install scripts, shell execution patterns |
|
|
151
160
|
| `low` | Informational | Native binaries, minimal metadata |
|
|
152
|
-
| `info` | Metadata only | Packages with install scripts that
|
|
161
|
+
| `info` | Metadata only | Packages with install scripts that are in trusted packages list (if configured) |
|
|
153
162
|
|
|
154
163
|
### Filtering by Severity
|
|
155
164
|
|
|
@@ -169,12 +178,13 @@ chain-audit --severity low,medium
|
|
|
169
178
|
chain-audit --severity critical,high --fail-on high
|
|
170
179
|
```
|
|
171
180
|
|
|
172
|
-
Issues will be displayed
|
|
181
|
+
Issues will be displayed sorted by severity (highest first), then by package name, grouped by severity level. When using `--severity`, only the specified severity levels are shown.
|
|
173
182
|
|
|
174
183
|
## Example Output
|
|
175
184
|
|
|
176
185
|
```
|
|
177
|
-
chain-audit v0.6.
|
|
186
|
+
chain-audit v0.6.5
|
|
187
|
+
Zero-dependency heuristic scanner CLI to detect supply chain attacks in node_modules
|
|
178
188
|
────────────────────────────────────────────────────────────
|
|
179
189
|
|
|
180
190
|
node_modules: /path/to/project/node_modules
|
|
@@ -186,9 +196,9 @@ Found 3 potential issue(s):
|
|
|
186
196
|
|
|
187
197
|
── CRITICAL ──
|
|
188
198
|
● evil-package@1.0.0
|
|
189
|
-
reason:
|
|
190
|
-
detail:
|
|
191
|
-
fix: Run `npm ci` to reinstall
|
|
199
|
+
reason: version_mismatch
|
|
200
|
+
detail: Installed version 1.0.0 does not match lockfile version 0.9.5
|
|
201
|
+
fix: Run `npm ci` to reinstall correct version
|
|
192
202
|
|
|
193
203
|
── HIGH ──
|
|
194
204
|
● suspic-lib@2.0.0
|
|
@@ -198,9 +208,9 @@ Found 3 potential issue(s):
|
|
|
198
208
|
|
|
199
209
|
── MEDIUM ──
|
|
200
210
|
● some-addon@1.2.3
|
|
201
|
-
reason:
|
|
202
|
-
detail:
|
|
203
|
-
fix:
|
|
211
|
+
reason: extraneous_package
|
|
212
|
+
detail: Package exists in node_modules but is missing from lockfile
|
|
213
|
+
fix: Run `npm ci` to reinstall from lockfile
|
|
204
214
|
|
|
205
215
|
────────────────────────────────────────────────────────────
|
|
206
216
|
Summary:
|
|
@@ -223,10 +233,10 @@ When `--detailed` is enabled, each finding includes:
|
|
|
223
233
|
- **Matched patterns** (regex) that triggered the detection
|
|
224
234
|
- **Package metadata**: author, repository URL, license, homepage, full file path
|
|
225
235
|
- **Trust assessment**: trust score (0-100) and trust level (low/medium/high)
|
|
226
|
-
- **Evidence**: file paths, line numbers, column numbers, matched text
|
|
236
|
+
- **Evidence**: file paths, line numbers, column numbers (in matches array), matched text
|
|
227
237
|
- **False positive hints**: guidance on legitimate uses that might trigger the detection
|
|
228
|
-
- **Verification steps**: actionable steps for manual investigation
|
|
229
|
-
- **Risk assessment**: for critical findings, notes about known attack patterns
|
|
238
|
+
- **Verification steps**: actionable steps for manual investigation (available for some findings like typosquatting)
|
|
239
|
+
- **Risk assessment**: for high/critical findings, notes about known attack patterns
|
|
230
240
|
|
|
231
241
|
### Trust Score Calculation
|
|
232
242
|
|
|
@@ -234,17 +244,17 @@ The trust score (0-100) is calculated based on multiple factors:
|
|
|
234
244
|
|
|
235
245
|
| Factor | Points | Description |
|
|
236
246
|
|--------|--------|-------------|
|
|
237
|
-
| **Trusted scope** | +40 | Package is from a scope
|
|
238
|
-
| **Known legitimate** | +50 | Package is in the
|
|
247
|
+
| **Trusted scope** | +40 | Package is from a scope in the internal trusted scopes list (currently empty by default) |
|
|
248
|
+
| **Known legitimate** | +50 | Package is in the internal known legitimate packages list (currently empty by default) |
|
|
239
249
|
| **Has repository** | +20 | Package has a repository URL in package.json |
|
|
240
250
|
| **Has homepage** | +10 | Package has a homepage URL |
|
|
241
251
|
| **Has author** | +10 | Package has author information |
|
|
242
252
|
| **Has license** | +10 | Package has a license field |
|
|
243
253
|
|
|
244
|
-
**Note:**
|
|
254
|
+
**Note:** Trust score is calculated independently from the `trustedPackages` config option. The `trustedPackages` config option affects severity levels for install scripts, but does not influence the trust score calculation. By default, no packages are whitelisted in the trust score calculation. All packages are checked with equal severity.
|
|
245
255
|
|
|
246
256
|
**Trust Levels:**
|
|
247
|
-
- **High (70-100)**: Package is likely legitimate (e.g.,
|
|
257
|
+
- **High (70-100)**: Package is likely legitimate (e.g., has repository, homepage, author, and license)
|
|
248
258
|
- **Medium (40-69)**: Package has some trust indicators but needs verification
|
|
249
259
|
- **Low (0-39)**: Package lacks trust indicators, warrants closer investigation
|
|
250
260
|
|
|
@@ -306,14 +316,16 @@ Alternatively, you can manually create a config file in your project root. Suppo
|
|
|
306
316
|
],
|
|
307
317
|
"trustedPatterns": {
|
|
308
318
|
"node-gyp rebuild": true,
|
|
309
|
-
"prebuild-install": true
|
|
319
|
+
"prebuild-install": true,
|
|
320
|
+
"node-pre-gyp": true
|
|
310
321
|
},
|
|
311
|
-
"scanCode":
|
|
322
|
+
"scanCode": true,
|
|
312
323
|
"checkTyposquatting": false,
|
|
324
|
+
"checkLockfile": false,
|
|
313
325
|
"failOn": "high",
|
|
314
326
|
"severity": ["critical", "high"],
|
|
315
327
|
"format": "text",
|
|
316
|
-
"verbose":
|
|
328
|
+
"verbose": true,
|
|
317
329
|
"maxFileSizeForCodeScan": 1048576,
|
|
318
330
|
"maxNestedDepth": 10,
|
|
319
331
|
"maxFilesPerPackage": 0,
|
|
@@ -329,12 +341,13 @@ Alternatively, you can manually create a config file in your project root. Suppo
|
|
|
329
341
|
| `ignoredRules` | `string[]` | `[]` | Rule IDs to ignore |
|
|
330
342
|
| `trustedPackages` | `string[]` | `[]` | Packages with reduced severity for install scripts (empty by default - all packages are checked) |
|
|
331
343
|
| `trustedPatterns` | `object` | `{node-gyp rebuild: true, ...}` | Install script patterns considered safe |
|
|
332
|
-
| `scanCode` | `boolean` | `false` | Enable deep code scanning
|
|
344
|
+
| `scanCode` | `boolean` | `false` | Enable deep code scanning (default: `false` without config, `true` when generated with `--init`) |
|
|
333
345
|
| `checkTyposquatting` | `boolean` | `false` | Enable typosquatting detection (disabled by default to reduce false positives) |
|
|
346
|
+
| `checkLockfile` | `boolean` | `false` | Enable lockfile integrity checks (disabled by default due to possible false positives) |
|
|
334
347
|
| `failOn` | `string` | `null` | Default fail threshold (`info\|low\|medium\|high\|critical`) |
|
|
335
348
|
| `severity` | `string[]` | `null` | Show only specified severity levels (e.g., `["critical", "high"]`) |
|
|
336
|
-
| `format` | `string` | `"text"` | Output format: `text`, `json`, or `sarif` |
|
|
337
|
-
| `verbose` | `boolean` | `false` | Show detailed analysis with code snippets and trust scores (Note: CLI flag is `--detailed`, but config uses `verbose` for consistency
|
|
349
|
+
| `format` | `string` | `"text"` | Output format: `text`, `json`, or `sarif` (sarif is experimental) |
|
|
350
|
+
| `verbose` | `boolean` | `false` | Show detailed analysis with code snippets and trust scores (default: `false` without config, `true` when generated with `--init`). Note: CLI flag is `--detailed`, but config uses `verbose` for consistency |
|
|
338
351
|
| `maxFileSizeForCodeScan` | `number` | `1048576` | Max file size (bytes) to scan for code patterns |
|
|
339
352
|
| `maxNestedDepth` | `number` | `10` | Max depth to traverse nested node_modules |
|
|
340
353
|
| `maxFilesPerPackage` | `number` | `0` | Max JS files to scan per package (0 = unlimited) |
|
|
@@ -372,7 +385,7 @@ jobs:
|
|
|
372
385
|
run: npm rebuild
|
|
373
386
|
```
|
|
374
387
|
|
|
375
|
-
### With SARIF Upload (GitHub Code Scanning)
|
|
388
|
+
### With SARIF Upload (GitHub Code Scanning) [experimental]
|
|
376
389
|
|
|
377
390
|
```yaml
|
|
378
391
|
name: Security Scan
|
|
@@ -429,7 +442,7 @@ jobs:
|
|
|
429
442
|
- `node-modules-path` (default: `./node_modules`) – Path to node_modules directory
|
|
430
443
|
- `fail-on` (default: `high`) – Severity threshold to fail on (info|low|medium|high|critical)
|
|
431
444
|
- `scan-code` (default: `false`) – Enable deep code scanning (slower)
|
|
432
|
-
- `upload-sarif` (default: `true`) – Upload SARIF to GitHub Code Scanning
|
|
445
|
+
- `upload-sarif` (default: `true`) – Upload SARIF to GitHub Code Scanning [experimental]
|
|
433
446
|
|
|
434
447
|
The reusable workflow automatically uses `--ignore-scripts` for safe installation.
|
|
435
448
|
|
|
@@ -448,7 +461,7 @@ The reusable workflow automatically uses `--ignore-scripts` for safe installatio
|
|
|
448
461
|
|
|
449
462
|
### CI/CD Security Best Practices
|
|
450
463
|
|
|
451
|
-
Supply chain attacks
|
|
464
|
+
Supply chain attacks have exploited misconfigured GitHub Actions. **Protect your CI/CD:**
|
|
452
465
|
|
|
453
466
|
```yaml
|
|
454
467
|
# DANGEROUS - Don't use pull_request_target with checkout
|
|
@@ -479,44 +492,48 @@ chain-audit automatically detects and parses:
|
|
|
479
492
|
|
|
480
493
|
| Lockfile | Package Manager |
|
|
481
494
|
|----------|-----------------|
|
|
482
|
-
| `package-lock.json` | npm v2/v3 |
|
|
483
|
-
| `npm-shrinkwrap.json` | npm |
|
|
495
|
+
| `package-lock.json` | npm v2/v3 (v1 supported as fallback) |
|
|
496
|
+
| `npm-shrinkwrap.json` | npm (v1/v2/v3) |
|
|
484
497
|
| `yarn.lock` | Yarn Classic & Berry |
|
|
485
498
|
| `pnpm-lock.yaml` | pnpm |
|
|
486
|
-
| `bun.lock` | Bun |
|
|
499
|
+
| `bun.lock` | Bun (text format) |
|
|
500
|
+
| `bun.lockb` | Bun (binary format, not supported - use `bun install --save-text-lockfile` to generate text format) |
|
|
487
501
|
|
|
488
502
|
## Detection Rules
|
|
489
503
|
|
|
490
504
|
### Critical Severity
|
|
491
|
-
- **
|
|
492
|
-
- **
|
|
505
|
+
- **version_mismatch** – Installed version differs from lockfile (requires `--check-lockfile`)
|
|
506
|
+
- **pipe_to_shell** – Script pipes content to shell (`| bash`)
|
|
507
|
+
- **potential_env_exfiltration** – Env access + network in install script
|
|
508
|
+
|
|
509
|
+
### High Severity
|
|
493
510
|
- **corrupted_package_json** – Package has malformed or unreadable package.json
|
|
511
|
+
- **network_access_script** – Install script with curl/wget/fetch patterns (high for install scripts, low for trusted install scripts, medium/low for others)
|
|
512
|
+
- **potential_typosquat** – Package name similar to popular package (requires `--check-typosquatting`)
|
|
513
|
+
- **suspicious_name_pattern** – Package name uses character substitution (l33t speak) or prefix patterns (requires `--check-typosquatting`) (high for character substitution, medium for prefix patterns)
|
|
514
|
+
- **eval_usage** – Code uses eval() or new Function() (requires `--scan-code`)
|
|
515
|
+
- **sensitive_path_access** – Code accesses ~/.ssh, ~/.aws, etc. (requires `--scan-code`)
|
|
516
|
+
- **shell_execution** – Script executes shell commands (high for install scripts, medium/low for others)
|
|
494
517
|
|
|
495
518
|
### High Severity (with `--verify-integrity`)
|
|
496
519
|
- **package_name_mismatch** – Package name in package.json doesn't match expected from path
|
|
497
520
|
- **suspicious_resolved_url** – Package resolved from local file or suspicious URL
|
|
498
|
-
- **pipe_to_shell** – Script pipes content to shell (`| bash`)
|
|
499
|
-
- **potential_env_exfiltration** – Env access + network in install script
|
|
500
|
-
- **env_with_network** – Code accesses env vars and has network/exec capabilities
|
|
501
|
-
- **obfuscated_code** – Base64/hex encoded strings, char code arrays
|
|
502
521
|
|
|
503
|
-
###
|
|
504
|
-
- **
|
|
505
|
-
- **
|
|
506
|
-
- **suspicious_name_pattern** – Package name uses character substitution (l33t speak) (requires `--check-typosquatting`)
|
|
507
|
-
- **eval_usage** – Code uses eval() or new Function()
|
|
508
|
-
- **sensitive_path_access** – Code accesses ~/.ssh, ~/.aws, etc.
|
|
509
|
-
- **shell_execution** – Script executes shell commands
|
|
522
|
+
### Critical Severity (with `--scan-code`)
|
|
523
|
+
- **obfuscated_code** – Base64/hex encoded strings, char code arrays
|
|
524
|
+
- **env_with_network** – Code accesses env vars and has network/exec capabilities (critical when network access is present, medium for install scripts without network)
|
|
510
525
|
|
|
511
526
|
### Medium Severity
|
|
512
|
-
- **
|
|
513
|
-
- **
|
|
527
|
+
- **extraneous_package** – Package in node_modules not in lockfile (requires `--check-lockfile`)
|
|
528
|
+
- **install_script** – Has preinstall/install/postinstall script (medium, or info/low for trusted packages/patterns)
|
|
529
|
+
- **code_execution** – Script runs code via node -e, python -c, etc. (high for install scripts, medium/low for others)
|
|
514
530
|
- **child_process_usage** – Code uses child_process module
|
|
515
531
|
- **node_network_access** – Code uses Node.js network APIs (fetch, https, axios)
|
|
516
532
|
- **git_operation_install** – Install script performs git operations
|
|
533
|
+
- **executable_files** – Contains executable files (shell scripts, etc.) - high if outside bin/, low if in bin/
|
|
517
534
|
|
|
518
535
|
### Low/Info Severity
|
|
519
|
-
- **native_binary** – Contains .node, .so, .
|
|
536
|
+
- **native_binary** – Contains native module binaries (.node, .so, .dylib files)
|
|
520
537
|
- **no_repository** – No repository URL in package.json
|
|
521
538
|
- **minimal_metadata** – Very short/missing description
|
|
522
539
|
|
|
@@ -525,11 +542,13 @@ chain-audit automatically detects and parses:
|
|
|
525
542
|
```javascript
|
|
526
543
|
const { run } = require('chain-audit');
|
|
527
544
|
|
|
528
|
-
const result =
|
|
545
|
+
const result = run(['node', 'script.js', '--json', '--fail-on', 'high']);
|
|
529
546
|
|
|
530
547
|
console.log(result.exitCode); // 0 or 1
|
|
531
|
-
console.log(result.issues); // Array of issues found
|
|
532
|
-
console.log(result.summary); // { counts: {...}, maxSeverity: 'high' }
|
|
548
|
+
console.log(result.issues); // Array of all issues found (not filtered by --severity)
|
|
549
|
+
console.log(result.summary); // { counts: {...}, maxSeverity: 'high' } (calculated from filtered issues if --severity is used)
|
|
550
|
+
|
|
551
|
+
// Note: run() also outputs to console.log() by default. Use --json format to get structured output.
|
|
533
552
|
```
|
|
534
553
|
|
|
535
554
|
## Best Practices
|
|
@@ -565,7 +584,7 @@ npm rebuild
|
|
|
565
584
|
3. **Run in sandboxed CI** – Isolate potentially malicious code
|
|
566
585
|
4. **Combine with npm audit** – chain-audit detects different threats
|
|
567
586
|
5. **Review all findings** – Some may be false positives
|
|
568
|
-
6. **Use `--scan-code`
|
|
587
|
+
6. **Recommended: Use `--scan-code --detailed` for thorough analysis** – Deep code scanning with detailed evidence (slower but most comprehensive)
|
|
569
588
|
7. **Use `--detailed` for manual investigation** – Get code snippets and trust assessment to distinguish false positives (`--verbose` is an alias)
|
|
570
589
|
8. **Keep registry secure** – Use private registry or npm audit signatures
|
|
571
590
|
9. **All packages are checked equally** – No packages are whitelisted by default. Even popular packages like `sharp`, `esbuild`, or `@babel/*` are checked for malicious patterns. This ensures that compromised packages are detected regardless of their reputation.
|
|
@@ -598,7 +617,13 @@ Hubert Kasperek
|
|
|
598
617
|
|
|
599
618
|
---
|
|
600
619
|
|
|
601
|
-
**Disclaimer:** chain-audit is a heuristic scanner created for **educational and research purposes**, provided "AS IS" without warranty of any kind.
|
|
620
|
+
**Disclaimer:** chain-audit is a **heuristic scanner** created for **educational and research purposes**, licensed under **MIT License**, provided "AS IS" without warranty of any kind. The author makes no guarantees about the tool's accuracy, completeness, or reliability.
|
|
621
|
+
|
|
622
|
+
**Important limitations:**
|
|
623
|
+
- chain-audit does **not** detect 100% confirmed attacks – it scans code for suspicious patterns
|
|
624
|
+
- It may produce **many false positives** – findings require human analysis to determine if they're actually suspicious
|
|
625
|
+
- It **cannot catch all attacks** – sophisticated or novel attack patterns may be missed
|
|
626
|
+
- The tool flags potentially suspicious behavior, but **you are responsible** for reviewing and validating all findings
|
|
602
627
|
|
|
603
628
|
**The author takes no responsibility for:**
|
|
604
629
|
- False positives or false negatives in detection
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chain-audit",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.5",
|
|
4
|
+
"description": "Zero-dependency heuristic scanner CLI to detect supply chain attacks in node_modules. Scans for malicious install scripts, typosquatting, extraneous packages, lockfile integrity, obfuscated code, executable files, and suspicious code patterns.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"chain-audit": "src/index.js"
|
|
@@ -31,17 +31,12 @@
|
|
|
31
31
|
"keywords": [
|
|
32
32
|
"security",
|
|
33
33
|
"supply-chain",
|
|
34
|
-
"supply-chain-attack",
|
|
35
|
-
"node-modules",
|
|
36
|
-
"audit",
|
|
37
|
-
"malware",
|
|
38
34
|
"dependency",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"sarif"
|
|
35
|
+
"heuristic-scanner",
|
|
36
|
+
"lockfile",
|
|
37
|
+
"obfuscation",
|
|
38
|
+
"static-analysis",
|
|
39
|
+
"ci"
|
|
45
40
|
],
|
|
46
41
|
"author": "Hubert Kasperek",
|
|
47
42
|
"license": "MIT",
|
package/src/analyzer.js
CHANGED
|
@@ -91,7 +91,7 @@ const CHILD_PROCESS_PATTERNS = [
|
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Node.js network/HTTP patterns (for code scanning)
|
|
94
|
-
* These are used for data exfiltration in
|
|
94
|
+
* These are used for data exfiltration in supply chain attacks
|
|
95
95
|
*/
|
|
96
96
|
const NODE_NETWORK_PATTERNS = [
|
|
97
97
|
/\bfetch\s*\(/,
|
|
@@ -164,9 +164,10 @@ const OBFUSCATION_PATTERNS = [
|
|
|
164
164
|
];
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
-
* Native binary extensions
|
|
167
|
+
* Native binary extensions (legitimate native modules)
|
|
168
|
+
* Note: .exe and .dll are checked separately as executable_files (high severity)
|
|
168
169
|
*/
|
|
169
|
-
const NATIVE_EXTENSIONS = ['.node', '.so', '.
|
|
170
|
+
const NATIVE_EXTENSIONS = ['.node', '.so', '.dylib'];
|
|
170
171
|
|
|
171
172
|
/**
|
|
172
173
|
* Git/version control patterns (lower risk but worth noting)
|
|
@@ -244,6 +245,9 @@ function analyzePackage(pkg, lockIndex, config = {}) {
|
|
|
244
245
|
// 4. Check for native binaries
|
|
245
246
|
checkNativeBinaries(pkg, issues, verbose);
|
|
246
247
|
|
|
248
|
+
// 4.5. Check for executable files (shell scripts, etc.) - potential supply chain attack
|
|
249
|
+
checkExecutableFiles(pkg, issues, verbose, config);
|
|
250
|
+
|
|
247
251
|
// 5. Check for typosquatting (optional, enabled with --check-typosquatting)
|
|
248
252
|
if (config.checkTyposquatting) {
|
|
249
253
|
checkTyposquatting(pkg, issues, verbose);
|
|
@@ -941,7 +945,7 @@ function analyzeScriptContent(script, scriptName, isInstall, isTrusted, issues,
|
|
|
941
945
|
'Legitimate uses: sending analytics with env-based config',
|
|
942
946
|
'Check if sensitive env vars (API keys, tokens) could be accessed',
|
|
943
947
|
],
|
|
944
|
-
riskAssessment: 'CRITICAL - Matches
|
|
948
|
+
riskAssessment: 'CRITICAL - Matches known credential exfiltration attack patterns',
|
|
945
949
|
};
|
|
946
950
|
}
|
|
947
951
|
|
|
@@ -1027,6 +1031,177 @@ function findNativeArtifacts(pkgDir, maxDepth = 3) {
|
|
|
1027
1031
|
return found;
|
|
1028
1032
|
}
|
|
1029
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Check for executable files (shell scripts, etc.) - potential supply chain attack
|
|
1036
|
+
*
|
|
1037
|
+
* NOTE: All packages are checked. Users can ignore this rule for specific packages
|
|
1038
|
+
* using --ignore-rules executable_files or by configuring ignoredRules in .chainauditrc.json
|
|
1039
|
+
*/
|
|
1040
|
+
function checkExecutableFiles(pkg, issues, verbose = false, config = {}) {
|
|
1041
|
+
// Check if this rule is ignored
|
|
1042
|
+
if (config.ignoredRules && config.ignoredRules.includes('executable_files')) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const found = findExecutableFiles(pkg.dir, 5);
|
|
1046
|
+
|
|
1047
|
+
if (found.length > 0) {
|
|
1048
|
+
// Check if files are in bin/ directory (expected) or root/other locations (suspicious)
|
|
1049
|
+
const binFiles = found.filter(f => /^bin\//i.test(path.relative(pkg.dir, f)));
|
|
1050
|
+
const suspiciousFiles = found.filter(f => !/^bin\//i.test(path.relative(pkg.dir, f)));
|
|
1051
|
+
|
|
1052
|
+
// Determine severity:
|
|
1053
|
+
// - HIGH: files outside bin/ (very suspicious - matches attack patterns)
|
|
1054
|
+
// - LOW: all files in bin/ (less suspicious - some packages use shell scripts for installation)
|
|
1055
|
+
const severity = suspiciousFiles.length > 0 ? 'high' : 'low';
|
|
1056
|
+
|
|
1057
|
+
const listed = found.slice(0, 5).map(p => {
|
|
1058
|
+
const relPath = path.relative(pkg.dir, p);
|
|
1059
|
+
return relPath;
|
|
1060
|
+
}).join(', ');
|
|
1061
|
+
|
|
1062
|
+
const issue = {
|
|
1063
|
+
severity: severity,
|
|
1064
|
+
reason: 'executable_files',
|
|
1065
|
+
detail: `Contains executable files (shell scripts, etc.): ${listed}${found.length > 5 ? `, +${found.length - 5} more` : ''}`,
|
|
1066
|
+
recommendation: suspiciousFiles.length > 0
|
|
1067
|
+
? 'Executable files outside bin/ directory are suspicious and may indicate a supply chain attack. Review immediately.'
|
|
1068
|
+
: 'Shell scripts in bin/ directory are less suspicious but should be reviewed for malicious content.',
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
if (verbose) {
|
|
1072
|
+
issue.verbose = {
|
|
1073
|
+
evidence: {
|
|
1074
|
+
executableCount: found.length,
|
|
1075
|
+
binFilesCount: binFiles.length,
|
|
1076
|
+
suspiciousFilesCount: suspiciousFiles.length,
|
|
1077
|
+
executableFiles: found.map(f => {
|
|
1078
|
+
const relPath = path.relative(pkg.dir, f);
|
|
1079
|
+
const isInBin = /^bin\//i.test(relPath);
|
|
1080
|
+
return {
|
|
1081
|
+
path: f,
|
|
1082
|
+
relativePath: relPath,
|
|
1083
|
+
filename: path.basename(f),
|
|
1084
|
+
extension: path.extname(f),
|
|
1085
|
+
isInBin: isInBin,
|
|
1086
|
+
isSuspicious: !isInBin,
|
|
1087
|
+
};
|
|
1088
|
+
}),
|
|
1089
|
+
},
|
|
1090
|
+
falsePositiveHints: [
|
|
1091
|
+
suspiciousFiles.length > 0 ? '⚠ Executable files outside bin/ are highly suspicious' : null,
|
|
1092
|
+
'Shell scripts (.sh) in npm packages are unusual and should be reviewed',
|
|
1093
|
+
'Check if executables are necessary for package functionality',
|
|
1094
|
+
'Review executable content for malicious code (network access, credential exfiltration)',
|
|
1095
|
+
binFiles.length > 0 && suspiciousFiles.length === 0 ? '✓ Shell scripts in bin/ are less suspicious (some packages use them for installation)' : null,
|
|
1096
|
+
'To ignore this check for specific packages, use --ignore-rules executable_files',
|
|
1097
|
+
].filter(Boolean),
|
|
1098
|
+
riskAssessment: suspiciousFiles.length > 0
|
|
1099
|
+
? 'HIGH - Executable files outside bin/ match supply chain attack patterns'
|
|
1100
|
+
: 'LOW - Shell scripts in bin/ are less suspicious but should be reviewed',
|
|
1101
|
+
attackPattern: 'Executable files (especially .sh scripts) in npm packages can be used for supply chain attacks',
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
issues.push(issue);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Find executable files in package (shell scripts, PowerShell, binaries, etc.)
|
|
1111
|
+
* Note: .js files are scanned by code scanner, not flagged here
|
|
1112
|
+
*/
|
|
1113
|
+
function findExecutableFiles(pkgDir, maxDepth = 5) {
|
|
1114
|
+
const found = [];
|
|
1115
|
+
const stack = [{ dir: pkgDir, depth: 0 }];
|
|
1116
|
+
|
|
1117
|
+
// Executable file extensions - only real executables, not .js (those are scanned by code scanner)
|
|
1118
|
+
const EXECUTABLE_EXTENSIONS = [
|
|
1119
|
+
// Shell scripts
|
|
1120
|
+
'.sh', '.bash', '.zsh', '.fish', '.csh', '.tcsh', '.ksh',
|
|
1121
|
+
// PowerShell
|
|
1122
|
+
'.ps1', '.psm1', '.psd1',
|
|
1123
|
+
// Windows batch
|
|
1124
|
+
'.bat', '.cmd', '.com',
|
|
1125
|
+
// Binaries (executables)
|
|
1126
|
+
'.exe', '.bin',
|
|
1127
|
+
// Other scripts
|
|
1128
|
+
'.py', '.pl', '.rb', '.php', '.r', '.lua',
|
|
1129
|
+
];
|
|
1130
|
+
|
|
1131
|
+
while (stack.length > 0) {
|
|
1132
|
+
const { dir, depth } = stack.pop();
|
|
1133
|
+
if (depth > maxDepth) continue;
|
|
1134
|
+
|
|
1135
|
+
let entries = [];
|
|
1136
|
+
try {
|
|
1137
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1138
|
+
} catch {
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
for (const entry of entries) {
|
|
1143
|
+
// Skip common directories that shouldn't be checked
|
|
1144
|
+
if (entry.name.startsWith('.') ||
|
|
1145
|
+
entry.name === 'node_modules' ||
|
|
1146
|
+
entry.name === '.git') {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const fullPath = path.join(dir, entry.name);
|
|
1151
|
+
|
|
1152
|
+
if (entry.isDirectory()) {
|
|
1153
|
+
stack.push({ dir: fullPath, depth: depth + 1 });
|
|
1154
|
+
} else if (entry.isFile()) {
|
|
1155
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
1156
|
+
|
|
1157
|
+
// Check by extension - only real executables, not .js files
|
|
1158
|
+
// .js files are already scanned by code scanner
|
|
1159
|
+
if (EXECUTABLE_EXTENSIONS.includes(ext)) {
|
|
1160
|
+
found.push(fullPath);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Check for files without extension that are executable
|
|
1165
|
+
// Only flag if they have shebang (not just chmod +x, which can be accidental)
|
|
1166
|
+
// Skip known text files (LICENSE, README, CHANGELOG, etc.)
|
|
1167
|
+
if (ext === '') {
|
|
1168
|
+
const filename = entry.name.toUpperCase();
|
|
1169
|
+
const knownTextFiles = ['LICENSE', 'LICENCE', 'README', 'CHANGELOG', 'HISTORY', 'AUTHORS', 'CONTRIBUTORS', 'NOTICE', 'COPYING', 'INSTALL', 'MAKEFILE', 'MAKEFILE.IN'];
|
|
1170
|
+
|
|
1171
|
+
// Skip known text files
|
|
1172
|
+
if (knownTextFiles.some(tf => filename === tf || filename.startsWith(tf + '.'))) {
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Check if file has execute permission AND shebang (not just chmod +x)
|
|
1177
|
+
try {
|
|
1178
|
+
const stat = fs.statSync(fullPath);
|
|
1179
|
+
if ((stat.mode & 0o111) !== 0) {
|
|
1180
|
+
// File is executable - check if it has shebang (real executable script)
|
|
1181
|
+
try {
|
|
1182
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
1183
|
+
const firstLine = content.split('\n')[0];
|
|
1184
|
+
// Only flag if it has shebang (#!/bin/bash, #!/usr/bin/env python, etc.)
|
|
1185
|
+
if (/^#!/.test(firstLine)) {
|
|
1186
|
+
found.push(fullPath);
|
|
1187
|
+
}
|
|
1188
|
+
} catch {
|
|
1189
|
+
// If we can't read content, skip (might be binary)
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
} catch {
|
|
1194
|
+
// Skip files we can't check
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
return found;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1030
1205
|
/**
|
|
1031
1206
|
* Trusted npm organizations/scopes (official packages, not typosquatting)
|
|
1032
1207
|
*
|
|
@@ -1610,8 +1785,8 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
1610
1785
|
// Check if "eval" is part of a longer word (not a function call)
|
|
1611
1786
|
const matchText = patternMatch[0];
|
|
1612
1787
|
const matchIndex = patternMatch.index;
|
|
1613
|
-
const beforeMatch = content.slice(Math.max(0, matchIndex -
|
|
1614
|
-
const afterMatch = content.slice(matchIndex + matchText.length, matchIndex + matchText.length +
|
|
1788
|
+
const beforeMatch = content.slice(Math.max(0, matchIndex - 30), matchIndex);
|
|
1789
|
+
const afterMatch = content.slice(matchIndex + matchText.length, matchIndex + matchText.length + 30);
|
|
1615
1790
|
|
|
1616
1791
|
// Check if it's part of a longer identifier (e.g., "unevaluated", "evaluate", "evaluation")
|
|
1617
1792
|
// Also check for common false positives like "unevaluatedProperties", "evaluateExpression", etc.
|
|
@@ -1622,6 +1797,17 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
1622
1797
|
continue; // Skip - it's part of a variable/function name, not eval() call
|
|
1623
1798
|
}
|
|
1624
1799
|
|
|
1800
|
+
// Check if it's a method definition (async eval, function eval, const eval =, etc.)
|
|
1801
|
+
// or method call on object (this.redis.eval, client.eval, etc.)
|
|
1802
|
+
// These are NOT JavaScript eval() calls
|
|
1803
|
+
const isMethodDefinition = /\b(async|function|const|let|var|class|static|get|set)\s+eval\s*\(/i.test(beforeMatch.slice(-20) + matchText);
|
|
1804
|
+
const isMethodCall = /\.eval\s*\(/.test(beforeMatch.slice(-10) + matchText);
|
|
1805
|
+
const isPropertyAccess = /\.eval\s*[=:]/.test(beforeMatch.slice(-10) + matchText + afterMatch.slice(0, 5));
|
|
1806
|
+
|
|
1807
|
+
if (isMethodDefinition || isMethodCall || isPropertyAccess) {
|
|
1808
|
+
continue; // Skip - it's a method name (e.g., Redis eval, async eval method), not JavaScript eval()
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1625
1811
|
// Skip if it's in a test directory or test file (tests often use eval for mocking)
|
|
1626
1812
|
const isTestFile = /(?:^|\/)(?:test|spec|__tests__|__mocks__)(?:\/|$)/i.test(relativePath) ||
|
|
1627
1813
|
/(?:^|\/)(?:test|spec)\.(js|mjs|cjs)$/i.test(relativePath);
|
|
@@ -1744,6 +1930,7 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
1744
1930
|
codeSnippet: snippet?.snippet || null,
|
|
1745
1931
|
lineNumber: snippet?.lineNumber || null,
|
|
1746
1932
|
matchedText: snippet?.matchedText || null,
|
|
1933
|
+
isMinified: snippet?.isMinified || false,
|
|
1747
1934
|
falsePositiveHints: [
|
|
1748
1935
|
isTestFile ? '✓ This appears to be a test file - eval usage for mocking is common' : null,
|
|
1749
1936
|
isMinifiedOrBundled ? '✓ This appears to be a minified/bundled file - eval patterns may be false positives' : null,
|
|
@@ -1876,6 +2063,7 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
1876
2063
|
codeSnippet: snippet?.snippet || null,
|
|
1877
2064
|
lineNumber: snippet?.lineNumber || null,
|
|
1878
2065
|
matchedText: snippet?.matchedText || null,
|
|
2066
|
+
isMinified: snippet?.isMinified || false,
|
|
1879
2067
|
falsePositiveHints: [
|
|
1880
2068
|
isBuildFile ? '✓ This appears to be a build file - child_process usage is common' : null,
|
|
1881
2069
|
'Build tools commonly use child_process (webpack, babel plugins)',
|
|
@@ -1944,6 +2132,7 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
1944
2132
|
codeSnippet: snippet?.snippet || null,
|
|
1945
2133
|
lineNumber: snippet?.lineNumber || null,
|
|
1946
2134
|
matchedText: snippet?.matchedText || null,
|
|
2135
|
+
isMinified: snippet?.isMinified || false,
|
|
1947
2136
|
falsePositiveHints: [
|
|
1948
2137
|
'SSH/Git tools may legitimately access ~/.ssh',
|
|
1949
2138
|
'AWS SDK wrappers may check ~/.aws for config',
|
|
@@ -1958,7 +2147,7 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
1958
2147
|
}
|
|
1959
2148
|
}
|
|
1960
2149
|
|
|
1961
|
-
// Check for Node.js network patterns (
|
|
2150
|
+
// Check for Node.js network patterns (common in supply chain attacks)
|
|
1962
2151
|
// Skip if package is clearly an HTTP client library or network-related utility
|
|
1963
2152
|
const isHttpClient = pkg && /^(axios|got|node-fetch|undici|ky|superagent|request|needle|phin|bent|httpie|type-check|xmlhttprequest)/i.test(pkg.name);
|
|
1964
2153
|
|
|
@@ -2032,6 +2221,7 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
2032
2221
|
codeSnippet: snippet?.snippet || null,
|
|
2033
2222
|
lineNumber: snippet?.lineNumber || null,
|
|
2034
2223
|
matchedText: snippet?.matchedText || null,
|
|
2224
|
+
isMinified: snippet?.isMinified || false,
|
|
2035
2225
|
falsePositiveHints: [
|
|
2036
2226
|
isTestFile ? '✓ This appears to be a test file - network mocking is common' : null,
|
|
2037
2227
|
'HTTP client libraries (axios, got, fetch) are common',
|
|
@@ -2210,21 +2400,60 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
2210
2400
|
}
|
|
2211
2401
|
}
|
|
2212
2402
|
|
|
2213
|
-
//
|
|
2214
|
-
//
|
|
2215
|
-
|
|
2403
|
+
// Analyze context: is env actually passed to network/exec, or just used locally?
|
|
2404
|
+
// Use broader context for analysis (envContext was already defined earlier, but we need more context)
|
|
2405
|
+
const broaderEnvContext = content.slice(Math.max(0, envMatchIndex - 150), Math.min(content.length, envMatchIndex + 300));
|
|
2406
|
+
const envPassedToExec = /(?:env|process\.env).*[:=].*(?:spawn|exec|fork|child_process)/i.test(broaderEnvContext) ||
|
|
2407
|
+
/(?:spawn|exec|fork).*\{[^}]*env/i.test(broaderEnvContext) ||
|
|
2408
|
+
/(?:spawn|exec|fork).*\{[^}]*\.\.\.process\.env/i.test(broaderEnvContext);
|
|
2409
|
+
|
|
2410
|
+
// Check if env is used only for local config (less suspicious)
|
|
2411
|
+
const envUsedLocally = /process\.env\.(?:NODE_ENV|DEBUG|CI|FORCE_COLOR|NO_COLOR|TZ|LANG|LC_|HOME|USER|PATH|SHELL|PWD|HADOOP_HOME|LIBC|FORMAT_START)/i.test(envContext);
|
|
2412
|
+
|
|
2413
|
+
// Check if it's just passing entire process.env to child_process (common pattern)
|
|
2414
|
+
const passesFullEnv = /(?:env|process\.env).*[:=].*process\.env|\.\.\.process\.env/i.test(broaderEnvContext);
|
|
2415
|
+
|
|
2416
|
+
// Determine severity based on context - no package is trusted by default
|
|
2417
|
+
// CRITICAL: If there's network access, always critical (most dangerous pattern)
|
|
2418
|
+
// This is a known supply chain attack pattern: env + network + exec
|
|
2216
2419
|
let severity = 'critical';
|
|
2217
|
-
|
|
2218
|
-
|
|
2420
|
+
|
|
2421
|
+
// Only lower severity if there's NO network access (only child_process)
|
|
2422
|
+
// If network is present, it's always critical - could be exfiltrating credentials
|
|
2423
|
+
const hasNetwork = networkMatch || nodeNetworkMatch;
|
|
2424
|
+
|
|
2425
|
+
if (hasNetwork) {
|
|
2426
|
+
// env + network + exec = CRITICAL (supply chain attack pattern)
|
|
2427
|
+
severity = 'critical';
|
|
2428
|
+
} else if (isInstallScriptFile) {
|
|
2429
|
+
// Install scripts with env + child_process (no network) - medium
|
|
2430
|
+
severity = 'medium';
|
|
2431
|
+
} else if (envUsedLocally && !envPassedToExec && childProcessMatch) {
|
|
2432
|
+
// If env is only used locally (like NODE_ENV) and only child_process (no network), lower severity
|
|
2433
|
+
severity = 'medium';
|
|
2434
|
+
} else if (passesFullEnv && childProcessMatch) {
|
|
2435
|
+
// Passing full process.env to child_process is common pattern, but still review
|
|
2436
|
+
severity = 'medium';
|
|
2437
|
+
}
|
|
2438
|
+
// Otherwise: env + child_process (unknown pattern) = critical
|
|
2439
|
+
|
|
2440
|
+
// Determine recommendation based on severity
|
|
2441
|
+
let recommendation;
|
|
2442
|
+
if (hasNetwork) {
|
|
2443
|
+
recommendation = 'DANGER: This pattern (env + network + exec) matches known credential exfiltration attack patterns. Investigate immediately.';
|
|
2444
|
+
} else if (isInstallScriptFile) {
|
|
2445
|
+
recommendation = 'Install scripts often use env vars + child_process. Review to ensure it\'s legitimate and not exfiltrating data.';
|
|
2446
|
+
} else if (severity === 'medium') {
|
|
2447
|
+
recommendation = 'This pattern (env + child_process, no network) can be legitimate. Review to ensure env vars are not being exfiltrated through child processes.';
|
|
2448
|
+
} else {
|
|
2449
|
+
recommendation = 'DANGER: This pattern matches known credential exfiltration attack patterns. Investigate immediately.';
|
|
2219
2450
|
}
|
|
2220
2451
|
|
|
2221
2452
|
const issue = {
|
|
2222
2453
|
severity: severity,
|
|
2223
2454
|
reason: 'env_with_network',
|
|
2224
2455
|
detail: `File "${relativePath}" accesses environment variables and has network/exec capabilities`,
|
|
2225
|
-
recommendation:
|
|
2226
|
-
? 'Install scripts often use env vars + network to download binaries. Review to ensure it\'s legitimate.'
|
|
2227
|
-
: 'DANGER: This pattern matches credential exfiltration attacks like Shai-Hulud 2.0. Investigate immediately.',
|
|
2456
|
+
recommendation: recommendation,
|
|
2228
2457
|
};
|
|
2229
2458
|
|
|
2230
2459
|
if (verbose) {
|
|
@@ -2233,6 +2462,9 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
2233
2462
|
? extractCodeSnippet(content, nodeNetworkMatch, 3)
|
|
2234
2463
|
: (networkMatch ? extractCodeSnippet(content, networkMatch, 3) : null);
|
|
2235
2464
|
|
|
2465
|
+
// If no network snippet but we have child_process, use that as exec capability
|
|
2466
|
+
const execSnippet = networkSnippet || (childProcessMatch ? extractCodeSnippet(content, childProcessMatch, 3) : null);
|
|
2467
|
+
|
|
2236
2468
|
const envMatches = findAllMatches(content, envMatch, 5);
|
|
2237
2469
|
|
|
2238
2470
|
issue.verbose = {
|
|
@@ -2246,17 +2478,25 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
2246
2478
|
},
|
|
2247
2479
|
envCodeSnippet: envSnippet?.snippet || null,
|
|
2248
2480
|
envLineNumber: envSnippet?.lineNumber || null,
|
|
2249
|
-
|
|
2250
|
-
|
|
2481
|
+
envIsMinified: envSnippet?.isMinified || false,
|
|
2482
|
+
networkCodeSnippet: execSnippet?.snippet || null,
|
|
2483
|
+
networkLineNumber: execSnippet?.lineNumber || null,
|
|
2484
|
+
networkIsMinified: execSnippet?.isMinified || false,
|
|
2251
2485
|
falsePositiveHints: [
|
|
2252
|
-
|
|
2253
|
-
'⚠ This
|
|
2486
|
+
hasNetwork ? '⚠ CRITICAL: matches known credential exfiltration attack patterns' : null,
|
|
2487
|
+
isInstallScriptFile ? '⚠ This appears to be an install script - may legitimately use child_process' : null,
|
|
2488
|
+
!hasNetwork && envUsedLocally && !envPassedToExec ? '✓ Environment variables appear to be used only for local configuration (no network detected)' : null,
|
|
2489
|
+
!hasNetwork && passesFullEnv ? '✓ Passing full process.env to child_process is a common pattern (but still review - no network detected)' : null,
|
|
2490
|
+
severity === 'critical' ? '⚠ This is a HIGH-RISK pattern matching known attacks' : null,
|
|
2254
2491
|
'Legitimate uses: reading config from env for API calls, install scripts downloading binaries',
|
|
2255
2492
|
'Check what env vars are accessed and where data is sent',
|
|
2493
|
+
hasNetwork ? '⚠ Network access combined with env + exec = potential credential exfiltration' : null,
|
|
2256
2494
|
].filter(Boolean),
|
|
2257
|
-
riskAssessment:
|
|
2258
|
-
? '
|
|
2259
|
-
:
|
|
2495
|
+
riskAssessment: hasNetwork || severity === 'critical'
|
|
2496
|
+
? 'CRITICAL - matches known credential exfiltration attack patterns'
|
|
2497
|
+
: isInstallScriptFile
|
|
2498
|
+
? 'MEDIUM - Install scripts often use env + child_process legitimately, but should be reviewed'
|
|
2499
|
+
: 'MEDIUM - Pattern can be legitimate (no network detected), review to ensure no credential exfiltration',
|
|
2260
2500
|
attackPattern: 'Environment variable access combined with network/exec = potential credential exfiltration',
|
|
2261
2501
|
};
|
|
2262
2502
|
}
|
|
@@ -2362,6 +2602,7 @@ function analyzeCode(pkg, config, issues, verbose = false) {
|
|
|
2362
2602
|
},
|
|
2363
2603
|
codeSnippet: snippet?.snippet || null,
|
|
2364
2604
|
lineNumber: snippet?.lineNumber || null,
|
|
2605
|
+
isMinified: snippet?.isMinified || false,
|
|
2365
2606
|
falsePositiveHints: [
|
|
2366
2607
|
isMinifiedFile ? '✓ This appears to be a minified file - obfuscation patterns are expected' : null,
|
|
2367
2608
|
isLikelyMinified ? '✓ This file appears to be minified - skipping' : null,
|
|
@@ -2482,6 +2723,30 @@ function truncate(str, maxLen) {
|
|
|
2482
2723
|
return str.slice(0, maxLen - 3) + '...';
|
|
2483
2724
|
}
|
|
2484
2725
|
|
|
2726
|
+
/**
|
|
2727
|
+
* Check if a line appears to be minified code
|
|
2728
|
+
* @param {string} line - Line of code to check
|
|
2729
|
+
* @returns {boolean} True if line appears minified
|
|
2730
|
+
*/
|
|
2731
|
+
function isMinifiedLine(line) {
|
|
2732
|
+
// Minified code typically has:
|
|
2733
|
+
// - Very long lines (>500 chars)
|
|
2734
|
+
// - Very few spaces (dense code)
|
|
2735
|
+
// - No comments
|
|
2736
|
+
// - Dense character usage
|
|
2737
|
+
if (line.length < 500) return false;
|
|
2738
|
+
|
|
2739
|
+
// Check if line has very few spaces relative to its length
|
|
2740
|
+
const spaceRatio = (line.match(/\s/g) || []).length / line.length;
|
|
2741
|
+
// Minified code usually has < 5% whitespace
|
|
2742
|
+
if (spaceRatio < 0.05) return true;
|
|
2743
|
+
|
|
2744
|
+
// Check if line has no typical formatting (no indentation, no line breaks in strings)
|
|
2745
|
+
if (line.length > 1000 && !line.includes('\n') && spaceRatio < 0.1) return true;
|
|
2746
|
+
|
|
2747
|
+
return false;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2485
2750
|
/**
|
|
2486
2751
|
* Extract code snippet with context around a match
|
|
2487
2752
|
* @param {string} content - Full file content
|
|
@@ -2495,6 +2760,51 @@ function extractCodeSnippet(content, pattern, contextLines = 3) {
|
|
|
2495
2760
|
for (let i = 0; i < lines.length; i++) {
|
|
2496
2761
|
const match = lines[i].match(pattern);
|
|
2497
2762
|
if (match) {
|
|
2763
|
+
const isMinified = isMinifiedLine(lines[i]);
|
|
2764
|
+
|
|
2765
|
+
if (isMinified) {
|
|
2766
|
+
// For minified code, show a truncated snippet around the match
|
|
2767
|
+
const matchIndex = match.index;
|
|
2768
|
+
const matchLength = match[0].length;
|
|
2769
|
+
const line = lines[i];
|
|
2770
|
+
|
|
2771
|
+
// Extract context around the match (150 chars before, 150 after)
|
|
2772
|
+
const contextBefore = 150;
|
|
2773
|
+
const contextAfter = 150;
|
|
2774
|
+
const start = Math.max(0, matchIndex - contextBefore);
|
|
2775
|
+
const end = Math.min(line.length, matchIndex + matchLength + contextAfter);
|
|
2776
|
+
|
|
2777
|
+
const beforeText = start > 0 ? '...' : '';
|
|
2778
|
+
const afterText = end < line.length ? '...' : '';
|
|
2779
|
+
const snippetText = beforeText + line.slice(start, end) + afterText;
|
|
2780
|
+
|
|
2781
|
+
// Calculate the position of the match marker in the snippet
|
|
2782
|
+
// The match position relative to the snippet start
|
|
2783
|
+
const matchPosInSnippet = (start > 0 ? 3 : 0) + (matchIndex - start); // 3 for "..."
|
|
2784
|
+
const linePrefix = `>>> ${i + 1} | `;
|
|
2785
|
+
const markerPadding = linePrefix.length + matchPosInSnippet;
|
|
2786
|
+
|
|
2787
|
+
const snippetLines = [];
|
|
2788
|
+
snippetLines.push(`${linePrefix}${snippetText}`);
|
|
2789
|
+
// Add marker pointing to the match (limit to reasonable length)
|
|
2790
|
+
const markerChars = Math.min(matchLength, 15);
|
|
2791
|
+
const markerLine = ' ' + ' '.repeat(markerPadding) +
|
|
2792
|
+
'^'.repeat(markerChars) +
|
|
2793
|
+
` (column ${matchIndex + 1})`;
|
|
2794
|
+
snippetLines.push(markerLine);
|
|
2795
|
+
snippetLines.push(` [Minified code - showing context around match only]`);
|
|
2796
|
+
|
|
2797
|
+
return {
|
|
2798
|
+
lineNumber: i + 1,
|
|
2799
|
+
column: matchIndex + 1,
|
|
2800
|
+
matchedText: match[0],
|
|
2801
|
+
snippet: snippetLines.join('\n'),
|
|
2802
|
+
lineContent: line.length > 150 ? line.slice(0, 150) + '...' : line,
|
|
2803
|
+
isMinified: true,
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// Normal code - show context lines
|
|
2498
2808
|
const startLine = Math.max(0, i - contextLines);
|
|
2499
2809
|
const endLine = Math.min(lines.length - 1, i + contextLines);
|
|
2500
2810
|
|
|
@@ -2502,7 +2812,12 @@ function extractCodeSnippet(content, pattern, contextLines = 3) {
|
|
|
2502
2812
|
for (let j = startLine; j <= endLine; j++) {
|
|
2503
2813
|
const lineNum = String(j + 1).padStart(4, ' ');
|
|
2504
2814
|
const marker = j === i ? '>>>' : ' ';
|
|
2505
|
-
|
|
2815
|
+
// Truncate very long lines even in normal code (but less aggressively)
|
|
2816
|
+
let lineText = lines[j];
|
|
2817
|
+
if (lineText.length > 300) {
|
|
2818
|
+
lineText = lineText.slice(0, 297) + '...';
|
|
2819
|
+
}
|
|
2820
|
+
snippetLines.push(`${marker} ${lineNum} | ${lineText}`);
|
|
2506
2821
|
}
|
|
2507
2822
|
|
|
2508
2823
|
return {
|
|
@@ -2510,7 +2825,8 @@ function extractCodeSnippet(content, pattern, contextLines = 3) {
|
|
|
2510
2825
|
column: match.index + 1,
|
|
2511
2826
|
matchedText: match[0],
|
|
2512
2827
|
snippet: snippetLines.join('\n'),
|
|
2513
|
-
lineContent: lines[i],
|
|
2828
|
+
lineContent: lines[i].length > 200 ? lines[i].slice(0, 200) + '...' : lines[i],
|
|
2829
|
+
isMinified: false,
|
|
2514
2830
|
};
|
|
2515
2831
|
}
|
|
2516
2832
|
}
|
package/src/collector.js
CHANGED
|
@@ -71,17 +71,6 @@ function safeReadJSONWithDetails(filePath, options = {}) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
* Safely read and parse a JSON file (simplified API)
|
|
76
|
-
* @param {string} filePath - Path to JSON file
|
|
77
|
-
* @returns {Object|null} Parsed JSON or null on error
|
|
78
|
-
* @deprecated Use safeReadJSONWithDetails for better error handling
|
|
79
|
-
*/
|
|
80
|
-
function safeReadJSON(filePath) {
|
|
81
|
-
const result = safeReadJSONWithDetails(filePath);
|
|
82
|
-
return result.data;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
74
|
/**
|
|
86
75
|
* Collect all packages from node_modules recursively
|
|
87
76
|
* @param {string} nodeModulesPath - Path to node_modules
|
|
@@ -289,7 +278,6 @@ function normalizeRepository(repo) {
|
|
|
289
278
|
module.exports = {
|
|
290
279
|
collectPackages,
|
|
291
280
|
readPackage,
|
|
292
|
-
safeReadJSON,
|
|
293
281
|
safeReadJSONWithDetails,
|
|
294
282
|
JSON_READ_ERROR,
|
|
295
283
|
};
|
package/src/config.js
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_CONFIG = {
|
|
|
17
17
|
checkLockfile: false, // Lockfile integrity checks (disabled by default due to false positives)
|
|
18
18
|
failOn: null,
|
|
19
19
|
severity: null, // Array of severity levels to show (e.g., ['critical', 'high'])
|
|
20
|
-
format: 'text', // Output format: 'text', 'json', 'sarif'
|
|
20
|
+
format: 'text', // Output format: 'text', 'json', 'sarif' (sarif is experimental)
|
|
21
21
|
verbose: false, // Show detailed analysis
|
|
22
22
|
// Known legitimate packages with install scripts
|
|
23
23
|
// NOTE: Whitelist cleared - all packages are now checked without exceptions.
|
|
@@ -282,13 +282,13 @@ function generateExampleConfig() {
|
|
|
282
282
|
trustedPatterns: {
|
|
283
283
|
"custom-build-script": true
|
|
284
284
|
},
|
|
285
|
-
scanCode:
|
|
285
|
+
scanCode: true,
|
|
286
286
|
checkTyposquatting: false,
|
|
287
287
|
checkLockfile: false,
|
|
288
288
|
failOn: "high",
|
|
289
289
|
severity: ["critical", "high", "medium"],
|
|
290
290
|
format: "text",
|
|
291
|
-
verbose:
|
|
291
|
+
verbose: true,
|
|
292
292
|
maxFileSizeForCodeScan: 1048576,
|
|
293
293
|
maxNestedDepth: 10,
|
|
294
294
|
maxFilesPerPackage: 0,
|
package/src/formatters.js
CHANGED
|
@@ -8,6 +8,9 @@ const SEVERITY_ORDER = ['info', 'low', 'medium', 'high', 'critical'];
|
|
|
8
8
|
* Get color code for severity level
|
|
9
9
|
*/
|
|
10
10
|
function colorSeverity(severity) {
|
|
11
|
+
if (severity === null) {
|
|
12
|
+
return color('none', colors.dim);
|
|
13
|
+
}
|
|
11
14
|
switch (severity) {
|
|
12
15
|
case 'critical':
|
|
13
16
|
return color(severity.toUpperCase(), colors.magenta + colors.bold);
|
|
@@ -43,6 +46,7 @@ function formatText(issues, summary, context) {
|
|
|
43
46
|
].filter(Boolean).join(' ');
|
|
44
47
|
|
|
45
48
|
lines.push(header);
|
|
49
|
+
lines.push(color('Zero-dependency heuristic scanner CLI to detect supply chain attacks in node_modules', colors.dim));
|
|
46
50
|
lines.push(color('─'.repeat(60), colors.dim));
|
|
47
51
|
lines.push('');
|
|
48
52
|
|
|
@@ -157,11 +161,26 @@ function formatText(issues, summary, context) {
|
|
|
157
161
|
if (issue.verbose.codeSnippet) {
|
|
158
162
|
lines.push(` ${color('Code Snippet:', colors.bold)}`);
|
|
159
163
|
const snippetLines = issue.verbose.codeSnippet.split('\n');
|
|
164
|
+
const isMinified = issue.verbose.isMinified;
|
|
165
|
+
|
|
160
166
|
for (const snippetLine of snippetLines) {
|
|
161
167
|
const isHighlighted = snippetLine.startsWith('>>>');
|
|
162
|
-
|
|
168
|
+
const isNote = snippetLine.includes('[Minified code') ||
|
|
169
|
+
snippetLine.includes('Note:') ||
|
|
170
|
+
snippetLine.includes('column') ||
|
|
171
|
+
snippetLine.includes('match at');
|
|
172
|
+
|
|
173
|
+
if (isNote) {
|
|
174
|
+
// Style notes differently - use cyan for informational notes
|
|
175
|
+
lines.push(` ${color(snippetLine, colors.cyan + colors.dim)}`);
|
|
176
|
+
} else if (isHighlighted) {
|
|
177
|
+
lines.push(` ${color(snippetLine, colors.yellow)}`);
|
|
178
|
+
} else {
|
|
179
|
+
lines.push(` ${color(snippetLine, colors.dim)}`);
|
|
180
|
+
}
|
|
163
181
|
}
|
|
164
|
-
|
|
182
|
+
|
|
183
|
+
if (issue.verbose.lineNumber && !isMinified) {
|
|
165
184
|
lines.push(` ${color(`↑ Line ${issue.verbose.lineNumber}`, colors.cyan)}`);
|
|
166
185
|
}
|
|
167
186
|
}
|
|
@@ -170,18 +189,54 @@ function formatText(issues, summary, context) {
|
|
|
170
189
|
if (issue.verbose.envCodeSnippet) {
|
|
171
190
|
lines.push(` ${color('Environment Access:', colors.bold)}`);
|
|
172
191
|
const snippetLines = issue.verbose.envCodeSnippet.split('\n');
|
|
192
|
+
const isMinified = issue.verbose.envIsMinified || false;
|
|
193
|
+
|
|
173
194
|
for (const snippetLine of snippetLines) {
|
|
174
195
|
const isHighlighted = snippetLine.startsWith('>>>');
|
|
175
|
-
|
|
196
|
+
const isNote = snippetLine.includes('[Minified code') ||
|
|
197
|
+
snippetLine.includes('Note:') ||
|
|
198
|
+
snippetLine.includes('column') ||
|
|
199
|
+
snippetLine.includes('match at');
|
|
200
|
+
|
|
201
|
+
if (isNote) {
|
|
202
|
+
lines.push(` ${color(snippetLine, colors.cyan + colors.dim)}`);
|
|
203
|
+
} else if (isHighlighted) {
|
|
204
|
+
lines.push(` ${color(snippetLine, colors.yellow)}`);
|
|
205
|
+
} else {
|
|
206
|
+
lines.push(` ${color(snippetLine, colors.dim)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (issue.verbose.envLineNumber && !isMinified) {
|
|
211
|
+
lines.push(` ${color(`↑ Line ${issue.verbose.envLineNumber}`, colors.cyan)}`);
|
|
176
212
|
}
|
|
177
213
|
}
|
|
178
214
|
|
|
179
215
|
if (issue.verbose.networkCodeSnippet) {
|
|
180
|
-
|
|
216
|
+
// For env_with_network, this could be network OR exec capability
|
|
217
|
+
const accessType = issue.reason === 'env_with_network' ? 'Network/Exec Access' : 'Network Access';
|
|
218
|
+
lines.push(` ${color(`${accessType}:`, colors.bold)}`);
|
|
181
219
|
const snippetLines = issue.verbose.networkCodeSnippet.split('\n');
|
|
220
|
+
const isMinified = issue.verbose.networkIsMinified || false;
|
|
221
|
+
|
|
182
222
|
for (const snippetLine of snippetLines) {
|
|
183
223
|
const isHighlighted = snippetLine.startsWith('>>>');
|
|
184
|
-
|
|
224
|
+
const isNote = snippetLine.includes('[Minified code') ||
|
|
225
|
+
snippetLine.includes('Note:') ||
|
|
226
|
+
snippetLine.includes('column') ||
|
|
227
|
+
snippetLine.includes('match at');
|
|
228
|
+
|
|
229
|
+
if (isNote) {
|
|
230
|
+
lines.push(` ${color(snippetLine, colors.cyan + colors.dim)}`);
|
|
231
|
+
} else if (isHighlighted) {
|
|
232
|
+
lines.push(` ${color(snippetLine, colors.yellow)}`);
|
|
233
|
+
} else {
|
|
234
|
+
lines.push(` ${color(snippetLine, colors.dim)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (issue.verbose.networkLineNumber && !isMinified) {
|
|
239
|
+
lines.push(` ${color(`↑ Line ${issue.verbose.networkLineNumber}`, colors.cyan)}`);
|
|
185
240
|
}
|
|
186
241
|
}
|
|
187
242
|
|
|
@@ -247,6 +302,11 @@ function formatText(issues, summary, context) {
|
|
|
247
302
|
lines.push(color('─'.repeat(60), colors.dim));
|
|
248
303
|
lines.push(color('chain-audit by Hubert Kasperek • MIT License', colors.dim));
|
|
249
304
|
lines.push(color('https://github.com/hukasx0/chain-audit', colors.dim));
|
|
305
|
+
lines.push('');
|
|
306
|
+
lines.push(color('Disclaimer: Licensed under MIT License, provided "AS IS" without warranty.', colors.dim));
|
|
307
|
+
lines.push(color('The author makes no guarantees and takes no responsibility for false', colors.dim));
|
|
308
|
+
lines.push(color('positives, false negatives, missed attacks, or any damages resulting from use.', colors.dim));
|
|
309
|
+
lines.push(color('Review all findings manually. Use at your own risk.', colors.dim));
|
|
250
310
|
|
|
251
311
|
return lines.join('\n');
|
|
252
312
|
}
|
|
@@ -341,6 +401,8 @@ function formatJson(issues, summary, context) {
|
|
|
341
401
|
/**
|
|
342
402
|
* Format issues as SARIF (Static Analysis Results Interchange Format)
|
|
343
403
|
* Compatible with GitHub Code Scanning
|
|
404
|
+
*
|
|
405
|
+
* @experimental This format is experimental and may change in future versions
|
|
344
406
|
*/
|
|
345
407
|
function formatSarif(issues, summary, context) {
|
|
346
408
|
const sarif = {
|
|
@@ -441,7 +503,7 @@ function generateSarifRules() {
|
|
|
441
503
|
id: 'install_script',
|
|
442
504
|
name: 'InstallScript',
|
|
443
505
|
shortDescription: { text: 'Has install lifecycle script' },
|
|
444
|
-
fullDescription: { text: 'Package has preinstall, install,
|
|
506
|
+
fullDescription: { text: 'Package has preinstall, install, or postinstall script' },
|
|
445
507
|
defaultConfiguration: { level: 'warning' },
|
|
446
508
|
},
|
|
447
509
|
{
|
|
@@ -483,9 +545,16 @@ function generateSarifRules() {
|
|
|
483
545
|
id: 'native_binary',
|
|
484
546
|
name: 'NativeBinary',
|
|
485
547
|
shortDescription: { text: 'Contains native binaries' },
|
|
486
|
-
fullDescription: { text: 'Package contains native binary files (.node, .so, .
|
|
548
|
+
fullDescription: { text: 'Package contains native module binary files (.node, .so, .dylib)' },
|
|
487
549
|
defaultConfiguration: { level: 'note' },
|
|
488
550
|
},
|
|
551
|
+
{
|
|
552
|
+
id: 'executable_files',
|
|
553
|
+
name: 'ExecutableFiles',
|
|
554
|
+
shortDescription: { text: 'Contains executable files' },
|
|
555
|
+
fullDescription: { text: 'Package contains executable files (shell scripts, etc.) that may indicate supply chain attacks' },
|
|
556
|
+
defaultConfiguration: { level: 'warning' },
|
|
557
|
+
},
|
|
489
558
|
{
|
|
490
559
|
id: 'potential_typosquat',
|
|
491
560
|
name: 'PotentialTyposquat',
|
|
@@ -570,6 +639,9 @@ function generateSarifRules() {
|
|
|
570
639
|
* Map our severity levels to SARIF levels
|
|
571
640
|
*/
|
|
572
641
|
function mapSeverityToSarif(severity) {
|
|
642
|
+
if (severity === null) {
|
|
643
|
+
return 'none';
|
|
644
|
+
}
|
|
573
645
|
switch (severity) {
|
|
574
646
|
case 'critical':
|
|
575
647
|
case 'high':
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* chain-audit - Supply chain attack scanner for node_modules
|
|
3
|
+
* chain-audit - Supply chain attack heuristic scanner for node_modules
|
|
4
4
|
*
|
|
5
5
|
* Detects suspicious patterns in dependencies including:
|
|
6
6
|
* - Malicious install scripts
|
|
@@ -18,12 +18,12 @@ const path = require('path');
|
|
|
18
18
|
const { parseArgs } = require('./cli');
|
|
19
19
|
const { loadConfig, mergeConfig, initConfig } = require('./config');
|
|
20
20
|
const { buildLockIndex } = require('./lockfile');
|
|
21
|
-
const { collectPackages,
|
|
21
|
+
const { collectPackages, safeReadJSONWithDetails } = require('./collector');
|
|
22
22
|
const { analyzePackage } = require('./analyzer');
|
|
23
23
|
const { formatText, formatJson, formatSarif } = require('./formatters');
|
|
24
24
|
const { color, colors } = require('./utils');
|
|
25
25
|
|
|
26
|
-
const pkgMeta =
|
|
26
|
+
const pkgMeta = (safeReadJSONWithDetails(path.join(__dirname, '..', 'package.json')).data) || {};
|
|
27
27
|
|
|
28
28
|
function detectDefaultLockfile(cwd) {
|
|
29
29
|
const candidates = [
|
|
@@ -42,7 +42,7 @@ function detectDefaultLockfile(cwd) {
|
|
|
42
42
|
|
|
43
43
|
function summarize(issues) {
|
|
44
44
|
const counts = { info: 0, low: 0, medium: 0, high: 0, critical: 0 };
|
|
45
|
-
let maxSeverity =
|
|
45
|
+
let maxSeverity = null;
|
|
46
46
|
const severityOrder = ['info', 'low', 'medium', 'high', 'critical'];
|
|
47
47
|
|
|
48
48
|
const rankSeverity = (level) => {
|
|
@@ -54,7 +54,7 @@ function summarize(issues) {
|
|
|
54
54
|
if (counts[issue.severity] !== undefined) {
|
|
55
55
|
counts[issue.severity] += 1;
|
|
56
56
|
}
|
|
57
|
-
if (rankSeverity(issue.severity) > rankSeverity(maxSeverity)) {
|
|
57
|
+
if (maxSeverity === null || rankSeverity(issue.severity) > rankSeverity(maxSeverity)) {
|
|
58
58
|
maxSeverity = issue.severity;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -81,13 +81,13 @@ function run(argv = process.argv) {
|
|
|
81
81
|
console.log(color('✓', colors.green), result.message);
|
|
82
82
|
console.log('\nConfiguration options:');
|
|
83
83
|
console.log(color(' ignoredPackages', colors.cyan), ' - Packages to skip during analysis (supports glob patterns)');
|
|
84
|
-
console.log(color(' ignoredRules', colors.cyan), ' - Rule IDs to ignore (e.g., "native_binary")');
|
|
84
|
+
console.log(color(' ignoredRules', colors.cyan), ' - Rule IDs to ignore (e.g., "native_binary,executable_files")');
|
|
85
85
|
console.log(color(' trustedPackages', colors.cyan), ' - Known legitimate packages with install scripts');
|
|
86
86
|
console.log(color(' trustedPatterns', colors.cyan), ' - Patterns that reduce severity for known use cases');
|
|
87
87
|
console.log(color(' scanCode', colors.cyan), ' - Enable deep JS file scanning (slower)');
|
|
88
88
|
console.log(color(' failOn', colors.cyan), ' - Exit 1 when max severity >= level');
|
|
89
89
|
console.log(color(' severity', colors.cyan), ' - Filter to show only specific severity levels');
|
|
90
|
-
console.log(color(' format', colors.cyan), ' - Output format: text, json, sarif');
|
|
90
|
+
console.log(color(' format', colors.cyan), ' - Output format: text, json, sarif (experimental)');
|
|
91
91
|
console.log(color(' detailed', colors.cyan), ' - Show detailed analysis (verbose is alias)');
|
|
92
92
|
return { exitCode: 0 };
|
|
93
93
|
} else {
|
|
@@ -185,9 +185,9 @@ function run(argv = process.argv) {
|
|
|
185
185
|
|
|
186
186
|
// Determine exit code
|
|
187
187
|
const severityOrder = ['info', 'low', 'medium', 'high', 'critical'];
|
|
188
|
-
const rankSeverity = (level) => severityOrder.indexOf(level);
|
|
188
|
+
const rankSeverity = (level) => level === null ? -1 : severityOrder.indexOf(level);
|
|
189
189
|
|
|
190
|
-
if (config.failOn && rankSeverity(summary.maxSeverity) >= rankSeverity(config.failOn)) {
|
|
190
|
+
if (config.failOn && summary.maxSeverity !== null && rankSeverity(summary.maxSeverity) >= rankSeverity(config.failOn)) {
|
|
191
191
|
return { exitCode: 1, issues, summary };
|
|
192
192
|
}
|
|
193
193
|
|
|
@@ -204,7 +204,7 @@ function matchPattern(pattern, name) {
|
|
|
204
204
|
|
|
205
205
|
function printHelp() {
|
|
206
206
|
const text = `
|
|
207
|
-
${color('chain-audit', colors.bold)} -
|
|
207
|
+
${color('chain-audit', colors.bold)} - Zero-dependency heuristic scanner CLI to detect supply chain attacks in node_modules
|
|
208
208
|
|
|
209
209
|
${color('USAGE:', colors.bold)}
|
|
210
210
|
chain-audit [options]
|
|
@@ -217,7 +217,7 @@ ${color('OPTIONS:', colors.bold)}
|
|
|
217
217
|
-c, --config <path> Path to config file (auto-detects .chainauditrc.json,
|
|
218
218
|
.chainauditrc, chainaudit.config.json)
|
|
219
219
|
--json Output as JSON
|
|
220
|
-
--sarif Output as SARIF (for GitHub Code Scanning)
|
|
220
|
+
--sarif Output as SARIF (for GitHub Code Scanning) [experimental]
|
|
221
221
|
-s, --severity <levels> Show only specified severity levels (comma-separated)
|
|
222
222
|
e.g., --severity critical,high or --severity low
|
|
223
223
|
--fail-on <level> Exit 1 when max severity >= level
|
|
@@ -291,21 +291,28 @@ ${color('EXAMPLES:', colors.bold)}
|
|
|
291
291
|
${color('CONFIGURATION:', colors.bold)}
|
|
292
292
|
Create a config file in your project root:
|
|
293
293
|
(.chainauditrc.json, .chainauditrc, or chainaudit.config.json)
|
|
294
|
+
|
|
295
|
+
Example (simplified):
|
|
294
296
|
{
|
|
295
297
|
"ignoredPackages": ["@types/*"],
|
|
296
298
|
"ignoredRules": ["native_binary"],
|
|
297
299
|
"trustedPackages": ["my-native-addon"],
|
|
298
|
-
"scanCode":
|
|
300
|
+
"scanCode": true,
|
|
301
|
+
"verbose": true,
|
|
299
302
|
"failOn": "high",
|
|
300
303
|
"verifyIntegrity": false,
|
|
301
|
-
"maxFilesPerPackage": 0
|
|
304
|
+
"maxFilesPerPackage": 0,
|
|
305
|
+
"format": "text"
|
|
302
306
|
}
|
|
307
|
+
|
|
308
|
+
For full config with all options, use: ${color('chain-audit --init', colors.cyan)}
|
|
303
309
|
|
|
304
310
|
${color('DISCLAIMER:', colors.bold)}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
311
|
+
Licensed under MIT License, provided "AS IS" without warranty.
|
|
312
|
+
The author makes no guarantees and takes no responsibility for false
|
|
313
|
+
positives, false negatives, missed attacks, or any damages resulting
|
|
314
|
+
from use of this tool. Use at your own risk. Always review findings
|
|
315
|
+
manually and use as part of defense-in-depth.
|
|
309
316
|
|
|
310
317
|
${color('MORE INFO:', colors.bold)}
|
|
311
318
|
https://github.com/hukasx0/chain-audit
|