chain-audit 0.6.5 → 0.6.6

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
@@ -183,7 +183,7 @@ Issues will be displayed sorted by severity (highest first), then by package nam
183
183
  ## Example Output
184
184
 
185
185
  ```
186
- chain-audit v0.6.5
186
+ chain-audit v0.6.6
187
187
  Zero-dependency heuristic scanner CLI to detect supply chain attacks in node_modules
188
188
  ────────────────────────────────────────────────────────────
189
189
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chain-audit",
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.",
3
+ "version": "0.6.6",
4
+ "description": "Zero-dependency heuristic scanner CLI to detect supply chain attacks in node_modules. Detects malicious install scripts, network access, env exfiltration, lockfile integrity, obfuscated code, and suspicious patterns. Works fully offline.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "chain-audit": "src/index.js"
@@ -52,6 +52,6 @@
52
52
  "node": ">=18.0.0"
53
53
  },
54
54
  "devDependencies": {
55
- "eslint": "^8.57.0"
55
+ "eslint": "9.39.2"
56
56
  }
57
57
  }
package/src/analyzer.js CHANGED
@@ -1010,7 +1010,8 @@ function findNativeArtifacts(pkgDir, maxDepth = 3) {
1010
1010
  let entries = [];
1011
1011
  try {
1012
1012
  entries = fs.readdirSync(dir, { withFileTypes: true });
1013
- } catch {
1013
+ } catch (err) {
1014
+ console.warn(`Warning: Cannot read directory ${dir} while searching for native artifacts: ${err.message}`);
1014
1015
  continue;
1015
1016
  }
1016
1017
 
@@ -1062,10 +1063,10 @@ function checkExecutableFiles(pkg, issues, verbose = false, config = {}) {
1062
1063
  const issue = {
1063
1064
  severity: severity,
1064
1065
  reason: 'executable_files',
1065
- detail: `Contains executable files (shell scripts, etc.): ${listed}${found.length > 5 ? `, +${found.length - 5} more` : ''}`,
1066
+ detail: `Contains executable files (shell scripts, binaries, etc.): ${listed}${found.length > 5 ? `, +${found.length - 5} more` : ''}`,
1066
1067
  recommendation: suspiciousFiles.length > 0
1067
1068
  ? '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
+ : 'Shell scripts and binaries in bin/ directory are less suspicious but should be reviewed for malicious content.',
1069
1070
  };
1070
1071
 
1071
1072
  if (verbose) {
@@ -1112,6 +1113,7 @@ function checkExecutableFiles(pkg, issues, verbose = false, config = {}) {
1112
1113
  */
1113
1114
  function findExecutableFiles(pkgDir, maxDepth = 5) {
1114
1115
  const found = [];
1116
+ const binaryFiles = []; // Track binary files that couldn't be read
1115
1117
  const stack = [{ dir: pkgDir, depth: 0 }];
1116
1118
 
1117
1119
  // Executable file extensions - only real executables, not .js (those are scanned by code scanner)
@@ -1135,7 +1137,8 @@ function findExecutableFiles(pkgDir, maxDepth = 5) {
1135
1137
  let entries = [];
1136
1138
  try {
1137
1139
  entries = fs.readdirSync(dir, { withFileTypes: true });
1138
- } catch {
1140
+ } catch (err) {
1141
+ console.warn(`Warning: Cannot read directory ${dir} while searching for executable files: ${err.message}`);
1139
1142
  continue;
1140
1143
  }
1141
1144
 
@@ -1186,12 +1189,13 @@ function findExecutableFiles(pkgDir, maxDepth = 5) {
1186
1189
  found.push(fullPath);
1187
1190
  }
1188
1191
  } catch {
1189
- // If we can't read content, skip (might be binary)
1190
- continue;
1192
+ // If we can't read content, it's likely a binary file
1193
+ // Track it as a binary executable (no extension but executable)
1194
+ binaryFiles.push(fullPath);
1191
1195
  }
1192
1196
  }
1193
- } catch {
1194
- // Skip files we can't check
1197
+ } catch (err) {
1198
+ console.warn(`Warning: Cannot check file ${fullPath} for executable permissions: ${err.message}`);
1195
1199
  continue;
1196
1200
  }
1197
1201
  }
@@ -1199,6 +1203,9 @@ function findExecutableFiles(pkgDir, maxDepth = 5) {
1199
1203
  }
1200
1204
  }
1201
1205
 
1206
+ // Add binary files to found list (they are executables too)
1207
+ found.push(...binaryFiles);
1208
+
1202
1209
  return found;
1203
1210
  }
1204
1211
 
@@ -2620,8 +2627,8 @@ function analyzeCode(pkg, config, issues, verbose = false) {
2620
2627
  }
2621
2628
  }
2622
2629
 
2623
- } catch {
2624
- // Skip files that can't be read
2630
+ } catch (err) {
2631
+ console.warn(`Warning: Cannot read or analyze file ${filePath}: ${err.message}`);
2625
2632
  continue;
2626
2633
  }
2627
2634
  }
@@ -2661,7 +2668,8 @@ function findJsFiles(dir, maxDepth = 2) {
2661
2668
  let entries = [];
2662
2669
  try {
2663
2670
  entries = fs.readdirSync(currentDir, { withFileTypes: true });
2664
- } catch {
2671
+ } catch (err) {
2672
+ console.warn(`Warning: Cannot read directory ${currentDir} while searching for JS files: ${err.message}`);
2665
2673
  continue;
2666
2674
  }
2667
2675
 
@@ -2761,21 +2769,33 @@ function extractCodeSnippet(content, pattern, contextLines = 3) {
2761
2769
  const match = lines[i].match(pattern);
2762
2770
  if (match) {
2763
2771
  const isMinified = isMinifiedLine(lines[i]);
2772
+ const obfuscationType = getObfuscationType(pattern);
2773
+ const isBase64 = obfuscationType === 'base64_encoding';
2774
+ const isLongBase64 = isBase64 && match[0].length > 200;
2764
2775
 
2765
- if (isMinified) {
2766
- // For minified code, show a truncated snippet around the match
2776
+ if (isMinified || isLongBase64) {
2777
+ // For minified code or long base64, show a truncated snippet around the match
2767
2778
  const matchIndex = match.index;
2768
2779
  const matchLength = match[0].length;
2769
2780
  const line = lines[i];
2770
2781
 
2782
+ // For very long base64 matches, limit how much of the match we show
2783
+ const maxMatchDisplay = isBase64 ? 200 : matchLength; // Show max 200 chars of base64 match
2784
+ const displayMatchLength = Math.min(matchLength, maxMatchDisplay);
2785
+ const matchIsTruncated = isBase64 && matchLength > maxMatchDisplay;
2786
+
2771
2787
  // Extract context around the match (150 chars before, 150 after)
2772
2788
  const contextBefore = 150;
2773
2789
  const contextAfter = 150;
2774
2790
  const start = Math.max(0, matchIndex - contextBefore);
2775
- const end = Math.min(line.length, matchIndex + matchLength + contextAfter);
2791
+ const end = Math.min(line.length, matchIndex + displayMatchLength + contextAfter);
2776
2792
 
2777
2793
  const beforeText = start > 0 ? '...' : '';
2778
- const afterText = end < line.length ? '...' : '';
2794
+ let afterText = '';
2795
+ if (matchIsTruncated || end < line.length) {
2796
+ afterText = '...';
2797
+ }
2798
+
2779
2799
  const snippetText = beforeText + line.slice(start, end) + afterText;
2780
2800
 
2781
2801
  // Calculate the position of the match marker in the snippet
@@ -2787,12 +2807,15 @@ function extractCodeSnippet(content, pattern, contextLines = 3) {
2787
2807
  const snippetLines = [];
2788
2808
  snippetLines.push(`${linePrefix}${snippetText}`);
2789
2809
  // Add marker pointing to the match (limit to reasonable length)
2790
- const markerChars = Math.min(matchLength, 15);
2810
+ const markerChars = Math.min(displayMatchLength, 15);
2791
2811
  const markerLine = ' ' + ' '.repeat(markerPadding) +
2792
2812
  '^'.repeat(markerChars) +
2793
2813
  ` (column ${matchIndex + 1})`;
2794
2814
  snippetLines.push(markerLine);
2795
- snippetLines.push(` [Minified code - showing context around match only]`);
2815
+ const contextNote = isBase64
2816
+ ? '[Base64 encoded data - showing context around match only]'
2817
+ : '[Minified code - showing context around match only]';
2818
+ snippetLines.push(` ${contextNote}`);
2796
2819
 
2797
2820
  return {
2798
2821
  lineNumber: i + 1,
package/src/collector.js CHANGED
@@ -92,7 +92,8 @@ function collectPackages(nodeModulesPath, maxDepth = 10) {
92
92
  let realDir;
93
93
  try {
94
94
  realDir = fs.realpathSync(dir);
95
- } catch {
95
+ } catch (err) {
96
+ console.warn(`Warning: Cannot resolve real path for ${dir}: ${err.message}`);
96
97
  continue;
97
98
  }
98
99
 
@@ -104,6 +105,7 @@ function collectPackages(nodeModulesPath, maxDepth = 10) {
104
105
  entries = fs.readdirSync(dir, { withFileTypes: true });
105
106
  } catch (err) {
106
107
  // Permission denied or other read error
108
+ console.warn(`Warning: Cannot read directory ${dir}: ${err.message}`);
107
109
  continue;
108
110
  }
109
111
 
@@ -137,7 +139,8 @@ function processScopedPackage(scopeDir, scopeRel, depth, stack, packages) {
137
139
  let scopeEntries = [];
138
140
  try {
139
141
  scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
140
- } catch {
142
+ } catch (err) {
143
+ console.warn(`Warning: Cannot read scoped package directory ${scopeDir}: ${err.message}`);
141
144
  return;
142
145
  }
143
146
 
package/src/formatters.js CHANGED
@@ -84,7 +84,8 @@ function formatText(issues, summary, context) {
84
84
  for (const issue of sorted) {
85
85
  if (issue.severity !== currentSeverity) {
86
86
  currentSeverity = issue.severity;
87
- lines.push(color(`── ${currentSeverity.toUpperCase()} ──`, getSeverityColor(issue.severity)));
87
+ const severityLabel = currentSeverity ? currentSeverity.toUpperCase() : 'UNKNOWN';
88
+ lines.push(color(`── ${severityLabel} ──`, getSeverityColor(issue.severity)));
88
89
  }
89
90
 
90
91
  const pkgInfo = `${issue.package}@${issue.version}`;
@@ -315,6 +316,9 @@ function formatText(issues, summary, context) {
315
316
  * Get ANSI color code for severity
316
317
  */
317
318
  function getSeverityColor(severity) {
319
+ if (severity === null || severity === undefined) {
320
+ return colors.dim;
321
+ }
318
322
  switch (severity) {
319
323
  case 'critical': return colors.magenta;
320
324
  case 'high': return colors.red;
package/src/lockfile.js CHANGED
@@ -594,7 +594,8 @@ function computePackageIntegrity(pkgDir, algorithm = 'sha512') {
594
594
  const hash = crypto.createHash(algorithm);
595
595
  hash.update(content);
596
596
  return hash.digest('base64');
597
- } catch {
597
+ } catch (err) {
598
+ console.warn(`Warning: Cannot compute package integrity for ${pkgDir}: ${err.message}`);
598
599
  return null;
599
600
  }
600
601
  }
@@ -657,7 +658,8 @@ function computeFileIntegrity(filePath, algorithm = 'sha512') {
657
658
  const hash = crypto.createHash(algorithm);
658
659
  hash.update(content);
659
660
  return `${algorithm}-${hash.digest('base64')}`;
660
- } catch {
661
+ } catch (err) {
662
+ console.warn(`Warning: Cannot compute file integrity for ${filePath}: ${err.message}`);
661
663
  return null;
662
664
  }
663
665
  }