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 CHANGED
@@ -5,7 +5,10 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Node.js Version](https://img.shields.io/node/v/chain-audit.svg)](https://nodejs.org)
7
7
 
8
- **Fast, zero-dependency CLI to detect supply chain attacks in `node_modules`.**
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 typosquatting | ✅ | ❌ |
19
- | Detects extraneous packages | ✅ | ❌ |
20
- | Detects obfuscated code | ✅ | ❌ |
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
- # Scan current project
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, extraneous packages |
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 match trusted patterns (if configured) |
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 in the order they are found, grouped by the severity levels you specified.
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.2
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: extraneous_package
190
- detail: Package exists in node_modules but is missing from lockfile
191
- fix: Run `npm ci` to reinstall from lockfile
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: install_script
202
- detail: Has postinstall script: node-gyp rebuild
203
- fix: Review the script to ensure it performs only expected operations
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 (e.g., Shai-Hulud 2.0)
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 listed in `trustedPackages` config (if configured) |
238
- | **Known legitimate** | +50 | Package is in the `trustedPackages` config list (if configured) |
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:** By default, no packages are whitelisted. All packages are checked with equal severity. You can configure `trustedPackages` in `.chainauditrc.json` if you need to reduce false positives for specific packages.
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., configured as trusted with repository)
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": false,
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": false,
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 by default |
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 like [Shai-Hulud 2.0](https://www.wiz.io/blog/shai-hulud-2-0-aftermath-ongoing-supply-chain-attack) exploited misconfigured GitHub Actions. **Protect your CI/CD:**
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
- - **extraneous_package** – Package in node_modules not in lockfile
492
- - **version_mismatch** – Installed version differs from lockfile
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
- ### High Severity
504
- - **network_access_script** – Install script with curl/wget/fetch patterns
505
- - **potential_typosquat** – Package name similar to popular package (requires `--check-typosquatting`)
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
- - **install_script** – Has preinstall/install/postinstall/prepare script
513
- - **code_execution** – Script runs code via node -e, python -c, etc.
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, .dll, .dylib files
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 = await run(['node', 'script.js', '--json', '--fail-on', 'high']);
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` periodically** – More thorough but slower
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. It may produce false positives and **cannot catch all attacks**.
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.2",
4
- "description": "Fast, zero-dependency CLI to detect supply chain attacks in node_modules. Scans for malicious install scripts, typosquatting, extraneous packages, and suspicious code patterns.",
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
- "npm-audit",
40
- "postinstall",
41
- "typosquatting",
42
- "ci",
43
- "github-actions",
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 attacks like Shai-Hulud 2.0
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', '.dll', '.dylib', '.exe'];
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 Shai-Hulud 2.0 attack pattern',
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 - 20), matchIndex);
1614
- const afterMatch = content.slice(matchIndex + matchText.length, matchIndex + matchText.length + 20);
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 (like Shai-Hulud 2.0 attack)
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
- // Determine severity based on context
2214
- // Install scripts often legitimately use env + network (downloading binaries)
2215
- // But we still flag them - user should review, but with lower severity
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
- if (isInstallScriptFile) {
2218
- severity = 'medium'; // Install scripts are less suspicious, but still worth reviewing
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: isInstallScriptFile
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
- networkCodeSnippet: networkSnippet?.snippet || null,
2250
- networkLineNumber: networkSnippet?.lineNumber || null,
2481
+ envIsMinified: envSnippet?.isMinified || false,
2482
+ networkCodeSnippet: execSnippet?.snippet || null,
2483
+ networkLineNumber: execSnippet?.lineNumber || null,
2484
+ networkIsMinified: execSnippet?.isMinified || false,
2251
2485
  falsePositiveHints: [
2252
- isInstallScriptFile ? '⚠ This appears to be an install script - may legitimately download binaries' : null,
2253
- '⚠ This is a HIGH-RISK pattern matching known attacks',
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: isInstallScriptFile
2258
- ? 'MEDIUM - Install scripts often use env + network legitimately, but should be reviewed'
2259
- : 'CRITICAL - Matches Shai-Hulud 2.0 and similar attack patterns',
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
- snippetLines.push(`${marker} ${lineNum} | ${lines[j]}`);
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: false,
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: false,
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
- lines.push(` ${isHighlighted ? color(snippetLine, colors.yellow) : color(snippetLine, colors.dim)}`);
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
- if (issue.verbose.lineNumber) {
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
- lines.push(` ${isHighlighted ? color(snippetLine, colors.yellow) : color(snippetLine, colors.dim)}`);
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
- lines.push(` ${color('Network Access:', colors.bold)}`);
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
- lines.push(` ${isHighlighted ? color(snippetLine, colors.yellow) : color(snippetLine, colors.dim)}`);
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, postinstall, or prepare script' },
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, .dll, .dylib)' },
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, safeReadJSON } = require('./collector');
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 = safeReadJSON(path.join(__dirname, '..', 'package.json')) || {};
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 = 'info';
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)} - Supply chain attack scanner for node_modules
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": false,
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
- This tool is provided "AS IS" without warranty. The author takes no
306
- responsibility for false positives, false negatives, missed attacks, or
307
- any damages resulting from use of this tool. Use at your own risk.
308
- Always review findings manually and use as part of defense-in-depth.
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