@lateos/npm-scan 0.18.2 → 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 (113) hide show
  1. package/CHANGELOG.md +265 -233
  2. package/LICENSING.md +19 -19
  3. package/README.de.md +708 -708
  4. package/README.fr.md +707 -707
  5. package/README.ja.md +704 -704
  6. package/README.md +861 -826
  7. package/README.zh.md +708 -708
  8. package/VALIDATION.md +92 -0
  9. package/backend/cra.js +68 -68
  10. package/backend/db/pg-schema.sql +155 -0
  11. package/backend/db/schema.sql +32 -32
  12. package/backend/db.js +88 -88
  13. package/backend/detectors/atk-001-lifecycle.js +17 -17
  14. package/backend/detectors/atk-002-obfusc.js +261 -261
  15. package/backend/detectors/atk-003-creds.js +13 -13
  16. package/backend/detectors/atk-004-persist.js +13 -13
  17. package/backend/detectors/atk-005-exfil.js +13 -13
  18. package/backend/detectors/atk-006-depconf.js +14 -14
  19. package/backend/detectors/atk-007-typosquat.js +34 -34
  20. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  21. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  22. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  23. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  24. package/backend/detectors/config/thresholds.js +66 -0
  25. package/backend/detectors/config/whitelist.json +74 -0
  26. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  27. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  28. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  29. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  30. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  31. package/backend/detectors/hf-impersonation/index.js +396 -396
  32. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  33. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  34. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  35. package/backend/detectors/index.js +87 -81
  36. package/backend/detectors/lib/ast-patterns.js +21 -0
  37. package/backend/detectors/lib/entropy-analyzer.js +24 -0
  38. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  39. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  40. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  41. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  42. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  43. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  44. package/backend/detectors/megalodon/index.js +80 -80
  45. package/backend/detectors/megalodon/types.js +9 -9
  46. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  47. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  48. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  49. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  50. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  51. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  52. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  53. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  54. package/backend/detectors/tier1-binary-embed.js +34 -5
  55. package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
  56. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  57. package/backend/detectors/tier1-version-anomaly.js +187 -0
  58. package/backend/detectors.test.js +88 -0
  59. package/backend/fetch.js +175 -175
  60. package/backend/index.js +4 -4
  61. package/backend/license.js +89 -89
  62. package/backend/lockfile.js +379 -379
  63. package/backend/pdf.js +245 -245
  64. package/backend/policy.js +193 -193
  65. package/backend/report.js +254 -254
  66. package/backend/sbom.js +66 -66
  67. package/backend/scripts/analyze-false-positives.js +146 -0
  68. package/backend/scripts/analyze-validation.js +151 -0
  69. package/backend/scripts/detect-false-positives.js +93 -0
  70. package/backend/scripts/fetch-top-packages.js +129 -0
  71. package/backend/scripts/validate-detectors.js +142 -0
  72. package/backend/siem/cef.js +32 -32
  73. package/backend/siem/ecs.js +40 -40
  74. package/backend/siem/index.js +18 -18
  75. package/backend/siem/qradar.js +56 -56
  76. package/backend/siem/sentinel.js +27 -27
  77. package/backend/tests-d5-enhanced.test.js +46 -0
  78. package/backend/tests-d6-version-anomaly.test.js +58 -0
  79. package/backend/tests-d6.test.js +116 -0
  80. package/backend/tests-d6c.test.js +106 -0
  81. package/backend/tests-d7-obfuscation.test.js +91 -0
  82. package/backend/tests.test.js +898 -0
  83. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  84. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  85. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  86. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  87. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  88. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  89. package/backend/vsix-scan/index.js +183 -183
  90. package/backend/vsix-scan/marketplace-client.js +145 -145
  91. package/backend/vsix-scan/vsix-iocs.json +31 -31
  92. package/cli/cli.js +458 -458
  93. package/package.json +74 -57
  94. package/.dockerignore +0 -20
  95. package/.husky/pre-commit +0 -1
  96. package/SECURITY.md +0 -73
  97. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  98. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  99. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  100. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  101. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  102. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  103. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  104. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  105. package/deploy/helm/npm-scan/values.yaml +0 -103
  106. package/scripts/download-corpus.js +0 -30
  107. package/scripts/gen-mal-corpus.js +0 -35
  108. package/scripts/generate-campaign-fixtures.js +0 -170
  109. package/src/config/top-5000.json +0 -87
  110. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  111. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  112. package/test/fixtures/lockfiles/yarn.lock +0 -104
  113. package/test/fixtures/mock-data.js +0 -69
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Detector confidence thresholds (calibrated post-validation)
3
+ *
4
+ * Format: { detector: { flag_threshold, warn_threshold } }
5
+ * Thresholds calibrated against:
6
+ * - 3 real May 2026 attack campaigns (validation)
7
+ * - Top 1,000 npm packages (false positive calibration)
8
+ */
9
+
10
+ export default {
11
+ 'TIER1-VERSION-ANOMALY': {
12
+ flag_threshold: 72,
13
+ warn_threshold: 60,
14
+ notes: 'Sentinel patterns (99.99.99/11.11.11/10.10.10) always flag at 92 regardless of threshold',
15
+ },
16
+ 'TIER1-OBFUSCATION-HEURISTICS': {
17
+ flag_threshold: 75,
18
+ warn_threshold: 60,
19
+ notes: 'Increased from 70 post-FP analysis; bundlers (webpack, terser) exempt via whitelist',
20
+ },
21
+ 'TIER1-BINARY-EMBED': {
22
+ flag_threshold: 80,
23
+ warn_threshold: 65,
24
+ notes: 'High threshold justified; platform-specific binary sets are rare in legitimate packages',
25
+ },
26
+ 'TIER1-LIFECYCLE-HOOK': {
27
+ flag_threshold: 65,
28
+ warn_threshold: 50,
29
+ notes: 'Moderate threshold; lifecycle hooks common but uncommon in top 1K packages',
30
+ },
31
+ 'TIER1-INFOSTEALER': {
32
+ flag_threshold: 72,
33
+ warn_threshold: 55,
34
+ notes: 'Pattern-based; calibrated for C2 signatures, credential exfil patterns',
35
+ },
36
+ 'TIER1-TYPOSQUAT': {
37
+ flag_threshold: 85,
38
+ warn_threshold: 70,
39
+ notes: 'Calibrated to 85 post-FP analysis on top 1,000 packages; 46 edit-distance=1 FPs eliminated at this threshold',
40
+ },
41
+ 'TIER1-METADATA-SPOOF': {
42
+ flag_threshold: 70,
43
+ warn_threshold: 55,
44
+ notes: 'Namespace/repo URL spoofing; moderate threshold for legitimate clones',
45
+ },
46
+ 'TIER1-VERSION-CONFUSION': {
47
+ flag_threshold: 75,
48
+ warn_threshold: 60,
49
+ notes: 'High-version heuristics (major >= 9); tuned to avoid FP on pre-release tags',
50
+ },
51
+ 'TIER1-CLOUD-IMDS': {
52
+ flag_threshold: 80,
53
+ warn_threshold: 65,
54
+ notes: 'IMDS endpoint targeting is rarely legitimate; high threshold',
55
+ },
56
+ 'TIER1-MULTISTAGE-POSTINSTALL': {
57
+ flag_threshold: 75,
58
+ warn_threshold: 60,
59
+ notes: 'Two-stage download+exec patterns; moderate threshold',
60
+ },
61
+ 'TIER1-SLSA-ATTESTATION': {
62
+ flag_threshold: 85,
63
+ warn_threshold: 70,
64
+ notes: 'Placeholder; threshold TBD when API stabilizes',
65
+ },
66
+ };
@@ -0,0 +1,74 @@
1
+ {
2
+ "packages": [
3
+ {
4
+ "name": "webpack",
5
+ "reason": "Bundler; naturally high entropy in bundled code",
6
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
7
+ },
8
+ {
9
+ "name": "terser",
10
+ "reason": "Minifier library; intentional obfuscation",
11
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
12
+ },
13
+ {
14
+ "name": "uglify-js",
15
+ "reason": "Minifier library; intentional obfuscation",
16
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
17
+ },
18
+ {
19
+ "name": "browserify",
20
+ "reason": "Bundler; bundled JS has high entropy",
21
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
22
+ },
23
+ {
24
+ "name": "rollup",
25
+ "reason": "Bundler; bundled JS has high entropy",
26
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
27
+ },
28
+ {
29
+ "name": "esbuild",
30
+ "reason": "Bundler/compiler; bundled JS has high entropy",
31
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
32
+ },
33
+ {
34
+ "name": "@babel/core",
35
+ "reason": "Transpiler; generated code has high pattern frequency",
36
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
37
+ },
38
+ {
39
+ "name": "typescript",
40
+ "reason": "Compiler; generated JS has high entropy",
41
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
42
+ },
43
+ {
44
+ "name": "lodash",
45
+ "reason": "Utility library; high pattern frequency from common JS idioms",
46
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
47
+ },
48
+ {
49
+ "name": "underscore",
50
+ "reason": "Utility library; high pattern frequency",
51
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
52
+ },
53
+ {
54
+ "name": "moment",
55
+ "reason": "Date library; legitimate build artifacts with binary-like data",
56
+ "detectors": ["TIER1-BINARY-EMBED"]
57
+ },
58
+ {
59
+ "name": "crypto-js",
60
+ "reason": "Cryptography library; legitimate use of hex/unicode escapes and bitwise ops",
61
+ "detectors": ["TIER1-OBFUSCATION-HEURISTICS"]
62
+ },
63
+ {
64
+ "name": "preact",
65
+ "reason": "React alternative; shares naming similarity with react, triggering TYPOSQUAT_VPMDHAJ",
66
+ "detectors": ["TYPOSQUAT_VPMDHAJ"]
67
+ },
68
+ {
69
+ "name": "@commitlint/read",
70
+ "reason": "Legitimate commitlint scoped sub-package; edit-distance FP",
71
+ "detectors": ["TIER1-TYPOSQUAT"]
72
+ }
73
+ ]
74
+ }
@@ -1,99 +1,99 @@
1
- import { codePatternAuthFinding, codePatternInfoFinding } from './findings.js';
2
-
3
- const AUTH_CONTEXT_PATHS = [
4
- 'middleware',
5
- 'auth',
6
- 'security',
7
- 'router',
8
- 'depends',
9
- 'guard',
10
- 'permission',
11
- ];
12
-
13
- const URL_PATH_PATTERN = /request\.url\.path|req\.url\.path|self\.request\.url\.path/g;
14
- const SCOPE_PATH_PATTERN = /request\.scope\s*\[\s*["']path["']\s*\]|request\.scope\.get\s*\(\s*["']path["']\s*\)/g;
15
-
16
- function hasAuthContext(filePath) {
17
- const lower = filePath.toLowerCase();
18
- return AUTH_CONTEXT_PATHS.some(ctx => lower.includes(ctx));
19
- }
20
-
21
- function findFunctionBoundaries(lines) {
22
- const functions = [];
23
- let currentFn = null;
24
- let fnBodyStart = -1;
25
- let indent = 0;
26
-
27
- for (let i = 0; i < lines.length; i++) {
28
- const line = lines[i];
29
- const defMatch = line.match(/^(def\s+\w+|async\s+def\s+\w+)/);
30
- if (defMatch) {
31
- if (currentFn) {
32
- functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
33
- }
34
- currentFn = defMatch[1];
35
- fnBodyStart = i;
36
- indent = line.length - line.trimStart().length;
37
- } else if (currentFn && line.trim() && line.length - line.trimStart().length <= indent && !line.trim().startsWith('#') && !line.trim().startsWith('@')) {
38
- functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
39
- currentFn = null;
40
- }
41
- }
42
- if (currentFn) {
43
- functions.push({ name: currentFn, startLine: fnBodyStart, endLine: lines.length - 1 });
44
- }
45
-
46
- return functions;
47
- }
48
-
49
- function hasScopePathInFunction(lines, fnStart, fnEnd) {
50
- for (let i = fnStart; i <= fnEnd && i < lines.length; i++) {
51
- if (SCOPE_PATH_PATTERN.test(lines[i])) return true;
52
- }
53
- return false;
54
- }
55
-
56
- export function scanCodePatterns(allFiles) {
57
- const findings = [];
58
-
59
- for (const file of (allFiles || [])) {
60
- const content = typeof file.content === 'string' ? file.content : '';
61
- if (!content) continue;
62
- const path = file.path || '';
63
- if (!path.endsWith('.py')) continue;
64
-
65
- const lines = content.split('\n');
66
- const isAuthContext = hasAuthContext(path);
67
- const functions = findFunctionBoundaries(lines);
68
- const suppressedLines = new Set();
69
-
70
- for (const fn of functions) {
71
- if (hasScopePathInFunction(lines, fn.startLine, fn.endLine)) {
72
- for (let i = fn.startLine; i <= fn.endLine && i < lines.length; i++) {
73
- if (URL_PATH_PATTERN.test(lines[i])) {
74
- suppressedLines.add(i + 1);
75
- }
76
- URL_PATH_PATTERN.lastIndex = 0;
77
- }
78
- }
79
- }
80
-
81
- if (isAuthContext) {
82
- let m;
83
- while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
84
- const lineNumber = content.slice(0, m.index).split('\n').length;
85
- if (suppressedLines.has(lineNumber)) continue;
86
- findings.push(codePatternAuthFinding(path, lineNumber));
87
- }
88
- } else {
89
- let m;
90
- while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
91
- const lineNumber = content.slice(0, m.index).split('\n').length;
92
- if (suppressedLines.has(lineNumber)) continue;
93
- findings.push(codePatternInfoFinding(path, lineNumber));
94
- }
95
- }
96
- }
97
-
98
- return findings;
99
- }
1
+ import { codePatternAuthFinding, codePatternInfoFinding } from './findings.js';
2
+
3
+ const AUTH_CONTEXT_PATHS = [
4
+ 'middleware',
5
+ 'auth',
6
+ 'security',
7
+ 'router',
8
+ 'depends',
9
+ 'guard',
10
+ 'permission',
11
+ ];
12
+
13
+ const URL_PATH_PATTERN = /request\.url\.path|req\.url\.path|self\.request\.url\.path/g;
14
+ const SCOPE_PATH_PATTERN = /request\.scope\s*\[\s*["']path["']\s*\]|request\.scope\.get\s*\(\s*["']path["']\s*\)/g;
15
+
16
+ function hasAuthContext(filePath) {
17
+ const lower = filePath.toLowerCase();
18
+ return AUTH_CONTEXT_PATHS.some(ctx => lower.includes(ctx));
19
+ }
20
+
21
+ function findFunctionBoundaries(lines) {
22
+ const functions = [];
23
+ let currentFn = null;
24
+ let fnBodyStart = -1;
25
+ let indent = 0;
26
+
27
+ for (let i = 0; i < lines.length; i++) {
28
+ const line = lines[i];
29
+ const defMatch = line.match(/^(def\s+\w+|async\s+def\s+\w+)/);
30
+ if (defMatch) {
31
+ if (currentFn) {
32
+ functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
33
+ }
34
+ currentFn = defMatch[1];
35
+ fnBodyStart = i;
36
+ indent = line.length - line.trimStart().length;
37
+ } else if (currentFn && line.trim() && line.length - line.trimStart().length <= indent && !line.trim().startsWith('#') && !line.trim().startsWith('@')) {
38
+ functions.push({ name: currentFn, startLine: fnBodyStart, endLine: i - 1 });
39
+ currentFn = null;
40
+ }
41
+ }
42
+ if (currentFn) {
43
+ functions.push({ name: currentFn, startLine: fnBodyStart, endLine: lines.length - 1 });
44
+ }
45
+
46
+ return functions;
47
+ }
48
+
49
+ function hasScopePathInFunction(lines, fnStart, fnEnd) {
50
+ for (let i = fnStart; i <= fnEnd && i < lines.length; i++) {
51
+ if (SCOPE_PATH_PATTERN.test(lines[i])) return true;
52
+ }
53
+ return false;
54
+ }
55
+
56
+ export function scanCodePatterns(allFiles) {
57
+ const findings = [];
58
+
59
+ for (const file of (allFiles || [])) {
60
+ const content = typeof file.content === 'string' ? file.content : '';
61
+ if (!content) continue;
62
+ const path = file.path || '';
63
+ if (!path.endsWith('.py')) continue;
64
+
65
+ const lines = content.split('\n');
66
+ const isAuthContext = hasAuthContext(path);
67
+ const functions = findFunctionBoundaries(lines);
68
+ const suppressedLines = new Set();
69
+
70
+ for (const fn of functions) {
71
+ if (hasScopePathInFunction(lines, fn.startLine, fn.endLine)) {
72
+ for (let i = fn.startLine; i <= fn.endLine && i < lines.length; i++) {
73
+ if (URL_PATH_PATTERN.test(lines[i])) {
74
+ suppressedLines.add(i + 1);
75
+ }
76
+ URL_PATH_PATTERN.lastIndex = 0;
77
+ }
78
+ }
79
+ }
80
+
81
+ if (isAuthContext) {
82
+ let m;
83
+ while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
84
+ const lineNumber = content.slice(0, m.index).split('\n').length;
85
+ if (suppressedLines.has(lineNumber)) continue;
86
+ findings.push(codePatternAuthFinding(path, lineNumber));
87
+ }
88
+ } else {
89
+ let m;
90
+ while ((m = URL_PATH_PATTERN.exec(content)) !== null) {
91
+ const lineNumber = content.slice(0, m.index).split('\n').length;
92
+ if (suppressedLines.has(lineNumber)) continue;
93
+ findings.push(codePatternInfoFinding(path, lineNumber));
94
+ }
95
+ }
96
+ }
97
+
98
+ return findings;
99
+ }
@@ -1,105 +1,105 @@
1
- const CVE = 'CVE-2026-48710';
2
- const NICKNAME = 'BadHost';
3
- const CVSS = 7.0;
4
- const REFERENCES = [
5
- 'https://ostif.org/disclosing-the-badhost-vulnerability-in-starlette/',
6
- 'https://github.com/Kludex/starlette/security/advisories/GHSA-86qp-5c8j-p5mr',
7
- 'https://badhost.org/',
8
- 'https://osv.dev/vulnerability/PYSEC-2026-161',
9
- ];
10
-
11
- const MITIGATION_NOTE = 'Partial mitigation: Cloudflare and AWS ALB reject malformed Host headers for properly proxied deployments. Direct uvicorn/hypercorn/daphne/granian exposure with no reverse proxy in front is highest risk.';
12
-
13
- const DEPENDENCY_REMEDIATION = 'Upgrade starlette to >= 1.0.1. If starlette is inherited transitively through fastapi, vllm, litellm, or an MCP server package, upgrade the top-level package to a version that pins starlette >= 1.0.1. Verify with: pip show starlette.';
14
-
15
- const CODE_REMEDIATION = 'Replace request.url.path with request.scope["path"] for all security-sensitive decisions (auth checks, path allowlists, rate limiting gates). The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.';
16
-
17
- function makeFinding(overrides = {}) {
18
- return {
19
- id: CVE,
20
- severity: 'high',
21
- title: `${NICKNAME} — ${overrides.source || 'unknown'}`,
22
- description: '',
23
- remediation: '',
24
- evidence: JSON.stringify({
25
- cve: CVE,
26
- nickname: NICKNAME,
27
- cvss: CVSS,
28
- references: REFERENCES,
29
- ...overrides,
30
- }),
31
- ...overrides,
32
- };
33
- }
34
-
35
- export function directDependencyFinding(version, specifier) {
36
- return makeFinding({
37
- severity: 'high',
38
- confidence: 'HIGH',
39
- source: 'direct-dependency',
40
- title: `${NICKNAME}: Starlette ${version} vulnerable`,
41
- description: `Starlette ${version} (${specifier}) is vulnerable to CVE-2026-48710 (BadHost) — authentication bypass via Host header injection. Upgrade to starlette >= 1.0.1.`,
42
- remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
43
- file: null,
44
- line: null,
45
- via: null,
46
- });
47
- }
48
-
49
- export function directDependencyUnpinnedFinding() {
50
- return makeFinding({
51
- severity: 'high',
52
- confidence: 'HIGH',
53
- source: 'direct-dependency-unpinned',
54
- title: `${NICKNAME}: Starlette unpinned`,
55
- description: 'Starlette is declared with no version constraint — assume vulnerable to CVE-2026-48710 (BadHost) until pinned to >= 1.0.1.',
56
- remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
57
- file: null,
58
- line: null,
59
- via: null,
60
- });
61
- }
62
-
63
- export function transitiveDependencyFinding(packageName, tier) {
64
- const confidence = tier === 1 ? 'HIGH' : 'MEDIUM';
65
- const tierLabel = tier === 1 ? 'Tier 1' : 'Tier 2';
66
- return makeFinding({
67
- severity: 'high',
68
- confidence,
69
- source: 'transitive-dependency',
70
- title: `${NICKNAME}: Transitive via ${packageName}`,
71
- description: `Starlette not directly pinned; inherited through ${packageName} (${tierLabel}). ${packageName} depends on Starlette — if its version constraint allows Starlette < 1.0.1, your deployment is vulnerable.`,
72
- remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
73
- file: null,
74
- line: null,
75
- via: packageName,
76
- });
77
- }
78
-
79
- export function codePatternAuthFinding(filePath, lineNumber) {
80
- return makeFinding({
81
- severity: 'medium',
82
- confidence: 'MEDIUM',
83
- source: 'code-pattern',
84
- title: `${NICKNAME}: Dangerous path extraction in auth/middleware`,
85
- description: `request.url.path used in auth/middleware context at ${filePath}:${lineNumber} — use request.scope['path'] instead. The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.`,
86
- remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
87
- file: filePath,
88
- line: lineNumber,
89
- via: null,
90
- });
91
- }
92
-
93
- export function codePatternInfoFinding(filePath, lineNumber) {
94
- return makeFinding({
95
- severity: 'info',
96
- confidence: 'LOW',
97
- source: 'code-pattern',
98
- title: `${NICKNAME}: request.url.path usage detected`,
99
- description: `request.url.path used at ${filePath}:${lineNumber} — may be influenced by unvalidated Host header in Starlette < 1.0.1. Verify request.scope['path'] is used for security decisions.`,
100
- remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
101
- file: filePath,
102
- line: lineNumber,
103
- via: null,
104
- });
105
- }
1
+ const CVE = 'CVE-2026-48710';
2
+ const NICKNAME = 'BadHost';
3
+ const CVSS = 7.0;
4
+ const REFERENCES = [
5
+ 'https://ostif.org/disclosing-the-badhost-vulnerability-in-starlette/',
6
+ 'https://github.com/Kludex/starlette/security/advisories/GHSA-86qp-5c8j-p5mr',
7
+ 'https://badhost.org/',
8
+ 'https://osv.dev/vulnerability/PYSEC-2026-161',
9
+ ];
10
+
11
+ const MITIGATION_NOTE = 'Partial mitigation: Cloudflare and AWS ALB reject malformed Host headers for properly proxied deployments. Direct uvicorn/hypercorn/daphne/granian exposure with no reverse proxy in front is highest risk.';
12
+
13
+ const DEPENDENCY_REMEDIATION = 'Upgrade starlette to >= 1.0.1. If starlette is inherited transitively through fastapi, vllm, litellm, or an MCP server package, upgrade the top-level package to a version that pins starlette >= 1.0.1. Verify with: pip show starlette.';
14
+
15
+ const CODE_REMEDIATION = 'Replace request.url.path with request.scope["path"] for all security-sensitive decisions (auth checks, path allowlists, rate limiting gates). The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.';
16
+
17
+ function makeFinding(overrides = {}) {
18
+ return {
19
+ id: CVE,
20
+ severity: 'high',
21
+ title: `${NICKNAME} — ${overrides.source || 'unknown'}`,
22
+ description: '',
23
+ remediation: '',
24
+ evidence: JSON.stringify({
25
+ cve: CVE,
26
+ nickname: NICKNAME,
27
+ cvss: CVSS,
28
+ references: REFERENCES,
29
+ ...overrides,
30
+ }),
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ export function directDependencyFinding(version, specifier) {
36
+ return makeFinding({
37
+ severity: 'high',
38
+ confidence: 'HIGH',
39
+ source: 'direct-dependency',
40
+ title: `${NICKNAME}: Starlette ${version} vulnerable`,
41
+ description: `Starlette ${version} (${specifier}) is vulnerable to CVE-2026-48710 (BadHost) — authentication bypass via Host header injection. Upgrade to starlette >= 1.0.1.`,
42
+ remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
43
+ file: null,
44
+ line: null,
45
+ via: null,
46
+ });
47
+ }
48
+
49
+ export function directDependencyUnpinnedFinding() {
50
+ return makeFinding({
51
+ severity: 'high',
52
+ confidence: 'HIGH',
53
+ source: 'direct-dependency-unpinned',
54
+ title: `${NICKNAME}: Starlette unpinned`,
55
+ description: 'Starlette is declared with no version constraint — assume vulnerable to CVE-2026-48710 (BadHost) until pinned to >= 1.0.1.',
56
+ remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
57
+ file: null,
58
+ line: null,
59
+ via: null,
60
+ });
61
+ }
62
+
63
+ export function transitiveDependencyFinding(packageName, tier) {
64
+ const confidence = tier === 1 ? 'HIGH' : 'MEDIUM';
65
+ const tierLabel = tier === 1 ? 'Tier 1' : 'Tier 2';
66
+ return makeFinding({
67
+ severity: 'high',
68
+ confidence,
69
+ source: 'transitive-dependency',
70
+ title: `${NICKNAME}: Transitive via ${packageName}`,
71
+ description: `Starlette not directly pinned; inherited through ${packageName} (${tierLabel}). ${packageName} depends on Starlette — if its version constraint allows Starlette < 1.0.1, your deployment is vulnerable.`,
72
+ remediation: `${DEPENDENCY_REMEDIATION} ${MITIGATION_NOTE}`,
73
+ file: null,
74
+ line: null,
75
+ via: packageName,
76
+ });
77
+ }
78
+
79
+ export function codePatternAuthFinding(filePath, lineNumber) {
80
+ return makeFinding({
81
+ severity: 'medium',
82
+ confidence: 'MEDIUM',
83
+ source: 'code-pattern',
84
+ title: `${NICKNAME}: Dangerous path extraction in auth/middleware`,
85
+ description: `request.url.path used in auth/middleware context at ${filePath}:${lineNumber} — use request.scope['path'] instead. The reconstructed request.url is influenced by the unvalidated Host header in Starlette < 1.0.1.`,
86
+ remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
87
+ file: filePath,
88
+ line: lineNumber,
89
+ via: null,
90
+ });
91
+ }
92
+
93
+ export function codePatternInfoFinding(filePath, lineNumber) {
94
+ return makeFinding({
95
+ severity: 'info',
96
+ confidence: 'LOW',
97
+ source: 'code-pattern',
98
+ title: `${NICKNAME}: request.url.path usage detected`,
99
+ description: `request.url.path used at ${filePath}:${lineNumber} — may be influenced by unvalidated Host header in Starlette < 1.0.1. Verify request.scope['path'] is used for security decisions.`,
100
+ remediation: `${CODE_REMEDIATION} ${MITIGATION_NOTE}`,
101
+ file: filePath,
102
+ line: lineNumber,
103
+ via: null,
104
+ });
105
+ }
@@ -1,15 +1,15 @@
1
- import { scanFiles } from './manifest.js';
2
- import { scanTransitive } from './transitive.js';
3
- import { scanCodePatterns } from './codePattern.js';
4
-
5
- export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
6
- const targetFiles = allFiles || files;
7
-
8
- const manifestFindings = scanFiles(targetFiles);
9
- const transitiveFindings = scanTransitive(targetFiles);
10
- const codeFindings = scanCodePatterns(targetFiles);
11
-
12
- const allFindings = [...manifestFindings, ...transitiveFindings, ...codeFindings];
13
-
14
- return allFindings;
15
- }
1
+ import { scanFiles } from './manifest.js';
2
+ import { scanTransitive } from './transitive.js';
3
+ import { scanCodePatterns } from './codePattern.js';
4
+
5
+ export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
6
+ const targetFiles = allFiles || files;
7
+
8
+ const manifestFindings = scanFiles(targetFiles);
9
+ const transitiveFindings = scanTransitive(targetFiles);
10
+ const codeFindings = scanCodePatterns(targetFiles);
11
+
12
+ const allFindings = [...manifestFindings, ...transitiveFindings, ...codeFindings];
13
+
14
+ return allFindings;
15
+ }