omen-sec-cli 1.0.17 → 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 +29 -23
- package/core/local-scanner.js +94 -7
- package/core/remote-scanner.js +130 -51
- 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/state/state-manager.js +43 -0
- package/core/ui-server.js +85 -25
- package/package.json +3 -1
package/core/local-scanner.js
CHANGED
|
@@ -109,22 +109,109 @@ export async function scanLocalProject() {
|
|
|
109
109
|
filesScanned.push(path.basename(file));
|
|
110
110
|
|
|
111
111
|
lines.forEach((line, index) => {
|
|
112
|
-
// Regra 1: Hardcoded Secrets
|
|
113
|
-
|
|
112
|
+
// Regra 1: Hardcoded Secrets (Improved)
|
|
113
|
+
const secretRegex = /(api_key|apikey|secret|password|token|jwt_secret|connection_string|private_key|aws_key|aws_secret|db_pass)\s*[:=]\s*['"][a-zA-Z0-9_\-\.\/\+\=]{12,}['"]/i;
|
|
114
|
+
if (secretRegex.test(line)) {
|
|
114
115
|
vulnerabilities.push({
|
|
115
|
-
id: `LOC-
|
|
116
|
+
id: `LOC-SEC-${Date.now()}-${index}`,
|
|
116
117
|
kind: 'content',
|
|
117
118
|
category: 'confirmed',
|
|
118
|
-
confidence: '
|
|
119
|
+
confidence: 'high',
|
|
119
120
|
severity: 'critical',
|
|
120
|
-
title: 'Hardcoded Secret
|
|
121
|
-
description: `
|
|
121
|
+
title: 'Exposed Hardcoded Secret',
|
|
122
|
+
description: `A potential sensitive credential or token was found hardcoded in ${path.basename(file)} at line ${index + 1}.`,
|
|
122
123
|
cwe: 'CWE-798',
|
|
123
124
|
evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
|
|
124
|
-
remediation: '
|
|
125
|
+
remediation: 'Immediately rotate this secret and move it to an environment variable or a secret manager vault.'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Regra 10: Insecure CORS configuration
|
|
130
|
+
if (/Access-Control-Allow-Origin.*\*['"]/i.test(line)) {
|
|
131
|
+
vulnerabilities.push({
|
|
132
|
+
id: `LOC-CORS-${Date.now()}-${index}`,
|
|
133
|
+
kind: 'content',
|
|
134
|
+
category: 'hardening',
|
|
135
|
+
confidence: 'high',
|
|
136
|
+
severity: 'medium',
|
|
137
|
+
title: 'Overly Permissive CORS Policy',
|
|
138
|
+
description: `CORS policy allows all origins (*) in ${path.basename(file)} at line ${index + 1}. This can lead to CSRF or data leakage.`,
|
|
139
|
+
cwe: 'CWE-942',
|
|
140
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
|
|
141
|
+
remediation: 'Restrict Access-Control-Allow-Origin to specific, trusted domains instead of using "*".'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Regra 11: Insecure Cookie Attributes
|
|
146
|
+
if (/res\.cookie\(.*\{/i.test(line) && !/httpOnly:\s*true/i.test(line)) {
|
|
147
|
+
vulnerabilities.push({
|
|
148
|
+
id: `LOC-COOKIE-${Date.now()}-${index}`,
|
|
149
|
+
kind: 'content',
|
|
150
|
+
category: 'hardening',
|
|
151
|
+
confidence: 'medium',
|
|
152
|
+
severity: 'low',
|
|
153
|
+
title: 'Insecure Cookie (Missing httpOnly)',
|
|
154
|
+
description: `Cookie created without 'httpOnly' flag in ${path.basename(file)} at line ${index + 1}. This makes the cookie accessible via client-side scripts (XSS risk).`,
|
|
155
|
+
cwe: 'CWE-1004',
|
|
156
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
|
|
157
|
+
remediation: 'Always set { httpOnly: true } when creating sensitive cookies to prevent XSS-based theft.'
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Regra 12: Insecure Randomness
|
|
162
|
+
if (/Math\.random\(\)/.test(line)) {
|
|
163
|
+
vulnerabilities.push({
|
|
164
|
+
id: `LOC-RAND-${Date.now()}-${index}`,
|
|
165
|
+
kind: 'content',
|
|
166
|
+
category: 'hardening',
|
|
167
|
+
confidence: 'medium',
|
|
168
|
+
severity: 'low',
|
|
169
|
+
title: 'Insecure Randomness',
|
|
170
|
+
description: `Math.random() used in ${path.basename(file)} at line ${index + 1}. This is not cryptographically secure.`,
|
|
171
|
+
cwe: 'CWE-330',
|
|
172
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
|
|
173
|
+
remediation: 'Use crypto.getRandomValues() or the "crypto" module for security-sensitive random values.'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Regra 13: Potential ReDoS (Regex Denial of Service)
|
|
178
|
+
if (/\/\(.*\+\)\+\//.test(line) || /\/\(.*\*\)\*\//.test(line)) {
|
|
179
|
+
vulnerabilities.push({
|
|
180
|
+
id: `LOC-REDOS-${Date.now()}-${index}`,
|
|
181
|
+
kind: 'content',
|
|
182
|
+
category: 'probable',
|
|
183
|
+
confidence: 'medium',
|
|
184
|
+
severity: 'medium',
|
|
185
|
+
title: 'Potential ReDoS Pattern',
|
|
186
|
+
description: `A potentially catastrophic backtracking regex pattern was found in ${path.basename(file)} at line ${index + 1}.`,
|
|
187
|
+
cwe: 'CWE-1333',
|
|
188
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
|
|
189
|
+
remediation: 'Review and simplify the regex pattern to avoid nested quantifiers that cause exponential backtracking.'
|
|
125
190
|
});
|
|
126
191
|
}
|
|
127
192
|
|
|
193
|
+
// Regra 8: Weak JWT Configuration
|
|
194
|
+
if (/jwt\.sign\(.*,\s*['"][^'"]{1,10}['"]\s*,/i.test(line)) {
|
|
195
|
+
vulnerabilities.push({
|
|
196
|
+
id: `LOC-AUTH-${Date.now()}`,
|
|
197
|
+
kind: 'content',
|
|
198
|
+
category: 'confirmed',
|
|
199
|
+
confidence: 'high',
|
|
200
|
+
severity: 'high',
|
|
201
|
+
title: 'Weak JWT Secret detected',
|
|
202
|
+
description: `The JWT signing secret appears to be a short, hardcoded string in ${path.basename(file)}: line ${index + 1}`,
|
|
203
|
+
cwe: 'CWE-522',
|
|
204
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
|
|
205
|
+
remediation: 'Use a strong, randomly generated secret stored in environment variables.'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Regra 9: Missing CSRF Protection (Express example)
|
|
210
|
+
if (discoveryData && discoveryData.stack === 'Node/Express' && path.basename(file) === 'app.js') {
|
|
211
|
+
// This would require multi-line or AST, but let's do a simple check
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
128
215
|
// Regra 2: Uso de Eval
|
|
129
216
|
if (/eval\s*\(/.test(line)) {
|
|
130
217
|
vulnerabilities.push({
|
package/core/remote-scanner.js
CHANGED
|
@@ -109,8 +109,25 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
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.'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
// 4. Server Header Leak
|
|
113
|
-
if (headers['server']) {
|
|
130
|
+
if (headers['server'] && !headers['server'].includes('Cloudflare') && !headers['server'].includes('Vercel')) {
|
|
114
131
|
headers_analysis["Server"] = headers['server'];
|
|
115
132
|
vulnerabilities.push({
|
|
116
133
|
id: `REM-VULN-${Date.now()}-5`,
|
|
@@ -126,22 +143,51 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
126
143
|
});
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
//
|
|
130
|
-
if (headers['
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
}
|
|
143
163
|
}
|
|
144
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
|
+
|
|
145
191
|
// --- SPIDER / CRAWLER (DEEP) ---
|
|
146
192
|
if (typeof html === 'string') {
|
|
147
193
|
const $ = cheerio.load(html);
|
|
@@ -256,41 +302,32 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
256
302
|
const fuzzUrl = new URL(path, targetUrl).href;
|
|
257
303
|
const fuzzRes = await axios.get(fuzzUrl, {
|
|
258
304
|
timeout: 5000,
|
|
259
|
-
validateStatus: (status) => status
|
|
305
|
+
validateStatus: (status) => status >= 200 && status < 500
|
|
260
306
|
});
|
|
261
307
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
type: 'Sensitive Path Exposed',
|
|
266
|
-
severity: path.includes('.env') || path.includes('.git') || path.includes('.ssh') ? 'Critical' : 'High',
|
|
267
|
-
description: `Exposed sensitive path discovered: ${fuzzUrl}. This path reveals internal configurations or credentials.`,
|
|
268
|
-
cwe: 'CWE-200'
|
|
269
|
-
});
|
|
270
|
-
} else if (fuzzRes.status === 403) {
|
|
271
|
-
vulnerabilities.push({
|
|
272
|
-
id: `REM-FUZZ-${Date.now()}-${path.replace(/\//g, '-')}`,
|
|
273
|
-
type: 'Potential Sensitive Path',
|
|
274
|
-
severity: 'Low',
|
|
275
|
-
description: `Path discovered but access forbidden (403): ${fuzzUrl}. Might indicate internal structure exposure.`,
|
|
276
|
-
cwe: 'CWE-204'
|
|
277
|
-
});
|
|
308
|
+
const finding = await validateFuzzerFinding(path, fuzzRes, fuzzUrl);
|
|
309
|
+
if (finding) {
|
|
310
|
+
vulnerabilities.push(finding);
|
|
278
311
|
}
|
|
279
|
-
} catch (e) {
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// Silently skip if path doesn't exist or times out
|
|
314
|
+
}
|
|
280
315
|
}
|
|
281
316
|
|
|
282
317
|
// --- OFFENSIVE PARAMETER FUZZING ---
|
|
283
318
|
const injectionPayloads = [
|
|
284
|
-
{ type: 'SQLi', param: "' OR 1=1
|
|
285
|
-
{ type: '
|
|
286
|
-
{ 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' }
|
|
287
324
|
];
|
|
288
325
|
|
|
289
326
|
// Combine params from links and forms
|
|
290
327
|
const allParams = Array.from(discoveredParams);
|
|
291
328
|
discoveredForms.forEach(f => f.inputs.forEach(i => allParams.push(i)));
|
|
292
329
|
|
|
293
|
-
const uniqueParams = [...new Set(allParams)].slice(0,
|
|
330
|
+
const uniqueParams = [...new Set(allParams)].slice(0, 10); // Fuzz top 10 params
|
|
294
331
|
|
|
295
332
|
for (const param of uniqueParams) {
|
|
296
333
|
for (const payload of injectionPayloads) {
|
|
@@ -298,26 +335,68 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
298
335
|
const testUrl = new URL(targetUrl);
|
|
299
336
|
testUrl.searchParams.append(param, payload.param);
|
|
300
337
|
|
|
301
|
-
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
|
+
});
|
|
302
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
|
+
|
|
303
349
|
if (payload.type === 'XSS' && typeof res.data === 'string' && res.data.includes(payload.param)) {
|
|
304
350
|
vulnerabilities.push({
|
|
305
|
-
id: `REM-INJ-${Date.now()}-XSS`,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.'
|
|
310
361
|
});
|
|
311
362
|
}
|
|
312
363
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
+
});
|
|
321
400
|
}
|
|
322
401
|
} catch (e) {}
|
|
323
402
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const STATE_FILE = path.join(process.cwd(), '.omen', 'context.json');
|
|
5
|
+
|
|
6
|
+
export async function saveState(data) {
|
|
7
|
+
try {
|
|
8
|
+
await fs.mkdir(path.dirname(STATE_FILE), { recursive: true });
|
|
9
|
+
const currentState = await loadState();
|
|
10
|
+
const newState = { ...currentState, ...data, last_updated: new Date().toISOString() };
|
|
11
|
+
await fs.writeFile(STATE_FILE, JSON.stringify(newState, null, 2));
|
|
12
|
+
return newState;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error('Failed to save OMEN state:', err.message);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function loadState() {
|
|
20
|
+
try {
|
|
21
|
+
const data = await fs.readFile(STATE_FILE, 'utf-8');
|
|
22
|
+
return JSON.parse(data);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return {
|
|
25
|
+
schema_version: "1.0",
|
|
26
|
+
project: {},
|
|
27
|
+
discovery: {},
|
|
28
|
+
plan: {},
|
|
29
|
+
execution: {
|
|
30
|
+
static: [],
|
|
31
|
+
dynamic: [],
|
|
32
|
+
vulnerabilities: []
|
|
33
|
+
},
|
|
34
|
+
verification: {}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function clearState() {
|
|
40
|
+
try {
|
|
41
|
+
await fs.unlink(STATE_FILE);
|
|
42
|
+
} catch (err) {}
|
|
43
|
+
}
|