@kevinrabun/judges 2.3.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -12
- package/dist/api.d.ts +40 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +56 -0
- package/dist/api.js.map +1 -0
- package/dist/ast/cross-file-taint.d.ts +43 -0
- package/dist/ast/cross-file-taint.d.ts.map +1 -0
- package/dist/ast/cross-file-taint.js +713 -0
- package/dist/ast/cross-file-taint.js.map +1 -0
- package/dist/ast/index.d.ts +4 -0
- package/dist/ast/index.d.ts.map +1 -1
- package/dist/ast/index.js +5 -0
- package/dist/ast/index.js.map +1 -1
- package/dist/ast/structural-parser.d.ts.map +1 -1
- package/dist/ast/structural-parser.js +66 -11
- package/dist/ast/structural-parser.js.map +1 -1
- package/dist/ast/taint-tracker.d.ts +35 -0
- package/dist/ast/taint-tracker.d.ts.map +1 -0
- package/dist/ast/taint-tracker.js +518 -0
- package/dist/ast/taint-tracker.js.map +1 -0
- package/dist/ast/types.d.ts +2 -0
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/ast/typescript-ast.d.ts.map +1 -1
- package/dist/ast/typescript-ast.js +25 -5
- package/dist/ast/typescript-ast.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -9
- package/dist/config.js.map +1 -1
- package/dist/dedup.d.ts +19 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/dedup.js +222 -0
- package/dist/dedup.js.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +57 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluators/accessibility.d.ts +1 -1
- package/dist/evaluators/accessibility.d.ts.map +1 -1
- package/dist/evaluators/accessibility.js +22 -16
- package/dist/evaluators/accessibility.js.map +1 -1
- package/dist/evaluators/agent-instructions.d.ts +1 -1
- package/dist/evaluators/agent-instructions.d.ts.map +1 -1
- package/dist/evaluators/agent-instructions.js +1 -2
- package/dist/evaluators/agent-instructions.js.map +1 -1
- package/dist/evaluators/ai-code-safety.d.ts +1 -1
- package/dist/evaluators/ai-code-safety.d.ts.map +1 -1
- package/dist/evaluators/ai-code-safety.js +2 -6
- package/dist/evaluators/ai-code-safety.js.map +1 -1
- package/dist/evaluators/api-design.d.ts +1 -1
- package/dist/evaluators/api-design.d.ts.map +1 -1
- package/dist/evaluators/api-design.js +2 -1
- package/dist/evaluators/api-design.js.map +1 -1
- package/dist/evaluators/app-builder.d.ts +34 -0
- package/dist/evaluators/app-builder.d.ts.map +1 -0
- package/dist/evaluators/app-builder.js +156 -0
- package/dist/evaluators/app-builder.js.map +1 -0
- package/dist/evaluators/authentication.d.ts +1 -1
- package/dist/evaluators/authentication.d.ts.map +1 -1
- package/dist/evaluators/authentication.js +2 -66
- package/dist/evaluators/authentication.js.map +1 -1
- package/dist/evaluators/backwards-compatibility.d.ts +1 -1
- package/dist/evaluators/backwards-compatibility.d.ts.map +1 -1
- package/dist/evaluators/backwards-compatibility.js.map +1 -1
- package/dist/evaluators/caching.d.ts +1 -1
- package/dist/evaluators/caching.d.ts.map +1 -1
- package/dist/evaluators/caching.js.map +1 -1
- package/dist/evaluators/ci-cd.d.ts +1 -1
- package/dist/evaluators/ci-cd.d.ts.map +1 -1
- package/dist/evaluators/ci-cd.js +4 -4
- package/dist/evaluators/ci-cd.js.map +1 -1
- package/dist/evaluators/cloud-readiness.d.ts +1 -1
- package/dist/evaluators/cloud-readiness.d.ts.map +1 -1
- package/dist/evaluators/cloud-readiness.js.map +1 -1
- package/dist/evaluators/code-structure.d.ts +1 -1
- package/dist/evaluators/code-structure.d.ts.map +1 -1
- package/dist/evaluators/code-structure.js +2 -6
- package/dist/evaluators/code-structure.js.map +1 -1
- package/dist/evaluators/compliance.d.ts +1 -1
- package/dist/evaluators/compliance.d.ts.map +1 -1
- package/dist/evaluators/compliance.js +15 -6
- package/dist/evaluators/compliance.js.map +1 -1
- package/dist/evaluators/concurrency.d.ts +1 -1
- package/dist/evaluators/concurrency.d.ts.map +1 -1
- package/dist/evaluators/concurrency.js +9 -4
- package/dist/evaluators/concurrency.js.map +1 -1
- package/dist/evaluators/configuration-management.d.ts +1 -1
- package/dist/evaluators/configuration-management.d.ts.map +1 -1
- package/dist/evaluators/configuration-management.js +7 -2
- package/dist/evaluators/configuration-management.js.map +1 -1
- package/dist/evaluators/cost-effectiveness.d.ts +1 -1
- package/dist/evaluators/cost-effectiveness.d.ts.map +1 -1
- package/dist/evaluators/cost-effectiveness.js +1 -3
- package/dist/evaluators/cost-effectiveness.js.map +1 -1
- package/dist/evaluators/cybersecurity.d.ts +1 -1
- package/dist/evaluators/cybersecurity.d.ts.map +1 -1
- package/dist/evaluators/cybersecurity.js +50 -1
- package/dist/evaluators/cybersecurity.js.map +1 -1
- package/dist/evaluators/data-security.d.ts +1 -1
- package/dist/evaluators/data-security.d.ts.map +1 -1
- package/dist/evaluators/data-security.js +9 -66
- package/dist/evaluators/data-security.js.map +1 -1
- package/dist/evaluators/data-sovereignty.d.ts +1 -1
- package/dist/evaluators/data-sovereignty.d.ts.map +1 -1
- package/dist/evaluators/data-sovereignty.js +4 -2
- package/dist/evaluators/data-sovereignty.js.map +1 -1
- package/dist/evaluators/database.d.ts +1 -1
- package/dist/evaluators/database.d.ts.map +1 -1
- package/dist/evaluators/database.js +3 -1
- package/dist/evaluators/database.js.map +1 -1
- package/dist/evaluators/dependencies.d.ts +6 -0
- package/dist/evaluators/dependencies.d.ts.map +1 -0
- package/dist/evaluators/dependencies.js +204 -0
- package/dist/evaluators/dependencies.js.map +1 -0
- package/dist/evaluators/dependency-health.d.ts +1 -1
- package/dist/evaluators/dependency-health.d.ts.map +1 -1
- package/dist/evaluators/dependency-health.js +198 -6
- package/dist/evaluators/dependency-health.js.map +1 -1
- package/dist/evaluators/documentation.d.ts +1 -1
- package/dist/evaluators/documentation.d.ts.map +1 -1
- package/dist/evaluators/documentation.js +5 -2
- package/dist/evaluators/documentation.js.map +1 -1
- package/dist/evaluators/error-handling.d.ts +1 -1
- package/dist/evaluators/error-handling.d.ts.map +1 -1
- package/dist/evaluators/error-handling.js.map +1 -1
- package/dist/evaluators/ethics-bias.d.ts +1 -1
- package/dist/evaluators/ethics-bias.d.ts.map +1 -1
- package/dist/evaluators/ethics-bias.js +10 -5
- package/dist/evaluators/ethics-bias.js.map +1 -1
- package/dist/evaluators/framework-safety.d.ts +13 -0
- package/dist/evaluators/framework-safety.d.ts.map +1 -0
- package/dist/evaluators/framework-safety.js +424 -0
- package/dist/evaluators/framework-safety.js.map +1 -0
- package/dist/evaluators/index.d.ts +20 -24
- package/dist/evaluators/index.d.ts.map +1 -1
- package/dist/evaluators/index.js +294 -728
- package/dist/evaluators/index.js.map +1 -1
- package/dist/evaluators/internationalization.d.ts +1 -1
- package/dist/evaluators/internationalization.d.ts.map +1 -1
- package/dist/evaluators/internationalization.js +14 -6
- package/dist/evaluators/internationalization.js.map +1 -1
- package/dist/evaluators/logging-privacy.d.ts +1 -1
- package/dist/evaluators/logging-privacy.d.ts.map +1 -1
- package/dist/evaluators/logging-privacy.js +3 -1
- package/dist/evaluators/logging-privacy.js.map +1 -1
- package/dist/evaluators/maintainability.d.ts +1 -1
- package/dist/evaluators/maintainability.d.ts.map +1 -1
- package/dist/evaluators/maintainability.js +15 -9
- package/dist/evaluators/maintainability.js.map +1 -1
- package/dist/evaluators/observability.d.ts +1 -1
- package/dist/evaluators/observability.d.ts.map +1 -1
- package/dist/evaluators/observability.js +2 -1
- package/dist/evaluators/observability.js.map +1 -1
- package/dist/evaluators/performance.d.ts +1 -1
- package/dist/evaluators/performance.d.ts.map +1 -1
- package/dist/evaluators/performance.js +181 -4
- package/dist/evaluators/performance.js.map +1 -1
- package/dist/evaluators/portability.d.ts +1 -1
- package/dist/evaluators/portability.d.ts.map +1 -1
- package/dist/evaluators/portability.js +2 -1
- package/dist/evaluators/portability.js.map +1 -1
- package/dist/evaluators/project.d.ts +16 -0
- package/dist/evaluators/project.d.ts.map +1 -0
- package/dist/evaluators/project.js +353 -0
- package/dist/evaluators/project.js.map +1 -0
- package/dist/evaluators/rate-limiting.d.ts +1 -1
- package/dist/evaluators/rate-limiting.d.ts.map +1 -1
- package/dist/evaluators/rate-limiting.js.map +1 -1
- package/dist/evaluators/reliability.d.ts +1 -1
- package/dist/evaluators/reliability.d.ts.map +1 -1
- package/dist/evaluators/reliability.js.map +1 -1
- package/dist/evaluators/scalability.d.ts +1 -1
- package/dist/evaluators/scalability.d.ts.map +1 -1
- package/dist/evaluators/scalability.js +3 -1
- package/dist/evaluators/scalability.js.map +1 -1
- package/dist/evaluators/shared.d.ts +24 -2
- package/dist/evaluators/shared.d.ts.map +1 -1
- package/dist/evaluators/shared.js +190 -2
- package/dist/evaluators/shared.js.map +1 -1
- package/dist/evaluators/software-practices.d.ts +1 -1
- package/dist/evaluators/software-practices.d.ts.map +1 -1
- package/dist/evaluators/software-practices.js +3 -3
- package/dist/evaluators/software-practices.js.map +1 -1
- package/dist/evaluators/testing.d.ts +1 -1
- package/dist/evaluators/testing.d.ts.map +1 -1
- package/dist/evaluators/testing.js +12 -4
- package/dist/evaluators/testing.js.map +1 -1
- package/dist/evaluators/ux.d.ts +1 -1
- package/dist/evaluators/ux.d.ts.map +1 -1
- package/dist/evaluators/ux.js.map +1 -1
- package/dist/evaluators/v2.d.ts +1 -1
- package/dist/evaluators/v2.d.ts.map +1 -1
- package/dist/evaluators/v2.js +13 -35
- package/dist/evaluators/v2.js.map +1 -1
- package/dist/formatters/sarif.d.ts +75 -0
- package/dist/formatters/sarif.d.ts.map +1 -0
- package/dist/formatters/sarif.js +93 -0
- package/dist/formatters/sarif.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -806
- package/dist/index.js.map +1 -1
- package/dist/judges/accessibility.d.ts +1 -1
- package/dist/judges/accessibility.d.ts.map +1 -1
- package/dist/judges/agent-instructions.d.ts +1 -1
- package/dist/judges/agent-instructions.d.ts.map +1 -1
- package/dist/judges/ai-code-safety.d.ts +1 -1
- package/dist/judges/ai-code-safety.d.ts.map +1 -1
- package/dist/judges/api-design.d.ts +1 -1
- package/dist/judges/api-design.d.ts.map +1 -1
- package/dist/judges/authentication.d.ts +1 -1
- package/dist/judges/authentication.d.ts.map +1 -1
- package/dist/judges/backwards-compatibility.d.ts +1 -1
- package/dist/judges/backwards-compatibility.d.ts.map +1 -1
- package/dist/judges/caching.d.ts +1 -1
- package/dist/judges/caching.d.ts.map +1 -1
- package/dist/judges/ci-cd.d.ts +1 -1
- package/dist/judges/ci-cd.d.ts.map +1 -1
- package/dist/judges/cloud-readiness.d.ts +1 -1
- package/dist/judges/cloud-readiness.d.ts.map +1 -1
- package/dist/judges/code-structure.d.ts +1 -1
- package/dist/judges/code-structure.d.ts.map +1 -1
- package/dist/judges/code-structure.js +7 -1
- package/dist/judges/code-structure.js.map +1 -1
- package/dist/judges/compliance.d.ts +1 -1
- package/dist/judges/compliance.d.ts.map +1 -1
- package/dist/judges/concurrency.d.ts +1 -1
- package/dist/judges/concurrency.d.ts.map +1 -1
- package/dist/judges/configuration-management.d.ts +1 -1
- package/dist/judges/configuration-management.d.ts.map +1 -1
- package/dist/judges/cost-effectiveness.d.ts +1 -1
- package/dist/judges/cost-effectiveness.d.ts.map +1 -1
- package/dist/judges/cybersecurity.d.ts +1 -1
- package/dist/judges/cybersecurity.d.ts.map +1 -1
- package/dist/judges/data-security.d.ts +1 -1
- package/dist/judges/data-security.d.ts.map +1 -1
- package/dist/judges/data-sovereignty.d.ts +1 -1
- package/dist/judges/data-sovereignty.d.ts.map +1 -1
- package/dist/judges/database.d.ts +1 -1
- package/dist/judges/database.d.ts.map +1 -1
- package/dist/judges/dependency-health.d.ts +1 -1
- package/dist/judges/dependency-health.d.ts.map +1 -1
- package/dist/judges/documentation.d.ts +1 -1
- package/dist/judges/documentation.d.ts.map +1 -1
- package/dist/judges/error-handling.d.ts +1 -1
- package/dist/judges/error-handling.d.ts.map +1 -1
- package/dist/judges/ethics-bias.d.ts +1 -1
- package/dist/judges/ethics-bias.d.ts.map +1 -1
- package/dist/judges/framework-safety.d.ts +3 -0
- package/dist/judges/framework-safety.d.ts.map +1 -0
- package/dist/judges/framework-safety.js +31 -0
- package/dist/judges/framework-safety.js.map +1 -0
- package/dist/judges/index.d.ts +1 -1
- package/dist/judges/index.d.ts.map +1 -1
- package/dist/judges/index.js +74 -0
- package/dist/judges/index.js.map +1 -1
- package/dist/judges/internationalization.d.ts +1 -1
- package/dist/judges/internationalization.d.ts.map +1 -1
- package/dist/judges/logging-privacy.d.ts +1 -1
- package/dist/judges/logging-privacy.d.ts.map +1 -1
- package/dist/judges/maintainability.d.ts +1 -1
- package/dist/judges/maintainability.d.ts.map +1 -1
- package/dist/judges/observability.d.ts +1 -1
- package/dist/judges/observability.d.ts.map +1 -1
- package/dist/judges/performance.d.ts +1 -1
- package/dist/judges/performance.d.ts.map +1 -1
- package/dist/judges/portability.d.ts +1 -1
- package/dist/judges/portability.d.ts.map +1 -1
- package/dist/judges/rate-limiting.d.ts +1 -1
- package/dist/judges/rate-limiting.d.ts.map +1 -1
- package/dist/judges/reliability.d.ts +1 -1
- package/dist/judges/reliability.d.ts.map +1 -1
- package/dist/judges/scalability.d.ts +1 -1
- package/dist/judges/scalability.d.ts.map +1 -1
- package/dist/judges/software-practices.d.ts +1 -1
- package/dist/judges/software-practices.d.ts.map +1 -1
- package/dist/judges/testing.d.ts +1 -1
- package/dist/judges/testing.d.ts.map +1 -1
- package/dist/judges/ux.d.ts +1 -1
- package/dist/judges/ux.d.ts.map +1 -1
- package/dist/language-patterns.d.ts +37 -0
- package/dist/language-patterns.d.ts.map +1 -1
- package/dist/language-patterns.js +58 -3
- package/dist/language-patterns.js.map +1 -1
- package/dist/patches/index.d.ts +10 -0
- package/dist/patches/index.d.ts.map +1 -0
- package/dist/patches/index.js +533 -0
- package/dist/patches/index.js.map +1 -0
- package/dist/reports/public-repo-report.d.ts +1 -1
- package/dist/reports/public-repo-report.d.ts.map +1 -1
- package/dist/scoring.d.ts +18 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +178 -0
- package/dist/scoring.js.map +1 -0
- package/dist/tools/deep-review.d.ts +4 -0
- package/dist/tools/deep-review.d.ts.map +1 -0
- package/dist/tools/deep-review.js +56 -0
- package/dist/tools/deep-review.js.map +1 -0
- package/dist/tools/prompts.d.ts +8 -0
- package/dist/tools/prompts.d.ts.map +1 -0
- package/dist/tools/prompts.js +66 -0
- package/dist/tools/prompts.js.map +1 -0
- package/dist/tools/register-evaluation.d.ts +7 -0
- package/dist/tools/register-evaluation.d.ts.map +1 -0
- package/dist/tools/register-evaluation.js +303 -0
- package/dist/tools/register-evaluation.js.map +1 -0
- package/dist/tools/register-workflow.d.ts +7 -0
- package/dist/tools/register-workflow.d.ts.map +1 -0
- package/dist/tools/register-workflow.js +395 -0
- package/dist/tools/register-workflow.js.map +1 -0
- package/dist/tools/register.d.ts +7 -0
- package/dist/tools/register.d.ts.map +1 -0
- package/dist/tools/register.js +14 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/tools/schemas.d.ts +26 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +42 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/types.d.ts +29 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +42 -3
- package/server.json +51 -3
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Cross-File Taint Tracker — Multi-module data-flow analysis
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Extends the single-file taint tracker to propagate taint across module
|
|
5
|
+
// boundaries. When a tainted variable is exported from file A and imported
|
|
6
|
+
// by file B, the taint propagates to wherever file B uses that import.
|
|
7
|
+
//
|
|
8
|
+
// Architecture:
|
|
9
|
+
// 1. Run single-file taint analysis on every file to find sources & tainted vars
|
|
10
|
+
// 2. Build an export map: file → { exportedName → taint info }
|
|
11
|
+
// 3. Resolve imports: for each import, check if the exported binding is tainted
|
|
12
|
+
// 4. Run a second pass on importing files with injected cross-file taint seeds
|
|
13
|
+
// 5. Emit CrossFileTaintFlow findings with full file-to-file provenance
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
import { normalizeLanguage } from "../language-patterns.js";
|
|
16
|
+
// ─── Source / Sink pattern references (same as taint-tracker.ts) ─────────────
|
|
17
|
+
const SOURCE_PATTERNS = [
|
|
18
|
+
{ pattern: /\breq(?:uest)?\.(?:body|query|params|headers|cookies)\b/i, kind: "http-param" },
|
|
19
|
+
{ pattern: /\brequest\.(?:form|args|json|data|values|files|get)\b/i, kind: "http-param" },
|
|
20
|
+
{ pattern: /\b(?:ctx|context)\.(?:query|params|request)\b/i, kind: "http-param" },
|
|
21
|
+
{ pattern: /\bgetParameter\s*\(/i, kind: "http-param" },
|
|
22
|
+
{ pattern: /\bRequest\.(?:Form|QueryString|Params)\b/i, kind: "http-param" },
|
|
23
|
+
{ pattern: /\b(?:process\.argv|sys\.argv|os\.Args|args)\b/i, kind: "user-input" },
|
|
24
|
+
{ pattern: /\b(?:prompt|readline|input)\s*\(/i, kind: "user-input" },
|
|
25
|
+
{ pattern: /\bsearchParams\.get\s*\(/i, kind: "url-param" },
|
|
26
|
+
{ pattern: /\.(?:useSearchParams|useParams)\b/i, kind: "url-param" },
|
|
27
|
+
];
|
|
28
|
+
const SINK_PATTERNS = [
|
|
29
|
+
{ pattern: /\beval\s*\(/i, kind: "code-execution" },
|
|
30
|
+
{ pattern: /\bnew\s+Function\s*\(/i, kind: "code-execution" },
|
|
31
|
+
{ pattern: /\b(?:exec|execSync|system|popen|subprocess\.(?:Popen|run|call)|os\.system)\s*\(/i, kind: "command-exec" },
|
|
32
|
+
{ pattern: /\b(?:spawn|spawnSync)\s*\(/i, kind: "command-exec" },
|
|
33
|
+
{ pattern: /\.(?:query|execute|exec)\s*\(/i, kind: "sql-query" },
|
|
34
|
+
{ pattern: /\.innerHTML\s*=/i, kind: "xss" },
|
|
35
|
+
{ pattern: /\bdocument\.write\s*\(/i, kind: "xss" },
|
|
36
|
+
{ pattern: /\bdangerouslySetInnerHTML/i, kind: "xss" },
|
|
37
|
+
{ pattern: /\b(?:readFile|readFileSync|open)\s*\(/i, kind: "path-traversal" },
|
|
38
|
+
{ pattern: /\.redirect\s*\(/i, kind: "redirect" },
|
|
39
|
+
{ pattern: /\b(?:render_template_string|nunjucks\.renderString|Handlebars\.compile)\s*\(/i, kind: "template" },
|
|
40
|
+
];
|
|
41
|
+
const SANITIZER_PATTERNS = [
|
|
42
|
+
/\bDOMPurify\.sanitize\s*\(/i,
|
|
43
|
+
/\bsanitizeHtml\s*\(/i,
|
|
44
|
+
/\bescapeHtml\s*\(/i,
|
|
45
|
+
/\bencodeURIComponent\s*\(/i,
|
|
46
|
+
/\bvalidator\.\w+\s*\(/i,
|
|
47
|
+
/\b(?:joi|yup|zod|ajv)\b.*\.(?:validate|parse|safeParse)\s*\(/i,
|
|
48
|
+
/\$\d+/,
|
|
49
|
+
/\?\s*(?:,|\))/,
|
|
50
|
+
/\bpath\.(?:normalize|resolve|basename)\s*\(/i,
|
|
51
|
+
/\bPreparedStatement\b/i,
|
|
52
|
+
];
|
|
53
|
+
function isSanitized(expression) {
|
|
54
|
+
return SANITIZER_PATTERNS.some((p) => p.test(expression));
|
|
55
|
+
}
|
|
56
|
+
// ─── Export Analysis ─────────────────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Analyze a file's exports to find which exported bindings carry taint.
|
|
59
|
+
* Detects:
|
|
60
|
+
* - Exported variables assigned from taint sources
|
|
61
|
+
* - Exported functions that return tainted data
|
|
62
|
+
* - Exported functions that pass tainted parameters through to dangerous sinks
|
|
63
|
+
*/
|
|
64
|
+
function analyzeTaintedExports(code, filePath) {
|
|
65
|
+
const lines = code.split("\n");
|
|
66
|
+
const exports = [];
|
|
67
|
+
// Track tainted variables within the file
|
|
68
|
+
const taintedVars = new Map();
|
|
69
|
+
// Pass 1: Find tainted variable assignments
|
|
70
|
+
for (let i = 0; i < lines.length; i++) {
|
|
71
|
+
const line = lines[i];
|
|
72
|
+
const lineNum = i + 1;
|
|
73
|
+
// Variable assignment: const x = req.body.foo
|
|
74
|
+
const assignMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*(.+)/);
|
|
75
|
+
if (assignMatch) {
|
|
76
|
+
const [, varName, rhs] = assignMatch;
|
|
77
|
+
if (isSanitized(rhs))
|
|
78
|
+
continue;
|
|
79
|
+
for (const src of SOURCE_PATTERNS) {
|
|
80
|
+
if (src.pattern.test(rhs)) {
|
|
81
|
+
taintedVars.set(varName, { sourceKind: src.kind, sourceExpr: rhs.trim(), sourceLine: lineNum });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Propagation from tainted variable
|
|
86
|
+
if (!taintedVars.has(varName)) {
|
|
87
|
+
for (const [tv, info] of taintedVars) {
|
|
88
|
+
if (new RegExp(`\\b${tv.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(rhs)) {
|
|
89
|
+
taintedVars.set(varName, info);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Pass 2: Find exported tainted bindings
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
const line = lines[i];
|
|
99
|
+
const lineNum = i + 1;
|
|
100
|
+
// Direct export of tainted variable: export const x = req.body...
|
|
101
|
+
const directExportMatch = line.match(/export\s+(?:const|let|var)\s+(\w+)\s*=\s*(.+)/);
|
|
102
|
+
if (directExportMatch) {
|
|
103
|
+
const [, name, rhs] = directExportMatch;
|
|
104
|
+
for (const src of SOURCE_PATTERNS) {
|
|
105
|
+
if (src.pattern.test(rhs)) {
|
|
106
|
+
exports.push({
|
|
107
|
+
exportedName: name,
|
|
108
|
+
kind: "tainted-variable",
|
|
109
|
+
sourceKind: src.kind,
|
|
110
|
+
sourceExpression: rhs.trim(),
|
|
111
|
+
sourceLine: lineNum,
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Named export of already-tainted variable: export { x }
|
|
118
|
+
const namedExportMatch = line.match(/export\s*\{([^}]+)\}/);
|
|
119
|
+
if (namedExportMatch) {
|
|
120
|
+
const names = namedExportMatch[1].split(",").map((n) => {
|
|
121
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
122
|
+
return { local: parts[0].trim(), exported: (parts[1] ?? parts[0]).trim() };
|
|
123
|
+
});
|
|
124
|
+
for (const { local, exported } of names) {
|
|
125
|
+
if (taintedVars.has(local)) {
|
|
126
|
+
const info = taintedVars.get(local);
|
|
127
|
+
exports.push({
|
|
128
|
+
exportedName: exported,
|
|
129
|
+
kind: "tainted-variable",
|
|
130
|
+
sourceKind: info.sourceKind,
|
|
131
|
+
sourceExpression: info.sourceExpr,
|
|
132
|
+
sourceLine: info.sourceLine,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Exported function that takes params and passes them to sinks
|
|
138
|
+
// export function processInput(userInput: string) { exec(userInput); }
|
|
139
|
+
const exportFnMatch = line.match(/export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/);
|
|
140
|
+
if (exportFnMatch) {
|
|
141
|
+
const [, fnName, params] = exportFnMatch;
|
|
142
|
+
const paramNames = params
|
|
143
|
+
.split(",")
|
|
144
|
+
.map((p) => p
|
|
145
|
+
.trim()
|
|
146
|
+
.split(/[:\s=]/)[0]
|
|
147
|
+
.trim())
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
// Look ahead in the function body for sinks using these params
|
|
150
|
+
const bodyStart = i;
|
|
151
|
+
let braceDepth = 0;
|
|
152
|
+
let foundOpen = false;
|
|
153
|
+
for (let j = i; j < Math.min(i + 100, lines.length); j++) {
|
|
154
|
+
for (const ch of lines[j]) {
|
|
155
|
+
if (ch === "{") {
|
|
156
|
+
braceDepth++;
|
|
157
|
+
foundOpen = true;
|
|
158
|
+
}
|
|
159
|
+
if (ch === "}") {
|
|
160
|
+
braceDepth--;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (foundOpen && braceDepth <= 0) {
|
|
164
|
+
// Scan body for param → sink flows
|
|
165
|
+
const body = lines.slice(bodyStart, j + 1).join("\n");
|
|
166
|
+
const taintedIndices = [];
|
|
167
|
+
for (let pi = 0; pi < paramNames.length; pi++) {
|
|
168
|
+
const pName = paramNames[pi];
|
|
169
|
+
if (!pName)
|
|
170
|
+
continue;
|
|
171
|
+
const pEsc = pName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
172
|
+
const pRe = new RegExp(`\\b${pEsc}\\b`);
|
|
173
|
+
for (const sink of SINK_PATTERNS) {
|
|
174
|
+
// Check each line of the body for a sink that uses this param
|
|
175
|
+
for (let bi = bodyStart; bi <= j && bi < lines.length; bi++) {
|
|
176
|
+
if (sink.pattern.test(lines[bi]) && pRe.test(lines[bi]) && !isSanitized(lines[bi])) {
|
|
177
|
+
taintedIndices.push(pi);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (taintedIndices.length > 0) {
|
|
184
|
+
exports.push({
|
|
185
|
+
exportedName: fnName,
|
|
186
|
+
kind: "tainted-param-passthrough",
|
|
187
|
+
sourceKind: "external-data",
|
|
188
|
+
sourceExpression: `parameter(s) of ${fnName}()`,
|
|
189
|
+
sourceLine: lineNum,
|
|
190
|
+
taintedParamIndices: taintedIndices,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Also check if the function returns tainted data
|
|
194
|
+
if (/\breturn\b/.test(body)) {
|
|
195
|
+
let foundReturn = false;
|
|
196
|
+
// Check if return statement directly contains a taint source
|
|
197
|
+
for (const src of SOURCE_PATTERNS) {
|
|
198
|
+
const returnSourceMatch = body.match(new RegExp(`return\\s+(.*${src.pattern.source}.*)`, "im"));
|
|
199
|
+
if (returnSourceMatch) {
|
|
200
|
+
exports.push({
|
|
201
|
+
exportedName: fnName,
|
|
202
|
+
kind: "tainted-return",
|
|
203
|
+
sourceKind: src.kind,
|
|
204
|
+
sourceExpression: returnSourceMatch[1].trim().replace(/;\s*$/, ""),
|
|
205
|
+
sourceLine: lineNum,
|
|
206
|
+
});
|
|
207
|
+
foundReturn = true;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!foundReturn) {
|
|
212
|
+
for (const [tv, info] of taintedVars) {
|
|
213
|
+
const tvEsc = tv.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
214
|
+
if (new RegExp(`return\\s+.*\\b${tvEsc}\\b`).test(body)) {
|
|
215
|
+
exports.push({
|
|
216
|
+
exportedName: fnName,
|
|
217
|
+
kind: "tainted-return",
|
|
218
|
+
sourceKind: info.sourceKind,
|
|
219
|
+
sourceExpression: info.sourceExpr,
|
|
220
|
+
sourceLine: info.sourceLine,
|
|
221
|
+
});
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// export default function — same as above but with "default" as export name
|
|
232
|
+
const defaultFnMatch = line.match(/export\s+default\s+(?:async\s+)?function\s*(\w*)\s*\(([^)]*)\)/);
|
|
233
|
+
if (defaultFnMatch) {
|
|
234
|
+
const [, fnName, params] = defaultFnMatch;
|
|
235
|
+
const paramNames = params
|
|
236
|
+
.split(",")
|
|
237
|
+
.map((p) => p
|
|
238
|
+
.trim()
|
|
239
|
+
.split(/[:\s=]/)[0]
|
|
240
|
+
.trim())
|
|
241
|
+
.filter(Boolean);
|
|
242
|
+
const bodyStart = i;
|
|
243
|
+
let braceDepth = 0;
|
|
244
|
+
let foundOpen = false;
|
|
245
|
+
for (let j = i; j < Math.min(i + 100, lines.length); j++) {
|
|
246
|
+
for (const ch of lines[j]) {
|
|
247
|
+
if (ch === "{") {
|
|
248
|
+
braceDepth++;
|
|
249
|
+
foundOpen = true;
|
|
250
|
+
}
|
|
251
|
+
if (ch === "}") {
|
|
252
|
+
braceDepth--;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (foundOpen && braceDepth <= 0) {
|
|
256
|
+
const body = lines.slice(bodyStart, j + 1).join("\n");
|
|
257
|
+
const taintedIndices = [];
|
|
258
|
+
for (let pi = 0; pi < paramNames.length; pi++) {
|
|
259
|
+
const pName = paramNames[pi];
|
|
260
|
+
if (!pName)
|
|
261
|
+
continue;
|
|
262
|
+
const pEsc = pName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
263
|
+
const pRe = new RegExp(`\\b${pEsc}\\b`);
|
|
264
|
+
for (const sink of SINK_PATTERNS) {
|
|
265
|
+
for (let bi = bodyStart; bi <= j && bi < lines.length; bi++) {
|
|
266
|
+
if (sink.pattern.test(lines[bi]) && pRe.test(lines[bi]) && !isSanitized(lines[bi])) {
|
|
267
|
+
taintedIndices.push(pi);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (taintedIndices.length > 0) {
|
|
274
|
+
exports.push({
|
|
275
|
+
exportedName: fnName || "default",
|
|
276
|
+
kind: "tainted-param-passthrough",
|
|
277
|
+
sourceKind: "external-data",
|
|
278
|
+
sourceExpression: `parameter(s) of ${fnName || "default"}()`,
|
|
279
|
+
sourceLine: lineNum,
|
|
280
|
+
taintedParamIndices: taintedIndices,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// CommonJS: module.exports = function(...) or module.exports = { ... }
|
|
288
|
+
const cjsExportFnMatch = line.match(/module\.exports\s*=\s*(?:async\s+)?function\s*(\w*)\s*\(([^)]*)\)/);
|
|
289
|
+
if (cjsExportFnMatch) {
|
|
290
|
+
const [, fnName, params] = cjsExportFnMatch;
|
|
291
|
+
const paramNames = params
|
|
292
|
+
.split(",")
|
|
293
|
+
.map((p) => p
|
|
294
|
+
.trim()
|
|
295
|
+
.split(/[:\s=]/)[0]
|
|
296
|
+
.trim())
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
const bodyStart = i;
|
|
299
|
+
let braceDepth = 0;
|
|
300
|
+
let foundOpen = false;
|
|
301
|
+
for (let j = i; j < Math.min(i + 100, lines.length); j++) {
|
|
302
|
+
for (const ch of lines[j]) {
|
|
303
|
+
if (ch === "{") {
|
|
304
|
+
braceDepth++;
|
|
305
|
+
foundOpen = true;
|
|
306
|
+
}
|
|
307
|
+
if (ch === "}") {
|
|
308
|
+
braceDepth--;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (foundOpen && braceDepth <= 0) {
|
|
312
|
+
const body = lines.slice(bodyStart, j + 1).join("\n");
|
|
313
|
+
const taintedIndices = [];
|
|
314
|
+
for (let pi = 0; pi < paramNames.length; pi++) {
|
|
315
|
+
const pName = paramNames[pi];
|
|
316
|
+
if (!pName)
|
|
317
|
+
continue;
|
|
318
|
+
const pEsc = pName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
319
|
+
const pRe = new RegExp(`\\b${pEsc}\\b`);
|
|
320
|
+
for (const sink of SINK_PATTERNS) {
|
|
321
|
+
for (let bi = bodyStart; bi <= j && bi < lines.length; bi++) {
|
|
322
|
+
if (sink.pattern.test(lines[bi]) && pRe.test(lines[bi]) && !isSanitized(lines[bi])) {
|
|
323
|
+
taintedIndices.push(pi);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (taintedIndices.length > 0) {
|
|
330
|
+
exports.push({
|
|
331
|
+
exportedName: "default",
|
|
332
|
+
kind: "tainted-param-passthrough",
|
|
333
|
+
sourceKind: "external-data",
|
|
334
|
+
sourceExpression: `parameter(s) of ${fnName || "default"}()`,
|
|
335
|
+
sourceLine: lineNum,
|
|
336
|
+
taintedParamIndices: taintedIndices,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Check if return directly contains a source
|
|
340
|
+
if (/\breturn\b/.test(body)) {
|
|
341
|
+
for (const src of SOURCE_PATTERNS) {
|
|
342
|
+
const returnSourceMatch = body.match(new RegExp(`return\\s+(.*${src.pattern.source}.*)`, "im"));
|
|
343
|
+
if (returnSourceMatch) {
|
|
344
|
+
exports.push({
|
|
345
|
+
exportedName: "default",
|
|
346
|
+
kind: "tainted-return",
|
|
347
|
+
sourceKind: src.kind,
|
|
348
|
+
sourceExpression: returnSourceMatch[1].trim().replace(/;\s*$/, ""),
|
|
349
|
+
sourceLine: lineNum,
|
|
350
|
+
});
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return exports;
|
|
361
|
+
}
|
|
362
|
+
// ─── Import Parsing ──────────────────────────────────────────────────────────
|
|
363
|
+
/**
|
|
364
|
+
* Parse import statements from a source file. Supports:
|
|
365
|
+
* - import { foo } from "./module"
|
|
366
|
+
* - import { foo as bar } from "./module"
|
|
367
|
+
* - import defaultExport from "./module"
|
|
368
|
+
* - const { foo } = require("./module")
|
|
369
|
+
* - const foo = require("./module")
|
|
370
|
+
* - import * as ns from "./module"
|
|
371
|
+
*/
|
|
372
|
+
function parseImports(code) {
|
|
373
|
+
const lines = code.split("\n");
|
|
374
|
+
const imports = [];
|
|
375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
376
|
+
const line = lines[i];
|
|
377
|
+
const lineNum = i + 1;
|
|
378
|
+
// ES named imports: import { foo, bar as baz } from "./module"
|
|
379
|
+
const namedImportMatch = line.match(/import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/);
|
|
380
|
+
if (namedImportMatch) {
|
|
381
|
+
const [, names, moduleSpec] = namedImportMatch;
|
|
382
|
+
if (!isRelativeImport(moduleSpec))
|
|
383
|
+
continue;
|
|
384
|
+
for (const name of names.split(",")) {
|
|
385
|
+
const parts = name.trim().split(/\s+as\s+/);
|
|
386
|
+
imports.push({
|
|
387
|
+
moduleSpecifier: moduleSpec,
|
|
388
|
+
importedName: parts[0].trim(),
|
|
389
|
+
localName: (parts[1] ?? parts[0]).trim(),
|
|
390
|
+
line: lineNum,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// ES default import: import foo from "./module"
|
|
396
|
+
const defaultImportMatch = line.match(/import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/);
|
|
397
|
+
if (defaultImportMatch) {
|
|
398
|
+
const [, name, moduleSpec] = defaultImportMatch;
|
|
399
|
+
if (!isRelativeImport(moduleSpec))
|
|
400
|
+
continue;
|
|
401
|
+
// Skip if it looks like import { which was already handled
|
|
402
|
+
if (name === "type")
|
|
403
|
+
continue;
|
|
404
|
+
imports.push({
|
|
405
|
+
moduleSpecifier: moduleSpec,
|
|
406
|
+
importedName: "default",
|
|
407
|
+
localName: name,
|
|
408
|
+
line: lineNum,
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
// Namespace import: import * as ns from "./module"
|
|
413
|
+
const nsImportMatch = line.match(/import\s*\*\s*as\s+(\w+)\s+from\s*['"]([^'"]+)['"]/);
|
|
414
|
+
if (nsImportMatch) {
|
|
415
|
+
const [, name, moduleSpec] = nsImportMatch;
|
|
416
|
+
if (!isRelativeImport(moduleSpec))
|
|
417
|
+
continue;
|
|
418
|
+
imports.push({
|
|
419
|
+
moduleSpecifier: moduleSpec,
|
|
420
|
+
importedName: "*",
|
|
421
|
+
localName: name,
|
|
422
|
+
line: lineNum,
|
|
423
|
+
});
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
// CommonJS: const foo = require("./module")
|
|
427
|
+
const requireMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
428
|
+
if (requireMatch) {
|
|
429
|
+
const [, name, moduleSpec] = requireMatch;
|
|
430
|
+
if (!isRelativeImport(moduleSpec))
|
|
431
|
+
continue;
|
|
432
|
+
imports.push({
|
|
433
|
+
moduleSpecifier: moduleSpec,
|
|
434
|
+
importedName: "default",
|
|
435
|
+
localName: name,
|
|
436
|
+
line: lineNum,
|
|
437
|
+
});
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// CommonJS destructured: const { foo, bar } = require("./module")
|
|
441
|
+
const requireDestructMatch = line.match(/(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
442
|
+
if (requireDestructMatch) {
|
|
443
|
+
const [, names, moduleSpec] = requireDestructMatch;
|
|
444
|
+
if (!isRelativeImport(moduleSpec))
|
|
445
|
+
continue;
|
|
446
|
+
for (const name of names.split(",")) {
|
|
447
|
+
const parts = name.trim().split(/\s*:\s*/);
|
|
448
|
+
imports.push({
|
|
449
|
+
moduleSpecifier: moduleSpec,
|
|
450
|
+
importedName: parts[0].trim(),
|
|
451
|
+
localName: (parts[1] ?? parts[0]).trim(),
|
|
452
|
+
line: lineNum,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return imports;
|
|
458
|
+
}
|
|
459
|
+
function isRelativeImport(spec) {
|
|
460
|
+
return spec.startsWith("./") || spec.startsWith("../");
|
|
461
|
+
}
|
|
462
|
+
// ─── Module Resolution ───────────────────────────────────────────────────────
|
|
463
|
+
/**
|
|
464
|
+
* Resolve a relative import specifier to a file path in the project.
|
|
465
|
+
*/
|
|
466
|
+
function resolveModulePath(importerPath, moduleSpecifier, knownPaths) {
|
|
467
|
+
// Compute the directory of the importing file
|
|
468
|
+
const dir = importerPath.replace(/\/[^/]+$/, "") || ".";
|
|
469
|
+
const parts = dir.split("/");
|
|
470
|
+
const importParts = moduleSpecifier.replace(/^\.\//, "").split("/");
|
|
471
|
+
for (const part of importParts) {
|
|
472
|
+
if (part === "..") {
|
|
473
|
+
parts.pop();
|
|
474
|
+
}
|
|
475
|
+
else if (part !== ".") {
|
|
476
|
+
parts.push(part);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const base = parts.join("/");
|
|
480
|
+
// Try exact match, then with common extensions
|
|
481
|
+
const candidates = [
|
|
482
|
+
base,
|
|
483
|
+
`${base}.ts`,
|
|
484
|
+
`${base}.js`,
|
|
485
|
+
`${base}.tsx`,
|
|
486
|
+
`${base}.jsx`,
|
|
487
|
+
`${base}/index.ts`,
|
|
488
|
+
`${base}/index.js`,
|
|
489
|
+
`${base}/index.tsx`,
|
|
490
|
+
`${base}/index.jsx`,
|
|
491
|
+
];
|
|
492
|
+
for (const candidate of candidates) {
|
|
493
|
+
if (knownPaths.has(candidate))
|
|
494
|
+
return candidate;
|
|
495
|
+
// Also try without leading ./
|
|
496
|
+
const trimmed = candidate.replace(/^\.\//, "");
|
|
497
|
+
if (knownPaths.has(trimmed))
|
|
498
|
+
return trimmed;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
503
|
+
/**
|
|
504
|
+
* Analyze taint flows across multiple files in a project. Traces tainted data
|
|
505
|
+
* from sources in one file through export/import boundaries to sinks in
|
|
506
|
+
* another file.
|
|
507
|
+
*
|
|
508
|
+
* Returns both intra-file flows (from the standard taint tracker) and
|
|
509
|
+
* cross-file flows where taint crosses module boundaries.
|
|
510
|
+
*/
|
|
511
|
+
export function analyzeCrossFileTaint(files) {
|
|
512
|
+
const crossFlows = [];
|
|
513
|
+
const knownPaths = new Set(files.map((f) => f.path));
|
|
514
|
+
// Step 1: Analyze each file's tainted exports
|
|
515
|
+
const exportsByFile = new Map();
|
|
516
|
+
for (const f of files) {
|
|
517
|
+
const lang = normalizeLanguage(f.language);
|
|
518
|
+
if (lang !== "javascript" && lang !== "typescript")
|
|
519
|
+
continue;
|
|
520
|
+
const taintedExports = analyzeTaintedExports(f.content, f.path);
|
|
521
|
+
if (taintedExports.length > 0) {
|
|
522
|
+
exportsByFile.set(f.path, taintedExports);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// If no files have tainted exports, no cross-file flows possible
|
|
526
|
+
if (exportsByFile.size === 0)
|
|
527
|
+
return crossFlows;
|
|
528
|
+
// Step 2: For each file, check if its imports reference tainted exports
|
|
529
|
+
for (const f of files) {
|
|
530
|
+
const lang = normalizeLanguage(f.language);
|
|
531
|
+
if (lang !== "javascript" && lang !== "typescript")
|
|
532
|
+
continue;
|
|
533
|
+
const imports = parseImports(f.content);
|
|
534
|
+
if (imports.length === 0)
|
|
535
|
+
continue;
|
|
536
|
+
const lines = f.content.split("\n");
|
|
537
|
+
for (const imp of imports) {
|
|
538
|
+
// Resolve which file this import points to
|
|
539
|
+
const resolvedPath = resolveModulePath(f.path, imp.moduleSpecifier, knownPaths);
|
|
540
|
+
if (!resolvedPath)
|
|
541
|
+
continue;
|
|
542
|
+
const fileExports = exportsByFile.get(resolvedPath);
|
|
543
|
+
if (!fileExports)
|
|
544
|
+
continue;
|
|
545
|
+
// Check if the imported binding matches a tainted export
|
|
546
|
+
for (const exp of fileExports) {
|
|
547
|
+
const nameMatches = imp.importedName === exp.exportedName ||
|
|
548
|
+
(imp.importedName === "default" && exp.exportedName === "default") ||
|
|
549
|
+
imp.importedName === "*"; // Namespace imports get all exports
|
|
550
|
+
if (!nameMatches)
|
|
551
|
+
continue;
|
|
552
|
+
// Determine the local name to track in this file
|
|
553
|
+
const localName = imp.importedName === "*" ? `${imp.localName}.${exp.exportedName}` : imp.localName;
|
|
554
|
+
const localNameEsc = localName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
555
|
+
const localNameRe = new RegExp(`\\b${localNameEsc}\\b`);
|
|
556
|
+
if (exp.kind === "tainted-variable" || exp.kind === "tainted-return") {
|
|
557
|
+
// The imported binding IS tainted — check if it reaches any sink
|
|
558
|
+
for (let li = 0; li < lines.length; li++) {
|
|
559
|
+
const line = lines[li];
|
|
560
|
+
const lineNum = li + 1;
|
|
561
|
+
if (lineNum === imp.line)
|
|
562
|
+
continue; // skip the import line itself
|
|
563
|
+
if (!localNameRe.test(line))
|
|
564
|
+
continue;
|
|
565
|
+
if (isSanitized(line))
|
|
566
|
+
continue;
|
|
567
|
+
for (const sink of SINK_PATTERNS) {
|
|
568
|
+
if (sink.pattern.test(line)) {
|
|
569
|
+
crossFlows.push({
|
|
570
|
+
sourceFile: resolvedPath,
|
|
571
|
+
sinkFile: f.path,
|
|
572
|
+
source: {
|
|
573
|
+
line: exp.sourceLine,
|
|
574
|
+
expression: exp.sourceExpression,
|
|
575
|
+
kind: exp.sourceKind,
|
|
576
|
+
},
|
|
577
|
+
sink: {
|
|
578
|
+
line: lineNum,
|
|
579
|
+
api: sink.pattern.source.slice(0, 40),
|
|
580
|
+
kind: sink.kind,
|
|
581
|
+
},
|
|
582
|
+
exportedBinding: exp.exportedName,
|
|
583
|
+
importedAs: localName,
|
|
584
|
+
confidence: 0.75, // Cross-file flows get slightly lower confidence
|
|
585
|
+
});
|
|
586
|
+
break; // One sink per line
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Also track propagation: if the imported tainted value is assigned
|
|
590
|
+
// to another variable, track that variable to sinks too
|
|
591
|
+
const reassignMatch = line.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*.*\\b${localNameEsc}\\b`));
|
|
592
|
+
if (reassignMatch) {
|
|
593
|
+
const derivedVar = reassignMatch[1];
|
|
594
|
+
const derivedEsc = derivedVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
595
|
+
const derivedRe = new RegExp(`\\b${derivedEsc}\\b`);
|
|
596
|
+
// Scan remaining lines for the derived variable reaching a sink
|
|
597
|
+
for (let dli = li + 1; dli < lines.length; dli++) {
|
|
598
|
+
const dline = lines[dli];
|
|
599
|
+
if (!derivedRe.test(dline))
|
|
600
|
+
continue;
|
|
601
|
+
if (isSanitized(dline))
|
|
602
|
+
continue;
|
|
603
|
+
for (const sink of SINK_PATTERNS) {
|
|
604
|
+
if (sink.pattern.test(dline)) {
|
|
605
|
+
crossFlows.push({
|
|
606
|
+
sourceFile: resolvedPath,
|
|
607
|
+
sinkFile: f.path,
|
|
608
|
+
source: {
|
|
609
|
+
line: exp.sourceLine,
|
|
610
|
+
expression: exp.sourceExpression,
|
|
611
|
+
kind: exp.sourceKind,
|
|
612
|
+
},
|
|
613
|
+
sink: {
|
|
614
|
+
line: dli + 1,
|
|
615
|
+
api: sink.pattern.source.slice(0, 40),
|
|
616
|
+
kind: sink.kind,
|
|
617
|
+
},
|
|
618
|
+
exportedBinding: exp.exportedName,
|
|
619
|
+
importedAs: `${localName} → ${derivedVar}`,
|
|
620
|
+
confidence: 0.65, // Lower confidence for derived propagation
|
|
621
|
+
});
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else if (exp.kind === "tainted-param-passthrough") {
|
|
630
|
+
// The exported function passes its params to sinks — check if callers
|
|
631
|
+
// pass tainted data to those parameters
|
|
632
|
+
const fnCallRe = new RegExp(`\\b${localNameEsc}\\s*\\(([^)]*(?:\\([^)]*\\)[^)]*)*)\\)`);
|
|
633
|
+
for (let li = 0; li < lines.length; li++) {
|
|
634
|
+
const line = lines[li];
|
|
635
|
+
const lineNum = li + 1;
|
|
636
|
+
const callMatch = line.match(fnCallRe);
|
|
637
|
+
if (!callMatch)
|
|
638
|
+
continue;
|
|
639
|
+
const argsStr = callMatch[1];
|
|
640
|
+
// Simple argument splitting (handles basic cases)
|
|
641
|
+
const args = splitArguments(argsStr);
|
|
642
|
+
for (const paramIdx of exp.taintedParamIndices ?? []) {
|
|
643
|
+
if (paramIdx >= args.length)
|
|
644
|
+
continue;
|
|
645
|
+
const arg = args[paramIdx].trim();
|
|
646
|
+
// Check if this argument is tainted (from a source pattern)
|
|
647
|
+
for (const src of SOURCE_PATTERNS) {
|
|
648
|
+
if (src.pattern.test(arg)) {
|
|
649
|
+
crossFlows.push({
|
|
650
|
+
sourceFile: f.path,
|
|
651
|
+
sinkFile: resolvedPath,
|
|
652
|
+
source: {
|
|
653
|
+
line: lineNum,
|
|
654
|
+
expression: arg,
|
|
655
|
+
kind: src.kind,
|
|
656
|
+
},
|
|
657
|
+
sink: {
|
|
658
|
+
line: exp.sourceLine,
|
|
659
|
+
api: `${exp.exportedName}() param[${paramIdx}]`,
|
|
660
|
+
kind: "code-execution", // Generic — the actual sink is in the callee
|
|
661
|
+
},
|
|
662
|
+
exportedBinding: exp.exportedName,
|
|
663
|
+
importedAs: localName,
|
|
664
|
+
confidence: 0.7,
|
|
665
|
+
});
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return deduplicateCrossFlows(crossFlows);
|
|
676
|
+
}
|
|
677
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
678
|
+
/**
|
|
679
|
+
* Split a function call's argument string into individual arguments.
|
|
680
|
+
* Handles nested parentheses but not template literals or complex expressions.
|
|
681
|
+
*/
|
|
682
|
+
function splitArguments(argsStr) {
|
|
683
|
+
const args = [];
|
|
684
|
+
let current = "";
|
|
685
|
+
let depth = 0;
|
|
686
|
+
for (const ch of argsStr) {
|
|
687
|
+
if (ch === "(" || ch === "[" || ch === "{")
|
|
688
|
+
depth++;
|
|
689
|
+
if (ch === ")" || ch === "]" || ch === "}")
|
|
690
|
+
depth--;
|
|
691
|
+
if (ch === "," && depth === 0) {
|
|
692
|
+
args.push(current);
|
|
693
|
+
current = "";
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
current += ch;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (current.trim())
|
|
700
|
+
args.push(current);
|
|
701
|
+
return args;
|
|
702
|
+
}
|
|
703
|
+
function deduplicateCrossFlows(flows) {
|
|
704
|
+
const seen = new Set();
|
|
705
|
+
return flows.filter((f) => {
|
|
706
|
+
const key = `${f.sourceFile}:${f.source.line}→${f.sinkFile}:${f.sink.line}:${f.sink.kind}`;
|
|
707
|
+
if (seen.has(key))
|
|
708
|
+
return false;
|
|
709
|
+
seen.add(key);
|
|
710
|
+
return true;
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
//# sourceMappingURL=cross-file-taint.js.map
|