omen-sec-cli 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -63
- package/bin/index.js +67 -25
- package/core/discover/stack-detector.js +67 -0
- package/core/engine-v2.js +170 -0
- package/core/generator.js +43 -11
- package/core/local-scanner.js +154 -41
- package/core/remote-scanner.js +220 -98
- package/core/reporters/fix-plan-reporter.js +46 -0
- package/core/reporters/markdown-reporter.js +25 -0
- package/core/runners/local-app-runner.js +39 -0
- package/core/scanner.js +34 -24
- package/core/state/state-manager.js +43 -0
- package/core/ui-server.js +101 -37
- package/package.json +6 -1
- package/ui/banner.js +1 -1
package/core/remote-scanner.js
CHANGED
|
@@ -36,47 +36,54 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
36
36
|
headers_analysis["Strict-Transport-Security"] = "Missing";
|
|
37
37
|
vulnerabilities.push({
|
|
38
38
|
id: `REM-VULN-${Date.now()}-1`,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
kind: 'header',
|
|
40
|
+
category: 'hardening',
|
|
41
|
+
confidence: 'high',
|
|
42
|
+
severity: 'medium',
|
|
43
|
+
title: 'HSTS Header Missing',
|
|
44
|
+
description: `HTTP Strict-Transport-Security (HSTS) header is missing. This prevents the browser from enforcing HTTPS-only connections for future visits.`,
|
|
43
45
|
cwe: 'CWE-319',
|
|
44
46
|
evidence: {
|
|
45
47
|
request: { headers: { ...response.request.headers } },
|
|
46
48
|
response: { status: response.status, headers: response.headers },
|
|
47
49
|
reason: 'Security header "Strict-Transport-Security" not found in server response.'
|
|
48
|
-
}
|
|
50
|
+
},
|
|
51
|
+
remediation: 'Implement the Strict-Transport-Security header with a long max-age (e.g., 31536000) and the includeSubDomains directive.'
|
|
49
52
|
});
|
|
50
|
-
} else {
|
|
51
|
-
headers_analysis["Strict-Transport-Security"] = headers['strict-transport-security'];
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
if (!headers['content-security-policy']) {
|
|
55
56
|
headers_analysis["Content-Security-Policy"] = "Missing";
|
|
56
57
|
vulnerabilities.push({
|
|
57
58
|
id: `REM-VULN-${Date.now()}-2`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
kind: 'header',
|
|
60
|
+
category: 'confirmed',
|
|
61
|
+
confidence: 'high',
|
|
62
|
+
severity: 'high',
|
|
63
|
+
title: 'Content-Security-Policy Missing',
|
|
61
64
|
description: `CSP header is missing. Without a strict Content-Security-Policy, the application is highly vulnerable to Cross-Site Scripting (XSS) and data injection attacks.`,
|
|
62
65
|
cwe: 'CWE-1022',
|
|
63
66
|
evidence: {
|
|
64
67
|
request: { headers: { ...response.request.headers } },
|
|
65
68
|
response: { status: response.status, headers: response.headers },
|
|
66
69
|
reason: 'Security header "Content-Security-Policy" not found in server response.'
|
|
67
|
-
}
|
|
70
|
+
},
|
|
71
|
+
remediation: 'Define a strict Content-Security-Policy to restrict source domains for scripts, styles, and other resources.'
|
|
68
72
|
});
|
|
69
73
|
} else {
|
|
70
74
|
headers_analysis["Content-Security-Policy"] = headers['content-security-policy'];
|
|
71
75
|
if (headers['content-security-policy'].includes("unsafe-inline")) {
|
|
72
76
|
vulnerabilities.push({
|
|
73
77
|
id: `REM-VULN-${Date.now()}-3`,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
kind: 'header',
|
|
79
|
+
category: 'confirmed',
|
|
80
|
+
confidence: 'high',
|
|
81
|
+
severity: 'high',
|
|
82
|
+
title: 'Insecure CSP (unsafe-inline)',
|
|
83
|
+
description: `The Content-Security-Policy allows 'unsafe-inline' for scripts or styles, significantly weakening protection against XSS.`,
|
|
78
84
|
cwe: 'CWE-16',
|
|
79
|
-
evidence: { finding: `policy: ${headers['content-security-policy']}` }
|
|
85
|
+
evidence: { finding: `policy: ${headers['content-security-policy']}` },
|
|
86
|
+
remediation: 'Refactor the application to avoid inline scripts and styles, then remove "unsafe-inline" from the CSP.'
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
}
|
|
@@ -86,50 +93,101 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
86
93
|
headers_analysis["X-Frame-Options"] = "Missing";
|
|
87
94
|
vulnerabilities.push({
|
|
88
95
|
id: `REM-VULN-${Date.now()}-4`,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
kind: 'header',
|
|
97
|
+
category: 'hardening',
|
|
98
|
+
confidence: 'high',
|
|
99
|
+
severity: 'low',
|
|
100
|
+
title: 'X-Frame-Options Missing',
|
|
101
|
+
description: `Missing X-Frame-Options header. This allows the application to be embedded in an iframe on other domains, increasing Clickjacking risk.`,
|
|
93
102
|
cwe: 'CWE-1021',
|
|
94
103
|
evidence: {
|
|
95
104
|
request: { headers: { ...response.request.headers } },
|
|
96
105
|
response: { status: response.status, headers: response.headers },
|
|
97
106
|
reason: 'Security header "X-Frame-Options" not found. This allows the site to be embedded in iframes on third-party domains.'
|
|
98
|
-
}
|
|
107
|
+
},
|
|
108
|
+
remediation: 'Set the X-Frame-Options header to DENY or SAMEORIGIN.'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 3.1 Analisar X-Content-Type-Options
|
|
113
|
+
if (!headers['x-content-type-options']) {
|
|
114
|
+
headers_analysis["X-Content-Type-Options"] = "Missing";
|
|
115
|
+
vulnerabilities.push({
|
|
116
|
+
id: `REM-VULN-${Date.now()}-6`,
|
|
117
|
+
kind: 'header',
|
|
118
|
+
category: 'hardening',
|
|
119
|
+
confidence: 'high',
|
|
120
|
+
severity: 'low',
|
|
121
|
+
title: 'X-Content-Type-Options Missing',
|
|
122
|
+
description: `The X-Content-Type-Options: nosniff header is missing. This could allow the browser to "sniff" the content type, potentially leading to MIME-type sniffing attacks.`,
|
|
123
|
+
cwe: 'CWE-116',
|
|
124
|
+
evidence: { response: { headers: response.headers } },
|
|
125
|
+
remediation: 'Add the "X-Content-Type-Options: nosniff" header to all responses.'
|
|
99
126
|
});
|
|
100
|
-
} else {
|
|
101
|
-
headers_analysis["X-Frame-Options"] = headers['x-frame-options'];
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
// 4. Server Header Leak
|
|
105
|
-
if (headers['server']) {
|
|
130
|
+
if (headers['server'] && !headers['server'].includes('Cloudflare') && !headers['server'].includes('Vercel')) {
|
|
106
131
|
headers_analysis["Server"] = headers['server'];
|
|
107
132
|
vulnerabilities.push({
|
|
108
133
|
id: `REM-VULN-${Date.now()}-5`,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
kind: 'tech',
|
|
135
|
+
category: 'informational',
|
|
136
|
+
confidence: 'high',
|
|
137
|
+
severity: 'low',
|
|
138
|
+
title: 'Server Header Disclosure',
|
|
139
|
+
description: `Server header leaks technology stack information: ${headers['server']}`,
|
|
113
140
|
cwe: 'CWE-200',
|
|
114
|
-
evidence: { finding: `Server: ${headers['server']}` }
|
|
141
|
+
evidence: { finding: `Server: ${headers['server']}` },
|
|
142
|
+
remediation: 'Configure the web server to hide or spoof the "Server" header.'
|
|
115
143
|
});
|
|
116
|
-
} else {
|
|
117
|
-
headers_analysis["Server"] = "Hidden (Good)";
|
|
118
144
|
}
|
|
119
145
|
|
|
120
|
-
//
|
|
121
|
-
if (headers['
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
// 6. CORS Analysis
|
|
147
|
+
if (headers['access-control-allow-origin']) {
|
|
148
|
+
const cors = headers['access-control-allow-origin'];
|
|
149
|
+
if (cors === '*') {
|
|
150
|
+
vulnerabilities.push({
|
|
151
|
+
id: `REM-CORS-${Date.now()}`,
|
|
152
|
+
kind: 'header',
|
|
153
|
+
category: 'confirmed',
|
|
154
|
+
confidence: 'high',
|
|
155
|
+
severity: 'high',
|
|
156
|
+
title: 'Permissive CORS Policy',
|
|
157
|
+
description: `The application allows Cross-Origin Resource Sharing from any domain (Access-Control-Allow-Origin: *).`,
|
|
158
|
+
cwe: 'CWE-942',
|
|
159
|
+
evidence: { finding: `Access-Control-Allow-Origin: ${cors}` },
|
|
160
|
+
remediation: 'Restrict Access-Control-Allow-Origin to trusted domains only.'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
131
163
|
}
|
|
132
164
|
|
|
165
|
+
// 7. Cookie Security
|
|
166
|
+
const setCookies = headers['set-cookie'] || [];
|
|
167
|
+
const cookies = Array.isArray(setCookies) ? setCookies : [setCookies];
|
|
168
|
+
cookies.forEach(cookie => {
|
|
169
|
+
const issues = [];
|
|
170
|
+
if (!cookie.toLowerCase().includes('secure')) issues.push('Missing "Secure" flag');
|
|
171
|
+
if (!cookie.toLowerCase().includes('httponly')) issues.push('Missing "HttpOnly" flag');
|
|
172
|
+
if (!cookie.toLowerCase().includes('samesite')) issues.push('Missing "SameSite" attribute');
|
|
173
|
+
|
|
174
|
+
if (issues.length > 0) {
|
|
175
|
+
vulnerabilities.push({
|
|
176
|
+
id: `REM-COOKIE-${Date.now()}`,
|
|
177
|
+
kind: 'header',
|
|
178
|
+
category: 'confirmed',
|
|
179
|
+
confidence: 'high',
|
|
180
|
+
severity: 'medium',
|
|
181
|
+
title: 'Insecure Cookie Configuration',
|
|
182
|
+
description: `Cookie detected with missing security flags: ${issues.join(', ')}.`,
|
|
183
|
+
cwe: 'CWE-614',
|
|
184
|
+
evidence: { cookie: cookie.split(';')[0] + '...', issues },
|
|
185
|
+
remediation: 'Apply Secure, HttpOnly, and SameSite=Strict/Lax attributes to all sensitive cookies.'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
|
|
133
191
|
// --- SPIDER / CRAWLER (DEEP) ---
|
|
134
192
|
if (typeof html === 'string') {
|
|
135
193
|
const $ = cheerio.load(html);
|
|
@@ -175,6 +233,22 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
175
233
|
});
|
|
176
234
|
}
|
|
177
235
|
|
|
236
|
+
// --- TECHNOLOGY FINGERPRINTING (Update categories) ---
|
|
237
|
+
if (techStack.length > 0) {
|
|
238
|
+
vulnerabilities.push({
|
|
239
|
+
id: `REM-TECH-${Date.now()}`,
|
|
240
|
+
kind: 'tech',
|
|
241
|
+
category: 'informational',
|
|
242
|
+
confidence: 'high',
|
|
243
|
+
severity: 'info',
|
|
244
|
+
title: 'Technology Stack Identified',
|
|
245
|
+
description: `Fingerprinting identified the following technologies: ${techStack.join(', ')}`,
|
|
246
|
+
cwe: 'CWE-200',
|
|
247
|
+
evidence: { tech_stack: techStack },
|
|
248
|
+
remediation: 'Minimal tech disclosure is recommended to prevent targeted attacks.'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
178
252
|
// --- DEEP CRAWL: robots.txt, sitemap.xml, and JS files ---
|
|
179
253
|
const robotsUrl = new URL('/robots.txt', targetUrl).href;
|
|
180
254
|
try {
|
|
@@ -228,41 +302,32 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
228
302
|
const fuzzUrl = new URL(path, targetUrl).href;
|
|
229
303
|
const fuzzRes = await axios.get(fuzzUrl, {
|
|
230
304
|
timeout: 5000,
|
|
231
|
-
validateStatus: (status) => status
|
|
305
|
+
validateStatus: (status) => status >= 200 && status < 500
|
|
232
306
|
});
|
|
233
307
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
type: 'Sensitive Path Exposed',
|
|
238
|
-
severity: path.includes('.env') || path.includes('.git') || path.includes('.ssh') ? 'Critical' : 'High',
|
|
239
|
-
description: `Exposed sensitive path discovered: ${fuzzUrl}. This path reveals internal configurations or credentials.`,
|
|
240
|
-
cwe: 'CWE-200'
|
|
241
|
-
});
|
|
242
|
-
} else if (fuzzRes.status === 403) {
|
|
243
|
-
vulnerabilities.push({
|
|
244
|
-
id: `REM-FUZZ-${Date.now()}-${path.replace(/\//g, '-')}`,
|
|
245
|
-
type: 'Potential Sensitive Path',
|
|
246
|
-
severity: 'Low',
|
|
247
|
-
description: `Path discovered but access forbidden (403): ${fuzzUrl}. Might indicate internal structure exposure.`,
|
|
248
|
-
cwe: 'CWE-204'
|
|
249
|
-
});
|
|
308
|
+
const finding = await validateFuzzerFinding(path, fuzzRes, fuzzUrl);
|
|
309
|
+
if (finding) {
|
|
310
|
+
vulnerabilities.push(finding);
|
|
250
311
|
}
|
|
251
|
-
} catch (e) {
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// Silently skip if path doesn't exist or times out
|
|
314
|
+
}
|
|
252
315
|
}
|
|
253
316
|
|
|
254
317
|
// --- OFFENSIVE PARAMETER FUZZING ---
|
|
255
318
|
const injectionPayloads = [
|
|
256
|
-
{ type: 'SQLi', param: "' OR 1=1
|
|
257
|
-
{ type: '
|
|
258
|
-
{ type: '
|
|
319
|
+
{ type: 'SQLi', param: "' OR '1'='1", severity: 'Critical', cwe: 'CWE-89' },
|
|
320
|
+
{ type: 'SQLi', param: '" OR "1"="1', severity: 'Critical', cwe: 'CWE-89' },
|
|
321
|
+
{ type: 'XSS', param: "<script>alert('OMEN')</script>", severity: 'High', cwe: 'CWE-79' },
|
|
322
|
+
{ type: 'XSS', param: "javascript:alert('OMEN')", severity: 'High', cwe: 'CWE-79' },
|
|
323
|
+
{ type: 'LFI', param: "../../../../../etc/passwd", severity: 'Critical', cwe: 'CWE-22' }
|
|
259
324
|
];
|
|
260
325
|
|
|
261
326
|
// Combine params from links and forms
|
|
262
327
|
const allParams = Array.from(discoveredParams);
|
|
263
328
|
discoveredForms.forEach(f => f.inputs.forEach(i => allParams.push(i)));
|
|
264
329
|
|
|
265
|
-
const uniqueParams = [...new Set(allParams)].slice(0,
|
|
330
|
+
const uniqueParams = [...new Set(allParams)].slice(0, 10); // Fuzz top 10 params
|
|
266
331
|
|
|
267
332
|
for (const param of uniqueParams) {
|
|
268
333
|
for (const payload of injectionPayloads) {
|
|
@@ -270,26 +335,68 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
270
335
|
const testUrl = new URL(targetUrl);
|
|
271
336
|
testUrl.searchParams.append(param, payload.param);
|
|
272
337
|
|
|
273
|
-
const res = await axios.get(testUrl.href, {
|
|
338
|
+
const res = await axios.get(testUrl.href, {
|
|
339
|
+
timeout: 5000,
|
|
340
|
+
validateStatus: () => true,
|
|
341
|
+
headers: { 'User-Agent': 'OMEN-SEC-CLI/1.0.17 (Security Audit)' }
|
|
342
|
+
});
|
|
274
343
|
|
|
344
|
+
const evidence = {
|
|
345
|
+
request: { url: testUrl.href, method: 'GET', parameter: param, payload: payload.param },
|
|
346
|
+
response: { status: res.status, body_snippet: typeof res.data === 'string' ? res.data.substring(0, 200) : '' }
|
|
347
|
+
};
|
|
348
|
+
|
|
275
349
|
if (payload.type === 'XSS' && typeof res.data === 'string' && res.data.includes(payload.param)) {
|
|
276
350
|
vulnerabilities.push({
|
|
277
|
-
id: `REM-INJ-${Date.now()}-XSS`,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
351
|
+
id: `REM-INJ-${Date.now()}-XSS-${param}`,
|
|
352
|
+
kind: 'injection',
|
|
353
|
+
category: 'confirmed',
|
|
354
|
+
confidence: 'high',
|
|
355
|
+
severity: payload.severity,
|
|
356
|
+
title: 'Reflected Cross-Site Scripting (XSS)',
|
|
357
|
+
description: `Confirmed reflected XSS at ${targetUrl} via parameter '${param}'. The payload was reflected verbatim in the response body.`,
|
|
358
|
+
cwe: payload.cwe,
|
|
359
|
+
evidence,
|
|
360
|
+
remediation: 'Sanitize all user inputs before reflecting them in the HTML. Use output encoding libraries.'
|
|
282
361
|
});
|
|
283
362
|
}
|
|
284
363
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
364
|
+
const sqliPatterns = [
|
|
365
|
+
/SQL syntax/i, /mysql_fetch/i, /PostgreSQL.*ERROR/i, /Oracle.*Error/i,
|
|
366
|
+
/SQLite3::/i, /Dynamic SQL/i, /Syntax error.*in query/i
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
if (payload.type === 'SQLi') {
|
|
370
|
+
const hasErrorPattern = typeof res.data === 'string' && sqliPatterns.some(p => p.test(res.data));
|
|
371
|
+
if (hasErrorPattern || res.status === 500) {
|
|
372
|
+
vulnerabilities.push({
|
|
373
|
+
id: `REM-INJ-${Date.now()}-SQLI-${param}`,
|
|
374
|
+
kind: 'injection',
|
|
375
|
+
category: 'probable',
|
|
376
|
+
confidence: hasErrorPattern ? 'high' : 'medium',
|
|
377
|
+
severity: payload.severity,
|
|
378
|
+
title: 'Potential SQL Injection',
|
|
379
|
+
description: `Potential SQLi detected via parameter '${param}'. The server returned a 500 error or a known SQL error pattern.`,
|
|
380
|
+
cwe: payload.cwe,
|
|
381
|
+
evidence,
|
|
382
|
+
remediation: 'Use parameterized queries or an ORM. Never concatenate user input directly into SQL strings.'
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (payload.type === 'LFI' && typeof res.data === 'string' && (res.data.includes('root:x:0:0') || res.data.includes('[boot loader]'))) {
|
|
388
|
+
vulnerabilities.push({
|
|
389
|
+
id: `REM-INJ-${Date.now()}-LFI-${param}`,
|
|
390
|
+
kind: 'injection',
|
|
391
|
+
category: 'confirmed',
|
|
392
|
+
confidence: 'high',
|
|
393
|
+
severity: payload.severity,
|
|
394
|
+
title: 'Local File Inclusion (LFI)',
|
|
395
|
+
description: `Confirmed LFI at ${targetUrl} via parameter '${param}'. System file content was detected in the response.`,
|
|
396
|
+
cwe: payload.cwe,
|
|
397
|
+
evidence,
|
|
398
|
+
remediation: 'Validate file paths against a whitelist and avoid using user-supplied input for file operations.'
|
|
399
|
+
});
|
|
293
400
|
}
|
|
294
401
|
} catch (e) {}
|
|
295
402
|
}
|
|
@@ -335,22 +442,28 @@ async function validateFuzzerFinding(path, response, url) {
|
|
|
335
442
|
if (typeof data === 'string' && !data.trim().startsWith('<html')) {
|
|
336
443
|
return {
|
|
337
444
|
id: `REM-CONFIRMED-FILE-${Date.now()}`,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
445
|
+
kind: 'content',
|
|
446
|
+
category: 'confirmed',
|
|
447
|
+
confidence: 'high',
|
|
448
|
+
severity: 'critical',
|
|
449
|
+
title: 'Sensitive File Exposed',
|
|
341
450
|
description: `CRITICAL: Sensitive file exposed at ${url}. Contents contain raw configuration data.`,
|
|
342
451
|
cwe: 'CWE-538',
|
|
343
|
-
evidence: { ...evidence, reason: 'Raw sensitive file content detected (Non-HTML response on sensitive path)' }
|
|
452
|
+
evidence: { ...evidence, reason: 'Raw sensitive file content detected (Non-HTML response on sensitive path)' },
|
|
453
|
+
remediation: 'Immediately remove the file from the web server and ensure sensitive files are not publicly accessible.'
|
|
344
454
|
};
|
|
345
455
|
} else {
|
|
346
456
|
return {
|
|
347
457
|
id: `REM-POTENTIAL-FILE-${Date.now()}`,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
458
|
+
kind: 'path',
|
|
459
|
+
category: 'informational',
|
|
460
|
+
confidence: 'low',
|
|
461
|
+
severity: 'info',
|
|
462
|
+
title: 'Potential Sensitive Path Discovered',
|
|
351
463
|
description: `Potential sensitive path found at ${url}, but returned HTML content. Likely a redirect or custom error page.`,
|
|
352
464
|
cwe: 'CWE-200',
|
|
353
|
-
evidence: { ...evidence, reason: 'HTML response on sensitive file path' }
|
|
465
|
+
evidence: { ...evidence, reason: 'HTML response on sensitive file path' },
|
|
466
|
+
remediation: 'Verify if this path should be accessible and ensure no sensitive information is leaked through custom error pages.'
|
|
354
467
|
};
|
|
355
468
|
}
|
|
356
469
|
}
|
|
@@ -361,23 +474,29 @@ async function validateFuzzerFinding(path, response, url) {
|
|
|
361
474
|
if (typeof data === 'string' && /password|login/i.test(data)) {
|
|
362
475
|
return {
|
|
363
476
|
id: `REM-INFO-LOGIN-${Date.now()}`,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
477
|
+
kind: 'path',
|
|
478
|
+
category: 'informational',
|
|
479
|
+
confidence: 'high',
|
|
480
|
+
severity: 'info',
|
|
481
|
+
title: 'Admin Login Page Discovered',
|
|
367
482
|
description: `Admin login page discovered at ${url}.`,
|
|
368
483
|
cwe: 'CWE-200',
|
|
369
|
-
evidence: { ...evidence, reason: '200 OK with login/password patterns detected in HTML.' }
|
|
484
|
+
evidence: { ...evidence, reason: '200 OK with login/password patterns detected in HTML.' },
|
|
485
|
+
remediation: 'Ensure the admin login page is protected by strong authentication and not easily discoverable if not necessary.'
|
|
370
486
|
};
|
|
371
487
|
}
|
|
372
488
|
// If it's a 200 but not a login page, it could be an exposed panel
|
|
373
489
|
return {
|
|
374
490
|
id: `REM-PROBABLE-PANEL-${Date.now()}`,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
491
|
+
kind: 'path',
|
|
492
|
+
category: 'probable',
|
|
493
|
+
confidence: 'low',
|
|
494
|
+
severity: 'medium',
|
|
495
|
+
title: 'Potential Admin Panel Exposure',
|
|
378
496
|
description: `Potential exposed admin panel or dashboard at ${url}. Manual verification required.`,
|
|
379
497
|
cwe: 'CWE-284',
|
|
380
|
-
evidence: { ...evidence, reason: '200 OK on admin-like path, but no explicit login form detected. Could be an unauthorized dashboard.' }
|
|
498
|
+
evidence: { ...evidence, reason: '200 OK on admin-like path, but no explicit login form detected. Could be an unauthorized dashboard.' },
|
|
499
|
+
remediation: 'Restrict access to the admin panel using IP whitelisting or other access control mechanisms.'
|
|
381
500
|
};
|
|
382
501
|
}
|
|
383
502
|
|
|
@@ -385,15 +504,18 @@ async function validateFuzzerFinding(path, response, url) {
|
|
|
385
504
|
if (status === 403) {
|
|
386
505
|
return {
|
|
387
506
|
id: `REM-INFO-FORBIDDEN-${Date.now()}`,
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
507
|
+
kind: 'path',
|
|
508
|
+
category: 'informational',
|
|
509
|
+
confidence: 'medium',
|
|
510
|
+
severity: 'info',
|
|
511
|
+
title: 'Protected Path Discovered',
|
|
391
512
|
description: `Potential protected path discovered (403 Forbidden): ${url}. This confirms the path exists but access is restricted.`,
|
|
392
513
|
cwe: 'CWE-204',
|
|
393
514
|
evidence: {
|
|
394
515
|
...evidence,
|
|
395
516
|
reason: 'Server returned 403 Forbidden. This confirms path existence but access control is active. No immediate exposure detected.'
|
|
396
|
-
}
|
|
517
|
+
},
|
|
518
|
+
remediation: 'None required, but ensure that the 403 response does not leak information about the internal structure.'
|
|
397
519
|
};
|
|
398
520
|
}
|
|
399
521
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function generateFixPlan(scanData) {
|
|
2
|
+
let md = `# 🛡️ OMEN Security Fix Plan: ${scanData.target || 'Local Project'}\n\n`;
|
|
3
|
+
md += `> **Data do Scan:** ${scanData.timestamp || new Date().toLocaleString()} \n`;
|
|
4
|
+
md += `> **Score Atual:** ${scanData.score || 0}/100 \n`;
|
|
5
|
+
md += `> **Vulnerabilidades:** ${scanData.vulnerabilities?.length || 0}\n\n`;
|
|
6
|
+
|
|
7
|
+
md += `Este documento é um guia passo-a-passo para remediar as falhas encontradas. Após aplicar as correções, execute \`omen verify\` para confirmar a resolução.\n\n`;
|
|
8
|
+
|
|
9
|
+
const vulnerabilities = scanData.vulnerabilities || [];
|
|
10
|
+
const prioritized = [...vulnerabilities].sort((a, b) => {
|
|
11
|
+
const weights = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
|
12
|
+
return (weights[b.severity] || 0) - (weights[a.severity] || 0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
md += `## 🚨 Checklist de Remediação\n\n`;
|
|
16
|
+
|
|
17
|
+
if (prioritized.length === 0) {
|
|
18
|
+
md += `✅ Nenhuma vulnerabilidade encontrada que exija ação imediata.\n`;
|
|
19
|
+
} else {
|
|
20
|
+
prioritized.forEach((v, index) => {
|
|
21
|
+
const severityEmoji = v.severity === 'critical' ? '🔴' : v.severity === 'high' ? '🟠' : v.severity === 'medium' ? '🟡' : '🔵';
|
|
22
|
+
|
|
23
|
+
md += `### [ ] ${severityEmoji} [${v.severity.toUpperCase()}] ${v.title || v.description}\n`;
|
|
24
|
+
md += `- **ID:** \`${v.id}\` \n`;
|
|
25
|
+
md += `- **CWE:** ${v.cwe || 'N/A'} \n`;
|
|
26
|
+
md += `- **Confiança:** ${v.confidence || 'medium'} \n\n`;
|
|
27
|
+
|
|
28
|
+
if (v.evidence) {
|
|
29
|
+
md += `#### 🔍 Evidência\n`;
|
|
30
|
+
if (v.evidence.file) md += `- **Arquivo:** \`${v.evidence.file}\`${v.evidence.line ? ` (Linha ${v.evidence.line})` : ''} \n`;
|
|
31
|
+
if (v.evidence.code) md += `\`\`\`javascript\n${v.evidence.code}\n\`\`\`\n`;
|
|
32
|
+
if (v.evidence.finding) md += `- **Detalhe:** ${v.evidence.finding} \n`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
md += `#### 🛠️ Estratégia de Correção\n`;
|
|
36
|
+
md += `${v.remediation || 'Consulte a documentação de segurança para mitigar este risco.'}\n\n`;
|
|
37
|
+
|
|
38
|
+
md += `#### ✅ Comando de Verificação\n`;
|
|
39
|
+
md += `\`npx omen-sec-cli verify --id ${v.id}\` (ou apenas \`omen verify\` para checar tudo)\n\n`;
|
|
40
|
+
md += `---\n\n`;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
md += `\n*Gerado automaticamente pelo OMEN SEC-CLI v1.0.17 - Protocolo Zero-Copy AI Ativo*\n`;
|
|
45
|
+
return md;
|
|
46
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function generateMarkdownReport(scanData) {
|
|
2
|
+
let md = `# OMEN Audit Report: ${scanData.target}\n\n`;
|
|
3
|
+
md += `**Scan ID:** \`${scanData.scan_id}\` \n`;
|
|
4
|
+
md += `**Timestamp:** ${scanData.timestamp} \n`;
|
|
5
|
+
md += `**Security Score:** ${scanData.score}/100 \n`;
|
|
6
|
+
md += `**Risk Level:** ${scanData.riskLevel}\n\n`;
|
|
7
|
+
|
|
8
|
+
md += `## Executive Summary\n`;
|
|
9
|
+
md += `The security audit identified ${scanData.vulnerabilities.length} total issues. `;
|
|
10
|
+
const confirmed = scanData.vulnerabilities.filter(v => v.category === 'confirmed').length;
|
|
11
|
+
md += `Of these, ${confirmed} are confirmed vulnerabilities that require immediate attention.\n\n`;
|
|
12
|
+
|
|
13
|
+
md += `## Detailed Findings\n\n`;
|
|
14
|
+
scanData.vulnerabilities.forEach(v => {
|
|
15
|
+
md += `### [${v.severity.toUpperCase()}] ${v.title || v.description}\n`;
|
|
16
|
+
md += `- **Category:** ${v.category}\n`;
|
|
17
|
+
md += `- **Confidence:** ${v.confidence}\n`;
|
|
18
|
+
md += `- **CWE:** ${v.cwe}\n\n`;
|
|
19
|
+
md += `#### Description\n${v.description}\n\n`;
|
|
20
|
+
md += `#### Remediation\n${v.remediation || 'Follow security best practices.'}\n\n`;
|
|
21
|
+
md += `---\n\n`;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return md;
|
|
25
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
|
|
5
|
+
export async function bootLocalApp(command, port = 3000, timeout = 30000) {
|
|
6
|
+
if (!command) return { success: false, error: 'No boot strategy found' };
|
|
7
|
+
|
|
8
|
+
console.log(chalk.cyan(`[Runner] Starting local app with: ${command}...`));
|
|
9
|
+
|
|
10
|
+
const [cmd, ...args] = command.split(' ');
|
|
11
|
+
const subprocess = execa(cmd, args, {
|
|
12
|
+
cleanup: true,
|
|
13
|
+
all: true
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
let bootLogs = '';
|
|
17
|
+
subprocess.all.on('data', (chunk) => {
|
|
18
|
+
bootLogs += chunk.toString();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Wait for healthcheck or timeout
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
while (Date.now() - start < timeout) {
|
|
24
|
+
try {
|
|
25
|
+
const res = await axios.get(`http://localhost:${port}`, { timeout: 1000, validateStatus: () => true });
|
|
26
|
+
if (res.status < 500) {
|
|
27
|
+
console.log(chalk.green(`[Runner] App is up and running on port ${port}!`));
|
|
28
|
+
return { success: true, subprocess, logs: bootLogs, port };
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Not up yet
|
|
32
|
+
}
|
|
33
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If we reach here, boot failed or timed out
|
|
37
|
+
subprocess.kill();
|
|
38
|
+
return { success: false, error: 'App failed to start or healthcheck timed out', logs: bootLogs };
|
|
39
|
+
}
|
package/core/scanner.js
CHANGED
|
@@ -48,39 +48,49 @@ export async function runScannerSteps(target, flags) {
|
|
|
48
48
|
spinner.succeed(`[${i + 1}/${steps.length}] ${step.text}`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
const scanData = {
|
|
52
|
+
schema_version: "1.0",
|
|
53
|
+
target: target,
|
|
54
|
+
scan_id: `OMEN-${Date.now()}`,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
score: 100,
|
|
57
|
+
riskLevel: 'Low',
|
|
58
|
+
attack_surface: {
|
|
59
|
+
endpoints: attack_surface.endpoints || [],
|
|
60
|
+
parameters: attack_surface.parameters || [],
|
|
61
|
+
forms: attack_surface.forms || [],
|
|
62
|
+
tech_stack: attack_surface.tech_stack || []
|
|
63
|
+
},
|
|
64
|
+
vulnerabilities: allVulnerabilities
|
|
65
|
+
};
|
|
66
|
+
|
|
51
67
|
// Calculate dynamic score based on confidence and severity
|
|
52
|
-
|
|
53
|
-
const penalties = allVulnerabilities.reduce((acc, v) => {
|
|
68
|
+
let penalties = scanData.vulnerabilities.reduce((acc, v) => {
|
|
54
69
|
// Only penalize for Confirmed or Probable issues
|
|
55
|
-
if (v.category !== '
|
|
70
|
+
if (v.category !== 'confirmed' && v.category !== 'probable') return acc;
|
|
56
71
|
|
|
57
72
|
let severityWeight = 0;
|
|
58
|
-
if (v.severity === '
|
|
59
|
-
else if (v.severity === '
|
|
60
|
-
else if (v.severity === '
|
|
61
|
-
else if (v.severity === '
|
|
73
|
+
if (v.severity === 'critical') severityWeight = 25;
|
|
74
|
+
else if (v.severity === 'high') severityWeight = 15;
|
|
75
|
+
else if (v.severity === 'medium') severityWeight = 10;
|
|
76
|
+
else if (v.severity === 'low') severityWeight = 5;
|
|
77
|
+
|
|
78
|
+
// Reduzir peso de achados baseados em caminhos (potential/enumeration)
|
|
79
|
+
if (v.kind === 'path' && v.category === 'probable') severityWeight *= 0.5;
|
|
62
80
|
|
|
63
81
|
let confidenceMultiplier = 1;
|
|
64
|
-
if (v.confidence === '
|
|
65
|
-
if (v.confidence === '
|
|
82
|
+
if (v.confidence === 'medium') confidenceMultiplier = 0.6;
|
|
83
|
+
if (v.confidence === 'low') confidenceMultiplier = 0.3;
|
|
66
84
|
|
|
67
85
|
return acc + (severityWeight * confidenceMultiplier);
|
|
68
86
|
}, 0);
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
let riskLevel = 'Low';
|
|
72
|
-
if (finalScore < 50) riskLevel = 'Critical';
|
|
73
|
-
else if (finalScore < 70) riskLevel = 'High';
|
|
74
|
-
else if (finalScore < 90) riskLevel = 'Medium';
|
|
88
|
+
scanData.score = Math.max(0, Math.round(100 - penalties));
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
attack_surface,
|
|
83
|
-
headers_analysis,
|
|
84
|
-
vulnerabilities: allVulnerabilities.length > 0 ? allVulnerabilities : [{ id: 'INFO', type: 'Clean', severity: 'Info', description: 'No immediate vulnerabilities detected in the surface scan.' }]
|
|
85
|
-
};
|
|
90
|
+
if (scanData.score < 40) scanData.riskLevel = 'Critical';
|
|
91
|
+
else if (scanData.score < 60) scanData.riskLevel = 'High';
|
|
92
|
+
else if (scanData.score < 80) scanData.riskLevel = 'Medium';
|
|
93
|
+
else scanData.riskLevel = 'Low';
|
|
94
|
+
|
|
95
|
+
return scanData;
|
|
86
96
|
}
|