omen-sec-cli 1.0.17 → 1.0.19

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.
@@ -109,22 +109,109 @@ export async function scanLocalProject() {
109
109
  filesScanned.push(path.basename(file));
110
110
 
111
111
  lines.forEach((line, index) => {
112
- // Regra 1: Hardcoded Secrets
113
- if (/(api_key|apikey|secret|password|token)\s*=\s*['"][a-zA-Z0-9_-]{10,}['"]/i.test(line)) {
112
+ // Regra 1: Hardcoded Secrets (Improved)
113
+ const secretRegex = /(api_key|apikey|secret|password|token|jwt_secret|connection_string|private_key|aws_key|aws_secret|db_pass)\s*[:=]\s*['"][a-zA-Z0-9_\-\.\/\+\=]{12,}['"]/i;
114
+ if (secretRegex.test(line)) {
114
115
  vulnerabilities.push({
115
- id: `LOC-VULN-${Date.now()}-3`,
116
+ id: `LOC-SEC-${Date.now()}-${index}`,
116
117
  kind: 'content',
117
118
  category: 'confirmed',
118
- confidence: 'medium',
119
+ confidence: 'high',
119
120
  severity: 'critical',
120
- title: 'Hardcoded Secret Detected',
121
- description: `Potential hardcoded secret found in ${path.basename(file)} at line ${index + 1}`,
121
+ title: 'Exposed Hardcoded Secret',
122
+ description: `A potential sensitive credential or token was found hardcoded in ${path.basename(file)} at line ${index + 1}.`,
122
123
  cwe: 'CWE-798',
123
124
  evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
124
- remediation: 'Move secrets to environment variables or a secure vault.'
125
+ remediation: 'Immediately rotate this secret and move it to an environment variable or a secret manager vault.'
126
+ });
127
+ }
128
+
129
+ // Regra 10: Insecure CORS configuration
130
+ if (/Access-Control-Allow-Origin.*\*['"]/i.test(line)) {
131
+ vulnerabilities.push({
132
+ id: `LOC-CORS-${Date.now()}-${index}`,
133
+ kind: 'content',
134
+ category: 'hardening',
135
+ confidence: 'high',
136
+ severity: 'medium',
137
+ title: 'Overly Permissive CORS Policy',
138
+ description: `CORS policy allows all origins (*) in ${path.basename(file)} at line ${index + 1}. This can lead to CSRF or data leakage.`,
139
+ cwe: 'CWE-942',
140
+ evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
141
+ remediation: 'Restrict Access-Control-Allow-Origin to specific, trusted domains instead of using "*".'
142
+ });
143
+ }
144
+
145
+ // Regra 11: Insecure Cookie Attributes
146
+ if (/res\.cookie\(.*\{/i.test(line) && !/httpOnly:\s*true/i.test(line)) {
147
+ vulnerabilities.push({
148
+ id: `LOC-COOKIE-${Date.now()}-${index}`,
149
+ kind: 'content',
150
+ category: 'hardening',
151
+ confidence: 'medium',
152
+ severity: 'low',
153
+ title: 'Insecure Cookie (Missing httpOnly)',
154
+ description: `Cookie created without 'httpOnly' flag in ${path.basename(file)} at line ${index + 1}. This makes the cookie accessible via client-side scripts (XSS risk).`,
155
+ cwe: 'CWE-1004',
156
+ evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
157
+ remediation: 'Always set { httpOnly: true } when creating sensitive cookies to prevent XSS-based theft.'
158
+ });
159
+ }
160
+
161
+ // Regra 12: Insecure Randomness
162
+ if (/Math\.random\(\)/.test(line)) {
163
+ vulnerabilities.push({
164
+ id: `LOC-RAND-${Date.now()}-${index}`,
165
+ kind: 'content',
166
+ category: 'hardening',
167
+ confidence: 'medium',
168
+ severity: 'low',
169
+ title: 'Insecure Randomness',
170
+ description: `Math.random() used in ${path.basename(file)} at line ${index + 1}. This is not cryptographically secure.`,
171
+ cwe: 'CWE-330',
172
+ evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
173
+ remediation: 'Use crypto.getRandomValues() or the "crypto" module for security-sensitive random values.'
174
+ });
175
+ }
176
+
177
+ // Regra 13: Potential ReDoS (Regex Denial of Service)
178
+ if (/\/\(.*\+\)\+\//.test(line) || /\/\(.*\*\)\*\//.test(line)) {
179
+ vulnerabilities.push({
180
+ id: `LOC-REDOS-${Date.now()}-${index}`,
181
+ kind: 'content',
182
+ category: 'probable',
183
+ confidence: 'medium',
184
+ severity: 'medium',
185
+ title: 'Potential ReDoS Pattern',
186
+ description: `A potentially catastrophic backtracking regex pattern was found in ${path.basename(file)} at line ${index + 1}.`,
187
+ cwe: 'CWE-1333',
188
+ evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
189
+ remediation: 'Review and simplify the regex pattern to avoid nested quantifiers that cause exponential backtracking.'
125
190
  });
126
191
  }
127
192
 
193
+ // Regra 8: Weak JWT Configuration
194
+ if (/jwt\.sign\(.*,\s*['"][^'"]{1,10}['"]\s*,/i.test(line)) {
195
+ vulnerabilities.push({
196
+ id: `LOC-AUTH-${Date.now()}`,
197
+ kind: 'content',
198
+ category: 'confirmed',
199
+ confidence: 'high',
200
+ severity: 'high',
201
+ title: 'Weak JWT Secret detected',
202
+ description: `The JWT signing secret appears to be a short, hardcoded string in ${path.basename(file)}: line ${index + 1}`,
203
+ cwe: 'CWE-522',
204
+ evidence: { file: path.basename(file), line: index + 1, code: line.trim() },
205
+ remediation: 'Use a strong, randomly generated secret stored in environment variables.'
206
+ });
207
+ }
208
+
209
+ // Regra 9: Missing CSRF Protection (Express example)
210
+ if (discoveryData && discoveryData.stack === 'Node/Express' && path.basename(file) === 'app.js') {
211
+ // This would require multi-line or AST, but let's do a simple check
212
+ }
213
+
214
+
128
215
  // Regra 2: Uso de Eval
129
216
  if (/eval\s*\(/.test(line)) {
130
217
  vulnerabilities.push({
@@ -15,7 +15,7 @@ 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.6 (Security Audit)' }
18
+ headers: { 'User-Agent': 'OMEN-SEC-CLI/1.0.19 (Security Audit)' }
19
19
  });
20
20
 
21
21
  serverStatus = response.status;
@@ -109,8 +109,25 @@ export async function scanRemoteTarget(targetUrl) {
109
109
  });
110
110
  }
111
111
 
112
+ // 3.1 Analisar X-Content-Type-Options
113
+ if (!headers['x-content-type-options']) {
114
+ headers_analysis["X-Content-Type-Options"] = "Missing";
115
+ vulnerabilities.push({
116
+ id: `REM-VULN-${Date.now()}-6`,
117
+ kind: 'header',
118
+ category: 'hardening',
119
+ confidence: 'high',
120
+ severity: 'low',
121
+ title: 'X-Content-Type-Options Missing',
122
+ description: `The X-Content-Type-Options: nosniff header is missing. This could allow the browser to "sniff" the content type, potentially leading to MIME-type sniffing attacks.`,
123
+ cwe: 'CWE-116',
124
+ evidence: { response: { headers: response.headers } },
125
+ remediation: 'Add the "X-Content-Type-Options: nosniff" header to all responses.'
126
+ });
127
+ }
128
+
112
129
  // 4. Server Header Leak
113
- if (headers['server']) {
130
+ if (headers['server'] && !headers['server'].includes('Cloudflare') && !headers['server'].includes('Vercel')) {
114
131
  headers_analysis["Server"] = headers['server'];
115
132
  vulnerabilities.push({
116
133
  id: `REM-VULN-${Date.now()}-5`,
@@ -126,22 +143,51 @@ export async function scanRemoteTarget(targetUrl) {
126
143
  });
127
144
  }
128
145
 
129
- // 5. X-Powered-By Leak
130
- if (headers['x-powered-by']) {
131
- vulnerabilities.push({
132
- id: `REM-VULN-${Date.now()}-6`,
133
- kind: 'tech',
134
- category: 'informational',
135
- confidence: 'high',
136
- severity: 'low',
137
- title: 'X-Powered-By Disclosure',
138
- description: `X-Powered-By header leaks framework information: ${headers['x-powered-by']}`,
139
- cwe: 'CWE-200',
140
- evidence: { finding: `X-Powered-By: ${headers['x-powered-by']}` },
141
- remediation: 'Disable the X-Powered-By header in the application framework settings.'
142
- });
146
+ // 6. CORS Analysis
147
+ if (headers['access-control-allow-origin']) {
148
+ const cors = headers['access-control-allow-origin'];
149
+ if (cors === '*') {
150
+ vulnerabilities.push({
151
+ id: `REM-CORS-${Date.now()}`,
152
+ kind: 'header',
153
+ category: 'confirmed',
154
+ confidence: 'high',
155
+ severity: 'high',
156
+ title: 'Permissive CORS Policy',
157
+ description: `The application allows Cross-Origin Resource Sharing from any domain (Access-Control-Allow-Origin: *).`,
158
+ cwe: 'CWE-942',
159
+ evidence: { finding: `Access-Control-Allow-Origin: ${cors}` },
160
+ remediation: 'Restrict Access-Control-Allow-Origin to trusted domains only.'
161
+ });
162
+ }
143
163
  }
144
164
 
165
+ // 7. Cookie Security
166
+ const setCookies = headers['set-cookie'] || [];
167
+ const cookies = Array.isArray(setCookies) ? setCookies : [setCookies];
168
+ cookies.forEach(cookie => {
169
+ const issues = [];
170
+ if (!cookie.toLowerCase().includes('secure')) issues.push('Missing "Secure" flag');
171
+ if (!cookie.toLowerCase().includes('httponly')) issues.push('Missing "HttpOnly" flag');
172
+ if (!cookie.toLowerCase().includes('samesite')) issues.push('Missing "SameSite" attribute');
173
+
174
+ if (issues.length > 0) {
175
+ vulnerabilities.push({
176
+ id: `REM-COOKIE-${Date.now()}`,
177
+ kind: 'header',
178
+ category: 'confirmed',
179
+ confidence: 'high',
180
+ severity: 'medium',
181
+ title: 'Insecure Cookie Configuration',
182
+ description: `Cookie detected with missing security flags: ${issues.join(', ')}.`,
183
+ cwe: 'CWE-614',
184
+ evidence: { cookie: cookie.split(';')[0] + '...', issues },
185
+ remediation: 'Apply Secure, HttpOnly, and SameSite=Strict/Lax attributes to all sensitive cookies.'
186
+ });
187
+ }
188
+ });
189
+
190
+
145
191
  // --- SPIDER / CRAWLER (DEEP) ---
146
192
  if (typeof html === 'string') {
147
193
  const $ = cheerio.load(html);
@@ -256,41 +302,32 @@ export async function scanRemoteTarget(targetUrl) {
256
302
  const fuzzUrl = new URL(path, targetUrl).href;
257
303
  const fuzzRes = await axios.get(fuzzUrl, {
258
304
  timeout: 5000,
259
- validateStatus: (status) => status === 200 || status === 403
305
+ validateStatus: (status) => status >= 200 && status < 500
260
306
  });
261
307
 
262
- if (fuzzRes.status === 200) {
263
- vulnerabilities.push({
264
- id: `REM-FUZZ-${Date.now()}-${path.replace(/\//g, '-')}`,
265
- type: 'Sensitive Path Exposed',
266
- severity: path.includes('.env') || path.includes('.git') || path.includes('.ssh') ? 'Critical' : 'High',
267
- description: `Exposed sensitive path discovered: ${fuzzUrl}. This path reveals internal configurations or credentials.`,
268
- cwe: 'CWE-200'
269
- });
270
- } else if (fuzzRes.status === 403) {
271
- vulnerabilities.push({
272
- id: `REM-FUZZ-${Date.now()}-${path.replace(/\//g, '-')}`,
273
- type: 'Potential Sensitive Path',
274
- severity: 'Low',
275
- description: `Path discovered but access forbidden (403): ${fuzzUrl}. Might indicate internal structure exposure.`,
276
- cwe: 'CWE-204'
277
- });
308
+ const finding = await validateFuzzerFinding(path, fuzzRes, fuzzUrl);
309
+ if (finding) {
310
+ vulnerabilities.push(finding);
278
311
  }
279
- } catch (e) {}
312
+ } catch (e) {
313
+ // Silently skip if path doesn't exist or times out
314
+ }
280
315
  }
281
316
 
282
317
  // --- OFFENSIVE PARAMETER FUZZING ---
283
318
  const injectionPayloads = [
284
- { type: 'SQLi', param: "' OR 1=1--", severity: 'Critical' },
285
- { type: 'XSS', param: "<script>alert('OMEN')</script>", severity: 'High' },
286
- { type: 'LFI', param: "/etc/passwd", severity: 'Critical' }
319
+ { type: 'SQLi', param: "' OR '1'='1", severity: 'Critical', cwe: 'CWE-89' },
320
+ { type: 'SQLi', param: '" OR "1"="1', severity: 'Critical', cwe: 'CWE-89' },
321
+ { type: 'XSS', param: "<script>alert('OMEN')</script>", severity: 'High', cwe: 'CWE-79' },
322
+ { type: 'XSS', param: "javascript:alert('OMEN')", severity: 'High', cwe: 'CWE-79' },
323
+ { type: 'LFI', param: "../../../../../etc/passwd", severity: 'Critical', cwe: 'CWE-22' }
287
324
  ];
288
325
 
289
326
  // Combine params from links and forms
290
327
  const allParams = Array.from(discoveredParams);
291
328
  discoveredForms.forEach(f => f.inputs.forEach(i => allParams.push(i)));
292
329
 
293
- const uniqueParams = [...new Set(allParams)].slice(0, 5); // Fuzz top 5 params
330
+ const uniqueParams = [...new Set(allParams)].slice(0, 10); // Fuzz top 10 params
294
331
 
295
332
  for (const param of uniqueParams) {
296
333
  for (const payload of injectionPayloads) {
@@ -298,26 +335,68 @@ export async function scanRemoteTarget(targetUrl) {
298
335
  const testUrl = new URL(targetUrl);
299
336
  testUrl.searchParams.append(param, payload.param);
300
337
 
301
- const res = await axios.get(testUrl.href, { timeout: 5000, validateStatus: () => true });
338
+ const res = await axios.get(testUrl.href, {
339
+ timeout: 5000,
340
+ validateStatus: () => true,
341
+ headers: { 'User-Agent': 'OMEN-SEC-CLI/1.0.19 (Security Audit)' }
342
+ });
302
343
 
344
+ const evidence = {
345
+ request: { url: testUrl.href, method: 'GET', parameter: param, payload: payload.param },
346
+ response: { status: res.status, body_snippet: typeof res.data === 'string' ? res.data.substring(0, 200) : '' }
347
+ };
348
+
303
349
  if (payload.type === 'XSS' && typeof res.data === 'string' && res.data.includes(payload.param)) {
304
350
  vulnerabilities.push({
305
- id: `REM-INJ-${Date.now()}-XSS`,
306
- type: 'Reflected XSS',
307
- severity: 'High',
308
- description: `Confirmed XSS at ${targetUrl} via parameter '${param}'. Payload was reflected in HTML.`,
309
- cwe: 'CWE-79'
351
+ id: `REM-INJ-${Date.now()}-XSS-${param}`,
352
+ kind: 'injection',
353
+ category: 'confirmed',
354
+ confidence: 'high',
355
+ severity: payload.severity,
356
+ title: 'Reflected Cross-Site Scripting (XSS)',
357
+ description: `Confirmed reflected XSS at ${targetUrl} via parameter '${param}'. The payload was reflected verbatim in the response body.`,
358
+ cwe: payload.cwe,
359
+ evidence,
360
+ remediation: 'Sanitize all user inputs before reflecting them in the HTML. Use output encoding libraries.'
310
361
  });
311
362
  }
312
363
 
313
- if (payload.type === 'SQLi' && (res.status === 500 || (typeof res.data === 'string' && /SQL|database|syntax/i.test(res.data)))) {
314
- vulnerabilities.push({
315
- id: `REM-INJ-${Date.now()}-SQLI`,
316
- type: 'SQL Injection',
317
- severity: 'Critical',
318
- description: `Potential SQLi detected via parameter '${param}'. Server showed error patterns on payload.`,
319
- cwe: 'CWE-89'
320
- });
364
+ const sqliPatterns = [
365
+ /SQL syntax/i, /mysql_fetch/i, /PostgreSQL.*ERROR/i, /Oracle.*Error/i,
366
+ /SQLite3::/i, /Dynamic SQL/i, /Syntax error.*in query/i
367
+ ];
368
+
369
+ if (payload.type === 'SQLi') {
370
+ const hasErrorPattern = typeof res.data === 'string' && sqliPatterns.some(p => p.test(res.data));
371
+ if (hasErrorPattern || res.status === 500) {
372
+ vulnerabilities.push({
373
+ id: `REM-INJ-${Date.now()}-SQLI-${param}`,
374
+ kind: 'injection',
375
+ category: 'probable',
376
+ confidence: hasErrorPattern ? 'high' : 'medium',
377
+ severity: payload.severity,
378
+ title: 'Potential SQL Injection',
379
+ description: `Potential SQLi detected via parameter '${param}'. The server returned a 500 error or a known SQL error pattern.`,
380
+ cwe: payload.cwe,
381
+ evidence,
382
+ remediation: 'Use parameterized queries or an ORM. Never concatenate user input directly into SQL strings.'
383
+ });
384
+ }
385
+ }
386
+
387
+ if (payload.type === 'LFI' && typeof res.data === 'string' && (res.data.includes('root:x:0:0') || res.data.includes('[boot loader]'))) {
388
+ vulnerabilities.push({
389
+ id: `REM-INJ-${Date.now()}-LFI-${param}`,
390
+ kind: 'injection',
391
+ category: 'confirmed',
392
+ confidence: 'high',
393
+ severity: payload.severity,
394
+ title: 'Local File Inclusion (LFI)',
395
+ description: `Confirmed LFI at ${targetUrl} via parameter '${param}'. System file content was detected in the response.`,
396
+ cwe: payload.cwe,
397
+ evidence,
398
+ remediation: 'Validate file paths against a whitelist and avoid using user-supplied input for file operations.'
399
+ });
321
400
  }
322
401
  } catch (e) {}
323
402
  }
@@ -0,0 +1,46 @@
1
+ export function generateFixPlan(scanData) {
2
+ let md = `# 🛡️ OMEN Security Fix Plan: ${scanData.target || 'Local Project'}\n\n`;
3
+ md += `> **Data do Scan:** ${scanData.timestamp || new Date().toLocaleString()} \n`;
4
+ md += `> **Score Atual:** ${scanData.score || 0}/100 \n`;
5
+ md += `> **Vulnerabilidades:** ${scanData.vulnerabilities?.length || 0}\n\n`;
6
+
7
+ md += `Este documento é um guia passo-a-passo para remediar as falhas encontradas. Após aplicar as correções, execute \`omen verify\` para confirmar a resolução.\n\n`;
8
+
9
+ const vulnerabilities = scanData.vulnerabilities || [];
10
+ const prioritized = [...vulnerabilities].sort((a, b) => {
11
+ const weights = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
12
+ return (weights[b.severity] || 0) - (weights[a.severity] || 0);
13
+ });
14
+
15
+ md += `## 🚨 Checklist de Remediação\n\n`;
16
+
17
+ if (prioritized.length === 0) {
18
+ md += `✅ Nenhuma vulnerabilidade encontrada que exija ação imediata.\n`;
19
+ } else {
20
+ prioritized.forEach((v, index) => {
21
+ const severityEmoji = v.severity === 'critical' ? '🔴' : v.severity === 'high' ? '🟠' : v.severity === 'medium' ? '🟡' : '🔵';
22
+
23
+ md += `### [ ] ${severityEmoji} [${v.severity.toUpperCase()}] ${v.title || v.description}\n`;
24
+ md += `- **ID:** \`${v.id}\` \n`;
25
+ md += `- **CWE:** ${v.cwe || 'N/A'} \n`;
26
+ md += `- **Confiança:** ${v.confidence || 'medium'} \n\n`;
27
+
28
+ if (v.evidence) {
29
+ md += `#### 🔍 Evidência\n`;
30
+ if (v.evidence.file) md += `- **Arquivo:** \`${v.evidence.file}\`${v.evidence.line ? ` (Linha ${v.evidence.line})` : ''} \n`;
31
+ if (v.evidence.code) md += `\`\`\`javascript\n${v.evidence.code}\n\`\`\`\n`;
32
+ if (v.evidence.finding) md += `- **Detalhe:** ${v.evidence.finding} \n`;
33
+ }
34
+
35
+ md += `#### 🛠️ Estratégia de Correção\n`;
36
+ md += `${v.remediation || 'Consulte a documentação de segurança para mitigar este risco.'}\n\n`;
37
+
38
+ md += `#### ✅ Comando de Verificação\n`;
39
+ md += `\`npx omen-sec-cli verify --id ${v.id}\` (ou apenas \`omen verify\` para checar tudo)\n\n`;
40
+ md += `---\n\n`;
41
+ });
42
+ }
43
+
44
+ md += `\n*Gerado automaticamente pelo OMEN SEC-CLI v1.0.19 - Protocolo Zero-Copy AI Ativo*\n`;
45
+ return md;
46
+ }
@@ -0,0 +1,25 @@
1
+ export function generateMarkdownReport(scanData) {
2
+ let md = `# OMEN Audit Report: ${scanData.target}\n\n`;
3
+ md += `**Scan ID:** \`${scanData.scan_id}\` \n`;
4
+ md += `**Timestamp:** ${scanData.timestamp} \n`;
5
+ md += `**Security Score:** ${scanData.score}/100 \n`;
6
+ md += `**Risk Level:** ${scanData.riskLevel}\n\n`;
7
+
8
+ md += `## Executive Summary\n`;
9
+ md += `The security audit identified ${scanData.vulnerabilities.length} total issues. `;
10
+ const confirmed = scanData.vulnerabilities.filter(v => v.category === 'confirmed').length;
11
+ md += `Of these, ${confirmed} are confirmed vulnerabilities that require immediate attention.\n\n`;
12
+
13
+ md += `## Detailed Findings\n\n`;
14
+ scanData.vulnerabilities.forEach(v => {
15
+ md += `### [${v.severity.toUpperCase()}] ${v.title || v.description}\n`;
16
+ md += `- **Category:** ${v.category}\n`;
17
+ md += `- **Confidence:** ${v.confidence}\n`;
18
+ md += `- **CWE:** ${v.cwe}\n\n`;
19
+ md += `#### Description\n${v.description}\n\n`;
20
+ md += `#### Remediation\n${v.remediation || 'Follow security best practices.'}\n\n`;
21
+ md += `---\n\n`;
22
+ });
23
+
24
+ return md;
25
+ }
@@ -0,0 +1,39 @@
1
+ import { execa } from 'execa';
2
+ import chalk from 'chalk';
3
+ import axios from 'axios';
4
+
5
+ export async function bootLocalApp(command, port = 3000, timeout = 30000) {
6
+ if (!command) return { success: false, error: 'No boot strategy found' };
7
+
8
+ console.log(chalk.cyan(`[Runner] Starting local app with: ${command}...`));
9
+
10
+ const [cmd, ...args] = command.split(' ');
11
+ const subprocess = execa(cmd, args, {
12
+ cleanup: true,
13
+ all: true
14
+ });
15
+
16
+ let bootLogs = '';
17
+ subprocess.all.on('data', (chunk) => {
18
+ bootLogs += chunk.toString();
19
+ });
20
+
21
+ // Wait for healthcheck or timeout
22
+ const start = Date.now();
23
+ while (Date.now() - start < timeout) {
24
+ try {
25
+ const res = await axios.get(`http://localhost:${port}`, { timeout: 1000, validateStatus: () => true });
26
+ if (res.status < 500) {
27
+ console.log(chalk.green(`[Runner] App is up and running on port ${port}!`));
28
+ return { success: true, subprocess, logs: bootLogs, port };
29
+ }
30
+ } catch (e) {
31
+ // Not up yet
32
+ }
33
+ await new Promise(r => setTimeout(r, 2000));
34
+ }
35
+
36
+ // If we reach here, boot failed or timed out
37
+ subprocess.kill();
38
+ return { success: false, error: 'App failed to start or healthcheck timed out', logs: bootLogs };
39
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ const STATE_FILE = path.join(process.cwd(), '.omen', 'context.json');
5
+
6
+ export async function saveState(data) {
7
+ try {
8
+ await fs.mkdir(path.dirname(STATE_FILE), { recursive: true });
9
+ const currentState = await loadState();
10
+ const newState = { ...currentState, ...data, last_updated: new Date().toISOString() };
11
+ await fs.writeFile(STATE_FILE, JSON.stringify(newState, null, 2));
12
+ return newState;
13
+ } catch (err) {
14
+ console.error('Failed to save OMEN state:', err.message);
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export async function loadState() {
20
+ try {
21
+ const data = await fs.readFile(STATE_FILE, 'utf-8');
22
+ return JSON.parse(data);
23
+ } catch (err) {
24
+ return {
25
+ schema_version: "1.0",
26
+ project: {},
27
+ discovery: {},
28
+ plan: {},
29
+ execution: {
30
+ static: [],
31
+ dynamic: [],
32
+ vulnerabilities: []
33
+ },
34
+ verification: {}
35
+ };
36
+ }
37
+ }
38
+
39
+ export async function clearState() {
40
+ try {
41
+ await fs.unlink(STATE_FILE);
42
+ } catch (err) {}
43
+ }