omen-sec-cli 1.0.12 → 1.0.14
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/core/diff-engine.js +35 -0
- package/core/engine.js +18 -1
- package/core/generator.js +12 -3
- package/core/local-scanner.js +30 -16
- package/core/remote-scanner.js +149 -15
- package/core/scanner.js +16 -6
- package/core/ui-server.js +89 -46
- package/package.json +1 -1
|
@@ -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
|
|
package/core/remote-scanner.js
CHANGED
|
@@ -36,10 +36,12 @@ 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: { request: { headers: { ...response.request.headers } }, response: { headers: response.headers } }
|
|
43
45
|
});
|
|
44
46
|
} else {
|
|
45
47
|
headers_analysis["Strict-Transport-Security"] = headers['strict-transport-security'];
|
|
@@ -49,20 +51,24 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
49
51
|
headers_analysis["Content-Security-Policy"] = "Missing";
|
|
50
52
|
vulnerabilities.push({
|
|
51
53
|
id: `REM-VULN-${Date.now()}-2`,
|
|
52
|
-
|
|
54
|
+
category: 'Confirmed',
|
|
55
|
+
confidence: 'High',
|
|
53
56
|
severity: 'High',
|
|
54
57
|
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'
|
|
58
|
+
cwe: 'CWE-1022',
|
|
59
|
+
evidence: { request: { headers: { ...response.request.headers } }, response: { headers: response.headers } }
|
|
56
60
|
});
|
|
57
61
|
} else {
|
|
58
62
|
headers_analysis["Content-Security-Policy"] = headers['content-security-policy'];
|
|
59
63
|
if (headers['content-security-policy'].includes("unsafe-inline")) {
|
|
60
64
|
vulnerabilities.push({
|
|
61
65
|
id: `REM-VULN-${Date.now()}-3`,
|
|
62
|
-
|
|
66
|
+
category: 'Confirmed',
|
|
67
|
+
confidence: 'High',
|
|
63
68
|
severity: 'High',
|
|
64
69
|
description: `Weak CSP detected: 'unsafe-inline' is allowed.`,
|
|
65
|
-
cwe: 'CWE-16'
|
|
70
|
+
cwe: 'CWE-16',
|
|
71
|
+
evidence: { finding: `policy: ${headers['content-security-policy']}` }
|
|
66
72
|
});
|
|
67
73
|
}
|
|
68
74
|
}
|
|
@@ -72,10 +78,12 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
72
78
|
headers_analysis["X-Frame-Options"] = "Missing";
|
|
73
79
|
vulnerabilities.push({
|
|
74
80
|
id: `REM-VULN-${Date.now()}-4`,
|
|
75
|
-
|
|
81
|
+
category: 'Hardening',
|
|
82
|
+
confidence: 'High',
|
|
76
83
|
severity: 'Low',
|
|
77
|
-
description: `Missing X-Frame-Options.
|
|
78
|
-
cwe: 'CWE-1021'
|
|
84
|
+
description: `Missing X-Frame-Options. Increases risk of Clickjacking.`,
|
|
85
|
+
cwe: 'CWE-1021',
|
|
86
|
+
evidence: { request: { headers: { ...response.request.headers } }, response: { headers: response.headers } }
|
|
79
87
|
});
|
|
80
88
|
} else {
|
|
81
89
|
headers_analysis["X-Frame-Options"] = headers['x-frame-options'];
|
|
@@ -86,10 +94,12 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
86
94
|
headers_analysis["Server"] = headers['server'];
|
|
87
95
|
vulnerabilities.push({
|
|
88
96
|
id: `REM-VULN-${Date.now()}-5`,
|
|
89
|
-
|
|
97
|
+
category: 'Informational',
|
|
98
|
+
confidence: 'High',
|
|
90
99
|
severity: 'Low',
|
|
91
100
|
description: `Server header leaks technology stack: ${headers['server']}`,
|
|
92
|
-
cwe: 'CWE-200'
|
|
101
|
+
cwe: 'CWE-200',
|
|
102
|
+
evidence: { finding: `Server: ${headers['server']}` }
|
|
93
103
|
});
|
|
94
104
|
} else {
|
|
95
105
|
headers_analysis["Server"] = "Hidden (Good)";
|
|
@@ -99,10 +109,12 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
99
109
|
if (headers['x-powered-by']) {
|
|
100
110
|
vulnerabilities.push({
|
|
101
111
|
id: `REM-VULN-${Date.now()}-6`,
|
|
102
|
-
|
|
112
|
+
category: 'Informational',
|
|
113
|
+
confidence: 'High',
|
|
103
114
|
severity: 'Low',
|
|
104
115
|
description: `X-Powered-By header leaks framework: ${headers['x-powered-by']}`,
|
|
105
|
-
cwe: 'CWE-200'
|
|
116
|
+
cwe: 'CWE-200',
|
|
117
|
+
evidence: { finding: `X-Powered-By: ${headers['x-powered-by']}` }
|
|
106
118
|
});
|
|
107
119
|
}
|
|
108
120
|
|
|
@@ -110,7 +122,7 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
110
122
|
if (typeof html === 'string') {
|
|
111
123
|
const $ = cheerio.load(html);
|
|
112
124
|
|
|
113
|
-
// Discover Links
|
|
125
|
+
// Discover Links, Params, and JS files
|
|
114
126
|
$('a').each((i, link) => {
|
|
115
127
|
const href = $(link).attr('href');
|
|
116
128
|
if (href && !href.startsWith('#') && !href.startsWith('mailto:')) {
|
|
@@ -119,7 +131,6 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
119
131
|
if (absoluteUrl.startsWith(targetUrl)) {
|
|
120
132
|
discoveredLinks.add(absoluteUrl);
|
|
121
133
|
|
|
122
|
-
// Extract query parameters
|
|
123
134
|
const urlObj = new URL(absoluteUrl);
|
|
124
135
|
urlObj.searchParams.forEach((value, name) => discoveredParams.add(name));
|
|
125
136
|
}
|
|
@@ -127,6 +138,18 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
127
138
|
}
|
|
128
139
|
});
|
|
129
140
|
|
|
141
|
+
$('script[src]').each((i, script) => {
|
|
142
|
+
const src = $(script).attr('src');
|
|
143
|
+
if (src) {
|
|
144
|
+
try {
|
|
145
|
+
const absoluteUrl = new URL(src, targetUrl).href;
|
|
146
|
+
if (absoluteUrl.startsWith(targetUrl)) {
|
|
147
|
+
discoveredLinks.add(absoluteUrl);
|
|
148
|
+
}
|
|
149
|
+
} catch (e) {}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
130
153
|
// Discover Forms
|
|
131
154
|
$('form').each((i, form) => {
|
|
132
155
|
const action = $(form).attr('action') || '';
|
|
@@ -140,6 +163,46 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
140
163
|
});
|
|
141
164
|
}
|
|
142
165
|
|
|
166
|
+
// --- DEEP CRAWL: robots.txt, sitemap.xml, and JS files ---
|
|
167
|
+
const robotsUrl = new URL('/robots.txt', targetUrl).href;
|
|
168
|
+
try {
|
|
169
|
+
const robotsRes = await axios.get(robotsUrl, { timeout: 5000 });
|
|
170
|
+
const robotsLines = robotsRes.data.split('\n');
|
|
171
|
+
robotsLines.forEach(line => {
|
|
172
|
+
if (line.toLowerCase().startsWith('allow:') || line.toLowerCase().startsWith('disallow:')) {
|
|
173
|
+
const path = line.split(':')[1].trim();
|
|
174
|
+
if (path !== '/') discoveredLinks.add(new URL(path, targetUrl).href);
|
|
175
|
+
}
|
|
176
|
+
if (line.toLowerCase().startsWith('sitemap:')) {
|
|
177
|
+
discoveredLinks.add(line.split(':')[1].trim());
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} catch (e) {}
|
|
181
|
+
|
|
182
|
+
// Process sitemaps and JS files found
|
|
183
|
+
const extraLinks = new Set();
|
|
184
|
+
for (const link of discoveredLinks) {
|
|
185
|
+
if (link.endsWith('.xml')) {
|
|
186
|
+
try {
|
|
187
|
+
const sitemapRes = await axios.get(link, { timeout: 5000 });
|
|
188
|
+
const $sitemap = cheerio.load(sitemapRes.data, { xmlMode: true });
|
|
189
|
+
$sitemap('loc').each((i, loc) => extraLinks.add($sitemap(loc).text()));
|
|
190
|
+
} catch (e) {}
|
|
191
|
+
} else if (link.endsWith('.js')) {
|
|
192
|
+
try {
|
|
193
|
+
const jsRes = await axios.get(link, { timeout: 10000 });
|
|
194
|
+
const jsContent = jsRes.data;
|
|
195
|
+
const paths = jsContent.match(/(['"])\/[a-zA-Z0-9_\-\/]+(\?.*)?\1/g) || [];
|
|
196
|
+
paths.forEach(path => {
|
|
197
|
+
const cleanPath = path.replace(/['"]/g, '');
|
|
198
|
+
extraLinks.add(new URL(cleanPath, targetUrl).href);
|
|
199
|
+
});
|
|
200
|
+
} catch (e) {}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
extraLinks.forEach(link => discoveredLinks.add(link));
|
|
204
|
+
|
|
205
|
+
|
|
143
206
|
// --- FUZZER (Path Discovery - Aggressive) ---
|
|
144
207
|
const aggressivePaths = [
|
|
145
208
|
'/.env', '/.git/config', '/admin', '/wp-admin', '/config.php', '/.vscode/settings.json',
|
|
@@ -239,4 +302,75 @@ export async function scanRemoteTarget(targetUrl) {
|
|
|
239
302
|
discoveredParams: Array.from(discoveredParams),
|
|
240
303
|
vulnerabilities
|
|
241
304
|
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function validateFuzzerFinding(path, response, url) {
|
|
308
|
+
const { status, data } = response;
|
|
309
|
+
const evidence = {
|
|
310
|
+
request: { url, method: 'GET' },
|
|
311
|
+
response: { status, headers: response.headers, body_snippet: typeof data === 'string' ? data.substring(0, 250) : '' }
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Rule 1: Detect soft 404s on 200 OK responses
|
|
315
|
+
if (status === 200 && typeof data === 'string' && /(page not found|could not be found|404)/i.test(data)) {
|
|
316
|
+
return null; // Ignore soft 404s as they are not interesting paths
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Rule 2: Handle truly sensitive file exposures
|
|
320
|
+
const isSensitiveFile = /\.env|\.git|\.ssh|config|credentials|password/i.test(path);
|
|
321
|
+
if (isSensitiveFile && status === 200) {
|
|
322
|
+
// If content is not HTML, it's likely the raw file
|
|
323
|
+
if (typeof data === 'string' && !data.trim().startsWith('<html')) {
|
|
324
|
+
return {
|
|
325
|
+
id: `REM-CONFIRMED-FILE-${Date.now()}`,
|
|
326
|
+
category: 'Confirmed',
|
|
327
|
+
confidence: 'High',
|
|
328
|
+
severity: 'Critical',
|
|
329
|
+
description: `CRITICAL: Sensitive file exposed at ${url}. Contents may contain credentials, private keys, or configuration secrets.`,
|
|
330
|
+
cwe: 'CWE-538', // File and Directory Information Exposure
|
|
331
|
+
evidence
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Rule 3: Analyze admin panel paths
|
|
337
|
+
const isAdminPath = /admin|dashboard|login/i.test(path);
|
|
338
|
+
if (isAdminPath && status === 200) {
|
|
339
|
+
if (typeof data === 'string' && /password|login/i.test(data)) {
|
|
340
|
+
return {
|
|
341
|
+
id: `REM-INFO-LOGIN-${Date.now()}`,
|
|
342
|
+
category: 'Informational',
|
|
343
|
+
confidence: 'High',
|
|
344
|
+
severity: 'Info',
|
|
345
|
+
description: `Admin login page discovered at ${url}. This is part of the attack surface.`,
|
|
346
|
+
cwe: 'CWE-200',
|
|
347
|
+
evidence
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
// If it's a 200 but not a login page, it could be an exposed panel
|
|
351
|
+
return {
|
|
352
|
+
id: `REM-PROBABLE-PANEL-${Date.now()}`,
|
|
353
|
+
category: 'Probable',
|
|
354
|
+
confidence: 'Medium',
|
|
355
|
+
severity: 'High',
|
|
356
|
+
description: `Potential exposed admin panel at ${url}. Manual verification required.`,
|
|
357
|
+
cwe: 'CWE-284', // Improper Access Control
|
|
358
|
+
evidence
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Rule 4: Treat 403 Forbidden as low-confidence informational
|
|
363
|
+
if (status === 403) {
|
|
364
|
+
return {
|
|
365
|
+
id: `REM-INFO-FORBIDDEN-${Date.now()}`,
|
|
366
|
+
category: 'Informational',
|
|
367
|
+
confidence: 'Low',
|
|
368
|
+
severity: 'Info',
|
|
369
|
+
description: `Path exists but is protected (403 Forbidden): ${url}. This confirms the path's existence.`,
|
|
370
|
+
cwe: 'CWE-406', // Insufficient Guarantees of Data Integrity
|
|
371
|
+
evidence
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return null; // By default, not a significant finding
|
|
242
376
|
}
|
package/core/scanner.js
CHANGED
|
@@ -48,16 +48,26 @@ export async function runScannerSteps(target, flags) {
|
|
|
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,106 @@ 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>${(report.attack_surface.tech_stack || []).join('\n')}</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_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">${(report.attack_surface.endpoints_discovered || []).join('\n')}</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.6</p>
|
|
82
|
-
</footer>
|
|
83
114
|
</div>
|
|
115
|
+
<script>
|
|
116
|
+
function showTab(tabName) {
|
|
117
|
+
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.add('hidden'));
|
|
118
|
+
document.getElementById(tabName).classList.remove('hidden');
|
|
119
|
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
120
|
+
document.querySelector(`[onclick="showTab('${tabName}')"]`).classList.add('active');
|
|
121
|
+
}
|
|
122
|
+
function toggleCollapse(index) {
|
|
123
|
+
const content = document.getElementById('content-' + index);
|
|
124
|
+
content.style.display = content.style.display === 'block' ? 'none' : 'block';
|
|
125
|
+
}
|
|
126
|
+
</script>
|
|
84
127
|
</body>
|
|
85
128
|
</html>
|
|
86
129
|
`;
|
|
87
130
|
res.send(html);
|
|
88
131
|
} catch (err) {
|
|
89
|
-
res.status(500).send(`<h1>Error loading report</h1><p>Please run a scan first to generate omen-report.json</p>`);
|
|
132
|
+
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
133
|
}
|
|
91
134
|
});
|
|
92
135
|
|
|
93
136
|
app.listen(port, () => {
|
|
94
|
-
console.log(chalk.cyan(`\n[OMEN UI]
|
|
137
|
+
console.log(chalk.cyan(`\n[OMEN UI] Evidence Center is running at:`));
|
|
95
138
|
console.log(chalk.bold.green(` http://localhost:${port}\n`));
|
|
96
139
|
console.log(chalk.gray(`Press Ctrl+C to stop the server.`));
|
|
97
140
|
});
|