omen-sec-cli 1.0.10 → 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.
@@ -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
- console.log(chalk.green(`\n[✔] Scan completed successfully!`));
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 a pasta se não existir
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 directory: ${err.message}`));
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, JSON.stringify(scanData, null, 2));
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
  }
@@ -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
- type: 'Vulnerable Components',
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
- type: 'Vulnerable Components',
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
- type: 'Sensitive Data Exposure',
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
- type: 'Code Injection',
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
- type: 'SQL Injection',
131
+ category: 'Probable',
132
+ confidence: 'Medium',
124
133
  severity: 'Critical',
125
- description: `Critical SQL Injection vulnerability (raw string concatenation) in ${path.basename(file)} at line ${index + 1}. Attacker can bypass authentication or dump the entire database.`,
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
- type: 'Local File Inclusion',
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
- type: 'Prototype Pollution Risk',
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
 
@@ -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
- type: 'Security Misconfiguration',
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,21 +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
- type: 'Security Misconfiguration',
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
- }
58
- // ... (rest of header analysis) else {
61
+ } else {
59
62
  headers_analysis["Content-Security-Policy"] = headers['content-security-policy'];
60
63
  if (headers['content-security-policy'].includes("unsafe-inline")) {
61
64
  vulnerabilities.push({
62
65
  id: `REM-VULN-${Date.now()}-3`,
63
- type: 'Security Misconfiguration',
66
+ category: 'Confirmed',
67
+ confidence: 'High',
64
68
  severity: 'High',
65
69
  description: `Weak CSP detected: 'unsafe-inline' is allowed.`,
66
- cwe: 'CWE-16'
70
+ cwe: 'CWE-16',
71
+ evidence: { finding: `policy: ${headers['content-security-policy']}` }
67
72
  });
68
73
  }
69
74
  }
@@ -73,10 +78,12 @@ export async function scanRemoteTarget(targetUrl) {
73
78
  headers_analysis["X-Frame-Options"] = "Missing";
74
79
  vulnerabilities.push({
75
80
  id: `REM-VULN-${Date.now()}-4`,
76
- type: 'Security Misconfiguration',
81
+ category: 'Hardening',
82
+ confidence: 'High',
77
83
  severity: 'Low',
78
- description: `Missing X-Frame-Options. Vulnerable to Clickjacking.`,
79
- 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 } }
80
87
  });
81
88
  } else {
82
89
  headers_analysis["X-Frame-Options"] = headers['x-frame-options'];
@@ -87,10 +94,12 @@ export async function scanRemoteTarget(targetUrl) {
87
94
  headers_analysis["Server"] = headers['server'];
88
95
  vulnerabilities.push({
89
96
  id: `REM-VULN-${Date.now()}-5`,
90
- type: 'Information Exposure',
97
+ category: 'Informational',
98
+ confidence: 'High',
91
99
  severity: 'Low',
92
100
  description: `Server header leaks technology stack: ${headers['server']}`,
93
- cwe: 'CWE-200'
101
+ cwe: 'CWE-200',
102
+ evidence: { finding: `Server: ${headers['server']}` }
94
103
  });
95
104
  } else {
96
105
  headers_analysis["Server"] = "Hidden (Good)";
@@ -100,10 +109,12 @@ export async function scanRemoteTarget(targetUrl) {
100
109
  if (headers['x-powered-by']) {
101
110
  vulnerabilities.push({
102
111
  id: `REM-VULN-${Date.now()}-6`,
103
- type: 'Information Exposure',
112
+ category: 'Informational',
113
+ confidence: 'High',
104
114
  severity: 'Low',
105
115
  description: `X-Powered-By header leaks framework: ${headers['x-powered-by']}`,
106
- cwe: 'CWE-200'
116
+ cwe: 'CWE-200',
117
+ evidence: { finding: `X-Powered-By: ${headers['x-powered-by']}` }
107
118
  });
108
119
  }
109
120
 
@@ -111,7 +122,7 @@ export async function scanRemoteTarget(targetUrl) {
111
122
  if (typeof html === 'string') {
112
123
  const $ = cheerio.load(html);
113
124
 
114
- // Discover Links & Params
125
+ // Discover Links, Params, and JS files
115
126
  $('a').each((i, link) => {
116
127
  const href = $(link).attr('href');
117
128
  if (href && !href.startsWith('#') && !href.startsWith('mailto:')) {
@@ -120,7 +131,6 @@ export async function scanRemoteTarget(targetUrl) {
120
131
  if (absoluteUrl.startsWith(targetUrl)) {
121
132
  discoveredLinks.add(absoluteUrl);
122
133
 
123
- // Extract query parameters
124
134
  const urlObj = new URL(absoluteUrl);
125
135
  urlObj.searchParams.forEach((value, name) => discoveredParams.add(name));
126
136
  }
@@ -128,6 +138,18 @@ export async function scanRemoteTarget(targetUrl) {
128
138
  }
129
139
  });
130
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
+
131
153
  // Discover Forms
132
154
  $('form').each((i, form) => {
133
155
  const action = $(form).attr('action') || '';
@@ -141,6 +163,46 @@ export async function scanRemoteTarget(targetUrl) {
141
163
  });
142
164
  }
143
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
+
144
206
  // --- FUZZER (Path Discovery - Aggressive) ---
145
207
  const aggressivePaths = [
146
208
  '/.env', '/.git/config', '/admin', '/wp-admin', '/config.php', '/.vscode/settings.json',
@@ -240,4 +302,75 @@ export async function scanRemoteTarget(targetUrl) {
240
302
  discoveredParams: Array.from(discoveredParams),
241
303
  vulnerabilities
242
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
243
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
- if (v.severity === 'Critical') return acc + 25;
55
- if (v.severity === 'High') return acc + 15;
56
- if (v.severity === 'Medium') return acc + 10;
57
- return acc + 5;
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: 'Courier New', Courier, monospace; }
26
- .card { background-color: #16161a; border: 1px solid #333; }
27
- .critical { color: #ff4d4d; }
28
- .high { color: #ff944d; }
29
- .medium { color: #ffd11a; }
30
- .low { color: #4dff88; }
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-6xl mx-auto">
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">SEC-CLI</span></h1>
37
- <div class="text-right">
38
- <p class="text-gray-500">Scan ID: ${report.scan_id}</p>
39
- <p class="text-gray-500">${new Date(report.timestamp).toLocaleString()}</p>
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
- <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
44
- <div class="card p-6 rounded-lg shadow-xl">
45
- <h3 class="text-gray-400 mb-2">Security Score</h3>
46
- <p class="text-5xl font-bold ${report.score < 50 ? 'critical' : report.score < 75 ? 'high' : 'low'}">${report.score}/100</p>
47
- </div>
48
- <div class="card p-6 rounded-lg shadow-xl">
49
- <h3 class="text-gray-400 mb-2">Risk Level</h3>
50
- <p class="text-5xl font-bold ${report.riskLevel === 'Critical' ? 'critical' : report.riskLevel === 'High' ? 'high' : 'low'}">${report.riskLevel}</p>
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
- <div class="card p-6 rounded-lg shadow-xl">
53
- <h3 class="text-gray-400 mb-2">Vulnerabilities</h3>
54
- <p class="text-5xl font-bold text-white">${report.vulnerabilities.length}</p>
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
- <div class="card p-6 rounded-lg shadow-xl">
57
- <h3 class="text-gray-400 mb-2">Attack Surface</h3>
58
- <p class="text-sm text-gray-400">Endpoints: ${report.attack_surface.endpoints_discovered}</p>
59
- <p class="text-sm text-gray-400">Forms: ${report.attack_surface.forms_detected}</p>
60
- <p class="text-sm text-gray-400">Tech: ${(report.attack_surface.tech_stack || []).join(', ')}</p>
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
- <div class="card p-6 rounded-lg shadow-xl mb-8">
65
- <h2 class="text-xl font-bold mb-4 border-b border-gray-800 pb-2">Detected Vulnerabilities</h2>
66
- <div class="space-y-4">
67
- ${report.vulnerabilities.map(v => `
68
- <div class="border-l-4 ${v.severity === 'Critical' ? 'border-red-600' : v.severity === 'High' ? 'border-orange-500' : 'border-yellow-400'} bg-black bg-opacity-30 p-4 rounded">
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] Dashboard is running at:`));
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omen-sec-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.14",
4
4
  "description": "OMEN — AI Security Engine",
5
5
  "main": "bin/index.js",
6
6
  "type": "module",