omen-sec-cli 1.0.12 → 1.0.15
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/bin/index.js +1 -1
- package/core/ai-protocol.js +22 -1
- package/core/diff-engine.js +35 -0
- package/core/engine.js +18 -1
- package/core/generator.js +12 -3
- package/core/local-scanner.js +31 -16
- package/core/remote-scanner.js +171 -15
- package/core/scanner.js +24 -14
- package/core/ui-server.js +91 -45
- package/package.json +1 -1
- package/ui/banner.js +1 -1
package/bin/index.js
CHANGED
package/core/ai-protocol.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
export function getMassiveAIProtocol(scanData) {
|
|
2
|
-
|
|
2
|
+
const summary = `
|
|
3
|
+
================================================================================
|
|
4
|
+
OMEN SEC-CLI: TARGET INTELLIGENCE SUMMARY
|
|
5
|
+
================================================================================
|
|
6
|
+
Target: ${scanData.target}
|
|
7
|
+
Scan Date: ${new Date().toISOString()}
|
|
8
|
+
Risk Level: ${scanData.riskLevel} (${scanData.score}/100)
|
|
9
|
+
|
|
10
|
+
--- DETECTED SURFACE ---
|
|
11
|
+
Endpoints Discovered: ${scanData.attack_surface.endpoints.length}
|
|
12
|
+
Forms Found: ${scanData.attack_surface.forms.length}
|
|
13
|
+
Tech Stack: ${(scanData.attack_surface.tech_stack || []).join(', ')}
|
|
14
|
+
|
|
15
|
+
--- TOP VULNERABILITIES ---
|
|
16
|
+
${scanData.vulnerabilities.slice(0, 5).map(v => `[${v.severity}] ${v.description} (Category: ${v.category})`).join('\n')}
|
|
17
|
+
|
|
18
|
+
================================================================================
|
|
19
|
+
FULL SCAN DATA ATTACHED BELOW
|
|
20
|
+
================================================================================
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
return summary + `
|
|
3
24
|
================================================================================
|
|
4
25
|
[OMEN_AI_PROTOCOL_V2_MAXIMUM_OVERRIDE]
|
|
5
26
|
================================================================================
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function compareWithPreviousScan() {
|
|
5
|
+
const historyDir = path.join(process.cwd(), '.omen', 'history');
|
|
6
|
+
try {
|
|
7
|
+
const files = await fs.readdir(historyDir);
|
|
8
|
+
if (files.length < 2) return null;
|
|
9
|
+
|
|
10
|
+
// Sort files by modification time to get the last two
|
|
11
|
+
const sortedFiles = files.map(async file => {
|
|
12
|
+
const stats = await fs.stat(path.join(historyDir, file));
|
|
13
|
+
return { name: file, mtime: stats.mtime };
|
|
14
|
+
});
|
|
15
|
+
const promisedFiles = await Promise.all(sortedFiles);
|
|
16
|
+
promisedFiles.sort((a, b) => b.mtime - a.mtime);
|
|
17
|
+
|
|
18
|
+
const [currentFile, previousFile] = promisedFiles;
|
|
19
|
+
|
|
20
|
+
const currentData = JSON.parse(await fs.readFile(path.join(historyDir, currentFile.name), 'utf-8'));
|
|
21
|
+
const previousData = JSON.parse(await fs.readFile(path.join(historyDir, previousFile.name), 'utf-8'));
|
|
22
|
+
|
|
23
|
+
const currentVulnIds = new Set(currentData.vulnerabilities.map(v => v.description)); // Use description for simple diff
|
|
24
|
+
const previousVulnIds = new Set(previousData.vulnerabilities.map(v => v.description));
|
|
25
|
+
|
|
26
|
+
const newVulnerabilities = currentData.vulnerabilities.filter(v => !previousVulnIds.has(v.description));
|
|
27
|
+
const fixedVulnerabilities = previousData.vulnerabilities.filter(v => !currentVulnIds.has(v.description));
|
|
28
|
+
const scoreDiff = currentData.score - previousData.score;
|
|
29
|
+
|
|
30
|
+
return { newVulnerabilities, fixedVulnerabilities, scoreDiff };
|
|
31
|
+
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/core/engine.js
CHANGED
|
@@ -3,6 +3,7 @@ import { runScannerSteps } from './scanner.js';
|
|
|
3
3
|
import { generateOutputs } from './generator.js';
|
|
4
4
|
import { showCommunitySection } from '../ui/banner.js';
|
|
5
5
|
import { getMassiveAIProtocol } from './ai-protocol.js';
|
|
6
|
+
import { compareWithPreviousScan } from './diff-engine.js';
|
|
6
7
|
|
|
7
8
|
export async function runScan(args) {
|
|
8
9
|
const target = args.flags.local ? 'Local Project' : args.target;
|
|
@@ -20,7 +21,23 @@ export async function runScan(args) {
|
|
|
20
21
|
|
|
21
22
|
await generateOutputs(scanData);
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
// Compare with previous scan
|
|
25
|
+
const diff = await compareWithPreviousScan();
|
|
26
|
+
if (diff) {
|
|
27
|
+
console.log(chalk.bold('\n--- Scan Comparison ---'));
|
|
28
|
+
if (diff.newVulnerabilities.length > 0) {
|
|
29
|
+
console.log(chalk.red(`[+] ${diff.newVulnerabilities.length} New Vulnerabilities Found:`));
|
|
30
|
+
diff.newVulnerabilities.forEach(v => console.log(` - ${v.description}`));
|
|
31
|
+
}
|
|
32
|
+
if (diff.fixedVulnerabilities.length > 0) {
|
|
33
|
+
console.log(chalk.green(`[✔] ${diff.fixedVulnerabilities.length} Vulnerabilities Fixed:`));
|
|
34
|
+
diff.fixedVulnerabilities.forEach(v => console.log(` - ${v.description}`));
|
|
35
|
+
}
|
|
36
|
+
console.log(chalk.yellow(`Score Change: ${diff.scoreDiff > 0 ? '+' : ''}${diff.scoreDiff} points`));
|
|
37
|
+
console.log(chalk.gray('-----------------------\n'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(chalk.green(`[✔] Scan completed successfully!`));
|
|
24
41
|
console.log(chalk.white(` Report JSON: ./omen-reports/omen-report.json`));
|
|
25
42
|
console.log(chalk.white(` AI Prompt: ./omen-reports/omen-ai.txt\n`));
|
|
26
43
|
|
package/core/generator.js
CHANGED
|
@@ -6,18 +6,22 @@ import { getMassiveAIProtocol } from './ai-protocol.js';
|
|
|
6
6
|
export async function generateOutputs(scanData) {
|
|
7
7
|
const cwd = process.cwd();
|
|
8
8
|
const outputDir = path.join(cwd, 'omen-reports');
|
|
9
|
+
const historyDir = path.join(cwd, '.omen', 'history');
|
|
9
10
|
|
|
10
|
-
// Criar
|
|
11
|
+
// Criar as pastas se não existirem
|
|
11
12
|
try {
|
|
12
13
|
await fs.mkdir(outputDir, { recursive: true });
|
|
14
|
+
await fs.mkdir(historyDir, { recursive: true });
|
|
13
15
|
} catch (err) {
|
|
14
|
-
console.error(chalk.red(`Failed to create output
|
|
16
|
+
console.error(chalk.red(`Failed to create output directories: ${err.message}`));
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
19
|
+
|
|
20
|
+
const jsonContent = JSON.stringify(scanData, null, 2);
|
|
17
21
|
|
|
18
22
|
// JSON Report
|
|
19
23
|
const jsonReportPath = path.join(outputDir, 'omen-report.json');
|
|
20
|
-
await fs.writeFile(jsonReportPath,
|
|
24
|
+
await fs.writeFile(jsonReportPath, jsonContent);
|
|
21
25
|
console.log(` /omen-reports/omen-report.json`);
|
|
22
26
|
|
|
23
27
|
// TXT Report
|
|
@@ -30,4 +34,9 @@ export async function generateOutputs(scanData) {
|
|
|
30
34
|
const aiReportPath = path.join(outputDir, 'omen-ai.txt');
|
|
31
35
|
await fs.writeFile(aiReportPath, getMassiveAIProtocol(scanData));
|
|
32
36
|
console.log(` /omen-reports/omen-ai.txt`);
|
|
37
|
+
|
|
38
|
+
// Save historical report
|
|
39
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-');
|
|
40
|
+
const historyReportPath = path.join(historyDir, `omen-report-${timestamp}.json`);
|
|
41
|
+
await fs.writeFile(historyReportPath, jsonContent);
|
|
33
42
|
}
|
package/core/local-scanner.js
CHANGED
|
@@ -61,19 +61,23 @@ export async function scanLocalProject() {
|
|
|
61
61
|
if (deps['lodash'] && deps['lodash'].match(/[~^]?4\.17\.[0-20]/)) {
|
|
62
62
|
vulnerabilities.push({
|
|
63
63
|
id: `LOC-VULN-${Date.now()}-1`,
|
|
64
|
-
|
|
64
|
+
category: 'Confirmed',
|
|
65
|
+
confidence: 'High',
|
|
65
66
|
severity: 'High',
|
|
66
|
-
description: `Outdated dependency detected in package.json: lodash (${deps['lodash']}). Prototype Pollution risk.`,
|
|
67
|
-
cwe: 'CWE-1321'
|
|
67
|
+
description: `Outdated dependency detected in package.json: lodash (${deps['lodash']}). Known Prototype Pollution risk.`,
|
|
68
|
+
cwe: 'CWE-1321',
|
|
69
|
+
evidence: { file: 'package.json', finding: `lodash: ${deps['lodash']}` }
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
if (deps['express'] && deps['express'].match(/[~^]?3\./)) {
|
|
71
73
|
vulnerabilities.push({
|
|
72
74
|
id: `LOC-VULN-${Date.now()}-2`,
|
|
73
|
-
|
|
75
|
+
category: 'Confirmed',
|
|
76
|
+
confidence: 'High',
|
|
74
77
|
severity: 'High',
|
|
75
78
|
description: `Severely outdated Express.js version (${deps['express']}) detected. Multiple CVEs exist.`,
|
|
76
|
-
cwe: 'CWE-1104'
|
|
79
|
+
cwe: 'CWE-1104',
|
|
80
|
+
evidence: { file: 'package.json', finding: `express: ${deps['express']}` }
|
|
77
81
|
});
|
|
78
82
|
}
|
|
79
83
|
} catch (err) {
|
|
@@ -98,10 +102,12 @@ export async function scanLocalProject() {
|
|
|
98
102
|
if (/(api_key|apikey|secret|password|token)\s*=\s*['"][a-zA-Z0-9_-]{10,}['"]/i.test(line)) {
|
|
99
103
|
vulnerabilities.push({
|
|
100
104
|
id: `LOC-VULN-${Date.now()}-3`,
|
|
101
|
-
|
|
105
|
+
category: 'Confirmed',
|
|
106
|
+
confidence: 'Medium', // Could be a test key
|
|
102
107
|
severity: 'Critical',
|
|
103
108
|
description: `Potential hardcoded secret found in ${path.basename(file)} at line ${index + 1}`,
|
|
104
|
-
cwe: 'CWE-798'
|
|
109
|
+
cwe: 'CWE-798',
|
|
110
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() }
|
|
105
111
|
});
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -109,10 +115,12 @@ export async function scanLocalProject() {
|
|
|
109
115
|
if (/eval\s*\(/.test(line)) {
|
|
110
116
|
vulnerabilities.push({
|
|
111
117
|
id: `LOC-VULN-${Date.now()}-4`,
|
|
112
|
-
|
|
118
|
+
category: 'Confirmed',
|
|
119
|
+
confidence: 'High',
|
|
113
120
|
severity: 'Critical',
|
|
114
121
|
description: `Dangerous use of eval() detected in ${path.basename(file)} at line ${index + 1}`,
|
|
115
|
-
cwe: 'CWE-94'
|
|
122
|
+
cwe: 'CWE-94',
|
|
123
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() }
|
|
116
124
|
});
|
|
117
125
|
}
|
|
118
126
|
|
|
@@ -120,10 +128,12 @@ export async function scanLocalProject() {
|
|
|
120
128
|
if (/SELECT.*FROM.*WHERE.*(\+|`|\${)/i.test(line)) {
|
|
121
129
|
vulnerabilities.push({
|
|
122
130
|
id: `LOC-VULN-${Date.now()}-5`,
|
|
123
|
-
|
|
131
|
+
category: 'Probable',
|
|
132
|
+
confidence: 'Medium',
|
|
124
133
|
severity: 'Critical',
|
|
125
|
-
description: `
|
|
126
|
-
cwe: 'CWE-89'
|
|
134
|
+
description: `Potential SQL Injection (raw string concatenation) in ${path.basename(file)} at line ${index + 1}.`,
|
|
135
|
+
cwe: 'CWE-89',
|
|
136
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() }
|
|
127
137
|
});
|
|
128
138
|
}
|
|
129
139
|
|
|
@@ -131,10 +141,12 @@ export async function scanLocalProject() {
|
|
|
131
141
|
if (/(require|import)\s*\(['"]?.*(\+|`|\${)/i.test(line)) {
|
|
132
142
|
vulnerabilities.push({
|
|
133
143
|
id: `LOC-VULN-${Date.now()}-6`,
|
|
134
|
-
|
|
144
|
+
category: 'Probable',
|
|
145
|
+
confidence: 'Medium',
|
|
135
146
|
severity: 'High',
|
|
136
147
|
description: `Potential LFI detected in ${path.basename(file)} at line ${index + 1}. Dynamic loading of files based on user input can lead to sensitive data exposure.`,
|
|
137
|
-
cwe: 'CWE-22'
|
|
148
|
+
cwe: 'CWE-22',
|
|
149
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() }
|
|
138
150
|
});
|
|
139
151
|
}
|
|
140
152
|
|
|
@@ -142,10 +154,12 @@ export async function scanLocalProject() {
|
|
|
142
154
|
if (/\.prototype\.[a-zA-Z0-9_]+\s*=\s*/.test(line)) {
|
|
143
155
|
vulnerabilities.push({
|
|
144
156
|
id: `LOC-VULN-${Date.now()}-7`,
|
|
145
|
-
|
|
157
|
+
category: 'Hardening',
|
|
158
|
+
confidence: 'Low',
|
|
146
159
|
severity: 'Medium',
|
|
147
160
|
description: `Direct prototype modification in ${path.basename(file)} at line ${index + 1}. This can lead to Prototype Pollution if user input reaches this assignment.`,
|
|
148
|
-
cwe: 'CWE-1321'
|
|
161
|
+
cwe: 'CWE-1321',
|
|
162
|
+
evidence: { file: path.basename(file), line: index + 1, code: line.trim() }
|
|
149
163
|
});
|
|
150
164
|
}
|
|
151
165
|
|
|
@@ -174,6 +188,7 @@ export async function scanLocalProject() {
|
|
|
174
188
|
|
|
175
189
|
return {
|
|
176
190
|
localFilesScanned: filesScanned.length,
|
|
191
|
+
filesScanned,
|
|
177
192
|
vulnerabilities
|
|
178
193
|
};
|
|
179
194
|
}
|
package/core/remote-scanner.js
CHANGED
|
@@ -36,10 +36,16 @@ 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
|
-
|
|
39
|
+
category: 'Hardening',
|
|
40
|
+
confidence: 'High',
|
|
40
41
|
severity: 'Medium',
|
|
41
42
|
description: `HSTS Header is missing. This lacks forced HTTPS enforcement for browsers that have already visited the site.`,
|
|
42
|
-
cwe: 'CWE-319'
|
|
43
|
+
cwe: 'CWE-319',
|
|
44
|
+
evidence: {
|
|
45
|
+
request: { headers: { ...response.request.headers } },
|
|
46
|
+
response: { status: response.status, headers: response.headers },
|
|
47
|
+
reason: 'Security header "Strict-Transport-Security" not found in server response.'
|
|
48
|
+
}
|
|
43
49
|
});
|
|
44
50
|
} else {
|
|
45
51
|
headers_analysis["Strict-Transport-Security"] = headers['strict-transport-security'];
|
|
@@ -49,20 +55,28 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
49
55
|
headers_analysis["Content-Security-Policy"] = "Missing";
|
|
50
56
|
vulnerabilities.push({
|
|
51
57
|
id: `REM-VULN-${Date.now()}-2`,
|
|
52
|
-
|
|
58
|
+
category: 'Confirmed',
|
|
59
|
+
confidence: 'High',
|
|
53
60
|
severity: 'High',
|
|
54
61
|
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.`,
|
|
55
|
-
cwe: 'CWE-1022'
|
|
62
|
+
cwe: 'CWE-1022',
|
|
63
|
+
evidence: {
|
|
64
|
+
request: { headers: { ...response.request.headers } },
|
|
65
|
+
response: { status: response.status, headers: response.headers },
|
|
66
|
+
reason: 'Security header "Content-Security-Policy" not found in server response.'
|
|
67
|
+
}
|
|
56
68
|
});
|
|
57
69
|
} else {
|
|
58
70
|
headers_analysis["Content-Security-Policy"] = headers['content-security-policy'];
|
|
59
71
|
if (headers['content-security-policy'].includes("unsafe-inline")) {
|
|
60
72
|
vulnerabilities.push({
|
|
61
73
|
id: `REM-VULN-${Date.now()}-3`,
|
|
62
|
-
|
|
74
|
+
category: 'Confirmed',
|
|
75
|
+
confidence: 'High',
|
|
63
76
|
severity: 'High',
|
|
64
77
|
description: `Weak CSP detected: 'unsafe-inline' is allowed.`,
|
|
65
|
-
cwe: 'CWE-16'
|
|
78
|
+
cwe: 'CWE-16',
|
|
79
|
+
evidence: { finding: `policy: ${headers['content-security-policy']}` }
|
|
66
80
|
});
|
|
67
81
|
}
|
|
68
82
|
}
|
|
@@ -72,10 +86,16 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
72
86
|
headers_analysis["X-Frame-Options"] = "Missing";
|
|
73
87
|
vulnerabilities.push({
|
|
74
88
|
id: `REM-VULN-${Date.now()}-4`,
|
|
75
|
-
|
|
89
|
+
category: 'Hardening',
|
|
90
|
+
confidence: 'High',
|
|
76
91
|
severity: 'Low',
|
|
77
|
-
description: `Missing X-Frame-Options.
|
|
78
|
-
cwe: 'CWE-1021'
|
|
92
|
+
description: `Missing X-Frame-Options. Increases risk of Clickjacking.`,
|
|
93
|
+
cwe: 'CWE-1021',
|
|
94
|
+
evidence: {
|
|
95
|
+
request: { headers: { ...response.request.headers } },
|
|
96
|
+
response: { status: response.status, headers: response.headers },
|
|
97
|
+
reason: 'Security header "X-Frame-Options" not found. This allows the site to be embedded in iframes on third-party domains.'
|
|
98
|
+
}
|
|
79
99
|
});
|
|
80
100
|
} else {
|
|
81
101
|
headers_analysis["X-Frame-Options"] = headers['x-frame-options'];
|
|
@@ -86,10 +106,12 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
86
106
|
headers_analysis["Server"] = headers['server'];
|
|
87
107
|
vulnerabilities.push({
|
|
88
108
|
id: `REM-VULN-${Date.now()}-5`,
|
|
89
|
-
|
|
109
|
+
category: 'Informational',
|
|
110
|
+
confidence: 'High',
|
|
90
111
|
severity: 'Low',
|
|
91
112
|
description: `Server header leaks technology stack: ${headers['server']}`,
|
|
92
|
-
cwe: 'CWE-200'
|
|
113
|
+
cwe: 'CWE-200',
|
|
114
|
+
evidence: { finding: `Server: ${headers['server']}` }
|
|
93
115
|
});
|
|
94
116
|
} else {
|
|
95
117
|
headers_analysis["Server"] = "Hidden (Good)";
|
|
@@ -99,10 +121,12 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
99
121
|
if (headers['x-powered-by']) {
|
|
100
122
|
vulnerabilities.push({
|
|
101
123
|
id: `REM-VULN-${Date.now()}-6`,
|
|
102
|
-
|
|
124
|
+
category: 'Informational',
|
|
125
|
+
confidence: 'High',
|
|
103
126
|
severity: 'Low',
|
|
104
127
|
description: `X-Powered-By header leaks framework: ${headers['x-powered-by']}`,
|
|
105
|
-
cwe: 'CWE-200'
|
|
128
|
+
cwe: 'CWE-200',
|
|
129
|
+
evidence: { finding: `X-Powered-By: ${headers['x-powered-by']}` }
|
|
106
130
|
});
|
|
107
131
|
}
|
|
108
132
|
|
|
@@ -110,7 +134,7 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
110
134
|
if (typeof html === 'string') {
|
|
111
135
|
const $ = cheerio.load(html);
|
|
112
136
|
|
|
113
|
-
// Discover Links
|
|
137
|
+
// Discover Links, Params, and JS files
|
|
114
138
|
$('a').each((i, link) => {
|
|
115
139
|
const href = $(link).attr('href');
|
|
116
140
|
if (href && !href.startsWith('#') && !href.startsWith('mailto:')) {
|
|
@@ -119,7 +143,6 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
119
143
|
if (absoluteUrl.startsWith(targetUrl)) {
|
|
120
144
|
discoveredLinks.add(absoluteUrl);
|
|
121
145
|
|
|
122
|
-
// Extract query parameters
|
|
123
146
|
const urlObj = new URL(absoluteUrl);
|
|
124
147
|
urlObj.searchParams.forEach((value, name) => discoveredParams.add(name));
|
|
125
148
|
}
|
|
@@ -127,6 +150,18 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
127
150
|
}
|
|
128
151
|
});
|
|
129
152
|
|
|
153
|
+
$('script[src]').each((i, script) => {
|
|
154
|
+
const src = $(script).attr('src');
|
|
155
|
+
if (src) {
|
|
156
|
+
try {
|
|
157
|
+
const absoluteUrl = new URL(src, targetUrl).href;
|
|
158
|
+
if (absoluteUrl.startsWith(targetUrl)) {
|
|
159
|
+
discoveredLinks.add(absoluteUrl);
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
130
165
|
// Discover Forms
|
|
131
166
|
$('form').each((i, form) => {
|
|
132
167
|
const action = $(form).attr('action') || '';
|
|
@@ -140,6 +175,46 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
140
175
|
});
|
|
141
176
|
}
|
|
142
177
|
|
|
178
|
+
// --- DEEP CRAWL: robots.txt, sitemap.xml, and JS files ---
|
|
179
|
+
const robotsUrl = new URL('/robots.txt', targetUrl).href;
|
|
180
|
+
try {
|
|
181
|
+
const robotsRes = await axios.get(robotsUrl, { timeout: 5000 });
|
|
182
|
+
const robotsLines = robotsRes.data.split('\n');
|
|
183
|
+
robotsLines.forEach(line => {
|
|
184
|
+
if (line.toLowerCase().startsWith('allow:') || line.toLowerCase().startsWith('disallow:')) {
|
|
185
|
+
const path = line.split(':')[1].trim();
|
|
186
|
+
if (path !== '/') discoveredLinks.add(new URL(path, targetUrl).href);
|
|
187
|
+
}
|
|
188
|
+
if (line.toLowerCase().startsWith('sitemap:')) {
|
|
189
|
+
discoveredLinks.add(line.split(':')[1].trim());
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
} catch (e) {}
|
|
193
|
+
|
|
194
|
+
// Process sitemaps and JS files found
|
|
195
|
+
const extraLinks = new Set();
|
|
196
|
+
for (const link of discoveredLinks) {
|
|
197
|
+
if (link.endsWith('.xml')) {
|
|
198
|
+
try {
|
|
199
|
+
const sitemapRes = await axios.get(link, { timeout: 5000 });
|
|
200
|
+
const $sitemap = cheerio.load(sitemapRes.data, { xmlMode: true });
|
|
201
|
+
$sitemap('loc').each((i, loc) => extraLinks.add($sitemap(loc).text()));
|
|
202
|
+
} catch (e) {}
|
|
203
|
+
} else if (link.endsWith('.js')) {
|
|
204
|
+
try {
|
|
205
|
+
const jsRes = await axios.get(link, { timeout: 10000 });
|
|
206
|
+
const jsContent = jsRes.data;
|
|
207
|
+
const paths = jsContent.match(/(['"])\/[a-zA-Z0-9_\-\/]+(\?.*)?\1/g) || [];
|
|
208
|
+
paths.forEach(path => {
|
|
209
|
+
const cleanPath = path.replace(/['"]/g, '');
|
|
210
|
+
extraLinks.add(new URL(cleanPath, targetUrl).href);
|
|
211
|
+
});
|
|
212
|
+
} catch (e) {}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
extraLinks.forEach(link => discoveredLinks.add(link));
|
|
216
|
+
|
|
217
|
+
|
|
143
218
|
// --- FUZZER (Path Discovery - Aggressive) ---
|
|
144
219
|
const aggressivePaths = [
|
|
145
220
|
'/.env', '/.git/config', '/admin', '/wp-admin', '/config.php', '/.vscode/settings.json',
|
|
@@ -239,4 +314,85 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
239
314
|
discoveredParams: Array.from(discoveredParams),
|
|
240
315
|
vulnerabilities
|
|
241
316
|
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function validateFuzzerFinding(path, response, url) {
|
|
320
|
+
const { status, data } = response;
|
|
321
|
+
const evidence = {
|
|
322
|
+
request: { url, method: 'GET' },
|
|
323
|
+
response: { status, headers: response.headers, body_snippet: typeof data === 'string' ? data.substring(0, 250) : '' }
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Rule 1: Detect soft 404s on 200 OK responses
|
|
327
|
+
if (status === 200 && typeof data === 'string' && /(page not found|could not be found|404)/i.test(data)) {
|
|
328
|
+
return null; // Ignore soft 404s as they are not interesting paths
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Rule 2: Handle truly sensitive file exposures
|
|
332
|
+
const isSensitiveFile = /\.env|\.git|\.ssh|config|credentials|password/i.test(path);
|
|
333
|
+
if (isSensitiveFile && status === 200) {
|
|
334
|
+
// If content is not HTML, it's likely the raw file
|
|
335
|
+
if (typeof data === 'string' && !data.trim().startsWith('<html')) {
|
|
336
|
+
return {
|
|
337
|
+
id: `REM-CONFIRMED-FILE-${Date.now()}`,
|
|
338
|
+
category: 'Confirmed',
|
|
339
|
+
confidence: 'High',
|
|
340
|
+
severity: 'Critical',
|
|
341
|
+
description: `CRITICAL: Sensitive file exposed at ${url}. Contents contain raw configuration data.`,
|
|
342
|
+
cwe: 'CWE-538',
|
|
343
|
+
evidence: { ...evidence, reason: 'Raw sensitive file content detected (Non-HTML response on sensitive path)' }
|
|
344
|
+
};
|
|
345
|
+
} else {
|
|
346
|
+
return {
|
|
347
|
+
id: `REM-POTENTIAL-FILE-${Date.now()}`,
|
|
348
|
+
category: 'Informational',
|
|
349
|
+
confidence: 'Low',
|
|
350
|
+
severity: 'Info',
|
|
351
|
+
description: `Potential sensitive path found at ${url}, but returned HTML content. Likely a redirect or custom error page.`,
|
|
352
|
+
cwe: 'CWE-200',
|
|
353
|
+
evidence: { ...evidence, reason: 'HTML response on sensitive file path' }
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Rule 3: Analyze admin panel paths
|
|
359
|
+
const isAdminPath = /admin|dashboard|login/i.test(path);
|
|
360
|
+
if (isAdminPath && status === 200) {
|
|
361
|
+
if (typeof data === 'string' && /password|login/i.test(data)) {
|
|
362
|
+
return {
|
|
363
|
+
id: `REM-INFO-LOGIN-${Date.now()}`,
|
|
364
|
+
category: 'Informational',
|
|
365
|
+
confidence: 'High',
|
|
366
|
+
severity: 'Info',
|
|
367
|
+
description: `Admin login page discovered at ${url}. This is part of the attack surface.`,
|
|
368
|
+
cwe: 'CWE-200',
|
|
369
|
+
evidence
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
// If it's a 200 but not a login page, it could be an exposed panel
|
|
373
|
+
return {
|
|
374
|
+
id: `REM-PROBABLE-PANEL-${Date.now()}`,
|
|
375
|
+
category: 'Probable',
|
|
376
|
+
confidence: 'Medium',
|
|
377
|
+
severity: 'High',
|
|
378
|
+
description: `Potential exposed admin panel at ${url}. Manual verification required.`,
|
|
379
|
+
cwe: 'CWE-284', // Improper Access Control
|
|
380
|
+
evidence
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Rule 4: Treat 403 Forbidden as low-confidence informational
|
|
385
|
+
if (status === 403) {
|
|
386
|
+
return {
|
|
387
|
+
id: `REM-INFO-FORBIDDEN-${Date.now()}`,
|
|
388
|
+
category: 'Informational',
|
|
389
|
+
confidence: 'Low',
|
|
390
|
+
severity: 'Info',
|
|
391
|
+
description: `Path exists but access is restricted (403 Forbidden): ${url}. This confirms the path's existence but does not prove exposure.`,
|
|
392
|
+
cwe: 'CWE-204', // Response Discrepancy (Path Enumeration)
|
|
393
|
+
evidence
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return null; // By default, not a significant finding
|
|
242
398
|
}
|
package/core/scanner.js
CHANGED
|
@@ -17,10 +17,10 @@ export async function runScannerSteps(target, flags) {
|
|
|
17
17
|
let allVulnerabilities = [];
|
|
18
18
|
let headers_analysis = {};
|
|
19
19
|
let attack_surface = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
endpoints: [],
|
|
21
|
+
parameters: [],
|
|
22
|
+
forms: [],
|
|
23
|
+
tech_stack: []
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -32,32 +32,42 @@ export async function runScannerSteps(target, flags) {
|
|
|
32
32
|
const remoteData = await scanRemoteTarget(target);
|
|
33
33
|
headers_analysis = remoteData.headers_analysis;
|
|
34
34
|
allVulnerabilities.push(...remoteData.vulnerabilities);
|
|
35
|
-
attack_surface.
|
|
36
|
-
attack_surface.
|
|
37
|
-
attack_surface.
|
|
35
|
+
attack_surface.endpoints = remoteData.discoveredLinks;
|
|
36
|
+
attack_surface.parameters = remoteData.discoveredParams;
|
|
37
|
+
attack_surface.forms = remoteData.discoveredForms;
|
|
38
38
|
attack_surface.tech_stack = remoteData.techStack;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (step.text === 'Scanning endpoints...' && flags.local) {
|
|
42
42
|
const localData = await scanLocalProject();
|
|
43
43
|
allVulnerabilities.push(...localData.vulnerabilities);
|
|
44
|
-
attack_surface.
|
|
44
|
+
attack_surface.endpoints.push(...(localData.filesScanned || []));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
await sleep(step.delay);
|
|
48
48
|
spinner.succeed(`[${i + 1}/${steps.length}] ${step.text}`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Calculate dynamic score
|
|
51
|
+
// Calculate dynamic score based on confidence and severity
|
|
52
52
|
const baseScore = 100;
|
|
53
53
|
const penalties = allVulnerabilities.reduce((acc, v) => {
|
|
54
|
-
|
|
55
|
-
if (v.
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
// Only penalize for Confirmed or Probable issues
|
|
55
|
+
if (v.category !== 'Confirmed' && v.category !== 'Probable') return acc;
|
|
56
|
+
|
|
57
|
+
let severityWeight = 0;
|
|
58
|
+
if (v.severity === 'Critical') severityWeight = 25;
|
|
59
|
+
else if (v.severity === 'High') severityWeight = 15;
|
|
60
|
+
else if (v.severity === 'Medium') severityWeight = 10;
|
|
61
|
+
else if (v.severity === 'Low') severityWeight = 5;
|
|
62
|
+
|
|
63
|
+
let confidenceMultiplier = 1;
|
|
64
|
+
if (v.confidence === 'Medium') confidenceMultiplier = 0.6;
|
|
65
|
+
if (v.confidence === 'Low') confidenceMultiplier = 0.3;
|
|
66
|
+
|
|
67
|
+
return acc + (severityWeight * confidenceMultiplier);
|
|
58
68
|
}, 0);
|
|
59
69
|
|
|
60
|
-
const finalScore = Math.max(0, baseScore - penalties);
|
|
70
|
+
const finalScore = Math.max(0, Math.round(baseScore - penalties));
|
|
61
71
|
let riskLevel = 'Low';
|
|
62
72
|
if (finalScore < 50) riskLevel = 'Critical';
|
|
63
73
|
else if (finalScore < 70) riskLevel = 'High';
|
package/core/ui-server.js
CHANGED
|
@@ -13,6 +13,19 @@ export async function startUIServer() {
|
|
|
13
13
|
const data = await fs.readFile(reportPath, 'utf-8');
|
|
14
14
|
const report = JSON.parse(data);
|
|
15
15
|
|
|
16
|
+
const getSeverityClass = (severity) => {
|
|
17
|
+
if (severity === 'Critical') return 'text-red-500';
|
|
18
|
+
if (severity === 'High') return 'text-orange-500';
|
|
19
|
+
if (severity === 'Medium') return 'text-yellow-500';
|
|
20
|
+
return 'text-green-500';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getConfidenceClass = (confidence) => {
|
|
24
|
+
if (confidence === 'High') return 'text-green-400';
|
|
25
|
+
if (confidence === 'Medium') return 'text-yellow-400';
|
|
26
|
+
return 'text-red-400';
|
|
27
|
+
};
|
|
28
|
+
|
|
16
29
|
const html = `
|
|
17
30
|
<!DOCTYPE html>
|
|
18
31
|
<html lang="en">
|
|
@@ -22,76 +35,109 @@ export async function startUIServer() {
|
|
|
22
35
|
<title>OMEN SEC-CLI Dashboard</title>
|
|
23
36
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
24
37
|
<style>
|
|
25
|
-
body { background-color: #0a0a0c; color: #e0e0e0; font-family: '
|
|
26
|
-
.card { background-color: #16161a; border: 1px solid #
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.
|
|
38
|
+
body { background-color: #0a0a0c; color: #e0e0e0; font-family: 'Inter', sans-serif; }
|
|
39
|
+
.card { background-color: #16161a; border: 1px solid #2d2d3a; }
|
|
40
|
+
.tab { cursor: pointer; padding: 10px 15px; border-bottom: 2px solid transparent; }
|
|
41
|
+
.tab.active { border-bottom: 2px solid #ef4444; color: #ef4444; }
|
|
42
|
+
.collapsible { cursor: pointer; }
|
|
43
|
+
.content { display: none; }
|
|
44
|
+
pre { background-color: #000; padding: 10px; border-radius: 5px; overflow-x: auto; }
|
|
31
45
|
</style>
|
|
32
46
|
</head>
|
|
33
|
-
<body class="p-8">
|
|
34
|
-
<div class="max-w-
|
|
47
|
+
<body class="p-4 md:p-8">
|
|
48
|
+
<div class="max-w-7xl mx-auto">
|
|
35
49
|
<header class="flex justify-between items-center mb-8 border-b border-gray-800 pb-4">
|
|
36
|
-
<h1 class="text-3xl font-bold tracking-tighter text-red-500">OMEN <span class="text-white">
|
|
37
|
-
<div class="text-right">
|
|
38
|
-
<p class="text-gray-
|
|
39
|
-
<p class="text-gray-
|
|
50
|
+
<h1 class="text-3xl font-bold tracking-tighter text-red-500">OMEN <span class="text-white">EVIDENCE-CENTER</span></h1>
|
|
51
|
+
<div class="text-right text-sm">
|
|
52
|
+
<p class="text-gray-400">Scan ID: ${report.scan_id}</p>
|
|
53
|
+
<p class="text-gray-400">${new Date(report.timestamp).toLocaleString()}</p>
|
|
40
54
|
</div>
|
|
41
55
|
</header>
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</div>
|
|
48
|
-
<div class="
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
<!-- Tabs -->
|
|
58
|
+
<div class="flex border-b border-gray-700 mb-6">
|
|
59
|
+
<div class="tab active" onclick="showTab('dashboard')">Dashboard</div>
|
|
60
|
+
<div class="tab" onclick="showTab('findings')">Findings</div>
|
|
61
|
+
<div class="tab" onclick="showTab('surface')">Attack Surface</div>
|
|
62
|
+
<div class="tab" onclick="showTab('history')">History</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Dashboard Tab -->
|
|
66
|
+
<div id="dashboard" class="tab-content block">
|
|
67
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
68
|
+
<div class="card p-4 rounded-lg"><h3 class="text-gray-400">Security Score</h3><p class="text-4xl font-bold ${getSeverityClass(report.riskLevel)}">${report.score}/100</p></div>
|
|
69
|
+
<div class="card p-4 rounded-lg"><h3 class="text-gray-400">Risk Level</h3><p class="text-4xl font-bold ${getSeverityClass(report.riskLevel)}">${report.riskLevel}</p></div>
|
|
70
|
+
<div class="card p-4 rounded-lg"><h3 class="text-gray-400">Confirmed</h3><p class="text-4xl font-bold">${report.vulnerabilities.filter(v => v.category === 'Confirmed').length}</p></div>
|
|
71
|
+
<div class="card p-4 rounded-lg"><h3 class="text-gray-400">Probable</h3><p class="text-4xl font-bold">${report.vulnerabilities.filter(v => v.category === 'Probable').length}</p></div>
|
|
51
72
|
</div>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Findings Tab -->
|
|
76
|
+
<div id="findings" class="tab-content hidden">
|
|
77
|
+
<div class="card p-4 rounded-lg">
|
|
78
|
+
<h2 class="text-xl font-bold mb-4">Vulnerability Details</h2>
|
|
79
|
+
<div class="space-y-3">
|
|
80
|
+
${report.vulnerabilities.map((v, i) => `
|
|
81
|
+
<div>
|
|
82
|
+
<div class="collapsible flex justify-between items-center p-3 bg-gray-800 rounded-t-lg" onclick="toggleCollapse(${i})">
|
|
83
|
+
<span class="font-bold ${getSeverityClass(v.severity)}">${v.severity}</span>
|
|
84
|
+
<span class="flex-1 mx-4">${v.description}</span>
|
|
85
|
+
<span class="${getConfidenceClass(v.confidence)}">${v.confidence} Confidence</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div id="content-${i}" class="content p-4 bg-gray-900 rounded-b-lg">
|
|
88
|
+
<h4 class="font-bold mb-2">Evidence</h4>
|
|
89
|
+
<pre>${JSON.stringify(v.evidence, null, 2)}</pre>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
`).join('')}
|
|
93
|
+
</div>
|
|
55
94
|
</div>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- Attack Surface Tab -->
|
|
98
|
+
<div id="surface" class="tab-content hidden">
|
|
99
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
100
|
+
<div class="card p-4 rounded-lg"> <h3 class="font-bold mb-2">Tech Stack</h3> <pre>${Array.isArray(report.attack_surface.tech_stack) ? report.attack_surface.tech_stack.join('\n') : (report.attack_surface.tech_stack || '')}</pre> </div>
|
|
101
|
+
<div class="card p-4 rounded-lg"> <h3 class="font-bold mb-2">Forms</h3> <pre>${JSON.stringify(report.attack_surface.forms || report.attack_surface.forms_detected || [], null, 2)}</pre> </div>
|
|
102
|
+
<div class="card p-4 rounded-lg col-span-1 md:col-span-2"> <h3 class="font-bold mb-2">Discovered Links</h3> <pre class="max-h-96">${Array.isArray(report.attack_surface.endpoints) ? report.attack_surface.endpoints.join('\n') : (report.attack_surface.endpoints || report.attack_surface.endpoints_discovered || '')}</pre> </div>
|
|
61
103
|
</div>
|
|
62
104
|
</div>
|
|
63
105
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<div class="
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<div class="flex justify-between">
|
|
70
|
-
<h4 class="font-bold text-lg">${v.type}</h4>
|
|
71
|
-
<span class="px-2 py-1 rounded text-xs font-bold ${v.severity === 'Critical' ? 'bg-red-900 text-red-100' : v.severity === 'High' ? 'bg-orange-900 text-orange-100' : 'bg-yellow-900 text-yellow-100'}">${v.severity}</span>
|
|
72
|
-
</div>
|
|
73
|
-
<p class="text-gray-300 mt-2">${v.description}</p>
|
|
74
|
-
<p class="text-gray-500 text-sm mt-1">CWE: ${v.cwe} | ID: ${v.id}</p>
|
|
75
|
-
</div>
|
|
76
|
-
`).join('')}
|
|
106
|
+
<!-- History Tab -->
|
|
107
|
+
<div id="history" class="tab-content hidden">
|
|
108
|
+
<div class="card p-4 rounded-lg">
|
|
109
|
+
<h2 class="text-xl font-bold mb-4">Scan History</h2>
|
|
110
|
+
<p class="text-gray-400">Feature coming soon. Historical data is being saved in the .omen/history/ directory.</p>
|
|
77
111
|
</div>
|
|
78
112
|
</div>
|
|
79
113
|
|
|
80
|
-
<footer class="text-center text-gray-600 mt-12">
|
|
81
|
-
<p>OMEN Security Framework - v1.0.
|
|
114
|
+
<footer class="text-center text-gray-600 mt-12 border-t border-gray-800 pt-4">
|
|
115
|
+
<p>OMEN Security Framework - v1.0.14</p>
|
|
82
116
|
</footer>
|
|
83
117
|
</div>
|
|
118
|
+
<script>
|
|
119
|
+
function showTab(tabName) {
|
|
120
|
+
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.add('hidden'));
|
|
121
|
+
document.getElementById(tabName).classList.remove('hidden');
|
|
122
|
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
123
|
+
document.querySelector(`[onclick="showTab('${tabName}')"]`).classList.add('active');
|
|
124
|
+
}
|
|
125
|
+
function toggleCollapse(index) {
|
|
126
|
+
const content = document.getElementById('content-' + index);
|
|
127
|
+
content.style.display = content.style.display === 'block' ? 'none' : 'block';
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
84
130
|
</body>
|
|
85
131
|
</html>
|
|
86
132
|
`;
|
|
87
133
|
res.send(html);
|
|
88
134
|
} catch (err) {
|
|
89
|
-
res.status(500).send(`<h1>Error loading report</h1><p>Please run a scan first to generate omen-report.json</p>`);
|
|
135
|
+
res.status(500).send(`<h1>Error loading report</h1><p>Please run a scan first to generate omen-report.json. Error: ${err.message}</p>`);
|
|
90
136
|
}
|
|
91
137
|
});
|
|
92
138
|
|
|
93
139
|
app.listen(port, () => {
|
|
94
|
-
console.log(chalk.cyan(`\n[OMEN UI]
|
|
140
|
+
console.log(chalk.cyan(`\n[OMEN UI] Evidence Center is running at:`));
|
|
95
141
|
console.log(chalk.bold.green(` http://localhost:${port}\n`));
|
|
96
142
|
console.log(chalk.gray(`Press Ctrl+C to stop the server.`));
|
|
97
143
|
});
|
package/package.json
CHANGED
package/ui/banner.js
CHANGED
|
@@ -9,7 +9,7 @@ export function showBanner() {
|
|
|
9
9
|
╚██████╔╝██║ ╚═╝ ██║███████╗██║ ╚████║
|
|
10
10
|
`));
|
|
11
11
|
console.log(chalk.cyan.bold(' OMEN — AI Security Engine '));
|
|
12
|
-
console.log(chalk.gray(' Version: 1.0.
|
|
12
|
+
console.log(chalk.gray(' Version: 1.0.14 \n'));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function showHelp() {
|