@aionlabsai/aion 0.2.0 → 0.2.1
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 +224 -9
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +461 -20
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/chat.d.ts +3 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +128 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/diff.d.ts +3 -0
- package/dist/cli/commands/diff.d.ts.map +1 -0
- package/dist/cli/commands/diff.js +144 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/health.d.ts.map +1 -1
- package/dist/cli/commands/health.js +17 -0
- package/dist/cli/commands/health.js.map +1 -1
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +52 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +8 -0
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/menu.d.ts.map +1 -1
- package/dist/cli/menu.js +43 -1
- package/dist/cli/menu.js.map +1 -1
- package/dist/core/orchestrator.d.ts +2 -1
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +2 -2
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/pipelines/audit-pipeline.d.ts +9 -1
- package/dist/core/pipelines/audit-pipeline.d.ts.map +1 -1
- package/dist/core/pipelines/audit-pipeline.js +98 -9
- package/dist/core/pipelines/audit-pipeline.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/aion-config.d.ts +26 -0
- package/dist/infra/aion-config.d.ts.map +1 -0
- package/dist/infra/aion-config.js +60 -0
- package/dist/infra/aion-config.js.map +1 -0
- package/dist/infra/aion-ignore.d.ts +3 -0
- package/dist/infra/aion-ignore.d.ts.map +1 -0
- package/dist/infra/aion-ignore.js +35 -0
- package/dist/infra/aion-ignore.js.map +1 -0
- package/dist/infra/audit-cache.d.ts +39 -0
- package/dist/infra/audit-cache.d.ts.map +1 -0
- package/dist/infra/audit-cache.js +82 -0
- package/dist/infra/audit-cache.js.map +1 -0
- package/dist/infra/audit-trend.d.ts +17 -0
- package/dist/infra/audit-trend.d.ts.map +1 -0
- package/dist/infra/audit-trend.js +113 -0
- package/dist/infra/audit-trend.js.map +1 -0
- package/dist/infra/db/store.d.ts.map +1 -1
- package/dist/infra/db/store.js +14 -7
- package/dist/infra/db/store.js.map +1 -1
- package/dist/infra/embeddings.d.ts.map +1 -1
- package/dist/infra/embeddings.js +32 -14
- package/dist/infra/embeddings.js.map +1 -1
- package/dist/prompts/scanner.d.ts +1 -0
- package/dist/prompts/scanner.d.ts.map +1 -1
- package/dist/prompts/scanner.js +4 -0
- package/dist/prompts/scanner.js.map +1 -1
- package/package.json +24 -19
- package/dist/agents/planner.test.d.ts +0 -2
- package/dist/agents/planner.test.d.ts.map +0 -1
- package/dist/agents/planner.test.js +0 -21
- package/dist/agents/planner.test.js.map +0 -1
- package/dist/core/cost-tracker.test.d.ts +0 -2
- package/dist/core/cost-tracker.test.d.ts.map +0 -1
- package/dist/core/cost-tracker.test.js +0 -36
- package/dist/core/cost-tracker.test.js.map +0 -1
- package/dist/core/pipelines/audit-pipeline.test.d.ts +0 -2
- package/dist/core/pipelines/audit-pipeline.test.d.ts.map +0 -1
- package/dist/core/pipelines/audit-pipeline.test.js +0 -135
- package/dist/core/pipelines/audit-pipeline.test.js.map +0 -1
- package/dist/core/repo-context.d.ts +0 -2
- package/dist/core/repo-context.d.ts.map +0 -1
- package/dist/core/repo-context.js +0 -12
- package/dist/core/repo-context.js.map +0 -1
- package/dist/core/repo-context.test.d.ts +0 -2
- package/dist/core/repo-context.test.d.ts.map +0 -1
- package/dist/core/repo-context.test.js +0 -40
- package/dist/core/repo-context.test.js.map +0 -1
- package/dist/core/runtime-policy.test.d.ts +0 -2
- package/dist/core/runtime-policy.test.d.ts.map +0 -1
- package/dist/core/runtime-policy.test.js +0 -27
- package/dist/core/runtime-policy.test.js.map +0 -1
- package/dist/infra/bm25.test.d.ts +0 -2
- package/dist/infra/bm25.test.d.ts.map +0 -1
- package/dist/infra/bm25.test.js +0 -17
- package/dist/infra/bm25.test.js.map +0 -1
- package/dist/infra/chunker.test.d.ts +0 -2
- package/dist/infra/chunker.test.d.ts.map +0 -1
- package/dist/infra/chunker.test.js +0 -33
- package/dist/infra/chunker.test.js.map +0 -1
- package/dist/infra/db/database.d.ts +0 -4
- package/dist/infra/db/database.d.ts.map +0 -1
- package/dist/infra/db/database.js +0 -25
- package/dist/infra/db/database.js.map +0 -1
- package/dist/infra/evidence-gate.test.d.ts +0 -2
- package/dist/infra/evidence-gate.test.d.ts.map +0 -1
- package/dist/infra/evidence-gate.test.js +0 -36
- package/dist/infra/evidence-gate.test.js.map +0 -1
- package/dist/infra/repo-index.test.d.ts +0 -2
- package/dist/infra/repo-index.test.d.ts.map +0 -1
- package/dist/infra/repo-index.test.js +0 -53
- package/dist/infra/repo-index.test.js.map +0 -1
- package/dist/infra/repo-query.test.d.ts +0 -2
- package/dist/infra/repo-query.test.d.ts.map +0 -1
- package/dist/infra/repo-query.test.js +0 -34
- package/dist/infra/repo-query.test.js.map +0 -1
- package/dist/infra/semgrep.test.d.ts +0 -2
- package/dist/infra/semgrep.test.d.ts.map +0 -1
- package/dist/infra/semgrep.test.js +0 -39
- package/dist/infra/semgrep.test.js.map +0 -1
- package/dist/schemas/audit.test.d.ts +0 -2
- package/dist/schemas/audit.test.d.ts.map +0 -1
- package/dist/schemas/audit.test.js +0 -41
- package/dist/schemas/audit.test.js.map +0 -1
|
@@ -20,6 +20,14 @@ const SEVERITY_ICON = {
|
|
|
20
20
|
low: '⚪',
|
|
21
21
|
info: '🔵',
|
|
22
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
|
+
}
|
|
23
31
|
function renderAuditReport(report, durationMs) {
|
|
24
32
|
console.log('\n' + chalk.bold('═'.repeat(60)));
|
|
25
33
|
console.log(chalk.bold.cyan(' AUDIT REPORT') + chalk.gray(` — ${report.totalFiles} files — ${(durationMs / 1000).toFixed(1)}s`));
|
|
@@ -83,15 +91,380 @@ function saveAuditReport(report, durationMs) {
|
|
|
83
91
|
const dir = join(process.cwd(), '.ai-runtime', 'reports');
|
|
84
92
|
mkdirSync(dir, { recursive: true });
|
|
85
93
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
94
|
+
const createdAt = new Date().toISOString();
|
|
95
|
+
const fullReport = { ...report, durationMs, createdAt };
|
|
86
96
|
const file = join(dir, `audit-${stamp}.json`);
|
|
87
|
-
writeFileSync(file, JSON.stringify(
|
|
88
|
-
|
|
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;
|
|
89
186
|
}
|
|
90
|
-
function
|
|
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');
|
|
240
|
+
}
|
|
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
|
+
}
|
|
462
|
+
function renderDryRun(pipeline, stats, maxFilesForAi) {
|
|
91
463
|
console.log('\n' + chalk.bold.cyan('Audit dry run'));
|
|
92
464
|
console.log(chalk.gray('No agents were started and no API tokens were used.'));
|
|
93
465
|
console.log(` total files seen: ${stats.totalFiles}`);
|
|
94
466
|
console.log(` audit source files: ${stats.auditFiles.length}`);
|
|
467
|
+
console.log(` AI target files: ${Math.min(stats.auditFiles.length, maxFilesForAi)} prioritized file(s)`);
|
|
95
468
|
console.log(` ignored directories: ${stats.ignoredDirs}`);
|
|
96
469
|
console.log(` ignored/non-source files: ${stats.ignoredFiles}`);
|
|
97
470
|
console.log(` oversized source files: ${stats.oversizedFiles}`);
|
|
@@ -101,8 +474,9 @@ function renderDryRun(stats) {
|
|
|
101
474
|
exts.forEach(([ext, count]) => console.log(` ${ext}: ${count}`));
|
|
102
475
|
}
|
|
103
476
|
if (stats.auditFiles.length > 0) {
|
|
104
|
-
|
|
105
|
-
|
|
477
|
+
const prioritized = pipeline.prioritizeFiles(stats.auditFiles, maxFilesForAi);
|
|
478
|
+
console.log('\n' + chalk.bold('AI target sample:'));
|
|
479
|
+
prioritized.slice(0, 15).forEach((file) => console.log(` ${file}`));
|
|
106
480
|
}
|
|
107
481
|
}
|
|
108
482
|
export function registerAudit(program) {
|
|
@@ -116,10 +490,15 @@ export function registerAudit(program) {
|
|
|
116
490
|
.option('--preset <name>', 'persona preset: security, ai, backend, devops, quality, saas, fintech, full')
|
|
117
491
|
.option('--domains <list>', 'comma-separated scanner domains, e.g. security,compliance,data')
|
|
118
492
|
.option('--list-personas', 'show all available personas and presets then exit')
|
|
493
|
+
.option('--local-only', 'run only deterministic local scans; no AI scanners or synthesizer')
|
|
494
|
+
.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 (default: 30 low, 60 normal, 120 deep)')
|
|
496
|
+
.option('--scanner-timeout <seconds>', 'timeout per AI scanner in seconds (default: 90 low, 150 normal, 240 deep)')
|
|
119
497
|
.option('--fix', 'auto-fix critical/high findings after audit')
|
|
120
498
|
.option('--fix-max <n>', 'max findings to auto-fix (default: 5)', '5')
|
|
121
499
|
.option('--fix-min-severity <s>', 'minimum severity to fix: critical|high|medium (default: high)', 'high')
|
|
122
500
|
.option('--dry-run', 'collect audit file stats without starting agents')
|
|
501
|
+
.option('--incremental', 'only scan files changed since last audit (reuse cache for unchanged)')
|
|
123
502
|
.action(async (target = '.', options) => {
|
|
124
503
|
if (options.listPersonas) {
|
|
125
504
|
const { listPresets, BUILT_IN_PRESETS } = await import('../../infra/persona-presets.js');
|
|
@@ -132,12 +511,22 @@ export function registerAudit(program) {
|
|
|
132
511
|
listPresets();
|
|
133
512
|
return;
|
|
134
513
|
}
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
514
|
+
// Merge .aionrc.json config under CLI options
|
|
515
|
+
const { loadAionConfig, mergeConfig } = await import('../../infra/aion-config.js');
|
|
516
|
+
const aionConfig = loadAionConfig(process.cwd());
|
|
517
|
+
const mergedOptions = mergeConfig(options, aionConfig);
|
|
518
|
+
const explicitN = mergedOptions.scanners ? Math.max(1, Math.min(15, parseInt(String(mergedOptions.scanners), 10) || 5)) : undefined;
|
|
519
|
+
const budget = (['low', 'normal', 'deep'].includes(mergedOptions.budget) ? mergedOptions.budget : 'low');
|
|
520
|
+
const scannerTimeoutSeconds = mergedOptions.scannerTimeout
|
|
521
|
+
? Math.max(10, Math.min(600, parseInt(String(mergedOptions.scannerTimeout), 10) || 90))
|
|
522
|
+
: 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;
|
|
138
527
|
const policyInput = {
|
|
139
528
|
budget,
|
|
140
|
-
...(
|
|
529
|
+
...(mergedOptions.model ? { openrouterModel: mergedOptions.model } : {}),
|
|
141
530
|
...(providerName ? {
|
|
142
531
|
plannerProvider: providerName,
|
|
143
532
|
investigatorProvider: providerName,
|
|
@@ -150,7 +539,7 @@ export function registerAudit(program) {
|
|
|
150
539
|
const orch = new Orchestrator(process.cwd(), policyInput);
|
|
151
540
|
if (options.dryRun) {
|
|
152
541
|
const pipeline = new AuditPipeline(process.cwd(), policy, new CostTracker(), () => { }, () => { });
|
|
153
|
-
renderDryRun(pipeline.collectAuditStats(target));
|
|
542
|
+
renderDryRun(pipeline, pipeline.collectAuditStats(target), maxFilesForAi);
|
|
154
543
|
return;
|
|
155
544
|
}
|
|
156
545
|
orch.on('agent:start', ({ agentName }) => renderer.agentStart(agentName));
|
|
@@ -158,25 +547,77 @@ export function registerAudit(program) {
|
|
|
158
547
|
orch.on('agent:done', ({ agentName, durationMs }) => renderer.agentDone(agentName, durationMs));
|
|
159
548
|
// Resolve persona domains
|
|
160
549
|
const { resolveDomainsFromConfig } = await import('../../infra/persona-presets.js');
|
|
161
|
-
const { domains: explicitDomains, source: domainSource } = resolveDomainsFromConfig(process.cwd(),
|
|
550
|
+
const { domains: explicitDomains, source: domainSource } = resolveDomainsFromConfig(process.cwd(), mergedOptions.preset, mergedOptions.domains, explicitN);
|
|
551
|
+
const maxAiScanners = explicitN ?? policy.maxAgents;
|
|
552
|
+
const requestsFullPreset = (mergedOptions.preset === 'full' && explicitN === undefined) || explicitDomains.length > maxAiScanners;
|
|
553
|
+
if (requestsFullPreset && !mergedOptions.forceFull && !mergedOptions.localOnly) {
|
|
554
|
+
console.error(chalk.red.bold('\nRefusing expensive full audit by default.\n'));
|
|
555
|
+
console.error(chalk.gray(`Requested ${explicitDomains.length} domains, but ${budget} budget allows ${maxAiScanners} AI scanner(s).`));
|
|
556
|
+
console.error(chalk.gray('Use one of:'));
|
|
557
|
+
console.error(` ${chalk.cyan('aion audit . --local-only')} no AI tokens`);
|
|
558
|
+
console.error(` ${chalk.cyan(`aion audit . --preset ${mergedOptions.preset ?? 'security'} --scanners ${maxAiScanners}`)} capped AI audit`);
|
|
559
|
+
console.error(` ${chalk.cyan('aion audit . --preset full --force-full')} explicit full-cost run`);
|
|
560
|
+
process.exitCode = 1;
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
162
563
|
const start = Date.now();
|
|
163
|
-
const nLabel =
|
|
164
|
-
?
|
|
165
|
-
:
|
|
564
|
+
const nLabel = mergedOptions.localOnly
|
|
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}]`
|
|
570
|
+
: explicitN ? `${explicitN} scanners` : `auto scanners (${budget} budget)`;
|
|
166
571
|
console.log(chalk.bold.cyan(`\nStarting audit with ${nLabel}...\n`));
|
|
167
572
|
try {
|
|
168
|
-
const report = await orch.runAuditPipeline(target, explicitN, explicitDomains.length > 0 ? explicitDomains : undefined
|
|
573
|
+
const report = await orch.runAuditPipeline(target, explicitN, explicitDomains.length > 0 ? explicitDomains : undefined, {
|
|
574
|
+
incremental: options.incremental,
|
|
575
|
+
localOnly: mergedOptions.localOnly,
|
|
576
|
+
maxAiScanners: mergedOptions.forceFull ? explicitDomains.length || explicitN : maxAiScanners,
|
|
577
|
+
maxFilesForAi,
|
|
578
|
+
scannerTimeoutMs: scannerTimeoutSeconds * 1000,
|
|
579
|
+
});
|
|
169
580
|
const durationMs = Date.now() - start;
|
|
170
581
|
renderAuditReport(report, durationMs);
|
|
171
|
-
|
|
582
|
+
const saved = saveAuditReport(report, durationMs);
|
|
583
|
+
console.log(chalk.gray(`report: ${saved.runDir}`));
|
|
584
|
+
console.log(chalk.gray(`html: ${saved.html}`));
|
|
585
|
+
console.log(chalk.gray(`summary: ${saved.summary}`));
|
|
586
|
+
console.log(chalk.gray(`action plan: ${saved.actionPlan}`));
|
|
172
587
|
console.log(chalk.dim(orch.costs.summary()));
|
|
173
|
-
if (options.fix
|
|
588
|
+
if (options.fix) {
|
|
174
589
|
const maxFixes = Math.max(1, Math.min(20, parseInt(options.fixMax, 10) || 5));
|
|
175
590
|
const minSev = (['critical', 'high', 'medium'].includes(options.fixMinSeverity)
|
|
176
591
|
? options.fixMinSeverity : 'high');
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
|
180
621
|
}
|
|
181
622
|
process.exit(report.criticalCount > 0 ? 2 : report.highCount > 0 ? 1 : 0);
|
|
182
623
|
}
|