hackmyagent 0.16.7 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.integrity-manifest.json +1 -1
- package/dist/arp/index.d.ts +3 -3
- package/dist/arp/index.d.ts.map +1 -1
- package/dist/arp/index.js +9 -9
- package/dist/arp/index.js.map +1 -1
- package/dist/arp/intelligence/behavioral-risk-server.d.ts +2 -2
- package/dist/arp/intelligence/behavioral-risk-server.js +1 -1
- package/dist/arp/intelligence/behavioral-risk.d.ts +9 -9
- package/dist/arp/intelligence/behavioral-risk.js +6 -6
- package/dist/arp/intelligence/coordinator.js +2 -2
- package/dist/arp/intelligence/runtime-twin.d.ts +157 -0
- package/dist/arp/intelligence/runtime-twin.d.ts.map +1 -0
- package/dist/arp/intelligence/runtime-twin.js +479 -0
- package/dist/arp/intelligence/runtime-twin.js.map +1 -0
- package/dist/arp/types.d.ts +4 -3
- package/dist/arp/types.d.ts.map +1 -1
- package/dist/cli.js +633 -163
- package/dist/cli.js.map +1 -1
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +11 -1
- package/dist/hardening/scanner.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -112,15 +112,21 @@ const noColorEnv = process.env.NO_COLOR !== undefined || !process.stdout.isTTY;
|
|
|
112
112
|
// Color codes - will be cleared if --no-color is passed
|
|
113
113
|
let colors = {
|
|
114
114
|
green: '\x1b[32m',
|
|
115
|
+
brightGreen: '\x1b[92m',
|
|
115
116
|
yellow: '\x1b[33m',
|
|
116
117
|
red: '\x1b[31m',
|
|
117
118
|
brightRed: '\x1b[91m',
|
|
118
119
|
cyan: '\x1b[36m',
|
|
120
|
+
blue: '\x1b[34m',
|
|
121
|
+
magenta: '\x1b[35m',
|
|
119
122
|
dim: '\x1b[2m',
|
|
123
|
+
bold: '\x1b[1m',
|
|
124
|
+
white: '\x1b[97m',
|
|
125
|
+
underline: '\x1b[4m',
|
|
120
126
|
reset: '\x1b[0m',
|
|
121
127
|
};
|
|
122
128
|
if (noColorEnv) {
|
|
123
|
-
colors = { green: '', yellow: '', red: '', brightRed: '', cyan: '', dim: '', reset: '' };
|
|
129
|
+
colors = { green: '', brightGreen: '', yellow: '', red: '', brightRed: '', cyan: '', blue: '', magenta: '', dim: '', bold: '', white: '', underline: '', reset: '' };
|
|
124
130
|
}
|
|
125
131
|
// Deprecation warning for removed HMAC auth
|
|
126
132
|
if (process.env.HMA_COMMUNITY_SECRET) {
|
|
@@ -128,36 +134,29 @@ if (process.env.HMA_COMMUNITY_SECRET) {
|
|
|
128
134
|
}
|
|
129
135
|
program
|
|
130
136
|
.name('hackmyagent')
|
|
131
|
-
.description(`
|
|
137
|
+
.description(`Security scanner for AI agents. ${CHECK_COUNT} checks, ${index_1.PAYLOAD_STATS.total} attack payloads, auto-fix.
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
payloads, auto-fix with rollback, and OASB benchmark compliance.
|
|
135
|
-
|
|
136
|
-
Documentation: https://hackmyagent.com/docs
|
|
137
|
-
|
|
138
|
-
Updates (v${index_1.VERSION}):
|
|
139
|
-
- 10 new static analysis patterns (NEMO series)
|
|
140
|
-
- Community trust contributions
|
|
141
|
-
- ${CHECK_COUNT} checks across 60 categories
|
|
139
|
+
Scan before you install. Harden before you deploy. Red-team before you ship.
|
|
142
140
|
|
|
143
141
|
Examples:
|
|
144
|
-
$ hackmyagent
|
|
145
|
-
$ hackmyagent
|
|
146
|
-
$ hackmyagent secure --fix
|
|
147
|
-
$ hackmyagent
|
|
148
|
-
$ hackmyagent scan
|
|
149
|
-
|
|
142
|
+
$ hackmyagent check <package> Is this package safe to install?
|
|
143
|
+
$ hackmyagent secure Full project scan (${CHECK_COUNT} checks)
|
|
144
|
+
$ hackmyagent secure --fix Auto-fix with rollback
|
|
145
|
+
$ hackmyagent attack --local Red-team with ${index_1.PAYLOAD_STATS.total} payloads
|
|
146
|
+
$ hackmyagent scan-soul Governance compliance scan
|
|
147
|
+
$ hackmyagent scan example.com External infrastructure scan`)
|
|
148
|
+
.version(`hackmyagent ${index_1.VERSION} — security scanner for AI agents`, '-v, --version', 'Output the version number')
|
|
150
149
|
.option('--no-color', 'Disable colored output (also respects NO_COLOR env)');
|
|
151
150
|
program.addHelpText('beforeAll', `
|
|
152
151
|
Quick start:
|
|
152
|
+
$ hackmyagent check <package> Is this safe to install?
|
|
153
153
|
$ hackmyagent secure Scan current directory (${CHECK_COUNT} checks)
|
|
154
|
-
$ hackmyagent
|
|
155
|
-
$ hackmyagent attack Red-team your agent
|
|
154
|
+
$ hackmyagent secure --fix Auto-fix with rollback
|
|
156
155
|
`);
|
|
157
156
|
program.hook('preAction', (thisCommand) => {
|
|
158
157
|
const opts = thisCommand.opts();
|
|
159
158
|
if (opts.color === false) {
|
|
160
|
-
colors = { green: '', yellow: '', red: '', brightRed: '', cyan: '', dim: '', reset: '' };
|
|
159
|
+
colors = { green: '', brightGreen: '', yellow: '', red: '', brightRed: '', cyan: '', blue: '', magenta: '', dim: '', bold: '', white: '', underline: '', reset: '' };
|
|
161
160
|
}
|
|
162
161
|
});
|
|
163
162
|
// Risk level colors and symbols
|
|
@@ -172,37 +171,44 @@ program
|
|
|
172
171
|
.command('check')
|
|
173
172
|
.description(`Check if a package, repo, or skill is safe
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
• npm package: queries the registry; downloads + scans (${CHECK_COUNT} checks + NanoMind) if data is missing or stale (>${STALE_SCAN_DAYS}d)
|
|
177
|
-
• PyPI package: downloads + scans (${CHECK_COUNT} checks + NanoMind)
|
|
178
|
-
• GitHub repo: queries the registry; shallow clones + scans if data is missing or stale (>${STALE_SCAN_DAYS}d)
|
|
179
|
-
• Local path: runs NanoMind semantic analysis
|
|
180
|
-
• Skill identifier: verifies publisher, permissions, revocation
|
|
174
|
+
Downloads + scans (${CHECK_COUNT} checks + NanoMind) by default, with trust context from the OpenA2A registry.
|
|
181
175
|
|
|
182
|
-
|
|
176
|
+
Accepts:
|
|
177
|
+
• npm package: hackmyagent check express
|
|
178
|
+
• PyPI package: hackmyagent check pip:requests
|
|
179
|
+
• GitHub repo: hackmyagent check getsentry/sentry-mcp
|
|
180
|
+
• Local path: hackmyagent check ./my-agent/
|
|
181
|
+
• Skill: hackmyagent check @publisher/skill
|
|
182
|
+
• URL: hackmyagent check https://example.com/agent-v1.tar.gz
|
|
183
|
+
|
|
184
|
+
Output includes: verdict, security score, findings with fix commands, registry trust context, and path forward for recovery.
|
|
183
185
|
|
|
184
186
|
Risk levels: low, medium, high, critical
|
|
185
187
|
Exit code 1 if high/critical risk detected.
|
|
186
188
|
|
|
187
189
|
Examples:
|
|
188
|
-
$ hackmyagent check
|
|
189
|
-
$ hackmyagent check
|
|
190
|
-
$ hackmyagent check
|
|
191
|
-
$ hackmyagent check
|
|
192
|
-
$ hackmyagent check
|
|
193
|
-
$ hackmyagent check
|
|
194
|
-
$ hackmyagent check @publisher/skill --verbose
|
|
195
|
-
$ hackmyagent check pip:requests
|
|
196
|
-
$ hackmyagent check pypi:flask
|
|
197
|
-
$ hackmyagent check modelcontextprotocol/servers --json
|
|
198
|
-
$ hackmyagent check https://gitlab.com/org/repo
|
|
199
|
-
$ hackmyagent check https://example.com/agent-v1.tar.gz`)
|
|
190
|
+
$ hackmyagent check @sentry/mcp-server
|
|
191
|
+
$ hackmyagent check pip:flask
|
|
192
|
+
$ hackmyagent check getsentry/sentry-mcp --verbose
|
|
193
|
+
$ hackmyagent check ./my-agent/ --json
|
|
194
|
+
$ hackmyagent check express --no-scan # registry only (fast)
|
|
195
|
+
$ hackmyagent check express --no-registry # offline mode`)
|
|
200
196
|
.argument('<target>', 'npm package, PyPI package (pip: or pypi: prefix), local path, GitHub repo, or skill identifier')
|
|
201
|
-
.option('-v, --verbose', 'Show detailed verification info')
|
|
197
|
+
.option('-v, --verbose', 'Show detailed verification info (check IDs, categories)')
|
|
202
198
|
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
203
|
-
.option('--
|
|
204
|
-
.option('--
|
|
199
|
+
.option('--no-scan', 'Registry only, skip local scan (fast mode for CI)')
|
|
200
|
+
.option('--no-registry', 'Local scan only, skip registry lookup (offline mode)')
|
|
201
|
+
.option('--offline', 'Alias for --no-registry')
|
|
202
|
+
.option('--rescan', 'Deprecated: local scan is now the default')
|
|
205
203
|
.action(async (skill, options) => {
|
|
204
|
+
// Commander parses --no-scan as scan:false, --no-registry as registry:false
|
|
205
|
+
// Normalize: --offline is alias for --no-registry
|
|
206
|
+
if (options.offline)
|
|
207
|
+
options.registry = false;
|
|
208
|
+
// --rescan deprecation
|
|
209
|
+
if (options.rescan && !options.json && !globalCiMode) {
|
|
210
|
+
console.error(`${colors.yellow}Note: --rescan is deprecated. Local scan is now the default.${RESET()}`);
|
|
211
|
+
}
|
|
206
212
|
try {
|
|
207
213
|
// Detect local file/directory paths - run NanoMind scan instead of registry lookup
|
|
208
214
|
const { existsSync, statSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
@@ -212,7 +218,6 @@ Examples:
|
|
|
212
218
|
if (isLocalPath) {
|
|
213
219
|
// Local path: run NanoMind semantic analysis directly
|
|
214
220
|
const targetDir = statSync(resolved).isFile() ? dirname(resolved) : resolved;
|
|
215
|
-
const targetFile = statSync(resolved).isFile() ? resolved : null;
|
|
216
221
|
const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
|
|
217
222
|
const nmResult = await orchestrateNanoMind(targetDir, [], { silent: !!options.json });
|
|
218
223
|
const issues = nmResult.mergedFindings.filter((f) => !f.passed);
|
|
@@ -232,27 +237,16 @@ Examples:
|
|
|
232
237
|
});
|
|
233
238
|
return;
|
|
234
239
|
}
|
|
240
|
+
displayUnifiedCheck({
|
|
241
|
+
name: resolved,
|
|
242
|
+
sourceLabel: 'local',
|
|
243
|
+
nanomindScan: {
|
|
244
|
+
compiledArtifacts: nmResult.compiledArtifacts,
|
|
245
|
+
findings: issues,
|
|
246
|
+
},
|
|
247
|
+
verbose: !!options.verbose,
|
|
248
|
+
});
|
|
235
249
|
const risk = critical.length > 0 ? 'critical' : high.length > 0 ? 'high' : issues.length > 0 ? 'medium' : 'low';
|
|
236
|
-
const riskDisplay = RISK_DISPLAY[risk];
|
|
237
|
-
console.log(`\n${riskDisplay.color()}${riskDisplay.symbol} ${risk.toUpperCase()} RISK${RESET()}\n`);
|
|
238
|
-
console.log(`Path: ${resolved}`);
|
|
239
|
-
console.log(`Semantic analysis: ${nmResult.compiledArtifacts} file(s) analyzed\n`);
|
|
240
|
-
if (issues.length === 0) {
|
|
241
|
-
console.log(`${colors.green}No security issues detected.${RESET()}\n`);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
console.log(`Findings: ${critical.length} critical, ${high.length} high, ${issues.length - critical.length - high.length} other\n`);
|
|
245
|
-
for (const f of issues.slice(0, 10)) {
|
|
246
|
-
const sev = SEVERITY_DISPLAY[f.severity];
|
|
247
|
-
console.log(`${sev.color()}${sev.symbol} [${f.checkId}] ${f.description}${RESET()}`);
|
|
248
|
-
if (f.fix)
|
|
249
|
-
console.log(` Fix: ${f.fix}`);
|
|
250
|
-
}
|
|
251
|
-
if (issues.length > 10)
|
|
252
|
-
console.log(`\n ... and ${issues.length - 10} more`);
|
|
253
|
-
console.log();
|
|
254
|
-
}
|
|
255
|
-
printCheckNextSteps(resolved);
|
|
256
250
|
if (risk === 'critical' || risk === 'high')
|
|
257
251
|
process.exit(1);
|
|
258
252
|
return;
|
|
@@ -366,10 +360,10 @@ Examples:
|
|
|
366
360
|
});
|
|
367
361
|
// Severity colors and symbols for secure command
|
|
368
362
|
const SEVERITY_DISPLAY = {
|
|
369
|
-
critical: { symbol: '[!!]', color: () => colors.brightRed },
|
|
370
|
-
high: { symbol: '[!]', color: () => colors.red },
|
|
371
|
-
medium: { symbol: '[~]', color: () => colors.yellow },
|
|
372
|
-
low: { symbol: '[.]', color: () => colors.green },
|
|
363
|
+
critical: { symbol: '[!!]', label: 'CRITICAL', color: () => colors.brightRed },
|
|
364
|
+
high: { symbol: '[!]', label: 'HIGH', color: () => colors.red },
|
|
365
|
+
medium: { symbol: '[~]', label: 'MEDIUM', color: () => colors.yellow },
|
|
366
|
+
low: { symbol: '[.]', label: 'LOW', color: () => colors.green },
|
|
373
367
|
};
|
|
374
368
|
/**
|
|
375
369
|
* Display check command findings with optional verbose details.
|
|
@@ -408,6 +402,390 @@ function displayCheckFindings(failed, verbose) {
|
|
|
408
402
|
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
409
403
|
}
|
|
410
404
|
}
|
|
405
|
+
function stripAnsi(s) {
|
|
406
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
407
|
+
}
|
|
408
|
+
/** Right-align a value at a fixed column width */
|
|
409
|
+
function rightAlign(left, right, width = 68) {
|
|
410
|
+
const leftLen = stripAnsi(left).length;
|
|
411
|
+
const rightLen = stripAnsi(right).length;
|
|
412
|
+
const pad = Math.max(1, width - leftLen - rightLen);
|
|
413
|
+
return `${left}${' '.repeat(pad)}${right}`;
|
|
414
|
+
}
|
|
415
|
+
/** Extract the actionable core of a fix/guidance string.
|
|
416
|
+
* Takes the first sentence, strips file path prefixes that duplicate
|
|
417
|
+
* the finding header, and wraps at terminal width. Never truncates with "...".
|
|
418
|
+
*/
|
|
419
|
+
function cleanFixText(text, fileAlreadyShown) {
|
|
420
|
+
// Take first meaningful line (skip blank lines)
|
|
421
|
+
let line = text.split('\n').map(l => l.trim()).filter(Boolean)[0] || text;
|
|
422
|
+
// Strip "In <file>," prefix when file is already shown in the finding header
|
|
423
|
+
if (fileAlreadyShown) {
|
|
424
|
+
const escapedFile = fileAlreadyShown.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
425
|
+
line = line.replace(new RegExp(`^In ${escapedFile},?\\s*`, 'i'), '');
|
|
426
|
+
// Capitalize first letter after stripping
|
|
427
|
+
if (line.length > 0)
|
|
428
|
+
line = line[0].toUpperCase() + line.slice(1);
|
|
429
|
+
}
|
|
430
|
+
return line;
|
|
431
|
+
}
|
|
432
|
+
/** Shorten a file path for display — show filename + parent dir only */
|
|
433
|
+
function shortenPath(filePath) {
|
|
434
|
+
const parts = filePath.split('/');
|
|
435
|
+
if (parts.length <= 2)
|
|
436
|
+
return filePath;
|
|
437
|
+
return parts.slice(-2).join('/');
|
|
438
|
+
}
|
|
439
|
+
function displayUnifiedCheck(opts) {
|
|
440
|
+
const { name, sourceLabel, projectType, localScan, registry, verbose, version, nanomindScan } = opts;
|
|
441
|
+
// ── Visual helpers ──────────────────────────────────────────────────
|
|
442
|
+
const METER_WIDTH = 20;
|
|
443
|
+
const divider = (label) => {
|
|
444
|
+
if (label) {
|
|
445
|
+
console.log(`\n ${colors.dim}──${RESET()} ${colors.bold}${label}${RESET()} ${colors.dim}${'─'.repeat(Math.max(1, 56 - label.length))}${RESET()}`);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
console.log(` ${colors.dim}${'─'.repeat(62)}${RESET()}`);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const scoreMeter = (value, max = 100) => {
|
|
452
|
+
const pct = Math.round((value / max) * METER_WIDTH);
|
|
453
|
+
const meterColor = value >= 70 ? colors.green : value >= 40 ? colors.yellow : colors.red;
|
|
454
|
+
const filled = '━'.repeat(pct);
|
|
455
|
+
const empty = '━'.repeat(METER_WIDTH - pct);
|
|
456
|
+
return `${meterColor}${filled}${RESET()}${colors.dim}${empty}${RESET()} ${meterColor}${colors.bold}${value}${RESET()}${colors.dim}/${max}${RESET()}`;
|
|
457
|
+
};
|
|
458
|
+
const sevBadge = (sev) => {
|
|
459
|
+
const d = SEVERITY_DISPLAY[sev];
|
|
460
|
+
return `${d.color()}${colors.bold}${d.label}${RESET()}`;
|
|
461
|
+
};
|
|
462
|
+
// ── Compute findings ────────────────────────────────────────────────
|
|
463
|
+
let failed = [];
|
|
464
|
+
let score = 0;
|
|
465
|
+
let maxScore = 100;
|
|
466
|
+
let critical = 0, high = 0, medium = 0, low = 0;
|
|
467
|
+
if (localScan) {
|
|
468
|
+
failed = localScan.findings.filter(f => !f.passed);
|
|
469
|
+
score = localScan.score;
|
|
470
|
+
maxScore = localScan.maxScore;
|
|
471
|
+
critical = failed.filter(f => f.severity === 'critical').length;
|
|
472
|
+
high = failed.filter(f => f.severity === 'high').length;
|
|
473
|
+
medium = failed.filter(f => f.severity === 'medium').length;
|
|
474
|
+
low = failed.filter(f => f.severity === 'low').length;
|
|
475
|
+
}
|
|
476
|
+
else if (nanomindScan) {
|
|
477
|
+
const issues = nanomindScan.findings.filter(f => !f.passed);
|
|
478
|
+
critical = issues.filter(f => f.severity === 'critical').length;
|
|
479
|
+
high = issues.filter(f => f.severity === 'high').length;
|
|
480
|
+
medium = issues.filter(f => f.severity === 'medium').length;
|
|
481
|
+
low = issues.filter(f => f.severity === 'low').length;
|
|
482
|
+
failed = issues.map(f => ({
|
|
483
|
+
checkId: f.checkId || '',
|
|
484
|
+
name: f.name || f.description || '',
|
|
485
|
+
description: f.description || '',
|
|
486
|
+
category: f.category || '',
|
|
487
|
+
severity: f.severity,
|
|
488
|
+
passed: false,
|
|
489
|
+
message: f.message || f.description || '',
|
|
490
|
+
fixable: false,
|
|
491
|
+
file: f.file,
|
|
492
|
+
line: f.line,
|
|
493
|
+
fix: f.fix,
|
|
494
|
+
guidance: f.guidance,
|
|
495
|
+
attackClass: f.attackClass,
|
|
496
|
+
}));
|
|
497
|
+
// Score governance-only scans more fairly — governance gaps aren't code vulns
|
|
498
|
+
const hasCodeFindings = issues.some(f => {
|
|
499
|
+
const cat = (f.category || '').toLowerCase();
|
|
500
|
+
const id = f.checkId || '';
|
|
501
|
+
return cat !== 'governance' && cat !== 'injection-hardening' && cat !== 'trust-hierarchy'
|
|
502
|
+
&& !id.startsWith('AST-GOV') && !id.startsWith('AST-GOVERN')
|
|
503
|
+
&& !id.startsWith('AST-PROMPT') && !id.startsWith('AST-HEARTBEAT');
|
|
504
|
+
});
|
|
505
|
+
if (hasCodeFindings) {
|
|
506
|
+
score = critical > 0 ? Math.max(10, 100 - critical * 20 - high * 10 - medium * 5) : high > 0 ? Math.max(30, 100 - high * 10 - medium * 5) : Math.max(50, 100 - medium * 5 - low * 2);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// Governance-only: floor at 25, each finding costs less
|
|
510
|
+
score = Math.max(25, 100 - critical * 8 - high * 5 - medium * 3 - low * 1);
|
|
511
|
+
}
|
|
512
|
+
maxScore = 100;
|
|
513
|
+
}
|
|
514
|
+
else if (registry?.found) {
|
|
515
|
+
score = Math.round(registry.trustScore * 100);
|
|
516
|
+
maxScore = 100;
|
|
517
|
+
}
|
|
518
|
+
const totalFindings = critical + high + medium + low;
|
|
519
|
+
// ── Header ──────────────────────────────────────────────────────────
|
|
520
|
+
const typeLabel = (registry?.packageType || projectType || 'unknown').replace(/_/g, ' ');
|
|
521
|
+
const meta = [typeLabel];
|
|
522
|
+
if (version)
|
|
523
|
+
meta.unshift(`v${version}`);
|
|
524
|
+
if (sourceLabel)
|
|
525
|
+
meta.push(sourceLabel);
|
|
526
|
+
if (nanomindScan)
|
|
527
|
+
meta.push(`${nanomindScan.compiledArtifacts} files analyzed`);
|
|
528
|
+
if (localScan?.filesScanned)
|
|
529
|
+
meta.push(`${localScan.filesScanned} files scanned`);
|
|
530
|
+
console.log();
|
|
531
|
+
console.log(` ${colors.bold}${colors.white}${name}${RESET()} ${colors.dim}${meta.join(' · ')}${RESET()}`);
|
|
532
|
+
// ── Verdict + Score ─────────────────────────────────────────────────
|
|
533
|
+
if (localScan || nanomindScan) {
|
|
534
|
+
let verdictText;
|
|
535
|
+
let verdictColor;
|
|
536
|
+
if (critical > 0) {
|
|
537
|
+
verdictColor = colors.brightRed;
|
|
538
|
+
verdictText = `${critical} critical issue${critical > 1 ? 's' : ''} found`;
|
|
539
|
+
}
|
|
540
|
+
else if (high > 0) {
|
|
541
|
+
verdictColor = colors.red;
|
|
542
|
+
verdictText = `${high} high-severity issue${high > 1 ? 's' : ''} found`;
|
|
543
|
+
}
|
|
544
|
+
else if (totalFindings > 0) {
|
|
545
|
+
verdictColor = colors.yellow;
|
|
546
|
+
verdictText = `${totalFindings} issue${totalFindings > 1 ? 's' : ''} found`;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
verdictColor = colors.green;
|
|
550
|
+
verdictText = 'No security issues found';
|
|
551
|
+
}
|
|
552
|
+
console.log(` ${verdictColor}${colors.bold}${verdictText}${RESET()}`);
|
|
553
|
+
console.log();
|
|
554
|
+
console.log(` Security ${scoreMeter(score, maxScore)}`);
|
|
555
|
+
}
|
|
556
|
+
else if (registry?.found) {
|
|
557
|
+
const normalized = normalizeTrustVerdict(registry.verdict);
|
|
558
|
+
let verdictText;
|
|
559
|
+
let verdictColor;
|
|
560
|
+
if (normalized === 'blocked') {
|
|
561
|
+
verdictColor = colors.brightRed;
|
|
562
|
+
verdictText = 'Blocked by registry';
|
|
563
|
+
}
|
|
564
|
+
else if (normalized === 'warning') {
|
|
565
|
+
verdictColor = colors.yellow;
|
|
566
|
+
verdictText = 'Warning — review before installing';
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
verdictColor = colors.green;
|
|
570
|
+
verdictText = 'No known issues';
|
|
571
|
+
}
|
|
572
|
+
console.log(` ${verdictColor}${colors.bold}${verdictText}${RESET()}`);
|
|
573
|
+
console.log();
|
|
574
|
+
console.log(` Trust ${scoreMeter(score, maxScore)}`);
|
|
575
|
+
}
|
|
576
|
+
// ── Findings ────────────────────────────────────────────────────────
|
|
577
|
+
if (failed.length > 0) {
|
|
578
|
+
// Severity summary as colored pills
|
|
579
|
+
const summaryParts = [];
|
|
580
|
+
if (critical > 0)
|
|
581
|
+
summaryParts.push(`${colors.brightRed}${colors.bold}${critical} critical${RESET()}`);
|
|
582
|
+
if (high > 0)
|
|
583
|
+
summaryParts.push(`${colors.red}${colors.bold}${high} high${RESET()}`);
|
|
584
|
+
if (medium > 0)
|
|
585
|
+
summaryParts.push(`${colors.yellow}${medium} medium${RESET()}`);
|
|
586
|
+
if (low > 0)
|
|
587
|
+
summaryParts.push(`${colors.dim}${low} low${RESET()}`);
|
|
588
|
+
divider('Findings');
|
|
589
|
+
console.log(` ${summaryParts.join(' ')}`);
|
|
590
|
+
// High-count mode: group by category when > 20 findings
|
|
591
|
+
if (totalFindings > 20 && !verbose) {
|
|
592
|
+
const groups = new Map();
|
|
593
|
+
for (const f of failed) {
|
|
594
|
+
const key = f.category || f.name || 'Other';
|
|
595
|
+
if (!groups.has(key))
|
|
596
|
+
groups.set(key, { critical: 0, high: 0, medium: 0, low: 0, files: new Set() });
|
|
597
|
+
const g = groups.get(key);
|
|
598
|
+
g[f.severity]++;
|
|
599
|
+
if (f.file)
|
|
600
|
+
g.files.add(f.file.split('/')[0] || f.file);
|
|
601
|
+
}
|
|
602
|
+
const sorted = [...groups.entries()].sort((a, b) => {
|
|
603
|
+
const wa = a[1].critical * 4 + a[1].high * 3 + a[1].medium * 2 + a[1].low;
|
|
604
|
+
const wb = b[1].critical * 4 + b[1].high * 3 + b[1].medium * 2 + b[1].low;
|
|
605
|
+
return wb - wa;
|
|
606
|
+
});
|
|
607
|
+
console.log();
|
|
608
|
+
for (const [cat, g] of sorted.slice(0, 8)) {
|
|
609
|
+
const counts = [];
|
|
610
|
+
if (g.critical > 0)
|
|
611
|
+
counts.push(`${colors.brightRed}${g.critical} crit${RESET()}`);
|
|
612
|
+
if (g.high > 0)
|
|
613
|
+
counts.push(`${colors.red}${g.high} high${RESET()}`);
|
|
614
|
+
if (g.medium > 0)
|
|
615
|
+
counts.push(`${colors.dim}${g.medium} med${RESET()}`);
|
|
616
|
+
if (g.low > 0)
|
|
617
|
+
counts.push(`${colors.dim}${g.low} low${RESET()}`);
|
|
618
|
+
const fileHint = g.files.size <= 3 ? ` ${colors.dim}${[...g.files].join(', ')}${RESET()}` : '';
|
|
619
|
+
console.log(` ${colors.dim}│${RESET()} ${cat.padEnd(26)} ${counts.join(', ')}${fileHint}`);
|
|
620
|
+
}
|
|
621
|
+
if (sorted.length > 8) {
|
|
622
|
+
console.log(` ${colors.dim}│ + ${sorted.length - 8} more categories${RESET()}`);
|
|
623
|
+
}
|
|
624
|
+
// Top 3 issues with full detail
|
|
625
|
+
divider('Top Issues');
|
|
626
|
+
const topFindings = [...failed]
|
|
627
|
+
.sort((a, b) => {
|
|
628
|
+
const sw = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
629
|
+
return (sw[b.severity] || 0) - (sw[a.severity] || 0);
|
|
630
|
+
})
|
|
631
|
+
.slice(0, 3);
|
|
632
|
+
for (const f of topFindings) {
|
|
633
|
+
const shortFile = f.file ? shortenPath(f.file) : '';
|
|
634
|
+
const loc = shortFile + (f.line ? `:${f.line}` : '');
|
|
635
|
+
const borderColor = SEVERITY_DISPLAY[f.severity].color();
|
|
636
|
+
console.log();
|
|
637
|
+
console.log(` ${borderColor}│${RESET()} ${sevBadge(f.severity)} ${colors.bold}${colors.white}${f.name || f.message}${RESET()}`);
|
|
638
|
+
if (loc)
|
|
639
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}${loc}${RESET()}`);
|
|
640
|
+
if (f.guidance) {
|
|
641
|
+
console.log(` ${borderColor}│${RESET()} ${cleanFixText(f.guidance, f.file)}`);
|
|
642
|
+
}
|
|
643
|
+
if (f.fix) {
|
|
644
|
+
console.log(` ${borderColor}│${RESET()} ${colors.cyan}Fix:${RESET()} ${cleanFixText(f.fix, f.file)}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
// Normal mode: individual findings sorted by severity, with collapse
|
|
650
|
+
const sevWeight = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
651
|
+
failed.sort((a, b) => (sevWeight[b.severity] || 0) - (sevWeight[a.severity] || 0));
|
|
652
|
+
const skipped = new Set();
|
|
653
|
+
let shown = 0;
|
|
654
|
+
const limit = verbose ? failed.length : 10;
|
|
655
|
+
for (let i = 0; i < failed.length; i++) {
|
|
656
|
+
if (shown >= limit)
|
|
657
|
+
break;
|
|
658
|
+
if (skipped.has(i))
|
|
659
|
+
continue;
|
|
660
|
+
const f = failed[i];
|
|
661
|
+
const shortFile = f.file ? shortenPath(f.file) : '';
|
|
662
|
+
const loc = shortFile + (f.line ? `:${f.line}` : '');
|
|
663
|
+
const borderColor = SEVERITY_DISPLAY[f.severity].color();
|
|
664
|
+
console.log();
|
|
665
|
+
console.log(` ${borderColor}│${RESET()} ${sevBadge(f.severity)} ${colors.bold}${colors.white}${f.name || f.message}${RESET()}`);
|
|
666
|
+
if (loc)
|
|
667
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}${loc}${RESET()}`);
|
|
668
|
+
if (f.guidance) {
|
|
669
|
+
console.log(` ${borderColor}│${RESET()} ${cleanFixText(f.guidance, f.file)}`);
|
|
670
|
+
}
|
|
671
|
+
if (f.fix) {
|
|
672
|
+
console.log(` ${borderColor}│${RESET()} ${colors.cyan}Fix:${RESET()} ${cleanFixText(f.fix, f.file)}`);
|
|
673
|
+
}
|
|
674
|
+
if (verbose) {
|
|
675
|
+
if (f.checkId)
|
|
676
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}Check: ${f.checkId}${RESET()}`);
|
|
677
|
+
if (f.category)
|
|
678
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}Category: ${f.category}${RESET()}`);
|
|
679
|
+
}
|
|
680
|
+
shown++;
|
|
681
|
+
// Collapse similar
|
|
682
|
+
if (!verbose) {
|
|
683
|
+
const dir = f.file?.split('/').slice(0, -1).join('/') || '';
|
|
684
|
+
let similarCount = 0;
|
|
685
|
+
for (let j = i + 1; j < failed.length; j++) {
|
|
686
|
+
if (skipped.has(j))
|
|
687
|
+
continue;
|
|
688
|
+
const other = failed[j];
|
|
689
|
+
if (other.name === f.name) {
|
|
690
|
+
const otherDir = other.file?.split('/').slice(0, -1).join('/') || '';
|
|
691
|
+
if (otherDir === dir) {
|
|
692
|
+
skipped.add(j);
|
|
693
|
+
similarCount++;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (similarCount > 0) {
|
|
698
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}+ ${similarCount} similar${dir ? ` in ${shortenPath(dir)}` : ''}${RESET()}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const remaining = failed.length - shown - skipped.size;
|
|
703
|
+
if (remaining > 0) {
|
|
704
|
+
console.log(`\n ${colors.dim}+ ${remaining} more findings (use --verbose to see all)${RESET()}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Path forward with recovery math
|
|
708
|
+
if (critical > 0 || high > 0) {
|
|
709
|
+
const recoveryParts = [];
|
|
710
|
+
if (critical > 0)
|
|
711
|
+
recoveryParts.push(`${critical} critical`);
|
|
712
|
+
if (high > 0)
|
|
713
|
+
recoveryParts.push(`${high} high`);
|
|
714
|
+
// Estimate recovered score: governance findings recover less per fix
|
|
715
|
+
const govFindings = failed.filter(f => {
|
|
716
|
+
const cat = (f.category || '').toLowerCase();
|
|
717
|
+
const id = f.checkId || '';
|
|
718
|
+
return cat === 'governance' || cat === 'injection-hardening' || cat === 'trust-hierarchy'
|
|
719
|
+
|| id.startsWith('AST-GOV') || id.startsWith('AST-GOVERN')
|
|
720
|
+
|| id.startsWith('AST-PROMPT') || id.startsWith('AST-HEARTBEAT');
|
|
721
|
+
});
|
|
722
|
+
const isGovernanceOnly = govFindings.length === failed.length;
|
|
723
|
+
const estRecovery = isGovernanceOnly
|
|
724
|
+
? Math.min(100, score + (critical * 8 + high * 5))
|
|
725
|
+
: Math.min(100, score + (critical * 15 + high * 8));
|
|
726
|
+
console.log();
|
|
727
|
+
console.log(` ${colors.cyan}${colors.bold}Path forward:${RESET()} ${colors.cyan}${score} ${colors.dim}->${RESET()} ${colors.green}${colors.bold}${estRecovery}${RESET()} ${colors.cyan}by fixing ${recoveryParts.join(' + ')}${RESET()}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// ── Registry ────────────────────────────────────────────────────────
|
|
731
|
+
if (registry?.found) {
|
|
732
|
+
const trustScore = Math.round(registry.trustScore * 100);
|
|
733
|
+
if (localScan || nanomindScan) {
|
|
734
|
+
divider('Registry');
|
|
735
|
+
console.log(` Trust ${scoreMeter(trustScore)}`);
|
|
736
|
+
}
|
|
737
|
+
const tlColor = trustLevelColor(registry.trustLevel);
|
|
738
|
+
const tlLabel = trustLevelLabel(registry.trustLevel);
|
|
739
|
+
console.log(` Level ${tlColor}${colors.bold}${tlLabel}${RESET()} ${colors.dim}(${registry.trustLevel}/4)${RESET()}`);
|
|
740
|
+
if (registry.communityScans !== undefined) {
|
|
741
|
+
console.log(` Community ${registry.communityScans > 0 ? colors.green : colors.dim}${registry.communityScans} scan${registry.communityScans !== 1 ? 's' : ''} shared${RESET()}`);
|
|
742
|
+
}
|
|
743
|
+
if (registry.cveCount !== undefined && registry.cveCount > 0) {
|
|
744
|
+
console.log(` CVEs ${colors.brightRed}${colors.bold}${registry.cveCount}${RESET()}`);
|
|
745
|
+
}
|
|
746
|
+
if (registry.dependencies) {
|
|
747
|
+
const d = registry.dependencies;
|
|
748
|
+
const depParts = [];
|
|
749
|
+
if (d.totalDeps !== undefined)
|
|
750
|
+
depParts.push(`${d.totalDeps} total`);
|
|
751
|
+
if (d.vulnerableDeps !== undefined && d.vulnerableDeps > 0)
|
|
752
|
+
depParts.push(`${colors.red}${d.vulnerableDeps} vulnerable${RESET()}`);
|
|
753
|
+
if (d.minTrustLevel !== undefined)
|
|
754
|
+
depParts.push(`min trust ${d.minTrustLevel}/4`);
|
|
755
|
+
if (depParts.length > 0) {
|
|
756
|
+
console.log(` Deps ${depParts.join(`${colors.dim} · ${RESET()}`)}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// Trust level legend (when not fully verified)
|
|
760
|
+
if (registry.trustLevel < 4) {
|
|
761
|
+
const levels = ['Blocked', 'Warning', 'Listed', 'Scanned', 'Verified'];
|
|
762
|
+
const legend = levels.map((l, i) => {
|
|
763
|
+
if (i === registry.trustLevel)
|
|
764
|
+
return `${tlColor}${colors.bold}${l}${RESET()}`;
|
|
765
|
+
if (i < registry.trustLevel)
|
|
766
|
+
return `${colors.dim}${l}${RESET()}`;
|
|
767
|
+
return `${colors.dim}${l}${RESET()}`;
|
|
768
|
+
}).join(`${colors.dim} > ${RESET()}`);
|
|
769
|
+
console.log(` ${colors.dim}${legend}${RESET()}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// ── Next steps ──────────────────────────────────────────────────────
|
|
773
|
+
const hasGovIssues = failed.some(f => f.category === 'governance' || f.category === 'Governance' || f.checkId?.startsWith('AST-GOV') || f.checkId?.startsWith('AST-PROMPT'));
|
|
774
|
+
const hasCredIssues = failed.some(f => f.checkId?.startsWith('CRED-') || f.name?.toLowerCase().includes('credential') || f.name?.toLowerCase().includes('api key') || f.name?.toLowerCase().includes('hardcoded'));
|
|
775
|
+
const hasCodeVulns = failed.some(f => {
|
|
776
|
+
const cat = (f.category || '').toLowerCase();
|
|
777
|
+
return cat !== 'governance' && cat !== 'injection-hardening' && cat !== 'trust-hierarchy'
|
|
778
|
+
&& !f.checkId?.startsWith('AST-GOV') && !f.checkId?.startsWith('AST-GOVERN')
|
|
779
|
+
&& !f.checkId?.startsWith('AST-PROMPT') && !f.checkId?.startsWith('AST-HEARTBEAT');
|
|
780
|
+
});
|
|
781
|
+
printCheckNextSteps(name, {
|
|
782
|
+
hasGovernanceIssues: hasGovIssues,
|
|
783
|
+
hasFindings: totalFindings > 0,
|
|
784
|
+
hasCredentialFindings: hasCredIssues,
|
|
785
|
+
hasCodeVulns,
|
|
786
|
+
isCleanScan: totalFindings === 0 && (!!localScan || !!nanomindScan),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
411
789
|
function groupFindingsBySeverity(findings) {
|
|
412
790
|
const grouped = {
|
|
413
791
|
critical: [],
|
|
@@ -5912,6 +6290,7 @@ async function queryRegistry(name) {
|
|
|
5912
6290
|
const data = await response.json();
|
|
5913
6291
|
if (!data.packageId)
|
|
5914
6292
|
return null;
|
|
6293
|
+
const deps = data.dependencies;
|
|
5915
6294
|
return {
|
|
5916
6295
|
found: true,
|
|
5917
6296
|
name: data.name ?? name,
|
|
@@ -5921,6 +6300,15 @@ async function queryRegistry(name) {
|
|
|
5921
6300
|
scanStatus: data.scanStatus,
|
|
5922
6301
|
lastScannedAt: data.lastScannedAt,
|
|
5923
6302
|
packageType: data.packageType,
|
|
6303
|
+
recommendation: data.recommendation,
|
|
6304
|
+
cveCount: typeof data.cveCount === 'number' ? data.cveCount : undefined,
|
|
6305
|
+
communityScans: typeof data.communityScans === 'number' ? data.communityScans : undefined,
|
|
6306
|
+
dependencies: deps ? {
|
|
6307
|
+
totalDeps: typeof deps.totalDeps === 'number' ? deps.totalDeps : undefined,
|
|
6308
|
+
vulnerableDeps: typeof deps.vulnerableDeps === 'number' ? deps.vulnerableDeps : undefined,
|
|
6309
|
+
minTrustLevel: typeof deps.minTrustLevel === 'number' ? deps.minTrustLevel : undefined,
|
|
6310
|
+
riskSummary: deps.riskSummary,
|
|
6311
|
+
} : undefined,
|
|
5924
6312
|
};
|
|
5925
6313
|
}
|
|
5926
6314
|
catch {
|
|
@@ -5954,7 +6342,9 @@ async function publishToRegistry(name, result) {
|
|
|
5954
6342
|
score: result.score,
|
|
5955
6343
|
maxScore: result.maxScore,
|
|
5956
6344
|
projectType: result.projectType,
|
|
5957
|
-
findings: result.findings
|
|
6345
|
+
findings: result.findings
|
|
6346
|
+
.filter(f => !PACKAGE_SCAN_LOCAL_ONLY_CATEGORIES.has(f.category))
|
|
6347
|
+
.map(f => ({
|
|
5958
6348
|
checkId: f.checkId,
|
|
5959
6349
|
name: f.name,
|
|
5960
6350
|
severity: f.severity,
|
|
@@ -6020,6 +6410,88 @@ function getFullScanHint() {
|
|
|
6020
6410
|
return override;
|
|
6021
6411
|
return `${CLI_PREFIX} secure <dir>`;
|
|
6022
6412
|
}
|
|
6413
|
+
/**
|
|
6414
|
+
* Categories that describe local dev-environment setup, not package security.
|
|
6415
|
+
* Findings in these categories are filtered from display when scanning a
|
|
6416
|
+
* *downloaded* package (npm pack, pip download, git clone to temp dir).
|
|
6417
|
+
* They remain visible when scanning a user's own project directory.
|
|
6418
|
+
*/
|
|
6419
|
+
const PACKAGE_SCAN_LOCAL_ONLY_CATEGORIES = new Set([
|
|
6420
|
+
'git',
|
|
6421
|
+
'permissions',
|
|
6422
|
+
'environment',
|
|
6423
|
+
'logging',
|
|
6424
|
+
'claude-code',
|
|
6425
|
+
'cursor',
|
|
6426
|
+
'vscode',
|
|
6427
|
+
]);
|
|
6428
|
+
/**
|
|
6429
|
+
* Paths that are AI tooling artifacts, not package source code.
|
|
6430
|
+
* Governance findings on these files are noise when scanning a downloaded
|
|
6431
|
+
* package or cloned repo — they're instructions to an AI assistant, not
|
|
6432
|
+
* security vulnerabilities in the package itself.
|
|
6433
|
+
*/
|
|
6434
|
+
const AI_TOOLING_PATH_PATTERNS = [
|
|
6435
|
+
/^\.claude\//,
|
|
6436
|
+
/^CLAUDE\.md$/i,
|
|
6437
|
+
/^\.cursorrules$/i,
|
|
6438
|
+
/^\.aider/,
|
|
6439
|
+
/^\.copilot\//,
|
|
6440
|
+
/^\.github\/copilot/,
|
|
6441
|
+
];
|
|
6442
|
+
/** Governance-related categories/checkId prefixes that are noise on AI tooling files */
|
|
6443
|
+
const GOVERNANCE_CATEGORIES = new Set([
|
|
6444
|
+
'governance',
|
|
6445
|
+
'injection-hardening',
|
|
6446
|
+
'trust-hierarchy',
|
|
6447
|
+
]);
|
|
6448
|
+
const GOVERNANCE_CHECK_PREFIXES = ['AST-GOV', 'AST-GOVERN', 'AST-PROMPT'];
|
|
6449
|
+
/** Test file path patterns — findings here are lower risk */
|
|
6450
|
+
const TEST_FILE_PATTERNS = [
|
|
6451
|
+
/\btests?\//i,
|
|
6452
|
+
/\b__tests__\//,
|
|
6453
|
+
/\btest_[^/]+$/,
|
|
6454
|
+
/[^/]+_test\.\w+$/,
|
|
6455
|
+
/[^/]+\.test\.\w+$/,
|
|
6456
|
+
/[^/]+\.spec\.\w+$/,
|
|
6457
|
+
/\bfixtures?\//i,
|
|
6458
|
+
];
|
|
6459
|
+
function isTestFile(filePath) {
|
|
6460
|
+
return TEST_FILE_PATTERNS.some(p => p.test(filePath));
|
|
6461
|
+
}
|
|
6462
|
+
function isAiToolingFile(filePath) {
|
|
6463
|
+
return AI_TOOLING_PATH_PATTERNS.some(p => p.test(filePath));
|
|
6464
|
+
}
|
|
6465
|
+
/**
|
|
6466
|
+
* Filter out local-dev-only findings that are meaningless for downloaded
|
|
6467
|
+
* packages (e.g. "Missing .gitignore" on an npm tarball). Also filters
|
|
6468
|
+
* governance findings on AI tooling files and demotes test file findings.
|
|
6469
|
+
* Mutates `result.findings` in place and recalculates the score.
|
|
6470
|
+
*/
|
|
6471
|
+
function filterLocalOnlyFindings(result, scanner) {
|
|
6472
|
+
result.findings = result.findings.filter(f => {
|
|
6473
|
+
// Remove local-only categories (git, permissions, env, etc.)
|
|
6474
|
+
if (PACKAGE_SCAN_LOCAL_ONLY_CATEGORIES.has(f.category))
|
|
6475
|
+
return false;
|
|
6476
|
+
// Remove governance findings on AI tooling files (CLAUDE.md, .claude/, etc.)
|
|
6477
|
+
if (f.file && isAiToolingFile(f.file)) {
|
|
6478
|
+
if (GOVERNANCE_CATEGORIES.has(f.category))
|
|
6479
|
+
return false;
|
|
6480
|
+
if (GOVERNANCE_CHECK_PREFIXES.some(p => f.checkId.startsWith(p)))
|
|
6481
|
+
return false;
|
|
6482
|
+
}
|
|
6483
|
+
return true;
|
|
6484
|
+
});
|
|
6485
|
+
// Demote test file findings to low severity (test code patterns are
|
|
6486
|
+
// lower risk — pickle.load in a test file is not an attack surface)
|
|
6487
|
+
for (const f of result.findings) {
|
|
6488
|
+
if (f.file && isTestFile(f.file) && (f.severity === 'critical' || f.severity === 'high')) {
|
|
6489
|
+
f.originalSeverity = f.severity;
|
|
6490
|
+
f.severity = 'low';
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
|
|
6494
|
+
}
|
|
6023
6495
|
/**
|
|
6024
6496
|
* Print the standard 3-line next-steps footer shown after every `check`
|
|
6025
6497
|
* invocation. Lines:
|
|
@@ -6031,13 +6503,30 @@ function getFullScanHint() {
|
|
|
6031
6503
|
*
|
|
6032
6504
|
* Suppressed in --ci so machine-readable output stays clean.
|
|
6033
6505
|
*/
|
|
6034
|
-
function printCheckNextSteps(target) {
|
|
6506
|
+
function printCheckNextSteps(target, context) {
|
|
6035
6507
|
if (globalCiMode)
|
|
6036
6508
|
return;
|
|
6037
6509
|
console.log();
|
|
6038
|
-
console.log(` ${colors.dim}
|
|
6039
|
-
|
|
6040
|
-
|
|
6510
|
+
console.log(` ${colors.dim}──${RESET()} ${colors.bold}Next Steps${RESET()} ${colors.dim}${'─'.repeat(49)}${RESET()}`);
|
|
6511
|
+
if (context?.hasGovernanceIssues) {
|
|
6512
|
+
console.log(` ${colors.cyan}Auto-fix governance:${RESET()} ${CLI_PREFIX} harden-soul ${target}`);
|
|
6513
|
+
}
|
|
6514
|
+
if (context?.hasCredentialFindings) {
|
|
6515
|
+
console.log(` ${colors.cyan}Protect credentials:${RESET()} npx secretless-ai scan`);
|
|
6516
|
+
}
|
|
6517
|
+
if (context?.hasCodeVulns) {
|
|
6518
|
+
console.log(` ${colors.cyan}Auto-fix all issues:${RESET()} ${CLI_PREFIX} secure --fix`);
|
|
6519
|
+
}
|
|
6520
|
+
if (context?.hasFindings) {
|
|
6521
|
+
console.log(` ${colors.cyan}Full project audit:${RESET()} ${getFullScanHint()}`);
|
|
6522
|
+
}
|
|
6523
|
+
else if (context?.isCleanScan) {
|
|
6524
|
+
console.log(` ${colors.cyan}Governance scan:${RESET()} ${CLI_PREFIX} scan-soul ${target}`);
|
|
6525
|
+
console.log(` ${colors.cyan}Red-team test:${RESET()} ${CLI_PREFIX} attack --local`);
|
|
6526
|
+
}
|
|
6527
|
+
else {
|
|
6528
|
+
console.log(` ${colors.cyan}Deep scan:${RESET()} ${CLI_PREFIX} check ${target} --rescan`);
|
|
6529
|
+
}
|
|
6041
6530
|
console.log();
|
|
6042
6531
|
}
|
|
6043
6532
|
/**
|
|
@@ -6106,27 +6595,23 @@ async function suggestSimilarPackages(name) {
|
|
|
6106
6595
|
async function checkGitHubRepo(target, options) {
|
|
6107
6596
|
const { org, repo, cloneUrl } = parseGitHubTarget(target);
|
|
6108
6597
|
const displayName = `${org}/${repo}`;
|
|
6109
|
-
//
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6598
|
+
// Fetch registry data in parallel with clone (unless --no-registry)
|
|
6599
|
+
const registryPromise = options.registry === false ? Promise.resolve(null) : queryRegistry(displayName);
|
|
6600
|
+
// Registry-only mode (--no-scan): skip local scan
|
|
6601
|
+
if (options.scan === false) {
|
|
6602
|
+
const registryData = await registryPromise;
|
|
6603
|
+
if (registryData?.found) {
|
|
6113
6604
|
if (options.json) {
|
|
6114
6605
|
writeJsonStdout({ ...registryData, source: 'registry' });
|
|
6115
6606
|
return;
|
|
6116
6607
|
}
|
|
6117
|
-
|
|
6608
|
+
displayUnifiedCheck({ name: displayName, sourceLabel: 'GitHub', registry: registryData, verbose: !!options.verbose });
|
|
6118
6609
|
return;
|
|
6119
6610
|
}
|
|
6120
|
-
if (
|
|
6121
|
-
|
|
6122
|
-
const days = Math.floor((Date.now() - new Date(registryData.lastScannedAt).getTime()) / (1000 * 60 * 60 * 24));
|
|
6123
|
-
console.error(`\nRegistry data is ${days} day(s) old. Re-scanning...`);
|
|
6124
|
-
}
|
|
6611
|
+
if (!options.json && !globalCiMode) {
|
|
6612
|
+
console.error(`No registry data found for ${displayName}. Running local scan...`);
|
|
6125
6613
|
}
|
|
6126
6614
|
}
|
|
6127
|
-
else if (options.rescan && !options.json && !globalCiMode) {
|
|
6128
|
-
console.error(`Forcing fresh local scan (--rescan)...`);
|
|
6129
|
-
}
|
|
6130
6615
|
// Step 2: Clone and scan
|
|
6131
6616
|
const { mkdtemp, rm } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
6132
6617
|
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
@@ -6157,6 +6642,8 @@ async function checkGitHubRepo(target, options) {
|
|
|
6157
6642
|
catch {
|
|
6158
6643
|
// NanoMind unavailable — use base scan results
|
|
6159
6644
|
}
|
|
6645
|
+
// Filter local-dev-only findings irrelevant to cloned repos
|
|
6646
|
+
filterLocalOnlyFindings(result, scanner);
|
|
6160
6647
|
const failed = result.findings.filter(f => !f.passed);
|
|
6161
6648
|
const critical = failed.filter(f => f.severity === 'critical');
|
|
6162
6649
|
const high = failed.filter(f => f.severity === 'high');
|
|
@@ -6174,19 +6661,21 @@ async function checkGitHubRepo(target, options) {
|
|
|
6174
6661
|
});
|
|
6175
6662
|
return;
|
|
6176
6663
|
}
|
|
6177
|
-
//
|
|
6178
|
-
const
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6664
|
+
// Await registry data (started in parallel with clone)
|
|
6665
|
+
const registryData = await registryPromise;
|
|
6666
|
+
// Display results using unified display
|
|
6667
|
+
displayUnifiedCheck({
|
|
6668
|
+
name: displayName,
|
|
6669
|
+
sourceLabel: 'GitHub',
|
|
6670
|
+
projectType: result.projectType,
|
|
6671
|
+
localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
|
|
6672
|
+
registry: registryData,
|
|
6673
|
+
verbose: !!options.verbose,
|
|
6674
|
+
});
|
|
6675
|
+
// Community contribution
|
|
6186
6676
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6187
6677
|
const scanCount = incrementScanCounter();
|
|
6188
6678
|
if (scanCount >= 3 && !hasContributeChoice()) {
|
|
6189
|
-
console.log();
|
|
6190
6679
|
console.log(` ${colors.dim}Your scans help other developers make safer choices.`);
|
|
6191
6680
|
console.log(` Sharing adds anonymized results to the OpenA2A trust registry`);
|
|
6192
6681
|
console.log(` so others can check packages before installing.${RESET()}`);
|
|
@@ -6201,7 +6690,7 @@ async function checkGitHubRepo(target, options) {
|
|
|
6201
6690
|
if (wantsToShare) {
|
|
6202
6691
|
const ok = await publishToRegistry(displayName, result);
|
|
6203
6692
|
if (ok) {
|
|
6204
|
-
console.error(
|
|
6693
|
+
console.error(`\n ${colors.green}Thanks for sharing! Future scans will auto-contribute.${RESET()}\n`);
|
|
6205
6694
|
}
|
|
6206
6695
|
else {
|
|
6207
6696
|
queuePendingScan(displayName, result);
|
|
@@ -6215,7 +6704,6 @@ async function checkGitHubRepo(target, options) {
|
|
|
6215
6704
|
queuePendingScan(displayName, result);
|
|
6216
6705
|
}
|
|
6217
6706
|
}
|
|
6218
|
-
printCheckNextSteps(displayName);
|
|
6219
6707
|
if (critical.length > 0 || high.length > 0)
|
|
6220
6708
|
process.exit(1);
|
|
6221
6709
|
}
|
|
@@ -6317,6 +6805,8 @@ async function checkPyPiPackage(target, options) {
|
|
|
6317
6805
|
catch {
|
|
6318
6806
|
// NanoMind unavailable -- use base scan results
|
|
6319
6807
|
}
|
|
6808
|
+
// Filter local-dev-only findings irrelevant to downloaded packages
|
|
6809
|
+
filterLocalOnlyFindings(result, scanner);
|
|
6320
6810
|
const failed = result.findings.filter(f => !f.passed);
|
|
6321
6811
|
const critical = failed.filter(f => f.severity === 'critical');
|
|
6322
6812
|
const high = failed.filter(f => f.severity === 'high');
|
|
@@ -6335,32 +6825,18 @@ async function checkPyPiPackage(target, options) {
|
|
|
6335
6825
|
});
|
|
6336
6826
|
return;
|
|
6337
6827
|
}
|
|
6338
|
-
// Display results
|
|
6339
|
-
|
|
6340
|
-
const
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
const sev = SEVERITY_DISPLAY[f.severity];
|
|
6351
|
-
const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
|
|
6352
|
-
console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
|
|
6353
|
-
}
|
|
6354
|
-
if (failed.length > limit) {
|
|
6355
|
-
console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
|
|
6356
|
-
}
|
|
6357
|
-
}
|
|
6358
|
-
else {
|
|
6359
|
-
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
6360
|
-
}
|
|
6361
|
-
// Pass the original target (with pip: / pypi: prefix preserved) so the
|
|
6362
|
-
// rescan hint stays runnable — `hackmyagent check requests` would try npm.
|
|
6363
|
-
printCheckNextSteps(target);
|
|
6828
|
+
// Display results using unified display
|
|
6829
|
+
// Query registry for trust context (PyPI packages have pip: prefix in registry)
|
|
6830
|
+
const registryData = options.registry === false ? null : await queryRegistry(`pip:${name}`);
|
|
6831
|
+
displayUnifiedCheck({
|
|
6832
|
+
name,
|
|
6833
|
+
sourceLabel: 'PyPI',
|
|
6834
|
+
projectType: result.projectType,
|
|
6835
|
+
version: meta.info.version,
|
|
6836
|
+
localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
|
|
6837
|
+
registry: registryData,
|
|
6838
|
+
verbose: !!options.verbose,
|
|
6839
|
+
});
|
|
6364
6840
|
if (critical.length > 0 || high.length > 0)
|
|
6365
6841
|
process.exit(1);
|
|
6366
6842
|
}
|
|
@@ -6484,6 +6960,8 @@ async function checkRawUrl(url, options) {
|
|
|
6484
6960
|
catch {
|
|
6485
6961
|
// NanoMind unavailable — use base scan results
|
|
6486
6962
|
}
|
|
6963
|
+
// Filter local-dev-only findings irrelevant to downloaded URLs
|
|
6964
|
+
filterLocalOnlyFindings(result, scanner);
|
|
6487
6965
|
const failed = result.findings.filter(f => !f.passed);
|
|
6488
6966
|
const critical = failed.filter(f => f.severity === 'critical');
|
|
6489
6967
|
const high = failed.filter(f => f.severity === 'high');
|
|
@@ -6502,14 +6980,14 @@ async function checkRawUrl(url, options) {
|
|
|
6502
6980
|
});
|
|
6503
6981
|
return;
|
|
6504
6982
|
}
|
|
6505
|
-
// Display results
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6983
|
+
// Display results using unified display
|
|
6984
|
+
displayUnifiedCheck({
|
|
6985
|
+
name: displayName,
|
|
6986
|
+
sourceLabel: 'URL',
|
|
6987
|
+
projectType: result.projectType,
|
|
6988
|
+
localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
|
|
6989
|
+
verbose: !!options.verbose,
|
|
6990
|
+
});
|
|
6513
6991
|
// Community contribution (auto-share if opted in, no first-time prompt for URLs)
|
|
6514
6992
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6515
6993
|
if (isContributeEnabled()) {
|
|
@@ -6519,7 +6997,6 @@ async function checkRawUrl(url, options) {
|
|
|
6519
6997
|
queuePendingScan(displayName, result);
|
|
6520
6998
|
}
|
|
6521
6999
|
}
|
|
6522
|
-
printCheckNextSteps(displayName);
|
|
6523
7000
|
if (critical.length > 0 || high.length > 0)
|
|
6524
7001
|
process.exit(1);
|
|
6525
7002
|
}
|
|
@@ -6544,30 +7021,24 @@ async function checkRawUrl(url, options) {
|
|
|
6544
7021
|
}
|
|
6545
7022
|
}
|
|
6546
7023
|
async function checkNpmPackage(name, options) {
|
|
6547
|
-
//
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
7024
|
+
// Fetch registry data in parallel with download+scan (unless --no-registry)
|
|
7025
|
+
const registryPromise = options.registry === false ? Promise.resolve(null) : queryRegistry(name);
|
|
7026
|
+
// Registry-only mode (--no-scan): skip local scan
|
|
7027
|
+
if (options.scan === false) {
|
|
7028
|
+
const registryData = await registryPromise;
|
|
7029
|
+
if (registryData?.found) {
|
|
6552
7030
|
if (options.json) {
|
|
6553
7031
|
writeJsonStdout({ ...registryData, source: 'registry' });
|
|
6554
7032
|
return;
|
|
6555
7033
|
}
|
|
6556
|
-
|
|
7034
|
+
displayUnifiedCheck({ name, registry: registryData, verbose: !!options.verbose });
|
|
6557
7035
|
return;
|
|
6558
7036
|
}
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
if (!options.json && !globalCiMode) {
|
|
6562
|
-
const days = Math.floor((Date.now() - new Date(registryData.lastScannedAt).getTime()) / (1000 * 60 * 60 * 24));
|
|
6563
|
-
console.error(`\nRegistry data is ${days} day(s) old. Re-scanning...`);
|
|
6564
|
-
}
|
|
7037
|
+
if (!options.json && !globalCiMode) {
|
|
7038
|
+
console.error(`No registry data found for ${name}. Running local scan...`);
|
|
6565
7039
|
}
|
|
6566
7040
|
}
|
|
6567
|
-
|
|
6568
|
-
console.error(`Forcing fresh local scan (--rescan)...`);
|
|
6569
|
-
}
|
|
6570
|
-
// Step 2: Download and scan
|
|
7041
|
+
// Download and scan
|
|
6571
7042
|
const { mkdtemp, rm } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
6572
7043
|
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
6573
7044
|
const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
@@ -6599,11 +7070,11 @@ async function checkNpmPackage(name, options) {
|
|
|
6599
7070
|
catch {
|
|
6600
7071
|
// NanoMind unavailable — use base scan results
|
|
6601
7072
|
}
|
|
7073
|
+
// Filter local-dev-only findings irrelevant to downloaded packages
|
|
7074
|
+
filterLocalOnlyFindings(result, scanner);
|
|
6602
7075
|
const failed = result.findings.filter(f => !f.passed);
|
|
6603
7076
|
const critical = failed.filter(f => f.severity === 'critical');
|
|
6604
7077
|
const high = failed.filter(f => f.severity === 'high');
|
|
6605
|
-
const medium = failed.filter(f => f.severity === 'medium');
|
|
6606
|
-
const low = failed.filter(f => f.severity === 'low');
|
|
6607
7078
|
if (options.json) {
|
|
6608
7079
|
writeJsonStdout({
|
|
6609
7080
|
name,
|
|
@@ -6616,19 +7087,20 @@ async function checkNpmPackage(name, options) {
|
|
|
6616
7087
|
});
|
|
6617
7088
|
return;
|
|
6618
7089
|
}
|
|
6619
|
-
//
|
|
6620
|
-
const
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
7090
|
+
// Await registry data (started in parallel with download)
|
|
7091
|
+
const registryData = await registryPromise;
|
|
7092
|
+
// Display results using unified display
|
|
7093
|
+
displayUnifiedCheck({
|
|
7094
|
+
name,
|
|
7095
|
+
projectType: result.projectType,
|
|
7096
|
+
localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
|
|
7097
|
+
registry: registryData,
|
|
7098
|
+
verbose: !!options.verbose,
|
|
7099
|
+
});
|
|
7100
|
+
// Community contribution (after 3 scans, interactive only)
|
|
6628
7101
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6629
7102
|
const scanCount = incrementScanCounter();
|
|
6630
7103
|
if (scanCount >= 3 && !hasContributeChoice()) {
|
|
6631
|
-
console.log();
|
|
6632
7104
|
console.log(` ${colors.dim}Your scans help other developers make safer choices.`);
|
|
6633
7105
|
console.log(` Sharing adds anonymized results to the OpenA2A trust registry`);
|
|
6634
7106
|
console.log(` so others can check packages before installing.${RESET()}`);
|
|
@@ -6643,10 +7115,9 @@ async function checkNpmPackage(name, options) {
|
|
|
6643
7115
|
if (wantsToShare) {
|
|
6644
7116
|
const ok = await publishToRegistry(name, result);
|
|
6645
7117
|
if (ok) {
|
|
6646
|
-
console.error(
|
|
7118
|
+
console.error(`\n ${colors.green}Thanks for sharing! Future scans will auto-contribute.${RESET()}\n`);
|
|
6647
7119
|
}
|
|
6648
7120
|
else {
|
|
6649
|
-
// Silent — queue locally and retry on next scan
|
|
6650
7121
|
queuePendingScan(name, result);
|
|
6651
7122
|
}
|
|
6652
7123
|
}
|
|
@@ -6659,7 +7130,6 @@ async function checkNpmPackage(name, options) {
|
|
|
6659
7130
|
queuePendingScan(name, result);
|
|
6660
7131
|
}
|
|
6661
7132
|
}
|
|
6662
|
-
printCheckNextSteps(name);
|
|
6663
7133
|
if (critical.length > 0 || high.length > 0)
|
|
6664
7134
|
process.exit(1);
|
|
6665
7135
|
}
|