praxis-sec 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/ai-defense/cost-protection.md +292 -0
  4. package/ai-defense/llm-security-checklist.md +324 -0
  5. package/ai-defense/prompt-injection-patterns.js +283 -0
  6. package/ai-defense/system-prompt-armor.md +327 -0
  7. package/checklists/launch-day.md +168 -0
  8. package/cli/agents/abom-generator.js +225 -0
  9. package/cli/agents/agent-attestation-agent.js +318 -0
  10. package/cli/agents/agent-config-scanner.js +787 -0
  11. package/cli/agents/agent-telemetry-agent.js +415 -0
  12. package/cli/agents/agentic-security-agent.js +296 -0
  13. package/cli/agents/agentic-supply-chain-agent.js +463 -0
  14. package/cli/agents/ai-infra-inventory-agent.js +449 -0
  15. package/cli/agents/api-fuzzer.js +345 -0
  16. package/cli/agents/auth-bypass-agent.js +348 -0
  17. package/cli/agents/base-agent.js +280 -0
  18. package/cli/agents/cicd-scanner.js +300 -0
  19. package/cli/agents/config-auditor.js +757 -0
  20. package/cli/agents/deep-analyzer.js +776 -0
  21. package/cli/agents/endpoint-agent-abuse-agent.js +404 -0
  22. package/cli/agents/exception-handler-agent.js +187 -0
  23. package/cli/agents/git-history-scanner.js +169 -0
  24. package/cli/agents/governance-audits.js +138 -0
  25. package/cli/agents/hermes-security-agent.js +536 -0
  26. package/cli/agents/html-reporter.js +1125 -0
  27. package/cli/agents/index.js +147 -0
  28. package/cli/agents/injection-tester.js +502 -0
  29. package/cli/agents/legal-risk-agent.js +328 -0
  30. package/cli/agents/llm-redteam.js +199 -0
  31. package/cli/agents/managed-agent-scanner.js +333 -0
  32. package/cli/agents/mcp-security-agent.js +588 -0
  33. package/cli/agents/memory-poisoning-agent.js +305 -0
  34. package/cli/agents/mobile-scanner.js +231 -0
  35. package/cli/agents/model-file-scanner.js +259 -0
  36. package/cli/agents/orchestrator.js +355 -0
  37. package/cli/agents/pii-compliance-agent.js +301 -0
  38. package/cli/agents/policy-engine.js +229 -0
  39. package/cli/agents/prompt-injection-prober.js +224 -0
  40. package/cli/agents/rag-security-agent.js +204 -0
  41. package/cli/agents/recon-agent.js +207 -0
  42. package/cli/agents/sbom-generator.js +265 -0
  43. package/cli/agents/scoring-engine.js +273 -0
  44. package/cli/agents/ssrf-prober.js +130 -0
  45. package/cli/agents/stateful-watcher.js +238 -0
  46. package/cli/agents/supabase-rls-agent.js +154 -0
  47. package/cli/agents/supply-chain-agent.js +857 -0
  48. package/cli/agents/swarm-orchestrator.js +200 -0
  49. package/cli/agents/verifier-agent.js +303 -0
  50. package/cli/agents/vibe-coding-agent.js +250 -0
  51. package/cli/bin/praxis.js +866 -0
  52. package/cli/commands/abom.js +73 -0
  53. package/cli/commands/agent-fix.js +1245 -0
  54. package/cli/commands/audit.js +1180 -0
  55. package/cli/commands/autofix.js +383 -0
  56. package/cli/commands/baseline.js +193 -0
  57. package/cli/commands/benchmark.js +327 -0
  58. package/cli/commands/checklist.js +223 -0
  59. package/cli/commands/ci.js +403 -0
  60. package/cli/commands/deps.js +516 -0
  61. package/cli/commands/diff.js +200 -0
  62. package/cli/commands/doctor.js +195 -0
  63. package/cli/commands/env-audit.js +349 -0
  64. package/cli/commands/fix.js +218 -0
  65. package/cli/commands/guard.js +396 -0
  66. package/cli/commands/hooks.js +278 -0
  67. package/cli/commands/init.js +514 -0
  68. package/cli/commands/legal.js +158 -0
  69. package/cli/commands/live-advisories.js +241 -0
  70. package/cli/commands/mcp.js +660 -0
  71. package/cli/commands/openclaw.js +386 -0
  72. package/cli/commands/red-team.js +350 -0
  73. package/cli/commands/redteam.js +78 -0
  74. package/cli/commands/remediate.js +797 -0
  75. package/cli/commands/rotate.js +768 -0
  76. package/cli/commands/rules.js +196 -0
  77. package/cli/commands/scan-mcp.js +534 -0
  78. package/cli/commands/scan-skill.js +588 -0
  79. package/cli/commands/scan-standard.js +251 -0
  80. package/cli/commands/scan.js +524 -0
  81. package/cli/commands/score.js +449 -0
  82. package/cli/commands/shell.js +514 -0
  83. package/cli/commands/team-report.js +398 -0
  84. package/cli/commands/undo.js +161 -0
  85. package/cli/commands/update-intel.js +126 -0
  86. package/cli/commands/vibe-check.js +276 -0
  87. package/cli/commands/watch.js +757 -0
  88. package/cli/commands/web.js +63 -0
  89. package/cli/core/ast/guardrail-detector.js +141 -0
  90. package/cli/core/ast/index.js +22 -0
  91. package/cli/core/ast/parser.js +676 -0
  92. package/cli/core/ast/scope-tree.js +287 -0
  93. package/cli/core/ast/taint-tracker.js +158 -0
  94. package/cli/core/branding.js +37 -0
  95. package/cli/core/env.js +38 -0
  96. package/cli/core/errors.js +61 -0
  97. package/cli/core/fs.js +62 -0
  98. package/cli/core/output/compliance.js +90 -0
  99. package/cli/core/output/html-theme.js +158 -0
  100. package/cli/core/output/index.js +57 -0
  101. package/cli/core/output/json.js +48 -0
  102. package/cli/core/output/sarif.js +240 -0
  103. package/cli/core/version.js +67 -0
  104. package/cli/core/web/jobs.js +183 -0
  105. package/cli/core/web/projects.js +146 -0
  106. package/cli/core/web/server.js +439 -0
  107. package/cli/data/atlas-knowledge.json +5640 -0
  108. package/cli/data/eaa-catalog.json +39 -0
  109. package/cli/data/known-mcps.json +26 -0
  110. package/cli/data/probes/prompt-injection-corpus.json +271 -0
  111. package/cli/data/threat-intel.json +85 -0
  112. package/cli/data/threatpacks/latest.json +41 -0
  113. package/cli/hooks/patterns.js +313 -0
  114. package/cli/hooks/post-tool-use.js +140 -0
  115. package/cli/hooks/pre-tool-use.js +186 -0
  116. package/cli/index.js +90 -0
  117. package/cli/providers/llm-provider.js +766 -0
  118. package/cli/utils/autofix-rules.js +74 -0
  119. package/cli/utils/cache-manager.js +310 -0
  120. package/cli/utils/compliance-map.js +191 -0
  121. package/cli/utils/entropy.js +132 -0
  122. package/cli/utils/fix-ledger.js +127 -0
  123. package/cli/utils/hermes-tool-registry.js +252 -0
  124. package/cli/utils/intel/cache.js +61 -0
  125. package/cli/utils/intel/http.js +88 -0
  126. package/cli/utils/intel/index.js +235 -0
  127. package/cli/utils/intel/merge.js +229 -0
  128. package/cli/utils/intel/sources/epss.js +54 -0
  129. package/cli/utils/intel/sources/ghsa.js +81 -0
  130. package/cli/utils/intel/sources/gitguardian.js +40 -0
  131. package/cli/utils/intel/sources/gitleaks.js +101 -0
  132. package/cli/utils/intel/sources/kev.js +38 -0
  133. package/cli/utils/intel/sources/nvd.js +84 -0
  134. package/cli/utils/intel/sources/osv.js +132 -0
  135. package/cli/utils/intel/sources/phylum.js +44 -0
  136. package/cli/utils/intel/sources/snyk.js +46 -0
  137. package/cli/utils/intel/sources/socket.js +69 -0
  138. package/cli/utils/intel/sources/sonatype.js +84 -0
  139. package/cli/utils/intel/sources/threatpack.js +69 -0
  140. package/cli/utils/mcp-trust.js +60 -0
  141. package/cli/utils/output.js +251 -0
  142. package/cli/utils/patterns.js +1130 -0
  143. package/cli/utils/pdf-generator.js +94 -0
  144. package/cli/utils/plugin-loader.js +364 -0
  145. package/cli/utils/rule-import.js +228 -0
  146. package/cli/utils/rule-registry.js +426 -0
  147. package/cli/utils/scan-fingerprint.js +109 -0
  148. package/cli/utils/scan-playbook.js +312 -0
  149. package/cli/utils/score-history.js +119 -0
  150. package/cli/utils/secrets-verifier.js +247 -0
  151. package/cli/utils/security-memory.js +296 -0
  152. package/cli/utils/standards/atlas-knowledge.js +87 -0
  153. package/cli/utils/standards/index.js +127 -0
  154. package/cli/utils/standards/sources/avid.js +45 -0
  155. package/cli/utils/standards/sources/eu-ai-act.js +89 -0
  156. package/cli/utils/standards/sources/google-saif.js +39 -0
  157. package/cli/utils/standards/sources/iso-42001.js +94 -0
  158. package/cli/utils/standards/sources/mitre-atlas.js +54 -0
  159. package/cli/utils/standards/sources/nist-ai-600-1.js +45 -0
  160. package/cli/utils/standards/sources/owasp-llm.js +45 -0
  161. package/cli/utils/standards/sources/owasp-ml.js +45 -0
  162. package/cli/utils/threat-intel.js +265 -0
  163. package/configs/firebase/firestore-rules.txt +215 -0
  164. package/configs/firebase/security-checklist.md +236 -0
  165. package/configs/firebase/storage-rules.txt +206 -0
  166. package/configs/gitignore-template +258 -0
  167. package/configs/nextjs-security-headers.js +220 -0
  168. package/configs/praxisignore-template +50 -0
  169. package/configs/supabase/secure-client.ts +225 -0
  170. package/configs/supabase/security-checklist.md +278 -0
  171. package/docs/THIRD_PARTY_NOTICES.md +26 -0
  172. package/docs/THREAT_INTEL.md +292 -0
  173. package/docs/USAGE.md +1205 -0
  174. package/docs/design/WEB-UI.md +82 -0
  175. package/package.json +71 -0
  176. package/scripts/check-determinism.mjs +119 -0
  177. package/snippets/README.md +122 -0
  178. package/snippets/api-security/api-security-checklist.md +412 -0
  179. package/snippets/api-security/cors-config.ts +322 -0
  180. package/snippets/api-security/input-validation.ts +430 -0
  181. package/snippets/auth/jwt-checklist.md +322 -0
  182. package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
  183. package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
@@ -0,0 +1,857 @@
1
+ /**
2
+ * SupplyChainAudit Agent
3
+ * =======================
4
+ *
5
+ * Comprehensive supply chain security analysis.
6
+ * Goes beyond npm audit: checks for dependency confusion,
7
+ * typosquatting, malicious install scripts, lockfile integrity,
8
+ * EPSS scoring, and KEV flagging.
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { BaseAgent, createFinding } from './base-agent.js';
14
+ import { ThreatIntel } from '../utils/threat-intel.js';
15
+
16
+ // =============================================================================
17
+ // KNOWN-COMPROMISED PACKAGE IOC LIST
18
+ // Source: TeamPCP/CanisterWorm campaign (March 2026) + prior incidents
19
+ // Format: { name, badVersions: [exact], note }
20
+ // =============================================================================
21
+ const COMPROMISED_PACKAGES = [
22
+ {
23
+ name: 'litellm',
24
+ badVersions: ['1.82.7', '1.82.8'],
25
+ note: 'TeamPCP supply chain attack (Mar 24 2026). Multi-stage credential stealer targeting SSH keys, cloud tokens, and AI API keys.',
26
+ },
27
+ {
28
+ name: 'axios',
29
+ badVersions: ['1.8.2'],
30
+ note: 'TeamPCP/CanisterWorm campaign (Mar 31 2026). Attributed to Sapphire Sleet (North Korean state actor) by Microsoft Threat Intelligence. Malicious publish connected to C2 domain delivering a second-stage RAT with persistence.',
31
+ },
32
+ {
33
+ name: 'telnyx',
34
+ badVersions: ['2.1.5'],
35
+ note: 'TeamPCP campaign (Mar 27 2026). Compromised PyPI release exfiltrated credentials.',
36
+ },
37
+ ];
38
+
39
+ // Packages that have no reason to depend on ICP/Internet Computer blockchain
40
+ // but CanisterWorm injected @dfinity/agent as its decentralized C2 mechanism
41
+ const ICP_BLOCKCHAIN_PACKAGES = [
42
+ '@dfinity/agent',
43
+ '@dfinity/candid',
44
+ '@dfinity/principal',
45
+ 'ic-agent',
46
+ ];
47
+
48
+ // Common packages that are often typosquatted
49
+ const POPULAR_PACKAGES = [
50
+ 'lodash', 'express', 'react', 'axios', 'moment', 'request',
51
+ 'chalk', 'commander', 'debug', 'uuid', 'dotenv', 'cors',
52
+ 'body-parser', 'jsonwebtoken', 'bcrypt', 'mongoose', 'sequelize',
53
+ 'webpack', 'babel', 'eslint', 'prettier', 'typescript',
54
+ 'next', 'nuxt', 'svelte', 'vue', 'angular',
55
+ ];
56
+
57
+ // Well-known packages that happen to be close to other popular names
58
+ // (not typosquats — verified legitimate packages)
59
+ const KNOWN_SAFE = new Set([
60
+ 'ora', 'got', 'ink', 'yup', 'joi', 'ava', 'tap', 'npm', 'nwb',
61
+ 'pug', 'koa', 'hap', 'ejs', 'csv', 'ws', 'pg', 'ms',
62
+ ]);
63
+
64
+ // Known malicious package name patterns
65
+ export const SUSPICIOUS_NAME_PATTERNS = [
66
+ /^@[^/]+\/[^/]+-[0-9]+$/, // @scope/package-123 (random suffix)
67
+ /^[a-z]+-[a-z]+-[a-z]+-[a-z]+$/, // overly-generic multi-word names
68
+ ];
69
+
70
+ export class SupplyChainAudit extends BaseAgent {
71
+ constructor() {
72
+ super('SupplyChainAudit', 'Comprehensive supply chain security audit', 'supply-chain');
73
+ }
74
+
75
+ async analyze(context) {
76
+ const { rootPath } = context;
77
+ const findings = [];
78
+
79
+ // ── 1. Check package.json ─────────────────────────────────────────────────
80
+ const pkgPath = path.join(rootPath, 'package.json');
81
+ if (fs.existsSync(pkgPath)) {
82
+ try {
83
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
84
+ const allDeps = {
85
+ ...(pkg.dependencies || {}),
86
+ ...(pkg.devDependencies || {}),
87
+ ...(pkg.optionalDependencies || {}),
88
+ };
89
+
90
+ // ── Typosquatting detection ───────────────────────────────────────────
91
+ for (const depName of Object.keys(allDeps)) {
92
+ if (KNOWN_SAFE.has(depName)) continue;
93
+ for (const popular of POPULAR_PACKAGES) {
94
+ const distance = this.levenshtein(depName, popular);
95
+ if (distance > 0 && distance <= 2 && depName !== popular) {
96
+ findings.push(createFinding({
97
+ file: pkgPath,
98
+ line: 0,
99
+ severity: 'high',
100
+ category: 'supply-chain',
101
+ rule: 'TYPOSQUAT_SUSPECT',
102
+ title: `Possible Typosquat: "${depName}" (similar to "${popular}")`,
103
+ description: `Package "${depName}" is ${distance} character(s) away from popular package "${popular}". This could be a typosquatting attempt.`,
104
+ matched: depName,
105
+ fix: `Verify this is the intended package. Did you mean "${popular}"?`,
106
+ }));
107
+ }
108
+ }
109
+ }
110
+
111
+ // ── Deprecated/suspicious version pins ───────────────────────────────
112
+ for (const [name, version] of Object.entries(allDeps)) {
113
+ if (typeof version === 'string' && version.startsWith('git+')) {
114
+ findings.push(createFinding({
115
+ file: pkgPath,
116
+ line: 0,
117
+ severity: 'high',
118
+ category: 'supply-chain',
119
+ rule: 'GIT_DEPENDENCY',
120
+ title: `Git Dependency: ${name}`,
121
+ description: `"${name}" is installed from a git URL. Git dependencies bypass registry integrity checks.`,
122
+ matched: `${name}: ${version}`,
123
+ fix: 'Pin to a specific commit hash or use a published npm package version',
124
+ }));
125
+ }
126
+
127
+ if (typeof version === 'string' && version.startsWith('http')) {
128
+ findings.push(createFinding({
129
+ file: pkgPath,
130
+ line: 0,
131
+ severity: 'critical',
132
+ category: 'supply-chain',
133
+ rule: 'URL_DEPENDENCY',
134
+ title: `URL Dependency: ${name}`,
135
+ description: `"${name}" is installed from a URL. This bypasses npm registry and integrity checks.`,
136
+ matched: `${name}: ${version}`,
137
+ fix: 'Publish the package to npm or use a private registry',
138
+ }));
139
+ }
140
+
141
+ if (typeof version === 'string' && version === '*') {
142
+ findings.push(createFinding({
143
+ file: pkgPath,
144
+ line: 0,
145
+ severity: 'high',
146
+ category: 'supply-chain',
147
+ rule: 'WILDCARD_VERSION',
148
+ title: `Wildcard Version: ${name}`,
149
+ description: `"${name}" uses "*" version which accepts any version including malicious updates.`,
150
+ matched: `${name}: *`,
151
+ fix: 'Pin to a specific version or use a caret range: "^x.y.z"',
152
+ }));
153
+ }
154
+ }
155
+
156
+ // ── Install scripts ──────────────────────────────────────────────────
157
+ if (pkg.scripts) {
158
+ const dangerousScripts = ['preinstall', 'postinstall', 'preuninstall', 'postuninstall'];
159
+ for (const script of dangerousScripts) {
160
+ if (pkg.scripts[script]) {
161
+ const cmd = pkg.scripts[script];
162
+ const suspicious = /curl|wget|bash|sh\s|powershell|eval|base64|nc\s|ncat/i.test(cmd);
163
+ if (suspicious) {
164
+ findings.push(createFinding({
165
+ file: pkgPath,
166
+ line: 0,
167
+ severity: 'critical',
168
+ category: 'supply-chain',
169
+ rule: 'SUSPICIOUS_INSTALL_SCRIPT',
170
+ title: `Suspicious ${script} Script`,
171
+ description: `The ${script} script contains potentially dangerous commands: ${cmd.slice(0, 100)}`,
172
+ matched: cmd,
173
+ fix: 'Review and remove suspicious install scripts',
174
+ }));
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ } catch { /* skip parse errors */ }
181
+ }
182
+
183
+ // ── 2. Dependency confusion detection ─────────────────────────────────────
184
+ if (fs.existsSync(pkgPath)) {
185
+ try {
186
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
187
+ const allDeps = {
188
+ ...(pkg.dependencies || {}),
189
+ ...(pkg.devDependencies || {}),
190
+ };
191
+
192
+ // Check for scoped packages without registry pinning
193
+ const scopedPkgs = Object.keys(allDeps).filter(n => n.startsWith('@'));
194
+ if (scopedPkgs.length > 0) {
195
+ const npmrcPath = path.join(rootPath, '.npmrc');
196
+ const yarnrcPath = path.join(rootPath, '.yarnrc');
197
+ const yarnrcYmlPath = path.join(rootPath, '.yarnrc.yml');
198
+ const hasRegistryConfig = [npmrcPath, yarnrcPath, yarnrcYmlPath].some(p => {
199
+ if (!fs.existsSync(p)) return false;
200
+ const content = this.readFile(p) || '';
201
+ // Check if any scope is pinned to a registry
202
+ return /@[^:]+:registry/i.test(content) || /npmRegistryServer/i.test(content);
203
+ });
204
+
205
+ // Extract unique scopes
206
+ const scopes = [...new Set(scopedPkgs.map(n => n.split('/')[0]))];
207
+ // Check if this looks like an internal scope (not well-known public ones)
208
+ const publicScopes = new Set([
209
+ '@types', '@babel', '@eslint', '@jest', '@testing-library',
210
+ '@react-native', '@angular', '@vue', '@nuxt', '@next',
211
+ '@emotion', '@mui', '@radix-ui', '@tanstack', '@trpc',
212
+ '@prisma', '@supabase', '@aws-sdk', '@azure', '@google-cloud',
213
+ '@octokit', '@sentry', '@stripe', '@anthropic-ai', '@openai',
214
+ ]);
215
+ const internalScopes = scopes.filter(s => !publicScopes.has(s));
216
+
217
+ if (internalScopes.length > 0 && !hasRegistryConfig) {
218
+ findings.push(createFinding({
219
+ file: pkgPath,
220
+ line: 0,
221
+ severity: 'high',
222
+ category: 'supply-chain',
223
+ rule: 'DEPCONF_NO_SCOPE_REGISTRY',
224
+ title: `Scoped Packages Without Registry Pin: ${internalScopes.join(', ')}`,
225
+ description: `Scoped packages (${internalScopes.join(', ')}) found without a .npmrc pinning the scope to a private registry. An attacker could claim the scope on the public npm registry.`,
226
+ matched: internalScopes.join(', '),
227
+ confidence: 'medium',
228
+ fix: 'Add to .npmrc: @yourscope:registry=https://your-private-registry.com/',
229
+ }));
230
+ }
231
+ }
232
+
233
+ // Check for suspicious install scripts in dependencies
234
+ const nodeModulesPath = path.join(rootPath, 'node_modules');
235
+ if (fs.existsSync(nodeModulesPath)) {
236
+ for (const depName of Object.keys(allDeps).slice(0, 50)) {
237
+ const depPkgPath = path.join(nodeModulesPath, depName, 'package.json');
238
+ if (!fs.existsSync(depPkgPath)) continue;
239
+ try {
240
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
241
+ const scripts = depPkg.scripts || {};
242
+ for (const hook of ['preinstall', 'install', 'postinstall']) {
243
+ const cmd = scripts[hook];
244
+ if (!cmd) continue;
245
+ if (/(?:curl|wget|powershell|base64\s|eval\s|nc\s|ncat|\.sh\b)/i.test(cmd)) {
246
+ findings.push(createFinding({
247
+ file: depPkgPath,
248
+ line: 0,
249
+ severity: 'critical',
250
+ category: 'supply-chain',
251
+ rule: 'DEPCONF_SUSPICIOUS_INSTALL_SCRIPT',
252
+ title: `Suspicious ${hook} in ${depName}`,
253
+ description: `Dependency "${depName}" has a suspicious ${hook} script: ${cmd.slice(0, 120)}`,
254
+ matched: cmd.slice(0, 200),
255
+ fix: 'Review the script. If untrusted, remove the dependency or use npm with --ignore-scripts',
256
+ }));
257
+ }
258
+ }
259
+ } catch { /* skip */ }
260
+ }
261
+ }
262
+ } catch { /* skip */ }
263
+ }
264
+
265
+ // ── 3. Check lockfile integrity ── ───────────────────────────────────────────
266
+ const lockFiles = [
267
+ { file: 'package-lock.json', manager: 'npm' },
268
+ { file: 'yarn.lock', manager: 'yarn' },
269
+ { file: 'pnpm-lock.yaml', manager: 'pnpm' },
270
+ { file: 'bun.lockb', manager: 'bun' },
271
+ ];
272
+
273
+ const hasPackageJson = fs.existsSync(pkgPath);
274
+ let hasLockfile = false;
275
+
276
+ for (const { file, manager } of lockFiles) {
277
+ if (fs.existsSync(path.join(rootPath, file))) {
278
+ hasLockfile = true;
279
+ }
280
+ }
281
+
282
+ if (hasPackageJson && !hasLockfile) {
283
+ findings.push(createFinding({
284
+ file: pkgPath,
285
+ line: 0,
286
+ severity: 'high',
287
+ category: 'supply-chain',
288
+ rule: 'MISSING_LOCKFILE',
289
+ title: 'No Lock File Found',
290
+ description: 'No package-lock.json, yarn.lock, or pnpm-lock.yaml found. Without a lockfile, installs are non-deterministic and vulnerable to dependency confusion.',
291
+ matched: 'package.json without lockfile',
292
+ fix: 'Run npm install, yarn install, or pnpm install to generate a lockfile, then commit it',
293
+ }));
294
+ }
295
+
296
+ // ── 4. Check .npmrc for security settings ─────────────────────────────────
297
+ const npmrcPath = path.join(rootPath, '.npmrc');
298
+ if (fs.existsSync(npmrcPath)) {
299
+ const content = this.readFile(npmrcPath) || '';
300
+ if (content.includes('ignore-scripts=true')) {
301
+ // Good — scripts are disabled
302
+ }
303
+ if (content.includes('registry=') && !content.includes('registry=https://registry.npmjs.org')) {
304
+ findings.push(createFinding({
305
+ file: npmrcPath,
306
+ line: 0,
307
+ severity: 'medium',
308
+ category: 'supply-chain',
309
+ rule: 'CUSTOM_REGISTRY',
310
+ title: 'Custom NPM Registry Configured',
311
+ description: 'A custom npm registry is configured. Verify it is trusted and uses HTTPS.',
312
+ matched: content.match(/registry=.*/)?.[0] || '',
313
+ confidence: 'medium',
314
+ fix: 'Verify the registry URL is trusted and uses HTTPS',
315
+ }));
316
+ }
317
+ }
318
+
319
+ // ── 5. Package behavioral signals (Socket-style) ─────────────────────────
320
+ if (fs.existsSync(pkgPath)) {
321
+ try {
322
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
323
+ const allDeps = {
324
+ ...(pkg.dependencies || {}),
325
+ ...(pkg.devDependencies || {}),
326
+ };
327
+
328
+ // Scan node_modules for behavioral red flags
329
+ const nodeModulesPath = path.join(rootPath, 'node_modules');
330
+ if (fs.existsSync(nodeModulesPath)) {
331
+ for (const depName of Object.keys(allDeps).slice(0, 50)) {
332
+ const depDir = path.join(nodeModulesPath, depName);
333
+ const depPkgPath = path.join(depDir, 'package.json');
334
+ if (!fs.existsSync(depPkgPath)) continue;
335
+ try {
336
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
337
+
338
+ // Check for postinstall scripts with network/eval calls
339
+ const scripts = depPkg.scripts || {};
340
+ for (const hook of ['preinstall', 'install', 'postinstall']) {
341
+ const cmd = scripts[hook];
342
+ if (!cmd) continue;
343
+ if (/node\s+-e|node\s+--eval/.test(cmd)) {
344
+ findings.push(createFinding({
345
+ file: depPkgPath,
346
+ line: 0,
347
+ severity: 'high',
348
+ category: 'supply-chain',
349
+ rule: 'BEHAVIORAL_INLINE_EVAL',
350
+ title: `Inline Code Execution in ${hook}: ${depName}`,
351
+ description: `Dependency "${depName}" runs inline Node.js code during ${hook}. This is a common pattern in malicious packages.`,
352
+ matched: cmd.slice(0, 200),
353
+ fix: 'Review the inline code. Consider using --ignore-scripts or removing the dependency.',
354
+ }));
355
+ }
356
+ }
357
+ } catch { /* skip */ }
358
+ }
359
+ }
360
+
361
+ // Detect obfuscated code patterns in dependencies
362
+ const codeFiles = (context.files || []).filter(f =>
363
+ f.includes('node_modules') &&
364
+ !f.includes('node_modules/.cache') &&
365
+ path.extname(f).toLowerCase() === '.js' &&
366
+ !path.basename(f).endsWith('.min.js')
367
+ ).slice(0, 30); // Sample up to 30 files
368
+
369
+ for (const file of codeFiles) {
370
+ const content = this.readFile(file);
371
+ if (!content || content.length < 100) continue;
372
+
373
+ // Excessive hex encoding
374
+ const hexMatches = (content.match(/\\x[0-9a-fA-F]{2}/g) || []).length;
375
+ if (hexMatches > 20) {
376
+ findings.push(createFinding({
377
+ file,
378
+ line: 1,
379
+ severity: 'high',
380
+ category: 'supply-chain',
381
+ rule: 'BEHAVIORAL_HEX_OBFUSCATION',
382
+ title: 'Obfuscated Code: Excessive Hex Encoding',
383
+ description: `File contains ${hexMatches} hex-encoded sequences. Common in malicious packages trying to hide payload.`,
384
+ matched: `${hexMatches} hex sequences detected`,
385
+ fix: 'Inspect the deobfuscated code. Consider removing this dependency.',
386
+ }));
387
+ }
388
+
389
+ // Excessive String.fromCharCode
390
+ const charCodeMatches = (content.match(/String\.fromCharCode/g) || []).length;
391
+ if (charCodeMatches > 5) {
392
+ findings.push(createFinding({
393
+ file,
394
+ line: 1,
395
+ severity: 'high',
396
+ category: 'supply-chain',
397
+ rule: 'BEHAVIORAL_CHARCODE_OBFUSCATION',
398
+ title: 'Obfuscated Code: Excessive String.fromCharCode',
399
+ description: `File contains ${charCodeMatches} String.fromCharCode calls. Common obfuscation technique in malicious packages.`,
400
+ matched: `${charCodeMatches} String.fromCharCode calls`,
401
+ fix: 'Inspect the deobfuscated code. Consider removing this dependency.',
402
+ }));
403
+ }
404
+
405
+ // Base64 decode chains
406
+ const base64Matches = (content.match(/Buffer\.from\s*\([^,]+,\s*['"]base64['"]\)/g) || []).length;
407
+ if (base64Matches > 3) {
408
+ findings.push(createFinding({
409
+ file,
410
+ line: 1,
411
+ severity: 'medium',
412
+ category: 'supply-chain',
413
+ rule: 'BEHAVIORAL_BASE64_DECODE',
414
+ title: 'Suspicious: Multiple Base64 Decode Operations',
415
+ description: `File contains ${base64Matches} base64 decode operations. May indicate hidden payload.`,
416
+ matched: `${base64Matches} base64 decode operations`,
417
+ confidence: 'medium',
418
+ fix: 'Review what data is being decoded. Legitimate use is possible but warrants inspection.',
419
+ }));
420
+ }
421
+ }
422
+
423
+ // Detect unused dependencies (in package.json but never imported)
424
+ const projectFiles = (context.files || []).filter(f =>
425
+ !f.includes('node_modules') &&
426
+ ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(path.extname(f).toLowerCase())
427
+ );
428
+
429
+ if (projectFiles.length > 0 && projectFiles.length < 500) {
430
+ const allImports = new Set();
431
+ for (const file of projectFiles) {
432
+ const content = this.readFile(file);
433
+ if (!content) continue;
434
+ // Capture import/require module names
435
+ const importMatches = content.matchAll(/(?:from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g);
436
+ for (const m of importMatches) {
437
+ const mod = (m[1] || m[2] || '').split('/')[0]; // Get package name (not subpath)
438
+ if (mod && !mod.startsWith('.')) allImports.add(mod);
439
+ // Handle scoped packages
440
+ const fullMod = m[1] || m[2] || '';
441
+ if (fullMod.startsWith('@')) {
442
+ const scopedPkg = fullMod.split('/').slice(0, 2).join('/');
443
+ allImports.add(scopedPkg);
444
+ }
445
+ }
446
+ }
447
+
448
+ const prodDeps = Object.keys(pkg.dependencies || {});
449
+ for (const dep of prodDeps) {
450
+ if (!allImports.has(dep) && !dep.startsWith('@types/')) {
451
+ findings.push(createFinding({
452
+ file: pkgPath,
453
+ line: 0,
454
+ severity: 'low',
455
+ category: 'supply-chain',
456
+ rule: 'UNUSED_DEPENDENCY',
457
+ title: `Unused Dependency: ${dep}`,
458
+ description: `"${dep}" is in dependencies but never imported in project code. Unused dependencies increase attack surface.`,
459
+ matched: dep,
460
+ confidence: 'low',
461
+ fix: `Remove if unused: npm uninstall ${dep}`,
462
+ }));
463
+ }
464
+ }
465
+ }
466
+
467
+ } catch { /* skip */ }
468
+ }
469
+
470
+ // ── 6. npm token scope in .npmrc ──────────────────────────────────────────
471
+ if (fs.existsSync(npmrcPath)) {
472
+ const content = this.readFile(npmrcPath) || '';
473
+ // Detect auth tokens stored in .npmrc — a prerequisite for worm spread
474
+ const tokenLines = content.split('\n').filter(l => /_authToken\s*=/.test(l));
475
+ for (const line of tokenLines) {
476
+ const isScopedToPackage = /\/\/registry\.npmjs\.org\/.+:_authToken/.test(line);
477
+ // Flag tokens that appear to be publish-level (not scoped to a single package)
478
+ findings.push(createFinding({
479
+ file: npmrcPath,
480
+ line: 0,
481
+ severity: 'high',
482
+ category: 'supply-chain',
483
+ rule: 'NPMRC_AUTH_TOKEN_EXPOSED',
484
+ title: 'npm Auth Token in .npmrc',
485
+ description: `An npm auth token is stored in .npmrc. ${isScopedToPackage ? 'Verify it is scoped to only the packages it needs to publish.' : 'If this token has publish rights, a compromised install script or dependency can steal it and spread a worm (CanisterWorm attack pattern).'} Commit this file only if absolutely necessary; prefer CI secret injection.`,
486
+ matched: line.replace(/=.+/, '=***'),
487
+ fix: 'Use npm token create --cidr-whitelist or scope tokens per-package. Never commit .npmrc with auth tokens.',
488
+ }));
489
+ }
490
+ }
491
+
492
+ // ── 7. Check Python requirements ──────────────────────────────────────────
493
+ const reqPath = path.join(rootPath, 'requirements.txt');
494
+ if (fs.existsSync(reqPath)) {
495
+ const content = this.readFile(reqPath) || '';
496
+ const lines = content.split('\n');
497
+ for (let i = 0; i < lines.length; i++) {
498
+ const line = lines[i].trim();
499
+ if (!line || line.startsWith('#')) continue;
500
+
501
+ // Unpinned versions
502
+ if (!line.includes('==') && !line.includes('>=') && !line.includes('~=') && !line.includes('@')) {
503
+ findings.push(createFinding({
504
+ file: reqPath,
505
+ line: i + 1,
506
+ severity: 'medium',
507
+ category: 'supply-chain',
508
+ rule: 'UNPINNED_PYTHON_DEP',
509
+ title: `Unpinned Python Dependency: ${line}`,
510
+ description: 'Python dependency without version pin. Pin to a specific version for reproducible builds.',
511
+ matched: line,
512
+ fix: `Pin version: ${line}==x.y.z`,
513
+ }));
514
+ }
515
+
516
+ // Git/URL dependencies
517
+ if (line.includes('git+') || line.startsWith('http')) {
518
+ findings.push(createFinding({
519
+ file: reqPath,
520
+ line: i + 1,
521
+ severity: 'high',
522
+ category: 'supply-chain',
523
+ rule: 'GIT_PYTHON_DEP',
524
+ title: `Git/URL Python Dependency: ${line.slice(0, 60)}`,
525
+ description: 'Installing from git/URL bypasses PyPI integrity checks.',
526
+ matched: line,
527
+ fix: 'Publish to PyPI or pin to a specific commit hash',
528
+ }));
529
+ }
530
+ }
531
+ }
532
+
533
+ // ── 8. Compromised package IOC matching (npm + PyPI) ──────────────────────
534
+ // Two layers: (a) the hardcoded COMPROMISED_PACKAGES list for offline
535
+ // use, and (b) the merged ThreatIntel feed (OSV + GHSA + paid sources)
536
+ // for live coverage. Matches from either layer are deduped by
537
+ // (file, package, version).
538
+ const iocSources = [];
539
+ const iocSeen = new Set();
540
+ const addIoc = (entry) => {
541
+ const key = `${entry.file}::${entry.name}@${entry.version}`;
542
+ if (iocSeen.has(key)) return;
543
+ iocSeen.add(key);
544
+ iocSources.push(entry);
545
+ };
546
+
547
+ const collectAdvisories = (name, version, ecosystem, file) => {
548
+ const advisories = [];
549
+ try {
550
+ const osv = ThreatIntel.lookupOsv(name, version, ecosystem);
551
+ for (const a of osv) {
552
+ if (a.isAffected === false) continue;
553
+ advisories.push({ source: 'osv', ...a });
554
+ }
555
+ const ghsa = ThreatIntel.lookupGhsa(name, ecosystem);
556
+ for (const a of ghsa) advisories.push({ source: 'ghsa', ...a });
557
+ } catch { /* threat-intel optional; ignore */ }
558
+ return advisories;
559
+ };
560
+
561
+ // npm packages
562
+ if (fs.existsSync(pkgPath)) {
563
+ try {
564
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
565
+ const allDeps = {
566
+ ...(pkg.dependencies || {}),
567
+ ...(pkg.devDependencies || {}),
568
+ ...(pkg.optionalDependencies || {}),
569
+ };
570
+ for (const [name, version] of Object.entries(allDeps)) {
571
+ const bare = String(version).replace(/^[\^~>=<]+/, '').trim();
572
+
573
+ // Hardcoded IOC list (offline fallback)
574
+ const ioc = COMPROMISED_PACKAGES.find(c => c.name === name);
575
+ if (ioc && ioc.badVersions.includes(bare)) {
576
+ addIoc({ file: pkgPath, name, version: bare, note: ioc.note, source: 'seed' });
577
+ }
578
+
579
+ // Live threat-intel lookup
580
+ for (const adv of collectAdvisories(name, bare, 'npm', pkgPath)) {
581
+ addIoc({
582
+ file: pkgPath, name, version: bare,
583
+ note: adv.summary || adv.note || `${adv.source.toUpperCase()} advisory`,
584
+ source: adv.source,
585
+ cves: adv.cves || (adv.cveId ? [adv.cveId] : []),
586
+ advisorySeverity: adv.severity,
587
+ });
588
+ }
589
+ }
590
+ } catch { /* skip */ }
591
+ }
592
+
593
+ // Python requirements.txt
594
+ if (fs.existsSync(path.join(rootPath, 'requirements.txt'))) {
595
+ const reqFile = path.join(rootPath, 'requirements.txt');
596
+ const lines = (this.readFile(reqFile) || '').split('\n');
597
+ for (const line of lines) {
598
+ const m = line.trim().match(/^([\w-]+)==([\d.]+)/);
599
+ if (!m) continue;
600
+ const [, name, version] = m;
601
+ const lname = name.toLowerCase();
602
+
603
+ const ioc = COMPROMISED_PACKAGES.find(c => c.name === lname);
604
+ if (ioc && ioc.badVersions.includes(version)) {
605
+ addIoc({ file: reqFile, name: lname, version, note: ioc.note, source: 'seed' });
606
+ }
607
+
608
+ for (const adv of collectAdvisories(lname, version, 'PyPI', reqFile)) {
609
+ addIoc({
610
+ file: reqFile, name: lname, version,
611
+ note: adv.summary || adv.note || `${adv.source.toUpperCase()} advisory`,
612
+ source: adv.source,
613
+ cves: adv.cves || (adv.cveId ? [adv.cveId] : []),
614
+ advisorySeverity: adv.severity,
615
+ });
616
+ }
617
+ }
618
+ }
619
+
620
+ for (const ioc of iocSources) {
621
+ // Tag with KEV / EPSS for prioritization
622
+ const tags = enrichWithCveSignals(ioc.cves || []);
623
+ const severity = tags.kev ? 'critical' : (ioc.advisorySeverity || 'critical');
624
+ const sourceLabel = ioc.source === 'seed' ? 'bundled IOC list' : ioc.source.toUpperCase();
625
+ const epssNote = tags.maxEpss != null
626
+ ? ` EPSS=${(tags.maxEpss * 100).toFixed(1)}% (${tags.maxEpssCve}).`
627
+ : '';
628
+ const kevNote = tags.kev ? ' Listed in CISA KEV (actively exploited).' : '';
629
+
630
+ findings.push(createFinding({
631
+ file: ioc.file,
632
+ line: 0,
633
+ severity,
634
+ category: 'supply-chain',
635
+ rule: 'KNOWN_COMPROMISED_PACKAGE',
636
+ title: `Known-Compromised Package: ${ioc.name}@${ioc.version}`,
637
+ description: `${ioc.name}@${ioc.version} flagged via ${sourceLabel}. ${ioc.note}${kevNote}${epssNote}`,
638
+ matched: `${ioc.name}@${ioc.version}`,
639
+ fix: `Update immediately to the latest safe release and rotate any credentials that may have been exfiltrated.`,
640
+ }));
641
+ }
642
+
643
+ // ── 9. Trojanized package behavioral signatures ───────────────────────────
644
+ // Patterns from Axios 1.8.2, LiteLLM 1.82.7, TeamPCP campaign (Mar 2026)
645
+ if (fs.existsSync(path.join(rootPath, 'node_modules'))) {
646
+ try {
647
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
648
+ const allDeps = {
649
+ ...(pkg.dependencies || {}),
650
+ ...(pkg.devDependencies || {}),
651
+ };
652
+
653
+ for (const depName of Object.keys(allDeps).slice(0, 80)) {
654
+ const depDir = path.join(rootPath, 'node_modules', depName);
655
+ const depPkgPath = path.join(depDir, 'package.json');
656
+ if (!fs.existsSync(depPkgPath)) continue;
657
+
658
+ try {
659
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
660
+
661
+ // 9a. Hidden dependencies added by attacker
662
+ // Axios 1.8.2 injected a hidden malicious dep that wasn't in the legitimate version.
663
+ // Check for deps that only exist in certain version ranges.
664
+ const depDeps = depPkg.dependencies || {};
665
+ for (const [subDep, subVer] of Object.entries(depDeps)) {
666
+ // Packages with very high download counts that suddenly gain unknown subdependencies
667
+ if (/^[a-z]+-[a-z0-9]+-[a-z0-9]+$/.test(subDep) && typeof subVer === 'string' && subVer.startsWith('git+')) {
668
+ findings.push(createFinding({
669
+ file: depPkgPath,
670
+ line: 0,
671
+ severity: 'critical',
672
+ category: 'supply-chain',
673
+ rule: 'TROJAN_HIDDEN_DEP',
674
+ title: `Suspicious Hidden Dependency in ${depName}: ${subDep}`,
675
+ description: `"${depName}" depends on "${subDep}" via a git URL. This matches the Axios/TeamPCP trojanization pattern where attackers inject a malicious dependency into a popular package.`,
676
+ matched: `${subDep}: ${subVer}`,
677
+ fix: `Compare this version's dependencies against the official release. If "${subDep}" was not in the previous version, this package may be trojanized.`,
678
+ }));
679
+ }
680
+ }
681
+
682
+ // 9b. Install scripts that read and exfiltrate env vars
683
+ // LiteLLM 1.82.7 harvested AWS/GCP/Azure tokens + SSH keys
684
+ const scripts = depPkg.scripts || {};
685
+ for (const hook of ['preinstall', 'install', 'postinstall']) {
686
+ const cmd = scripts[hook];
687
+ if (!cmd) continue;
688
+
689
+ // Environment variable harvesting
690
+ if (/process\.env|os\.environ|ENV\[|getenv/i.test(cmd) &&
691
+ /https?:|fetch|request|axios|curl|wget|net\./i.test(cmd)) {
692
+ findings.push(createFinding({
693
+ file: depPkgPath,
694
+ line: 0,
695
+ severity: 'critical',
696
+ category: 'supply-chain',
697
+ rule: 'TROJAN_ENV_EXFIL',
698
+ title: `Credential Harvesting in ${hook}: ${depName}`,
699
+ description: `"${depName}" reads environment variables and makes network requests during ${hook}. This is the exact pattern used in the LiteLLM/TeamPCP attack to steal cloud credentials.`,
700
+ matched: cmd.slice(0, 200),
701
+ fix: 'Remove this package immediately. Rotate any credentials (AWS, GCP, Azure tokens, SSH keys) that may have been exfiltrated.',
702
+ }));
703
+ }
704
+
705
+ // SSH key / credential file access in install scripts
706
+ if (/\.ssh|\.aws|\.azure|\.gcp|\.kube|\.docker|credentials|\.npmrc|\.pypirc/i.test(cmd)) {
707
+ findings.push(createFinding({
708
+ file: depPkgPath,
709
+ line: 0,
710
+ severity: 'critical',
711
+ category: 'supply-chain',
712
+ rule: 'TROJAN_CREDENTIAL_ACCESS',
713
+ title: `Credential File Access in ${hook}: ${depName}`,
714
+ description: `"${depName}" accesses credential files (.ssh, .aws, .kube, etc.) during ${hook}. This matches the TeamPCP credential theft pattern.`,
715
+ matched: cmd.slice(0, 200),
716
+ fix: 'Remove this package immediately and rotate all credentials in the accessed directories.',
717
+ }));
718
+ }
719
+ }
720
+
721
+ // 9c. Scan actual JS entry files for runtime exfiltration patterns
722
+ const main = depPkg.main || 'index.js';
723
+ const entryPath = path.join(depDir, main);
724
+ if (fs.existsSync(entryPath)) {
725
+ try {
726
+ const entryContent = fs.readFileSync(entryPath, 'utf-8');
727
+ if (entryContent.length < 500_000) {
728
+ // DNS-based exfiltration (encode data in subdomain)
729
+ if (/dns\.resolve|dns\.lookup/i.test(entryContent) &&
730
+ /process\.env|os\.hostname/i.test(entryContent)) {
731
+ findings.push(createFinding({
732
+ file: entryPath,
733
+ line: 1,
734
+ severity: 'high',
735
+ category: 'supply-chain',
736
+ rule: 'TROJAN_DNS_EXFIL',
737
+ title: `DNS Exfiltration Pattern: ${depName}`,
738
+ description: `"${depName}" combines DNS lookups with system/env data reads — a known technique for exfiltrating data via DNS subdomains to bypass firewalls.`,
739
+ matched: 'dns.resolve + process.env',
740
+ confidence: 'medium',
741
+ fix: 'Inspect the DNS usage. Legitimate packages rarely combine DNS with environment variable reading.',
742
+ }));
743
+ }
744
+
745
+ // WebSocket-based C2
746
+ if (/new\s+WebSocket/i.test(entryContent) &&
747
+ /process\.env|child_process|exec/i.test(entryContent)) {
748
+ findings.push(createFinding({
749
+ file: entryPath,
750
+ line: 1,
751
+ severity: 'critical',
752
+ category: 'supply-chain',
753
+ rule: 'TROJAN_WEBSOCKET_C2',
754
+ title: `WebSocket C2 Pattern: ${depName}`,
755
+ description: `"${depName}" opens WebSocket connections combined with system command execution — consistent with a Remote Access Trojan.`,
756
+ matched: 'WebSocket + child_process',
757
+ fix: 'Remove this package immediately. Scan for persistence mechanisms (cron jobs, startup scripts).',
758
+ }));
759
+ }
760
+ }
761
+ } catch { /* skip */ }
762
+ }
763
+
764
+ } catch { /* skip */ }
765
+ }
766
+ } catch { /* skip */ }
767
+ }
768
+
769
+ // ── 10. Blockchain C2 indicators (CanisterWorm / ICP) ────────────────────
770
+ if (fs.existsSync(path.join(rootPath, 'node_modules'))) {
771
+ try {
772
+ const pkg2 = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
773
+ const allDeps2 = {
774
+ ...(pkg2.dependencies || {}),
775
+ ...(pkg2.devDependencies || {}),
776
+ };
777
+ for (const depName of Object.keys(allDeps2)) {
778
+ const depPkgPath = path.join(rootPath, 'node_modules', depName, 'package.json');
779
+ if (!fs.existsSync(depPkgPath)) continue;
780
+ try {
781
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
782
+ const depDeps = {
783
+ ...(depPkg.dependencies || {}),
784
+ ...(depPkg.devDependencies || {}),
785
+ };
786
+ const suspiciousICP = ICP_BLOCKCHAIN_PACKAGES.filter(p => p in depDeps);
787
+ if (suspiciousICP.length > 0) {
788
+ findings.push(createFinding({
789
+ file: depPkgPath,
790
+ line: 0,
791
+ severity: 'critical',
792
+ category: 'supply-chain',
793
+ rule: 'BLOCKCHAIN_C2_INDICATOR',
794
+ title: `Blockchain C2 Indicator in ${depName}: ${suspiciousICP.join(', ')}`,
795
+ description: `Dependency "${depName}" imports ICP/Internet Computer blockchain packages (${suspiciousICP.join(', ')}). This matches the CanisterWorm attack pattern, which used an ICP canister as a decentralized, takedown-resistant C2 server to coordinate credential exfiltration.`,
796
+ matched: suspiciousICP.join(', '),
797
+ fix: 'Immediately audit or remove this dependency. ICP blockchain packages have no legitimate role in most application dependencies.',
798
+ }));
799
+ }
800
+ } catch { /* skip */ }
801
+ }
802
+ } catch { /* skip */ }
803
+ }
804
+
805
+ return findings;
806
+ }
807
+
808
+ /**
809
+ * Simple Levenshtein distance for typosquatting detection.
810
+ */
811
+ levenshtein(a, b) {
812
+ if (a.length === 0) return b.length;
813
+ if (b.length === 0) return a.length;
814
+
815
+ const matrix = [];
816
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
817
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
818
+
819
+ for (let i = 1; i <= b.length; i++) {
820
+ for (let j = 1; j <= a.length; j++) {
821
+ const cost = b[i - 1] === a[j - 1] ? 0 : 1;
822
+ matrix[i][j] = Math.min(
823
+ matrix[i - 1][j] + 1,
824
+ matrix[i][j - 1] + 1,
825
+ matrix[i - 1][j - 1] + cost
826
+ );
827
+ }
828
+ }
829
+
830
+ return matrix[b.length][a.length];
831
+ }
832
+ }
833
+
834
+ /**
835
+ * Look up KEV / EPSS for a list of CVE IDs and return aggregated tags.
836
+ * Used to elevate severity and decorate descriptions on findings that
837
+ * carry a CVE.
838
+ */
839
+ function enrichWithCveSignals(cves) {
840
+ const result = { kev: false, kevCves: [], maxEpss: null, maxEpssCve: null };
841
+ for (const cve of cves || []) {
842
+ try {
843
+ if (ThreatIntel.isInKev(cve)) {
844
+ result.kev = true;
845
+ result.kevCves.push(cve);
846
+ }
847
+ const epss = ThreatIntel.getEpss(cve);
848
+ if (epss && (result.maxEpss == null || epss.score > result.maxEpss)) {
849
+ result.maxEpss = epss.score;
850
+ result.maxEpssCve = cve;
851
+ }
852
+ } catch { /* threat-intel not loaded; ignore */ }
853
+ }
854
+ return result;
855
+ }
856
+
857
+ export default SupplyChainAudit;