vaspera 2.8.0 → 2.9.2
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/CHANGELOG.md +109 -7
- package/README.md +111 -7
- package/dist/__tests__/agents/adversary/tactics/api.test.d.ts +5 -0
- package/dist/__tests__/agents/adversary/tactics/api.test.d.ts.map +1 -0
- package/dist/__tests__/agents/adversary/tactics/api.test.js +369 -0
- package/dist/__tests__/agents/adversary/tactics/api.test.js.map +1 -0
- package/dist/__tests__/agents/adversary/tactics/llm.test.d.ts +5 -0
- package/dist/__tests__/agents/adversary/tactics/llm.test.d.ts.map +1 -0
- package/dist/__tests__/agents/adversary/tactics/llm.test.js +409 -0
- package/dist/__tests__/agents/adversary/tactics/llm.test.js.map +1 -0
- package/dist/__tests__/agents/adversary/tactics/registry.test.d.ts +7 -0
- package/dist/__tests__/agents/adversary/tactics/registry.test.d.ts.map +1 -0
- package/dist/__tests__/agents/adversary/tactics/registry.test.js +74 -0
- package/dist/__tests__/agents/adversary/tactics/registry.test.js.map +1 -0
- package/dist/__tests__/agents/adversary/tactics/web-app.test.d.ts +7 -0
- package/dist/__tests__/agents/adversary/tactics/web-app.test.d.ts.map +1 -0
- package/dist/__tests__/agents/adversary/tactics/web-app.test.js +374 -0
- package/dist/__tests__/agents/adversary/tactics/web-app.test.js.map +1 -0
- package/dist/__tests__/compliance-bundle.test.d.ts +9 -0
- package/dist/__tests__/compliance-bundle.test.d.ts.map +1 -0
- package/dist/__tests__/compliance-bundle.test.js +344 -0
- package/dist/__tests__/compliance-bundle.test.js.map +1 -0
- package/dist/__tests__/healthcare-compliance.test.d.ts +9 -0
- package/dist/__tests__/healthcare-compliance.test.d.ts.map +1 -0
- package/dist/__tests__/healthcare-compliance.test.js +233 -0
- package/dist/__tests__/healthcare-compliance.test.js.map +1 -0
- package/dist/action/diff-mode.d.ts +124 -8
- package/dist/action/diff-mode.d.ts.map +1 -1
- package/dist/action/diff-mode.js +384 -65
- package/dist/action/diff-mode.js.map +1 -1
- package/dist/action/diff-mode.test.js +3 -3
- package/dist/action/diff-mode.test.js.map +1 -1
- package/dist/action/pr-comment.test.js +1 -0
- package/dist/action/pr-comment.test.js.map +1 -1
- package/dist/action/sarif-upload.test.js +1 -0
- package/dist/action/sarif-upload.test.js.map +1 -1
- package/dist/agents/adversary/config.d.ts +25 -4
- package/dist/agents/adversary/config.d.ts.map +1 -1
- package/dist/agents/adversary/config.js +38 -8
- package/dist/agents/adversary/config.js.map +1 -1
- package/dist/agents/adversary/index.d.ts +7 -0
- package/dist/agents/adversary/index.d.ts.map +1 -1
- package/dist/agents/adversary/index.js +83 -1
- package/dist/agents/adversary/index.js.map +1 -1
- package/dist/agents/adversary/reporting/compliance-mapper.d.ts +108 -0
- package/dist/agents/adversary/reporting/compliance-mapper.d.ts.map +1 -0
- package/dist/agents/adversary/reporting/compliance-mapper.js +391 -0
- package/dist/agents/adversary/reporting/compliance-mapper.js.map +1 -0
- package/dist/agents/adversary/reporting/index.d.ts +10 -0
- package/dist/agents/adversary/reporting/index.d.ts.map +1 -0
- package/dist/agents/adversary/reporting/index.js +10 -0
- package/dist/agents/adversary/reporting/index.js.map +1 -0
- package/dist/agents/adversary/reporting/poc-generator.d.ts +44 -0
- package/dist/agents/adversary/reporting/poc-generator.d.ts.map +1 -0
- package/dist/agents/adversary/reporting/poc-generator.js +308 -0
- package/dist/agents/adversary/reporting/poc-generator.js.map +1 -0
- package/dist/agents/adversary/tactics/api.d.ts +13 -0
- package/dist/agents/adversary/tactics/api.d.ts.map +1 -0
- package/dist/agents/adversary/tactics/api.js +815 -0
- package/dist/agents/adversary/tactics/api.js.map +1 -0
- package/dist/agents/adversary/tactics/auth.d.ts +13 -0
- package/dist/agents/adversary/tactics/auth.d.ts.map +1 -0
- package/dist/agents/adversary/tactics/auth.js +676 -0
- package/dist/agents/adversary/tactics/auth.js.map +1 -0
- package/dist/agents/adversary/tactics/index.d.ts +129 -0
- package/dist/agents/adversary/tactics/index.d.ts.map +1 -0
- package/dist/agents/adversary/tactics/index.js +199 -0
- package/dist/agents/adversary/tactics/index.js.map +1 -0
- package/dist/agents/adversary/tactics/infra.d.ts +13 -0
- package/dist/agents/adversary/tactics/infra.d.ts.map +1 -0
- package/dist/agents/adversary/tactics/infra.js +827 -0
- package/dist/agents/adversary/tactics/infra.js.map +1 -0
- package/dist/agents/adversary/tactics/injection.d.ts +12 -0
- package/dist/agents/adversary/tactics/injection.d.ts.map +1 -0
- package/dist/agents/adversary/tactics/injection.js +549 -0
- package/dist/agents/adversary/tactics/injection.js.map +1 -0
- package/dist/agents/adversary/tactics/llm.d.ts +13 -0
- package/dist/agents/adversary/tactics/llm.d.ts.map +1 -0
- package/dist/agents/adversary/tactics/llm.js +767 -0
- package/dist/agents/adversary/tactics/llm.js.map +1 -0
- package/dist/agents/adversary/tactics/web-app.d.ts +13 -0
- package/dist/agents/adversary/tactics/web-app.d.ts.map +1 -0
- package/dist/agents/adversary/tactics/web-app.js +717 -0
- package/dist/agents/adversary/tactics/web-app.js.map +1 -0
- package/dist/agents/adversary/types.d.ts +66 -10
- package/dist/agents/adversary/types.d.ts.map +1 -1
- package/dist/agents/zero-day-hunter.d.ts +1 -1
- package/dist/agents/zero-day-hunter.d.ts.map +1 -1
- package/dist/analysis/data-flow.d.ts +154 -0
- package/dist/analysis/data-flow.d.ts.map +1 -0
- package/dist/analysis/data-flow.js +393 -0
- package/dist/analysis/data-flow.js.map +1 -0
- package/dist/analysis/index.d.ts +9 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +9 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/badge-service/index.d.ts +144 -0
- package/dist/badge-service/index.d.ts.map +1 -0
- package/dist/badge-service/index.js +206 -0
- package/dist/badge-service/index.js.map +1 -0
- package/dist/certification/types.d.ts +1 -1
- package/dist/certification/types.d.ts.map +1 -1
- package/dist/certification/types.js.map +1 -1
- package/dist/commands/certification/certify.d.ts.map +1 -1
- package/dist/commands/certification/certify.js +18 -4
- package/dist/commands/certification/certify.js.map +1 -1
- package/dist/compliance/attestation.d.ts +39 -0
- package/dist/compliance/attestation.d.ts.map +1 -0
- package/dist/compliance/attestation.js +364 -0
- package/dist/compliance/attestation.js.map +1 -0
- package/dist/compliance/cfr42-part2.d.ts +42 -0
- package/dist/compliance/cfr42-part2.d.ts.map +1 -0
- package/dist/compliance/cfr42-part2.js +408 -0
- package/dist/compliance/cfr42-part2.js.map +1 -0
- package/dist/compliance/compliance-bundle.d.ts +100 -0
- package/dist/compliance/compliance-bundle.d.ts.map +1 -0
- package/dist/compliance/compliance-bundle.js +210 -0
- package/dist/compliance/compliance-bundle.js.map +1 -0
- package/dist/compliance/healthcare-bundle.d.ts +68 -0
- package/dist/compliance/healthcare-bundle.d.ts.map +1 -0
- package/dist/compliance/healthcare-bundle.js +104 -0
- package/dist/compliance/healthcare-bundle.js.map +1 -0
- package/dist/compliance/hipaa.d.ts.map +1 -1
- package/dist/compliance/hipaa.js +14 -11
- package/dist/compliance/hipaa.js.map +1 -1
- package/dist/compliance/index.d.ts +10 -2
- package/dist/compliance/index.d.ts.map +1 -1
- package/dist/compliance/index.js +9 -3
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/mapper.d.ts.map +1 -1
- package/dist/compliance/mapper.js +3 -17
- package/dist/compliance/mapper.js.map +1 -1
- package/dist/compliance/nist-800-53.d.ts +22 -6
- package/dist/compliance/nist-800-53.d.ts.map +1 -1
- package/dist/compliance/nist-800-53.js +264 -272
- package/dist/compliance/nist-800-53.js.map +1 -1
- package/dist/compliance/report.d.ts +31 -2
- package/dist/compliance/report.d.ts.map +1 -1
- package/dist/compliance/report.js +255 -4
- package/dist/compliance/report.js.map +1 -1
- package/dist/compliance/types.d.ts +1 -1
- package/dist/compliance/types.d.ts.map +1 -1
- package/dist/config/flags.d.ts +12 -12
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.d.ts.map +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/index.js.map +1 -1
- package/dist/cost/tracker.d.ts +64 -0
- package/dist/cost/tracker.d.ts.map +1 -1
- package/dist/cost/tracker.js +165 -0
- package/dist/cost/tracker.js.map +1 -1
- package/dist/eval/fixtures/healthcare/audit-gaps.d.ts +28 -0
- package/dist/eval/fixtures/healthcare/audit-gaps.d.ts.map +1 -0
- package/dist/eval/fixtures/healthcare/audit-gaps.js +90 -0
- package/dist/eval/fixtures/healthcare/audit-gaps.js.map +1 -0
- package/dist/eval/fixtures/healthcare/consent-bypass.d.ts +31 -0
- package/dist/eval/fixtures/healthcare/consent-bypass.d.ts.map +1 -0
- package/dist/eval/fixtures/healthcare/consent-bypass.js +61 -0
- package/dist/eval/fixtures/healthcare/consent-bypass.js.map +1 -0
- package/dist/eval/fixtures/healthcare/phi-in-logs.d.ts +24 -0
- package/dist/eval/fixtures/healthcare/phi-in-logs.d.ts.map +1 -0
- package/dist/eval/fixtures/healthcare/phi-in-logs.js +41 -0
- package/dist/eval/fixtures/healthcare/phi-in-logs.js.map +1 -0
- package/dist/evidence/collector.d.ts +21 -0
- package/dist/evidence/collector.d.ts.map +1 -0
- package/dist/evidence/collector.js +340 -0
- package/dist/evidence/collector.js.map +1 -0
- package/dist/evidence/index.d.ts +11 -0
- package/dist/evidence/index.d.ts.map +1 -0
- package/dist/evidence/index.js +12 -0
- package/dist/evidence/index.js.map +1 -0
- package/dist/evidence/store.d.ts +39 -0
- package/dist/evidence/store.d.ts.map +1 -0
- package/dist/evidence/store.js +173 -0
- package/dist/evidence/store.js.map +1 -0
- package/dist/evidence/types.d.ts +175 -0
- package/dist/evidence/types.d.ts.map +1 -0
- package/dist/evidence/types.js +9 -0
- package/dist/evidence/types.js.map +1 -0
- package/dist/exporters/checkmarx.d.ts +18 -0
- package/dist/exporters/checkmarx.d.ts.map +1 -0
- package/dist/exporters/checkmarx.js +203 -0
- package/dist/exporters/checkmarx.js.map +1 -0
- package/dist/exporters/index.d.ts +22 -0
- package/dist/exporters/index.d.ts.map +1 -0
- package/dist/exporters/index.js +41 -0
- package/dist/exporters/index.js.map +1 -0
- package/dist/exporters/snyk.d.ts +18 -0
- package/dist/exporters/snyk.d.ts.map +1 -0
- package/dist/exporters/snyk.js +119 -0
- package/dist/exporters/snyk.js.map +1 -0
- package/dist/exporters/sonarqube.d.ts +18 -0
- package/dist/exporters/sonarqube.d.ts.map +1 -0
- package/dist/exporters/sonarqube.js +125 -0
- package/dist/exporters/sonarqube.js.map +1 -0
- package/dist/exporters/types.d.ts +190 -0
- package/dist/exporters/types.d.ts.map +1 -0
- package/dist/exporters/types.js +9 -0
- package/dist/exporters/types.js.map +1 -0
- package/dist/frontier/index.d.ts +12 -0
- package/dist/frontier/index.d.ts.map +1 -0
- package/dist/frontier/index.js +12 -0
- package/dist/frontier/index.js.map +1 -0
- package/dist/frontier/orchestrator.d.ts +73 -0
- package/dist/frontier/orchestrator.d.ts.map +1 -0
- package/dist/frontier/orchestrator.js +312 -0
- package/dist/frontier/orchestrator.js.map +1 -0
- package/dist/frontier/providers/stub.d.ts +32 -0
- package/dist/frontier/providers/stub.d.ts.map +1 -0
- package/dist/frontier/providers/stub.js +66 -0
- package/dist/frontier/providers/stub.js.map +1 -0
- package/dist/frontier/types.d.ts +318 -0
- package/dist/frontier/types.d.ts.map +1 -0
- package/dist/frontier/types.js +27 -0
- package/dist/frontier/types.js.map +1 -0
- package/dist/history/index.d.ts +13 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/index.js +15 -0
- package/dist/history/index.js.map +1 -0
- package/dist/history/store.d.ts +74 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/store.js +399 -0
- package/dist/history/store.js.map +1 -0
- package/dist/history/types.d.ts +282 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +41 -0
- package/dist/history/types.js.map +1 -0
- package/dist/history/verify.d.ts +44 -0
- package/dist/history/verify.d.ts.map +1 -0
- package/dist/history/verify.js +230 -0
- package/dist/history/verify.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +515 -19
- package/dist/index.js.map +1 -1
- package/dist/multimodel/index.d.ts +1 -0
- package/dist/multimodel/index.d.ts.map +1 -1
- package/dist/multimodel/index.js +2 -0
- package/dist/multimodel/index.js.map +1 -1
- package/dist/multimodel/leaderboard.d.ts +116 -0
- package/dist/multimodel/leaderboard.d.ts.map +1 -0
- package/dist/multimodel/leaderboard.js +262 -0
- package/dist/multimodel/leaderboard.js.map +1 -0
- package/dist/observability/otel.d.ts.map +1 -1
- package/dist/observability/otel.js +1 -3
- package/dist/observability/otel.js.map +1 -1
- package/dist/plugins/loader.js +1 -1
- package/dist/plugins/loader.js.map +1 -1
- package/dist/scanners/agent/agent-chain-analysis.d.ts +152 -0
- package/dist/scanners/agent/agent-chain-analysis.d.ts.map +1 -0
- package/dist/scanners/agent/agent-chain-analysis.js +438 -0
- package/dist/scanners/agent/agent-chain-analysis.js.map +1 -0
- package/dist/scanners/agent/payloads/index.d.ts +2 -1
- package/dist/scanners/agent/payloads/index.d.ts.map +1 -1
- package/dist/scanners/agent/payloads/index.js +25 -6
- package/dist/scanners/agent/payloads/index.js.map +1 -1
- package/dist/scanners/agent/prompt-injection-fuzzer.d.ts.map +1 -1
- package/dist/scanners/agent/prompt-injection-fuzzer.js +14 -0
- package/dist/scanners/agent/prompt-injection-fuzzer.js.map +1 -1
- package/dist/scanners/agent/types.d.ts +5 -5
- package/dist/scanners/agent/types.d.ts.map +1 -1
- package/dist/scanners/agent/types.js.map +1 -1
- package/dist/scanners/cache.d.ts +156 -0
- package/dist/scanners/cache.d.ts.map +1 -0
- package/dist/scanners/cache.js +462 -0
- package/dist/scanners/cache.js.map +1 -0
- package/dist/scanners/dependencies.js +4 -4
- package/dist/scanners/dependencies.js.map +1 -1
- package/dist/scanners/gosec.d.ts.map +1 -1
- package/dist/scanners/gosec.js +47 -9
- package/dist/scanners/gosec.js.map +1 -1
- package/dist/scanners/healthcare.d.ts +29 -0
- package/dist/scanners/healthcare.d.ts.map +1 -0
- package/dist/scanners/healthcare.js +526 -0
- package/dist/scanners/healthcare.js.map +1 -0
- package/dist/scanners/index.d.ts +1 -0
- package/dist/scanners/index.d.ts.map +1 -1
- package/dist/scanners/index.js +33 -0
- package/dist/scanners/index.js.map +1 -1
- package/dist/scanners/index.test.js +6 -6
- package/dist/scanners/index.test.js.map +1 -1
- package/dist/scanners/secrets.js +4 -4
- package/dist/scanners/secrets.js.map +1 -1
- package/dist/scanners/semgrep.js +5 -5
- package/dist/scanners/semgrep.js.map +1 -1
- package/dist/scanners/types.d.ts +1 -1
- package/dist/scanners/types.d.ts.map +1 -1
- package/dist/scanners/types.js +1 -0
- package/dist/scanners/types.js.map +1 -1
- package/dist/scanners/typescript.test.js +1 -1
- package/dist/scanners/typescript.test.js.map +1 -1
- package/dist/telemetry/index.d.ts +10 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +10 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/registry.d.ts +178 -0
- package/dist/telemetry/registry.d.ts.map +1 -0
- package/dist/telemetry/registry.js +297 -0
- package/dist/telemetry/registry.js.map +1 -0
- package/dist/telemetry/usage.d.ts +197 -0
- package/dist/telemetry/usage.d.ts.map +1 -0
- package/dist/telemetry/usage.js +252 -0
- package/dist/telemetry/usage.js.map +1 -0
- package/package.json +2 -6
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Security Tactics Module
|
|
3
|
+
*
|
|
4
|
+
* Detects API security vulnerabilities including IDOR, BOLA, GraphQL issues,
|
|
5
|
+
* mass assignment, rate limiting, and excessive data exposure.
|
|
6
|
+
* Priority 2 - critical for modern API-driven applications.
|
|
7
|
+
*
|
|
8
|
+
* @module agents/adversary/tactics/api
|
|
9
|
+
*/
|
|
10
|
+
import { registerTactic, generateFindingId, } from "./index.js";
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Patterns
|
|
13
|
+
// ============================================================================
|
|
14
|
+
const IDOR_PATTERNS = [
|
|
15
|
+
{
|
|
16
|
+
id: "idor-direct-id",
|
|
17
|
+
name: "IDOR via Direct Object ID",
|
|
18
|
+
description: "Direct use of user-provided ID without authorization check",
|
|
19
|
+
cwe: "CWE-639",
|
|
20
|
+
severity: "high",
|
|
21
|
+
regex: /(?:findOne|findById|findByPk|get|query)\s*\([^)]*(?:req\.params|req\.query|params\.|query\.)(?:id|userId|postId|orderId)/gi,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "idor-array-access",
|
|
25
|
+
name: "IDOR via Array Index",
|
|
26
|
+
description: "Array access with user-provided index without bounds/ownership check",
|
|
27
|
+
cwe: "CWE-639",
|
|
28
|
+
severity: "high",
|
|
29
|
+
regex: /\[[^\]]*(?:req\.params|req\.query|params\.|query\.)\w+\](?![^;]*(?:owner|user|authorize|check|verify))/gi,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "idor-file-path",
|
|
33
|
+
name: "IDOR via File Path",
|
|
34
|
+
description: "File access with user-provided path without validation",
|
|
35
|
+
cwe: "CWE-639",
|
|
36
|
+
severity: "critical",
|
|
37
|
+
regex: /(?:readFile|createReadStream|unlink|rm)\s*\([^)]*(?:req\.params|req\.query|req\.body)/gi,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "idor-sql-where",
|
|
41
|
+
name: "IDOR in SQL WHERE Clause",
|
|
42
|
+
description: "SQL query filtering by user-provided ID without authorization",
|
|
43
|
+
cwe: "CWE-639",
|
|
44
|
+
severity: "high",
|
|
45
|
+
regex: /WHERE\s+\w+\s*=\s*(?:\$\{|%s|:)(?:id|userId|itemId)(?![^;]*(?:AND\s+(?:owner|user)|JOIN\s+\w+\s+ON))/gi,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
const BOLA_PATTERNS = [
|
|
49
|
+
{
|
|
50
|
+
id: "bola-missing-authz",
|
|
51
|
+
name: "BOLA - Missing Object-Level Authorization",
|
|
52
|
+
description: "Object retrieval without verifying ownership/permission",
|
|
53
|
+
cwe: "CWE-284",
|
|
54
|
+
severity: "high",
|
|
55
|
+
regex: /(?:findOne|findById|get|retrieve)\s*\([^)]*\)(?:[^;]*\.then|[^{]*=>)(?![^}]{0,300}(?:owner|userId|belongsTo|canAccess|isAuthorized|authorize))/gis,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "bola-update-no-check",
|
|
59
|
+
name: "BOLA - Update Without Authorization",
|
|
60
|
+
description: "Object update without ownership verification",
|
|
61
|
+
cwe: "CWE-284",
|
|
62
|
+
severity: "critical",
|
|
63
|
+
regex: /(?:update|patch|put|set)\s*\([^)]*(?:req\.params|params\.)\w+[^)]*\)(?![^}]{0,200}(?:owner|userId|canUpdate|authorize))/gis,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "bola-delete-no-check",
|
|
67
|
+
name: "BOLA - Delete Without Authorization",
|
|
68
|
+
description: "Object deletion without ownership verification",
|
|
69
|
+
cwe: "CWE-284",
|
|
70
|
+
severity: "critical",
|
|
71
|
+
regex: /(?:delete|destroy|remove)\s*\([^)]*(?:req\.params|params\.)\w+[^)]*\)(?![^}]{0,200}(?:owner|userId|canDelete|authorize))/gis,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "bola-bulk-operation",
|
|
75
|
+
name: "BOLA - Bulk Operation Without Filtering",
|
|
76
|
+
description: "Bulk operation without filtering by user ownership",
|
|
77
|
+
cwe: "CWE-284",
|
|
78
|
+
severity: "high",
|
|
79
|
+
regex: /(?:updateMany|deleteMany|findMany)\s*\([^)]*\)(?![^}]{0,300}(?:where.*userId|filter.*owner))/gis,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
const GRAPHQL_PATTERNS = [
|
|
83
|
+
{
|
|
84
|
+
id: "graphql-no-depth-limit",
|
|
85
|
+
name: "GraphQL Missing Depth Limit",
|
|
86
|
+
description: "GraphQL endpoint without query depth limiting",
|
|
87
|
+
cwe: "CWE-770",
|
|
88
|
+
severity: "medium",
|
|
89
|
+
regex: /GraphQLServer|ApolloServer|createServer\s*\(\s*\{(?![^}]*(?:depthLimit|validationRules|maxDepth))/gis,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "graphql-no-cost-analysis",
|
|
93
|
+
name: "GraphQL Missing Cost Analysis",
|
|
94
|
+
description: "GraphQL without query cost analysis or complexity limiting",
|
|
95
|
+
cwe: "CWE-770",
|
|
96
|
+
severity: "medium",
|
|
97
|
+
regex: /(?:GraphQLServer|ApolloServer)\s*\(\s*\{(?![^}]*(?:costAnalysis|queryComplexity|maxComplexity))/gis,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "graphql-introspection-prod",
|
|
101
|
+
name: "GraphQL Introspection Enabled",
|
|
102
|
+
description: "GraphQL introspection enabled in production",
|
|
103
|
+
cwe: "CWE-200",
|
|
104
|
+
severity: "low",
|
|
105
|
+
regex: /introspection\s*:\s*true|(?:GraphQLServer|ApolloServer)(?![^}]*introspection\s*:\s*false)/gi,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "graphql-batch-attack",
|
|
109
|
+
name: "GraphQL Batch Query Vulnerability",
|
|
110
|
+
description: "GraphQL allows unbounded batch queries",
|
|
111
|
+
cwe: "CWE-770",
|
|
112
|
+
severity: "medium",
|
|
113
|
+
regex: /(?:GraphQLServer|ApolloServer)(?![^}]*(?:batchLimit|maxBatchSize|queryBatching\s*:\s*false))/gi,
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
const MASS_ASSIGNMENT_PATTERNS = [
|
|
117
|
+
{
|
|
118
|
+
id: "mass-assignment-spread",
|
|
119
|
+
name: "Mass Assignment via Object Spread",
|
|
120
|
+
description: "Object spread operator with unsanitized user input",
|
|
121
|
+
cwe: "CWE-915",
|
|
122
|
+
severity: "high",
|
|
123
|
+
regex: /(?:create|update|set)\s*\(\s*\{\s*\.\.\.(?:req\.body|body|input|data)\s*\}/gi,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "mass-assignment-assign",
|
|
127
|
+
name: "Mass Assignment via Object.assign",
|
|
128
|
+
description: "Object.assign with unfiltered user input",
|
|
129
|
+
cwe: "CWE-915",
|
|
130
|
+
severity: "high",
|
|
131
|
+
regex: /Object\.assign\s*\([^,]*,\s*(?:req\.body|body|input|data)\s*\)/gi,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "mass-assignment-orm",
|
|
135
|
+
name: "Mass Assignment in ORM",
|
|
136
|
+
description: "ORM accepting all user input without field filtering",
|
|
137
|
+
cwe: "CWE-915",
|
|
138
|
+
severity: "high",
|
|
139
|
+
regex: /(?:User|Model|Entity)\.(?:create|update)\s*\(\s*(?:req\.body|body|input)(?!\s*,\s*\{[^}]*(?:fields|attributes|only|pick)\s*:)/gi,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "mass-assignment-python",
|
|
143
|
+
name: "Mass Assignment in Python ORM",
|
|
144
|
+
description: "Django/SQLAlchemy accepting unfiltered input",
|
|
145
|
+
cwe: "CWE-915",
|
|
146
|
+
severity: "high",
|
|
147
|
+
regex: /(?:Model|objects)\.(?:create|update)\s*\(\s*\*\*(?:request\.data|data|input)\s*\)/gi,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
const RATE_LIMIT_PATTERNS = [
|
|
151
|
+
{
|
|
152
|
+
id: "api-no-rate-limit",
|
|
153
|
+
name: "API Endpoint Without Rate Limiting",
|
|
154
|
+
description: "Public API endpoint without rate limiting middleware",
|
|
155
|
+
cwe: "CWE-770",
|
|
156
|
+
severity: "medium",
|
|
157
|
+
regex: /(?:app|router)\.(?:get|post|put|delete|patch)\s*\(\s*['"][^'"]+['"](?![^}]{0,500}(?:rateLimit|throttle|limiter|slowDown))/gis,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "api-expensive-no-limit",
|
|
161
|
+
name: "Expensive Operation Without Rate Limit",
|
|
162
|
+
description: "Resource-intensive operation without throttling",
|
|
163
|
+
cwe: "CWE-770",
|
|
164
|
+
severity: "high",
|
|
165
|
+
regex: /(?:export|generate|process|compute|calculate|search)\s*[:=]\s*async[^{]*\{(?![^}]{0,800}(?:rateLimit|throttle|queue|defer))/gis,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "api-file-upload-no-limit",
|
|
169
|
+
name: "File Upload Without Rate Limiting",
|
|
170
|
+
description: "File upload endpoint without rate limiting or size limits",
|
|
171
|
+
cwe: "CWE-770",
|
|
172
|
+
severity: "high",
|
|
173
|
+
regex: /(?:upload|multer|formidable)(?![^}]{0,500}(?:limits|maxFileSize|rateLimit))/gi,
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
const DATA_EXPOSURE_PATTERNS = [
|
|
177
|
+
{
|
|
178
|
+
id: "api-full-object-return",
|
|
179
|
+
name: "Excessive Data Exposure - Full Object Return",
|
|
180
|
+
description: "Returning entire object including sensitive fields",
|
|
181
|
+
cwe: "CWE-200",
|
|
182
|
+
severity: "medium",
|
|
183
|
+
regex: /res\.(?:json|send)\s*\(\s*(?:user|account|profile|model)(?!\.[^)]*(?:toPublic|sanitize|pick|omit|select))\s*\)/gi,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "api-all-fields-select",
|
|
187
|
+
name: "Excessive Data Exposure - SELECT *",
|
|
188
|
+
description: "Database query selecting all fields instead of specific ones",
|
|
189
|
+
cwe: "CWE-200",
|
|
190
|
+
severity: "low",
|
|
191
|
+
regex: /SELECT\s+\*\s+FROM|find(?:All|Many)\s*\(\s*(?:\{|$)(?![^}]*(?:select|attributes|fields))/gi,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "api-internal-details",
|
|
195
|
+
name: "Excessive Data Exposure - Internal Details",
|
|
196
|
+
description: "API response includes internal implementation details",
|
|
197
|
+
cwe: "CWE-200",
|
|
198
|
+
severity: "low",
|
|
199
|
+
regex: /res\.(?:json|send)\s*\([^)]*(?:stack|error\.stack|__v|_id|internal|debug)/gi,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "api-sensitive-fields",
|
|
203
|
+
name: "Excessive Data Exposure - Sensitive Fields",
|
|
204
|
+
description: "Response includes password, token, or other sensitive fields",
|
|
205
|
+
cwe: "CWE-200",
|
|
206
|
+
severity: "high",
|
|
207
|
+
regex: /res\.(?:json|send)\s*\([^)]*\{[^}]*(?:password|token|secret|apiKey|privateKey)/gi,
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Tactic Implementation
|
|
212
|
+
// ============================================================================
|
|
213
|
+
const apiTactic = {
|
|
214
|
+
focusArea: "api",
|
|
215
|
+
name: "API Security",
|
|
216
|
+
description: "Detects API vulnerabilities including IDOR, BOLA, GraphQL issues, mass assignment, and data exposure",
|
|
217
|
+
patterns: [
|
|
218
|
+
...IDOR_PATTERNS,
|
|
219
|
+
...BOLA_PATTERNS,
|
|
220
|
+
...GRAPHQL_PATTERNS,
|
|
221
|
+
...MASS_ASSIGNMENT_PATTERNS,
|
|
222
|
+
...RATE_LIMIT_PATTERNS,
|
|
223
|
+
...DATA_EXPOSURE_PATTERNS,
|
|
224
|
+
],
|
|
225
|
+
async analyzeFile(file, config) {
|
|
226
|
+
const findings = [];
|
|
227
|
+
for (const pattern of this.patterns) {
|
|
228
|
+
if (!pattern.regex)
|
|
229
|
+
continue;
|
|
230
|
+
// Reset regex state
|
|
231
|
+
pattern.regex.lastIndex = 0;
|
|
232
|
+
let match;
|
|
233
|
+
while ((match = pattern.regex.exec(file.content)) !== null) {
|
|
234
|
+
// Calculate line number
|
|
235
|
+
const beforeMatch = file.content.substring(0, match.index);
|
|
236
|
+
const lineNum = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
237
|
+
// Skip if in comment
|
|
238
|
+
const line = file.lines[lineNum - 1] || "";
|
|
239
|
+
if (isInComment(line)) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Skip known false positives
|
|
243
|
+
if (isFalsePositive(match[0], pattern.id, file)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const finding = {
|
|
247
|
+
id: generateFindingId("api", file.relativePath, lineNum, pattern.id),
|
|
248
|
+
tacticName: "api",
|
|
249
|
+
focusArea: "api",
|
|
250
|
+
patternId: pattern.id,
|
|
251
|
+
file: file.relativePath,
|
|
252
|
+
line: lineNum,
|
|
253
|
+
message: `${pattern.name}: ${pattern.description}`,
|
|
254
|
+
severity: pattern.severity,
|
|
255
|
+
confidence: calculateConfidence(match[0], pattern, file),
|
|
256
|
+
evidence: match[0].substring(0, 200),
|
|
257
|
+
cweIds: [pattern.cwe],
|
|
258
|
+
mitreIds: getMitreIds(pattern.id),
|
|
259
|
+
suggestedFix: getSuggestedFix(pattern.id),
|
|
260
|
+
};
|
|
261
|
+
findings.push(finding);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return findings;
|
|
265
|
+
},
|
|
266
|
+
async generatePoC(finding) {
|
|
267
|
+
const pocMap = {
|
|
268
|
+
"idor-direct-id": () => idorPoC(finding),
|
|
269
|
+
"idor-file-path": () => idorFilePoC(finding),
|
|
270
|
+
"bola-missing-authz": () => bolaPoC(finding),
|
|
271
|
+
"bola-update-no-check": () => bolaUpdatePoC(finding),
|
|
272
|
+
"bola-delete-no-check": () => bolaDeletePoC(finding),
|
|
273
|
+
"graphql-no-depth-limit": () => graphqlDepthPoC(finding),
|
|
274
|
+
"graphql-batch-attack": () => graphqlBatchPoC(finding),
|
|
275
|
+
"mass-assignment-spread": () => massAssignmentPoC(finding),
|
|
276
|
+
"mass-assignment-orm": () => massAssignmentPoC(finding),
|
|
277
|
+
"api-no-rate-limit": () => rateLimitPoC(finding),
|
|
278
|
+
"api-full-object-return": () => dataExposurePoC(finding),
|
|
279
|
+
};
|
|
280
|
+
const generator = pocMap[finding.patternId];
|
|
281
|
+
return generator ? generator() : null;
|
|
282
|
+
},
|
|
283
|
+
getPromptEnhancement() {
|
|
284
|
+
return `When analyzing for API security vulnerabilities, focus on:
|
|
285
|
+
|
|
286
|
+
1. **IDOR (Insecure Direct Object References)**:
|
|
287
|
+
- User-provided IDs used directly in database queries
|
|
288
|
+
- No verification that user owns/can access the resource
|
|
289
|
+
- Array/file access with user-controlled indices/paths
|
|
290
|
+
- Check for authorization logic after object retrieval
|
|
291
|
+
|
|
292
|
+
2. **BOLA (Broken Object Level Authorization)**:
|
|
293
|
+
- Create, Read, Update, Delete operations on objects
|
|
294
|
+
- Verify ownership/permission checks exist before operations
|
|
295
|
+
- Look for missing \`userId\`, \`owner\`, \`canAccess\` checks
|
|
296
|
+
- Check bulk operations filter by user
|
|
297
|
+
|
|
298
|
+
3. **GraphQL Security**:
|
|
299
|
+
- Query depth limiting to prevent nested query attacks
|
|
300
|
+
- Query complexity/cost analysis
|
|
301
|
+
- Batch query limits
|
|
302
|
+
- Introspection disabled in production
|
|
303
|
+
- Field-level authorization
|
|
304
|
+
|
|
305
|
+
4. **Mass Assignment**:
|
|
306
|
+
- Object spread (\`{ ...req.body }\`) without field filtering
|
|
307
|
+
- \`Object.assign()\` with unvalidated input
|
|
308
|
+
- ORM \`.create()\` or \`.update()\` accepting raw input
|
|
309
|
+
- Look for \`pick\`, \`omit\`, \`fields\`, \`attributes\` filtering
|
|
310
|
+
|
|
311
|
+
5. **Rate Limiting**:
|
|
312
|
+
- Public API endpoints without rate limiting middleware
|
|
313
|
+
- Expensive operations (export, search, file upload)
|
|
314
|
+
- Authentication endpoints separately limited
|
|
315
|
+
- Different limits for authenticated vs unauthenticated
|
|
316
|
+
|
|
317
|
+
6. **Excessive Data Exposure**:
|
|
318
|
+
- Returning full model objects instead of DTOs
|
|
319
|
+
- \`SELECT *\` instead of specific fields
|
|
320
|
+
- Sensitive fields (password, tokens, internal IDs) in responses
|
|
321
|
+
- Error messages revealing stack traces or internal details
|
|
322
|
+
|
|
323
|
+
For each vulnerability, trace:
|
|
324
|
+
- Where does user input enter the system?
|
|
325
|
+
- What authorization checks exist (if any)?
|
|
326
|
+
- Can an attacker access/modify resources they don't own?
|
|
327
|
+
- What data is returned to the client?`;
|
|
328
|
+
},
|
|
329
|
+
getRelevantFilePatterns() {
|
|
330
|
+
return [
|
|
331
|
+
"**/api/**",
|
|
332
|
+
"**/routes/**",
|
|
333
|
+
"**/handlers/**",
|
|
334
|
+
"**/controllers/**",
|
|
335
|
+
"**/resolvers/**",
|
|
336
|
+
"**/graphql/**",
|
|
337
|
+
"**/rest/**",
|
|
338
|
+
"**/*route*",
|
|
339
|
+
"**/*handler*",
|
|
340
|
+
"**/*controller*",
|
|
341
|
+
"**/*resolver*",
|
|
342
|
+
"**/*endpoint*",
|
|
343
|
+
];
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// Helper Functions
|
|
348
|
+
// ============================================================================
|
|
349
|
+
function isInComment(line) {
|
|
350
|
+
const trimmed = line.trim();
|
|
351
|
+
return trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("#");
|
|
352
|
+
}
|
|
353
|
+
function isFalsePositive(match, patternId, file) {
|
|
354
|
+
// Skip test files for most patterns
|
|
355
|
+
if (file.relativePath.includes("test") ||
|
|
356
|
+
file.relativePath.includes("spec") ||
|
|
357
|
+
file.relativePath.includes("mock") ||
|
|
358
|
+
file.relativePath.includes("fixture")) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
// Skip example/demo files
|
|
362
|
+
if (file.relativePath.includes("example") ||
|
|
363
|
+
file.relativePath.includes("demo") ||
|
|
364
|
+
file.relativePath.includes("sample")) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
// Skip if authorization check is nearby (within the match)
|
|
368
|
+
const authKeywords = /(?:authorize|canAccess|isOwner|checkPermission|hasPermission|verifyOwnership|belongsTo)/i;
|
|
369
|
+
if (authKeywords.test(match)) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
function calculateConfidence(match, pattern, file) {
|
|
375
|
+
let confidence = 70;
|
|
376
|
+
// Higher confidence for production-looking code
|
|
377
|
+
if (!file.relativePath.includes("test") && !file.relativePath.includes("example")) {
|
|
378
|
+
confidence += 10;
|
|
379
|
+
}
|
|
380
|
+
// Higher confidence for critical patterns
|
|
381
|
+
if (pattern.severity === "critical") {
|
|
382
|
+
confidence += 10;
|
|
383
|
+
}
|
|
384
|
+
// Lower confidence if there are authorization-related imports nearby
|
|
385
|
+
if (file.content.includes("authorize") || file.content.includes("permission")) {
|
|
386
|
+
confidence -= 10;
|
|
387
|
+
}
|
|
388
|
+
// Higher confidence for obvious unsafe patterns
|
|
389
|
+
if (match.includes("req.params") || match.includes("req.query")) {
|
|
390
|
+
confidence += 5;
|
|
391
|
+
}
|
|
392
|
+
return Math.min(95, Math.max(50, confidence));
|
|
393
|
+
}
|
|
394
|
+
function getMitreIds(patternId) {
|
|
395
|
+
const mapping = {
|
|
396
|
+
"idor-direct-id": ["T1078", "T1213"],
|
|
397
|
+
"idor-file-path": ["T1083", "T1005"],
|
|
398
|
+
"bola-missing-authz": ["T1078", "T1213"],
|
|
399
|
+
"bola-update-no-check": ["T1565", "T1078"],
|
|
400
|
+
"bola-delete-no-check": ["T1485", "T1078"],
|
|
401
|
+
"graphql-no-depth-limit": ["T1499"],
|
|
402
|
+
"graphql-batch-attack": ["T1499"],
|
|
403
|
+
"mass-assignment-spread": ["T1068", "T1078"],
|
|
404
|
+
"mass-assignment-orm": ["T1068", "T1078"],
|
|
405
|
+
"api-no-rate-limit": ["T1499", "T1110"],
|
|
406
|
+
"api-expensive-no-limit": ["T1499"],
|
|
407
|
+
"api-full-object-return": ["T1530", "T1213"],
|
|
408
|
+
"api-sensitive-fields": ["T1552", "T1213"],
|
|
409
|
+
};
|
|
410
|
+
return mapping[patternId] || ["T1190"];
|
|
411
|
+
}
|
|
412
|
+
function getSuggestedFix(patternId) {
|
|
413
|
+
const fixes = {
|
|
414
|
+
"idor-direct-id": "Add authorization check: verify user owns/can access the resource before retrieval",
|
|
415
|
+
"idor-file-path": "Validate file path against allowed list, check ownership, use indirect references",
|
|
416
|
+
"idor-array-access": "Validate array index, check ownership of accessed resource",
|
|
417
|
+
"bola-missing-authz": "Add ownership check: WHERE user_id = current_user_id or use canAccess() method",
|
|
418
|
+
"bola-update-no-check": "Verify user owns resource before update: if (resource.userId !== req.user.id) throw Forbidden",
|
|
419
|
+
"bola-delete-no-check": "Verify user owns resource before delete: check ownership in WHERE clause",
|
|
420
|
+
"bola-bulk-operation": "Filter bulk operations by user: { where: { userId: req.user.id } }",
|
|
421
|
+
"graphql-no-depth-limit": "Add depth limiting: validationRules: [depthLimit(7)]",
|
|
422
|
+
"graphql-no-cost-analysis": "Add query complexity analysis: validationRules: [costAnalysis({ maximumCost: 1000 })]",
|
|
423
|
+
"graphql-introspection-prod": "Disable introspection in production: introspection: process.env.NODE_ENV !== 'production'",
|
|
424
|
+
"graphql-batch-attack": "Limit batch queries: batchLimit: 5 or queryBatching: false",
|
|
425
|
+
"mass-assignment-spread": "Use field filtering: const { allowedField1, allowedField2 } = req.body",
|
|
426
|
+
"mass-assignment-assign": "Use pick/omit: Object.assign(model, _.pick(req.body, ['safe', 'fields']))",
|
|
427
|
+
"mass-assignment-orm": "Specify allowed fields: Model.create(data, { fields: ['name', 'email'] })",
|
|
428
|
+
"api-no-rate-limit": "Add rate limiting middleware: app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }))",
|
|
429
|
+
"api-expensive-no-limit": "Add throttling: use queue/defer for expensive operations, add rate limits",
|
|
430
|
+
"api-file-upload-no-limit": "Add limits: multer({ limits: { fileSize: 5 * 1024 * 1024 } }), add rate limiting",
|
|
431
|
+
"api-full-object-return": "Return DTO instead: res.json(user.toPublicJSON()) or use serializer",
|
|
432
|
+
"api-all-fields-select": "Select specific fields: Model.findOne({ attributes: ['id', 'name', 'email'] })",
|
|
433
|
+
"api-internal-details": "Remove internal details from responses, use custom error handler",
|
|
434
|
+
"api-sensitive-fields": "Exclude sensitive fields: use toJSON method, serializer, or explicit field selection",
|
|
435
|
+
};
|
|
436
|
+
return fixes[patternId] || "Review authorization and data exposure in API endpoint";
|
|
437
|
+
}
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// PoC Generators
|
|
440
|
+
// ============================================================================
|
|
441
|
+
function idorPoC(finding) {
|
|
442
|
+
const steps = [
|
|
443
|
+
{
|
|
444
|
+
order: 1,
|
|
445
|
+
action: "identify-endpoint",
|
|
446
|
+
description: "Identify the API endpoint accepting resource ID",
|
|
447
|
+
command: `Review ${finding.file}:${finding.line}`,
|
|
448
|
+
expectedResult: "Endpoint identified (e.g., GET /api/users/:id)",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
order: 2,
|
|
452
|
+
action: "access-own-resource",
|
|
453
|
+
description: "Access your own resource to understand response format",
|
|
454
|
+
command: `curl -H "Authorization: Bearer USER_A_TOKEN" https://target/api/resource/123`,
|
|
455
|
+
expectedResult: "Own resource data returned successfully",
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
order: 3,
|
|
459
|
+
action: "enumerate-ids",
|
|
460
|
+
description: "Try accessing sequential IDs or discovered IDs",
|
|
461
|
+
command: `curl -H "Authorization: Bearer USER_A_TOKEN" https://target/api/resource/124`,
|
|
462
|
+
expectedResult: "Other user's resource data returned without authorization check",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
order: 4,
|
|
466
|
+
action: "confirm-idor",
|
|
467
|
+
description: "Confirm access to resources belonging to other users",
|
|
468
|
+
command: `for id in {1..100}; do curl -H "Authorization: Bearer USER_A_TOKEN" https://target/api/resource/$id; done`,
|
|
469
|
+
expectedResult: "Can read resources owned by other users",
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
return {
|
|
473
|
+
id: `poc-${finding.id}`,
|
|
474
|
+
findingId: finding.id,
|
|
475
|
+
prerequisites: [
|
|
476
|
+
"Valid user account and authentication token",
|
|
477
|
+
"Knowledge of resource ID format",
|
|
478
|
+
],
|
|
479
|
+
steps,
|
|
480
|
+
payload: "GET /api/resource/{other_user_id}",
|
|
481
|
+
expectedResult: "Unauthorized access to other users' resources",
|
|
482
|
+
safeTestInstructions: "Test with multiple test accounts you control. Never access real user data. Test on staging/development environment only.",
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function idorFilePoC(finding) {
|
|
486
|
+
const steps = [
|
|
487
|
+
{
|
|
488
|
+
order: 1,
|
|
489
|
+
action: "identify-endpoint",
|
|
490
|
+
description: "Identify file download/access endpoint",
|
|
491
|
+
command: `Review ${finding.file}:${finding.line}`,
|
|
492
|
+
expectedResult: "File endpoint identified",
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
order: 2,
|
|
496
|
+
action: "test-path-traversal",
|
|
497
|
+
description: "Test for path traversal vulnerability",
|
|
498
|
+
command: `curl -H "Authorization: Bearer TOKEN" https://target/api/file?path=../../../etc/passwd`,
|
|
499
|
+
expectedResult: "System file or other user's file accessible",
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
order: 3,
|
|
503
|
+
action: "enumerate-files",
|
|
504
|
+
description: "Enumerate accessible files",
|
|
505
|
+
command: `curl -H "Authorization: Bearer TOKEN" https://target/api/file?path=../uploads/user_123/document.pdf`,
|
|
506
|
+
expectedResult: "Access to files belonging to other users",
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
return {
|
|
510
|
+
id: `poc-${finding.id}`,
|
|
511
|
+
findingId: finding.id,
|
|
512
|
+
prerequisites: [
|
|
513
|
+
"Valid user account",
|
|
514
|
+
"Understanding of file storage structure",
|
|
515
|
+
],
|
|
516
|
+
steps,
|
|
517
|
+
payload: "?path=../../../../etc/passwd",
|
|
518
|
+
expectedResult: "Unauthorized file access via path manipulation",
|
|
519
|
+
safeTestInstructions: "Test on isolated environment. Try accessing only test files you created. Never access system files or real user data.",
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function bolaPoC(finding) {
|
|
523
|
+
const steps = [
|
|
524
|
+
{
|
|
525
|
+
order: 1,
|
|
526
|
+
action: "create-resources",
|
|
527
|
+
description: "Create test resources with two different user accounts",
|
|
528
|
+
command: "Create resource A with User A, resource B with User B",
|
|
529
|
+
expectedResult: "Both resources created with known IDs",
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
order: 2,
|
|
533
|
+
action: "test-cross-access",
|
|
534
|
+
description: "Try to access User B's resource using User A's token",
|
|
535
|
+
command: `curl -H "Authorization: Bearer USER_A_TOKEN" https://target/api/resource/{USER_B_RESOURCE_ID}`,
|
|
536
|
+
expectedResult: "User B's resource returned without authorization check",
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
order: 3,
|
|
540
|
+
action: "verify-bola",
|
|
541
|
+
description: "Verify authorization is not enforced at object level",
|
|
542
|
+
command: "Confirm authentication is checked but not object-level authorization",
|
|
543
|
+
expectedResult: "Authenticated users can access any resource regardless of ownership",
|
|
544
|
+
},
|
|
545
|
+
];
|
|
546
|
+
return {
|
|
547
|
+
id: `poc-${finding.id}`,
|
|
548
|
+
findingId: finding.id,
|
|
549
|
+
prerequisites: [
|
|
550
|
+
"Two test user accounts",
|
|
551
|
+
"Ability to create resources",
|
|
552
|
+
],
|
|
553
|
+
steps,
|
|
554
|
+
payload: "GET /api/resource/{other_user_resource_id}",
|
|
555
|
+
expectedResult: "Access to resources without ownership verification",
|
|
556
|
+
safeTestInstructions: "Use only test accounts you control. Test on development/staging environment. Do not access real user resources.",
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function bolaUpdatePoC(finding) {
|
|
560
|
+
const steps = [
|
|
561
|
+
{
|
|
562
|
+
order: 1,
|
|
563
|
+
action: "identify-update-endpoint",
|
|
564
|
+
description: "Identify the update endpoint",
|
|
565
|
+
command: `Review ${finding.file}:${finding.line}`,
|
|
566
|
+
expectedResult: "Update endpoint identified (e.g., PUT /api/resource/:id)",
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
order: 2,
|
|
570
|
+
action: "create-target-resource",
|
|
571
|
+
description: "Create a resource with User B",
|
|
572
|
+
command: `curl -X POST -H "Authorization: Bearer USER_B_TOKEN" https://target/api/resource -d '{"name":"Original"}'`,
|
|
573
|
+
expectedResult: "Resource created with known ID",
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
order: 3,
|
|
577
|
+
action: "modify-other-resource",
|
|
578
|
+
description: "Attempt to modify User B's resource using User A's token",
|
|
579
|
+
command: `curl -X PUT -H "Authorization: Bearer USER_A_TOKEN" https://target/api/resource/{USER_B_ID} -d '{"name":"Modified"}'`,
|
|
580
|
+
expectedResult: "Resource modified without ownership check",
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
return {
|
|
584
|
+
id: `poc-${finding.id}`,
|
|
585
|
+
findingId: finding.id,
|
|
586
|
+
prerequisites: [
|
|
587
|
+
"Two test accounts",
|
|
588
|
+
"Ability to create and modify resources",
|
|
589
|
+
],
|
|
590
|
+
steps,
|
|
591
|
+
payload: `PUT /api/resource/{other_user_id} {"name":"Attacker Modified"}`,
|
|
592
|
+
expectedResult: "Unauthorized modification of other users' resources",
|
|
593
|
+
safeTestInstructions: "Test with controlled test accounts only. Never modify real user data. Test on isolated environment.",
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function bolaDeletePoC(finding) {
|
|
597
|
+
const steps = [
|
|
598
|
+
{
|
|
599
|
+
order: 1,
|
|
600
|
+
action: "identify-delete-endpoint",
|
|
601
|
+
description: "Identify the delete endpoint",
|
|
602
|
+
command: `Review ${finding.file}:${finding.line}`,
|
|
603
|
+
expectedResult: "Delete endpoint identified",
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
order: 2,
|
|
607
|
+
action: "create-target-resource",
|
|
608
|
+
description: "Create a test resource with User B",
|
|
609
|
+
command: `curl -X POST -H "Authorization: Bearer USER_B_TOKEN" https://target/api/resource -d '{"name":"Test"}'`,
|
|
610
|
+
expectedResult: "Resource created",
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
order: 3,
|
|
614
|
+
action: "delete-other-resource",
|
|
615
|
+
description: "Attempt to delete User B's resource using User A's token",
|
|
616
|
+
command: `curl -X DELETE -H "Authorization: Bearer USER_A_TOKEN" https://target/api/resource/{USER_B_ID}`,
|
|
617
|
+
expectedResult: "Resource deleted without ownership verification",
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
return {
|
|
621
|
+
id: `poc-${finding.id}`,
|
|
622
|
+
findingId: finding.id,
|
|
623
|
+
prerequisites: [
|
|
624
|
+
"Two test accounts",
|
|
625
|
+
"Ability to create resources",
|
|
626
|
+
],
|
|
627
|
+
steps,
|
|
628
|
+
payload: "DELETE /api/resource/{other_user_id}",
|
|
629
|
+
expectedResult: "Unauthorized deletion of other users' resources",
|
|
630
|
+
safeTestInstructions: "Test with disposable test data only. Never delete real user resources. Use isolated test environment.",
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function graphqlDepthPoC(finding) {
|
|
634
|
+
const steps = [
|
|
635
|
+
{
|
|
636
|
+
order: 1,
|
|
637
|
+
action: "craft-deep-query",
|
|
638
|
+
description: "Craft a deeply nested GraphQL query",
|
|
639
|
+
command: `Create query with 20+ levels of nesting`,
|
|
640
|
+
expectedResult: "Deeply nested query prepared",
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
order: 2,
|
|
644
|
+
action: "execute-query",
|
|
645
|
+
description: "Execute the deeply nested query",
|
|
646
|
+
command: `curl -X POST https://target/graphql -d '{"query":"query { user { posts { comments { author { posts { comments { ... } } } } } } }"}'`,
|
|
647
|
+
expectedResult: "Query executes, potentially causing performance degradation",
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
order: 3,
|
|
651
|
+
action: "measure-impact",
|
|
652
|
+
description: "Measure server response time and resource usage",
|
|
653
|
+
command: "Monitor query execution time and server CPU/memory",
|
|
654
|
+
expectedResult: "Significant resource consumption, potential DoS",
|
|
655
|
+
},
|
|
656
|
+
];
|
|
657
|
+
return {
|
|
658
|
+
id: `poc-${finding.id}`,
|
|
659
|
+
findingId: finding.id,
|
|
660
|
+
prerequisites: [
|
|
661
|
+
"Access to GraphQL endpoint",
|
|
662
|
+
"Understanding of GraphQL schema",
|
|
663
|
+
],
|
|
664
|
+
steps,
|
|
665
|
+
payload: "query { user { posts { comments { author { posts { comments { author { ... nested 20 levels } } } } } } }",
|
|
666
|
+
expectedResult: "Server resource exhaustion via deeply nested query",
|
|
667
|
+
safeTestInstructions: "Test on staging environment only. Start with moderate nesting (5-7 levels). Monitor server resources. Never DoS production.",
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function graphqlBatchPoC(finding) {
|
|
671
|
+
const steps = [
|
|
672
|
+
{
|
|
673
|
+
order: 1,
|
|
674
|
+
action: "craft-batch",
|
|
675
|
+
description: "Craft a batch query with many operations",
|
|
676
|
+
command: "Create query array with 100+ identical expensive queries",
|
|
677
|
+
expectedResult: "Batch query prepared",
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
order: 2,
|
|
681
|
+
action: "execute-batch",
|
|
682
|
+
description: "Execute the batch query",
|
|
683
|
+
command: `curl -X POST https://target/graphql -d '[{"query":"query{users{...}}"},{"query":"query{users{...}}"},...x100]'`,
|
|
684
|
+
expectedResult: "All queries execute, multiplying resource consumption",
|
|
685
|
+
},
|
|
686
|
+
];
|
|
687
|
+
return {
|
|
688
|
+
id: `poc-${finding.id}`,
|
|
689
|
+
findingId: finding.id,
|
|
690
|
+
prerequisites: [
|
|
691
|
+
"GraphQL endpoint allowing batched queries",
|
|
692
|
+
],
|
|
693
|
+
steps,
|
|
694
|
+
payload: "[{\"query\":\"expensive_query\"}] repeated 100+ times",
|
|
695
|
+
expectedResult: "Server DoS via batch query amplification",
|
|
696
|
+
safeTestInstructions: "Test on isolated environment. Start with small batches (5-10). Never DoS production systems.",
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function massAssignmentPoC(finding) {
|
|
700
|
+
const steps = [
|
|
701
|
+
{
|
|
702
|
+
order: 1,
|
|
703
|
+
action: "identify-model-fields",
|
|
704
|
+
description: "Identify model fields including privileged ones",
|
|
705
|
+
command: `Review ${finding.file} and model definition`,
|
|
706
|
+
expectedResult: "Fields like 'isAdmin', 'role', 'credits' identified",
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
order: 2,
|
|
710
|
+
action: "test-normal-update",
|
|
711
|
+
description: "Test normal profile update",
|
|
712
|
+
command: `curl -X PUT -H "Authorization: Bearer TOKEN" https://target/api/profile -d '{"name":"Test"}'`,
|
|
713
|
+
expectedResult: "Normal field updated successfully",
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
order: 3,
|
|
717
|
+
action: "inject-privileged-field",
|
|
718
|
+
description: "Inject privileged field in request",
|
|
719
|
+
command: `curl -X PUT -H "Authorization: Bearer TOKEN" https://target/api/profile -d '{"name":"Test","isAdmin":true,"role":"admin"}'`,
|
|
720
|
+
expectedResult: "Privileged field accepted and user escalated to admin",
|
|
721
|
+
},
|
|
722
|
+
];
|
|
723
|
+
return {
|
|
724
|
+
id: `poc-${finding.id}`,
|
|
725
|
+
findingId: finding.id,
|
|
726
|
+
prerequisites: [
|
|
727
|
+
"Valid user account",
|
|
728
|
+
"Knowledge of model schema",
|
|
729
|
+
"Update endpoint accepting JSON body",
|
|
730
|
+
],
|
|
731
|
+
steps,
|
|
732
|
+
payload: `{"name":"Test","isAdmin":true,"role":"admin","credits":999999}`,
|
|
733
|
+
expectedResult: "Privilege escalation or unauthorized field modification via mass assignment",
|
|
734
|
+
safeTestInstructions: "Test with test account only. Verify after test and revert any privilege changes. Test on development environment.",
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function rateLimitPoC(finding) {
|
|
738
|
+
const steps = [
|
|
739
|
+
{
|
|
740
|
+
order: 1,
|
|
741
|
+
action: "identify-endpoint",
|
|
742
|
+
description: "Identify endpoint without rate limiting",
|
|
743
|
+
command: `Review ${finding.file}:${finding.line}`,
|
|
744
|
+
expectedResult: "Endpoint identified",
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
order: 2,
|
|
748
|
+
action: "test-rapid-requests",
|
|
749
|
+
description: "Send rapid successive requests",
|
|
750
|
+
command: `for i in {1..100}; do curl https://target/api/endpoint & done`,
|
|
751
|
+
expectedResult: "All requests accepted without throttling",
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
order: 3,
|
|
755
|
+
action: "verify-no-blocking",
|
|
756
|
+
description: "Verify no rate limiting or IP blocking occurs",
|
|
757
|
+
command: "Continue sending requests and monitor response codes",
|
|
758
|
+
expectedResult: "No 429 Too Many Requests or blocking observed",
|
|
759
|
+
},
|
|
760
|
+
];
|
|
761
|
+
return {
|
|
762
|
+
id: `poc-${finding.id}`,
|
|
763
|
+
findingId: finding.id,
|
|
764
|
+
prerequisites: [
|
|
765
|
+
"Access to API endpoint",
|
|
766
|
+
],
|
|
767
|
+
steps,
|
|
768
|
+
payload: "Automated script sending 1000+ requests per minute",
|
|
769
|
+
expectedResult: "API abuse, potential DoS, resource exhaustion, or brute force attacks",
|
|
770
|
+
safeTestInstructions: "Test on isolated/staging environment only. Use small request volumes (10-20 requests). Never DoS production systems.",
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function dataExposurePoC(finding) {
|
|
774
|
+
const steps = [
|
|
775
|
+
{
|
|
776
|
+
order: 1,
|
|
777
|
+
action: "identify-endpoint",
|
|
778
|
+
description: "Identify endpoint returning full objects",
|
|
779
|
+
command: `Review ${finding.file}:${finding.line}`,
|
|
780
|
+
expectedResult: "Endpoint identified",
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
order: 2,
|
|
784
|
+
action: "make-request",
|
|
785
|
+
description: "Make normal API request",
|
|
786
|
+
command: `curl -H "Authorization: Bearer TOKEN" https://target/api/users/me`,
|
|
787
|
+
expectedResult: "Response contains excessive data",
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
order: 3,
|
|
791
|
+
action: "analyze-response",
|
|
792
|
+
description: "Analyze response for sensitive fields",
|
|
793
|
+
command: "Inspect response JSON for internal fields, IDs, hashes, tokens",
|
|
794
|
+
expectedResult: "Fields like __v, _id, passwordHash, internalId, createdBy exposed",
|
|
795
|
+
},
|
|
796
|
+
];
|
|
797
|
+
return {
|
|
798
|
+
id: `poc-${finding.id}`,
|
|
799
|
+
findingId: finding.id,
|
|
800
|
+
prerequisites: [
|
|
801
|
+
"Access to API endpoint",
|
|
802
|
+
"Valid authentication",
|
|
803
|
+
],
|
|
804
|
+
steps,
|
|
805
|
+
payload: "GET /api/resource",
|
|
806
|
+
expectedResult: "Excessive data exposure revealing sensitive or internal fields",
|
|
807
|
+
safeTestInstructions: "Test with your own account. Document exposed fields. Never share or use exposed sensitive data.",
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
// ============================================================================
|
|
811
|
+
// Register Tactic
|
|
812
|
+
// ============================================================================
|
|
813
|
+
registerTactic(apiTactic);
|
|
814
|
+
export { apiTactic };
|
|
815
|
+
//# sourceMappingURL=api.js.map
|