@oculum/scanner 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 (281) hide show
  1. package/dist/formatters/cli-terminal.d.ts +27 -0
  2. package/dist/formatters/cli-terminal.d.ts.map +1 -0
  3. package/dist/formatters/cli-terminal.js +412 -0
  4. package/dist/formatters/cli-terminal.js.map +1 -0
  5. package/dist/formatters/github-comment.d.ts +41 -0
  6. package/dist/formatters/github-comment.d.ts.map +1 -0
  7. package/dist/formatters/github-comment.js +306 -0
  8. package/dist/formatters/github-comment.js.map +1 -0
  9. package/dist/formatters/grouping.d.ts +52 -0
  10. package/dist/formatters/grouping.d.ts.map +1 -0
  11. package/dist/formatters/grouping.js +152 -0
  12. package/dist/formatters/grouping.js.map +1 -0
  13. package/dist/formatters/index.d.ts +9 -0
  14. package/dist/formatters/index.d.ts.map +1 -0
  15. package/dist/formatters/index.js +35 -0
  16. package/dist/formatters/index.js.map +1 -0
  17. package/dist/formatters/vscode-diagnostic.d.ts +103 -0
  18. package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
  19. package/dist/formatters/vscode-diagnostic.js +151 -0
  20. package/dist/formatters/vscode-diagnostic.js.map +1 -0
  21. package/dist/index.d.ts +52 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +648 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/layer1/comments.d.ts +8 -0
  26. package/dist/layer1/comments.d.ts.map +1 -0
  27. package/dist/layer1/comments.js +203 -0
  28. package/dist/layer1/comments.js.map +1 -0
  29. package/dist/layer1/config-audit.d.ts +8 -0
  30. package/dist/layer1/config-audit.d.ts.map +1 -0
  31. package/dist/layer1/config-audit.js +252 -0
  32. package/dist/layer1/config-audit.js.map +1 -0
  33. package/dist/layer1/entropy.d.ts +8 -0
  34. package/dist/layer1/entropy.d.ts.map +1 -0
  35. package/dist/layer1/entropy.js +500 -0
  36. package/dist/layer1/entropy.js.map +1 -0
  37. package/dist/layer1/file-flags.d.ts +7 -0
  38. package/dist/layer1/file-flags.d.ts.map +1 -0
  39. package/dist/layer1/file-flags.js +112 -0
  40. package/dist/layer1/file-flags.js.map +1 -0
  41. package/dist/layer1/index.d.ts +36 -0
  42. package/dist/layer1/index.d.ts.map +1 -0
  43. package/dist/layer1/index.js +132 -0
  44. package/dist/layer1/index.js.map +1 -0
  45. package/dist/layer1/patterns.d.ts +8 -0
  46. package/dist/layer1/patterns.d.ts.map +1 -0
  47. package/dist/layer1/patterns.js +482 -0
  48. package/dist/layer1/patterns.js.map +1 -0
  49. package/dist/layer1/urls.d.ts +8 -0
  50. package/dist/layer1/urls.d.ts.map +1 -0
  51. package/dist/layer1/urls.js +296 -0
  52. package/dist/layer1/urls.js.map +1 -0
  53. package/dist/layer1/weak-crypto.d.ts +7 -0
  54. package/dist/layer1/weak-crypto.d.ts.map +1 -0
  55. package/dist/layer1/weak-crypto.js +291 -0
  56. package/dist/layer1/weak-crypto.js.map +1 -0
  57. package/dist/layer2/ai-agent-tools.d.ts +19 -0
  58. package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
  59. package/dist/layer2/ai-agent-tools.js +528 -0
  60. package/dist/layer2/ai-agent-tools.js.map +1 -0
  61. package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
  62. package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
  63. package/dist/layer2/ai-endpoint-protection.js +332 -0
  64. package/dist/layer2/ai-endpoint-protection.js.map +1 -0
  65. package/dist/layer2/ai-execution-sinks.d.ts +18 -0
  66. package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
  67. package/dist/layer2/ai-execution-sinks.js +496 -0
  68. package/dist/layer2/ai-execution-sinks.js.map +1 -0
  69. package/dist/layer2/ai-fingerprinting.d.ts +7 -0
  70. package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
  71. package/dist/layer2/ai-fingerprinting.js +654 -0
  72. package/dist/layer2/ai-fingerprinting.js.map +1 -0
  73. package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
  74. package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
  75. package/dist/layer2/ai-prompt-hygiene.js +356 -0
  76. package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
  77. package/dist/layer2/ai-rag-safety.d.ts +21 -0
  78. package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
  79. package/dist/layer2/ai-rag-safety.js +459 -0
  80. package/dist/layer2/ai-rag-safety.js.map +1 -0
  81. package/dist/layer2/ai-schema-validation.d.ts +25 -0
  82. package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
  83. package/dist/layer2/ai-schema-validation.js +375 -0
  84. package/dist/layer2/ai-schema-validation.js.map +1 -0
  85. package/dist/layer2/auth-antipatterns.d.ts +20 -0
  86. package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
  87. package/dist/layer2/auth-antipatterns.js +333 -0
  88. package/dist/layer2/auth-antipatterns.js.map +1 -0
  89. package/dist/layer2/byok-patterns.d.ts +12 -0
  90. package/dist/layer2/byok-patterns.d.ts.map +1 -0
  91. package/dist/layer2/byok-patterns.js +299 -0
  92. package/dist/layer2/byok-patterns.js.map +1 -0
  93. package/dist/layer2/dangerous-functions.d.ts +7 -0
  94. package/dist/layer2/dangerous-functions.d.ts.map +1 -0
  95. package/dist/layer2/dangerous-functions.js +1375 -0
  96. package/dist/layer2/dangerous-functions.js.map +1 -0
  97. package/dist/layer2/data-exposure.d.ts +16 -0
  98. package/dist/layer2/data-exposure.d.ts.map +1 -0
  99. package/dist/layer2/data-exposure.js +279 -0
  100. package/dist/layer2/data-exposure.js.map +1 -0
  101. package/dist/layer2/framework-checks.d.ts +7 -0
  102. package/dist/layer2/framework-checks.d.ts.map +1 -0
  103. package/dist/layer2/framework-checks.js +388 -0
  104. package/dist/layer2/framework-checks.js.map +1 -0
  105. package/dist/layer2/index.d.ts +58 -0
  106. package/dist/layer2/index.d.ts.map +1 -0
  107. package/dist/layer2/index.js +380 -0
  108. package/dist/layer2/index.js.map +1 -0
  109. package/dist/layer2/logic-gates.d.ts +7 -0
  110. package/dist/layer2/logic-gates.d.ts.map +1 -0
  111. package/dist/layer2/logic-gates.js +182 -0
  112. package/dist/layer2/logic-gates.js.map +1 -0
  113. package/dist/layer2/risky-imports.d.ts +7 -0
  114. package/dist/layer2/risky-imports.d.ts.map +1 -0
  115. package/dist/layer2/risky-imports.js +161 -0
  116. package/dist/layer2/risky-imports.js.map +1 -0
  117. package/dist/layer2/variables.d.ts +8 -0
  118. package/dist/layer2/variables.d.ts.map +1 -0
  119. package/dist/layer2/variables.js +152 -0
  120. package/dist/layer2/variables.js.map +1 -0
  121. package/dist/layer3/anthropic.d.ts +83 -0
  122. package/dist/layer3/anthropic.d.ts.map +1 -0
  123. package/dist/layer3/anthropic.js +1745 -0
  124. package/dist/layer3/anthropic.js.map +1 -0
  125. package/dist/layer3/index.d.ts +24 -0
  126. package/dist/layer3/index.d.ts.map +1 -0
  127. package/dist/layer3/index.js +119 -0
  128. package/dist/layer3/index.js.map +1 -0
  129. package/dist/layer3/openai.d.ts +25 -0
  130. package/dist/layer3/openai.d.ts.map +1 -0
  131. package/dist/layer3/openai.js +238 -0
  132. package/dist/layer3/openai.js.map +1 -0
  133. package/dist/layer3/package-check.d.ts +63 -0
  134. package/dist/layer3/package-check.d.ts.map +1 -0
  135. package/dist/layer3/package-check.js +508 -0
  136. package/dist/layer3/package-check.js.map +1 -0
  137. package/dist/modes/incremental.d.ts +66 -0
  138. package/dist/modes/incremental.d.ts.map +1 -0
  139. package/dist/modes/incremental.js +200 -0
  140. package/dist/modes/incremental.js.map +1 -0
  141. package/dist/tiers.d.ts +125 -0
  142. package/dist/tiers.d.ts.map +1 -0
  143. package/dist/tiers.js +234 -0
  144. package/dist/tiers.js.map +1 -0
  145. package/dist/types.d.ts +175 -0
  146. package/dist/types.d.ts.map +1 -0
  147. package/dist/types.js +50 -0
  148. package/dist/types.js.map +1 -0
  149. package/dist/utils/auth-helper-detector.d.ts +56 -0
  150. package/dist/utils/auth-helper-detector.d.ts.map +1 -0
  151. package/dist/utils/auth-helper-detector.js +360 -0
  152. package/dist/utils/auth-helper-detector.js.map +1 -0
  153. package/dist/utils/context-helpers.d.ts +96 -0
  154. package/dist/utils/context-helpers.d.ts.map +1 -0
  155. package/dist/utils/context-helpers.js +493 -0
  156. package/dist/utils/context-helpers.js.map +1 -0
  157. package/dist/utils/diff-detector.d.ts +53 -0
  158. package/dist/utils/diff-detector.d.ts.map +1 -0
  159. package/dist/utils/diff-detector.js +104 -0
  160. package/dist/utils/diff-detector.js.map +1 -0
  161. package/dist/utils/diff-parser.d.ts +80 -0
  162. package/dist/utils/diff-parser.d.ts.map +1 -0
  163. package/dist/utils/diff-parser.js +202 -0
  164. package/dist/utils/diff-parser.js.map +1 -0
  165. package/dist/utils/imported-auth-detector.d.ts +37 -0
  166. package/dist/utils/imported-auth-detector.d.ts.map +1 -0
  167. package/dist/utils/imported-auth-detector.js +251 -0
  168. package/dist/utils/imported-auth-detector.js.map +1 -0
  169. package/dist/utils/middleware-detector.d.ts +55 -0
  170. package/dist/utils/middleware-detector.d.ts.map +1 -0
  171. package/dist/utils/middleware-detector.js +260 -0
  172. package/dist/utils/middleware-detector.js.map +1 -0
  173. package/dist/utils/oauth-flow-detector.d.ts +41 -0
  174. package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
  175. package/dist/utils/oauth-flow-detector.js +202 -0
  176. package/dist/utils/oauth-flow-detector.js.map +1 -0
  177. package/dist/utils/path-exclusions.d.ts +55 -0
  178. package/dist/utils/path-exclusions.d.ts.map +1 -0
  179. package/dist/utils/path-exclusions.js +222 -0
  180. package/dist/utils/path-exclusions.js.map +1 -0
  181. package/dist/utils/project-context-builder.d.ts +119 -0
  182. package/dist/utils/project-context-builder.d.ts.map +1 -0
  183. package/dist/utils/project-context-builder.js +534 -0
  184. package/dist/utils/project-context-builder.js.map +1 -0
  185. package/dist/utils/registry-clients.d.ts +93 -0
  186. package/dist/utils/registry-clients.d.ts.map +1 -0
  187. package/dist/utils/registry-clients.js +273 -0
  188. package/dist/utils/registry-clients.js.map +1 -0
  189. package/dist/utils/trpc-analyzer.d.ts +78 -0
  190. package/dist/utils/trpc-analyzer.d.ts.map +1 -0
  191. package/dist/utils/trpc-analyzer.js +297 -0
  192. package/dist/utils/trpc-analyzer.js.map +1 -0
  193. package/package.json +45 -0
  194. package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
  195. package/src/__tests__/benchmark/fixtures/index.ts +68 -0
  196. package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
  197. package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
  198. package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
  199. package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
  200. package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
  201. package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
  202. package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
  203. package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
  204. package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
  205. package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
  206. package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
  207. package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
  208. package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
  209. package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
  210. package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
  211. package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
  212. package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
  213. package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
  214. package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
  215. package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
  216. package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
  217. package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
  218. package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
  219. package/src/__tests__/benchmark/index.ts +29 -0
  220. package/src/__tests__/benchmark/run-benchmark.ts +144 -0
  221. package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
  222. package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
  223. package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
  224. package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
  225. package/src/__tests__/benchmark/types.ts +144 -0
  226. package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
  227. package/src/__tests__/regression/known-false-positives.test.ts +467 -0
  228. package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
  229. package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
  230. package/src/__tests__/validation/analyze-results.ts +542 -0
  231. package/src/__tests__/validation/extract-for-triage.ts +146 -0
  232. package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
  233. package/src/__tests__/validation/run-validation.ts +364 -0
  234. package/src/__tests__/validation/triage-template.md +132 -0
  235. package/src/formatters/cli-terminal.ts +446 -0
  236. package/src/formatters/github-comment.ts +382 -0
  237. package/src/formatters/grouping.ts +190 -0
  238. package/src/formatters/index.ts +47 -0
  239. package/src/formatters/vscode-diagnostic.ts +243 -0
  240. package/src/index.ts +823 -0
  241. package/src/layer1/comments.ts +218 -0
  242. package/src/layer1/config-audit.ts +289 -0
  243. package/src/layer1/entropy.ts +583 -0
  244. package/src/layer1/file-flags.ts +127 -0
  245. package/src/layer1/index.ts +181 -0
  246. package/src/layer1/patterns.ts +516 -0
  247. package/src/layer1/urls.ts +334 -0
  248. package/src/layer1/weak-crypto.ts +328 -0
  249. package/src/layer2/ai-agent-tools.ts +601 -0
  250. package/src/layer2/ai-endpoint-protection.ts +387 -0
  251. package/src/layer2/ai-execution-sinks.ts +580 -0
  252. package/src/layer2/ai-fingerprinting.ts +758 -0
  253. package/src/layer2/ai-prompt-hygiene.ts +411 -0
  254. package/src/layer2/ai-rag-safety.ts +511 -0
  255. package/src/layer2/ai-schema-validation.ts +421 -0
  256. package/src/layer2/auth-antipatterns.ts +394 -0
  257. package/src/layer2/byok-patterns.ts +336 -0
  258. package/src/layer2/dangerous-functions.ts +1563 -0
  259. package/src/layer2/data-exposure.ts +315 -0
  260. package/src/layer2/framework-checks.ts +433 -0
  261. package/src/layer2/index.ts +473 -0
  262. package/src/layer2/logic-gates.ts +206 -0
  263. package/src/layer2/risky-imports.ts +186 -0
  264. package/src/layer2/variables.ts +166 -0
  265. package/src/layer3/anthropic.ts +2030 -0
  266. package/src/layer3/index.ts +130 -0
  267. package/src/layer3/package-check.ts +604 -0
  268. package/src/modes/incremental.ts +293 -0
  269. package/src/tiers.ts +318 -0
  270. package/src/types.ts +284 -0
  271. package/src/utils/auth-helper-detector.ts +443 -0
  272. package/src/utils/context-helpers.ts +535 -0
  273. package/src/utils/diff-detector.ts +135 -0
  274. package/src/utils/diff-parser.ts +272 -0
  275. package/src/utils/imported-auth-detector.ts +320 -0
  276. package/src/utils/middleware-detector.ts +333 -0
  277. package/src/utils/oauth-flow-detector.ts +246 -0
  278. package/src/utils/path-exclusions.ts +266 -0
  279. package/src/utils/project-context-builder.ts +707 -0
  280. package/src/utils/registry-clients.ts +351 -0
  281. package/src/utils/trpc-analyzer.ts +382 -0
@@ -0,0 +1,604 @@
1
+ /**
2
+ * Layer 3: Package Hallucination Check (Story C - Hallucination Firewall)
3
+ *
4
+ * Verifies if imported packages actually exist and assesses their risk
5
+ * Prevents typosquatting, dependency confusion, and AI-hallucinated packages
6
+ *
7
+ * Features:
8
+ * - Registry metadata fetching (npm, PyPI)
9
+ * - Risk score calculation based on multiple factors
10
+ * - Typosquatting detection via Levenshtein distance
11
+ * - Package age and popularity analysis
12
+ */
13
+
14
+ import type { Vulnerability, VulnerabilitySeverity } from '../types'
15
+ import {
16
+ fetchNPMMetadata,
17
+ fetchPyPIMetadata,
18
+ extractNpmDependencies,
19
+ extractPythonRequirements,
20
+ calculatePackageAgeDays,
21
+ rateLimitDelay,
22
+ getPackageFileType,
23
+ type NPMPackageMetadata,
24
+ type PyPIPackageMetadata,
25
+ type ExtractedDependency,
26
+ } from '../utils/registry-clients'
27
+
28
+ // ============================================================================
29
+ // Configuration
30
+ // ============================================================================
31
+
32
+ // Maximum packages to check per scan (cost/time control)
33
+ const MAX_PACKAGES_TO_CHECK = 50
34
+
35
+ // ============================================================================
36
+ // Popular Packages for Typosquatting Detection
37
+ // ============================================================================
38
+
39
+ const POPULAR_NPM_PACKAGES = new Set([
40
+ // Core frameworks
41
+ 'react', 'vue', 'angular', 'svelte', 'next', 'nuxt', 'gatsby',
42
+ 'express', 'fastify', 'koa', 'hapi', 'nest', 'nestjs',
43
+ // Utilities
44
+ 'lodash', 'underscore', 'ramda', 'date-fns', 'dayjs', 'moment',
45
+ 'axios', 'node-fetch', 'got', 'request', 'superagent',
46
+ // Build tools
47
+ 'webpack', 'rollup', 'vite', 'parcel', 'esbuild', 'swc',
48
+ 'babel', 'typescript', 'eslint', 'prettier', 'jest', 'vitest', 'mocha',
49
+ // Database
50
+ 'mongoose', 'sequelize', 'prisma', 'typeorm', 'knex', 'pg', 'mysql', 'sqlite3',
51
+ // Other popular
52
+ 'socket.io', 'ws', 'graphql', 'apollo', 'redux', 'mobx', 'zustand',
53
+ 'tailwindcss', 'styled-components', 'emotion', 'sass', 'postcss',
54
+ 'dotenv', 'cors', 'helmet', 'morgan', 'winston', 'pino',
55
+ 'uuid', 'crypto-js', 'bcrypt', 'jsonwebtoken', 'passport',
56
+ 'commander', 'yargs', 'inquirer', 'chalk', 'ora',
57
+ ])
58
+
59
+ const POPULAR_PYTHON_PACKAGES = new Set([
60
+ 'requests', 'flask', 'django', 'fastapi', 'numpy', 'pandas',
61
+ 'scipy', 'matplotlib', 'tensorflow', 'pytorch', 'torch', 'keras',
62
+ 'scikit-learn', 'sklearn', 'pillow', 'opencv-python', 'beautifulsoup4',
63
+ 'sqlalchemy', 'celery', 'redis', 'boto3', 'pytest', 'black', 'flake8',
64
+ 'pydantic', 'httpx', 'aiohttp', 'uvicorn', 'gunicorn',
65
+ ])
66
+
67
+ // ============================================================================
68
+ // Legitimate Packages (Skip checking)
69
+ // ============================================================================
70
+
71
+ const LEGITIMATE_PACKAGES = new Set([
72
+ // Scoped packages from trusted orgs
73
+ '@supabase/ssr', '@supabase/supabase-js', '@supabase/auth-helpers-nextjs',
74
+ '@anthropic-ai/sdk', '@openai/openai', '@langchain/core', '@langchain/openai',
75
+ '@octokit/rest', '@octokit/core',
76
+ '@radix-ui/react-avatar', '@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu',
77
+ '@radix-ui/react-scroll-area', '@radix-ui/react-slot', '@radix-ui/react-tabs',
78
+ '@tailwindcss/postcss', '@tailwindcss/typography',
79
+ '@types/node', '@types/react', '@types/react-dom',
80
+ // Common packages with unusual names
81
+ 'class-variance-authority', 'clsx', 'tailwind-merge', 'cva',
82
+ 'lucide-react', 'next-themes', 'sonner', 'zod', 'zustand',
83
+ 'geist', 'sharp', 'turbo', 'tsup', 'tsx',
84
+ // Known short names
85
+ 'ms', 'qs', 'ws', 'pg', 'ip', 'os', 'fs', 'vm',
86
+ ])
87
+
88
+ // ============================================================================
89
+ // Risk Factor Definitions
90
+ // ============================================================================
91
+
92
+ interface RiskFactor {
93
+ name: string
94
+ score: number
95
+ description: string
96
+ }
97
+
98
+ interface DependencyRiskScore {
99
+ package: string
100
+ ecosystem: 'npm' | 'python'
101
+ totalScore: number
102
+ factors: RiskFactor[]
103
+ recommendation: 'allow' | 'review' | 'block'
104
+ severity: VulnerabilitySeverity
105
+ }
106
+
107
+ // ============================================================================
108
+ // Typosquatting Detection
109
+ // ============================================================================
110
+
111
+ /**
112
+ * Calculate Levenshtein distance between two strings
113
+ */
114
+ function levenshteinDistance(a: string, b: string): number {
115
+ const matrix: number[][] = []
116
+
117
+ for (let i = 0; i <= b.length; i++) {
118
+ matrix[i] = [i]
119
+ }
120
+ for (let j = 0; j <= a.length; j++) {
121
+ matrix[0][j] = j
122
+ }
123
+
124
+ for (let i = 1; i <= b.length; i++) {
125
+ for (let j = 1; j <= a.length; j++) {
126
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
127
+ matrix[i][j] = matrix[i - 1][j - 1]
128
+ } else {
129
+ matrix[i][j] = Math.min(
130
+ matrix[i - 1][j - 1] + 1,
131
+ matrix[i][j - 1] + 1,
132
+ matrix[i - 1][j] + 1
133
+ )
134
+ }
135
+ }
136
+ }
137
+
138
+ return matrix[b.length][a.length]
139
+ }
140
+
141
+ /**
142
+ * Check if package name is similar to a popular package (potential typosquat)
143
+ */
144
+ function checkTyposquatting(
145
+ packageName: string,
146
+ ecosystem: 'npm' | 'python'
147
+ ): { isSimilar: boolean; similarTo?: string; distance?: number } {
148
+ const name = packageName.toLowerCase()
149
+ const popularPackages = ecosystem === 'npm' ? POPULAR_NPM_PACKAGES : POPULAR_PYTHON_PACKAGES
150
+
151
+ for (const popular of popularPackages) {
152
+ // Skip if it's the actual package
153
+ if (name === popular) continue
154
+
155
+ const distance = levenshteinDistance(name, popular)
156
+
157
+ // Flag if 1-2 character difference and similar length
158
+ if (distance === 1 && Math.abs(name.length - popular.length) <= 1) {
159
+ return { isSimilar: true, similarTo: popular, distance }
160
+ }
161
+ if (distance === 2 && name.length >= 5 && Math.abs(name.length - popular.length) <= 1) {
162
+ return { isSimilar: true, similarTo: popular, distance }
163
+ }
164
+ }
165
+
166
+ return { isSimilar: false }
167
+ }
168
+
169
+ /**
170
+ * Check for suspicious naming patterns
171
+ */
172
+ function hasSuspiciousNamingPattern(packageName: string): { suspicious: boolean; pattern?: string } {
173
+ const suspiciousPatterns = [
174
+ { pattern: /^[a-z]+-js$/, desc: 'package-js suffix (common typosquat pattern)' },
175
+ { pattern: /^node-[a-z]{2,}$/, desc: 'node-package prefix' },
176
+ { pattern: /^[a-z]+-node$/, desc: 'package-node suffix' },
177
+ { pattern: /-\d{3,}$/, desc: 'ends with many numbers' },
178
+ { pattern: /^[a-z]{1,2}-[a-z]+$/, desc: 'very short prefix' },
179
+ { pattern: /[0o][0o]|[1l][1l]/i, desc: 'character substitution (0/o, 1/l)' },
180
+ ]
181
+
182
+ for (const { pattern, desc } of suspiciousPatterns) {
183
+ if (pattern.test(packageName)) {
184
+ return { suspicious: true, pattern: desc }
185
+ }
186
+ }
187
+
188
+ return { suspicious: false }
189
+ }
190
+
191
+ // ============================================================================
192
+ // Risk Score Calculation
193
+ // ============================================================================
194
+
195
+ /**
196
+ * Compute risk score for an npm package
197
+ */
198
+ async function computeNPMRiskScore(
199
+ dep: ExtractedDependency,
200
+ metadata: NPMPackageMetadata | null
201
+ ): Promise<DependencyRiskScore> {
202
+ const factors: RiskFactor[] = []
203
+ let totalScore = 0
204
+
205
+ // Factor 1: Package not found (highest risk - likely hallucinated)
206
+ if (!metadata) {
207
+ factors.push({
208
+ name: 'package_not_found',
209
+ score: 100,
210
+ description: 'Package does not exist in npm registry. Likely a hallucinated package name.',
211
+ })
212
+ return {
213
+ package: dep.name,
214
+ ecosystem: 'npm',
215
+ totalScore: 100,
216
+ factors,
217
+ recommendation: 'block',
218
+ severity: 'critical',
219
+ }
220
+ }
221
+
222
+ // Factor 2: Package age
223
+ const ageInDays = calculatePackageAgeDays(metadata.time?.created)
224
+ if (ageInDays < 7) {
225
+ factors.push({
226
+ name: 'very_new_package',
227
+ score: 30,
228
+ description: `Package created ${ageInDays} days ago (< 7 days)`,
229
+ })
230
+ totalScore += 30
231
+ } else if (ageInDays < 30) {
232
+ factors.push({
233
+ name: 'new_package',
234
+ score: 15,
235
+ description: `Package created ${ageInDays} days ago (< 30 days)`,
236
+ })
237
+ totalScore += 15
238
+ }
239
+
240
+ // Factor 3: Download count
241
+ const weeklyDownloads = metadata.downloads?.weekly || 0
242
+ if (weeklyDownloads < 10) {
243
+ factors.push({
244
+ name: 'no_downloads',
245
+ score: 25,
246
+ description: `Only ${weeklyDownloads} weekly downloads`,
247
+ })
248
+ totalScore += 25
249
+ } else if (weeklyDownloads < 100) {
250
+ factors.push({
251
+ name: 'low_downloads',
252
+ score: 15,
253
+ description: `Only ${weeklyDownloads} weekly downloads (< 100)`,
254
+ })
255
+ totalScore += 15
256
+ } else if (weeklyDownloads < 1000) {
257
+ factors.push({
258
+ name: 'moderate_downloads',
259
+ score: 5,
260
+ description: `${weeklyDownloads} weekly downloads (< 1000)`,
261
+ })
262
+ totalScore += 5
263
+ }
264
+
265
+ // Factor 4: Typosquatting similarity
266
+ const typoCheck = checkTyposquatting(dep.name, 'npm')
267
+ if (typoCheck.isSimilar && typoCheck.distance === 1) {
268
+ factors.push({
269
+ name: 'likely_typosquat',
270
+ score: 40,
271
+ description: `Name differs by 1 character from popular package "${typoCheck.similarTo}"`,
272
+ })
273
+ totalScore += 40
274
+ } else if (typoCheck.isSimilar && typoCheck.distance === 2) {
275
+ factors.push({
276
+ name: 'possible_typosquat',
277
+ score: 20,
278
+ description: `Name similar to popular package "${typoCheck.similarTo}" (${typoCheck.distance} char diff)`,
279
+ })
280
+ totalScore += 20
281
+ }
282
+
283
+ // Factor 5: Suspicious naming pattern
284
+ const namingCheck = hasSuspiciousNamingPattern(dep.name)
285
+ if (namingCheck.suspicious) {
286
+ factors.push({
287
+ name: 'suspicious_name',
288
+ score: 15,
289
+ description: `Suspicious naming pattern: ${namingCheck.pattern}`,
290
+ })
291
+ totalScore += 15
292
+ }
293
+
294
+ // Factor 6: No repository/homepage
295
+ const hasRepo = !!metadata.repository?.url
296
+ const hasHomepage = !!metadata.homepage
297
+ if (!hasRepo && !hasHomepage) {
298
+ factors.push({
299
+ name: 'no_source_links',
300
+ score: 15,
301
+ description: 'Package has no repository or homepage link',
302
+ })
303
+ totalScore += 15
304
+ }
305
+
306
+ // Factor 7: No description
307
+ if (!metadata.description || metadata.description.length < 10) {
308
+ factors.push({
309
+ name: 'no_description',
310
+ score: 10,
311
+ description: 'Package has no meaningful description',
312
+ })
313
+ totalScore += 10
314
+ }
315
+
316
+ // Factor 8: Single maintainer on new package
317
+ const maintainerCount = metadata.maintainers?.length || 0
318
+ if (maintainerCount === 1 && ageInDays < 30) {
319
+ factors.push({
320
+ name: 'single_new_maintainer',
321
+ score: 10,
322
+ description: 'Single maintainer on a new package',
323
+ })
324
+ totalScore += 10
325
+ }
326
+
327
+ // Determine recommendation and severity
328
+ let recommendation: DependencyRiskScore['recommendation']
329
+ let severity: VulnerabilitySeverity
330
+
331
+ if (totalScore >= 70) {
332
+ recommendation = 'block'
333
+ severity = 'high'
334
+ } else if (totalScore >= 40) {
335
+ recommendation = 'review'
336
+ severity = 'medium'
337
+ } else if (totalScore >= 20) {
338
+ recommendation = 'review'
339
+ severity = 'low'
340
+ } else {
341
+ recommendation = 'allow'
342
+ severity = 'info'
343
+ }
344
+
345
+ return {
346
+ package: dep.name,
347
+ ecosystem: 'npm',
348
+ totalScore: Math.min(totalScore, 100),
349
+ factors,
350
+ recommendation,
351
+ severity,
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Compute risk score for a Python package
357
+ */
358
+ async function computePyPIRiskScore(
359
+ dep: ExtractedDependency,
360
+ metadata: PyPIPackageMetadata | null
361
+ ): Promise<DependencyRiskScore> {
362
+ const factors: RiskFactor[] = []
363
+ let totalScore = 0
364
+
365
+ // Factor 1: Package not found
366
+ if (!metadata) {
367
+ factors.push({
368
+ name: 'package_not_found',
369
+ score: 100,
370
+ description: 'Package does not exist in PyPI registry. Likely a hallucinated package name.',
371
+ })
372
+ return {
373
+ package: dep.name,
374
+ ecosystem: 'python',
375
+ totalScore: 100,
376
+ factors,
377
+ recommendation: 'block',
378
+ severity: 'critical',
379
+ }
380
+ }
381
+
382
+ // Factor 2: Typosquatting
383
+ const typoCheck = checkTyposquatting(dep.name, 'python')
384
+ if (typoCheck.isSimilar && typoCheck.distance === 1) {
385
+ factors.push({
386
+ name: 'likely_typosquat',
387
+ score: 40,
388
+ description: `Name differs by 1 character from popular package "${typoCheck.similarTo}"`,
389
+ })
390
+ totalScore += 40
391
+ } else if (typoCheck.isSimilar && typoCheck.distance === 2) {
392
+ factors.push({
393
+ name: 'possible_typosquat',
394
+ score: 20,
395
+ description: `Name similar to popular package "${typoCheck.similarTo}"`,
396
+ })
397
+ totalScore += 20
398
+ }
399
+
400
+ // Factor 3: Suspicious naming
401
+ const namingCheck = hasSuspiciousNamingPattern(dep.name)
402
+ if (namingCheck.suspicious) {
403
+ factors.push({
404
+ name: 'suspicious_name',
405
+ score: 15,
406
+ description: `Suspicious naming pattern: ${namingCheck.pattern}`,
407
+ })
408
+ totalScore += 15
409
+ }
410
+
411
+ // Factor 4: No project URLs
412
+ const hasProjectUrls = metadata.projectUrls && Object.keys(metadata.projectUrls).length > 0
413
+ if (!hasProjectUrls) {
414
+ factors.push({
415
+ name: 'no_project_urls',
416
+ score: 15,
417
+ description: 'Package has no project URLs (repository, homepage, etc.)',
418
+ })
419
+ totalScore += 15
420
+ }
421
+
422
+ // Factor 5: No summary/description
423
+ if (!metadata.summary || metadata.summary.length < 10) {
424
+ factors.push({
425
+ name: 'no_description',
426
+ score: 10,
427
+ description: 'Package has no meaningful description',
428
+ })
429
+ totalScore += 10
430
+ }
431
+
432
+ // Determine recommendation and severity
433
+ let recommendation: DependencyRiskScore['recommendation']
434
+ let severity: VulnerabilitySeverity
435
+
436
+ if (totalScore >= 70) {
437
+ recommendation = 'block'
438
+ severity = 'high'
439
+ } else if (totalScore >= 40) {
440
+ recommendation = 'review'
441
+ severity = 'medium'
442
+ } else if (totalScore >= 20) {
443
+ recommendation = 'review'
444
+ severity = 'low'
445
+ } else {
446
+ recommendation = 'allow'
447
+ severity = 'info'
448
+ }
449
+
450
+ return {
451
+ package: dep.name,
452
+ ecosystem: 'python',
453
+ totalScore: Math.min(totalScore, 100),
454
+ factors,
455
+ recommendation,
456
+ severity,
457
+ }
458
+ }
459
+
460
+ // ============================================================================
461
+ // Vulnerability Generation
462
+ // ============================================================================
463
+
464
+ /**
465
+ * Build description from risk score
466
+ */
467
+ function buildRiskDescription(risk: DependencyRiskScore): string {
468
+ const factorList = risk.factors.map(f => `- ${f.description}`).join('\n')
469
+
470
+ if (risk.totalScore >= 70) {
471
+ return `Package "${risk.package}" has high risk indicators (score: ${risk.totalScore}/100):\n${factorList}\n\nThis may be a hallucinated package name or a typosquatting attempt.`
472
+ }
473
+
474
+ if (risk.totalScore >= 40) {
475
+ return `Package "${risk.package}" has moderate risk indicators (score: ${risk.totalScore}/100):\n${factorList}\n\nReview this dependency before using.`
476
+ }
477
+
478
+ return `Package "${risk.package}" has some risk factors (score: ${risk.totalScore}/100):\n${factorList}`
479
+ }
480
+
481
+ /**
482
+ * Build suggested fix from risk score
483
+ */
484
+ function buildRiskSuggestedFix(risk: DependencyRiskScore): string {
485
+ if (risk.factors.some(f => f.name === 'package_not_found')) {
486
+ return 'Verify the package name is correct. This package does not exist in the registry - it may be a hallucinated name from an AI tool.'
487
+ }
488
+
489
+ if (risk.factors.some(f => f.name.includes('typosquat'))) {
490
+ const typoFactor = risk.factors.find(f => f.name.includes('typosquat'))
491
+ const match = typoFactor?.description.match(/"([^"]+)"/)
492
+ const intendedPackage = match?.[1]
493
+ return `Verify this is the intended package. Did you mean "${intendedPackage}"?`
494
+ }
495
+
496
+ if (risk.totalScore >= 40) {
497
+ return 'Review this package before using. Check the repository, maintainers, and recent activity.'
498
+ }
499
+
500
+ return 'Consider reviewing this package\'s repository and maintainers.'
501
+ }
502
+
503
+ // ============================================================================
504
+ // Main Check Function
505
+ // ============================================================================
506
+
507
+ /**
508
+ * Check packages in a file for hallucination and risk indicators
509
+ */
510
+ export async function checkPackages(
511
+ content: string,
512
+ filePath: string
513
+ ): Promise<Vulnerability[]> {
514
+ const vulnerabilities: Vulnerability[] = []
515
+
516
+ // Determine file type
517
+ const fileType = getPackageFileType(filePath)
518
+ if (!fileType) {
519
+ return vulnerabilities
520
+ }
521
+
522
+ // Extract dependencies based on file type
523
+ let dependencies: ExtractedDependency[] = []
524
+
525
+ if (fileType === 'npm' && filePath.endsWith('package.json')) {
526
+ dependencies = extractNpmDependencies(content)
527
+ } else if (fileType === 'python') {
528
+ dependencies = extractPythonRequirements(content)
529
+ }
530
+
531
+ if (dependencies.length === 0) {
532
+ return vulnerabilities
533
+ }
534
+
535
+ const lines = content.split('\n')
536
+
537
+ // Filter out legitimate/known packages and scoped packages
538
+ const packagesToCheck = dependencies.filter(dep => {
539
+ // Skip scoped packages (@org/package) - usually legitimate
540
+ if (dep.name.startsWith('@')) return false
541
+
542
+ // Skip known legitimate packages
543
+ if (LEGITIMATE_PACKAGES.has(dep.name)) return false
544
+
545
+ // Skip exact matches to popular packages
546
+ if (POPULAR_NPM_PACKAGES.has(dep.name.toLowerCase())) return false
547
+ if (POPULAR_PYTHON_PACKAGES.has(dep.name.toLowerCase())) return false
548
+
549
+ return true
550
+ })
551
+
552
+ // Limit packages to check (cost control)
553
+ const limitedPackages = packagesToCheck.slice(0, MAX_PACKAGES_TO_CHECK)
554
+
555
+ // Check each package
556
+ for (const dep of limitedPackages) {
557
+ let risk: DependencyRiskScore
558
+
559
+ if (fileType === 'npm') {
560
+ const metadata = await fetchNPMMetadata(dep.name)
561
+ risk = await computeNPMRiskScore(dep, metadata)
562
+ } else {
563
+ const metadata = await fetchPyPIMetadata(dep.name)
564
+ risk = await computePyPIRiskScore(dep, metadata)
565
+ }
566
+
567
+ // Only create vulnerabilities for packages that need attention
568
+ if (risk.recommendation !== 'allow') {
569
+ vulnerabilities.push({
570
+ id: `pkg-risk-${filePath}-${dep.name}`,
571
+ filePath,
572
+ lineNumber: dep.line,
573
+ lineContent: lines[dep.line - 1]?.trim() || dep.name,
574
+ severity: risk.severity,
575
+ category: 'suspicious_package',
576
+ title: risk.totalScore >= 70
577
+ ? 'Potentially hallucinated dependency'
578
+ : 'Suspicious dependency',
579
+ description: buildRiskDescription(risk),
580
+ suggestedFix: buildRiskSuggestedFix(risk),
581
+ confidence: risk.totalScore >= 70 ? 'high' : 'medium',
582
+ layer: 3,
583
+ requiresAIValidation: risk.totalScore < 70, // High-confidence issues don't need AI validation
584
+ })
585
+ }
586
+
587
+ // Rate limiting between requests
588
+ await rateLimitDelay()
589
+ }
590
+
591
+ return vulnerabilities
592
+ }
593
+
594
+ // Export for testing
595
+ export {
596
+ levenshteinDistance,
597
+ checkTyposquatting,
598
+ hasSuspiciousNamingPattern,
599
+ computeNPMRiskScore,
600
+ computePyPIRiskScore,
601
+ POPULAR_NPM_PACKAGES,
602
+ POPULAR_PYTHON_PACKAGES,
603
+ LEGITIMATE_PACKAGES,
604
+ }