omen-sec-cli 1.0.19 → 1.0.21

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.
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { glob } from 'glob';
3
+ import axios from 'axios';
4
+ import * as cheerio from 'cheerio';
4
5
 
5
6
  export async function runDiscovery(projectPath = process.cwd()) {
6
7
  const discovery = {
@@ -9,18 +10,27 @@ export async function runDiscovery(projectPath = process.cwd()) {
9
10
  entrypoints: [],
10
11
  boot_strategy: null,
11
12
  critical_files: [],
12
- dependencies: {}
13
+ dependencies: {},
14
+ is_remote: false
13
15
  };
14
16
 
17
+ // Check if it's a URL
18
+ if (projectPath.startsWith('http')) {
19
+ discovery.is_remote = true;
20
+ return await runRemoteDiscovery(projectPath, discovery);
21
+ }
22
+
15
23
  // 1. Load package.json if exists
16
24
  try {
17
25
  const pkgPath = path.join(projectPath, 'package.json');
18
- const pkgData = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
26
+ const pkgContent = await fs.readFile(pkgPath, 'utf-8');
27
+ const pkgData = JSON.parse(pkgContent);
19
28
  discovery.dependencies = { ...pkgData.dependencies, ...pkgData.devDependencies };
20
29
 
21
30
  if (discovery.dependencies['next']) discovery.stack = 'Next.js';
22
31
  else if (discovery.dependencies['express']) discovery.stack = 'Node/Express';
23
32
  else if (discovery.dependencies['react']) discovery.stack = 'React SPA';
33
+ else discovery.stack = 'Node.js';
24
34
 
25
35
  // Boot strategy for Node
26
36
  if (pkgData.scripts) {
@@ -33,7 +43,7 @@ export async function runDiscovery(projectPath = process.cwd()) {
33
43
  try {
34
44
  const requirementsPath = path.join(projectPath, 'requirements.txt');
35
45
  await fs.access(requirementsPath);
36
- discovery.stack = discovery.stack === 'Unknown' ? 'Python' : discovery.stack;
46
+ if (discovery.stack === 'Unknown') discovery.stack = 'Python';
37
47
  discovery.critical_files.push('requirements.txt');
38
48
  } catch (e) {}
39
49
 
@@ -46,7 +56,7 @@ export async function runDiscovery(projectPath = process.cwd()) {
46
56
  } catch (e) {}
47
57
 
48
58
  // 4. Find entrypoints
49
- const commonEntrypoints = ['server.js', 'app.js', 'index.js', 'src/index.js', 'main.py', 'app.py'];
59
+ const commonEntrypoints = ['server.js', 'app.js', 'index.js', 'src/index.js', 'main.py', 'app.py', 'index.php'];
50
60
  for (const entry of commonEntrypoints) {
51
61
  try {
52
62
  await fs.access(path.join(projectPath, entry));
@@ -55,13 +65,52 @@ export async function runDiscovery(projectPath = process.cwd()) {
55
65
  }
56
66
 
57
67
  // 5. Critical Security Files
58
- const securityFiles = ['.env', '.env.local', '.env.production', '.git/config', 'docker-compose.yml', 'Dockerfile'];
68
+ const securityFiles = ['.env', '.env.local', '.env.production', '.git/config', 'docker-compose.yml', 'Dockerfile', 'package.json', 'composer.json', 'requirements.txt'];
59
69
  for (const file of securityFiles) {
60
70
  try {
61
71
  await fs.access(path.join(projectPath, file));
62
- discovery.critical_files.push(file);
72
+ if (!discovery.critical_files.includes(file)) discovery.critical_files.push(file);
63
73
  } catch (e) {}
64
74
  }
65
75
 
66
76
  return discovery;
67
77
  }
78
+
79
+ async function runRemoteDiscovery(url, discovery) {
80
+ try {
81
+ const response = await axios.get(url, {
82
+ timeout: 10000,
83
+ validateStatus: () => true,
84
+ headers: { 'User-Agent': 'OMEN-SEC-CLI/1.0.20 (Discovery)' }
85
+ });
86
+
87
+ const headers = response.headers;
88
+ const html = response.data || '';
89
+
90
+ // Header-based detection
91
+ if (headers['x-powered-by']) discovery.stack = headers['x-powered-by'];
92
+ if (headers['server']) discovery.stack = headers['server'];
93
+
94
+ // HTML-based detection
95
+ if (html.includes('next-head') || html.includes('_next/static')) discovery.stack = 'Next.js';
96
+ else if (html.includes('react-root') || html.includes('data-react')) discovery.stack = 'React';
97
+ else if (html.includes('wp-content')) discovery.stack = 'WordPress';
98
+ else if (html.includes('nuxt')) discovery.stack = 'Nuxt.js';
99
+
100
+ // Entrypoints as routes
101
+ const $ = cheerio.load(html);
102
+ $('a').each((i, el) => {
103
+ const href = $(el).attr('href');
104
+ if (href && !href.startsWith('#') && !href.startsWith('http') && discovery.entrypoints.length < 5) {
105
+ discovery.entrypoints.push(href);
106
+ }
107
+ });
108
+
109
+ discovery.entrypoints = [...new Set(discovery.entrypoints)];
110
+ discovery.boot_strategy = 'Remote URL';
111
+
112
+ } catch (err) {
113
+ discovery.stack = 'Unknown (Remote unreachable)';
114
+ }
115
+ return discovery;
116
+ }
package/core/engine-v2.js CHANGED
@@ -23,33 +23,49 @@ export async function plan() {
23
23
  const state = await loadState();
24
24
  const discovery = state.discovery;
25
25
 
26
- if (!discovery || !discovery.stack) {
26
+ if (!discovery || !discovery.stack || discovery.stack === 'Unknown') {
27
27
  console.log(chalk.red('No discovery data found. Run "omen discover" first.'));
28
28
  return;
29
29
  }
30
30
 
31
31
  const plan = {
32
32
  steps: [
33
- { id: 'static', action: 'Static Analysis', status: 'pending' },
34
- { id: 'dependencies', action: 'Dependency Audit', status: 'pending' }
33
+ { id: 'static', action: 'Static Analysis', status: 'pending' }
35
34
  ]
36
35
  };
37
36
 
38
- if (discovery.boot_strategy) {
37
+ // If local, check for dependencies
38
+ if (!discovery.is_remote) {
39
+ plan.steps.push({ id: 'dependencies', action: 'Dependency Audit', status: 'pending' });
40
+ }
41
+
42
+ if (discovery.boot_strategy && discovery.boot_strategy !== 'Remote URL') {
39
43
  plan.steps.push({ id: 'boot', action: `Boot App (${discovery.boot_strategy})`, status: 'pending' });
40
44
  plan.steps.push({ id: 'dynamic', action: 'Dynamic Analysis (Localhost)', status: 'pending' });
45
+ } else if (discovery.is_remote) {
46
+ plan.steps.push({ id: 'dynamic', action: 'Dynamic Analysis (Remote)', status: 'pending' });
41
47
  }
42
48
 
43
49
  console.log(chalk.white(`Plan generated with ${plan.steps.length} steps.`));
50
+ // IMPORTANTE: Retornar o plano e salvar no estado
44
51
  await saveState({ plan });
45
52
  return plan;
46
53
  }
47
54
 
48
55
  export async function execute() {
49
56
  console.log(chalk.bold.cyan('\n--- Phase 3: Execution ---'));
50
- const state = await loadState();
57
+
58
+ // Recarregar o estado para garantir que temos o plano
59
+ let state = await loadState();
60
+
61
+ if (!state.plan || !state.plan.steps || state.plan.steps.length === 0) {
62
+ console.log(chalk.yellow('No plan found in state. Attempting to generate one...'));
63
+ await plan();
64
+ state = await loadState(); // Recarregar após plan()
65
+ }
66
+
51
67
  if (!state.plan || !state.plan.steps) {
52
- console.log(chalk.red('No plan found. Run "omen plan" first.'));
68
+ console.log(chalk.red('Failed to generate or load plan. Run "omen discover" first.'));
53
69
  return;
54
70
  }
55
71
 
@@ -59,12 +75,18 @@ export async function execute() {
59
75
  for (const step of state.plan.steps) {
60
76
  console.log(chalk.yellow(`\nExecuting: ${step.action}...`));
61
77
 
62
- if (step.id === 'static') {
78
+ if (step.id === 'static' && !state.discovery.is_remote) {
63
79
  const localResult = await scanLocalProject();
64
80
  vulnerabilities.push(...localResult.vulnerabilities);
65
81
  executionLogs.push(`Static analysis scanned ${localResult.localFilesScanned} files.`);
66
82
  }
67
83
 
84
+ if (step.id === 'dynamic' && state.discovery.is_remote) {
85
+ const dynamicResult = await scanRemoteTarget(state.discovery.path);
86
+ vulnerabilities.push(...dynamicResult.vulnerabilities);
87
+ executionLogs.push(`Dynamic analysis performed on ${state.discovery.path}`);
88
+ }
89
+
68
90
  if (step.id === 'boot') {
69
91
  const bootResult = await bootLocalApp(state.discovery.boot_strategy);
70
92
  if (!bootResult.success) {
@@ -106,11 +128,18 @@ export async function execute() {
106
128
  const resultData = {
107
129
  execution,
108
130
  score,
109
- vulnerabilities,
131
+ vulnerabilities: vulnerabilities || [],
110
132
  riskLevel: score < 40 ? 'Critical' : score < 70 ? 'High' : score < 90 ? 'Medium' : 'Low',
111
133
  timestamp: new Date().toISOString(),
112
134
  target: state.discovery?.path || process.cwd(),
113
- scan_id: `OMEN-${Date.now()}`
135
+ scan_id: `OMEN-${Date.now()}`,
136
+ discovery: state.discovery || {},
137
+ plan: state.plan || {},
138
+ attack_surface: {
139
+ endpoints: state.discovery?.entrypoints || [],
140
+ tech_stack: state.discovery?.stack ? [state.discovery.stack] : [],
141
+ critical_files: state.discovery?.critical_files || []
142
+ }
114
143
  };
115
144
 
116
145
  await saveState(resultData);
@@ -15,12 +15,18 @@ export async function scanRemoteTarget(targetUrl) {
15
15
  const response = await axios.get(targetUrl, {
16
16
  timeout: 15000,
17
17
  validateStatus: () => true,
18
- headers: { 'User-Agent': 'OMEN-SEC-CLI/1.0.19 (Security Audit)' }
18
+ headers: { 'User-Agent': 'OMEN-SEC-CLI/1.0.21 (Security Audit)' }
19
19
  });
20
20
 
21
21
  serverStatus = response.status;
22
22
  const headers = response.headers;
23
- const html = response.data;
23
+ const html = response.data || '';
24
+
25
+ // --- WAF / CHALLENGE DETECTION ---
26
+ const isVercelChallenge = headers['x-vercel-mitigated'] === 'challenge' || (typeof html === 'string' && html.includes('Vercel Security Checkpoint'));
27
+ if (isVercelChallenge) {
28
+ techStack.push('Vercel WAF (Challenge Active)');
29
+ }
24
30
 
25
31
  // --- TECHNOLOGY FINGERPRINTING ---
26
32
  if (headers['x-powered-by']) techStack.push(headers['x-powered-by']);
@@ -32,98 +38,100 @@ export async function scanRemoteTarget(targetUrl) {
32
38
  if (html.includes('nuxt')) techStack.push('Nuxt.js');
33
39
 
34
40
  // --- Header Analysis (Existing - Refined Descriptions) ---
35
- if (!headers['strict-transport-security']) {
36
- headers_analysis["Strict-Transport-Security"] = "Missing";
37
- vulnerabilities.push({
38
- id: `REM-VULN-${Date.now()}-1`,
39
- kind: 'header',
40
- category: 'hardening',
41
- confidence: 'high',
42
- severity: 'medium',
43
- title: 'HSTS Header Missing',
44
- description: `HTTP Strict-Transport-Security (HSTS) header is missing. This prevents the browser from enforcing HTTPS-only connections for future visits.`,
45
- cwe: 'CWE-319',
46
- evidence: {
47
- request: { headers: { ...response.request.headers } },
48
- response: { status: response.status, headers: response.headers },
49
- reason: 'Security header "Strict-Transport-Security" not found in server response.'
50
- },
51
- remediation: 'Implement the Strict-Transport-Security header with a long max-age (e.g., 31536000) and the includeSubDomains directive.'
52
- });
53
- }
41
+ if (!isVercelChallenge) {
42
+ if (!headers['strict-transport-security']) {
43
+ headers_analysis["Strict-Transport-Security"] = "Missing";
44
+ vulnerabilities.push({
45
+ id: `REM-VULN-${Date.now()}-1`,
46
+ kind: 'header',
47
+ category: 'hardening',
48
+ confidence: 'high',
49
+ severity: 'medium',
50
+ title: 'HSTS Header Missing',
51
+ description: `HTTP Strict-Transport-Security (HSTS) header is missing. This prevents the browser from enforcing HTTPS-only connections for future visits.`,
52
+ cwe: 'CWE-319',
53
+ evidence: {
54
+ request: { headers: { ...response.request.headers } },
55
+ response: { status: response.status, headers: response.headers },
56
+ reason: 'Security header "Strict-Transport-Security" not found in server response.'
57
+ },
58
+ remediation: 'Implement the Strict-Transport-Security header with a long max-age (e.g., 31536000) and the includeSubDomains directive.'
59
+ });
60
+ }
54
61
 
55
- if (!headers['content-security-policy']) {
56
- headers_analysis["Content-Security-Policy"] = "Missing";
57
- vulnerabilities.push({
58
- id: `REM-VULN-${Date.now()}-2`,
59
- kind: 'header',
60
- category: 'confirmed',
61
- confidence: 'high',
62
- severity: 'high',
63
- title: 'Content-Security-Policy Missing',
64
- description: `CSP header is missing. Without a strict Content-Security-Policy, the application is highly vulnerable to Cross-Site Scripting (XSS) and data injection attacks.`,
65
- cwe: 'CWE-1022',
66
- evidence: {
67
- request: { headers: { ...response.request.headers } },
68
- response: { status: response.status, headers: response.headers },
69
- reason: 'Security header "Content-Security-Policy" not found in server response.'
70
- },
71
- remediation: 'Define a strict Content-Security-Policy to restrict source domains for scripts, styles, and other resources.'
72
- });
73
- } else {
74
- headers_analysis["Content-Security-Policy"] = headers['content-security-policy'];
75
- if (headers['content-security-policy'].includes("unsafe-inline")) {
62
+ if (!headers['content-security-policy']) {
63
+ headers_analysis["Content-Security-Policy"] = "Missing";
76
64
  vulnerabilities.push({
77
- id: `REM-VULN-${Date.now()}-3`,
65
+ id: `REM-VULN-${Date.now()}-2`,
78
66
  kind: 'header',
79
67
  category: 'confirmed',
80
68
  confidence: 'high',
81
69
  severity: 'high',
82
- title: 'Insecure CSP (unsafe-inline)',
83
- description: `The Content-Security-Policy allows 'unsafe-inline' for scripts or styles, significantly weakening protection against XSS.`,
84
- cwe: 'CWE-16',
85
- evidence: { finding: `policy: ${headers['content-security-policy']}` },
86
- remediation: 'Refactor the application to avoid inline scripts and styles, then remove "unsafe-inline" from the CSP.'
70
+ title: 'Content-Security-Policy Missing',
71
+ 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.`,
72
+ cwe: 'CWE-1022',
73
+ evidence: {
74
+ request: { headers: { ...response.request.headers } },
75
+ response: { status: response.status, headers: response.headers },
76
+ reason: 'Security header "Content-Security-Policy" not found in server response.'
77
+ },
78
+ remediation: 'Define a strict Content-Security-Policy to restrict source domains for scripts, styles, and other resources.'
87
79
  });
80
+ } else {
81
+ headers_analysis["Content-Security-Policy"] = headers['content-security-policy'];
82
+ if (headers['content-security-policy'].includes("unsafe-inline")) {
83
+ vulnerabilities.push({
84
+ id: `REM-VULN-${Date.now()}-3`,
85
+ kind: 'header',
86
+ category: 'confirmed',
87
+ confidence: 'high',
88
+ severity: 'high',
89
+ title: 'Insecure CSP (unsafe-inline)',
90
+ description: `The Content-Security-Policy allows 'unsafe-inline' for scripts or styles, significantly weakening protection against XSS.`,
91
+ cwe: 'CWE-16',
92
+ evidence: { finding: `policy: ${headers['content-security-policy']}` },
93
+ remediation: 'Refactor the application to avoid inline scripts and styles, then remove "unsafe-inline" from the CSP.'
94
+ });
95
+ }
88
96
  }
89
- }
90
97
 
91
- // 3. Analisar X-Frame-Options
92
- if (!headers['x-frame-options']) {
93
- headers_analysis["X-Frame-Options"] = "Missing";
94
- vulnerabilities.push({
95
- id: `REM-VULN-${Date.now()}-4`,
96
- kind: 'header',
97
- category: 'hardening',
98
- confidence: 'high',
99
- severity: 'low',
100
- title: 'X-Frame-Options Missing',
101
- description: `Missing X-Frame-Options header. This allows the application to be embedded in an iframe on other domains, increasing Clickjacking risk.`,
102
- cwe: 'CWE-1021',
103
- evidence: {
104
- request: { headers: { ...response.request.headers } },
105
- response: { status: response.status, headers: response.headers },
106
- reason: 'Security header "X-Frame-Options" not found. This allows the site to be embedded in iframes on third-party domains.'
107
- },
108
- remediation: 'Set the X-Frame-Options header to DENY or SAMEORIGIN.'
109
- });
110
- }
98
+ // 3. Analisar X-Frame-Options
99
+ if (!headers['x-frame-options']) {
100
+ headers_analysis["X-Frame-Options"] = "Missing";
101
+ vulnerabilities.push({
102
+ id: `REM-VULN-${Date.now()}-4`,
103
+ kind: 'header',
104
+ category: 'hardening',
105
+ confidence: 'high',
106
+ severity: 'low',
107
+ title: 'X-Frame-Options Missing',
108
+ description: `Missing X-Frame-Options header. This allows the application to be embedded in an iframe on other domains, increasing Clickjacking risk.`,
109
+ cwe: 'CWE-1021',
110
+ evidence: {
111
+ request: { headers: { ...response.request.headers } },
112
+ response: { status: response.status, headers: response.headers },
113
+ reason: 'Security header "X-Frame-Options" not found. This allows the site to be embedded in iframes on third-party domains.'
114
+ },
115
+ remediation: 'Set the X-Frame-Options header to DENY or SAMEORIGIN.'
116
+ });
117
+ }
111
118
 
112
- // 3.1 Analisar X-Content-Type-Options
113
- if (!headers['x-content-type-options']) {
114
- headers_analysis["X-Content-Type-Options"] = "Missing";
115
- vulnerabilities.push({
116
- id: `REM-VULN-${Date.now()}-6`,
117
- kind: 'header',
118
- category: 'hardening',
119
- confidence: 'high',
120
- severity: 'low',
121
- title: 'X-Content-Type-Options Missing',
122
- description: `The X-Content-Type-Options: nosniff header is missing. This could allow the browser to "sniff" the content type, potentially leading to MIME-type sniffing attacks.`,
123
- cwe: 'CWE-116',
124
- evidence: { response: { headers: response.headers } },
125
- remediation: 'Add the "X-Content-Type-Options: nosniff" header to all responses.'
126
- });
119
+ // 3.1 Analisar X-Content-Type-Options
120
+ if (!headers['x-content-type-options']) {
121
+ headers_analysis["X-Content-Type-Options"] = "Missing";
122
+ vulnerabilities.push({
123
+ id: `REM-VULN-${Date.now()}-6`,
124
+ kind: 'header',
125
+ category: 'hardening',
126
+ confidence: 'high',
127
+ severity: 'low',
128
+ title: 'X-Content-Type-Options Missing',
129
+ description: `The X-Content-Type-Options: nosniff header is missing. This could allow the browser to "sniff" the content type, potentially leading to MIME-type sniffing attacks.`,
130
+ cwe: 'CWE-116',
131
+ evidence: { response: { headers: response.headers } },
132
+ remediation: 'Add the "X-Content-Type-Options: nosniff" header to all responses.'
133
+ });
134
+ }
127
135
  }
128
136
 
129
137
  // 4. Server Header Leak
@@ -297,6 +305,8 @@ export async function scanRemoteTarget(targetUrl) {
297
305
  '/api/auth/session', '/api/graphql', '/actuator/health', '/.ssh/id_rsa'
298
306
  ];
299
307
 
308
+ const forbiddenPaths = [];
309
+
300
310
  for (const path of aggressivePaths) {
301
311
  try {
302
312
  const fuzzUrl = new URL(path, targetUrl).href;
@@ -305,6 +315,11 @@ export async function scanRemoteTarget(targetUrl) {
305
315
  validateStatus: (status) => status >= 200 && status < 500
306
316
  });
307
317
 
318
+ if (fuzzRes.status === 403) {
319
+ forbiddenPaths.push(path);
320
+ continue;
321
+ }
322
+
308
323
  const finding = await validateFuzzerFinding(path, fuzzRes, fuzzUrl);
309
324
  if (finding) {
310
325
  vulnerabilities.push(finding);
@@ -314,6 +329,21 @@ export async function scanRemoteTarget(targetUrl) {
314
329
  }
315
330
  }
316
331
 
332
+ if (forbiddenPaths.length > 0) {
333
+ vulnerabilities.push({
334
+ id: `REM-ENUM-FORBIDDEN-${Date.now()}`,
335
+ kind: 'path',
336
+ category: 'informational',
337
+ confidence: 'high',
338
+ severity: 'info',
339
+ title: 'Path Enumeration: Protected Resources',
340
+ description: `Multiple paths (${forbiddenPaths.length}) returned 403 Forbidden, confirming their existence but restricted access.`,
341
+ cwe: 'CWE-204',
342
+ evidence: { forbidden_paths: forbiddenPaths },
343
+ remediation: 'Ensure that 403 responses do not leak internal structure and that access controls are correctly configured.'
344
+ });
345
+ }
346
+
317
347
  // --- OFFENSIVE PARAMETER FUZZING ---
318
348
  const injectionPayloads = [
319
349
  { type: 'SQLi', param: "' OR '1'='1", severity: 'Critical', cwe: 'CWE-89' },
@@ -338,7 +368,7 @@ export async function scanRemoteTarget(targetUrl) {
338
368
  const res = await axios.get(testUrl.href, {
339
369
  timeout: 5000,
340
370
  validateStatus: () => true,
341
- headers: { 'User-Agent': 'OMEN-SEC-CLI/1.0.19 (Security Audit)' }
371
+ headers: { 'User-Agent': 'OMEN-SEC-CLI/1.0.21 (Security Audit)' }
342
372
  });
343
373
 
344
374
  const evidence = {
@@ -41,6 +41,6 @@ export function generateFixPlan(scanData) {
41
41
  });
42
42
  }
43
43
 
44
- md += `\n*Gerado automaticamente pelo OMEN SEC-CLI v1.0.19 - Protocolo Zero-Copy AI Ativo*\n`;
44
+ md += `\n*Gerado automaticamente pelo OMEN SEC-CLI v1.0.21 - Protocolo Zero-Copy AI Ativo*\n`;
45
45
  return md;
46
46
  }
package/core/ui-server.js CHANGED
@@ -44,6 +44,10 @@ export async function startUIServer() {
44
44
  <title>OMEN SEC-CLI Evidence Center</title>
45
45
  <script src="https://cdn.tailwindcss.com"></script>
46
46
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
47
+ <script>
48
+ // Ensure data safety for JS
49
+ const report = ${JSON.stringify(report)};
50
+ </script>
47
51
  <style>
48
52
  body { background-color: #0a0a0c; color: #e0e0e0; font-family: 'Inter', sans-serif; }
49
53
  .mono { font-family: 'JetBrains Mono', monospace; }
@@ -124,10 +128,10 @@ export async function startUIServer() {
124
128
  <span class="w-2 h-2 bg-blue-500 rounded-full mr-2"></span> Phase Intelligence
125
129
  </h2>
126
130
  <div class="space-y-4 text-sm">
127
- <div class="flex justify-between border-b border-gray-800 pb-2"> <span class="text-gray-500">Stack Detected</span> <span class="text-gray-300 font-bold">${report.discovery?.stack || 'N/A'}</span> </div>
128
- <div class="flex justify-between border-b border-gray-800 pb-2"> <span class="text-gray-500">Boot Strategy</span> <span class="mono text-gray-300">${report.discovery?.boot_strategy || 'None'}</span> </div>
129
- <div class="flex justify-between border-b border-gray-800 pb-2"> <span class="text-gray-500">Execution Steps</span> <span class="text-gray-300">${(report.plan?.steps || []).length} steps</span> </div>
130
- <div class="flex justify-between border-b border-gray-800 pb-2"> <span class="text-gray-500">Last Scan Status</span> <span class="text-green-500 font-bold uppercase">${report.execution?.status || 'N/A'}</span> </div>
131
+ <div class="flex justify-between border-b border-gray-800 pb-2"> <span class="text-gray-500">Target URL</span> <span class="mono text-gray-300">${(report && report.target) || 'N/A'}</span> </div>
132
+ <div class="flex justify-between border-b border-gray-800 pb-2"> <span class="text-gray-500">Tech Stack</span> <span class="text-gray-300">${(report && report.attack_surface && report.attack_surface.tech_stack && report.attack_surface.tech_stack.join(', ')) || 'N/A'}</span> </div>
133
+ <div class="flex justify-between border-b border-gray-800 pb-2"> <span class="text-gray-500">Endpoints Discovered</span> <span class="text-gray-300">${(report && report.attack_surface && report.attack_surface.endpoints && report.attack_surface.endpoints.length) || 0}</span> </div>
134
+ <div class="flex justify-between border-b border-gray-800 pb-2"> <span class="text-gray-500">Critical Files</span> <span class="text-gray-300">${(report && report.attack_surface && report.attack_surface.critical_files && report.attack_surface.critical_files.length) || 0}</span> </div>
131
135
  </div>
132
136
  </div>
133
137
  </div>
@@ -222,18 +226,18 @@ export async function startUIServer() {
222
226
  <div class="lg:col-span-2 space-y-8">
223
227
  <div class="card p-6 rounded-xl">
224
228
  <h3 class="text-xl font-bold mb-4">Discovered Assets</h3>
225
- <pre class="mono max-h-[600px]">${(report.discovery?.entrypoints || []).concat(report.discovery?.critical_files || []).join('\n') || 'No assets discovered.'}</pre>
229
+ <pre class="mono max-h-[600px]">${((report && report.discovery && report.discovery.entrypoints) || []).concat((report && report.discovery && report.discovery.critical_files) || []).join('\n') || 'No assets discovered.'}</pre>
226
230
  </div>
227
231
  </div>
228
232
  <div class="space-y-8">
229
233
  <div class="card p-6 rounded-xl">
230
234
  <h3 class="text-xl font-bold mb-4">Phase Log</h3>
231
- <pre class="mono text-[10px]">${(report.execution?.logs || []).join('\n') || 'No execution logs.'}</pre>
235
+ <pre class="mono text-[10px]">${(report && report.execution && report.execution.logs && report.execution.logs.join('\n')) || 'No execution logs.'}</pre>
232
236
  </div>
233
237
  <div class="card p-6 rounded-xl">
234
238
  <h3 class="text-xl font-bold mb-4">Tech Fingerprint</h3>
235
239
  <div class="flex flex-wrap gap-2">
236
- ${(report.attack_surface?.tech_stack || report.discovery?.stack ? [report.discovery.stack] : []).map(t => `<span class="px-3 py-1 bg-gray-800 rounded-full text-xs font-bold text-blue-400 border border-gray-700">${t}</span>`).join('') || '<span class="text-gray-500 italic">No stack identified.</span>'}
240
+ ${(report && report.discovery && report.discovery.stack ? [report.discovery.stack] : []).map(t => `<span class="px-3 py-1 bg-gray-800 rounded-full text-xs font-bold text-blue-400 border border-gray-700">${t}</span>`).join('') || '<span class="text-gray-500 italic">No stack identified.</span>'}
237
241
  </div>
238
242
  </div>
239
243
  </div>
@@ -262,7 +266,7 @@ export async function startUIServer() {
262
266
  </div>
263
267
 
264
268
  <footer class="text-center text-gray-600 mt-16 border-t border-gray-900 pt-8 mb-10">
265
- <p class="text-xs uppercase tracking-widest font-bold mb-2">OMEN Security Framework - v1.0.19</p>
269
+ <p class="text-xs uppercase tracking-widest font-bold mb-2">OMEN Security Framework - v1.0.21</p>
266
270
  <p class="text-[10px] text-gray-700 italic">"The eye that never sleeps, the code that never fails."</p>
267
271
  </footer>
268
272
  </div>