@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.
- package/dist/formatters/cli-terminal.d.ts +27 -0
- package/dist/formatters/cli-terminal.d.ts.map +1 -0
- package/dist/formatters/cli-terminal.js +412 -0
- package/dist/formatters/cli-terminal.js.map +1 -0
- package/dist/formatters/github-comment.d.ts +41 -0
- package/dist/formatters/github-comment.d.ts.map +1 -0
- package/dist/formatters/github-comment.js +306 -0
- package/dist/formatters/github-comment.js.map +1 -0
- package/dist/formatters/grouping.d.ts +52 -0
- package/dist/formatters/grouping.d.ts.map +1 -0
- package/dist/formatters/grouping.js +152 -0
- package/dist/formatters/grouping.js.map +1 -0
- package/dist/formatters/index.d.ts +9 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +35 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/vscode-diagnostic.d.ts +103 -0
- package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
- package/dist/formatters/vscode-diagnostic.js +151 -0
- package/dist/formatters/vscode-diagnostic.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +648 -0
- package/dist/index.js.map +1 -0
- package/dist/layer1/comments.d.ts +8 -0
- package/dist/layer1/comments.d.ts.map +1 -0
- package/dist/layer1/comments.js +203 -0
- package/dist/layer1/comments.js.map +1 -0
- package/dist/layer1/config-audit.d.ts +8 -0
- package/dist/layer1/config-audit.d.ts.map +1 -0
- package/dist/layer1/config-audit.js +252 -0
- package/dist/layer1/config-audit.js.map +1 -0
- package/dist/layer1/entropy.d.ts +8 -0
- package/dist/layer1/entropy.d.ts.map +1 -0
- package/dist/layer1/entropy.js +500 -0
- package/dist/layer1/entropy.js.map +1 -0
- package/dist/layer1/file-flags.d.ts +7 -0
- package/dist/layer1/file-flags.d.ts.map +1 -0
- package/dist/layer1/file-flags.js +112 -0
- package/dist/layer1/file-flags.js.map +1 -0
- package/dist/layer1/index.d.ts +36 -0
- package/dist/layer1/index.d.ts.map +1 -0
- package/dist/layer1/index.js +132 -0
- package/dist/layer1/index.js.map +1 -0
- package/dist/layer1/patterns.d.ts +8 -0
- package/dist/layer1/patterns.d.ts.map +1 -0
- package/dist/layer1/patterns.js +482 -0
- package/dist/layer1/patterns.js.map +1 -0
- package/dist/layer1/urls.d.ts +8 -0
- package/dist/layer1/urls.d.ts.map +1 -0
- package/dist/layer1/urls.js +296 -0
- package/dist/layer1/urls.js.map +1 -0
- package/dist/layer1/weak-crypto.d.ts +7 -0
- package/dist/layer1/weak-crypto.d.ts.map +1 -0
- package/dist/layer1/weak-crypto.js +291 -0
- package/dist/layer1/weak-crypto.js.map +1 -0
- package/dist/layer2/ai-agent-tools.d.ts +19 -0
- package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
- package/dist/layer2/ai-agent-tools.js +528 -0
- package/dist/layer2/ai-agent-tools.js.map +1 -0
- package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
- package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
- package/dist/layer2/ai-endpoint-protection.js +332 -0
- package/dist/layer2/ai-endpoint-protection.js.map +1 -0
- package/dist/layer2/ai-execution-sinks.d.ts +18 -0
- package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
- package/dist/layer2/ai-execution-sinks.js +496 -0
- package/dist/layer2/ai-execution-sinks.js.map +1 -0
- package/dist/layer2/ai-fingerprinting.d.ts +7 -0
- package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
- package/dist/layer2/ai-fingerprinting.js +654 -0
- package/dist/layer2/ai-fingerprinting.js.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.js +356 -0
- package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
- package/dist/layer2/ai-rag-safety.d.ts +21 -0
- package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
- package/dist/layer2/ai-rag-safety.js +459 -0
- package/dist/layer2/ai-rag-safety.js.map +1 -0
- package/dist/layer2/ai-schema-validation.d.ts +25 -0
- package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
- package/dist/layer2/ai-schema-validation.js +375 -0
- package/dist/layer2/ai-schema-validation.js.map +1 -0
- package/dist/layer2/auth-antipatterns.d.ts +20 -0
- package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
- package/dist/layer2/auth-antipatterns.js +333 -0
- package/dist/layer2/auth-antipatterns.js.map +1 -0
- package/dist/layer2/byok-patterns.d.ts +12 -0
- package/dist/layer2/byok-patterns.d.ts.map +1 -0
- package/dist/layer2/byok-patterns.js +299 -0
- package/dist/layer2/byok-patterns.js.map +1 -0
- package/dist/layer2/dangerous-functions.d.ts +7 -0
- package/dist/layer2/dangerous-functions.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions.js +1375 -0
- package/dist/layer2/dangerous-functions.js.map +1 -0
- package/dist/layer2/data-exposure.d.ts +16 -0
- package/dist/layer2/data-exposure.d.ts.map +1 -0
- package/dist/layer2/data-exposure.js +279 -0
- package/dist/layer2/data-exposure.js.map +1 -0
- package/dist/layer2/framework-checks.d.ts +7 -0
- package/dist/layer2/framework-checks.d.ts.map +1 -0
- package/dist/layer2/framework-checks.js +388 -0
- package/dist/layer2/framework-checks.js.map +1 -0
- package/dist/layer2/index.d.ts +58 -0
- package/dist/layer2/index.d.ts.map +1 -0
- package/dist/layer2/index.js +380 -0
- package/dist/layer2/index.js.map +1 -0
- package/dist/layer2/logic-gates.d.ts +7 -0
- package/dist/layer2/logic-gates.d.ts.map +1 -0
- package/dist/layer2/logic-gates.js +182 -0
- package/dist/layer2/logic-gates.js.map +1 -0
- package/dist/layer2/risky-imports.d.ts +7 -0
- package/dist/layer2/risky-imports.d.ts.map +1 -0
- package/dist/layer2/risky-imports.js +161 -0
- package/dist/layer2/risky-imports.js.map +1 -0
- package/dist/layer2/variables.d.ts +8 -0
- package/dist/layer2/variables.d.ts.map +1 -0
- package/dist/layer2/variables.js +152 -0
- package/dist/layer2/variables.js.map +1 -0
- package/dist/layer3/anthropic.d.ts +83 -0
- package/dist/layer3/anthropic.d.ts.map +1 -0
- package/dist/layer3/anthropic.js +1745 -0
- package/dist/layer3/anthropic.js.map +1 -0
- package/dist/layer3/index.d.ts +24 -0
- package/dist/layer3/index.d.ts.map +1 -0
- package/dist/layer3/index.js +119 -0
- package/dist/layer3/index.js.map +1 -0
- package/dist/layer3/openai.d.ts +25 -0
- package/dist/layer3/openai.d.ts.map +1 -0
- package/dist/layer3/openai.js +238 -0
- package/dist/layer3/openai.js.map +1 -0
- package/dist/layer3/package-check.d.ts +63 -0
- package/dist/layer3/package-check.d.ts.map +1 -0
- package/dist/layer3/package-check.js +508 -0
- package/dist/layer3/package-check.js.map +1 -0
- package/dist/modes/incremental.d.ts +66 -0
- package/dist/modes/incremental.d.ts.map +1 -0
- package/dist/modes/incremental.js +200 -0
- package/dist/modes/incremental.js.map +1 -0
- package/dist/tiers.d.ts +125 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +234 -0
- package/dist/tiers.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/auth-helper-detector.d.ts +56 -0
- package/dist/utils/auth-helper-detector.d.ts.map +1 -0
- package/dist/utils/auth-helper-detector.js +360 -0
- package/dist/utils/auth-helper-detector.js.map +1 -0
- package/dist/utils/context-helpers.d.ts +96 -0
- package/dist/utils/context-helpers.d.ts.map +1 -0
- package/dist/utils/context-helpers.js +493 -0
- package/dist/utils/context-helpers.js.map +1 -0
- package/dist/utils/diff-detector.d.ts +53 -0
- package/dist/utils/diff-detector.d.ts.map +1 -0
- package/dist/utils/diff-detector.js +104 -0
- package/dist/utils/diff-detector.js.map +1 -0
- package/dist/utils/diff-parser.d.ts +80 -0
- package/dist/utils/diff-parser.d.ts.map +1 -0
- package/dist/utils/diff-parser.js +202 -0
- package/dist/utils/diff-parser.js.map +1 -0
- package/dist/utils/imported-auth-detector.d.ts +37 -0
- package/dist/utils/imported-auth-detector.d.ts.map +1 -0
- package/dist/utils/imported-auth-detector.js +251 -0
- package/dist/utils/imported-auth-detector.js.map +1 -0
- package/dist/utils/middleware-detector.d.ts +55 -0
- package/dist/utils/middleware-detector.d.ts.map +1 -0
- package/dist/utils/middleware-detector.js +260 -0
- package/dist/utils/middleware-detector.js.map +1 -0
- package/dist/utils/oauth-flow-detector.d.ts +41 -0
- package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
- package/dist/utils/oauth-flow-detector.js +202 -0
- package/dist/utils/oauth-flow-detector.js.map +1 -0
- package/dist/utils/path-exclusions.d.ts +55 -0
- package/dist/utils/path-exclusions.d.ts.map +1 -0
- package/dist/utils/path-exclusions.js +222 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/dist/utils/project-context-builder.d.ts +119 -0
- package/dist/utils/project-context-builder.d.ts.map +1 -0
- package/dist/utils/project-context-builder.js +534 -0
- package/dist/utils/project-context-builder.js.map +1 -0
- package/dist/utils/registry-clients.d.ts +93 -0
- package/dist/utils/registry-clients.d.ts.map +1 -0
- package/dist/utils/registry-clients.js +273 -0
- package/dist/utils/registry-clients.js.map +1 -0
- package/dist/utils/trpc-analyzer.d.ts +78 -0
- package/dist/utils/trpc-analyzer.d.ts.map +1 -0
- package/dist/utils/trpc-analyzer.js +297 -0
- package/dist/utils/trpc-analyzer.js.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
- package/src/__tests__/benchmark/fixtures/index.ts +68 -0
- package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
- package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
- package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
- package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
- package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
- package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
- package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
- package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
- package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
- package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
- package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
- package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
- package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
- package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
- package/src/__tests__/benchmark/index.ts +29 -0
- package/src/__tests__/benchmark/run-benchmark.ts +144 -0
- package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
- package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
- package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
- package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
- package/src/__tests__/benchmark/types.ts +144 -0
- package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
- package/src/__tests__/regression/known-false-positives.test.ts +467 -0
- package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
- package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
- package/src/__tests__/validation/analyze-results.ts +542 -0
- package/src/__tests__/validation/extract-for-triage.ts +146 -0
- package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
- package/src/__tests__/validation/run-validation.ts +364 -0
- package/src/__tests__/validation/triage-template.md +132 -0
- package/src/formatters/cli-terminal.ts +446 -0
- package/src/formatters/github-comment.ts +382 -0
- package/src/formatters/grouping.ts +190 -0
- package/src/formatters/index.ts +47 -0
- package/src/formatters/vscode-diagnostic.ts +243 -0
- package/src/index.ts +823 -0
- package/src/layer1/comments.ts +218 -0
- package/src/layer1/config-audit.ts +289 -0
- package/src/layer1/entropy.ts +583 -0
- package/src/layer1/file-flags.ts +127 -0
- package/src/layer1/index.ts +181 -0
- package/src/layer1/patterns.ts +516 -0
- package/src/layer1/urls.ts +334 -0
- package/src/layer1/weak-crypto.ts +328 -0
- package/src/layer2/ai-agent-tools.ts +601 -0
- package/src/layer2/ai-endpoint-protection.ts +387 -0
- package/src/layer2/ai-execution-sinks.ts +580 -0
- package/src/layer2/ai-fingerprinting.ts +758 -0
- package/src/layer2/ai-prompt-hygiene.ts +411 -0
- package/src/layer2/ai-rag-safety.ts +511 -0
- package/src/layer2/ai-schema-validation.ts +421 -0
- package/src/layer2/auth-antipatterns.ts +394 -0
- package/src/layer2/byok-patterns.ts +336 -0
- package/src/layer2/dangerous-functions.ts +1563 -0
- package/src/layer2/data-exposure.ts +315 -0
- package/src/layer2/framework-checks.ts +433 -0
- package/src/layer2/index.ts +473 -0
- package/src/layer2/logic-gates.ts +206 -0
- package/src/layer2/risky-imports.ts +186 -0
- package/src/layer2/variables.ts +166 -0
- package/src/layer3/anthropic.ts +2030 -0
- package/src/layer3/index.ts +130 -0
- package/src/layer3/package-check.ts +604 -0
- package/src/modes/incremental.ts +293 -0
- package/src/tiers.ts +318 -0
- package/src/types.ts +284 -0
- package/src/utils/auth-helper-detector.ts +443 -0
- package/src/utils/context-helpers.ts +535 -0
- package/src/utils/diff-detector.ts +135 -0
- package/src/utils/diff-parser.ts +272 -0
- package/src/utils/imported-auth-detector.ts +320 -0
- package/src/utils/middleware-detector.ts +333 -0
- package/src/utils/oauth-flow-detector.ts +246 -0
- package/src/utils/path-exclusions.ts +266 -0
- package/src/utils/project-context-builder.ts +707 -0
- package/src/utils/registry-clients.ts +351 -0
- 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
|
+
}
|