opena2a-cli 0.3.2 → 0.3.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 +22 -21
- package/dist/adapters/python.d.ts.map +1 -1
- package/dist/adapters/python.js +7 -3
- package/dist/adapters/python.js.map +1 -1
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +1 -7
- package/dist/adapters/registry.js.map +1 -1
- package/dist/commands/guard.d.ts +8 -0
- package/dist/commands/guard.d.ts.map +1 -1
- package/dist/commands/guard.js +30 -0
- package/dist/commands/guard.js.map +1 -1
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +612 -162
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/onepassword-migration.d.ts.map +1 -1
- package/dist/commands/onepassword-migration.js +6 -0
- package/dist/commands/onepassword-migration.js.map +1 -1
- package/dist/commands/protect.d.ts +4 -0
- package/dist/commands/protect.d.ts.map +1 -1
- package/dist/commands/protect.js +250 -12
- package/dist/commands/protect.js.map +1 -1
- package/dist/commands/review.d.ts +2 -2
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +7 -7
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/shield.d.ts +1 -1
- package/dist/commands/shield.js +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/natural/llm-fallback.d.ts.map +1 -1
- package/dist/natural/llm-fallback.js +24 -4
- package/dist/natural/llm-fallback.js.map +1 -1
- package/dist/report/review-html.js +2 -2
- package/dist/router.js +1 -1
- package/dist/router.js.map +1 -1
- package/dist/semantic/command-index.json +1 -1
- package/dist/shield/status.d.ts.map +1 -1
- package/dist/shield/status.js +16 -16
- package/dist/shield/status.js.map +1 -1
- package/dist/shield/types.d.ts +3 -3
- package/dist/shield/types.d.ts.map +1 -1
- package/dist/util/ai-config.d.ts +40 -0
- package/dist/util/ai-config.d.ts.map +1 -0
- package/dist/util/ai-config.js +389 -0
- package/dist/util/ai-config.js.map +1 -0
- package/dist/util/detect.d.ts +2 -1
- package/dist/util/detect.d.ts.map +1 -1
- package/dist/util/detect.js +31 -1
- package/dist/util/detect.js.map +1 -1
- package/dist/util/format.d.ts +1 -0
- package/dist/util/format.d.ts.map +1 -1
- package/dist/util/format.js +20 -0
- package/dist/util/format.js.map +1 -1
- package/dist/util/hygiene.d.ts +16 -0
- package/dist/util/hygiene.d.ts.map +1 -0
- package/dist/util/hygiene.js +119 -0
- package/dist/util/hygiene.js.map +1 -0
- package/dist/util/scoring.d.ts +34 -0
- package/dist/util/scoring.d.ts.map +1 -0
- package/dist/util/scoring.js +144 -0
- package/dist/util/scoring.js.map +1 -0
- package/dist/util/secretless-config.d.ts +39 -0
- package/dist/util/secretless-config.d.ts.map +1 -0
- package/dist/util/secretless-config.js +265 -0
- package/dist/util/secretless-config.js.map +1 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* opena2a init -- Initialize security posture assessment for a project.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* calculates
|
|
5
|
+
* Findings-first design: shows what was found, explains why it matters,
|
|
6
|
+
* calculates a unified security score, and generates prioritized actions.
|
|
7
7
|
*/
|
|
8
8
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
9
|
if (k2 === undefined) k2 = k;
|
|
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.calculateSecurityScore = void 0;
|
|
42
43
|
exports.init = init;
|
|
43
44
|
const fs = __importStar(require("node:fs"));
|
|
44
45
|
const path = __importStar(require("node:path"));
|
|
@@ -46,9 +47,13 @@ const colors_js_1 = require("../util/colors.js");
|
|
|
46
47
|
const detect_js_1 = require("../util/detect.js");
|
|
47
48
|
const credential_patterns_js_1 = require("../util/credential-patterns.js");
|
|
48
49
|
const advisories_js_1 = require("../util/advisories.js");
|
|
50
|
+
const format_js_1 = require("../util/format.js");
|
|
49
51
|
const version_js_1 = require("../util/version.js");
|
|
52
|
+
const spinner_js_1 = require("../util/spinner.js");
|
|
50
53
|
const events_js_1 = require("../shield/events.js");
|
|
51
54
|
const status_js_1 = require("../shield/status.js");
|
|
55
|
+
const ai_config_js_1 = require("../util/ai-config.js");
|
|
56
|
+
const scoring_js_1 = require("../util/scoring.js");
|
|
52
57
|
// --- Core ---
|
|
53
58
|
async function init(options) {
|
|
54
59
|
const targetDir = path.resolve(options.targetDir ?? process.cwd());
|
|
@@ -56,54 +61,86 @@ async function init(options) {
|
|
|
56
61
|
process.stderr.write((0, colors_js_1.red)(`Directory not found: ${targetDir}\n`));
|
|
57
62
|
return 1;
|
|
58
63
|
}
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
const isTTY = process.stderr.isTTY && options.format !== 'json';
|
|
66
|
+
const spinner = new spinner_js_1.Spinner('Scanning project...');
|
|
67
|
+
if (isTTY)
|
|
68
|
+
spinner.start();
|
|
59
69
|
// 1. Detect project type
|
|
60
70
|
const project = (0, detect_js_1.detectProject)(targetDir);
|
|
61
|
-
// 2. Quick credential scan
|
|
71
|
+
// 2. Quick credential scan (source files + MCP configs)
|
|
72
|
+
if (isTTY)
|
|
73
|
+
spinner.update('Scanning for credentials...');
|
|
62
74
|
const credentialMatches = (0, credential_patterns_js_1.quickCredentialScan)(targetDir);
|
|
75
|
+
// Scan MCP config files for credentials (these are skipped by walkFiles)
|
|
76
|
+
const mcpCreds = (0, ai_config_js_1.scanMcpCredentials)(targetDir);
|
|
77
|
+
const seenCredValues = new Set(credentialMatches.map(m => m.value));
|
|
78
|
+
for (const mc of mcpCreds) {
|
|
79
|
+
if (!seenCredValues.has(mc.value)) {
|
|
80
|
+
credentialMatches.push(mc);
|
|
81
|
+
seenCredValues.add(mc.value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
63
84
|
const credsBySeverity = {};
|
|
64
85
|
for (const m of credentialMatches) {
|
|
65
86
|
credsBySeverity[m.severity] = (credsBySeverity[m.severity] || 0) + 1;
|
|
66
87
|
}
|
|
67
88
|
// 3. Security hygiene checks
|
|
68
|
-
|
|
89
|
+
if (isTTY)
|
|
90
|
+
spinner.update('Checking environment...');
|
|
91
|
+
const checks = await runHygieneChecks(targetDir, project, credentialMatches.length);
|
|
69
92
|
// 4. Check advisories (non-blocking)
|
|
70
93
|
let advisoryCheck = { advisories: [], matchedPackages: [], total: 0, fromCache: false };
|
|
71
94
|
try {
|
|
72
95
|
advisoryCheck = await (0, advisories_js_1.checkAdvisories)(targetDir);
|
|
73
96
|
}
|
|
74
97
|
catch {
|
|
75
|
-
// Advisory check is best-effort
|
|
98
|
+
// Advisory check is best-effort
|
|
99
|
+
}
|
|
100
|
+
// 5. HMA integration (optional dynamic import)
|
|
101
|
+
if (isTTY)
|
|
102
|
+
spinner.update('Scanning shell environment...');
|
|
103
|
+
let hmaAvailable = false;
|
|
104
|
+
const hmaFindings = [];
|
|
105
|
+
try {
|
|
106
|
+
const hma = await import('hackmyagent');
|
|
107
|
+
hmaAvailable = true;
|
|
108
|
+
if (typeof hma.checkShellEnvironment === 'function') {
|
|
109
|
+
const shellEnv = await hma.checkShellEnvironment();
|
|
110
|
+
if (Array.isArray(shellEnv))
|
|
111
|
+
hmaFindings.push(...shellEnv);
|
|
112
|
+
}
|
|
113
|
+
if (typeof hma.checkShellHistory === 'function') {
|
|
114
|
+
const shellHistory = await hma.checkShellHistory();
|
|
115
|
+
if (Array.isArray(shellHistory))
|
|
116
|
+
hmaFindings.push(...shellHistory);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// HMA not installed -- skip silently
|
|
76
121
|
}
|
|
77
|
-
//
|
|
78
|
-
const
|
|
79
|
-
//
|
|
122
|
+
// 6. Group findings
|
|
123
|
+
const groupedFindings = groupFindings(credentialMatches, checks, hmaFindings);
|
|
124
|
+
// 7. Calculate unified security score
|
|
125
|
+
if (isTTY)
|
|
126
|
+
spinner.update('Assessing security posture...');
|
|
127
|
+
const hmaBySeverity = {};
|
|
128
|
+
for (const f of hmaFindings) {
|
|
129
|
+
hmaBySeverity[f.severity] = (hmaBySeverity[f.severity] || 0) + 1;
|
|
130
|
+
}
|
|
131
|
+
const { score, grade, breakdown } = (0, exports.calculateSecurityScore)(credsBySeverity, checks, hmaBySeverity);
|
|
132
|
+
// 8. Generate actions
|
|
133
|
+
const actions = generateActions(credentialMatches, credsBySeverity, checks, groupedFindings);
|
|
134
|
+
// 9. Generate legacy next steps (backward compat)
|
|
80
135
|
const nextSteps = generateNextSteps(credentialMatches.length, credsBySeverity, checks, project.type);
|
|
81
|
-
//
|
|
136
|
+
// 10. Shield tool status (for backward compat and tip line)
|
|
82
137
|
const shieldStatus = (0, status_js_1.getShieldStatus)(targetDir);
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
postureScore += Math.min(activeProducts * 10, 60);
|
|
87
|
-
if (shieldStatus.policyLoaded)
|
|
88
|
-
postureScore += 10;
|
|
89
|
-
if (shieldStatus.shellIntegration)
|
|
90
|
-
postureScore += 5;
|
|
91
|
-
if (credentialMatches.length === 0)
|
|
92
|
-
postureScore += 15;
|
|
93
|
-
const sigDir = path.join(targetDir, '.opena2a', 'signatures');
|
|
94
|
-
if (fs.existsSync(sigDir))
|
|
95
|
-
postureScore += 10;
|
|
96
|
-
postureScore = Math.max(0, Math.min(100, postureScore));
|
|
97
|
-
const riskLevel = postureScore < 30 ? 'CRITICAL'
|
|
98
|
-
: postureScore < 50 ? 'HIGH'
|
|
99
|
-
: postureScore < 70 ? 'MEDIUM'
|
|
100
|
-
: postureScore < 90 ? 'LOW'
|
|
101
|
-
: 'SECURE';
|
|
102
|
-
// 6.6. Write shield events for posture and credential findings
|
|
103
|
-
// Events are written to the project-local .opena2a/shield/ when available,
|
|
104
|
-
// falling back to the global ~/.opena2a/shield/.
|
|
138
|
+
const activeTools = shieldStatus.tools.filter(p => p.active).length;
|
|
139
|
+
const totalTools = shieldStatus.tools.length;
|
|
140
|
+
// 11. Write shield events
|
|
105
141
|
try {
|
|
106
142
|
(0, events_js_1.getShieldDir)(targetDir);
|
|
143
|
+
const riskLevel = scoreToRiskLevel(score);
|
|
107
144
|
(0, events_js_1.writeEvent)({
|
|
108
145
|
source: 'shield',
|
|
109
146
|
category: 'shield.posture',
|
|
@@ -113,7 +150,7 @@ async function init(options) {
|
|
|
113
150
|
action: 'posture-assessment',
|
|
114
151
|
target: targetDir,
|
|
115
152
|
outcome: 'monitored',
|
|
116
|
-
detail: { score
|
|
153
|
+
detail: { score, grade, breakdown, activeTools, totalTools },
|
|
117
154
|
orgId: null,
|
|
118
155
|
managed: false,
|
|
119
156
|
agentId: null,
|
|
@@ -138,8 +175,13 @@ async function init(options) {
|
|
|
138
175
|
catch {
|
|
139
176
|
// Shield event writing is best-effort
|
|
140
177
|
}
|
|
141
|
-
|
|
178
|
+
if (isTTY)
|
|
179
|
+
spinner.stop();
|
|
180
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
181
|
+
// 12. Build report
|
|
182
|
+
const riskLevel = scoreToRiskLevel(score);
|
|
142
183
|
const report = {
|
|
184
|
+
version: 2,
|
|
143
185
|
projectName: project.name,
|
|
144
186
|
projectVersion: project.version,
|
|
145
187
|
projectType: formatProjectType(project),
|
|
@@ -147,41 +189,31 @@ async function init(options) {
|
|
|
147
189
|
credentialFindings: credentialMatches.length,
|
|
148
190
|
credentialsBySeverity: credsBySeverity,
|
|
149
191
|
hygieneChecks: checks,
|
|
150
|
-
|
|
151
|
-
grade,
|
|
192
|
+
securityScore: score,
|
|
193
|
+
securityGrade: grade,
|
|
194
|
+
scoreBreakdown: breakdown,
|
|
195
|
+
findings: groupedFindings,
|
|
196
|
+
actions,
|
|
152
197
|
nextSteps,
|
|
153
198
|
advisories: {
|
|
154
199
|
count: advisoryCheck.advisories.length,
|
|
155
200
|
matchedPackages: advisoryCheck.matchedPackages,
|
|
156
201
|
},
|
|
157
|
-
|
|
202
|
+
hmaAvailable,
|
|
203
|
+
// Backward compat aliases
|
|
204
|
+
trustScore: score,
|
|
205
|
+
grade,
|
|
206
|
+
postureScore: score,
|
|
158
207
|
riskLevel,
|
|
159
|
-
|
|
160
|
-
|
|
208
|
+
activeTools,
|
|
209
|
+
totalTools,
|
|
161
210
|
};
|
|
162
|
-
//
|
|
211
|
+
// 13. Output
|
|
163
212
|
if (options.format === 'json') {
|
|
164
213
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
165
214
|
}
|
|
166
215
|
else {
|
|
167
|
-
printReport(report, options.verbose);
|
|
168
|
-
// Verbose: show individual credential findings
|
|
169
|
-
if (options.verbose && credentialMatches.length > 0) {
|
|
170
|
-
process.stdout.write((0, colors_js_1.bold)(' Credential Details') + '\n');
|
|
171
|
-
process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
|
|
172
|
-
for (const m of credentialMatches) {
|
|
173
|
-
const sev = m.severity === 'critical' ? (0, colors_js_1.red)('[CRITICAL]')
|
|
174
|
-
: m.severity === 'high' ? (0, colors_js_1.yellow)('[HIGH]')
|
|
175
|
-
: (0, colors_js_1.cyan)('[MEDIUM]');
|
|
176
|
-
const relPath = path.relative(targetDir, m.filePath);
|
|
177
|
-
process.stdout.write(` ${sev} ${(0, colors_js_1.bold)(m.findingId)}: ${m.title}\n`);
|
|
178
|
-
process.stdout.write(` ${(0, colors_js_1.dim)(' File:')} ${relPath}:${m.line}\n`);
|
|
179
|
-
if (m.explanation) {
|
|
180
|
-
process.stdout.write(` ${(0, colors_js_1.dim)(' Why:')} ${m.explanation}\n`);
|
|
181
|
-
}
|
|
182
|
-
process.stdout.write('\n');
|
|
183
|
-
}
|
|
184
|
-
}
|
|
216
|
+
printReport(report, elapsed, options.verbose);
|
|
185
217
|
// Drift detection callout (always shown when drift findings exist)
|
|
186
218
|
const driftFindings = credentialMatches.filter(m => m.findingId.startsWith('DRIFT'));
|
|
187
219
|
if (driftFindings.length > 0) {
|
|
@@ -208,7 +240,7 @@ async function init(options) {
|
|
|
208
240
|
return hasCritical ? 1 : 0;
|
|
209
241
|
}
|
|
210
242
|
// --- Hygiene checks ---
|
|
211
|
-
function runHygieneChecks(dir, project, credCount) {
|
|
243
|
+
async function runHygieneChecks(dir, project, credCount) {
|
|
212
244
|
const checks = [];
|
|
213
245
|
// Credential scan result
|
|
214
246
|
if (credCount === 0) {
|
|
@@ -268,48 +300,287 @@ function runHygieneChecks(dir, project, credCount) {
|
|
|
268
300
|
if (project.hasMcp) {
|
|
269
301
|
checks.push({ label: 'MCP config', status: 'info', detail: 'found' });
|
|
270
302
|
}
|
|
303
|
+
// LLM server exposure (lightweight probe of common ports)
|
|
304
|
+
const llmCheck = await checkLLMServerExposure();
|
|
305
|
+
if (llmCheck) {
|
|
306
|
+
checks.push(llmCheck);
|
|
307
|
+
}
|
|
308
|
+
// AI-specific configuration scans
|
|
309
|
+
for (const f of (0, ai_config_js_1.scanMcpConfig)(dir)) {
|
|
310
|
+
checks.push({ label: f.label, status: f.status, detail: f.detail });
|
|
311
|
+
}
|
|
312
|
+
const aiCfg = (0, ai_config_js_1.scanAiConfigFiles)(dir);
|
|
313
|
+
if (aiCfg)
|
|
314
|
+
checks.push({ label: aiCfg.label, status: aiCfg.status, detail: aiCfg.detail });
|
|
315
|
+
const skills = (0, ai_config_js_1.scanSkillFiles)(dir);
|
|
316
|
+
if (skills)
|
|
317
|
+
checks.push({ label: skills.label, status: skills.status, detail: skills.detail });
|
|
318
|
+
const soul = (0, ai_config_js_1.scanSoulFile)(dir);
|
|
319
|
+
if (soul)
|
|
320
|
+
checks.push({ label: soul.label, status: soul.status, detail: soul.detail });
|
|
271
321
|
return checks;
|
|
272
322
|
}
|
|
273
|
-
// ---
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
323
|
+
// --- LLM server exposure check ---
|
|
324
|
+
const LLM_PROBE_PORTS = [
|
|
325
|
+
{ name: 'Ollama', port: 11434, path: '/api/tags' },
|
|
326
|
+
{ name: 'LM Studio', port: 1234, path: '/v1/models' },
|
|
327
|
+
];
|
|
328
|
+
async function checkLLMServerExposure() {
|
|
329
|
+
for (const server of LLM_PROBE_PORTS) {
|
|
330
|
+
const controller = new AbortController();
|
|
331
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
332
|
+
try {
|
|
333
|
+
const resp = await fetch(`http://127.0.0.1:${server.port}${server.path}`, {
|
|
334
|
+
signal: controller.signal,
|
|
335
|
+
});
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
if (resp.ok || resp.status < 500) {
|
|
338
|
+
// Check if no auth required
|
|
339
|
+
const noAuth = resp.status !== 401 && resp.status !== 403;
|
|
340
|
+
if (noAuth) {
|
|
341
|
+
return {
|
|
342
|
+
label: 'LLM server exposure',
|
|
343
|
+
status: 'warn',
|
|
344
|
+
detail: `${server.name} on :${server.port} (no auth)`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
label: 'LLM server exposure',
|
|
349
|
+
status: 'info',
|
|
350
|
+
detail: `${server.name} on :${server.port}`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
// Server not running on this port, continue
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
// --- Finding grouping ---
|
|
362
|
+
function groupFindings(creds, checks, hmaFindings) {
|
|
363
|
+
const groups = new Map();
|
|
364
|
+
// Group credential findings by findingId
|
|
365
|
+
for (const cred of creds) {
|
|
366
|
+
const existing = groups.get(cred.findingId);
|
|
367
|
+
if (existing) {
|
|
368
|
+
existing.count++;
|
|
369
|
+
existing.locations.push({
|
|
370
|
+
file: cred.filePath,
|
|
371
|
+
line: cred.line,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
groups.set(cred.findingId, {
|
|
376
|
+
findingId: cred.findingId,
|
|
377
|
+
title: cred.title,
|
|
378
|
+
severity: cred.severity,
|
|
379
|
+
count: 1,
|
|
380
|
+
explanation: cred.explanation ?? '',
|
|
381
|
+
businessImpact: cred.businessImpact ?? '',
|
|
382
|
+
locations: [{ file: cred.filePath, line: cred.line }],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Add hygiene findings as grouped findings
|
|
387
|
+
const llmCheck = checks.find(c => c.label === 'LLM server exposure' && c.status === 'warn');
|
|
388
|
+
if (llmCheck) {
|
|
389
|
+
groups.set('ENV-LLM', {
|
|
390
|
+
findingId: 'ENV-LLM',
|
|
391
|
+
title: llmCheck.detail,
|
|
392
|
+
severity: 'high',
|
|
393
|
+
count: 1,
|
|
394
|
+
explanation: 'Local LLM server is responding without authentication. Adding auth limits access to authorized users.',
|
|
395
|
+
businessImpact: 'Unauthenticated model access. Adding auth ensures only intended users can query.',
|
|
396
|
+
locations: [],
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const envCheck = checks.find(c => c.label === '.env protection' && c.status === 'warn');
|
|
400
|
+
if (envCheck) {
|
|
401
|
+
groups.set('ENV-DOTENV', {
|
|
402
|
+
findingId: 'ENV-DOTENV',
|
|
403
|
+
title: '.env not in .gitignore',
|
|
404
|
+
severity: 'medium',
|
|
405
|
+
count: 1,
|
|
406
|
+
explanation: 'Environment files may be committed to version control.',
|
|
407
|
+
businessImpact: 'Adding .env to .gitignore keeps secrets out of version control.',
|
|
408
|
+
locations: [],
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
// Add AI config findings
|
|
412
|
+
const mcpToolsCheck = checks.find(c => c.label === 'MCP high-risk tools' && c.status === 'warn');
|
|
413
|
+
if (mcpToolsCheck) {
|
|
414
|
+
groups.set('MCP-TOOLS', {
|
|
415
|
+
findingId: 'MCP-TOOLS',
|
|
416
|
+
title: mcpToolsCheck.detail,
|
|
417
|
+
severity: 'high',
|
|
418
|
+
count: 1,
|
|
419
|
+
explanation: 'MCP servers with filesystem or shell access can read, modify, or delete files on your system when invoked by an AI assistant.',
|
|
420
|
+
businessImpact: 'Review server permissions to ensure each server has only the access it needs.',
|
|
421
|
+
locations: [],
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const mcpCredCheck = checks.find(c => c.label === 'MCP credentials' && c.status === 'warn');
|
|
425
|
+
if (mcpCredCheck) {
|
|
426
|
+
groups.set('MCP-CRED', {
|
|
427
|
+
findingId: 'MCP-CRED',
|
|
428
|
+
title: mcpCredCheck.detail,
|
|
429
|
+
severity: 'high',
|
|
430
|
+
count: 1,
|
|
431
|
+
explanation: 'API keys hardcoded in MCP config files are readable by anyone with access to the project directory.',
|
|
432
|
+
businessImpact: 'Move credentials to environment variables so they are not stored in plaintext.',
|
|
433
|
+
locations: [],
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const aiConfigCheck = checks.find(c => c.label === 'AI config exposure' && c.status === 'warn');
|
|
437
|
+
if (aiConfigCheck) {
|
|
438
|
+
groups.set('AI-CONFIG', {
|
|
439
|
+
findingId: 'AI-CONFIG',
|
|
440
|
+
title: aiConfigCheck.detail,
|
|
441
|
+
severity: 'medium',
|
|
442
|
+
count: 1,
|
|
443
|
+
explanation: 'AI instruction files (CLAUDE.md, .cursorrules, etc.) reveal tooling choices and system prompts when committed to a public repository.',
|
|
444
|
+
businessImpact: 'Add these files to .git/info/exclude to keep them local without modifying .gitignore.',
|
|
445
|
+
locations: [],
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
// Add HMA findings
|
|
449
|
+
for (const f of hmaFindings) {
|
|
450
|
+
const key = `HMA-${f.checkId}`;
|
|
451
|
+
const existing = groups.get(key);
|
|
452
|
+
if (existing) {
|
|
453
|
+
existing.count++;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
groups.set(key, {
|
|
457
|
+
findingId: key,
|
|
458
|
+
title: f.message,
|
|
459
|
+
severity: f.severity,
|
|
460
|
+
count: 1,
|
|
461
|
+
explanation: '',
|
|
462
|
+
businessImpact: '',
|
|
463
|
+
locations: [],
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Sort by severity order: critical > high > medium > low
|
|
468
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
469
|
+
return Array.from(groups.values()).sort((a, b) => {
|
|
470
|
+
const sa = severityOrder[a.severity] ?? 4;
|
|
471
|
+
const sb = severityOrder[b.severity] ?? 4;
|
|
472
|
+
if (sa !== sb)
|
|
473
|
+
return sa - sb;
|
|
474
|
+
return b.count - a.count;
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
// --- Unified Security Score ---
|
|
478
|
+
/**
|
|
479
|
+
* Re-export from shared module for backward compatibility.
|
|
480
|
+
* Tests (security-score.test.ts) import this from init.ts.
|
|
481
|
+
*/
|
|
482
|
+
exports.calculateSecurityScore = scoring_js_1.calculateSecurityScore;
|
|
483
|
+
const scoreToRiskLevel = scoring_js_1.scoreToRiskLevel;
|
|
484
|
+
// --- Actions ---
|
|
485
|
+
function generateActions(creds, credsBySeverity, checks, findings) {
|
|
486
|
+
const actions = [];
|
|
487
|
+
// Credential migration action
|
|
488
|
+
if (creds.length > 0) {
|
|
489
|
+
// Build breakdown string
|
|
490
|
+
const byTitle = new Map();
|
|
491
|
+
for (const c of creds) {
|
|
492
|
+
byTitle.set(c.title, (byTitle.get(c.title) || 0) + 1);
|
|
493
|
+
}
|
|
494
|
+
const breakdownParts = Array.from(byTitle.entries())
|
|
495
|
+
.map(([title, count]) => `${count} ${title.replace(/ \(.*\)/, '')}`)
|
|
496
|
+
.join(', ');
|
|
497
|
+
actions.push({
|
|
498
|
+
description: `Migrate ${creds.length} hardcoded credential${creds.length === 1 ? '' : 's'} to a vault`,
|
|
499
|
+
command: 'opena2a protect',
|
|
500
|
+
why: 'Credentials in source files are readable by anyone with repo access. A vault stores them encrypted, rotates them automatically, and provides an audit trail.',
|
|
501
|
+
approach: 'Moves keys to environment variables backed by an encrypted vault. Keys rotate without code changes, and access is auditable.',
|
|
502
|
+
detail: `Keys found: ${breakdownParts}`,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
// .env protection
|
|
285
506
|
const envCheck = checks.find(c => c.label === '.env protection');
|
|
286
|
-
if (envCheck?.status === 'warn')
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
507
|
+
if (envCheck?.status === 'warn') {
|
|
508
|
+
actions.push({
|
|
509
|
+
description: 'Add .env to .gitignore',
|
|
510
|
+
command: 'opena2a protect',
|
|
511
|
+
why: 'Adding .env to .gitignore prevents secrets from entering version control. Existing tracked .env files also need `git rm --cached .env`.',
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
// .gitignore
|
|
515
|
+
const gitignoreCheck = checks.find(c => c.label === '.gitignore');
|
|
516
|
+
if (gitignoreCheck?.status !== 'pass') {
|
|
517
|
+
actions.push({
|
|
518
|
+
description: 'Create .gitignore with .env exclusion',
|
|
519
|
+
command: 'opena2a protect',
|
|
520
|
+
why: 'Without a .gitignore, build artifacts and sensitive files can be committed accidentally. Protect creates one with .env exclusion to prevent secret leaks.',
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
// Shell environment findings (from HMA)
|
|
524
|
+
const hmaFindings = findings.filter(f => f.findingId.startsWith('HMA-'));
|
|
525
|
+
if (hmaFindings.length > 0) {
|
|
526
|
+
const totalHma = hmaFindings.reduce((n, f) => n + f.count, 0);
|
|
527
|
+
actions.push({
|
|
528
|
+
description: `Clean ${totalHma} shell environment finding${totalHma === 1 ? '' : 's'}`,
|
|
529
|
+
command: 'opena2a scan secure',
|
|
530
|
+
why: 'Shell config files and history can contain API keys in plaintext. Rotating exposed keys and clearing history entries removes persistent exposure.',
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// LLM server exposure
|
|
534
|
+
const llmCheck = checks.find(c => c.label === 'LLM server exposure' && c.status === 'warn');
|
|
535
|
+
if (llmCheck) {
|
|
536
|
+
actions.push({
|
|
537
|
+
description: 'Secure LLM server',
|
|
538
|
+
command: 'opena2a shield status',
|
|
539
|
+
why: 'A local LLM server without authentication accepts requests from any process on the network. Binding to localhost or adding auth limits access.',
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
// MCP high-risk tools
|
|
543
|
+
const mcpToolsFinding = findings.find(f => f.findingId === 'MCP-TOOLS');
|
|
544
|
+
if (mcpToolsFinding) {
|
|
545
|
+
actions.push({
|
|
546
|
+
description: 'Review MCP server permissions',
|
|
547
|
+
command: 'opena2a shield status',
|
|
548
|
+
why: 'MCP servers with filesystem or shell access can read, modify, or delete files when invoked by an AI assistant. Review each server to confirm it has only the access it needs.',
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
// MCP credentials
|
|
552
|
+
const mcpCredFinding = findings.find(f => f.findingId === 'MCP-CRED');
|
|
553
|
+
if (mcpCredFinding) {
|
|
554
|
+
actions.push({
|
|
555
|
+
description: 'Move MCP config credentials to environment variables',
|
|
556
|
+
command: 'opena2a protect',
|
|
557
|
+
why: 'API keys hardcoded in MCP config files are stored in plaintext. Environment variables keep credentials out of the project directory and version control.',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
// AI config exposure
|
|
561
|
+
const aiConfigFinding = findings.find(f => f.findingId === 'AI-CONFIG');
|
|
562
|
+
if (aiConfigFinding) {
|
|
563
|
+
actions.push({
|
|
564
|
+
description: 'Exclude AI instruction files from git',
|
|
565
|
+
command: 'opena2a protect',
|
|
566
|
+
why: 'AI instruction files reveal tooling choices and system prompts when committed to a public repository. Adding them to .git/info/exclude keeps them local without modifying .gitignore.',
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
// Config signing (only suggest if fewer than 5 actions already -- lower priority)
|
|
292
570
|
const secConfig = checks.find(c => c.label === 'Security config');
|
|
293
|
-
if (secConfig?.status
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
grade = 'C';
|
|
303
|
-
else if (score >= 60)
|
|
304
|
-
grade = 'D';
|
|
305
|
-
else
|
|
306
|
-
grade = 'F';
|
|
307
|
-
return { score, grade };
|
|
571
|
+
if (actions.length < 5 && secConfig?.status !== 'pass') {
|
|
572
|
+
actions.push({
|
|
573
|
+
description: 'Sign config files for integrity monitoring',
|
|
574
|
+
command: 'opena2a protect',
|
|
575
|
+
why: 'Signed baselines let you detect unintended config changes before they affect runtime behavior.',
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
// Cap at 5 actions
|
|
579
|
+
return actions.slice(0, 5);
|
|
308
580
|
}
|
|
309
|
-
// ---
|
|
581
|
+
// --- Legacy next steps (backward compat) ---
|
|
310
582
|
function generateNextSteps(credCount, credsBySeverity, checks, projectType) {
|
|
311
583
|
const steps = [];
|
|
312
|
-
// Credentials -> protect
|
|
313
584
|
if (credCount > 0) {
|
|
314
585
|
steps.push({
|
|
315
586
|
severity: 'critical',
|
|
@@ -317,34 +588,27 @@ function generateNextSteps(credCount, credsBySeverity, checks, projectType) {
|
|
|
317
588
|
command: 'opena2a protect',
|
|
318
589
|
});
|
|
319
590
|
}
|
|
320
|
-
// .env protection
|
|
321
591
|
const envCheck = checks.find(c => c.label === '.env protection');
|
|
322
592
|
if (envCheck?.status === 'warn') {
|
|
323
593
|
steps.push({
|
|
324
594
|
severity: 'high',
|
|
325
595
|
description: 'Add .env to .gitignore',
|
|
326
|
-
command:
|
|
596
|
+
command: 'opena2a protect',
|
|
327
597
|
});
|
|
328
598
|
}
|
|
329
|
-
// No .gitignore
|
|
330
599
|
const gitignoreCheck = checks.find(c => c.label === '.gitignore');
|
|
331
600
|
if (gitignoreCheck?.status !== 'pass') {
|
|
332
|
-
const gitignoreTemplate = projectType === 'python' ? 'python'
|
|
333
|
-
: projectType === 'go' ? 'go'
|
|
334
|
-
: 'node';
|
|
335
601
|
steps.push({
|
|
336
602
|
severity: 'high',
|
|
337
|
-
description: 'Create .gitignore',
|
|
338
|
-
command:
|
|
603
|
+
description: 'Create .gitignore with .env exclusion',
|
|
604
|
+
command: 'opena2a protect',
|
|
339
605
|
});
|
|
340
606
|
}
|
|
341
|
-
// Sign config files
|
|
342
607
|
steps.push({
|
|
343
608
|
severity: 'medium',
|
|
344
609
|
description: 'Sign config files for integrity',
|
|
345
|
-
command: 'opena2a
|
|
610
|
+
command: 'opena2a protect',
|
|
346
611
|
});
|
|
347
|
-
// Runtime protection
|
|
348
612
|
steps.push({
|
|
349
613
|
severity: 'low',
|
|
350
614
|
description: 'Start runtime protection',
|
|
@@ -352,84 +616,270 @@ function generateNextSteps(credCount, credsBySeverity, checks, projectType) {
|
|
|
352
616
|
});
|
|
353
617
|
return steps;
|
|
354
618
|
}
|
|
619
|
+
// --- Verification & Recommendation helpers ---
|
|
620
|
+
function getVerificationCommand(finding, reportDir) {
|
|
621
|
+
// Credential/drift findings with file locations
|
|
622
|
+
if ((finding.findingId.startsWith('CRED-') || finding.findingId.startsWith('DRIFT-')) &&
|
|
623
|
+
finding.locations.length > 0) {
|
|
624
|
+
const loc = finding.locations[0];
|
|
625
|
+
const rel = path.relative(reportDir, loc.file);
|
|
626
|
+
return `sed -n '${loc.line}p' ${rel}`;
|
|
627
|
+
}
|
|
628
|
+
// HMA findings -- title contains "~/.zshrc:132 contains ..." pattern
|
|
629
|
+
if (finding.findingId.startsWith('HMA-')) {
|
|
630
|
+
const match = finding.title.match(/^(.+?):(\d+)\s+contains\s+/);
|
|
631
|
+
if (match) {
|
|
632
|
+
return `sed -n '${match[2]}p' ${match[1]}`;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (finding.findingId === 'ENV-LLM') {
|
|
636
|
+
return 'curl -s http://127.0.0.1:11434/api/tags | head -c 200';
|
|
637
|
+
}
|
|
638
|
+
if (finding.findingId === 'ENV-DOTENV') {
|
|
639
|
+
return "cat .gitignore | grep -c '.env'";
|
|
640
|
+
}
|
|
641
|
+
if (finding.findingId === 'MCP-TOOLS') {
|
|
642
|
+
// Show the first MCP config file found
|
|
643
|
+
for (const f of ['mcp.json', '.mcp.json', '.claude/settings.json', '.cursor/mcp.json']) {
|
|
644
|
+
if (fs.existsSync(path.join(reportDir, f))) {
|
|
645
|
+
return `cat ${f}`;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return 'cat mcp.json';
|
|
649
|
+
}
|
|
650
|
+
if (finding.findingId === 'MCP-CRED') {
|
|
651
|
+
for (const f of ['mcp.json', '.mcp.json', '.claude/settings.json', '.cursor/mcp.json']) {
|
|
652
|
+
if (fs.existsSync(path.join(reportDir, f))) {
|
|
653
|
+
return `cat ${f}`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return 'cat mcp.json';
|
|
657
|
+
}
|
|
658
|
+
if (finding.findingId === 'AI-CONFIG') {
|
|
659
|
+
return 'cat .gitignore';
|
|
660
|
+
}
|
|
661
|
+
if (finding.findingId === 'AI-SKILLS') {
|
|
662
|
+
return 'ls *.skill.md SKILL.md 2>/dev/null';
|
|
663
|
+
}
|
|
664
|
+
if (finding.findingId === 'AI-SOUL') {
|
|
665
|
+
return 'head -20 soul.md';
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
function getToolRecommendation(findingId) {
|
|
670
|
+
if (findingId.startsWith('CRED-') || findingId.startsWith('DRIFT-')) {
|
|
671
|
+
return { command: 'opena2a protect', label: 'opena2a protect' };
|
|
672
|
+
}
|
|
673
|
+
if (findingId === 'ENV-LLM') {
|
|
674
|
+
return { command: 'opena2a shield status', label: 'opena2a shield status' };
|
|
675
|
+
}
|
|
676
|
+
if (findingId === 'ENV-DOTENV') {
|
|
677
|
+
return { command: 'opena2a protect', label: 'opena2a protect' };
|
|
678
|
+
}
|
|
679
|
+
if (findingId.startsWith('HMA-')) {
|
|
680
|
+
return { command: 'opena2a scan secure', label: 'opena2a scan secure' };
|
|
681
|
+
}
|
|
682
|
+
if (findingId === 'MCP-TOOLS') {
|
|
683
|
+
return { command: 'opena2a shield status', label: 'opena2a shield status' };
|
|
684
|
+
}
|
|
685
|
+
if (findingId === 'MCP-CRED') {
|
|
686
|
+
return { command: 'opena2a protect', label: 'opena2a protect' };
|
|
687
|
+
}
|
|
688
|
+
if (findingId === 'AI-CONFIG') {
|
|
689
|
+
return { command: 'opena2a protect', label: 'opena2a protect' };
|
|
690
|
+
}
|
|
691
|
+
if (findingId === 'AI-SKILLS') {
|
|
692
|
+
return { command: 'opena2a guard sign --skills', label: 'opena2a guard sign --skills' };
|
|
693
|
+
}
|
|
694
|
+
if (findingId === 'AI-SOUL') {
|
|
695
|
+
return { command: 'opena2a guard sign', label: 'opena2a guard sign' };
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
function getContextualTip(report) {
|
|
700
|
+
const hasAnyCreds = report.findings.some(f => f.findingId.startsWith('CRED-') || f.findingId.startsWith('DRIFT-'));
|
|
701
|
+
if (hasAnyCreds) {
|
|
702
|
+
return {
|
|
703
|
+
text: 'Migrate credentials out of source files',
|
|
704
|
+
command: 'opena2a protect',
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
const hasMcpCred = report.findings.some(f => f.findingId === 'MCP-CRED');
|
|
708
|
+
if (hasMcpCred) {
|
|
709
|
+
return {
|
|
710
|
+
text: 'Move credentials out of MCP config files',
|
|
711
|
+
command: 'opena2a protect',
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const hasAiConfig = report.findings.some(f => f.findingId === 'AI-CONFIG');
|
|
715
|
+
if (hasAiConfig) {
|
|
716
|
+
return {
|
|
717
|
+
text: 'Fix all auto-fixable findings',
|
|
718
|
+
command: 'opena2a protect',
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const hasLLM = report.findings.some(f => f.findingId === 'ENV-LLM');
|
|
722
|
+
if (hasLLM) {
|
|
723
|
+
return {
|
|
724
|
+
text: 'Add authentication to your LLM server',
|
|
725
|
+
command: 'opena2a shield status',
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
const hasEnv = report.findings.some(f => f.findingId === 'ENV-DOTENV');
|
|
729
|
+
if (hasEnv) {
|
|
730
|
+
return {
|
|
731
|
+
text: 'Fix all auto-fixable findings',
|
|
732
|
+
command: 'opena2a protect',
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (report.securityScore >= 90) {
|
|
736
|
+
return {
|
|
737
|
+
text: 'Strong baseline. Run a full scan for deeper coverage',
|
|
738
|
+
command: 'opena2a scan secure',
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (report.securityScore >= 70) {
|
|
742
|
+
return {
|
|
743
|
+
text: 'Good posture. Lock it in with config file integrity signing',
|
|
744
|
+
command: 'opena2a guard sign',
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
text: 'See all available security tools',
|
|
749
|
+
command: 'opena2a shield status',
|
|
750
|
+
};
|
|
751
|
+
}
|
|
355
752
|
// --- Output ---
|
|
356
753
|
function formatProjectType(project) {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
parts.push('+ MCP server');
|
|
754
|
+
const primary = {
|
|
755
|
+
node: 'Node.js',
|
|
756
|
+
go: 'Go',
|
|
757
|
+
python: 'Python',
|
|
758
|
+
rust: 'Rust',
|
|
759
|
+
java: 'Java',
|
|
760
|
+
ruby: 'Ruby',
|
|
761
|
+
docker: 'Docker',
|
|
762
|
+
generic: 'Project',
|
|
763
|
+
};
|
|
764
|
+
const parts = [primary[project.type]];
|
|
765
|
+
for (const hint of project.frameworkHints) {
|
|
766
|
+
parts.push(`+ ${hint}`);
|
|
767
|
+
}
|
|
372
768
|
return parts.join(' ');
|
|
373
769
|
}
|
|
374
|
-
function printReport(report,
|
|
770
|
+
function printReport(report, elapsed, verbose) {
|
|
375
771
|
const VERSION = (0, version_js_1.getVersion)();
|
|
376
772
|
process.stdout.write('\n');
|
|
377
|
-
process.stdout.write((0, colors_js_1.bold)(' OpenA2A Security
|
|
773
|
+
process.stdout.write((0, colors_js_1.bold)(' OpenA2A Security Assessment') + (0, colors_js_1.dim)(` v${VERSION}`) + (0, colors_js_1.dim)(` ${elapsed}s`) + '\n\n');
|
|
378
774
|
// Project info
|
|
379
775
|
const projectDisplay = report.projectName
|
|
380
776
|
? `${report.projectName}${report.projectVersion ? ' v' + report.projectVersion : ''}`
|
|
381
777
|
: path.basename(report.directory);
|
|
382
778
|
process.stdout.write(` ${(0, colors_js_1.dim)('Project')} ${projectDisplay}\n`);
|
|
383
|
-
process.stdout.write(` ${(0, colors_js_1.dim)('
|
|
779
|
+
process.stdout.write(` ${(0, colors_js_1.dim)('Stack')} ${report.projectType}\n`);
|
|
384
780
|
process.stdout.write(` ${(0, colors_js_1.dim)('Directory')} ${report.directory}\n`);
|
|
385
781
|
process.stdout.write('\n');
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
782
|
+
// --- Findings section ---
|
|
783
|
+
if (report.findings.length > 0) {
|
|
784
|
+
process.stdout.write((0, colors_js_1.bold)(' Findings') + '\n');
|
|
785
|
+
process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
|
|
786
|
+
const maxFindings = verbose ? report.findings.length : 5;
|
|
787
|
+
const displayFindings = report.findings.slice(0, maxFindings);
|
|
788
|
+
for (const finding of displayFindings) {
|
|
789
|
+
const sevTag = finding.severity === 'critical' ? (0, colors_js_1.red)('CRITICAL')
|
|
790
|
+
: finding.severity === 'high' ? (0, colors_js_1.yellow)('HIGH ')
|
|
791
|
+
: finding.severity === 'medium' ? (0, colors_js_1.cyan)('MEDIUM ')
|
|
792
|
+
: (0, colors_js_1.dim)('LOW ');
|
|
793
|
+
const countPrefix = finding.count > 1 ? `${finding.count} ` : '';
|
|
794
|
+
process.stdout.write(` ${sevTag} ${countPrefix}${(0, colors_js_1.bold)(finding.title)}\n`);
|
|
795
|
+
if (finding.explanation) {
|
|
796
|
+
const wrapped = (0, format_js_1.wordWrap)(finding.explanation, 70, 12);
|
|
797
|
+
process.stdout.write((0, colors_js_1.dim)(wrapped) + '\n');
|
|
798
|
+
}
|
|
799
|
+
// Show file locations (max 3 unless verbose)
|
|
800
|
+
if (finding.locations.length > 0) {
|
|
801
|
+
const maxLocs = verbose ? finding.locations.length : 3;
|
|
802
|
+
const locs = finding.locations.slice(0, maxLocs);
|
|
803
|
+
const locStrings = locs.map(l => {
|
|
804
|
+
const rel = path.relative(report.directory, l.file);
|
|
805
|
+
return `${rel}:${l.line}`;
|
|
806
|
+
});
|
|
807
|
+
process.stdout.write((0, colors_js_1.dim)(' ' + locStrings.join(' ')) + '\n');
|
|
808
|
+
if (finding.locations.length > maxLocs) {
|
|
809
|
+
process.stdout.write((0, colors_js_1.dim)(` +${finding.locations.length - maxLocs} more`) + '\n');
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// Verification command
|
|
813
|
+
const verifyCmd = getVerificationCommand(finding, report.directory);
|
|
814
|
+
if (verifyCmd) {
|
|
815
|
+
process.stdout.write(` ${(0, colors_js_1.dim)('Verify:')} ${(0, colors_js_1.cyan)(verifyCmd)}\n`);
|
|
816
|
+
}
|
|
817
|
+
// Tool recommendation
|
|
818
|
+
const toolRec = getToolRecommendation(finding.findingId);
|
|
819
|
+
if (toolRec) {
|
|
820
|
+
process.stdout.write(` ${(0, colors_js_1.dim)('Fix:')} ${(0, colors_js_1.cyan)(toolRec.command)}\n`);
|
|
821
|
+
}
|
|
822
|
+
process.stdout.write('\n');
|
|
823
|
+
}
|
|
824
|
+
if (!verbose && report.findings.length > maxFindings) {
|
|
825
|
+
const remaining = report.findings.length - maxFindings;
|
|
826
|
+
process.stdout.write((0, colors_js_1.dim)(` [+${remaining} more finding${remaining === 1 ? '' : 's'} -- run with --verbose to see all]`) + '\n');
|
|
827
|
+
process.stdout.write('\n');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
process.stdout.write((0, colors_js_1.green)(' No security findings detected.') + '\n\n');
|
|
832
|
+
}
|
|
833
|
+
// --- Security Score ---
|
|
834
|
+
const scoreColor = report.securityScore >= 80 ? colors_js_1.green
|
|
835
|
+
: report.securityScore >= 60 ? colors_js_1.yellow
|
|
408
836
|
: colors_js_1.red;
|
|
409
|
-
|
|
410
|
-
process.stdout.write(` ${(0, colors_js_1.
|
|
837
|
+
const breakdown = report.scoreBreakdown;
|
|
838
|
+
process.stdout.write(` ${(0, colors_js_1.bold)('Security Score:')} ${scoreColor(`${report.securityScore}`)} ${(0, colors_js_1.dim)('/ 100')}\n`);
|
|
411
839
|
process.stdout.write('\n');
|
|
412
|
-
//
|
|
413
|
-
if (
|
|
414
|
-
process.stdout.write((0, colors_js_1.
|
|
840
|
+
// Breakdown as factual deductions
|
|
841
|
+
if (breakdown.credentials.deduction > 0) {
|
|
842
|
+
process.stdout.write(` ${(0, colors_js_1.dim)('Credentials')} ${(0, colors_js_1.red)(`-${breakdown.credentials.deduction}`)} ${(0, colors_js_1.dim)(breakdown.credentials.detail)}\n`);
|
|
843
|
+
}
|
|
844
|
+
if (breakdown.environment.deduction > 0) {
|
|
845
|
+
process.stdout.write(` ${(0, colors_js_1.dim)('Environment')} ${(0, colors_js_1.red)(`-${breakdown.environment.deduction}`)} ${(0, colors_js_1.dim)(breakdown.environment.detail)}\n`);
|
|
846
|
+
}
|
|
847
|
+
if (breakdown.configuration.deduction > 0) {
|
|
848
|
+
process.stdout.write(` ${(0, colors_js_1.dim)('Configuration')} ${(0, colors_js_1.red)(`-${breakdown.configuration.deduction}`)} ${(0, colors_js_1.dim)(breakdown.configuration.detail)}\n`);
|
|
849
|
+
}
|
|
850
|
+
else if (breakdown.configuration.deduction < 0) {
|
|
851
|
+
process.stdout.write(` ${(0, colors_js_1.dim)('Configuration')} ${(0, colors_js_1.green)(`+${Math.abs(breakdown.configuration.deduction)}`)} ${(0, colors_js_1.dim)(breakdown.configuration.detail)}\n`);
|
|
852
|
+
}
|
|
853
|
+
// "After fixes" line when improvements are possible
|
|
854
|
+
const totalRecoverable = breakdown.credentials.deduction
|
|
855
|
+
+ breakdown.environment.deduction
|
|
856
|
+
+ Math.max(0, breakdown.configuration.deduction);
|
|
857
|
+
const potentialScore = Math.min(100, report.securityScore + totalRecoverable);
|
|
858
|
+
if (totalRecoverable > 0 && potentialScore > report.securityScore) {
|
|
859
|
+
process.stdout.write('\n');
|
|
860
|
+
process.stdout.write(` ${(0, colors_js_1.dim)('After fixes:')} ${report.securityScore} ${(0, colors_js_1.dim)('->')} ${(0, colors_js_1.green)(String(potentialScore))}\n`);
|
|
861
|
+
}
|
|
862
|
+
process.stdout.write('\n');
|
|
863
|
+
// --- Recommendations section ---
|
|
864
|
+
if (report.actions.length > 0) {
|
|
865
|
+
process.stdout.write((0, colors_js_1.bold)(' Recommendations') + '\n');
|
|
415
866
|
process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
|
|
416
|
-
for (
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
process.stdout.write(
|
|
422
|
-
|
|
867
|
+
for (let i = 0; i < report.actions.length; i++) {
|
|
868
|
+
const action = report.actions[i];
|
|
869
|
+
process.stdout.write(` ${(0, colors_js_1.bold)(`${i + 1}.`)} ${action.description}\n`);
|
|
870
|
+
// Prose explanation as a paragraph (word-wrapped)
|
|
871
|
+
const wrapped = (0, format_js_1.wordWrap)(action.why, 70, 5);
|
|
872
|
+
process.stdout.write((0, colors_js_1.dim)(wrapped) + '\n');
|
|
873
|
+
// Command at bottom with $ prefix
|
|
874
|
+
process.stdout.write(` ${(0, colors_js_1.cyan)('$ ' + action.command)}\n`);
|
|
875
|
+
process.stdout.write('\n');
|
|
423
876
|
}
|
|
424
877
|
process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
|
|
425
878
|
}
|
|
426
879
|
process.stdout.write('\n');
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
process.stdout.write((0, colors_js_1.dim)(
|
|
430
|
-
process.stdout.write((0, colors_js_1.dim)(' opena2a shield report Generate security posture report') + '\n');
|
|
431
|
-
process.stdout.write((0, colors_js_1.dim)(' opena2a shield monitor Start ARP runtime monitoring') + '\n');
|
|
432
|
-
process.stdout.write((0, colors_js_1.dim)(' opena2a ~<query> Search commands (e.g. opena2a ~drift)') + '\n');
|
|
880
|
+
// Contextual tip
|
|
881
|
+
const tip = getContextualTip(report);
|
|
882
|
+
process.stdout.write((0, colors_js_1.dim)(` Tip: ${tip.command} -- ${tip.text}`) + '\n');
|
|
433
883
|
process.stdout.write('\n');
|
|
434
884
|
}
|
|
435
885
|
//# sourceMappingURL=init.js.map
|