@aionlabsai/aion 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/agents/base-agent.d.ts.map +1 -1
- package/dist/agents/base-agent.js +23 -12
- package/dist/agents/base-agent.js.map +1 -1
- package/dist/agents/synthesizer.d.ts +3 -0
- package/dist/agents/synthesizer.d.ts.map +1 -1
- package/dist/agents/synthesizer.js +93 -52
- package/dist/agents/synthesizer.js.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +87 -534
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/chat.d.ts.map +1 -1
- package/dist/cli/commands/chat.js +2 -1
- package/dist/cli/commands/chat.js.map +1 -1
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.d.ts.map +1 -0
- package/dist/cli/commands/context.js +72 -0
- package/dist/cli/commands/context.js.map +1 -0
- package/dist/cli/commands/next.d.ts +3 -0
- package/dist/cli/commands/next.d.ts.map +1 -0
- package/dist/cli/commands/next.js +49 -0
- package/dist/cli/commands/next.js.map +1 -0
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +52 -400
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/search.d.ts +3 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +50 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/tree.d.ts +3 -0
- package/dist/cli/commands/tree.d.ts.map +1 -0
- package/dist/cli/commands/tree.js +85 -0
- package/dist/cli/commands/tree.js.map +1 -0
- package/dist/cli/menu.d.ts.map +1 -1
- package/dist/cli/menu.js +99 -56
- package/dist/cli/menu.js.map +1 -1
- package/dist/cli/tui.d.ts +1 -0
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -3
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/cost-tracker.d.ts +10 -0
- package/dist/core/cost-tracker.d.ts.map +1 -1
- package/dist/core/cost-tracker.js +53 -0
- package/dist/core/cost-tracker.js.map +1 -1
- package/dist/core/pipelines/audit-pipeline.d.ts +4 -1
- package/dist/core/pipelines/audit-pipeline.d.ts.map +1 -1
- package/dist/core/pipelines/audit-pipeline.js +60 -30
- package/dist/core/pipelines/audit-pipeline.js.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/audit-cache.d.ts +2 -1
- package/dist/infra/audit-cache.d.ts.map +1 -1
- package/dist/infra/audit-cache.js +18 -6
- package/dist/infra/audit-cache.js.map +1 -1
- package/dist/infra/audit-model.d.ts +72 -0
- package/dist/infra/audit-model.d.ts.map +1 -0
- package/dist/infra/audit-model.js +146 -0
- package/dist/infra/audit-model.js.map +1 -0
- package/dist/infra/audit-report-writer.d.ts +4 -0
- package/dist/infra/audit-report-writer.d.ts.map +1 -0
- package/dist/infra/audit-report-writer.js +290 -0
- package/dist/infra/audit-report-writer.js.map +1 -0
- package/dist/infra/code-metrics.d.ts +11 -0
- package/dist/infra/code-metrics.d.ts.map +1 -1
- package/dist/infra/code-metrics.js +62 -0
- package/dist/infra/code-metrics.js.map +1 -1
- package/dist/infra/embeddings.d.ts +2 -0
- package/dist/infra/embeddings.d.ts.map +1 -1
- package/dist/infra/embeddings.js +2 -2
- package/dist/infra/embeddings.js.map +1 -1
- package/dist/infra/project-report.d.ts +67 -0
- package/dist/infra/project-report.d.ts.map +1 -0
- package/dist/infra/project-report.js +159 -0
- package/dist/infra/project-report.js.map +1 -0
- package/dist/infra/repo-vectors.d.ts +31 -0
- package/dist/infra/repo-vectors.d.ts.map +1 -0
- package/dist/infra/repo-vectors.js +83 -0
- package/dist/infra/repo-vectors.js.map +1 -0
- package/dist/infra/update-check.d.ts +3 -0
- package/dist/infra/update-check.d.ts.map +1 -0
- package/dist/infra/update-check.js +84 -0
- package/dist/infra/update-check.js.map +1 -0
- package/dist/prompts/scanner.js +1 -1
- package/dist/prompts/synthesizer.d.ts.map +1 -1
- package/dist/prompts/synthesizer.js +7 -23
- package/dist/prompts/synthesizer.js.map +1 -1
- package/dist/providers/cli-provider.d.ts +1 -0
- package/dist/providers/cli-provider.d.ts.map +1 -1
- package/dist/providers/cli-provider.js +22 -2
- package/dist/providers/cli-provider.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,463 +1,28 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
2
|
import { Orchestrator } from '../../core/orchestrator.js';
|
|
5
3
|
import { AuditPipeline } from '../../core/pipelines/audit-pipeline.js';
|
|
6
4
|
import { createRuntimePolicy } from '../../core/runtime-policy.js';
|
|
7
5
|
import { CostTracker } from '../../core/cost-tracker.js';
|
|
8
6
|
import { Renderer } from '../ui/renderer.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
high: chalk.red.bold,
|
|
12
|
-
medium: chalk.yellow,
|
|
13
|
-
low: chalk.gray,
|
|
14
|
-
info: chalk.dim,
|
|
15
|
-
};
|
|
16
|
-
const SEVERITY_ICON = {
|
|
17
|
-
critical: '🔴',
|
|
18
|
-
high: '🟠',
|
|
19
|
-
medium: '🟡',
|
|
20
|
-
low: '⚪',
|
|
21
|
-
info: '🔵',
|
|
22
|
-
};
|
|
23
|
-
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
|
24
|
-
function esc(value) {
|
|
25
|
-
return String(value ?? '')
|
|
26
|
-
.replace(/&/g, '&')
|
|
27
|
-
.replace(/</g, '<')
|
|
28
|
-
.replace(/>/g, '>')
|
|
29
|
-
.replace(/"/g, '"');
|
|
30
|
-
}
|
|
7
|
+
import { saveAuditReport } from '../../infra/audit-report-writer.js';
|
|
8
|
+
import { SEVERITY_RANK } from '../../infra/audit-model.js';
|
|
31
9
|
function renderAuditReport(report, durationMs) {
|
|
32
|
-
|
|
33
|
-
console.log(chalk.bold.cyan(' AUDIT REPORT') + chalk.gray(` — ${report.totalFiles} files — ${(durationMs / 1000).toFixed(1)}s`));
|
|
34
|
-
console.log(chalk.bold('═'.repeat(60)));
|
|
35
|
-
// Summary
|
|
36
|
-
console.log('\n' + chalk.bold('Summary:'));
|
|
37
|
-
console.log(' ' + report.summary);
|
|
38
|
-
// Counts
|
|
10
|
+
const bySeverity = report.findings.reduce((acc, f) => { acc[f.severity] = (acc[f.severity] ?? 0) + 1; return acc; }, {});
|
|
39
11
|
const counts = [
|
|
40
12
|
report.criticalCount > 0 ? chalk.bgRed.white.bold(` ${report.criticalCount} critical `) : null,
|
|
41
13
|
report.highCount > 0 ? chalk.red.bold(`${report.highCount} high`) : null,
|
|
42
|
-
chalk.
|
|
43
|
-
chalk.gray(`${
|
|
14
|
+
(bySeverity['medium'] ?? 0) > 0 ? chalk.yellow(`${bySeverity['medium']} medium`) : null,
|
|
15
|
+
(bySeverity['low'] ?? 0) > 0 ? chalk.gray(`${bySeverity['low']} low`) : null,
|
|
44
16
|
].filter(Boolean);
|
|
45
|
-
console.log('\n' + chalk.bold('
|
|
46
|
-
|
|
17
|
+
console.log('\n' + chalk.bold.cyan('Audit complete') + chalk.gray(` — ${report.totalFiles} files, ${(durationMs / 1000).toFixed(1)}s`));
|
|
18
|
+
console.log(chalk.bold('Findings:') + ' ' + counts.join(' ') + chalk.dim(` (${report.findings.length} total)`));
|
|
19
|
+
console.log(' ' + chalk.dim(report.summary));
|
|
47
20
|
if (report.topPriorities.length > 0) {
|
|
48
|
-
console.log(
|
|
49
|
-
report.topPriorities.forEach((p, i) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
// Findings: if sections exist, render by domain; otherwise by severity
|
|
54
|
-
const auditReport = report;
|
|
55
|
-
if (auditReport.sections && auditReport.sections.length > 0) {
|
|
56
|
-
for (const section of auditReport.sections) {
|
|
57
|
-
if (section.findings.length === 0)
|
|
58
|
-
continue;
|
|
59
|
-
console.log('\n' + chalk.bold.blue(` ▸ ${section.domain.toUpperCase()} (${section.findings.length})`));
|
|
60
|
-
for (const f of section.findings) {
|
|
61
|
-
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
62
|
-
const color = SEVERITY_COLOR[f.severity] ?? chalk.white;
|
|
63
|
-
console.log(color(` ${loc}`) + chalk.gray(` [${f.severity}]`));
|
|
64
|
-
console.log(` ${f.finding}`);
|
|
65
|
-
console.log(chalk.dim(` → ${f.recommendation}`));
|
|
66
|
-
console.log();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
const order = ['critical', 'high', 'medium', 'low', 'info'];
|
|
72
|
-
for (const sev of order) {
|
|
73
|
-
const group = report.findings.filter(f => f.severity === sev);
|
|
74
|
-
if (group.length === 0)
|
|
75
|
-
continue;
|
|
76
|
-
const color = SEVERITY_COLOR[sev] ?? chalk.white;
|
|
77
|
-
const icon = SEVERITY_ICON[sev] ?? '';
|
|
78
|
-
console.log('\n' + color(` ${icon} ${sev.toUpperCase()} (${group.length}) `));
|
|
79
|
-
for (const f of group) {
|
|
80
|
-
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
81
|
-
console.log(chalk.bold(` ${loc}`) + chalk.gray(` [${f.category}]`));
|
|
82
|
-
console.log(` ${f.finding}`);
|
|
83
|
-
console.log(chalk.dim(` → ${f.recommendation}`));
|
|
84
|
-
console.log();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
console.log(chalk.bold('═'.repeat(60)));
|
|
89
|
-
}
|
|
90
|
-
function saveAuditReport(report, durationMs) {
|
|
91
|
-
const dir = join(process.cwd(), '.ai-runtime', 'reports');
|
|
92
|
-
mkdirSync(dir, { recursive: true });
|
|
93
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
94
|
-
const createdAt = new Date().toISOString();
|
|
95
|
-
const fullReport = { ...report, durationMs, createdAt };
|
|
96
|
-
const file = join(dir, `audit-${stamp}.json`);
|
|
97
|
-
writeFileSync(file, JSON.stringify(fullReport, null, 2), 'utf8');
|
|
98
|
-
const runDir = join(dir, 'audits', stamp);
|
|
99
|
-
mkdirSync(runDir, { recursive: true });
|
|
100
|
-
const html = join(runDir, 'index.html');
|
|
101
|
-
const summary = join(runDir, 'summary.md');
|
|
102
|
-
const actionPlan = join(runDir, 'action-plan.md');
|
|
103
|
-
writeFileSync(join(runDir, 'report.json'), JSON.stringify(fullReport, null, 2), 'utf8');
|
|
104
|
-
writeFileSync(join(runDir, 'findings-by-persona.json'), JSON.stringify(groupFindings(report.findings, (f) => f.persona ?? 'local'), null, 2), 'utf8');
|
|
105
|
-
writeFileSync(join(runDir, 'findings-by-severity.json'), JSON.stringify(groupFindings(report.findings, (f) => f.severity), null, 2), 'utf8');
|
|
106
|
-
writeFileSync(join(runDir, 'findings-by-category.json'), JSON.stringify(groupFindings(report.findings, (f) => f.category), null, 2), 'utf8');
|
|
107
|
-
writeFileSync(join(runDir, 'files-hotspots.json'), JSON.stringify(buildFileHotspots(report.findings), null, 2), 'utf8');
|
|
108
|
-
writeFileSync(join(runDir, 'action-items.json'), JSON.stringify(buildActionItems(report.findings), null, 2), 'utf8');
|
|
109
|
-
writeFileSync(join(runDir, 'README.md'), renderAuditIndex(fullReport), 'utf8');
|
|
110
|
-
writeFileSync(summary, renderAuditSummary(fullReport), 'utf8');
|
|
111
|
-
writeFileSync(actionPlan, renderActionPlan(fullReport), 'utf8');
|
|
112
|
-
writeFileSync(html, renderAuditHtml(fullReport), 'utf8');
|
|
113
|
-
writeFileSync(join(dir, 'latest-audit.json'), JSON.stringify({ report: file, runDir, html, summary, actionPlan, createdAt }, null, 2), 'utf8');
|
|
114
|
-
return { runDir, html, summary, actionPlan, report: file };
|
|
115
|
-
}
|
|
116
|
-
function groupFindings(findings, keyFn) {
|
|
117
|
-
const grouped = {};
|
|
118
|
-
for (const finding of findings) {
|
|
119
|
-
const key = keyFn(finding);
|
|
120
|
-
grouped[key] ??= [];
|
|
121
|
-
grouped[key].push(finding);
|
|
122
|
-
}
|
|
123
|
-
return grouped;
|
|
124
|
-
}
|
|
125
|
-
function compact(value, max = 120) {
|
|
126
|
-
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
127
|
-
if (normalized.length <= max)
|
|
128
|
-
return normalized;
|
|
129
|
-
return `${normalized.slice(0, max - 3)}...`;
|
|
130
|
-
}
|
|
131
|
-
function uniqueSorted(values) {
|
|
132
|
-
return [...new Set(values.filter(Boolean))].sort();
|
|
133
|
-
}
|
|
134
|
-
function maxSeverity(findings) {
|
|
135
|
-
return findings
|
|
136
|
-
.map((f) => f.severity)
|
|
137
|
-
.sort((a, b) => (SEVERITY_RANK[b] ?? 0) - (SEVERITY_RANK[a] ?? 0))[0] ?? 'info';
|
|
138
|
-
}
|
|
139
|
-
function actionKey(finding) {
|
|
140
|
-
const normalized = finding.finding
|
|
141
|
-
.toLowerCase()
|
|
142
|
-
.replace(/src\/[^\s:]+(:\d+)?/g, '')
|
|
143
|
-
.replace(/\d+/g, '#')
|
|
144
|
-
.replace(/[^a-z0-9]+/g, ' ')
|
|
145
|
-
.trim()
|
|
146
|
-
.slice(0, 90);
|
|
147
|
-
return `${finding.category}:${normalized}`;
|
|
148
|
-
}
|
|
149
|
-
function buildActionItems(findings) {
|
|
150
|
-
return Object.values(groupFindings(findings, actionKey))
|
|
151
|
-
.map((items) => {
|
|
152
|
-
const files = Object.entries(items.reduce((acc, finding) => {
|
|
153
|
-
acc[finding.file] ??= [];
|
|
154
|
-
if (typeof finding.line === 'number')
|
|
155
|
-
acc[finding.file].push(finding.line);
|
|
156
|
-
return acc;
|
|
157
|
-
}, {})).map(([file, lines]) => ({ file, lines: [...new Set(lines)].sort((a, b) => a - b) }));
|
|
158
|
-
const severity = maxSeverity(items);
|
|
159
|
-
return {
|
|
160
|
-
id: 0,
|
|
161
|
-
title: compact(items[0]?.finding ?? 'Review finding', 90),
|
|
162
|
-
severity,
|
|
163
|
-
category: items[0]?.category ?? 'maintainability',
|
|
164
|
-
personas: uniqueSorted(items.map((f) => f.persona ?? 'local')),
|
|
165
|
-
files,
|
|
166
|
-
findings: items.sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0)),
|
|
167
|
-
recommendation: compact(items[0]?.recommendation ?? 'Review and remediate.', 220),
|
|
168
|
-
};
|
|
169
|
-
})
|
|
170
|
-
.sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0) || b.findings.length - a.findings.length)
|
|
171
|
-
.map((item, index) => ({ ...item, id: index + 1 }));
|
|
172
|
-
}
|
|
173
|
-
function buildFileHotspots(findings) {
|
|
174
|
-
return Object.entries(groupFindings(findings, (f) => f.file))
|
|
175
|
-
.map(([file, items]) => ({
|
|
176
|
-
file,
|
|
177
|
-
findings: items.length,
|
|
178
|
-
maxSeverity: maxSeverity(items),
|
|
179
|
-
categories: uniqueSorted(items.map((f) => f.category)),
|
|
180
|
-
personas: uniqueSorted(items.map((f) => f.persona ?? 'local')),
|
|
181
|
-
}))
|
|
182
|
-
.sort((a, b) => (SEVERITY_RANK[b.maxSeverity] ?? 0) - (SEVERITY_RANK[a.maxSeverity] ?? 0) || b.findings - a.findings);
|
|
183
|
-
}
|
|
184
|
-
function markdownLocation(finding) {
|
|
185
|
-
return finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
186
|
-
}
|
|
187
|
-
function renderAuditSummary(report) {
|
|
188
|
-
const actions = buildActionItems(report.findings).slice(0, 10);
|
|
189
|
-
const hotspots = buildFileHotspots(report.findings).slice(0, 10);
|
|
190
|
-
const lines = [
|
|
191
|
-
'# Audit Summary',
|
|
192
|
-
'',
|
|
193
|
-
`Generated: ${report.createdAt}`,
|
|
194
|
-
`Duration: ${(report.durationMs / 1000).toFixed(1)}s`,
|
|
195
|
-
`Files covered by local scan: ${report.totalFiles}`,
|
|
196
|
-
`Findings: ${report.findings.length}`,
|
|
197
|
-
`Critical: ${report.criticalCount}`,
|
|
198
|
-
`High: ${report.highCount}`,
|
|
199
|
-
'',
|
|
200
|
-
'## Summary',
|
|
201
|
-
'',
|
|
202
|
-
report.summary,
|
|
203
|
-
'',
|
|
204
|
-
'## Top Priorities',
|
|
205
|
-
'',
|
|
206
|
-
...(report.topPriorities.length > 0 ? report.topPriorities.map((p, i) => `${i + 1}. ${p}`) : ['No top priorities.']),
|
|
207
|
-
'',
|
|
208
|
-
'## Consolidated Actions',
|
|
209
|
-
'',
|
|
210
|
-
...(actions.length > 0 ? actions.map((item) => `${item.id}. [${item.severity}] ${item.title} (${item.files.length} file(s))`) : ['No action items.']),
|
|
211
|
-
'',
|
|
212
|
-
'## File Hotspots',
|
|
213
|
-
'',
|
|
214
|
-
...(hotspots.length > 0 ? hotspots.map((h) => `- ${h.file}: ${h.findings} finding(s), max ${h.maxSeverity}`) : ['No file hotspots.']),
|
|
215
|
-
'',
|
|
216
|
-
'## Generated Files',
|
|
217
|
-
'',
|
|
218
|
-
'- `index.html`: navigable report',
|
|
219
|
-
'- `action-plan.md`: consolidated remediation plan',
|
|
220
|
-
'- `report.json`: raw machine-readable audit',
|
|
221
|
-
'- `findings-by-persona.json`: findings grouped by persona',
|
|
222
|
-
'- `findings-by-severity.json`: findings grouped by severity',
|
|
223
|
-
'- `findings-by-category.json`: findings grouped by category',
|
|
224
|
-
];
|
|
225
|
-
return lines.join('\n');
|
|
226
|
-
}
|
|
227
|
-
function renderActionPlan(report) {
|
|
228
|
-
const actions = buildActionItems(report.findings);
|
|
229
|
-
const lines = [
|
|
230
|
-
'# Audit Action Plan',
|
|
231
|
-
'',
|
|
232
|
-
`Generated: ${report.createdAt}`,
|
|
233
|
-
`Findings consolidated into ${actions.length} action item(s).`,
|
|
234
|
-
'',
|
|
235
|
-
];
|
|
236
|
-
if (actions.length === 0) {
|
|
237
|
-
lines.push('No remediation actions were produced by this audit.');
|
|
238
|
-
lines.push('');
|
|
239
|
-
return lines.join('\n');
|
|
21
|
+
console.log(chalk.bold('\nTop Priorities:'));
|
|
22
|
+
report.topPriorities.slice(0, 3).forEach((p, i) => console.log(chalk.cyan(` ${i + 1}.`) + ' ' + p));
|
|
23
|
+
if (report.topPriorities.length > 3)
|
|
24
|
+
console.log(chalk.dim(` ... ${report.topPriorities.length - 3} more in HTML`));
|
|
240
25
|
}
|
|
241
|
-
for (const item of actions) {
|
|
242
|
-
lines.push(`## ${item.id}. [${item.severity.toUpperCase()}] ${item.title}`);
|
|
243
|
-
lines.push('');
|
|
244
|
-
lines.push(`Category: ${item.category}`);
|
|
245
|
-
lines.push(`Personas: ${item.personas.join(', ') || 'local'}`);
|
|
246
|
-
lines.push('');
|
|
247
|
-
lines.push('Files:');
|
|
248
|
-
for (const file of item.files) {
|
|
249
|
-
const suffix = file.lines.length > 0 ? `:${file.lines.slice(0, 8).join(',')}` : '';
|
|
250
|
-
lines.push(`- ${file.file}${suffix}`);
|
|
251
|
-
}
|
|
252
|
-
lines.push('');
|
|
253
|
-
lines.push('Recommendation:');
|
|
254
|
-
lines.push(item.recommendation);
|
|
255
|
-
lines.push('');
|
|
256
|
-
lines.push('Evidence:');
|
|
257
|
-
for (const finding of item.findings.slice(0, 5)) {
|
|
258
|
-
lines.push(`- ${markdownLocation(finding)}: ${finding.finding}`);
|
|
259
|
-
}
|
|
260
|
-
if (item.findings.length > 5)
|
|
261
|
-
lines.push(`- ... ${item.findings.length - 5} more related finding(s)`);
|
|
262
|
-
lines.push('');
|
|
263
|
-
}
|
|
264
|
-
return lines.join('\n');
|
|
265
|
-
}
|
|
266
|
-
function renderAuditHtml(report) {
|
|
267
|
-
const actions = buildActionItems(report.findings);
|
|
268
|
-
const hotspots = buildFileHotspots(report.findings);
|
|
269
|
-
const byPersona = Object.entries(groupFindings(report.findings, (f) => f.persona ?? 'local'))
|
|
270
|
-
.sort((a, b) => b[1].length - a[1].length);
|
|
271
|
-
const bySeverity = Object.entries(groupFindings(report.findings, (f) => f.severity))
|
|
272
|
-
.sort((a, b) => (SEVERITY_RANK[b[0]] ?? 0) - (SEVERITY_RANK[a[0]] ?? 0));
|
|
273
|
-
const byCategory = Object.entries(groupFindings(report.findings, (f) => f.category))
|
|
274
|
-
.sort((a, b) => b[1].length - a[1].length);
|
|
275
|
-
const actionCards = actions.map((item) => `
|
|
276
|
-
<article class="card action" data-severity="${esc(item.severity)}" data-category="${esc(item.category)}">
|
|
277
|
-
<div class="row">
|
|
278
|
-
<span class="badge sev-${esc(item.severity)}">${esc(item.severity)}</span>
|
|
279
|
-
<span class="muted">#${item.id} · ${item.findings.length} finding(s) · ${item.files.length} file(s)</span>
|
|
280
|
-
</div>
|
|
281
|
-
<h3>${esc(item.title)}</h3>
|
|
282
|
-
<p>${esc(item.recommendation)}</p>
|
|
283
|
-
<div class="meta">Category: ${esc(item.category)} · Personas: ${esc(item.personas.join(', ') || 'local')}</div>
|
|
284
|
-
<details>
|
|
285
|
-
<summary>Evidence and files</summary>
|
|
286
|
-
<ul>
|
|
287
|
-
${item.files.map((file) => `<li><code>${esc(file.file)}${file.lines.length ? ':' + esc(file.lines.slice(0, 8).join(',')) : ''}</code></li>`).join('')}
|
|
288
|
-
</ul>
|
|
289
|
-
<ol>
|
|
290
|
-
${item.findings.slice(0, 6).map((finding) => `<li><code>${esc(markdownLocation(finding))}</code> ${esc(finding.finding)}</li>`).join('')}
|
|
291
|
-
</ol>
|
|
292
|
-
</details>
|
|
293
|
-
</article>`).join('');
|
|
294
|
-
const findingRows = report.findings.map((finding) => `
|
|
295
|
-
<tr data-severity="${esc(finding.severity)}" data-category="${esc(finding.category)}" data-persona="${esc(finding.persona ?? 'local')}" data-file="${esc(finding.file)}">
|
|
296
|
-
<td><span class="badge sev-${esc(finding.severity)}">${esc(finding.severity)}</span></td>
|
|
297
|
-
<td>${esc(finding.category)}</td>
|
|
298
|
-
<td>${esc(finding.persona ?? 'local')}</td>
|
|
299
|
-
<td><code>${esc(markdownLocation(finding))}</code></td>
|
|
300
|
-
<td>${esc(finding.finding)}<div class="recommendation">${esc(finding.recommendation)}</div></td>
|
|
301
|
-
</tr>`).join('');
|
|
302
|
-
const statList = (items, label) => items.map(([key, findings]) => {
|
|
303
|
-
const severity = maxSeverity(findings);
|
|
304
|
-
return `<li><span>${esc(key)}</span><strong class="sev-text-${esc(severity)}">${findings.length}</strong><small>${esc(label)} · max ${esc(severity)}</small></li>`;
|
|
305
|
-
}).join('');
|
|
306
|
-
return `<!doctype html>
|
|
307
|
-
<html lang="en">
|
|
308
|
-
<head>
|
|
309
|
-
<meta charset="utf-8">
|
|
310
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
311
|
-
<title>Aion Audit Report</title>
|
|
312
|
-
<style>
|
|
313
|
-
:root{color-scheme:dark;--bg:#0d1117;--panel:#161b22;--line:#30363d;--text:#e6edf3;--muted:#8b949e;--blue:#79c0ff;--green:#3fb950;--yellow:#e3b341;--red:#f85149;--gray:#6e7681}
|
|
314
|
-
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
315
|
-
a{color:var(--blue);text-decoration:none}code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#c9d1d9}
|
|
316
|
-
.layout{display:grid;grid-template-columns:250px 1fr;min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;border-right:1px solid var(--line);background:#010409;padding:22px;overflow:auto}
|
|
317
|
-
.sidebar h1{font-size:18px;margin:0 0 4px}.sidebar p{margin:0 0 18px;color:var(--muted);font-size:12px}.sidebar a{display:block;padding:7px 0;color:var(--muted)}.sidebar a:hover{color:var(--text)}
|
|
318
|
-
main{padding:28px;max-width:1300px}.hero{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;border-bottom:1px solid var(--line);padding-bottom:22px;margin-bottom:22px}
|
|
319
|
-
h2{font-size:20px;margin:34px 0 14px;color:var(--blue)}h3{font-size:15px;margin:10px 0}.muted,.meta{color:var(--muted);font-size:12px}.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
|
|
320
|
-
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px}.num{font-size:32px;font-weight:700}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
321
|
-
.badge{display:inline-flex;align-items:center;border-radius:4px;padding:2px 8px;font-size:11px;font-weight:700;text-transform:uppercase}.sev-critical{background:#f8514920;color:var(--red)}.sev-high{background:#e3b34120;color:var(--yellow)}.sev-medium{background:#79c0ff20;color:var(--blue)}.sev-low{background:#8b949e20;color:var(--muted)}.sev-info{background:#6e768120;color:var(--gray)}
|
|
322
|
-
.sev-text-critical{color:var(--red)}.sev-text-high{color:var(--yellow)}.sev-text-medium{color:var(--blue)}.sev-text-low,.sev-text-info{color:var(--muted)}
|
|
323
|
-
.action{margin-bottom:12px}.action p{margin:6px 0 8px}.action details{margin-top:10px}.action summary{cursor:pointer;color:var(--blue)}
|
|
324
|
-
.stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.stats ul{list-style:none;margin:0;padding:0}.stats li{display:grid;grid-template-columns:1fr auto;gap:3px;border-bottom:1px solid #21262d;padding:8px 0}.stats small{grid-column:1/-1;color:var(--muted)}
|
|
325
|
-
.filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}.filters input,.filters select{background:#010409;color:var(--text);border:1px solid var(--line);border-radius:6px;padding:8px}
|
|
326
|
-
table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:8px;overflow:hidden}th,td{text-align:left;vertical-align:top;border-bottom:1px solid #21262d;padding:9px}th{color:var(--muted);font-size:12px}.recommendation{color:var(--muted);font-size:12px;margin-top:4px}
|
|
327
|
-
.hotspots{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.hotspot{display:grid;grid-template-columns:1fr auto;gap:4px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px}
|
|
328
|
-
@media (max-width:900px){.layout{grid-template-columns:1fr}.sidebar{position:relative;height:auto}.cards,.stats,.hotspots{grid-template-columns:1fr}.hero{display:block}}
|
|
329
|
-
</style>
|
|
330
|
-
</head>
|
|
331
|
-
<body>
|
|
332
|
-
<div class="layout">
|
|
333
|
-
<nav class="sidebar">
|
|
334
|
-
<h1>Aion Audit</h1>
|
|
335
|
-
<p>${esc(report.createdAt)}</p>
|
|
336
|
-
<a href="#overview">Overview</a>
|
|
337
|
-
<a href="#actions">Action Plan</a>
|
|
338
|
-
<a href="#findings">Findings</a>
|
|
339
|
-
<a href="#personas">Personas</a>
|
|
340
|
-
<a href="#files">Files</a>
|
|
341
|
-
<a href="#raw">Raw Data</a>
|
|
342
|
-
</nav>
|
|
343
|
-
<main>
|
|
344
|
-
<section class="hero" id="overview">
|
|
345
|
-
<div>
|
|
346
|
-
<h1>Audit Report</h1>
|
|
347
|
-
<p class="muted">${esc(report.summary)}</p>
|
|
348
|
-
</div>
|
|
349
|
-
<div class="meta">Duration ${(report.durationMs / 1000).toFixed(1)}s · ${report.totalFiles} files covered</div>
|
|
350
|
-
</section>
|
|
351
|
-
<section class="cards">
|
|
352
|
-
<div class="card"><div class="num">${report.findings.length}</div><div class="muted">Findings</div></div>
|
|
353
|
-
<div class="card"><div class="num sev-text-critical">${report.criticalCount}</div><div class="muted">Critical</div></div>
|
|
354
|
-
<div class="card"><div class="num sev-text-high">${report.highCount}</div><div class="muted">High</div></div>
|
|
355
|
-
<div class="card"><div class="num">${actions.length}</div><div class="muted">Action items</div></div>
|
|
356
|
-
</section>
|
|
357
|
-
<section>
|
|
358
|
-
<h2>Top Priorities</h2>
|
|
359
|
-
<ol>${report.topPriorities.length ? report.topPriorities.map((p) => `<li>${esc(p)}</li>`).join('') : '<li>No top priorities.</li>'}</ol>
|
|
360
|
-
</section>
|
|
361
|
-
<section id="actions">
|
|
362
|
-
<h2>Action Plan</h2>
|
|
363
|
-
${actionCards || '<p class="muted">No action items.</p>'}
|
|
364
|
-
</section>
|
|
365
|
-
<section id="findings">
|
|
366
|
-
<h2>Findings</h2>
|
|
367
|
-
<div class="filters">
|
|
368
|
-
<input id="q" placeholder="Search findings, files, recommendations">
|
|
369
|
-
<select id="severity"><option value="">All severities</option>${bySeverity.map(([s]) => `<option value="${esc(s)}">${esc(s)}</option>`).join('')}</select>
|
|
370
|
-
<select id="persona"><option value="">All personas</option>${byPersona.map(([p]) => `<option value="${esc(p)}">${esc(p)}</option>`).join('')}</select>
|
|
371
|
-
<select id="category"><option value="">All categories</option>${byCategory.map(([c]) => `<option value="${esc(c)}">${esc(c)}</option>`).join('')}</select>
|
|
372
|
-
</div>
|
|
373
|
-
<table id="findingsTable"><thead><tr><th>Severity</th><th>Category</th><th>Persona</th><th>Location</th><th>Finding</th></tr></thead><tbody>${findingRows}</tbody></table>
|
|
374
|
-
</section>
|
|
375
|
-
<section id="personas">
|
|
376
|
-
<h2>Personas And Categories</h2>
|
|
377
|
-
<div class="stats">
|
|
378
|
-
<div class="card"><h3>By Persona</h3><ul>${statList(byPersona, 'persona')}</ul></div>
|
|
379
|
-
<div class="card"><h3>By Severity</h3><ul>${statList(bySeverity, 'severity')}</ul></div>
|
|
380
|
-
<div class="card"><h3>By Category</h3><ul>${statList(byCategory, 'category')}</ul></div>
|
|
381
|
-
</div>
|
|
382
|
-
</section>
|
|
383
|
-
<section id="files">
|
|
384
|
-
<h2>File Hotspots</h2>
|
|
385
|
-
<div class="hotspots">${hotspots.slice(0, 30).map((h) => `<div class="hotspot"><code>${esc(h.file)}</code><strong class="sev-text-${esc(h.maxSeverity)}">${h.findings}</strong><small class="muted">${esc(h.categories.join(', '))}</small></div>`).join('') || '<p class="muted">No hotspots.</p>'}</div>
|
|
386
|
-
</section>
|
|
387
|
-
<section id="raw">
|
|
388
|
-
<h2>Raw Data</h2>
|
|
389
|
-
<ul>
|
|
390
|
-
<li><a href="report.json">report.json</a></li>
|
|
391
|
-
<li><a href="action-items.json">action-items.json</a></li>
|
|
392
|
-
<li><a href="findings-by-persona.json">findings-by-persona.json</a></li>
|
|
393
|
-
<li><a href="findings-by-severity.json">findings-by-severity.json</a></li>
|
|
394
|
-
<li><a href="findings-by-category.json">findings-by-category.json</a></li>
|
|
395
|
-
<li><a href="files-hotspots.json">files-hotspots.json</a></li>
|
|
396
|
-
</ul>
|
|
397
|
-
</section>
|
|
398
|
-
</main>
|
|
399
|
-
</div>
|
|
400
|
-
<script>
|
|
401
|
-
const q = document.getElementById('q');
|
|
402
|
-
const severity = document.getElementById('severity');
|
|
403
|
-
const persona = document.getElementById('persona');
|
|
404
|
-
const category = document.getElementById('category');
|
|
405
|
-
const rows = Array.from(document.querySelectorAll('#findingsTable tbody tr'));
|
|
406
|
-
function applyFilters() {
|
|
407
|
-
const text = q.value.toLowerCase();
|
|
408
|
-
for (const row of rows) {
|
|
409
|
-
const okText = !text || row.textContent.toLowerCase().includes(text);
|
|
410
|
-
const okSeverity = !severity.value || row.dataset.severity === severity.value;
|
|
411
|
-
const okPersona = !persona.value || row.dataset.persona === persona.value;
|
|
412
|
-
const okCategory = !category.value || row.dataset.category === category.value;
|
|
413
|
-
row.style.display = okText && okSeverity && okPersona && okCategory ? '' : 'none';
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
[q, severity, persona, category].forEach((el) => el.addEventListener('input', applyFilters));
|
|
417
|
-
</script>
|
|
418
|
-
</body>
|
|
419
|
-
</html>`;
|
|
420
|
-
}
|
|
421
|
-
function renderAuditIndex(report) {
|
|
422
|
-
const byPersona = Object.entries(groupFindings(report.findings, (f) => f.persona ?? 'local'))
|
|
423
|
-
.sort((a, b) => b[1].length - a[1].length);
|
|
424
|
-
const bySeverity = Object.entries(groupFindings(report.findings, (f) => f.severity))
|
|
425
|
-
.sort((a, b) => b[1].length - a[1].length);
|
|
426
|
-
const lines = [
|
|
427
|
-
'# Audit Report',
|
|
428
|
-
'',
|
|
429
|
-
`- Created: ${report.createdAt}`,
|
|
430
|
-
`- Duration: ${(report.durationMs / 1000).toFixed(1)}s`,
|
|
431
|
-
`- Files covered by local scan: ${report.totalFiles}`,
|
|
432
|
-
`- Findings: ${report.findings.length}`,
|
|
433
|
-
`- Critical: ${report.criticalCount}`,
|
|
434
|
-
`- High: ${report.highCount}`,
|
|
435
|
-
'',
|
|
436
|
-
'## Summary',
|
|
437
|
-
'',
|
|
438
|
-
report.summary,
|
|
439
|
-
'',
|
|
440
|
-
'## Top Priorities',
|
|
441
|
-
'',
|
|
442
|
-
...report.topPriorities.map((priority, index) => `${index + 1}. ${priority}`),
|
|
443
|
-
'',
|
|
444
|
-
'## Findings By Persona',
|
|
445
|
-
'',
|
|
446
|
-
...byPersona.map(([persona, findings]) => `- ${persona}: ${findings.length}`),
|
|
447
|
-
'',
|
|
448
|
-
'## Findings By Severity',
|
|
449
|
-
'',
|
|
450
|
-
...bySeverity.map(([severity, findings]) => `- ${severity}: ${findings.length}`),
|
|
451
|
-
'',
|
|
452
|
-
'## Files',
|
|
453
|
-
'',
|
|
454
|
-
'- `report.json`: full machine-readable audit report',
|
|
455
|
-
'- `findings-by-persona.json`: findings grouped by scanner persona',
|
|
456
|
-
'- `findings-by-severity.json`: findings grouped by severity',
|
|
457
|
-
'- `findings-by-category.json`: findings grouped by category',
|
|
458
|
-
'',
|
|
459
|
-
];
|
|
460
|
-
return lines.join('\n');
|
|
461
26
|
}
|
|
462
27
|
function renderDryRun(pipeline, stats, maxFilesForAi) {
|
|
463
28
|
console.log('\n' + chalk.bold.cyan('Audit dry run'));
|
|
@@ -474,78 +39,96 @@ function renderDryRun(pipeline, stats, maxFilesForAi) {
|
|
|
474
39
|
exts.forEach(([ext, count]) => console.log(` ${ext}: ${count}`));
|
|
475
40
|
}
|
|
476
41
|
if (stats.auditFiles.length > 0) {
|
|
477
|
-
const prioritized = pipeline.prioritizeFiles(stats.auditFiles, maxFilesForAi);
|
|
478
42
|
console.log('\n' + chalk.bold('AI target sample:'));
|
|
479
|
-
|
|
43
|
+
pipeline.prioritizeFiles(stats.auditFiles, maxFilesForAi).slice(0, 15).forEach((file) => console.log(` ${file}`));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function parseBudget(value) {
|
|
47
|
+
return (['low', 'normal', 'deep'].includes(value) ? value : 'low');
|
|
48
|
+
}
|
|
49
|
+
function parsePositiveInt(value, fallback, max) {
|
|
50
|
+
if (!value)
|
|
51
|
+
return fallback;
|
|
52
|
+
return Math.max(1, Math.min(max, parseInt(String(value), 10) || fallback));
|
|
53
|
+
}
|
|
54
|
+
async function printPersonas() {
|
|
55
|
+
const { listPresets } = await import('../../infra/persona-presets.js');
|
|
56
|
+
console.log(chalk.bold.cyan('\nPersonas (scanner domains):\n'));
|
|
57
|
+
[
|
|
58
|
+
'security', 'bugs', 'redundancy', 'error-handling', 'architecture', 'testing',
|
|
59
|
+
'performance', 'infrastructure', 'observability', 'resilience', 'data',
|
|
60
|
+
'dependencies', 'compliance', 'multitenancy', 'prompt-audit',
|
|
61
|
+
].forEach((d) => console.log(` ${chalk.cyan(d)}`));
|
|
62
|
+
listPresets();
|
|
63
|
+
}
|
|
64
|
+
async function maybeAutoFix(options, report, orch) {
|
|
65
|
+
if (!options.fix)
|
|
66
|
+
return;
|
|
67
|
+
const maxFixes = parsePositiveInt(options.fixMax, 5, 20);
|
|
68
|
+
const minSev = (['critical', 'high', 'medium'].includes(options.fixMinSeverity)
|
|
69
|
+
? options.fixMinSeverity : 'high');
|
|
70
|
+
const eligible = report.findings
|
|
71
|
+
.filter((f) => f.file && f.line && ((SEVERITY_RANK[f.severity] ?? 0) >= (SEVERITY_RANK[minSev] ?? 0)))
|
|
72
|
+
.sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0))
|
|
73
|
+
.slice(0, maxFixes);
|
|
74
|
+
if (options.dryRun) {
|
|
75
|
+
console.log('\n' + chalk.bold.yellow(`Fix dry-run: ${eligible.length} finding(s) would be targeted.`));
|
|
76
|
+
eligible.forEach((f, i) => console.log(` ${i + 1}. ${f.severity} ${f.file}:${f.line} ${f.finding}`));
|
|
77
|
+
}
|
|
78
|
+
else if (report.criticalCount > 0 || report.highCount > 0 || minSev === 'medium') {
|
|
79
|
+
console.log(chalk.bold.yellow(`\nAuto-fixing up to ${maxFixes} ${minSev}+ findings...\n`));
|
|
80
|
+
const fixReport = await orch.runAuditFixPipeline(report, { maxFixes, minSeverity: minSev });
|
|
81
|
+
console.log(chalk.bold(`Fix summary: ${fixReport.succeeded} fixed, ${fixReport.failed} failed, ${fixReport.skipped} skipped`));
|
|
480
82
|
}
|
|
481
83
|
}
|
|
482
84
|
export function registerAudit(program) {
|
|
483
85
|
program
|
|
484
86
|
.command('audit [target]')
|
|
485
|
-
.description('Deep
|
|
87
|
+
.description('Deep audit with local scan, capped AI file scope, and compact reports')
|
|
486
88
|
.option('-n, --scanners <n>', 'number of scanner agents (auto-selected if omitted)')
|
|
487
89
|
.option('--budget <budget>', 'low | normal | deep (default: low)', 'low')
|
|
488
90
|
.option('--provider <provider>', 'claude | openrouter (default: claude)')
|
|
489
|
-
.option('--model <model>', 'model override for openrouter
|
|
91
|
+
.option('--model <model>', 'model override for openrouter')
|
|
490
92
|
.option('--preset <name>', 'persona preset: security, ai, backend, devops, quality, saas, fintech, full')
|
|
491
|
-
.option('--domains <list>', 'comma-separated scanner domains
|
|
93
|
+
.option('--domains <list>', 'comma-separated scanner domains')
|
|
492
94
|
.option('--list-personas', 'show all available personas and presets then exit')
|
|
493
95
|
.option('--local-only', 'run only deterministic local scans; no AI scanners or synthesizer')
|
|
494
96
|
.option('--force-full', 'allow the full preset to start all requested AI scanner domains')
|
|
495
|
-
.option('--max-files <n>', 'max prioritized source files sent to AI scanners
|
|
496
|
-
.option('--scanner-timeout <seconds>', 'timeout per AI scanner in seconds
|
|
97
|
+
.option('--max-files <n>', 'max prioritized source files sent to AI scanners')
|
|
98
|
+
.option('--scanner-timeout <seconds>', 'timeout per AI scanner in seconds')
|
|
99
|
+
.option('--ai-context-budget <tokens>', 'max approximate tokens for ai-context.md', '8000')
|
|
497
100
|
.option('--fix', 'auto-fix critical/high findings after audit')
|
|
498
101
|
.option('--fix-max <n>', 'max findings to auto-fix (default: 5)', '5')
|
|
499
102
|
.option('--fix-min-severity <s>', 'minimum severity to fix: critical|high|medium (default: high)', 'high')
|
|
500
103
|
.option('--dry-run', 'collect audit file stats without starting agents')
|
|
501
|
-
.option('--incremental', 'only scan files changed since last audit
|
|
104
|
+
.option('--incremental', 'only scan files changed since last audit')
|
|
502
105
|
.action(async (target = '.', options) => {
|
|
503
|
-
if (options.listPersonas)
|
|
504
|
-
|
|
505
|
-
console.log(chalk.bold.cyan('\nPersonas (scanner domains):\n'));
|
|
506
|
-
const allDomains = [
|
|
507
|
-
'security', 'bugs', 'redundancy', 'error-handling', 'architecture', 'testing', 'performance',
|
|
508
|
-
'infrastructure', 'observability', 'resilience', 'data', 'dependencies', 'compliance', 'multitenancy', 'prompt-audit',
|
|
509
|
-
];
|
|
510
|
-
allDomains.forEach((d) => console.log(` ${chalk.cyan(d)}`));
|
|
511
|
-
listPresets();
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
// Merge .aionrc.json config under CLI options
|
|
106
|
+
if (options.listPersonas)
|
|
107
|
+
return printPersonas();
|
|
515
108
|
const { loadAionConfig, mergeConfig } = await import('../../infra/aion-config.js');
|
|
516
|
-
const
|
|
517
|
-
const mergedOptions = mergeConfig(options, aionConfig);
|
|
109
|
+
const mergedOptions = mergeConfig(options, loadAionConfig(process.cwd()));
|
|
518
110
|
const explicitN = mergedOptions.scanners ? Math.max(1, Math.min(15, parseInt(String(mergedOptions.scanners), 10) || 5)) : undefined;
|
|
519
|
-
const budget = (
|
|
111
|
+
const budget = parseBudget(mergedOptions.budget);
|
|
520
112
|
const scannerTimeoutSeconds = mergedOptions.scannerTimeout
|
|
521
113
|
? Math.max(10, Math.min(600, parseInt(String(mergedOptions.scannerTimeout), 10) || 90))
|
|
522
114
|
: budget === 'deep' ? 240 : budget === 'normal' ? 150 : 90;
|
|
523
|
-
const maxFilesForAi = mergedOptions.maxFiles
|
|
524
|
-
? Math.max(1, Math.min(500, parseInt(String(mergedOptions.maxFiles), 10) || 30))
|
|
525
|
-
: budget === 'deep' ? 120 : budget === 'normal' ? 60 : 30;
|
|
526
|
-
const providerName = mergedOptions.provider === 'openrouter' ? 'openrouter' : undefined;
|
|
115
|
+
const maxFilesForAi = parsePositiveInt(mergedOptions.maxFiles, budget === 'deep' ? 120 : budget === 'normal' ? 60 : 30, 500);
|
|
527
116
|
const policyInput = {
|
|
528
117
|
budget,
|
|
529
118
|
...(mergedOptions.model ? { openrouterModel: mergedOptions.model } : {}),
|
|
530
|
-
...(
|
|
531
|
-
plannerProvider: providerName,
|
|
532
|
-
investigatorProvider: providerName,
|
|
533
|
-
developerProvider: providerName,
|
|
534
|
-
reviewerProvider: providerName,
|
|
535
|
-
} : {}),
|
|
119
|
+
...(mergedOptions.provider === 'openrouter' ? { plannerProvider: 'openrouter', investigatorProvider: 'openrouter', developerProvider: 'openrouter', reviewerProvider: 'openrouter' } : {}),
|
|
536
120
|
};
|
|
537
121
|
const policy = createRuntimePolicy(policyInput);
|
|
538
|
-
const renderer = new Renderer();
|
|
539
|
-
const orch = new Orchestrator(process.cwd(), policyInput);
|
|
540
122
|
if (options.dryRun) {
|
|
541
123
|
const pipeline = new AuditPipeline(process.cwd(), policy, new CostTracker(), () => { }, () => { });
|
|
542
124
|
renderDryRun(pipeline, pipeline.collectAuditStats(target), maxFilesForAi);
|
|
543
125
|
return;
|
|
544
126
|
}
|
|
127
|
+
const renderer = new Renderer();
|
|
128
|
+
const orch = new Orchestrator(process.cwd(), policyInput);
|
|
545
129
|
orch.on('agent:start', ({ agentName }) => renderer.agentStart(agentName));
|
|
546
130
|
orch.on('agent:output', ({ agentName, text }) => renderer.agentChunk(agentName, text));
|
|
547
131
|
orch.on('agent:done', ({ agentName, durationMs }) => renderer.agentDone(agentName, durationMs));
|
|
548
|
-
// Resolve persona domains
|
|
549
132
|
const { resolveDomainsFromConfig } = await import('../../infra/persona-presets.js');
|
|
550
133
|
const { domains: explicitDomains, source: domainSource } = resolveDomainsFromConfig(process.cwd(), mergedOptions.preset, mergedOptions.domains, explicitN);
|
|
551
134
|
const maxAiScanners = explicitN ?? policy.maxAgents;
|
|
@@ -553,22 +136,17 @@ export function registerAudit(program) {
|
|
|
553
136
|
if (requestsFullPreset && !mergedOptions.forceFull && !mergedOptions.localOnly) {
|
|
554
137
|
console.error(chalk.red.bold('\nRefusing expensive full audit by default.\n'));
|
|
555
138
|
console.error(chalk.gray(`Requested ${explicitDomains.length} domains, but ${budget} budget allows ${maxAiScanners} AI scanner(s).`));
|
|
556
|
-
console.error(chalk.
|
|
557
|
-
console.error(` ${chalk.cyan(
|
|
558
|
-
console.error(` ${chalk.cyan(
|
|
559
|
-
console.error(` ${chalk.cyan('aion audit . --preset full --force-full')} explicit full-cost run`);
|
|
139
|
+
console.error(` ${chalk.cyan('aion audit . --local-only')} no AI tokens`);
|
|
140
|
+
console.error(` ${chalk.cyan(`aion audit . --preset ${mergedOptions.preset ?? 'security'} --scanners ${maxAiScanners}`)} capped AI audit`);
|
|
141
|
+
console.error(` ${chalk.cyan('aion audit . --preset full --force-full')} explicit full-cost run`);
|
|
560
142
|
process.exitCode = 1;
|
|
561
143
|
return;
|
|
562
144
|
}
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
? 'local-only scan (no AI tokens)'
|
|
566
|
-
: explicitDomains.length > 0
|
|
567
|
-
? mergedOptions.localOnly
|
|
568
|
-
? `local-only scan [${domainSource}]`
|
|
569
|
-
: `personas: ${explicitDomains.slice(0, maxAiScanners).join(', ')} [${domainSource}]`
|
|
145
|
+
const label = mergedOptions.localOnly ? 'local-only scan (no AI tokens)'
|
|
146
|
+
: explicitDomains.length > 0 ? `personas: ${explicitDomains.slice(0, maxAiScanners).join(', ')} [${domainSource}]`
|
|
570
147
|
: explicitN ? `${explicitN} scanners` : `auto scanners (${budget} budget)`;
|
|
571
|
-
console.log(chalk.bold.cyan(`\nStarting audit with ${
|
|
148
|
+
console.log(chalk.bold.cyan(`\nStarting audit with ${label}...\n`));
|
|
149
|
+
const start = Date.now();
|
|
572
150
|
try {
|
|
573
151
|
const report = await orch.runAuditPipeline(target, explicitN, explicitDomains.length > 0 ? explicitDomains : undefined, {
|
|
574
152
|
incremental: options.incremental,
|
|
@@ -579,46 +157,21 @@ export function registerAudit(program) {
|
|
|
579
157
|
});
|
|
580
158
|
const durationMs = Date.now() - start;
|
|
581
159
|
renderAuditReport(report, durationMs);
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
160
|
+
const costSummary = {
|
|
161
|
+
totalUsd: orch.costs.totalUsd(),
|
|
162
|
+
model: policy.claudeModel,
|
|
163
|
+
perAgent: orch.costs.byAgent().map((e) => ({
|
|
164
|
+
name: e.agentName,
|
|
165
|
+
costUsd: e.costUsd,
|
|
166
|
+
inputTokens: e.usage.inputTokens,
|
|
167
|
+
outputTokens: e.usage.outputTokens,
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
const saved = saveAuditReport(process.cwd(), report, durationMs, parsePositiveInt(mergedOptions.aiContextBudget, 8000, 100000), costSummary);
|
|
171
|
+
console.log(chalk.gray(`\nhtml: ${saved.html}`));
|
|
172
|
+
console.log(chalk.gray(`dashboard: ${saved.dashboard}`));
|
|
587
173
|
console.log(chalk.dim(orch.costs.summary()));
|
|
588
|
-
|
|
589
|
-
const maxFixes = Math.max(1, Math.min(20, parseInt(options.fixMax, 10) || 5));
|
|
590
|
-
const minSev = (['critical', 'high', 'medium'].includes(options.fixMinSeverity)
|
|
591
|
-
? options.fixMinSeverity : 'high');
|
|
592
|
-
const eligible = report.findings
|
|
593
|
-
.filter((f) => f.file && f.line && ((SEVERITY_RANK[f.severity] ?? 0) >= (SEVERITY_RANK[minSev] ?? 0)))
|
|
594
|
-
.sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0))
|
|
595
|
-
.slice(0, maxFixes);
|
|
596
|
-
if (options.dryRun) {
|
|
597
|
-
// Dry-run: show what would be fixed without running
|
|
598
|
-
console.log('\n' + chalk.bold.yellow(`Fix dry-run — ${eligible.length} finding(s) would be targeted (up to ${maxFixes}, severity ≥ ${minSev}):\n`));
|
|
599
|
-
if (eligible.length === 0) {
|
|
600
|
-
console.log(chalk.dim(' No eligible findings (need file+line and severity ≥ ' + minSev + ')'));
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
eligible.forEach((f, i) => {
|
|
604
|
-
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
605
|
-
const sev = SEVERITY_COLOR[f.severity]?.(f.severity) ?? chalk.white(f.severity);
|
|
606
|
-
console.log(` ${i + 1}. ${sev} ${chalk.bold(loc)}`);
|
|
607
|
-
console.log(` ${f.finding}`);
|
|
608
|
-
console.log(chalk.dim(` → ${f.recommendation}`));
|
|
609
|
-
console.log();
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
const skipped = report.findings.length - eligible.length;
|
|
613
|
-
if (skipped > 0)
|
|
614
|
-
console.log(chalk.dim(` ${skipped} finding(s) skipped (below threshold or missing file+line)`));
|
|
615
|
-
}
|
|
616
|
-
else if (report.criticalCount > 0 || report.highCount > 0 || minSev === 'medium') {
|
|
617
|
-
console.log(chalk.bold.yellow(`\nAuto-fixing up to ${maxFixes} ${minSev}+ findings...\n`));
|
|
618
|
-
const fixReport = await orch.runAuditFixPipeline(report, { maxFixes, minSeverity: minSev });
|
|
619
|
-
console.log(chalk.bold(`Fix summary: ${fixReport.succeeded} fixed, ${fixReport.failed} failed, ${fixReport.skipped} skipped`));
|
|
620
|
-
}
|
|
621
|
-
}
|
|
174
|
+
await maybeAutoFix(options, report, orch);
|
|
622
175
|
process.exit(report.criticalCount > 0 ? 2 : report.highCount > 0 ? 1 : 0);
|
|
623
176
|
}
|
|
624
177
|
catch (err) {
|