ship-safe 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/__tests__/agents.test.js +496 -0
- package/cli/agents/api-fuzzer.js +234 -224
- package/cli/agents/auth-bypass-agent.js +348 -326
- package/cli/agents/cicd-scanner.js +201 -200
- package/cli/agents/config-auditor.js +458 -413
- package/cli/agents/git-history-scanner.js +170 -167
- package/cli/agents/injection-tester.js +455 -401
- package/cli/agents/llm-redteam.js +251 -251
- package/cli/agents/mobile-scanner.js +225 -225
- package/cli/agents/orchestrator.js +220 -157
- package/cli/agents/scoring-engine.js +225 -207
- package/cli/bin/ship-safe.js +14 -0
- package/cli/commands/audit.js +849 -620
- package/cli/commands/doctor.js +149 -0
- package/cli/commands/remediate.js +7 -3
- package/cli/index.js +56 -53
- package/cli/providers/llm-provider.js +287 -288
- package/cli/utils/cache-manager.js +311 -258
- package/package.json +2 -2
|
@@ -1,413 +1,458 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ConfigAuditor Agent
|
|
3
|
-
* ====================
|
|
4
|
-
*
|
|
5
|
-
* Detects security misconfigurations in:
|
|
6
|
-
* Dockerfile, docker-compose, vercel.json, netlify.toml,
|
|
7
|
-
* next.config.js, Terraform, Kubernetes, nginx, firebase,
|
|
8
|
-
* and security headers.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import { BaseAgent, createFinding } from './base-agent.js';
|
|
13
|
-
|
|
14
|
-
// =============================================================================
|
|
15
|
-
// DOCKERFILE PATTERNS
|
|
16
|
-
// =============================================================================
|
|
17
|
-
|
|
18
|
-
const DOCKERFILE_PATTERNS = [
|
|
19
|
-
{
|
|
20
|
-
rule: 'DOCKER_RUN_AS_ROOT',
|
|
21
|
-
title: 'Docker: Running as Root',
|
|
22
|
-
regex: /^(?!.*USER\s+\w).*CMD|ENTRYPOINT/gm,
|
|
23
|
-
severity: 'high',
|
|
24
|
-
cwe: 'CWE-250',
|
|
25
|
-
owasp: 'A05:2021',
|
|
26
|
-
confidence: 'medium',
|
|
27
|
-
description: 'No USER instruction found. Container runs as root by default.',
|
|
28
|
-
fix: 'Add USER nonroot before CMD/ENTRYPOINT',
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
rule: 'DOCKER_LATEST_TAG',
|
|
32
|
-
title: 'Docker: Using :latest Tag',
|
|
33
|
-
regex: /FROM\s+\S+:latest/gi,
|
|
34
|
-
severity: 'medium',
|
|
35
|
-
cwe: 'CWE-1104',
|
|
36
|
-
description: ':latest tag is mutable and can change unexpectedly. Pin to a specific version.',
|
|
37
|
-
fix: 'Pin to specific version: FROM node:20-alpine instead of FROM node:latest',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
rule: 'DOCKER_ADD_REMOTE',
|
|
41
|
-
title: 'Docker: ADD with Remote URL',
|
|
42
|
-
regex: /ADD\s+https?:\/\//gi,
|
|
43
|
-
severity: 'high',
|
|
44
|
-
cwe: 'CWE-829',
|
|
45
|
-
description: 'ADD with URL downloads without checksum verification. Use COPY + curl with checksum.',
|
|
46
|
-
fix: 'Replace ADD URL with: RUN curl -fsSL url -o file && sha256sum -c <<< "hash file"',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
rule: 'DOCKER_SECRET_ENV',
|
|
50
|
-
title: 'Docker: Secret in ENV/ARG',
|
|
51
|
-
regex: /(?:ENV|ARG)\s+(?:.*(?:PASSWORD|SECRET|KEY|TOKEN|CREDENTIAL|API_KEY))\s*=/gi,
|
|
52
|
-
severity: 'critical',
|
|
53
|
-
cwe: 'CWE-798',
|
|
54
|
-
description: 'Secrets in ENV/ARG are baked into image layers. Use Docker secrets or runtime env.',
|
|
55
|
-
fix: 'Use --secret flag in docker build, or pass secrets at runtime via -e',
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
rule: 'DOCKER_EXPOSE_ALL',
|
|
59
|
-
title: 'Docker: Exposing Privileged Port',
|
|
60
|
-
regex: /EXPOSE\s+(?:22|23|3389|5432|3306|27017|6379|11211)\b/g,
|
|
61
|
-
severity: 'medium',
|
|
62
|
-
cwe: 'CWE-200',
|
|
63
|
-
description: 'Exposing database/admin ports in container. Only expose application ports.',
|
|
64
|
-
fix: 'Remove EXPOSE for database ports. Use Docker networking for internal communication.',
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
rule: 'DOCKER_PRIVILEGED',
|
|
68
|
-
title: 'Docker: Privileged Mode',
|
|
69
|
-
regex: /privileged\s*:\s*true/g,
|
|
70
|
-
severity: 'critical',
|
|
71
|
-
cwe: 'CWE-250',
|
|
72
|
-
description: 'Privileged containers have full host access. This enables container escape.',
|
|
73
|
-
fix: 'Remove privileged: true. Use specific capabilities if needed (cap_add).',
|
|
74
|
-
},
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
// =============================================================================
|
|
78
|
-
// CONFIG FILE PATTERNS
|
|
79
|
-
// =============================================================================
|
|
80
|
-
|
|
81
|
-
const CONFIG_PATTERNS = [
|
|
82
|
-
// ── Security Headers ───────────────────────────────────────────────────────
|
|
83
|
-
{
|
|
84
|
-
rule: 'MISSING_CSP',
|
|
85
|
-
title: 'Missing Content-Security-Policy',
|
|
86
|
-
regex: /headers\s*(?::|=)\s*\[/g,
|
|
87
|
-
severity: 'medium',
|
|
88
|
-
cwe: 'CWE-693',
|
|
89
|
-
owasp: 'A05:2021',
|
|
90
|
-
confidence: 'low',
|
|
91
|
-
description: 'No Content-Security-Policy header detected. CSP prevents XSS and data injection.',
|
|
92
|
-
fix: "Add Content-Security-Policy header: \"default-src 'self'; script-src 'self'\"",
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
rule: 'CORS_WILDCARD',
|
|
96
|
-
title: 'CORS Wildcard Origin',
|
|
97
|
-
regex: /(?:Access-Control-Allow-Origin|origin)\s*[:=]\s*['"]?\*['"]?/g,
|
|
98
|
-
severity: 'high',
|
|
99
|
-
cwe: 'CWE-942',
|
|
100
|
-
owasp: 'A05:2021',
|
|
101
|
-
description: 'CORS wildcard (*) allows any origin. Use specific trusted origins.',
|
|
102
|
-
fix: 'Replace * with specific origins: ["https://yourdomain.com"]',
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
rule: 'CORS_CREDENTIALS_WILDCARD',
|
|
106
|
-
title: 'CORS Credentials with Wildcard',
|
|
107
|
-
regex: /credentials\s*:\s*true.*origin\s*:\s*true|origin\s*:\s*true.*credentials\s*:\s*true/g,
|
|
108
|
-
severity: 'critical',
|
|
109
|
-
cwe: 'CWE-942',
|
|
110
|
-
owasp: 'A05:2021',
|
|
111
|
-
description: 'CORS with credentials: true and origin: true reflects any origin, enabling credential theft.',
|
|
112
|
-
fix: 'Use a specific origin allowlist when credentials: true',
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
// ── Next.js Config ─────────────────────────────────────────────────────────
|
|
116
|
-
{
|
|
117
|
-
rule: 'NEXTJS_POWERED_BY',
|
|
118
|
-
title: 'Next.js: X-Powered-By Header Enabled',
|
|
119
|
-
regex: /poweredByHeader\s*:\s*true/g,
|
|
120
|
-
severity: 'low',
|
|
121
|
-
cwe: 'CWE-200',
|
|
122
|
-
description: 'X-Powered-By header reveals technology stack. Disable for security through obscurity.',
|
|
123
|
-
fix: 'Set poweredByHeader: false in next.config.js',
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
rule: 'NEXTJS_WILDCARD_IMAGES',
|
|
127
|
-
title: 'Next.js: Wildcard Image Domain',
|
|
128
|
-
regex: /images\s*:\s*\{[^}]*(?:domains|remotePatterns)[^}]*\*\*/g,
|
|
129
|
-
severity: 'medium',
|
|
130
|
-
cwe: 'CWE-918',
|
|
131
|
-
description: 'Wildcard image domains can be abused for SSRF via Next.js image optimization.',
|
|
132
|
-
fix: 'Specify exact domains in images.remotePatterns',
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
// ── Firebase ───────────────────────────────────────────────────────────────
|
|
136
|
-
{
|
|
137
|
-
rule: 'FIREBASE_OPEN_RULES',
|
|
138
|
-
title: 'Firebase: Open Security Rules',
|
|
139
|
-
regex: /allow\s+read\s*,\s*write\s*:\s*if\s+true/g,
|
|
140
|
-
severity: 'critical',
|
|
141
|
-
cwe: 'CWE-284',
|
|
142
|
-
description: 'Firebase rules allow unauthenticated read/write. Any user can access all data.',
|
|
143
|
-
fix: 'Change to: allow read, write: if request.auth != null;',
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
rule: 'FIREBASE_OPEN_STORAGE',
|
|
147
|
-
title: 'Firebase: Open Storage Rules',
|
|
148
|
-
regex: /allow\s+read\s*,\s*write\s*;/g,
|
|
149
|
-
severity: 'critical',
|
|
150
|
-
cwe: 'CWE-284',
|
|
151
|
-
description: 'Firebase Storage rules allow unrestricted access. Add authentication checks.',
|
|
152
|
-
fix: 'Add auth check: allow read, write: if request.auth != null;',
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
// ── Terraform ──────────────────────────────────────────────────────────────
|
|
156
|
-
{
|
|
157
|
-
rule: 'TERRAFORM_PUBLIC_S3',
|
|
158
|
-
title: 'Terraform: Public S3 Bucket',
|
|
159
|
-
regex: /acl\s*=\s*"public-read(?:-write)?"/g,
|
|
160
|
-
severity: 'critical',
|
|
161
|
-
cwe: 'CWE-284',
|
|
162
|
-
description: 'S3 bucket with public ACL. Data is accessible to the internet.',
|
|
163
|
-
fix: 'Use acl = "private" and configure bucket policy for specific access',
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
rule: 'TERRAFORM_OPEN_SG',
|
|
167
|
-
title: 'Terraform: Open Security Group (0.0.0.0/0)',
|
|
168
|
-
regex: /cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0\/0"\s*\]/g,
|
|
169
|
-
severity: 'high',
|
|
170
|
-
cwe: 'CWE-284',
|
|
171
|
-
description: 'Security group open to all IPs. Restrict to specific CIDR blocks.',
|
|
172
|
-
fix: 'Replace 0.0.0.0/0 with specific IP ranges for the service',
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
rule: 'TERRAFORM_WILDCARD_IAM',
|
|
176
|
-
title: 'Terraform: Wildcard IAM Action',
|
|
177
|
-
regex: /actions?\s*=\s*\[\s*"\*"\s*\]/g,
|
|
178
|
-
severity: 'critical',
|
|
179
|
-
cwe: 'CWE-250',
|
|
180
|
-
description: 'IAM policy with Action: "*" grants unrestricted access. Apply least privilege.',
|
|
181
|
-
fix: 'Replace with specific actions: ["s3:GetObject", "s3:PutObject"]',
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
rule: 'TERRAFORM_NO_ENCRYPTION',
|
|
185
|
-
title: 'Terraform: Unencrypted Storage',
|
|
186
|
-
regex: /encrypted\s*=\s*false/g,
|
|
187
|
-
severity: 'high',
|
|
188
|
-
cwe: 'CWE-311',
|
|
189
|
-
description: 'Storage is not encrypted. Enable encryption at rest.',
|
|
190
|
-
fix: 'Set encrypted = true and configure KMS key',
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
rule: 'TERRAFORM_NO_LOGGING',
|
|
194
|
-
title: 'Terraform: Missing Access Logging',
|
|
195
|
-
regex: /logging\s*\{[^}]*enabled\s*=\s*false/g,
|
|
196
|
-
severity: 'medium',
|
|
197
|
-
cwe: 'CWE-778',
|
|
198
|
-
description: 'Access logging is disabled. Enable for audit trail and incident response.',
|
|
199
|
-
fix: 'Set enabled = true and configure log destination',
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
// ── Kubernetes ─────────────────────────────────────────────────────────────
|
|
203
|
-
{
|
|
204
|
-
rule: 'K8S_PRIVILEGED_CONTAINER',
|
|
205
|
-
title: 'Kubernetes: Privileged Container',
|
|
206
|
-
regex: /privileged\s*:\s*true/g,
|
|
207
|
-
severity: 'critical',
|
|
208
|
-
cwe: 'CWE-250',
|
|
209
|
-
description: 'Privileged Kubernetes pod can escape to the host. Remove privileged flag.',
|
|
210
|
-
fix: 'Set privileged: false. Use specific capabilities if needed.',
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
rule: 'K8S_HOST_NETWORK',
|
|
214
|
-
title: 'Kubernetes: Host Network Mode',
|
|
215
|
-
regex: /hostNetwork\s*:\s*true/g,
|
|
216
|
-
severity: 'high',
|
|
217
|
-
cwe: 'CWE-284',
|
|
218
|
-
description: 'Pod uses host network, bypassing network policies and isolation.',
|
|
219
|
-
fix: 'Remove hostNetwork: true. Use Kubernetes Services for networking.',
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
rule: 'K8S_NO_RESOURCE_LIMITS',
|
|
223
|
-
title: 'Kubernetes: Missing Resource Limits',
|
|
224
|
-
regex: /containers\s*:/g,
|
|
225
|
-
severity: 'medium',
|
|
226
|
-
cwe: 'CWE-770',
|
|
227
|
-
confidence: 'low',
|
|
228
|
-
description: 'Container without resource limits can consume unbounded CPU/memory (DoS).',
|
|
229
|
-
fix: 'Add resources.limits.cpu and resources.limits.memory to container spec',
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
rule: 'K8S_RUN_AS_ROOT',
|
|
233
|
-
title: 'Kubernetes: Running as Root',
|
|
234
|
-
regex: /runAsUser\s*:\s*0\b/g,
|
|
235
|
-
severity: 'high',
|
|
236
|
-
cwe: 'CWE-250',
|
|
237
|
-
description: 'Pod running as root (UID 0). Use a non-root user.',
|
|
238
|
-
fix: 'Set runAsUser: 1000 and runAsNonRoot: true in securityContext',
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
rule: 'K8S_DEFAULT_SA',
|
|
242
|
-
title: 'Kubernetes: Default Service Account',
|
|
243
|
-
regex: /serviceAccountName\s*:\s*["']?default["']?/g,
|
|
244
|
-
severity: 'medium',
|
|
245
|
-
cwe: 'CWE-284',
|
|
246
|
-
description: 'Using default service account. Create a dedicated SA with minimal permissions.',
|
|
247
|
-
fix: 'Create a dedicated ServiceAccount with only needed RBAC bindings',
|
|
248
|
-
},
|
|
249
|
-
|
|
250
|
-
// ── Docker Compose ─────────────────────────────────────────────────────────
|
|
251
|
-
{
|
|
252
|
-
rule: 'COMPOSE_HOST_MOUNT',
|
|
253
|
-
title: 'Docker Compose: Sensitive Host Mount',
|
|
254
|
-
regex: /volumes\s*:\s*\n\s*-\s*(?:\/etc|\/var\/run\/docker\.sock|\/root|\/proc|\/sys)/gm,
|
|
255
|
-
severity: 'critical',
|
|
256
|
-
cwe: 'CWE-284',
|
|
257
|
-
description: 'Mounting sensitive host paths into container enables escape and privilege escalation.',
|
|
258
|
-
fix: 'Remove sensitive host mounts. Use named volumes for data persistence.',
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
// ── Nginx ──────────────────────────────────────────────────────────────────
|
|
262
|
-
{
|
|
263
|
-
rule: 'NGINX_AUTOINDEX',
|
|
264
|
-
title: 'Nginx: Directory Listing Enabled',
|
|
265
|
-
regex: /autoindex\s+on/g,
|
|
266
|
-
severity: 'medium',
|
|
267
|
-
cwe: 'CWE-548',
|
|
268
|
-
description: 'Directory listing exposes file structure. Disable autoindex.',
|
|
269
|
-
fix: 'Set autoindex off; in nginx configuration',
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
rule: 'NGINX_SERVER_TOKENS',
|
|
273
|
-
title: 'Nginx: Server Version Exposed',
|
|
274
|
-
regex: /server_tokens\s+on/g,
|
|
275
|
-
severity: 'low',
|
|
276
|
-
cwe: 'CWE-200',
|
|
277
|
-
description: 'Server tokens reveal nginx version. Disable for security.',
|
|
278
|
-
fix: 'Set server_tokens off; in nginx.conf',
|
|
279
|
-
},
|
|
280
|
-
|
|
281
|
-
// ── General Config ─────────────────────────────────────────────────────────
|
|
282
|
-
{
|
|
283
|
-
rule: 'DEBUG_MODE_PRODUCTION',
|
|
284
|
-
title: 'Debug Mode in Production Config',
|
|
285
|
-
regex: /(?:DEBUG|debug)\s*[:=]\s*(?:true|True|1|['"]true['"])/g,
|
|
286
|
-
severity: 'high',
|
|
287
|
-
cwe: 'CWE-215',
|
|
288
|
-
owasp: 'A05:2021',
|
|
289
|
-
confidence: 'medium',
|
|
290
|
-
description: 'Debug mode exposes stack traces, internal state, and sensitive information.',
|
|
291
|
-
fix: 'Set DEBUG=false in production. Use environment-specific config.',
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
rule: 'VERBOSE_ERROR_MESSAGES',
|
|
295
|
-
title: 'Verbose Error Messages',
|
|
296
|
-
regex: /(?:stack|stackTrace|err\.message|error\.message|traceback)\s*(?:\)|,)/g,
|
|
297
|
-
severity: 'medium',
|
|
298
|
-
cwe: 'CWE-209',
|
|
299
|
-
owasp: 'A05:2021',
|
|
300
|
-
confidence: 'low',
|
|
301
|
-
description: 'Exposing stack traces or detailed errors in responses leaks internal information.',
|
|
302
|
-
fix: 'Log errors server-side. Return generic error messages to clients.',
|
|
303
|
-
},
|
|
304
|
-
|
|
305
|
-
// ──
|
|
306
|
-
{
|
|
307
|
-
rule: '
|
|
308
|
-
title: '
|
|
309
|
-
regex:
|
|
310
|
-
severity: '
|
|
311
|
-
cwe: 'CWE-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// ── Scan
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
return
|
|
374
|
-
});
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ConfigAuditor Agent
|
|
3
|
+
* ====================
|
|
4
|
+
*
|
|
5
|
+
* Detects security misconfigurations in:
|
|
6
|
+
* Dockerfile, docker-compose, vercel.json, netlify.toml,
|
|
7
|
+
* next.config.js, Terraform, Kubernetes, nginx, firebase,
|
|
8
|
+
* and security headers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { BaseAgent, createFinding } from './base-agent.js';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// DOCKERFILE PATTERNS
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
const DOCKERFILE_PATTERNS = [
|
|
19
|
+
{
|
|
20
|
+
rule: 'DOCKER_RUN_AS_ROOT',
|
|
21
|
+
title: 'Docker: Running as Root',
|
|
22
|
+
regex: /^(?!.*USER\s+\w).*CMD|ENTRYPOINT/gm,
|
|
23
|
+
severity: 'high',
|
|
24
|
+
cwe: 'CWE-250',
|
|
25
|
+
owasp: 'A05:2021',
|
|
26
|
+
confidence: 'medium',
|
|
27
|
+
description: 'No USER instruction found. Container runs as root by default.',
|
|
28
|
+
fix: 'Add USER nonroot before CMD/ENTRYPOINT',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
rule: 'DOCKER_LATEST_TAG',
|
|
32
|
+
title: 'Docker: Using :latest Tag',
|
|
33
|
+
regex: /FROM\s+\S+:latest/gi,
|
|
34
|
+
severity: 'medium',
|
|
35
|
+
cwe: 'CWE-1104',
|
|
36
|
+
description: ':latest tag is mutable and can change unexpectedly. Pin to a specific version.',
|
|
37
|
+
fix: 'Pin to specific version: FROM node:20-alpine instead of FROM node:latest',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
rule: 'DOCKER_ADD_REMOTE',
|
|
41
|
+
title: 'Docker: ADD with Remote URL',
|
|
42
|
+
regex: /ADD\s+https?:\/\//gi,
|
|
43
|
+
severity: 'high',
|
|
44
|
+
cwe: 'CWE-829',
|
|
45
|
+
description: 'ADD with URL downloads without checksum verification. Use COPY + curl with checksum.',
|
|
46
|
+
fix: 'Replace ADD URL with: RUN curl -fsSL url -o file && sha256sum -c <<< "hash file"',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
rule: 'DOCKER_SECRET_ENV',
|
|
50
|
+
title: 'Docker: Secret in ENV/ARG',
|
|
51
|
+
regex: /(?:ENV|ARG)\s+(?:.*(?:PASSWORD|SECRET|KEY|TOKEN|CREDENTIAL|API_KEY))\s*=/gi,
|
|
52
|
+
severity: 'critical',
|
|
53
|
+
cwe: 'CWE-798',
|
|
54
|
+
description: 'Secrets in ENV/ARG are baked into image layers. Use Docker secrets or runtime env.',
|
|
55
|
+
fix: 'Use --secret flag in docker build, or pass secrets at runtime via -e',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
rule: 'DOCKER_EXPOSE_ALL',
|
|
59
|
+
title: 'Docker: Exposing Privileged Port',
|
|
60
|
+
regex: /EXPOSE\s+(?:22|23|3389|5432|3306|27017|6379|11211)\b/g,
|
|
61
|
+
severity: 'medium',
|
|
62
|
+
cwe: 'CWE-200',
|
|
63
|
+
description: 'Exposing database/admin ports in container. Only expose application ports.',
|
|
64
|
+
fix: 'Remove EXPOSE for database ports. Use Docker networking for internal communication.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
rule: 'DOCKER_PRIVILEGED',
|
|
68
|
+
title: 'Docker: Privileged Mode',
|
|
69
|
+
regex: /privileged\s*:\s*true/g,
|
|
70
|
+
severity: 'critical',
|
|
71
|
+
cwe: 'CWE-250',
|
|
72
|
+
description: 'Privileged containers have full host access. This enables container escape.',
|
|
73
|
+
fix: 'Remove privileged: true. Use specific capabilities if needed (cap_add).',
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// CONFIG FILE PATTERNS
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
const CONFIG_PATTERNS = [
|
|
82
|
+
// ── Security Headers ───────────────────────────────────────────────────────
|
|
83
|
+
{
|
|
84
|
+
rule: 'MISSING_CSP',
|
|
85
|
+
title: 'Missing Content-Security-Policy',
|
|
86
|
+
regex: /headers\s*(?::|=)\s*\[/g,
|
|
87
|
+
severity: 'medium',
|
|
88
|
+
cwe: 'CWE-693',
|
|
89
|
+
owasp: 'A05:2021',
|
|
90
|
+
confidence: 'low',
|
|
91
|
+
description: 'No Content-Security-Policy header detected. CSP prevents XSS and data injection.',
|
|
92
|
+
fix: "Add Content-Security-Policy header: \"default-src 'self'; script-src 'self'\"",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
rule: 'CORS_WILDCARD',
|
|
96
|
+
title: 'CORS Wildcard Origin',
|
|
97
|
+
regex: /(?:Access-Control-Allow-Origin|origin)\s*[:=]\s*['"]?\*['"]?/g,
|
|
98
|
+
severity: 'high',
|
|
99
|
+
cwe: 'CWE-942',
|
|
100
|
+
owasp: 'A05:2021',
|
|
101
|
+
description: 'CORS wildcard (*) allows any origin. Use specific trusted origins.',
|
|
102
|
+
fix: 'Replace * with specific origins: ["https://yourdomain.com"]',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
rule: 'CORS_CREDENTIALS_WILDCARD',
|
|
106
|
+
title: 'CORS Credentials with Wildcard',
|
|
107
|
+
regex: /credentials\s*:\s*true.*origin\s*:\s*true|origin\s*:\s*true.*credentials\s*:\s*true/g,
|
|
108
|
+
severity: 'critical',
|
|
109
|
+
cwe: 'CWE-942',
|
|
110
|
+
owasp: 'A05:2021',
|
|
111
|
+
description: 'CORS with credentials: true and origin: true reflects any origin, enabling credential theft.',
|
|
112
|
+
fix: 'Use a specific origin allowlist when credentials: true',
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// ── Next.js Config ─────────────────────────────────────────────────────────
|
|
116
|
+
{
|
|
117
|
+
rule: 'NEXTJS_POWERED_BY',
|
|
118
|
+
title: 'Next.js: X-Powered-By Header Enabled',
|
|
119
|
+
regex: /poweredByHeader\s*:\s*true/g,
|
|
120
|
+
severity: 'low',
|
|
121
|
+
cwe: 'CWE-200',
|
|
122
|
+
description: 'X-Powered-By header reveals technology stack. Disable for security through obscurity.',
|
|
123
|
+
fix: 'Set poweredByHeader: false in next.config.js',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
rule: 'NEXTJS_WILDCARD_IMAGES',
|
|
127
|
+
title: 'Next.js: Wildcard Image Domain',
|
|
128
|
+
regex: /images\s*:\s*\{[^}]*(?:domains|remotePatterns)[^}]*\*\*/g,
|
|
129
|
+
severity: 'medium',
|
|
130
|
+
cwe: 'CWE-918',
|
|
131
|
+
description: 'Wildcard image domains can be abused for SSRF via Next.js image optimization.',
|
|
132
|
+
fix: 'Specify exact domains in images.remotePatterns',
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ── Firebase ───────────────────────────────────────────────────────────────
|
|
136
|
+
{
|
|
137
|
+
rule: 'FIREBASE_OPEN_RULES',
|
|
138
|
+
title: 'Firebase: Open Security Rules',
|
|
139
|
+
regex: /allow\s+read\s*,\s*write\s*:\s*if\s+true/g,
|
|
140
|
+
severity: 'critical',
|
|
141
|
+
cwe: 'CWE-284',
|
|
142
|
+
description: 'Firebase rules allow unauthenticated read/write. Any user can access all data.',
|
|
143
|
+
fix: 'Change to: allow read, write: if request.auth != null;',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
rule: 'FIREBASE_OPEN_STORAGE',
|
|
147
|
+
title: 'Firebase: Open Storage Rules',
|
|
148
|
+
regex: /allow\s+read\s*,\s*write\s*;/g,
|
|
149
|
+
severity: 'critical',
|
|
150
|
+
cwe: 'CWE-284',
|
|
151
|
+
description: 'Firebase Storage rules allow unrestricted access. Add authentication checks.',
|
|
152
|
+
fix: 'Add auth check: allow read, write: if request.auth != null;',
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// ── Terraform ──────────────────────────────────────────────────────────────
|
|
156
|
+
{
|
|
157
|
+
rule: 'TERRAFORM_PUBLIC_S3',
|
|
158
|
+
title: 'Terraform: Public S3 Bucket',
|
|
159
|
+
regex: /acl\s*=\s*"public-read(?:-write)?"/g,
|
|
160
|
+
severity: 'critical',
|
|
161
|
+
cwe: 'CWE-284',
|
|
162
|
+
description: 'S3 bucket with public ACL. Data is accessible to the internet.',
|
|
163
|
+
fix: 'Use acl = "private" and configure bucket policy for specific access',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
rule: 'TERRAFORM_OPEN_SG',
|
|
167
|
+
title: 'Terraform: Open Security Group (0.0.0.0/0)',
|
|
168
|
+
regex: /cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0\/0"\s*\]/g,
|
|
169
|
+
severity: 'high',
|
|
170
|
+
cwe: 'CWE-284',
|
|
171
|
+
description: 'Security group open to all IPs. Restrict to specific CIDR blocks.',
|
|
172
|
+
fix: 'Replace 0.0.0.0/0 with specific IP ranges for the service',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
rule: 'TERRAFORM_WILDCARD_IAM',
|
|
176
|
+
title: 'Terraform: Wildcard IAM Action',
|
|
177
|
+
regex: /actions?\s*=\s*\[\s*"\*"\s*\]/g,
|
|
178
|
+
severity: 'critical',
|
|
179
|
+
cwe: 'CWE-250',
|
|
180
|
+
description: 'IAM policy with Action: "*" grants unrestricted access. Apply least privilege.',
|
|
181
|
+
fix: 'Replace with specific actions: ["s3:GetObject", "s3:PutObject"]',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
rule: 'TERRAFORM_NO_ENCRYPTION',
|
|
185
|
+
title: 'Terraform: Unencrypted Storage',
|
|
186
|
+
regex: /encrypted\s*=\s*false/g,
|
|
187
|
+
severity: 'high',
|
|
188
|
+
cwe: 'CWE-311',
|
|
189
|
+
description: 'Storage is not encrypted. Enable encryption at rest.',
|
|
190
|
+
fix: 'Set encrypted = true and configure KMS key',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
rule: 'TERRAFORM_NO_LOGGING',
|
|
194
|
+
title: 'Terraform: Missing Access Logging',
|
|
195
|
+
regex: /logging\s*\{[^}]*enabled\s*=\s*false/g,
|
|
196
|
+
severity: 'medium',
|
|
197
|
+
cwe: 'CWE-778',
|
|
198
|
+
description: 'Access logging is disabled. Enable for audit trail and incident response.',
|
|
199
|
+
fix: 'Set enabled = true and configure log destination',
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// ── Kubernetes ─────────────────────────────────────────────────────────────
|
|
203
|
+
{
|
|
204
|
+
rule: 'K8S_PRIVILEGED_CONTAINER',
|
|
205
|
+
title: 'Kubernetes: Privileged Container',
|
|
206
|
+
regex: /privileged\s*:\s*true/g,
|
|
207
|
+
severity: 'critical',
|
|
208
|
+
cwe: 'CWE-250',
|
|
209
|
+
description: 'Privileged Kubernetes pod can escape to the host. Remove privileged flag.',
|
|
210
|
+
fix: 'Set privileged: false. Use specific capabilities if needed.',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
rule: 'K8S_HOST_NETWORK',
|
|
214
|
+
title: 'Kubernetes: Host Network Mode',
|
|
215
|
+
regex: /hostNetwork\s*:\s*true/g,
|
|
216
|
+
severity: 'high',
|
|
217
|
+
cwe: 'CWE-284',
|
|
218
|
+
description: 'Pod uses host network, bypassing network policies and isolation.',
|
|
219
|
+
fix: 'Remove hostNetwork: true. Use Kubernetes Services for networking.',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
rule: 'K8S_NO_RESOURCE_LIMITS',
|
|
223
|
+
title: 'Kubernetes: Missing Resource Limits',
|
|
224
|
+
regex: /containers\s*:/g,
|
|
225
|
+
severity: 'medium',
|
|
226
|
+
cwe: 'CWE-770',
|
|
227
|
+
confidence: 'low',
|
|
228
|
+
description: 'Container without resource limits can consume unbounded CPU/memory (DoS).',
|
|
229
|
+
fix: 'Add resources.limits.cpu and resources.limits.memory to container spec',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
rule: 'K8S_RUN_AS_ROOT',
|
|
233
|
+
title: 'Kubernetes: Running as Root',
|
|
234
|
+
regex: /runAsUser\s*:\s*0\b/g,
|
|
235
|
+
severity: 'high',
|
|
236
|
+
cwe: 'CWE-250',
|
|
237
|
+
description: 'Pod running as root (UID 0). Use a non-root user.',
|
|
238
|
+
fix: 'Set runAsUser: 1000 and runAsNonRoot: true in securityContext',
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
rule: 'K8S_DEFAULT_SA',
|
|
242
|
+
title: 'Kubernetes: Default Service Account',
|
|
243
|
+
regex: /serviceAccountName\s*:\s*["']?default["']?/g,
|
|
244
|
+
severity: 'medium',
|
|
245
|
+
cwe: 'CWE-284',
|
|
246
|
+
description: 'Using default service account. Create a dedicated SA with minimal permissions.',
|
|
247
|
+
fix: 'Create a dedicated ServiceAccount with only needed RBAC bindings',
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// ── Docker Compose ─────────────────────────────────────────────────────────
|
|
251
|
+
{
|
|
252
|
+
rule: 'COMPOSE_HOST_MOUNT',
|
|
253
|
+
title: 'Docker Compose: Sensitive Host Mount',
|
|
254
|
+
regex: /volumes\s*:\s*\n\s*-\s*(?:\/etc|\/var\/run\/docker\.sock|\/root|\/proc|\/sys)/gm,
|
|
255
|
+
severity: 'critical',
|
|
256
|
+
cwe: 'CWE-284',
|
|
257
|
+
description: 'Mounting sensitive host paths into container enables escape and privilege escalation.',
|
|
258
|
+
fix: 'Remove sensitive host mounts. Use named volumes for data persistence.',
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// ── Nginx ──────────────────────────────────────────────────────────────────
|
|
262
|
+
{
|
|
263
|
+
rule: 'NGINX_AUTOINDEX',
|
|
264
|
+
title: 'Nginx: Directory Listing Enabled',
|
|
265
|
+
regex: /autoindex\s+on/g,
|
|
266
|
+
severity: 'medium',
|
|
267
|
+
cwe: 'CWE-548',
|
|
268
|
+
description: 'Directory listing exposes file structure. Disable autoindex.',
|
|
269
|
+
fix: 'Set autoindex off; in nginx configuration',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
rule: 'NGINX_SERVER_TOKENS',
|
|
273
|
+
title: 'Nginx: Server Version Exposed',
|
|
274
|
+
regex: /server_tokens\s+on/g,
|
|
275
|
+
severity: 'low',
|
|
276
|
+
cwe: 'CWE-200',
|
|
277
|
+
description: 'Server tokens reveal nginx version. Disable for security.',
|
|
278
|
+
fix: 'Set server_tokens off; in nginx.conf',
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// ── General Config ─────────────────────────────────────────────────────────
|
|
282
|
+
{
|
|
283
|
+
rule: 'DEBUG_MODE_PRODUCTION',
|
|
284
|
+
title: 'Debug Mode in Production Config',
|
|
285
|
+
regex: /(?:DEBUG|debug)\s*[:=]\s*(?:true|True|1|['"]true['"])/g,
|
|
286
|
+
severity: 'high',
|
|
287
|
+
cwe: 'CWE-215',
|
|
288
|
+
owasp: 'A05:2021',
|
|
289
|
+
confidence: 'medium',
|
|
290
|
+
description: 'Debug mode exposes stack traces, internal state, and sensitive information.',
|
|
291
|
+
fix: 'Set DEBUG=false in production. Use environment-specific config.',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
rule: 'VERBOSE_ERROR_MESSAGES',
|
|
295
|
+
title: 'Verbose Error Messages',
|
|
296
|
+
regex: /(?:stack|stackTrace|err\.message|error\.message|traceback)\s*(?:\)|,)/g,
|
|
297
|
+
severity: 'medium',
|
|
298
|
+
cwe: 'CWE-209',
|
|
299
|
+
owasp: 'A05:2021',
|
|
300
|
+
confidence: 'low',
|
|
301
|
+
description: 'Exposing stack traces or detailed errors in responses leaks internal information.',
|
|
302
|
+
fix: 'Log errors server-side. Return generic error messages to clients.',
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
// ── Go Security ──────────────────────────────────────────────────────────
|
|
306
|
+
{
|
|
307
|
+
rule: 'GO_SQL_SPRINTF',
|
|
308
|
+
title: 'Go: SQL Injection via fmt.Sprintf',
|
|
309
|
+
regex: /fmt\.Sprintf\s*\(\s*["'](?:SELECT|INSERT|UPDATE|DELETE)\s+[^"']*%/gi,
|
|
310
|
+
severity: 'critical',
|
|
311
|
+
cwe: 'CWE-89',
|
|
312
|
+
owasp: 'A03:2021',
|
|
313
|
+
description: 'Go SQL query built with fmt.Sprintf enables SQL injection. Use parameterized queries.',
|
|
314
|
+
fix: 'Use db.Query("SELECT * FROM users WHERE id = $1", id)',
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
rule: 'GO_TEMPLATE_UNESCAPED',
|
|
318
|
+
title: 'Go: Unescaped HTML Template',
|
|
319
|
+
regex: /template\.HTML\s*\(/g,
|
|
320
|
+
severity: 'high',
|
|
321
|
+
cwe: 'CWE-79',
|
|
322
|
+
owasp: 'A03:2021',
|
|
323
|
+
description: 'template.HTML() marks content as safe, bypassing Go HTML template escaping.',
|
|
324
|
+
fix: 'Avoid template.HTML() with user input. Let html/template auto-escape.',
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// ── Rust Security ──────────────────────────────────────────────────────────
|
|
328
|
+
{
|
|
329
|
+
rule: 'RUST_UNSAFE_BLOCK',
|
|
330
|
+
title: 'Rust: unsafe Block',
|
|
331
|
+
regex: /\bunsafe\s*\{/g,
|
|
332
|
+
severity: 'medium',
|
|
333
|
+
cwe: 'CWE-676',
|
|
334
|
+
confidence: 'low',
|
|
335
|
+
description: 'Rust unsafe block bypasses memory safety guarantees. Review for correctness.',
|
|
336
|
+
fix: 'Minimize unsafe code. Wrap in safe abstractions with safety invariants documented.',
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
rule: 'RUST_UNWRAP_IN_PROD',
|
|
340
|
+
title: 'Rust: .unwrap() Without Error Handling',
|
|
341
|
+
regex: /\.unwrap\(\)/g,
|
|
342
|
+
severity: 'low',
|
|
343
|
+
cwe: 'CWE-391',
|
|
344
|
+
confidence: 'low',
|
|
345
|
+
description: '.unwrap() panics on error. Use proper error handling in production code.',
|
|
346
|
+
fix: 'Use .unwrap_or(), .unwrap_or_default(), or ? operator for error propagation.',
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// ── Deprecated Node.js ─────────────────────────────────────────────────────
|
|
350
|
+
{
|
|
351
|
+
rule: 'DEPRECATED_BUFFER',
|
|
352
|
+
title: 'Deprecated: new Buffer()',
|
|
353
|
+
regex: /\bnew\s+Buffer\s*\(/g,
|
|
354
|
+
severity: 'medium',
|
|
355
|
+
cwe: 'CWE-676',
|
|
356
|
+
description: 'new Buffer() is deprecated and has security implications. Use Buffer.from().',
|
|
357
|
+
fix: 'Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()',
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
export class ConfigAuditor extends BaseAgent {
|
|
362
|
+
constructor() {
|
|
363
|
+
super('ConfigAuditor', 'Detect security misconfigurations in infrastructure and app config', 'config');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async analyze(context) {
|
|
367
|
+
const { rootPath, files, recon } = context;
|
|
368
|
+
let findings = [];
|
|
369
|
+
|
|
370
|
+
// ── Scan Dockerfiles ──────────────────────────────────────────────────────
|
|
371
|
+
const dockerfiles = files.filter(f => {
|
|
372
|
+
const basename = path.basename(f);
|
|
373
|
+
return basename === 'Dockerfile' || basename.startsWith('Dockerfile.');
|
|
374
|
+
});
|
|
375
|
+
for (const file of dockerfiles) {
|
|
376
|
+
findings = findings.concat(this.scanFileWithPatterns(file, DOCKERFILE_PATTERNS));
|
|
377
|
+
findings = findings.concat(this.checkDockerfileUser(file));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Scan docker-compose ───────────────────────────────────────────────────
|
|
381
|
+
const composeFiles = files.filter(f => /docker-compose\.ya?ml$/i.test(path.basename(f)));
|
|
382
|
+
for (const file of composeFiles) {
|
|
383
|
+
findings = findings.concat(this.scanFileWithPatterns(file, CONFIG_PATTERNS));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── Scan Terraform ────────────────────────────────────────────────────────
|
|
387
|
+
const tfFiles = files.filter(f => path.extname(f) === '.tf');
|
|
388
|
+
for (const file of tfFiles) {
|
|
389
|
+
findings = findings.concat(this.scanFileWithPatterns(file, CONFIG_PATTERNS));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── Scan Kubernetes manifests ─────────────────────────────────────────────
|
|
393
|
+
const k8sFiles = files.filter(f => {
|
|
394
|
+
const relPath = path.relative(rootPath, f).replace(/\\/g, '/');
|
|
395
|
+
return /\.ya?ml$/i.test(f) && /(?:k8s|kubernetes|deploy|helm|manifests)/i.test(relPath);
|
|
396
|
+
});
|
|
397
|
+
for (const file of k8sFiles) {
|
|
398
|
+
findings = findings.concat(this.scanFileWithPatterns(file, CONFIG_PATTERNS));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Scan config files ─────────────────────────────────────────────────────
|
|
402
|
+
const configFiles = files.filter(f => {
|
|
403
|
+
const basename = path.basename(f);
|
|
404
|
+
return [
|
|
405
|
+
'vercel.json', 'netlify.toml', 'next.config.js', 'next.config.mjs', 'next.config.ts',
|
|
406
|
+
'nginx.conf', 'Caddyfile', 'firebase.json', 'firestore.rules', 'storage.rules',
|
|
407
|
+
'.env.example', '.env.sample', '.env.local',
|
|
408
|
+
].includes(basename);
|
|
409
|
+
});
|
|
410
|
+
for (const file of configFiles) {
|
|
411
|
+
findings = findings.concat(this.scanFileWithPatterns(file, CONFIG_PATTERNS));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── Scan all code files for general config issues ─────────────────────────
|
|
415
|
+
const codeFiles = files.filter(f => {
|
|
416
|
+
const ext = path.extname(f).toLowerCase();
|
|
417
|
+
return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.py', '.rb', '.go', '.rs', '.php'].includes(ext);
|
|
418
|
+
});
|
|
419
|
+
const generalPatterns = CONFIG_PATTERNS.filter(p =>
|
|
420
|
+
['CORS_WILDCARD', 'CORS_CREDENTIALS_WILDCARD', 'DEBUG_MODE_PRODUCTION',
|
|
421
|
+
'DEPRECATED_BUFFER', 'GO_SQL_SPRINTF', 'GO_TEMPLATE_UNESCAPED',
|
|
422
|
+
'RUST_UNSAFE_BLOCK', 'RUST_UNWRAP_IN_PROD'].includes(p.rule)
|
|
423
|
+
);
|
|
424
|
+
for (const file of codeFiles) {
|
|
425
|
+
findings = findings.concat(this.scanFileWithPatterns(file, generalPatterns));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return findings;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if a Dockerfile has a USER instruction before CMD/ENTRYPOINT.
|
|
433
|
+
*/
|
|
434
|
+
checkDockerfileUser(filePath) {
|
|
435
|
+
const content = this.readFile(filePath);
|
|
436
|
+
if (!content) return [];
|
|
437
|
+
|
|
438
|
+
const hasUser = /^USER\s+(?!root)\S+/m.test(content);
|
|
439
|
+
const hasCmd = /^(?:CMD|ENTRYPOINT)\s+/m.test(content);
|
|
440
|
+
|
|
441
|
+
if (hasCmd && !hasUser) {
|
|
442
|
+
return [createFinding({
|
|
443
|
+
file: filePath,
|
|
444
|
+
line: 1,
|
|
445
|
+
severity: 'high',
|
|
446
|
+
category: 'config',
|
|
447
|
+
rule: 'DOCKER_NO_USER',
|
|
448
|
+
title: 'Dockerfile: No Non-Root USER',
|
|
449
|
+
description: 'No USER instruction found. Container runs as root, enabling escape attacks.',
|
|
450
|
+
matched: 'Missing USER instruction',
|
|
451
|
+
fix: 'Add before CMD: RUN addgroup -S app && adduser -S app -G app\nUSER app',
|
|
452
|
+
})];
|
|
453
|
+
}
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export default ConfigAuditor;
|