relsec 0.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +376 -0
  3. package/SECURITY.md +36 -0
  4. package/dist/advisories.d.ts +5 -0
  5. package/dist/advisories.js +111 -0
  6. package/dist/advisories.js.map +1 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +181 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +15 -0
  11. package/dist/config.js +50 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/engine.d.ts +8 -0
  14. package/dist/engine.js +76 -0
  15. package/dist/engine.js.map +1 -0
  16. package/dist/interactive.d.ts +58 -0
  17. package/dist/interactive.js +698 -0
  18. package/dist/interactive.js.map +1 -0
  19. package/dist/inventory.d.ts +3 -0
  20. package/dist/inventory.js +27 -0
  21. package/dist/inventory.js.map +1 -0
  22. package/dist/ioc.d.ts +16 -0
  23. package/dist/ioc.js +46 -0
  24. package/dist/ioc.js.map +1 -0
  25. package/dist/manifests.d.ts +10 -0
  26. package/dist/manifests.js +157 -0
  27. package/dist/manifests.js.map +1 -0
  28. package/dist/osv.d.ts +13 -0
  29. package/dist/osv.js +82 -0
  30. package/dist/osv.js.map +1 -0
  31. package/dist/reachability.d.ts +6 -0
  32. package/dist/reachability.js +19 -0
  33. package/dist/reachability.js.map +1 -0
  34. package/dist/report.d.ts +4 -0
  35. package/dist/report.js +74 -0
  36. package/dist/report.js.map +1 -0
  37. package/dist/security-modules.d.ts +14 -0
  38. package/dist/security-modules.js +126 -0
  39. package/dist/security-modules.js.map +1 -0
  40. package/dist/theme.d.ts +43 -0
  41. package/dist/theme.js +191 -0
  42. package/dist/theme.js.map +1 -0
  43. package/dist/types.d.ts +37 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/dist/version-info.d.ts +1 -0
  47. package/dist/version-info.js +2 -0
  48. package/dist/version-info.js.map +1 -0
  49. package/dist/version.d.ts +4 -0
  50. package/dist/version.js +74 -0
  51. package/dist/version.js.map +1 -0
  52. package/docs/RELEASE.md +41 -0
  53. package/package.json +64 -0
@@ -0,0 +1,698 @@
1
+ import { exec } from 'node:child_process';
2
+ import { readdirSync, statSync } from 'node:fs';
3
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline/promises';
7
+ import { stdin as input, stdout as output } from 'node:process';
8
+ import { listAdvisories } from './advisories.js';
9
+ import { migrateConfig } from './config.js';
10
+ import { analyzeCveRelevance } from './engine.js';
11
+ import { scanIocsInLogs } from './ioc.js';
12
+ import { renderMarkdownReport } from './report.js';
13
+ import { scanAuth, scanCloudTrail, scanExposure, scanSbom, scanSecrets, scanSigma, scanYara } from './security-modules.js';
14
+ import { prompt, renderBanner, renderError, renderHelp, renderIocSummary, renderOk, renderOptions, renderResultCard, renderSet, renderStatus } from './theme.js';
15
+ const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
16
+ const ROOT_COMMANDS = ['help', 'clear', 'cls', 'status', 'set', 'use', 'show', 'run', 'back', 'cve', 'scan', 'check', 'ioc', 'iocs', 'explain', 'evidence', 'actions', 'export', 'config', 'profile', 'workspace', 'sessions', 'session', 'exit', 'quit'];
17
+ const MODULES = ['cve', 'ioc', 'exposure', 'sbom', 'secrets', 'cloudtrail', 'auth', 'yara', 'sigma', 'asset', 'vuln', 'hunt'];
18
+ const SETTINGS = ['workspace', 'inventory', 'indicators', 'advisories', 'format', 'cve', 'logs', 'logdir', 'logglob', 'target', 'rule'];
19
+ const FORMATS = ['text', 'json', 'markdown'];
20
+ const DEFAULT_LOG_GLOB = '*.log';
21
+ export function createInteractiveSession(options = {}) {
22
+ return {
23
+ workspace: path.resolve(options.workspace ?? '.'),
24
+ inventory: options.inventory ? path.resolve(options.inventory) : undefined,
25
+ indicators: options.indicators ? path.resolve(options.indicators) : undefined,
26
+ advisories: options.advisories ? path.resolve(options.advisories) : undefined,
27
+ format: options.format ?? 'text',
28
+ configPath: options.configPath ?? path.resolve('.relevant.json'),
29
+ historyPath: options.historyPath ?? path.resolve('.relevant_history'),
30
+ logs: [],
31
+ logglob: DEFAULT_LOG_GLOB,
32
+ sessions: [],
33
+ profiles: {},
34
+ workspaces: {},
35
+ shouldExit: false,
36
+ shellRunner: options.shellRunner ?? runShell
37
+ };
38
+ }
39
+ export async function executeInteractiveCommand(session, line) {
40
+ const trimmed = line.trim();
41
+ if (!trimmed)
42
+ return '';
43
+ if (trimmed === 'exit' || trimmed === 'quit' || trimmed === '/exit') {
44
+ session.shouldExit = true;
45
+ return renderOk('bye');
46
+ }
47
+ if (trimmed === 'clear' || trimmed === 'cls')
48
+ return CLEAR_SCREEN;
49
+ if (trimmed === 'help' || trimmed === '?' || trimmed === '/help')
50
+ return renderHelp();
51
+ if (trimmed === 'status')
52
+ return status(session);
53
+ if (trimmed.startsWith('!'))
54
+ return session.shellRunner(trimmed.slice(1).trim());
55
+ const parts = splitCommand(trimmed);
56
+ const [command, ...args] = parts;
57
+ if (!command)
58
+ return '';
59
+ if (command === 'set')
60
+ return setValue(session, args);
61
+ if (command === 'use')
62
+ return useModule(session, args);
63
+ if (command === 'show')
64
+ return showCommand(session, args);
65
+ if (command === 'profile')
66
+ return profileCommand(session, args);
67
+ if (command === 'workspace')
68
+ return workspaceCommand(session, args);
69
+ if (command === 'sessions')
70
+ return renderSessions(session);
71
+ if (command === 'session')
72
+ return showSession(session, args);
73
+ if (command === 'run')
74
+ return runModule(session);
75
+ if (command === 'back') {
76
+ session.module = undefined;
77
+ return renderOk('module cleared');
78
+ }
79
+ if (command === 'config')
80
+ return configCommand(session, args);
81
+ if (command === 'cve' || command === 'scan' || command === 'check')
82
+ return runCve(session, args);
83
+ if (command === 'ioc' || command === 'iocs')
84
+ return runIoc(session, args);
85
+ if (command === 'explain')
86
+ return requireLast(session).summary;
87
+ if (command === 'evidence')
88
+ return requireLast(session).evidence.map((item) => `- ${item}`).join('\n');
89
+ if (command === 'actions')
90
+ return requireLast(session).recommendedActions.map((item) => `- ${item}`).join('\n');
91
+ if (command === 'export')
92
+ return exportLast(session, args);
93
+ throw new Error(`Unknown interactive command: ${command}. Type help for commands.`);
94
+ }
95
+ export async function startInteractive(session = createInteractiveSession()) {
96
+ output.write(`${renderBanner(session)}\n`);
97
+ const rl = readline.createInterface({
98
+ input,
99
+ output,
100
+ prompt: prompt(session.module),
101
+ completer: createCyclingCompleter(session)
102
+ });
103
+ rl.prompt();
104
+ for await (const line of rl) {
105
+ try {
106
+ await recordHistory(session, line);
107
+ const result = await executeInteractiveCommand(session, line);
108
+ if (result)
109
+ output.write(`${result}\n\n`);
110
+ if (session.shouldExit)
111
+ break;
112
+ }
113
+ catch (error) {
114
+ output.write(`${renderError(error.message)}\n\n`);
115
+ }
116
+ rl.setPrompt(prompt(session.module));
117
+ rl.prompt();
118
+ }
119
+ rl.close();
120
+ }
121
+ function setValue(session, args) {
122
+ const [key, ...valueParts] = args;
123
+ const value = valueParts.join(' ');
124
+ if (!key || !value)
125
+ return 'usage: set workspace <path> | set inventory <path> | set indicators <path> | set format <text|json|markdown>';
126
+ if (key === 'workspace') {
127
+ session.workspace = path.resolve(value);
128
+ return renderSet('workspace', session.workspace);
129
+ }
130
+ if (key === 'inventory') {
131
+ session.inventory = path.resolve(value);
132
+ return renderSet('inventory', session.inventory);
133
+ }
134
+ if (key === 'indicators') {
135
+ session.indicators = path.resolve(value);
136
+ return renderSet('indicators', session.indicators);
137
+ }
138
+ if (key === 'advisories') {
139
+ session.advisories = path.resolve(value);
140
+ return renderSet('advisories', session.advisories);
141
+ }
142
+ if (key === 'format') {
143
+ if (value !== 'text' && value !== 'json' && value !== 'markdown')
144
+ throw new Error(`Unsupported format: ${value}`);
145
+ session.format = value;
146
+ return renderSet('format', session.format);
147
+ }
148
+ if (key === 'cve') {
149
+ session.cve = value;
150
+ return renderSet('cve', session.cve);
151
+ }
152
+ if (key === 'logs') {
153
+ session.logs = value.split(',').map((entry) => path.resolve(unquote(entry.trim()))).filter(Boolean);
154
+ return renderSet('logs', session.logs.join(', '));
155
+ }
156
+ if (key === 'logdir') {
157
+ session.logdir = path.resolve(value);
158
+ return renderSet('logdir', session.logdir);
159
+ }
160
+ if (key === 'logglob') {
161
+ session.logglob = value;
162
+ return renderSet('logglob', session.logglob);
163
+ }
164
+ if (key === 'target') {
165
+ session.target = path.resolve(value);
166
+ return renderSet('target', session.target);
167
+ }
168
+ if (key === 'rule') {
169
+ session.rule = path.resolve(value);
170
+ return renderSet('rule', session.rule);
171
+ }
172
+ throw new Error(`Unknown setting: ${key}`);
173
+ }
174
+ function useModule(session, args) {
175
+ const moduleName = args[0];
176
+ if (!isModule(moduleName))
177
+ return `usage: use ${MODULES.join(' | ')}`;
178
+ session.module = moduleName;
179
+ return renderSet('module', moduleName);
180
+ }
181
+ function showCommand(session, args) {
182
+ if (args[0] === 'modules')
183
+ return [
184
+ 'Modules',
185
+ ' cve assess dependency CVE relevance',
186
+ ' ioc scan logs for indicators',
187
+ ' exposure list externally exposed services',
188
+ ' sbom inventory package manifests',
189
+ ' secrets find obvious local secrets',
190
+ ' cloudtrail triage AWS CloudTrail logs',
191
+ ' auth triage identity/auth logs',
192
+ ' yara apply simple YARA literal rules',
193
+ ' sigma apply simple Sigma keyword rules',
194
+ ' asset/vuln/hunt aliases for exposure/cve/ioc workflows'
195
+ ].join('\n');
196
+ if (args[0] === 'advisories')
197
+ return listAdvisories().map((advisory) => `${advisory.id} ${advisory.packageName} ${advisory.title}`).join('\n');
198
+ if (args[0] !== 'options')
199
+ return 'usage: show options | show modules | show advisories';
200
+ return renderOptions(session);
201
+ }
202
+ async function runModule(session) {
203
+ if (session.module === 'cve') {
204
+ if (!session.cve)
205
+ return 'set cve <CVE-ID> first';
206
+ return runCve(session, [session.cve]);
207
+ }
208
+ if (session.module === 'ioc') {
209
+ const logs = await currentLogs(session);
210
+ if (logs.length === 0)
211
+ return 'set logs <path>[,<path>...] or set logdir <dir> first';
212
+ return runIoc(session, logs);
213
+ }
214
+ if (session.module === 'asset')
215
+ return captureOutput(session, 'asset', 'exposure', await scanExposure(moduleInput(session)));
216
+ if (session.module === 'vuln')
217
+ return session.cve ? runCve(session, [session.cve]) : 'set cve <CVE-ID> first';
218
+ if (session.module === 'hunt')
219
+ return captureOutput(session, 'hunt', 'ioc hunt', await scanAuth({ ...moduleInput(session), logs: await currentLogs(session) }));
220
+ if (session.module === 'exposure')
221
+ return captureOutput(session, 'exposure', 'exposure', await scanExposure(moduleInput(session)));
222
+ if (session.module === 'sbom')
223
+ return captureOutput(session, 'sbom', 'sbom', await scanSbom(moduleInput(session)));
224
+ if (session.module === 'secrets')
225
+ return captureOutput(session, 'secrets', 'secrets', await scanSecrets(moduleInput(session)));
226
+ if (session.module === 'cloudtrail')
227
+ return captureOutput(session, 'cloudtrail', 'cloudtrail', await scanCloudTrail({ ...moduleInput(session), logs: await currentLogs(session) }));
228
+ if (session.module === 'auth')
229
+ return captureOutput(session, 'auth', 'auth', await scanAuth({ ...moduleInput(session), logs: await currentLogs(session) }));
230
+ if (session.module === 'yara')
231
+ return captureOutput(session, 'yara', 'yara', await scanYara({ ...moduleInput(session), logs: await currentLogs(session) }));
232
+ if (session.module === 'sigma')
233
+ return captureOutput(session, 'sigma', 'sigma', await scanSigma({ ...moduleInput(session), logs: await currentLogs(session) }));
234
+ return 'use cve or use ioc first';
235
+ }
236
+ async function runCve(session, args) {
237
+ const cveId = args[0];
238
+ if (!cveId)
239
+ return 'usage: cve <CVE-ID>';
240
+ const result = await analyzeCveRelevance({
241
+ workspace: session.workspace,
242
+ cveId,
243
+ inventoryPath: session.inventory,
244
+ advisoryPath: session.advisories
245
+ });
246
+ session.lastResult = result;
247
+ return captureOutput(session, 'cve', result.subject, formatResult(result, session.format));
248
+ }
249
+ async function runIoc(session, args) {
250
+ const indicators = session.indicators;
251
+ if (!indicators)
252
+ return 'usage: set indicators <path>, then ioc <log> [more logs...]';
253
+ if (args.length === 0)
254
+ return 'usage: ioc <log> [more logs...]';
255
+ const result = await scanIocsInLogs({
256
+ indicatorsPath: indicators,
257
+ logPaths: args.map((entry) => path.resolve(entry))
258
+ });
259
+ if (session.format === 'json')
260
+ return JSON.stringify(result, null, 2);
261
+ return captureOutput(session, 'ioc', `IOC matches: ${result.totalMatches}`, renderIocSummary(result));
262
+ }
263
+ async function configCommand(session, args) {
264
+ const [action] = args;
265
+ if (action === 'save') {
266
+ await mkdir(path.dirname(session.configPath), { recursive: true });
267
+ await writeFile(session.configPath, JSON.stringify({
268
+ schemaVersion: 1,
269
+ workspace: session.workspace,
270
+ inventory: session.inventory,
271
+ indicators: session.indicators,
272
+ format: session.format,
273
+ profiles: session.profiles,
274
+ workspaces: session.workspaces
275
+ }, null, 2));
276
+ return renderOk(`config saved to ${session.configPath}`);
277
+ }
278
+ if (action === 'show') {
279
+ return status(session);
280
+ }
281
+ return 'usage: config save | config show';
282
+ }
283
+ async function profileCommand(session, args) {
284
+ const [action, name] = args;
285
+ if (action === 'save' && name) {
286
+ session.profiles[name] = snapshotProfile(session);
287
+ await persistConsoleConfig(session);
288
+ return renderOk(`profile saved: ${name}`);
289
+ }
290
+ if (action === 'use' && name) {
291
+ await loadConsoleConfig(session);
292
+ const profile = session.profiles[name];
293
+ if (!profile)
294
+ throw new Error(`Unknown profile: ${name}`);
295
+ applyProfile(session, profile);
296
+ return renderSet('profile', name);
297
+ }
298
+ if (action === 'list') {
299
+ await loadConsoleConfig(session);
300
+ const names = Object.keys(session.profiles);
301
+ return ['Profiles', ...(names.length > 0 ? names.map((item) => `- ${item}`) : ['- none'])].join('\n');
302
+ }
303
+ return 'usage: profile save <name> | profile use <name> | profile list';
304
+ }
305
+ async function workspaceCommand(session, args) {
306
+ const [action, name, ...valueParts] = args;
307
+ const value = valueParts.join(' ');
308
+ if (action === 'add' && name && value) {
309
+ session.workspaces[name] = path.resolve(value);
310
+ await persistConsoleConfig(session);
311
+ return renderOk(`workspace saved: ${name}`);
312
+ }
313
+ if (action === 'use' && name) {
314
+ await loadConsoleConfig(session);
315
+ const workspace = session.workspaces[name];
316
+ if (!workspace)
317
+ throw new Error(`Unknown workspace: ${name}`);
318
+ session.workspace = workspace;
319
+ return renderSet('workspace', session.workspace);
320
+ }
321
+ if (action === 'list') {
322
+ await loadConsoleConfig(session);
323
+ const rows = Object.entries(session.workspaces).map(([key, value]) => `- ${key} ${value}`);
324
+ return ['Workspaces', ...(rows.length > 0 ? rows : ['- none'])].join('\n');
325
+ }
326
+ return 'usage: workspace add <name> <path> | workspace use <name> | workspace list';
327
+ }
328
+ async function exportLast(session, args) {
329
+ const [format, target] = args;
330
+ if (!target || (format !== 'json' && format !== 'markdown' && format !== 'text')) {
331
+ return 'usage: export markdown <file> | export json <file> | export text <file>';
332
+ }
333
+ const result = requireLast(session);
334
+ const body = formatResult(result, format);
335
+ await mkdir(path.dirname(path.resolve(target)), { recursive: true });
336
+ await writeFile(target, body);
337
+ return renderOk(`exported ${format} to ${target}`);
338
+ }
339
+ function formatResult(result, format) {
340
+ if (format === 'json')
341
+ return JSON.stringify(result, null, 2);
342
+ if (format === 'markdown')
343
+ return renderMarkdownReport(result);
344
+ return renderResultCard(result);
345
+ }
346
+ function requireLast(session) {
347
+ if (!session.lastResult)
348
+ throw new Error('No last result. Run cve <CVE-ID> first.');
349
+ return session.lastResult;
350
+ }
351
+ function status(session) {
352
+ return renderStatus(session);
353
+ }
354
+ export async function recordHistory(session, line) {
355
+ const trimmed = line.trim();
356
+ if (!trimmed)
357
+ return;
358
+ let existing = [];
359
+ try {
360
+ existing = (await readFile(session.historyPath, 'utf8')).split(/\r?\n/).filter(Boolean);
361
+ }
362
+ catch {
363
+ // no history yet
364
+ }
365
+ const next = [...existing.filter((entry) => entry !== trimmed), trimmed].slice(-500);
366
+ await mkdir(path.dirname(session.historyPath), { recursive: true });
367
+ await writeFile(session.historyPath, `${next.join('\n')}\n`);
368
+ }
369
+ function splitCommand(inputLine) {
370
+ const parts = [];
371
+ const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
372
+ for (const match of inputLine.matchAll(pattern)) {
373
+ parts.push(match[1] ?? match[2] ?? match[3] ?? '');
374
+ }
375
+ return parts;
376
+ }
377
+ export function completeInteractiveLine(session, line) {
378
+ const trimmedLeft = line.trimStart();
379
+ const leading = line.slice(0, line.length - trimmedLeft.length);
380
+ const endsWithSpace = /\s$/.test(line);
381
+ const parts = splitCommand(trimmedLeft);
382
+ const command = parts[0] ?? '';
383
+ if (parts.length === 0 || (parts.length === 1 && !endsWithSpace)) {
384
+ return [ROOT_COMMANDS.filter((candidate) => candidate.startsWith(command)).map((candidate) => leading + candidate), line];
385
+ }
386
+ if (command === 'use') {
387
+ const prefix = parts[1] ?? '';
388
+ return [MODULES.filter((moduleName) => moduleName.startsWith(prefix)).map((moduleName) => `${leading}use ${moduleName}`), line];
389
+ }
390
+ if (command === 'show')
391
+ return [filterFull(['show options', 'show modules', 'show advisories'], line), line];
392
+ if (command === 'set') {
393
+ const key = parts[1] ?? '';
394
+ if (parts.length === 1 && endsWithSpace)
395
+ return [SETTINGS.map((candidate) => `${leading}set ${candidate}`), line];
396
+ if (parts.length === 2 && !endsWithSpace)
397
+ return [SETTINGS.filter((candidate) => candidate.startsWith(key)).map((candidate) => `${leading}set ${candidate}`), line];
398
+ if (key === 'format')
399
+ return [FORMATS.map((format) => `${leading}set format ${format}`), line];
400
+ if (key === 'inventory' || key === 'indicators' || key === 'advisories' || key === 'workspace' || key === 'logs' || key === 'logdir' || key === 'target' || key === 'rule')
401
+ return [completePath(line), line];
402
+ return [[], line];
403
+ }
404
+ if (command === 'cve' || command === 'scan' || command === 'check') {
405
+ const prefix = parts[1] ?? '';
406
+ return [
407
+ listAdvisories()
408
+ .map((advisory) => advisory.id)
409
+ .filter((id) => id.startsWith(prefix.toUpperCase()))
410
+ .map((id) => `${leading}${command} ${id}`),
411
+ line
412
+ ];
413
+ }
414
+ if (command === 'ioc' || command === 'iocs' || command === 'export')
415
+ return [completePath(line), line];
416
+ if (command === 'config')
417
+ return [filterFull(['config save', 'config show'], line), line];
418
+ if (command === 'profile')
419
+ return [filterFull(['profile save', 'profile use', 'profile list'], line), line];
420
+ if (command === 'workspace')
421
+ return [filterFull(['workspace add', 'workspace use', 'workspace list'], line), line];
422
+ if (command === 'session')
423
+ return [filterFull(['session show'], line), line];
424
+ return [[], line];
425
+ }
426
+ export function createCyclingCompleter(session) {
427
+ let previous;
428
+ return (line) => {
429
+ const source = previous?.candidates[previous.index] === line ? previous.source : line;
430
+ const [candidates] = completeInteractiveLine(session, source);
431
+ if (candidates.length === 0) {
432
+ previous = undefined;
433
+ return [[], line];
434
+ }
435
+ const index = previous?.source === source && previous.candidates[previous.index] === line
436
+ ? (previous.index + 1) % candidates.length
437
+ : 0;
438
+ previous = { source, candidates, index };
439
+ return [[candidates[index] ?? line], line];
440
+ };
441
+ }
442
+ function filterFull(candidates, line) {
443
+ return candidates.filter((candidate) => candidate.startsWith(line.trimStart()));
444
+ }
445
+ function completePath(line) {
446
+ const token = currentPathToken(line);
447
+ const searchPartial = expandHome(token.partial);
448
+ const dir = searchPartial.endsWith('/') || searchPartial.endsWith('\\') ? searchPartial : path.dirname(searchPartial);
449
+ const base = searchPartial.endsWith('/') || searchPartial.endsWith('\\') ? '' : path.basename(searchPartial);
450
+ const searchDir = dir === '.' ? '.' : dir;
451
+ let entries;
452
+ try {
453
+ entries = readdirSync(searchDir);
454
+ }
455
+ catch {
456
+ return [];
457
+ }
458
+ return entries
459
+ .filter((entry) => entry.toLowerCase().startsWith(base.toLowerCase()))
460
+ .filter((entry) => base.startsWith('.') || !entry.startsWith('.'))
461
+ .filter((entry) => !['node_modules', '.git', 'dist'].includes(entry))
462
+ .sort((a, b) => {
463
+ const aDir = isDirectory(path.join(searchDir, a));
464
+ const bDir = isDirectory(path.join(searchDir, b));
465
+ if (aDir !== bDir)
466
+ return aDir ? -1 : 1;
467
+ return a.localeCompare(b);
468
+ })
469
+ .map((entry) => {
470
+ const full = path.join(searchDir, entry).replaceAll('\\', '/');
471
+ const suffix = isDirectory(full) ? '/' : '';
472
+ const completed = `${full}${suffix}`;
473
+ return `${token.prefix}${formatCompletedPath(completed, token.quote)}`;
474
+ });
475
+ }
476
+ function isModule(value) {
477
+ return MODULES.includes(value);
478
+ }
479
+ function captureOutput(session, kind, subject, outputText) {
480
+ session.lastOutput = outputText;
481
+ session.sessions.push({ id: session.sessions.length + 1, kind, subject, output: outputText });
482
+ return outputText;
483
+ }
484
+ function renderSessions(session) {
485
+ return ['Sessions', ...(session.sessions.length > 0
486
+ ? session.sessions.map((entry) => `#${entry.id} ${entry.kind} ${entry.subject}`)
487
+ : ['- none'])].join('\n');
488
+ }
489
+ function showSession(session, args) {
490
+ if (args[0] !== 'show' || !args[1])
491
+ return 'usage: session show <id>';
492
+ const item = session.sessions.find((entry) => entry.id === Number(args[1]));
493
+ if (!item)
494
+ throw new Error(`Unknown session: ${args[1]}`);
495
+ return item.output;
496
+ }
497
+ function moduleInput(session) {
498
+ return {
499
+ workspace: session.workspace,
500
+ inventory: session.inventory,
501
+ logs: session.logs,
502
+ target: session.target,
503
+ rule: session.rule
504
+ };
505
+ }
506
+ async function currentLogs(session) {
507
+ if (session.logs.length > 0)
508
+ return session.logs;
509
+ if (!session.logdir)
510
+ return [];
511
+ let entries;
512
+ try {
513
+ entries = await collectLogEntries(session.logdir);
514
+ }
515
+ catch {
516
+ return [];
517
+ }
518
+ const pattern = globToRegExp(session.logglob);
519
+ return entries
520
+ .filter((entry) => pattern.test(entry.replaceAll('\\', '/')))
521
+ .map((entry) => path.join(session.logdir ?? '', entry));
522
+ }
523
+ async function collectLogEntries(root, prefix = '') {
524
+ const entries = await readdir(path.join(root, prefix), { withFileTypes: true });
525
+ const results = [];
526
+ for (const entry of entries) {
527
+ const relative = prefix ? path.join(prefix, entry.name) : entry.name;
528
+ if (entry.isDirectory()) {
529
+ results.push(...await collectLogEntries(root, relative));
530
+ }
531
+ else if (entry.isFile()) {
532
+ results.push(relative);
533
+ }
534
+ }
535
+ return results;
536
+ }
537
+ function globToRegExp(glob) {
538
+ let source = '';
539
+ for (let i = 0; i < glob.length; i++) {
540
+ const ch = glob[i];
541
+ const next = glob[i + 1];
542
+ const afterNext = glob[i + 2];
543
+ if (ch === '*' && next === '*' && (afterNext === '/' || afterNext === '\\')) {
544
+ source += '(?:.*/)?';
545
+ i += 2;
546
+ }
547
+ else if (ch === '*') {
548
+ source += '[^/\\\\]*';
549
+ }
550
+ else if (ch === '?') {
551
+ source += '[^/\\\\]';
552
+ }
553
+ else if (ch === '/' || ch === '\\') {
554
+ source += '[/\\\\]';
555
+ }
556
+ else {
557
+ source += escapeRegExp(ch);
558
+ }
559
+ }
560
+ return new RegExp(`^${source}$`);
561
+ }
562
+ function escapeRegExp(value) {
563
+ return value.replace(/[.+^${}()|[\]\\]/g, '\\$&');
564
+ }
565
+ function snapshotProfile(session) {
566
+ return {
567
+ workspace: session.workspace,
568
+ inventory: session.inventory,
569
+ indicators: session.indicators,
570
+ advisories: session.advisories,
571
+ format: session.format,
572
+ logs: session.logs,
573
+ logdir: session.logdir,
574
+ logglob: session.logglob
575
+ };
576
+ }
577
+ function applyProfile(session, profile) {
578
+ session.workspace = profile.workspace;
579
+ session.inventory = profile.inventory;
580
+ session.indicators = profile.indicators;
581
+ session.advisories = profile.advisories;
582
+ session.format = profile.format;
583
+ session.logs = profile.logs ?? [];
584
+ session.logdir = profile.logdir;
585
+ session.logglob = profile.logglob ?? DEFAULT_LOG_GLOB;
586
+ }
587
+ async function loadConsoleConfig(session) {
588
+ try {
589
+ const parsed = migrateConfig(JSON.parse(await readFile(session.configPath, 'utf8')));
590
+ session.profiles = normalizeProfiles(parsed.profiles) ?? session.profiles;
591
+ session.workspaces = parsed.workspaces ?? session.workspaces;
592
+ }
593
+ catch {
594
+ // config is optional
595
+ }
596
+ }
597
+ async function persistConsoleConfig(session) {
598
+ await mkdir(path.dirname(session.configPath), { recursive: true });
599
+ await writeFile(session.configPath, JSON.stringify({
600
+ schemaVersion: 1,
601
+ workspace: session.workspace,
602
+ inventory: session.inventory,
603
+ indicators: session.indicators,
604
+ advisories: session.advisories,
605
+ format: session.format,
606
+ profiles: session.profiles,
607
+ workspaces: session.workspaces
608
+ }, null, 2));
609
+ }
610
+ function normalizeProfiles(value) {
611
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
612
+ return undefined;
613
+ const profiles = {};
614
+ for (const [name, profile] of Object.entries(value)) {
615
+ if (typeof profile !== 'object' || profile === null || Array.isArray(profile))
616
+ continue;
617
+ const candidate = profile;
618
+ if (typeof candidate.workspace !== 'string')
619
+ continue;
620
+ profiles[name] = {
621
+ workspace: candidate.workspace,
622
+ inventory: typeof candidate.inventory === 'string' ? candidate.inventory : undefined,
623
+ indicators: typeof candidate.indicators === 'string' ? candidate.indicators : undefined,
624
+ advisories: typeof candidate.advisories === 'string' ? candidate.advisories : undefined,
625
+ format: candidate.format === 'json' || candidate.format === 'markdown' || candidate.format === 'text' ? candidate.format : 'text',
626
+ logs: Array.isArray(candidate.logs) ? candidate.logs.filter((item) => typeof item === 'string') : [],
627
+ logdir: typeof candidate.logdir === 'string' ? candidate.logdir : undefined,
628
+ logglob: typeof candidate.logglob === 'string' ? candidate.logglob : DEFAULT_LOG_GLOB
629
+ };
630
+ }
631
+ return profiles;
632
+ }
633
+ function currentPathToken(line) {
634
+ let activeQuote;
635
+ let tokenStart = 0;
636
+ for (let i = 0; i < line.length; i++) {
637
+ const ch = line[i];
638
+ if (activeQuote) {
639
+ if (ch === activeQuote)
640
+ activeQuote = undefined;
641
+ continue;
642
+ }
643
+ if (ch === '"' || ch === "'") {
644
+ activeQuote = ch;
645
+ continue;
646
+ }
647
+ if (/\s/.test(ch ?? ''))
648
+ tokenStart = i + 1;
649
+ }
650
+ const rawToken = line.slice(tokenStart);
651
+ const quote = rawToken[0] === '"' || rawToken[0] === "'" ? rawToken[0] : undefined;
652
+ return {
653
+ prefix: line.slice(0, tokenStart),
654
+ partial: quote ? rawToken.slice(1) : rawToken,
655
+ quote
656
+ };
657
+ }
658
+ function expandHome(value) {
659
+ if (value === '~')
660
+ return homedir();
661
+ if (value.startsWith('~/') || value.startsWith('~\\'))
662
+ return path.join(homedir(), value.slice(2));
663
+ return value;
664
+ }
665
+ function formatCompletedPath(value, quote) {
666
+ if (quote)
667
+ return `${quote}${value}${quote}`;
668
+ return quoteIfNeeded(value);
669
+ }
670
+ function isDirectory(target) {
671
+ try {
672
+ return statSync(target).isDirectory();
673
+ }
674
+ catch {
675
+ return false;
676
+ }
677
+ }
678
+ function quoteIfNeeded(value) {
679
+ return /\s/.test(value) ? `"${value}"` : value;
680
+ }
681
+ function unquote(value) {
682
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
683
+ return value.slice(1, -1);
684
+ return value;
685
+ }
686
+ function runShell(command) {
687
+ return new Promise((resolve, reject) => {
688
+ exec(command, { cwd: process.cwd() }, (error, stdout, stderr) => {
689
+ if (error) {
690
+ reject(new Error(stderr.trim() || error.message));
691
+ }
692
+ else {
693
+ resolve(stdout.trimEnd());
694
+ }
695
+ });
696
+ });
697
+ }
698
+ //# sourceMappingURL=interactive.js.map