@datahogo/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +17 -0
  3. package/dist/engines/config.d.ts +3 -0
  4. package/dist/engines/config.d.ts.map +1 -0
  5. package/dist/engines/config.js +433 -0
  6. package/dist/engines/config.js.map +1 -0
  7. package/dist/engines/dast.d.ts +3 -0
  8. package/dist/engines/dast.d.ts.map +1 -0
  9. package/dist/engines/dast.js +167 -0
  10. package/dist/engines/dast.js.map +1 -0
  11. package/dist/engines/db-rules.d.ts +3 -0
  12. package/dist/engines/db-rules.d.ts.map +1 -0
  13. package/dist/engines/db-rules.js +166 -0
  14. package/dist/engines/db-rules.js.map +1 -0
  15. package/dist/engines/dependencies.d.ts +3 -0
  16. package/dist/engines/dependencies.d.ts.map +1 -0
  17. package/dist/engines/dependencies.js +167 -0
  18. package/dist/engines/dependencies.js.map +1 -0
  19. package/dist/engines/github-actions.d.ts +3 -0
  20. package/dist/engines/github-actions.d.ts.map +1 -0
  21. package/dist/engines/github-actions.js +215 -0
  22. package/dist/engines/github-actions.js.map +1 -0
  23. package/dist/engines/gitleaks.d.ts +3 -0
  24. package/dist/engines/gitleaks.d.ts.map +1 -0
  25. package/dist/engines/gitleaks.js +107 -0
  26. package/dist/engines/gitleaks.js.map +1 -0
  27. package/dist/engines/npm-audit.d.ts +3 -0
  28. package/dist/engines/npm-audit.d.ts.map +1 -0
  29. package/dist/engines/npm-audit.js +113 -0
  30. package/dist/engines/npm-audit.js.map +1 -0
  31. package/dist/engines/patterns.d.ts +3 -0
  32. package/dist/engines/patterns.d.ts.map +1 -0
  33. package/dist/engines/patterns.js +1330 -0
  34. package/dist/engines/patterns.js.map +1 -0
  35. package/dist/engines/secrets.d.ts +4 -0
  36. package/dist/engines/secrets.d.ts.map +1 -0
  37. package/dist/engines/secrets.js +260 -0
  38. package/dist/engines/secrets.js.map +1 -0
  39. package/dist/engines/semgrep.d.ts +3 -0
  40. package/dist/engines/semgrep.d.ts.map +1 -0
  41. package/dist/engines/semgrep.js +173 -0
  42. package/dist/engines/semgrep.js.map +1 -0
  43. package/dist/engines/types.d.ts +30 -0
  44. package/dist/engines/types.d.ts.map +1 -0
  45. package/dist/engines/types.js +3 -0
  46. package/dist/engines/types.js.map +1 -0
  47. package/dist/engines/url-scanner.d.ts +3 -0
  48. package/dist/engines/url-scanner.d.ts.map +1 -0
  49. package/dist/engines/url-scanner.js +469 -0
  50. package/dist/engines/url-scanner.js.map +1 -0
  51. package/dist/index.d.ts +8 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +8 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/orchestrator.d.ts +32 -0
  56. package/dist/orchestrator.d.ts.map +1 -0
  57. package/dist/orchestrator.js +237 -0
  58. package/dist/orchestrator.js.map +1 -0
  59. package/dist/rules/.gitkeep +0 -0
  60. package/dist/rules/ai-llm-security.yaml +199 -0
  61. package/dist/rules/api-security.yaml +489 -0
  62. package/dist/rules/auth-patterns.yaml +406 -0
  63. package/dist/rules/crypto-patterns.yaml +227 -0
  64. package/dist/rules/database-cloud-security.yaml +169 -0
  65. package/dist/rules/general-security.yaml +588 -0
  66. package/dist/rules/injection-patterns.yaml +318 -0
  67. package/dist/rules/nextjs-security.yaml +532 -0
  68. package/dist/rules/node-security.yaml +380 -0
  69. package/dist/rules/rules/.gitkeep +0 -0
  70. package/dist/rules/rules/ai-llm-security.yaml +199 -0
  71. package/dist/rules/rules/api-security.yaml +489 -0
  72. package/dist/rules/rules/auth-patterns.yaml +406 -0
  73. package/dist/rules/rules/crypto-patterns.yaml +227 -0
  74. package/dist/rules/rules/database-cloud-security.yaml +169 -0
  75. package/dist/rules/rules/general-security.yaml +588 -0
  76. package/dist/rules/rules/injection-patterns.yaml +318 -0
  77. package/dist/rules/rules/nextjs-security.yaml +532 -0
  78. package/dist/rules/rules/node-security.yaml +380 -0
  79. package/dist/rules/rules/supabase-security.yaml +387 -0
  80. package/dist/rules/supabase-security.yaml +387 -0
  81. package/dist/scanning/agents/csharp-agent.d.ts +12 -0
  82. package/dist/scanning/agents/csharp-agent.d.ts.map +1 -0
  83. package/dist/scanning/agents/csharp-agent.js +592 -0
  84. package/dist/scanning/agents/csharp-agent.js.map +1 -0
  85. package/dist/scanning/agents/go-agent.d.ts +14 -0
  86. package/dist/scanning/agents/go-agent.d.ts.map +1 -0
  87. package/dist/scanning/agents/go-agent.js +641 -0
  88. package/dist/scanning/agents/go-agent.js.map +1 -0
  89. package/dist/scanning/agents/java-agent.d.ts +8 -0
  90. package/dist/scanning/agents/java-agent.d.ts.map +1 -0
  91. package/dist/scanning/agents/java-agent.js +807 -0
  92. package/dist/scanning/agents/java-agent.js.map +1 -0
  93. package/dist/scanning/agents/javascript-agent.d.ts +8 -0
  94. package/dist/scanning/agents/javascript-agent.d.ts.map +1 -0
  95. package/dist/scanning/agents/javascript-agent.js +77 -0
  96. package/dist/scanning/agents/javascript-agent.js.map +1 -0
  97. package/dist/scanning/agents/mobile-agent.d.ts +8 -0
  98. package/dist/scanning/agents/mobile-agent.d.ts.map +1 -0
  99. package/dist/scanning/agents/mobile-agent.js +916 -0
  100. package/dist/scanning/agents/mobile-agent.js.map +1 -0
  101. package/dist/scanning/agents/php-agent.d.ts +8 -0
  102. package/dist/scanning/agents/php-agent.d.ts.map +1 -0
  103. package/dist/scanning/agents/php-agent.js +679 -0
  104. package/dist/scanning/agents/php-agent.js.map +1 -0
  105. package/dist/scanning/agents/python-agent.d.ts +8 -0
  106. package/dist/scanning/agents/python-agent.d.ts.map +1 -0
  107. package/dist/scanning/agents/python-agent.js +701 -0
  108. package/dist/scanning/agents/python-agent.js.map +1 -0
  109. package/dist/scanning/agents/supabase-agent.d.ts +8 -0
  110. package/dist/scanning/agents/supabase-agent.d.ts.map +1 -0
  111. package/dist/scanning/agents/supabase-agent.js +484 -0
  112. package/dist/scanning/agents/supabase-agent.js.map +1 -0
  113. package/dist/scanning/orchestrator.d.ts +29 -0
  114. package/dist/scanning/orchestrator.d.ts.map +1 -0
  115. package/dist/scanning/orchestrator.js +186 -0
  116. package/dist/scanning/orchestrator.js.map +1 -0
  117. package/dist/scanning/tech-detector.d.ts +12 -0
  118. package/dist/scanning/tech-detector.d.ts.map +1 -0
  119. package/dist/scanning/tech-detector.js +252 -0
  120. package/dist/scanning/tech-detector.js.map +1 -0
  121. package/dist/scanning/types.d.ts +87 -0
  122. package/dist/scanning/types.d.ts.map +1 -0
  123. package/dist/scanning/types.js +5 -0
  124. package/dist/scanning/types.js.map +1 -0
  125. package/dist/utils/child-process.d.ts +2 -0
  126. package/dist/utils/child-process.d.ts.map +1 -0
  127. package/dist/utils/child-process.js +4 -0
  128. package/dist/utils/child-process.js.map +1 -0
  129. package/dist/utils/context-classifier.d.ts +11 -0
  130. package/dist/utils/context-classifier.d.ts.map +1 -0
  131. package/dist/utils/context-classifier.js +79 -0
  132. package/dist/utils/context-classifier.js.map +1 -0
  133. package/dist/utils/exec.d.ts +18 -0
  134. package/dist/utils/exec.d.ts.map +1 -0
  135. package/dist/utils/exec.js +51 -0
  136. package/dist/utils/exec.js.map +1 -0
  137. package/dist/utils/file-filter.d.ts +4 -0
  138. package/dist/utils/file-filter.d.ts.map +1 -0
  139. package/dist/utils/file-filter.js +68 -0
  140. package/dist/utils/file-filter.js.map +1 -0
  141. package/dist/utils/fs.d.ts +2 -0
  142. package/dist/utils/fs.d.ts.map +1 -0
  143. package/dist/utils/fs.js +5 -0
  144. package/dist/utils/fs.js.map +1 -0
  145. package/dist/utils/logger.d.ts +12 -0
  146. package/dist/utils/logger.d.ts.map +1 -0
  147. package/dist/utils/logger.js +27 -0
  148. package/dist/utils/logger.js.map +1 -0
  149. package/dist/utils/post-processor.d.ts +10 -0
  150. package/dist/utils/post-processor.d.ts.map +1 -0
  151. package/dist/utils/post-processor.js +183 -0
  152. package/dist/utils/post-processor.js.map +1 -0
  153. package/package.json +44 -0
@@ -0,0 +1,701 @@
1
+ // Python ecosystem security scanner agent.
2
+ // Detects Python projects (Django, FastAPI, Flask) and scans for
3
+ // framework-specific misconfigurations, SQL injection, unsafe pickle,
4
+ // committed .env files, and known vulnerable dependencies.
5
+ // Known vulnerable Python packages with CVE references.
6
+ // This is a curated subset — in production this would query OSV or PyPI advisory DB.
7
+ const KNOWN_VULNERABLE_PACKAGES = {
8
+ django: [
9
+ { maxSafe: "4.2.11", cve: "CVE-2024-27351", severity: "HIGH", description: "Potential ReDoS in django.utils.text.Truncator.words()" },
10
+ { maxSafe: "3.2.24", cve: "CVE-2024-24680", severity: "HIGH", description: "Denial-of-service in intcomma template filter" },
11
+ { maxSafe: "4.1.13", cve: "CVE-2023-46695", severity: "HIGH", description: "Potential DoS via large file uploads" },
12
+ ],
13
+ flask: [
14
+ { maxSafe: "2.3.2", cve: "CVE-2023-30861", severity: "HIGH", description: "Session cookie set without Secure flag on HTTP" },
15
+ ],
16
+ requests: [
17
+ { maxSafe: "2.31.0", cve: "CVE-2023-32681", severity: "MEDIUM", description: "Unintended leak of Proxy-Authorization header" },
18
+ ],
19
+ urllib3: [
20
+ { maxSafe: "2.0.6", cve: "CVE-2023-43804", severity: "HIGH", description: "Cookie leak via HTTP redirect to different host" },
21
+ { maxSafe: "1.26.17", cve: "CVE-2023-45803", severity: "MEDIUM", description: "Request body not stripped after redirect" },
22
+ ],
23
+ werkzeug: [
24
+ { maxSafe: "3.0.1", cve: "CVE-2023-46136", severity: "HIGH", description: "DoS via multipart form data parsing" },
25
+ ],
26
+ cryptography: [
27
+ { maxSafe: "41.0.6", cve: "CVE-2023-49083", severity: "HIGH", description: "NULL pointer dereference on PKCS7 certificates" },
28
+ ],
29
+ jinja2: [
30
+ { maxSafe: "3.1.3", cve: "CVE-2024-22195", severity: "MEDIUM", description: "XSS via xmlattr filter" },
31
+ ],
32
+ pillow: [
33
+ { maxSafe: "10.2.0", cve: "CVE-2023-50447", severity: "CRITICAL", description: "Arbitrary code execution via crafted PIL image" },
34
+ ],
35
+ pyyaml: [
36
+ { maxSafe: "6.0.1", cve: "CVE-2020-14343", severity: "CRITICAL", description: "Arbitrary code execution via yaml.load() without SafeLoader" },
37
+ ],
38
+ sqlparse: [
39
+ { maxSafe: "0.5.0", cve: "CVE-2024-4340", severity: "HIGH", description: "ReDoS via crafted SQL input" },
40
+ ],
41
+ };
42
+ // Auth decorator names recognized as protecting a route.
43
+ const FLASK_AUTH_DECORATORS = [
44
+ "login_required",
45
+ "auth_required",
46
+ "jwt_required",
47
+ "requires_auth",
48
+ "permission_required",
49
+ "protected",
50
+ "token_required",
51
+ ];
52
+ // FastAPI dependency names recognized as auth guards.
53
+ const FASTAPI_AUTH_DEPS = [
54
+ "get_current_user",
55
+ "verify_token",
56
+ "auth",
57
+ "Security(",
58
+ ];
59
+ // URL path segments that are conventionally public — skip auth checks for these.
60
+ const PUBLIC_PATH_SEGMENTS = [
61
+ "/health",
62
+ "/docs",
63
+ "/openapi",
64
+ "/redoc",
65
+ "/public",
66
+ "/webhook",
67
+ "/callback",
68
+ "/oauth",
69
+ "/status",
70
+ "/metrics",
71
+ "/openapi.json",
72
+ "/favicon",
73
+ "/static",
74
+ "/login",
75
+ "/register",
76
+ ];
77
+ export class PythonScanAgent {
78
+ async detect(files) {
79
+ for (const filePath of files.keys()) {
80
+ if (filePath === "requirements.txt" ||
81
+ filePath === "Pipfile" ||
82
+ filePath === "pyproject.toml" ||
83
+ filePath === "setup.py" ||
84
+ filePath.endsWith("requirements.txt")) {
85
+ return true;
86
+ }
87
+ }
88
+ return false;
89
+ }
90
+ async scan(files) {
91
+ const results = [];
92
+ const framework = detectFramework(files);
93
+ // Detect project-level middleware once, before per-file checks.
94
+ const projectContext = detectProjectMiddleware(files, framework);
95
+ // 1. Dependency vulnerability check
96
+ results.push(...checkVulnerableDependencies(files));
97
+ // 2. Django-specific checks
98
+ if (framework === "django") {
99
+ results.push(...checkDjangoMisconfig(files));
100
+ }
101
+ // 3. FastAPI / Flask specific checks
102
+ if (framework === "fastapi" || framework === "flask") {
103
+ results.push(...checkApiFrameworkIssues(files, framework, projectContext));
104
+ }
105
+ // 4. SQL injection via cursor.execute() / .raw() with f-strings or .format()
106
+ results.push(...checkSqlInjection(files));
107
+ // 5. Committed .env files with secrets
108
+ results.push(...checkCommittedEnvFiles(files));
109
+ // 6. Unsafe pickle.loads usage
110
+ results.push(...checkUnsafePickle(files));
111
+ // 7. Insecure randomness in security contexts
112
+ results.push(...checkInsecureRandom(files));
113
+ // 8. yaml.load() without SafeLoader
114
+ results.push(...checkUnsafeYamlLoad(files));
115
+ return results;
116
+ }
117
+ getMetadata() {
118
+ return {
119
+ name: "python-agent",
120
+ version: "1.0.0",
121
+ technologies: ["python", "django", "flask", "fastapi"],
122
+ };
123
+ }
124
+ getChecks() {
125
+ return [
126
+ {
127
+ id: "python:vulnerable-dependency",
128
+ name: "Vulnerable Python dependency (CVE in requirements.txt)",
129
+ severity: "HIGH",
130
+ },
131
+ {
132
+ id: "python:django-debug-enabled",
133
+ name: "Django DEBUG mode enabled",
134
+ severity: "HIGH",
135
+ },
136
+ {
137
+ id: "python:django-secret-key-hardcoded",
138
+ name: "Django SECRET_KEY hardcoded in source",
139
+ severity: "CRITICAL",
140
+ },
141
+ {
142
+ id: "python:django-allowed-hosts-wildcard",
143
+ name: "Django ALLOWED_HOSTS accepts all domains",
144
+ severity: "HIGH",
145
+ },
146
+ {
147
+ id: "python:django-cors-wildcard",
148
+ name: "Django CORS allows all origins",
149
+ severity: "MEDIUM",
150
+ },
151
+ {
152
+ id: "python:fastapi-missing-auth",
153
+ name: "FastAPI endpoint without auth dependency",
154
+ severity: "MEDIUM",
155
+ },
156
+ {
157
+ id: "python:flask-missing-auth",
158
+ name: "Flask route without authentication",
159
+ severity: "MEDIUM",
160
+ },
161
+ {
162
+ id: "python:fastapi-cors-wildcard",
163
+ name: "FastAPI CORS allows all origins",
164
+ severity: "MEDIUM",
165
+ },
166
+ {
167
+ id: "python:flask-cors-wildcard",
168
+ name: "Flask CORS allows all origins",
169
+ severity: "MEDIUM",
170
+ },
171
+ {
172
+ id: "python:sql-injection-execute-interpolation",
173
+ name: "SQL injection via cursor.execute() with string interpolation",
174
+ severity: "CRITICAL",
175
+ },
176
+ {
177
+ id: "python:sql-injection-raw-interpolation",
178
+ name: "SQL injection via Django .raw() with string interpolation",
179
+ severity: "CRITICAL",
180
+ },
181
+ {
182
+ id: "python:sql-injection-concatenation",
183
+ name: "SQL injection via string concatenation in query",
184
+ severity: "CRITICAL",
185
+ },
186
+ {
187
+ id: "python:committed-env-secrets",
188
+ name: "Secret committed in .env file",
189
+ severity: "CRITICAL",
190
+ },
191
+ {
192
+ id: "python:unsafe-pickle",
193
+ name: "Unsafe pickle deserialization",
194
+ severity: "CRITICAL",
195
+ },
196
+ {
197
+ id: "python:insecure-random",
198
+ name: "Insecure random for security-sensitive value",
199
+ severity: "HIGH",
200
+ },
201
+ {
202
+ id: "python:unsafe-yaml-load",
203
+ name: "yaml.load() without SafeLoader",
204
+ severity: "HIGH",
205
+ },
206
+ ];
207
+ }
208
+ }
209
+ /** Detect which Python web framework (if any) the project uses. */
210
+ function detectFramework(files) {
211
+ for (const [filePath, content] of files) {
212
+ if (isManifestFile(filePath)) {
213
+ const lower = content.toLowerCase();
214
+ if (lower.includes("django"))
215
+ return "django";
216
+ if (lower.includes("fastapi"))
217
+ return "fastapi";
218
+ if (lower.includes("flask"))
219
+ return "flask";
220
+ }
221
+ }
222
+ return "none";
223
+ }
224
+ /**
225
+ * Scan all Python files for project-level middleware that provides
226
+ * global authentication or rate limiting. Used to reduce false positives
227
+ * on per-route missing-auth findings.
228
+ *
229
+ * Django: presence of AuthenticationMiddleware, LoginRequiredMiddleware,
230
+ * RateLimitMiddleware, or CsrfViewMiddleware in MIDDLEWARE array.
231
+ * Flask: @app.before_request with a recognizable auth check.
232
+ */
233
+ function detectProjectMiddleware(files, framework) {
234
+ let hasGlobalAuth = false;
235
+ let hasGlobalRateLimit = false;
236
+ for (const [filePath, content] of files) {
237
+ if (!isPythonFile(filePath))
238
+ continue;
239
+ if (framework === "django") {
240
+ // Check for Django MIDDLEWARE list entries
241
+ if (/AuthenticationMiddleware|LoginRequiredMiddleware/.test(content)) {
242
+ hasGlobalAuth = true;
243
+ }
244
+ if (/RateLimitMiddleware/.test(content)) {
245
+ hasGlobalRateLimit = true;
246
+ }
247
+ }
248
+ if (framework === "flask") {
249
+ // Check for @app.before_request with auth logic
250
+ // The decorator must be followed (within 10 lines) by an auth check
251
+ const lines = content.split("\n");
252
+ for (let i = 0; i < lines.length; i++) {
253
+ if (/@app\.before_request|@app\.before_each_request/.test(lines[i])) {
254
+ const block = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
255
+ if (/login_required|current_user|authenticate|verify_token|jwt_required|auth/.test(block)) {
256
+ hasGlobalAuth = true;
257
+ }
258
+ if (/rate.?limit|throttle/.test(block)) {
259
+ hasGlobalRateLimit = true;
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ return { hasGlobalAuth, hasGlobalRateLimit };
266
+ }
267
+ function isManifestFile(filePath) {
268
+ return (filePath === "requirements.txt" ||
269
+ filePath === "Pipfile" ||
270
+ filePath === "pyproject.toml" ||
271
+ filePath === "setup.py" ||
272
+ filePath.endsWith("/requirements.txt"));
273
+ }
274
+ function isPythonFile(filePath) {
275
+ return filePath.endsWith(".py");
276
+ }
277
+ /** Returns true when a URL path string contains a known public segment. */
278
+ function isPublicPath(routeLine) {
279
+ return PUBLIC_PATH_SEGMENTS.some((segment) => routeLine.includes(segment));
280
+ }
281
+ // --- Vulnerability checks ---
282
+ function checkVulnerableDependencies(files) {
283
+ const results = [];
284
+ for (const [filePath, content] of files) {
285
+ if (!filePath.endsWith("requirements.txt"))
286
+ continue;
287
+ const lines = content.split("\n");
288
+ for (let i = 0; i < lines.length; i++) {
289
+ const line = lines[i].trim();
290
+ if (!line || line.startsWith("#") || line.startsWith("-"))
291
+ continue;
292
+ const parsed = parseRequirementLine(line);
293
+ if (!parsed)
294
+ continue;
295
+ const advisories = KNOWN_VULNERABLE_PACKAGES[parsed.name.toLowerCase()];
296
+ if (!advisories)
297
+ continue;
298
+ for (const advisory of advisories) {
299
+ if (parsed.version && isVersionLessThanOrEqual(parsed.version, advisory.maxSafe)) {
300
+ results.push({
301
+ checkId: "python:vulnerable-dependency",
302
+ title: `Vulnerable dependency: ${parsed.name} (${advisory.cve})`,
303
+ severity: advisory.severity,
304
+ confidence: "high",
305
+ file: filePath,
306
+ line: i + 1,
307
+ description: `${advisory.description}. Installed: ${parsed.version}, fix available above ${advisory.maxSafe}.`,
308
+ fix: `Update ${parsed.name} to a version newer than ${advisory.maxSafe}: pip install --upgrade ${parsed.name}`,
309
+ cwe: "CWE-1395",
310
+ });
311
+ }
312
+ }
313
+ }
314
+ }
315
+ return results;
316
+ }
317
+ function checkDjangoMisconfig(files) {
318
+ const results = [];
319
+ for (const [filePath, content] of files) {
320
+ if (!isPythonFile(filePath))
321
+ continue;
322
+ const lines = content.split("\n");
323
+ for (let i = 0; i < lines.length; i++) {
324
+ const line = lines[i];
325
+ // DEBUG = True — direct assignment, high confidence
326
+ if (/^\s*DEBUG\s*=\s*True\b/.test(line)) {
327
+ results.push({
328
+ checkId: "python:django-debug-enabled",
329
+ title: "Django DEBUG mode enabled",
330
+ severity: "HIGH",
331
+ confidence: "high",
332
+ file: filePath,
333
+ line: i + 1,
334
+ description: "DEBUG=True exposes detailed error pages with stack traces, local variables, and settings to anyone who triggers an error. Must be False in production.",
335
+ fix: "Set DEBUG = False and use environment variables: DEBUG = os.environ.get('DEBUG', 'False') == 'True'",
336
+ cwe: "CWE-215",
337
+ });
338
+ }
339
+ // SECRET_KEY hardcoded (visible in source) — high confidence
340
+ if (/^\s*SECRET_KEY\s*=\s*['"][^'"]{8,}['"]/.test(line)) {
341
+ results.push({
342
+ checkId: "python:django-secret-key-hardcoded",
343
+ title: "Django SECRET_KEY hardcoded in source",
344
+ severity: "CRITICAL",
345
+ confidence: "high",
346
+ file: filePath,
347
+ line: i + 1,
348
+ description: "The Django SECRET_KEY is hardcoded in source code. This key is used for cryptographic signing (sessions, CSRF tokens, password reset tokens). If leaked, attackers can forge any of these.",
349
+ fix: "Move SECRET_KEY to an environment variable: SECRET_KEY = os.environ['SECRET_KEY']",
350
+ cwe: "CWE-798",
351
+ });
352
+ }
353
+ // ALLOWED_HOSTS = ['*'] — only when * is the sole element in the list.
354
+ // Previous regex matched any array containing * anywhere (e.g. ['foo', '*']).
355
+ // Corrected: require * to be the only element, optionally quoted.
356
+ if (/^\s*ALLOWED_HOSTS\s*=\s*\[\s*['"]?\*['"]?\s*\]/.test(line)) {
357
+ results.push({
358
+ checkId: "python:django-allowed-hosts-wildcard",
359
+ title: "Django ALLOWED_HOSTS accepts all domains",
360
+ severity: "HIGH",
361
+ confidence: "high",
362
+ file: filePath,
363
+ line: i + 1,
364
+ description: "ALLOWED_HOSTS=['*'] disables Django's host header validation, enabling HTTP Host header attacks (cache poisoning, password reset poisoning).",
365
+ fix: "Set ALLOWED_HOSTS to your actual domain(s): ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']",
366
+ cwe: "CWE-644",
367
+ });
368
+ }
369
+ // CORS_ALLOW_ALL_ORIGINS = True — high confidence
370
+ if (/^\s*CORS_ALLOW_ALL_ORIGINS\s*=\s*True\b/.test(line)) {
371
+ results.push({
372
+ checkId: "python:django-cors-wildcard",
373
+ title: "Django CORS allows all origins",
374
+ severity: "MEDIUM",
375
+ confidence: "high",
376
+ file: filePath,
377
+ line: i + 1,
378
+ description: "CORS_ALLOW_ALL_ORIGINS=True allows any website to make authenticated requests to your API, potentially leaking user data.",
379
+ fix: "Set CORS_ALLOWED_ORIGINS to specific domains: CORS_ALLOWED_ORIGINS = ['https://yourdomain.com']",
380
+ cwe: "CWE-942",
381
+ });
382
+ }
383
+ }
384
+ }
385
+ return results;
386
+ }
387
+ function checkApiFrameworkIssues(files, framework, projectContext) {
388
+ const results = [];
389
+ for (const [filePath, content] of files) {
390
+ if (!isPythonFile(filePath))
391
+ continue;
392
+ const lines = content.split("\n");
393
+ for (let i = 0; i < lines.length; i++) {
394
+ const line = lines[i];
395
+ if (framework === "fastapi") {
396
+ if (/^\s*@app\.(?:get|post|put|patch|delete)\s*\(/.test(line)) {
397
+ // Skip known-public paths
398
+ if (isPublicPath(line))
399
+ continue;
400
+ // Look back up to 5 lines for auth decorators/dependencies
401
+ const lookbackStart = Math.max(0, i - 5);
402
+ const lookbackChunk = lines.slice(lookbackStart, i).join("\n");
403
+ // Look ahead until next decorator or blank line (max 4 lines) for Depends()
404
+ const aheadLines = [];
405
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
406
+ if (/^\s*@/.test(lines[j]) || lines[j].trim() === "")
407
+ break;
408
+ aheadLines.push(lines[j]);
409
+ }
410
+ const aheadChunk = aheadLines.join("\n");
411
+ const hasAuthLookback = FASTAPI_AUTH_DEPS.some((dep) => lookbackChunk.includes(dep)) ||
412
+ /Depends\s*\(/.test(lookbackChunk);
413
+ const hasAuthAhead = /Depends\s*\(/.test(aheadChunk) ||
414
+ FASTAPI_AUTH_DEPS.some((dep) => aheadChunk.includes(dep));
415
+ if (!hasAuthLookback && !hasAuthAhead) {
416
+ // If a global auth middleware was found, demote to low confidence
417
+ const confidence = projectContext.hasGlobalAuth ? "low" : "medium";
418
+ results.push({
419
+ checkId: "python:fastapi-missing-auth",
420
+ title: "FastAPI endpoint without auth dependency",
421
+ severity: "MEDIUM",
422
+ confidence,
423
+ file: filePath,
424
+ line: i + 1,
425
+ description: "This FastAPI endpoint does not use Depends() for authentication. Without an auth dependency, the endpoint is accessible to anyone.",
426
+ fix: "Add an auth dependency: @app.get('/path')\nasync def handler(user: User = Depends(get_current_user)):",
427
+ cwe: "CWE-306",
428
+ });
429
+ }
430
+ }
431
+ }
432
+ if (framework === "flask") {
433
+ if (/^\s*@app\.route\s*\(/.test(line)) {
434
+ // Skip known-public paths
435
+ if (isPublicPath(line))
436
+ continue;
437
+ // Look back up to 5 lines for auth decorators
438
+ const lookbackStart = Math.max(0, i - 5);
439
+ const lookbackChunk = lines.slice(lookbackStart, i).join("\n");
440
+ // Look ahead (up to 6 lines) for auth checks inside the handler
441
+ const aheadChunk = lines.slice(i, Math.min(i + 6, lines.length)).join("\n");
442
+ const authPattern = new RegExp(FLASK_AUTH_DECORATORS.join("|"));
443
+ const handlerAuthPattern = /login_required|auth_required|jwt_required|token_required|current_user/;
444
+ const hasAuth = authPattern.test(lookbackChunk) ||
445
+ handlerAuthPattern.test(aheadChunk);
446
+ if (!hasAuth) {
447
+ const confidence = projectContext.hasGlobalAuth ? "low" : "medium";
448
+ results.push({
449
+ checkId: "python:flask-missing-auth",
450
+ title: "Flask route without authentication",
451
+ severity: "MEDIUM",
452
+ confidence,
453
+ file: filePath,
454
+ line: i + 1,
455
+ description: "This Flask route does not have an authentication decorator (@login_required) or auth check. It may be accessible without authentication.",
456
+ fix: "Add @login_required decorator or check current_user in the handler.",
457
+ cwe: "CWE-306",
458
+ });
459
+ }
460
+ }
461
+ }
462
+ // Open CORS — low confidence because CORS(app) alone may be a default
463
+ // that the developer intends to configure further, or only exposes
464
+ // non-sensitive public endpoints.
465
+ if (/CORS\s*\(\s*app\s*\)/.test(line) || /allow_origins\s*=\s*\[\s*["']\*["']\s*\]/.test(line)) {
466
+ results.push({
467
+ checkId: framework === "fastapi" ? "python:fastapi-cors-wildcard" : "python:flask-cors-wildcard",
468
+ title: `${framework === "fastapi" ? "FastAPI" : "Flask"} CORS allows all origins`,
469
+ severity: "MEDIUM",
470
+ confidence: "low",
471
+ file: filePath,
472
+ line: i + 1,
473
+ description: "CORS is configured to allow all origins. This lets any website make authenticated cross-origin requests to your API.",
474
+ fix: "Restrict origins: allow_origins=['https://yourdomain.com']",
475
+ cwe: "CWE-942",
476
+ });
477
+ }
478
+ }
479
+ }
480
+ return results;
481
+ }
482
+ function checkSqlInjection(files) {
483
+ const results = [];
484
+ for (const [filePath, content] of files) {
485
+ if (!isPythonFile(filePath))
486
+ continue;
487
+ const lines = content.split("\n");
488
+ for (let i = 0; i < lines.length; i++) {
489
+ const line = lines[i];
490
+ // cursor.execute() with f-string or .format()
491
+ if (/cursor\.execute\s*\(\s*f['"]/.test(line) || /cursor\.execute\s*\([^)]*\.format\s*\(/.test(line)) {
492
+ // Lower confidence if the same line or next 2 lines contain a %s placeholder
493
+ // or a parameter tuple — this may indicate the f-string is only for the table/column
494
+ // name and actual user values are parameterized (still bad practice, but less clear).
495
+ const contextAhead = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
496
+ const looksParameterized = /%s/.test(contextAhead) || /,\s*\(/.test(contextAhead);
497
+ const confidence = looksParameterized ? "low" : "medium";
498
+ results.push({
499
+ checkId: "python:sql-injection-execute-interpolation",
500
+ title: "SQL injection via cursor.execute() with string interpolation",
501
+ severity: "CRITICAL",
502
+ confidence,
503
+ file: filePath,
504
+ line: i + 1,
505
+ description: "Using f-strings or .format() in cursor.execute() allows SQL injection. User-controlled input can break out of the query and execute arbitrary SQL.",
506
+ fix: "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))",
507
+ cwe: "CWE-89",
508
+ });
509
+ }
510
+ // .raw() with f-string or .format() (Django ORM)
511
+ if (/\.raw\s*\(\s*f['"]/.test(line) || /\.raw\s*\([^)]*\.format\s*\(/.test(line)) {
512
+ const contextAhead = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
513
+ const looksParameterized = /%s/.test(contextAhead) || /,\s*\[/.test(contextAhead);
514
+ const confidence = looksParameterized ? "low" : "medium";
515
+ results.push({
516
+ checkId: "python:sql-injection-raw-interpolation",
517
+ title: "SQL injection via Django .raw() with string interpolation",
518
+ severity: "CRITICAL",
519
+ confidence,
520
+ file: filePath,
521
+ line: i + 1,
522
+ description: "Using f-strings or .format() in Model.objects.raw() allows SQL injection. Django's raw() accepts parameterized queries natively.",
523
+ fix: "Use parameterized raw queries: Model.objects.raw('SELECT * FROM app_model WHERE id = %s', [user_id])",
524
+ cwe: "CWE-89",
525
+ });
526
+ }
527
+ // execute() or .raw() with string concatenation (+ operator in the call)
528
+ if (/(?:cursor\.execute|\.raw)\s*\(.*["'].*["']\s*\+/.test(line) || /(?:cursor\.execute|\.raw)\s*\(\s*\w+\s*\+/.test(line)) {
529
+ results.push({
530
+ checkId: "python:sql-injection-concatenation",
531
+ title: "SQL injection via string concatenation in query",
532
+ severity: "CRITICAL",
533
+ confidence: "high",
534
+ file: filePath,
535
+ line: i + 1,
536
+ description: "Concatenating strings into SQL queries allows injection attacks. Never build SQL from user input with + operator.",
537
+ fix: "Use parameterized queries instead of string concatenation.",
538
+ cwe: "CWE-89",
539
+ });
540
+ }
541
+ }
542
+ }
543
+ return results;
544
+ }
545
+ function checkCommittedEnvFiles(files) {
546
+ const results = [];
547
+ // Template/example files are not real secrets — skip them entirely.
548
+ const TEMPLATE_SUFFIXES = [".example", ".template", ".sample"];
549
+ for (const [filePath, content] of files) {
550
+ // Match .env files (not .env.example, .env.template, .env.sample)
551
+ if (!/(?:^|\/)\.(env|env\.local|env\.production|env\.staging)$/.test(filePath))
552
+ continue;
553
+ if (TEMPLATE_SUFFIXES.some((suffix) => filePath.endsWith(suffix)))
554
+ continue;
555
+ const lines = content.split("\n");
556
+ for (let i = 0; i < lines.length; i++) {
557
+ const line = lines[i].trim();
558
+ // Skip blank lines and comments
559
+ if (!line || line.startsWith("#"))
560
+ continue;
561
+ // Check for sensitive-looking keys with non-empty values
562
+ if (/(?:SECRET|PASSWORD|TOKEN|KEY|PRIVATE|CREDENTIAL|API_KEY)\s*=\s*.{4,}/i.test(line)) {
563
+ results.push({
564
+ checkId: "python:committed-env-secrets",
565
+ title: "Secret committed in .env file",
566
+ severity: "CRITICAL",
567
+ confidence: "high",
568
+ file: filePath,
569
+ line: i + 1,
570
+ description: "A .env file with secrets is committed to the repository. Anyone with repo access can read these credentials. Secrets may persist in git history even after deletion.",
571
+ fix: "Remove the .env file from git: git rm --cached .env && echo '.env' >> .gitignore. Rotate all exposed credentials immediately.",
572
+ cwe: "CWE-540",
573
+ });
574
+ // One finding per .env file is enough
575
+ break;
576
+ }
577
+ }
578
+ }
579
+ return results;
580
+ }
581
+ function checkUnsafePickle(files) {
582
+ const results = [];
583
+ for (const [filePath, content] of files) {
584
+ if (!isPythonFile(filePath))
585
+ continue;
586
+ const lines = content.split("\n");
587
+ for (let i = 0; i < lines.length; i++) {
588
+ const line = lines[i];
589
+ // pickle.loads / pickle.load with any data source.
590
+ // Confidence is medium because the data source determines actual risk:
591
+ // loading from a local, developer-controlled file is far less dangerous
592
+ // than loading from user input or a network source. Without data-flow
593
+ // analysis we cannot distinguish the two, so we flag all cases but
594
+ // acknowledge the ambiguity with medium confidence.
595
+ if (/pickle\.loads?\s*\(/.test(line)) {
596
+ results.push({
597
+ checkId: "python:unsafe-pickle",
598
+ title: "Unsafe pickle deserialization",
599
+ severity: "CRITICAL",
600
+ confidence: "medium",
601
+ file: filePath,
602
+ line: i + 1,
603
+ description: "pickle.loads() can execute arbitrary code during deserialization. If the data comes from an untrusted source (user input, network, uploaded files), an attacker can achieve remote code execution.",
604
+ fix: "Use safer alternatives: json.loads() for data, or hmac-sign pickled data. If pickle is required, use restrictedpickle or a custom Unpickler with find_class restrictions.",
605
+ cwe: "CWE-502",
606
+ });
607
+ }
608
+ }
609
+ }
610
+ return results;
611
+ }
612
+ function checkInsecureRandom(files) {
613
+ const results = [];
614
+ for (const [filePath, content] of files) {
615
+ if (!isPythonFile(filePath))
616
+ continue;
617
+ const lines = content.split("\n");
618
+ for (let i = 0; i < lines.length; i++) {
619
+ const line = lines[i];
620
+ // random.random/randint/choice used near security context
621
+ if (/random\.(?:random|randint|choice|randrange|getrandbits|sample)\s*\(/.test(line)) {
622
+ // Check surrounding lines for security context
623
+ const contextStart = Math.max(0, i - 3);
624
+ const contextEnd = Math.min(lines.length, i + 3);
625
+ const context = lines.slice(contextStart, contextEnd).join("\n").toLowerCase();
626
+ if (/token|secret|key|session|nonce|salt|otp|password|code|csrf|verify/.test(context)) {
627
+ results.push({
628
+ checkId: "python:insecure-random",
629
+ title: "Insecure random for security-sensitive value",
630
+ severity: "HIGH",
631
+ confidence: "medium",
632
+ file: filePath,
633
+ line: i + 1,
634
+ description: "The random module is not cryptographically secure. For tokens, passwords, OTPs, or session IDs, use secrets.token_hex(), secrets.token_urlsafe(), or os.urandom().",
635
+ fix: "import secrets; token = secrets.token_urlsafe(32)",
636
+ cwe: "CWE-330",
637
+ });
638
+ }
639
+ }
640
+ }
641
+ }
642
+ return results;
643
+ }
644
+ function checkUnsafeYamlLoad(files) {
645
+ const results = [];
646
+ for (const [filePath, content] of files) {
647
+ if (!isPythonFile(filePath))
648
+ continue;
649
+ const lines = content.split("\n");
650
+ for (let i = 0; i < lines.length; i++) {
651
+ const line = lines[i];
652
+ // yaml.load() without SafeLoader / safe_load
653
+ if (/yaml\.load\s*\(/.test(line) && !/SafeLoader|safe_load/.test(line)) {
654
+ // Check next 2 lines for Loader=SafeLoader
655
+ const ahead = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
656
+ if (!/SafeLoader/.test(ahead)) {
657
+ results.push({
658
+ checkId: "python:unsafe-yaml-load",
659
+ title: "yaml.load() without SafeLoader",
660
+ severity: "HIGH",
661
+ confidence: "high",
662
+ file: filePath,
663
+ line: i + 1,
664
+ description: "yaml.load() without SafeLoader can execute arbitrary Python code during parsing. An attacker who controls the YAML input achieves remote code execution.",
665
+ fix: "Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader)",
666
+ cwe: "CWE-502",
667
+ });
668
+ }
669
+ }
670
+ }
671
+ }
672
+ return results;
673
+ }
674
+ function parseRequirementLine(line) {
675
+ // Handle: package==1.2.3, package>=1.2.3, package~=1.2.3, package
676
+ const match = line.match(/^([a-zA-Z0-9_-]+(?:\[[a-zA-Z0-9_,-]+\])?)\s*(?:[=~><!=]+\s*([0-9][0-9a-zA-Z.*-]*))?/);
677
+ if (!match)
678
+ return null;
679
+ // Strip extras like package[extra]
680
+ const name = match[1].replace(/\[.*\]/, "");
681
+ const version = match[2] ?? null;
682
+ return { name, version };
683
+ }
684
+ /** Simple semver comparison: is versionA <= versionB? */
685
+ function isVersionLessThanOrEqual(versionA, versionB) {
686
+ const partsA = versionA.split(".").map(Number);
687
+ const partsB = versionB.split(".").map(Number);
688
+ const maxLen = Math.max(partsA.length, partsB.length);
689
+ for (let i = 0; i < maxLen; i++) {
690
+ const a = partsA[i] ?? 0;
691
+ const b = partsB[i] ?? 0;
692
+ if (isNaN(a) || isNaN(b))
693
+ return false;
694
+ if (a < b)
695
+ return true;
696
+ if (a > b)
697
+ return false;
698
+ }
699
+ return true; // equal
700
+ }
701
+ //# sourceMappingURL=python-agent.js.map