hackmyagent 0.16.7 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +699 -186
- package/dist/cli.js.map +1 -1
- package/dist/hardening/index.d.ts +1 -1
- package/dist/hardening/index.d.ts.map +1 -1
- package/dist/hardening/index.js +2 -1
- package/dist/hardening/index.js.map +1 -1
- package/dist/hardening/scanner.d.ts +16 -0
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +28 -18
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/hardening/taxonomy.d.ts +5 -0
- package/dist/hardening/taxonomy.d.ts.map +1 -1
- package/dist/hardening/taxonomy.js +66 -0
- package/dist/hardening/taxonomy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/nanomind-core/analyzers/credential-analyzer.js +6 -0
- package/dist/nanomind-core/analyzers/credential-analyzer.js.map +1 -1
- package/dist/nanomind-core/compiler/semantic-compiler.js +7 -2
- package/dist/nanomind-core/compiler/semantic-compiler.js.map +1 -1
- package/dist/nanomind-core/scanner-bridge.d.ts.map +1 -1
- package/dist/nanomind-core/scanner-bridge.js +33 -5
- package/dist/nanomind-core/scanner-bridge.js.map +1 -1
- package/dist/registry/publish.d.ts.map +1 -1
- package/dist/registry/publish.js +3 -4
- package/dist/registry/publish.js.map +1 -1
- package/dist/scanner/external-scanner.js +2 -2
- package/dist/scanner/external-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
|
-
|
|
177
|
-
|
|
178
|
-
•
|
|
179
|
-
•
|
|
180
|
-
•
|
|
174
|
+
Downloads + scans (${CHECK_COUNT} checks + NanoMind) by default, with trust context from the OpenA2A registry.
|
|
175
|
+
|
|
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
|
|
181
183
|
|
|
182
|
-
|
|
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;
|
|
@@ -273,9 +267,23 @@ Examples:
|
|
|
273
267
|
return;
|
|
274
268
|
}
|
|
275
269
|
// npm package name: download, run full HMA scan, clean up
|
|
270
|
+
// On npm 404, fall through to skill check (skill identifiers look like @scope/name)
|
|
276
271
|
if (looksLikeNpmPackage(skill)) {
|
|
277
|
-
|
|
278
|
-
|
|
272
|
+
try {
|
|
273
|
+
await checkNpmPackage(skill, options);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
catch (npmErr) {
|
|
277
|
+
if (npmErr instanceof Error && npmErr.name === 'NpmNotFoundError') {
|
|
278
|
+
// Not on npm — fall through to skill check
|
|
279
|
+
if (!options.json && !globalCiMode) {
|
|
280
|
+
console.error(`Package "${skill}" not found on npm. Trying as skill identifier...`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
throw npmErr; // Re-throw non-404 errors
|
|
285
|
+
}
|
|
286
|
+
}
|
|
279
287
|
}
|
|
280
288
|
// --rescan only applies to targets that otherwise hit the registry cache.
|
|
281
289
|
// For skill identifiers we fall through to the registry lookup below.
|
|
@@ -366,10 +374,10 @@ Examples:
|
|
|
366
374
|
});
|
|
367
375
|
// Severity colors and symbols for secure command
|
|
368
376
|
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 },
|
|
377
|
+
critical: { symbol: '[!!]', label: 'CRITICAL', color: () => colors.brightRed },
|
|
378
|
+
high: { symbol: '[!]', label: 'HIGH', color: () => colors.red },
|
|
379
|
+
medium: { symbol: '[~]', label: 'MEDIUM', color: () => colors.yellow },
|
|
380
|
+
low: { symbol: '[.]', label: 'LOW', color: () => colors.green },
|
|
373
381
|
};
|
|
374
382
|
/**
|
|
375
383
|
* Display check command findings with optional verbose details.
|
|
@@ -408,6 +416,378 @@ function displayCheckFindings(failed, verbose) {
|
|
|
408
416
|
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
409
417
|
}
|
|
410
418
|
}
|
|
419
|
+
function stripAnsi(s) {
|
|
420
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
421
|
+
}
|
|
422
|
+
/** Right-align a value at a fixed column width */
|
|
423
|
+
function rightAlign(left, right, width = 68) {
|
|
424
|
+
const leftLen = stripAnsi(left).length;
|
|
425
|
+
const rightLen = stripAnsi(right).length;
|
|
426
|
+
const pad = Math.max(1, width - leftLen - rightLen);
|
|
427
|
+
return `${left}${' '.repeat(pad)}${right}`;
|
|
428
|
+
}
|
|
429
|
+
/** Extract the actionable core of a fix/guidance string.
|
|
430
|
+
* Takes the first sentence, strips file path prefixes that duplicate
|
|
431
|
+
* the finding header, and wraps at terminal width. Never truncates with "...".
|
|
432
|
+
*/
|
|
433
|
+
function cleanFixText(text, fileAlreadyShown) {
|
|
434
|
+
// Take first meaningful line (skip blank lines)
|
|
435
|
+
let line = text.split('\n').map(l => l.trim()).filter(Boolean)[0] || text;
|
|
436
|
+
// Strip "In <file>," prefix when file is already shown in the finding header
|
|
437
|
+
if (fileAlreadyShown) {
|
|
438
|
+
const escapedFile = fileAlreadyShown.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
439
|
+
line = line.replace(new RegExp(`^In ${escapedFile},?\\s*`, 'i'), '');
|
|
440
|
+
// Capitalize first letter after stripping
|
|
441
|
+
if (line.length > 0)
|
|
442
|
+
line = line[0].toUpperCase() + line.slice(1);
|
|
443
|
+
}
|
|
444
|
+
return line;
|
|
445
|
+
}
|
|
446
|
+
/** Shorten a file path for display — show filename + parent dir only */
|
|
447
|
+
function shortenPath(filePath) {
|
|
448
|
+
const parts = filePath.split('/');
|
|
449
|
+
if (parts.length <= 2)
|
|
450
|
+
return filePath;
|
|
451
|
+
return parts.slice(-2).join('/');
|
|
452
|
+
}
|
|
453
|
+
function displayUnifiedCheck(opts) {
|
|
454
|
+
const { name, sourceLabel, projectType, localScan, registry, verbose, version, nanomindScan } = opts;
|
|
455
|
+
// ── Visual helpers ──────────────────────────────────────────────────
|
|
456
|
+
const METER_WIDTH = 20;
|
|
457
|
+
const divider = (label) => {
|
|
458
|
+
if (label) {
|
|
459
|
+
console.log(`\n ${colors.dim}──${RESET()} ${colors.bold}${label}${RESET()} ${colors.dim}${'─'.repeat(Math.max(1, 56 - label.length))}${RESET()}`);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
console.log(` ${colors.dim}${'─'.repeat(62)}${RESET()}`);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const scoreMeter = (value, max = 100) => {
|
|
466
|
+
const pct = Math.round((value / max) * METER_WIDTH);
|
|
467
|
+
const meterColor = value >= 70 ? colors.green : value >= 40 ? colors.yellow : colors.red;
|
|
468
|
+
const filled = '━'.repeat(pct);
|
|
469
|
+
const empty = '━'.repeat(METER_WIDTH - pct);
|
|
470
|
+
return `${meterColor}${filled}${RESET()}${colors.dim}${empty}${RESET()} ${meterColor}${colors.bold}${value}${RESET()}${colors.dim}/${max}${RESET()}`;
|
|
471
|
+
};
|
|
472
|
+
const sevBadge = (sev) => {
|
|
473
|
+
const d = SEVERITY_DISPLAY[sev];
|
|
474
|
+
return `${d.color()}${colors.bold}${d.label}${RESET()}`;
|
|
475
|
+
};
|
|
476
|
+
// ── Compute findings ────────────────────────────────────────────────
|
|
477
|
+
let failed = [];
|
|
478
|
+
let score = 0;
|
|
479
|
+
let maxScore = 100;
|
|
480
|
+
let critical = 0, high = 0, medium = 0, low = 0;
|
|
481
|
+
if (localScan) {
|
|
482
|
+
failed = localScan.findings.filter(f => !f.passed);
|
|
483
|
+
score = localScan.score;
|
|
484
|
+
maxScore = localScan.maxScore;
|
|
485
|
+
critical = failed.filter(f => f.severity === 'critical').length;
|
|
486
|
+
high = failed.filter(f => f.severity === 'high').length;
|
|
487
|
+
medium = failed.filter(f => f.severity === 'medium').length;
|
|
488
|
+
low = failed.filter(f => f.severity === 'low').length;
|
|
489
|
+
}
|
|
490
|
+
else if (nanomindScan) {
|
|
491
|
+
const issues = nanomindScan.findings.filter(f => !f.passed);
|
|
492
|
+
critical = issues.filter(f => f.severity === 'critical').length;
|
|
493
|
+
high = issues.filter(f => f.severity === 'high').length;
|
|
494
|
+
medium = issues.filter(f => f.severity === 'medium').length;
|
|
495
|
+
low = issues.filter(f => f.severity === 'low').length;
|
|
496
|
+
failed = issues.map(f => ({
|
|
497
|
+
checkId: f.checkId || '',
|
|
498
|
+
name: f.name || f.description || '',
|
|
499
|
+
description: f.description || '',
|
|
500
|
+
category: f.category || '',
|
|
501
|
+
severity: f.severity,
|
|
502
|
+
passed: false,
|
|
503
|
+
message: f.message || f.description || '',
|
|
504
|
+
fixable: false,
|
|
505
|
+
file: f.file,
|
|
506
|
+
line: f.line,
|
|
507
|
+
fix: f.fix,
|
|
508
|
+
guidance: f.guidance,
|
|
509
|
+
attackClass: f.attackClass,
|
|
510
|
+
}));
|
|
511
|
+
// Use the canonical scoring formula (exponential decay + 0.4x governance weight)
|
|
512
|
+
const scoreResult = (0, index_1.calculateSecurityScore)(issues);
|
|
513
|
+
score = scoreResult.score;
|
|
514
|
+
maxScore = scoreResult.maxScore;
|
|
515
|
+
}
|
|
516
|
+
else if (registry?.found) {
|
|
517
|
+
score = Math.round(registry.trustScore * 100);
|
|
518
|
+
maxScore = 100;
|
|
519
|
+
}
|
|
520
|
+
const totalFindings = critical + high + medium + low;
|
|
521
|
+
// ── Header ──────────────────────────────────────────────────────────
|
|
522
|
+
const typeLabel = (registry?.packageType || projectType || 'unknown').replace(/_/g, ' ');
|
|
523
|
+
const meta = [typeLabel];
|
|
524
|
+
if (version)
|
|
525
|
+
meta.unshift(`v${version}`);
|
|
526
|
+
if (sourceLabel)
|
|
527
|
+
meta.push(sourceLabel);
|
|
528
|
+
if (nanomindScan)
|
|
529
|
+
meta.push(`${nanomindScan.compiledArtifacts} files analyzed`);
|
|
530
|
+
if (localScan?.filesScanned)
|
|
531
|
+
meta.push(`${localScan.filesScanned} files scanned`);
|
|
532
|
+
console.log();
|
|
533
|
+
console.log(` ${colors.bold}${colors.white}${name}${RESET()} ${colors.dim}${meta.join(' · ')}${RESET()}`);
|
|
534
|
+
// ── Verdict + Score ─────────────────────────────────────────────────
|
|
535
|
+
if (localScan || nanomindScan) {
|
|
536
|
+
let verdictText;
|
|
537
|
+
let verdictColor;
|
|
538
|
+
if (critical > 0) {
|
|
539
|
+
verdictColor = colors.brightRed;
|
|
540
|
+
verdictText = `${critical} critical issue${critical > 1 ? 's' : ''} found`;
|
|
541
|
+
}
|
|
542
|
+
else if (high > 0) {
|
|
543
|
+
verdictColor = colors.red;
|
|
544
|
+
verdictText = `${high} high-severity issue${high > 1 ? 's' : ''} found`;
|
|
545
|
+
}
|
|
546
|
+
else if (totalFindings > 0) {
|
|
547
|
+
verdictColor = colors.yellow;
|
|
548
|
+
verdictText = `${totalFindings} issue${totalFindings > 1 ? 's' : ''} found`;
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
verdictColor = colors.green;
|
|
552
|
+
verdictText = 'No security issues found';
|
|
553
|
+
}
|
|
554
|
+
console.log(` ${verdictColor}${colors.bold}${verdictText}${RESET()}`);
|
|
555
|
+
console.log();
|
|
556
|
+
console.log(` Security ${scoreMeter(score, maxScore)}`);
|
|
557
|
+
}
|
|
558
|
+
else if (registry?.found) {
|
|
559
|
+
const normalized = normalizeTrustVerdict(registry.verdict);
|
|
560
|
+
let verdictText;
|
|
561
|
+
let verdictColor;
|
|
562
|
+
if (normalized === 'blocked') {
|
|
563
|
+
verdictColor = colors.brightRed;
|
|
564
|
+
verdictText = 'Blocked by registry';
|
|
565
|
+
}
|
|
566
|
+
else if (normalized === 'warning') {
|
|
567
|
+
verdictColor = colors.yellow;
|
|
568
|
+
verdictText = 'Warning — review before installing';
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
verdictColor = colors.green;
|
|
572
|
+
verdictText = 'No known issues';
|
|
573
|
+
}
|
|
574
|
+
console.log(` ${verdictColor}${colors.bold}${verdictText}${RESET()}`);
|
|
575
|
+
console.log();
|
|
576
|
+
console.log(` Trust ${scoreMeter(score, maxScore)}`);
|
|
577
|
+
}
|
|
578
|
+
// ── Findings ────────────────────────────────────────────────────────
|
|
579
|
+
if (failed.length > 0) {
|
|
580
|
+
// Severity summary as colored pills
|
|
581
|
+
const summaryParts = [];
|
|
582
|
+
if (critical > 0)
|
|
583
|
+
summaryParts.push(`${colors.brightRed}${colors.bold}${critical} critical${RESET()}`);
|
|
584
|
+
if (high > 0)
|
|
585
|
+
summaryParts.push(`${colors.red}${colors.bold}${high} high${RESET()}`);
|
|
586
|
+
if (medium > 0)
|
|
587
|
+
summaryParts.push(`${colors.yellow}${medium} medium${RESET()}`);
|
|
588
|
+
if (low > 0)
|
|
589
|
+
summaryParts.push(`${colors.dim}${low} low${RESET()}`);
|
|
590
|
+
divider('Findings');
|
|
591
|
+
console.log(` ${summaryParts.join(' ')}`);
|
|
592
|
+
// High-count mode: group by category when > 20 findings
|
|
593
|
+
if (totalFindings > 20 && !verbose) {
|
|
594
|
+
const groups = new Map();
|
|
595
|
+
for (const f of failed) {
|
|
596
|
+
const key = f.category || f.name || 'Other';
|
|
597
|
+
if (!groups.has(key))
|
|
598
|
+
groups.set(key, { critical: 0, high: 0, medium: 0, low: 0, files: new Set() });
|
|
599
|
+
const g = groups.get(key);
|
|
600
|
+
g[f.severity]++;
|
|
601
|
+
if (f.file)
|
|
602
|
+
g.files.add(f.file.split('/')[0] || f.file);
|
|
603
|
+
}
|
|
604
|
+
const sorted = [...groups.entries()].sort((a, b) => {
|
|
605
|
+
const wa = a[1].critical * 4 + a[1].high * 3 + a[1].medium * 2 + a[1].low;
|
|
606
|
+
const wb = b[1].critical * 4 + b[1].high * 3 + b[1].medium * 2 + b[1].low;
|
|
607
|
+
return wb - wa;
|
|
608
|
+
});
|
|
609
|
+
console.log();
|
|
610
|
+
for (const [cat, g] of sorted.slice(0, 8)) {
|
|
611
|
+
const counts = [];
|
|
612
|
+
if (g.critical > 0)
|
|
613
|
+
counts.push(`${colors.brightRed}${g.critical} crit${RESET()}`);
|
|
614
|
+
if (g.high > 0)
|
|
615
|
+
counts.push(`${colors.red}${g.high} high${RESET()}`);
|
|
616
|
+
if (g.medium > 0)
|
|
617
|
+
counts.push(`${colors.dim}${g.medium} med${RESET()}`);
|
|
618
|
+
if (g.low > 0)
|
|
619
|
+
counts.push(`${colors.dim}${g.low} low${RESET()}`);
|
|
620
|
+
const fileHint = g.files.size <= 3 ? ` ${colors.dim}${[...g.files].join(', ')}${RESET()}` : '';
|
|
621
|
+
console.log(` ${colors.dim}│${RESET()} ${cat.padEnd(26)} ${counts.join(', ')}${fileHint}`);
|
|
622
|
+
}
|
|
623
|
+
if (sorted.length > 8) {
|
|
624
|
+
console.log(` ${colors.dim}│ + ${sorted.length - 8} more categories${RESET()}`);
|
|
625
|
+
}
|
|
626
|
+
// Top 3 issues with full detail
|
|
627
|
+
divider('Top Issues');
|
|
628
|
+
const topFindings = [...failed]
|
|
629
|
+
.sort((a, b) => {
|
|
630
|
+
const sw = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
631
|
+
return (sw[b.severity] || 0) - (sw[a.severity] || 0);
|
|
632
|
+
})
|
|
633
|
+
.slice(0, 3);
|
|
634
|
+
for (const f of topFindings) {
|
|
635
|
+
const shortFile = f.file ? shortenPath(f.file) : '';
|
|
636
|
+
const loc = shortFile + (f.line ? `:${f.line}` : '');
|
|
637
|
+
const borderColor = SEVERITY_DISPLAY[f.severity].color();
|
|
638
|
+
console.log();
|
|
639
|
+
console.log(` ${borderColor}│${RESET()} ${sevBadge(f.severity)} ${colors.bold}${colors.white}${f.name || f.message}${RESET()}`);
|
|
640
|
+
if (loc)
|
|
641
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}${loc}${RESET()}`);
|
|
642
|
+
if (f.guidance) {
|
|
643
|
+
console.log(` ${borderColor}│${RESET()} ${cleanFixText(f.guidance, f.file)}`);
|
|
644
|
+
}
|
|
645
|
+
if (f.fix) {
|
|
646
|
+
console.log(` ${borderColor}│${RESET()} ${colors.cyan}Fix:${RESET()} ${cleanFixText(f.fix, f.file)}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
// Normal mode: individual findings sorted by severity, with collapse
|
|
652
|
+
const sevWeight = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
653
|
+
failed.sort((a, b) => (sevWeight[b.severity] || 0) - (sevWeight[a.severity] || 0));
|
|
654
|
+
const skipped = new Set();
|
|
655
|
+
let shown = 0;
|
|
656
|
+
const limit = verbose ? failed.length : 10;
|
|
657
|
+
for (let i = 0; i < failed.length; i++) {
|
|
658
|
+
if (shown >= limit)
|
|
659
|
+
break;
|
|
660
|
+
if (skipped.has(i))
|
|
661
|
+
continue;
|
|
662
|
+
const f = failed[i];
|
|
663
|
+
const shortFile = f.file ? shortenPath(f.file) : '';
|
|
664
|
+
const loc = shortFile + (f.line ? `:${f.line}` : '');
|
|
665
|
+
const borderColor = SEVERITY_DISPLAY[f.severity].color();
|
|
666
|
+
console.log();
|
|
667
|
+
console.log(` ${borderColor}│${RESET()} ${sevBadge(f.severity)} ${colors.bold}${colors.white}${f.name || f.message}${RESET()}`);
|
|
668
|
+
if (loc)
|
|
669
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}${loc}${RESET()}`);
|
|
670
|
+
if (f.guidance) {
|
|
671
|
+
console.log(` ${borderColor}│${RESET()} ${cleanFixText(f.guidance, f.file)}`);
|
|
672
|
+
}
|
|
673
|
+
if (f.fix) {
|
|
674
|
+
console.log(` ${borderColor}│${RESET()} ${colors.cyan}Fix:${RESET()} ${cleanFixText(f.fix, f.file)}`);
|
|
675
|
+
}
|
|
676
|
+
if (verbose) {
|
|
677
|
+
if (f.checkId)
|
|
678
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}Check: ${f.checkId}${RESET()}`);
|
|
679
|
+
if (f.category)
|
|
680
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}Category: ${f.category}${RESET()}`);
|
|
681
|
+
}
|
|
682
|
+
shown++;
|
|
683
|
+
// Collapse similar
|
|
684
|
+
if (!verbose) {
|
|
685
|
+
const dir = f.file?.split('/').slice(0, -1).join('/') || '';
|
|
686
|
+
let similarCount = 0;
|
|
687
|
+
for (let j = i + 1; j < failed.length; j++) {
|
|
688
|
+
if (skipped.has(j))
|
|
689
|
+
continue;
|
|
690
|
+
const other = failed[j];
|
|
691
|
+
if (other.name === f.name) {
|
|
692
|
+
const otherDir = other.file?.split('/').slice(0, -1).join('/') || '';
|
|
693
|
+
if (otherDir === dir) {
|
|
694
|
+
skipped.add(j);
|
|
695
|
+
similarCount++;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (similarCount > 0) {
|
|
700
|
+
console.log(` ${borderColor}│${RESET()} ${colors.dim}+ ${similarCount} similar${dir ? ` in ${shortenPath(dir)}` : ''}${RESET()}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const remaining = failed.length - shown - skipped.size;
|
|
705
|
+
if (remaining > 0) {
|
|
706
|
+
console.log(`\n ${colors.dim}+ ${remaining} more findings (use --verbose to see all)${RESET()}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Path forward with recovery math
|
|
710
|
+
if (critical > 0 || high > 0) {
|
|
711
|
+
const recoveryParts = [];
|
|
712
|
+
if (critical > 0)
|
|
713
|
+
recoveryParts.push(`${critical} critical`);
|
|
714
|
+
if (high > 0)
|
|
715
|
+
recoveryParts.push(`${high} high`);
|
|
716
|
+
// Estimate recovered score: governance findings recover less per fix
|
|
717
|
+
const govFindings = failed.filter(f => {
|
|
718
|
+
const cat = (f.category || '').toLowerCase();
|
|
719
|
+
const id = f.checkId || '';
|
|
720
|
+
return cat === 'governance' || cat === 'injection-hardening' || cat === 'trust-hierarchy'
|
|
721
|
+
|| id.startsWith('AST-GOV') || id.startsWith('AST-GOVERN')
|
|
722
|
+
|| id.startsWith('AST-PROMPT') || id.startsWith('AST-HEARTBEAT');
|
|
723
|
+
});
|
|
724
|
+
const isGovernanceOnly = govFindings.length === failed.length;
|
|
725
|
+
const estRecovery = isGovernanceOnly
|
|
726
|
+
? Math.min(100, score + (critical * 8 + high * 5))
|
|
727
|
+
: Math.min(100, score + (critical * 15 + high * 8));
|
|
728
|
+
console.log();
|
|
729
|
+
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()}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// ── Registry ────────────────────────────────────────────────────────
|
|
733
|
+
if (registry?.found) {
|
|
734
|
+
const trustScore = Math.round(registry.trustScore * 100);
|
|
735
|
+
if (localScan || nanomindScan) {
|
|
736
|
+
divider('Registry');
|
|
737
|
+
console.log(` Trust ${scoreMeter(trustScore)}`);
|
|
738
|
+
}
|
|
739
|
+
const tlColor = trustLevelColor(registry.trustLevel);
|
|
740
|
+
const tlLabel = trustLevelLabel(registry.trustLevel);
|
|
741
|
+
console.log(` Level ${tlColor}${colors.bold}${tlLabel}${RESET()} ${colors.dim}(${registry.trustLevel}/4)${RESET()}`);
|
|
742
|
+
if (registry.communityScans !== undefined) {
|
|
743
|
+
console.log(` Community ${registry.communityScans > 0 ? colors.green : colors.dim}${registry.communityScans} scan${registry.communityScans !== 1 ? 's' : ''} shared${RESET()}`);
|
|
744
|
+
}
|
|
745
|
+
if (registry.cveCount !== undefined && registry.cveCount > 0) {
|
|
746
|
+
console.log(` CVEs ${colors.brightRed}${colors.bold}${registry.cveCount}${RESET()}`);
|
|
747
|
+
}
|
|
748
|
+
if (registry.dependencies) {
|
|
749
|
+
const d = registry.dependencies;
|
|
750
|
+
const depParts = [];
|
|
751
|
+
if (d.totalDeps !== undefined)
|
|
752
|
+
depParts.push(`${d.totalDeps} total`);
|
|
753
|
+
if (d.vulnerableDeps !== undefined && d.vulnerableDeps > 0)
|
|
754
|
+
depParts.push(`${colors.red}${d.vulnerableDeps} vulnerable${RESET()}`);
|
|
755
|
+
if (d.minTrustLevel !== undefined)
|
|
756
|
+
depParts.push(`min trust ${d.minTrustLevel}/4`);
|
|
757
|
+
if (depParts.length > 0) {
|
|
758
|
+
console.log(` Deps ${depParts.join(`${colors.dim} · ${RESET()}`)}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Trust level legend (when not fully verified)
|
|
762
|
+
if (registry.trustLevel < 4) {
|
|
763
|
+
const levels = ['Blocked', 'Warning', 'Listed', 'Scanned', 'Verified'];
|
|
764
|
+
const legend = levels.map((l, i) => {
|
|
765
|
+
if (i === registry.trustLevel)
|
|
766
|
+
return `${tlColor}${colors.bold}${l}${RESET()}`;
|
|
767
|
+
if (i < registry.trustLevel)
|
|
768
|
+
return `${colors.dim}${l}${RESET()}`;
|
|
769
|
+
return `${colors.dim}${l}${RESET()}`;
|
|
770
|
+
}).join(`${colors.dim} > ${RESET()}`);
|
|
771
|
+
console.log(` ${colors.dim}${legend}${RESET()}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// ── Next steps ──────────────────────────────────────────────────────
|
|
775
|
+
const hasGovIssues = failed.some(f => f.category === 'governance' || f.category === 'Governance' || f.checkId?.startsWith('AST-GOV') || f.checkId?.startsWith('AST-PROMPT'));
|
|
776
|
+
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'));
|
|
777
|
+
const hasCodeVulns = failed.some(f => {
|
|
778
|
+
const cat = (f.category || '').toLowerCase();
|
|
779
|
+
return cat !== 'governance' && cat !== 'injection-hardening' && cat !== 'trust-hierarchy'
|
|
780
|
+
&& !f.checkId?.startsWith('AST-GOV') && !f.checkId?.startsWith('AST-GOVERN')
|
|
781
|
+
&& !f.checkId?.startsWith('AST-PROMPT') && !f.checkId?.startsWith('AST-HEARTBEAT');
|
|
782
|
+
});
|
|
783
|
+
printCheckNextSteps(name, {
|
|
784
|
+
hasGovernanceIssues: hasGovIssues,
|
|
785
|
+
hasFindings: totalFindings > 0,
|
|
786
|
+
hasCredentialFindings: hasCredIssues,
|
|
787
|
+
hasCodeVulns,
|
|
788
|
+
isCleanScan: totalFindings === 0 && (!!localScan || !!nanomindScan),
|
|
789
|
+
});
|
|
790
|
+
}
|
|
411
791
|
function groupFindingsBySeverity(findings) {
|
|
412
792
|
const grouped = {
|
|
413
793
|
critical: [],
|
|
@@ -2862,7 +3242,7 @@ Examples:
|
|
|
2862
3242
|
.argument('<target>', 'Target hostname or IP address')
|
|
2863
3243
|
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
2864
3244
|
.option('-p, --ports <ports>', 'Comma-separated ports to scan (default: common MCP ports)')
|
|
2865
|
-
.option('-t, --timeout <ms>', 'Connection timeout in milliseconds', '
|
|
3245
|
+
.option('-t, --timeout <ms>', 'Connection timeout in milliseconds', '2000')
|
|
2866
3246
|
.option('-v, --verbose', 'Show detailed finding information')
|
|
2867
3247
|
.action(async (target, options) => {
|
|
2868
3248
|
try {
|
|
@@ -2878,11 +3258,11 @@ Examples:
|
|
|
2878
3258
|
`\n`);
|
|
2879
3259
|
process.exit(1);
|
|
2880
3260
|
}
|
|
2881
|
-
const timeoutMs = parseInt(options.timeout ?? '
|
|
3261
|
+
const timeoutMs = parseInt(options.timeout ?? '2000', 10);
|
|
2882
3262
|
const customPorts = options.ports
|
|
2883
3263
|
? options.ports.split(',').map((p) => parseInt(p.trim(), 10))
|
|
2884
3264
|
: undefined;
|
|
2885
|
-
const portCount = customPorts?.length ??
|
|
3265
|
+
const portCount = customPorts?.length ?? 2;
|
|
2886
3266
|
if (!options.json) {
|
|
2887
3267
|
console.log(`\nScanning ${target} (${portCount} ports, ${timeoutMs}ms timeout)...\n`);
|
|
2888
3268
|
}
|
|
@@ -5197,6 +5577,28 @@ Examples:
|
|
|
5197
5577
|
if (opts.json) {
|
|
5198
5578
|
writeJsonStdout(result);
|
|
5199
5579
|
}
|
|
5580
|
+
else if (result.found) {
|
|
5581
|
+
// Use the unified display (same as `check --no-scan`) for visual consistency
|
|
5582
|
+
const registryData = {
|
|
5583
|
+
found: true,
|
|
5584
|
+
name: result.name,
|
|
5585
|
+
trustScore: result.trustScore,
|
|
5586
|
+
trustLevel: result.trustLevel,
|
|
5587
|
+
verdict: result.verdict,
|
|
5588
|
+
scanStatus: result.scanStatus,
|
|
5589
|
+
lastScannedAt: result.lastScannedAt,
|
|
5590
|
+
packageType: result.packageType,
|
|
5591
|
+
recommendation: result.recommendation,
|
|
5592
|
+
cveCount: result.cveCount,
|
|
5593
|
+
communityScans: result.communityScans,
|
|
5594
|
+
dependencies: result.dependencies ? {
|
|
5595
|
+
totalDeps: result.dependencies.totalDeps,
|
|
5596
|
+
vulnerableDeps: result.dependencies.vulnerableDeps,
|
|
5597
|
+
minTrustLevel: result.dependencies.minTrustLevel,
|
|
5598
|
+
} : undefined,
|
|
5599
|
+
};
|
|
5600
|
+
displayUnifiedCheck({ name: packageName, registry: registryData, verbose: false });
|
|
5601
|
+
}
|
|
5200
5602
|
else {
|
|
5201
5603
|
process.stdout.write(formatTrustCheck(result));
|
|
5202
5604
|
}
|
|
@@ -5215,7 +5617,7 @@ program
|
|
|
5215
5617
|
.option('-d, --directory <dir>', 'Scan a specific directory to collect check metadata from findings')
|
|
5216
5618
|
.option('--json', 'Output as JSON (default)')
|
|
5217
5619
|
.action(async (options) => {
|
|
5218
|
-
const { getAttackClass, getTaxonomyMap } = require('./hardening/taxonomy');
|
|
5620
|
+
const { getAttackClass, getTaxonomyMap, getCheckSeverity } = require('./hardening/taxonomy');
|
|
5219
5621
|
// Build static registry from taxonomy map (covers all known checks)
|
|
5220
5622
|
const taxMap = getTaxonomyMap();
|
|
5221
5623
|
const metadata = {};
|
|
@@ -5227,7 +5629,7 @@ program
|
|
|
5227
5629
|
name: checkId,
|
|
5228
5630
|
category: prefix.toLowerCase(),
|
|
5229
5631
|
attackClass: taxMap[checkId] || '',
|
|
5230
|
-
severity:
|
|
5632
|
+
severity: getCheckSeverity(checkId),
|
|
5231
5633
|
};
|
|
5232
5634
|
}
|
|
5233
5635
|
// If a directory is provided, enrich with actual finding data (names, severity, etc.)
|
|
@@ -5912,6 +6314,7 @@ async function queryRegistry(name) {
|
|
|
5912
6314
|
const data = await response.json();
|
|
5913
6315
|
if (!data.packageId)
|
|
5914
6316
|
return null;
|
|
6317
|
+
const deps = data.dependencies;
|
|
5915
6318
|
return {
|
|
5916
6319
|
found: true,
|
|
5917
6320
|
name: data.name ?? name,
|
|
@@ -5921,6 +6324,15 @@ async function queryRegistry(name) {
|
|
|
5921
6324
|
scanStatus: data.scanStatus,
|
|
5922
6325
|
lastScannedAt: data.lastScannedAt,
|
|
5923
6326
|
packageType: data.packageType,
|
|
6327
|
+
recommendation: data.recommendation,
|
|
6328
|
+
cveCount: typeof data.cveCount === 'number' ? data.cveCount : undefined,
|
|
6329
|
+
communityScans: typeof data.communityScans === 'number' ? data.communityScans : undefined,
|
|
6330
|
+
dependencies: deps ? {
|
|
6331
|
+
totalDeps: typeof deps.totalDeps === 'number' ? deps.totalDeps : undefined,
|
|
6332
|
+
vulnerableDeps: typeof deps.vulnerableDeps === 'number' ? deps.vulnerableDeps : undefined,
|
|
6333
|
+
minTrustLevel: typeof deps.minTrustLevel === 'number' ? deps.minTrustLevel : undefined,
|
|
6334
|
+
riskSummary: deps.riskSummary,
|
|
6335
|
+
} : undefined,
|
|
5924
6336
|
};
|
|
5925
6337
|
}
|
|
5926
6338
|
catch {
|
|
@@ -5954,7 +6366,9 @@ async function publishToRegistry(name, result) {
|
|
|
5954
6366
|
score: result.score,
|
|
5955
6367
|
maxScore: result.maxScore,
|
|
5956
6368
|
projectType: result.projectType,
|
|
5957
|
-
findings: result.findings
|
|
6369
|
+
findings: result.findings
|
|
6370
|
+
.filter(f => !PACKAGE_SCAN_LOCAL_ONLY_CATEGORIES.has(f.category))
|
|
6371
|
+
.map(f => ({
|
|
5958
6372
|
checkId: f.checkId,
|
|
5959
6373
|
name: f.name,
|
|
5960
6374
|
severity: f.severity,
|
|
@@ -6020,6 +6434,90 @@ function getFullScanHint() {
|
|
|
6020
6434
|
return override;
|
|
6021
6435
|
return `${CLI_PREFIX} secure <dir>`;
|
|
6022
6436
|
}
|
|
6437
|
+
/**
|
|
6438
|
+
* Categories that describe local dev-environment setup, not package security.
|
|
6439
|
+
* Findings in these categories are filtered from display when scanning a
|
|
6440
|
+
* *downloaded* package (npm pack, pip download, git clone to temp dir).
|
|
6441
|
+
* They remain visible when scanning a user's own project directory.
|
|
6442
|
+
*/
|
|
6443
|
+
const PACKAGE_SCAN_LOCAL_ONLY_CATEGORIES = new Set([
|
|
6444
|
+
'git',
|
|
6445
|
+
'permissions',
|
|
6446
|
+
'environment',
|
|
6447
|
+
'logging',
|
|
6448
|
+
'claude-code',
|
|
6449
|
+
'cursor',
|
|
6450
|
+
'vscode',
|
|
6451
|
+
]);
|
|
6452
|
+
/**
|
|
6453
|
+
* Paths that are AI tooling artifacts, not package source code.
|
|
6454
|
+
* Governance findings on these files are noise when scanning a downloaded
|
|
6455
|
+
* package or cloned repo — they're instructions to an AI assistant, not
|
|
6456
|
+
* security vulnerabilities in the package itself.
|
|
6457
|
+
*/
|
|
6458
|
+
const AI_TOOLING_PATH_PATTERNS = [
|
|
6459
|
+
/^\.claude\//,
|
|
6460
|
+
/^CLAUDE\.md$/i,
|
|
6461
|
+
/^\.cursorrules$/i,
|
|
6462
|
+
/^\.aider/,
|
|
6463
|
+
/^\.copilot\//,
|
|
6464
|
+
/^\.github\/copilot/,
|
|
6465
|
+
/\.env\.example$/i, // Example env files are not real credentials
|
|
6466
|
+
/\.env\.sample$/i,
|
|
6467
|
+
/\.env\.template$/i,
|
|
6468
|
+
];
|
|
6469
|
+
/** Governance-related categories/checkId prefixes that are noise on AI tooling files */
|
|
6470
|
+
const GOVERNANCE_CATEGORIES = new Set([
|
|
6471
|
+
'governance',
|
|
6472
|
+
'injection-hardening',
|
|
6473
|
+
'trust-hierarchy',
|
|
6474
|
+
]);
|
|
6475
|
+
const GOVERNANCE_CHECK_PREFIXES = ['AST-GOV', 'AST-GOVERN', 'AST-PROMPT'];
|
|
6476
|
+
/** Test file path patterns — findings here are lower risk */
|
|
6477
|
+
const TEST_FILE_PATTERNS = [
|
|
6478
|
+
/\btests?\//i,
|
|
6479
|
+
/\b__tests__\//,
|
|
6480
|
+
/\btest_[^/]+$/,
|
|
6481
|
+
/[^/]+_test\.\w+$/,
|
|
6482
|
+
/[^/]+\.test\.\w+$/,
|
|
6483
|
+
/[^/]+\.spec\.\w+$/,
|
|
6484
|
+
/\bfixtures?\//i,
|
|
6485
|
+
];
|
|
6486
|
+
function isTestFile(filePath) {
|
|
6487
|
+
return TEST_FILE_PATTERNS.some(p => p.test(filePath));
|
|
6488
|
+
}
|
|
6489
|
+
function isAiToolingFile(filePath) {
|
|
6490
|
+
return AI_TOOLING_PATH_PATTERNS.some(p => p.test(filePath));
|
|
6491
|
+
}
|
|
6492
|
+
/**
|
|
6493
|
+
* Filter out local-dev-only findings that are meaningless for downloaded
|
|
6494
|
+
* packages (e.g. "Missing .gitignore" on an npm tarball). Also filters
|
|
6495
|
+
* governance findings on AI tooling files and demotes test file findings.
|
|
6496
|
+
* Mutates `result.findings` in place and recalculates the score.
|
|
6497
|
+
*/
|
|
6498
|
+
function filterLocalOnlyFindings(result, scanner) {
|
|
6499
|
+
result.findings = result.findings.filter(f => {
|
|
6500
|
+
// Remove local-only categories (git, permissions, env, etc.)
|
|
6501
|
+
if (PACKAGE_SCAN_LOCAL_ONLY_CATEGORIES.has(f.category))
|
|
6502
|
+
return false;
|
|
6503
|
+
// Exclude ALL findings on AI tooling files (CLAUDE.md, .claude/, .cursorrules, etc.)
|
|
6504
|
+
// These files contain instructions to AI assistants, not package source code.
|
|
6505
|
+
// Credential patterns, injection patterns, and governance findings in these
|
|
6506
|
+
// files are false positives — they describe security practices, not vulnerabilities.
|
|
6507
|
+
if (f.file && isAiToolingFile(f.file))
|
|
6508
|
+
return false;
|
|
6509
|
+
return true;
|
|
6510
|
+
});
|
|
6511
|
+
// Demote test file findings to low severity (test code patterns are
|
|
6512
|
+
// lower risk — pickle.load in a test file is not an attack surface)
|
|
6513
|
+
for (const f of result.findings) {
|
|
6514
|
+
if (f.file && isTestFile(f.file) && (f.severity === 'critical' || f.severity === 'high')) {
|
|
6515
|
+
f.originalSeverity = f.severity;
|
|
6516
|
+
f.severity = 'low';
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
|
|
6520
|
+
}
|
|
6023
6521
|
/**
|
|
6024
6522
|
* Print the standard 3-line next-steps footer shown after every `check`
|
|
6025
6523
|
* invocation. Lines:
|
|
@@ -6031,13 +6529,30 @@ function getFullScanHint() {
|
|
|
6031
6529
|
*
|
|
6032
6530
|
* Suppressed in --ci so machine-readable output stays clean.
|
|
6033
6531
|
*/
|
|
6034
|
-
function printCheckNextSteps(target) {
|
|
6532
|
+
function printCheckNextSteps(target, context) {
|
|
6035
6533
|
if (globalCiMode)
|
|
6036
6534
|
return;
|
|
6037
6535
|
console.log();
|
|
6038
|
-
console.log(` ${colors.dim}
|
|
6039
|
-
|
|
6040
|
-
|
|
6536
|
+
console.log(` ${colors.dim}──${RESET()} ${colors.bold}Next Steps${RESET()} ${colors.dim}${'─'.repeat(49)}${RESET()}`);
|
|
6537
|
+
if (context?.hasGovernanceIssues) {
|
|
6538
|
+
console.log(` ${colors.cyan}Auto-fix governance:${RESET()} ${CLI_PREFIX} harden-soul ${target}`);
|
|
6539
|
+
}
|
|
6540
|
+
if (context?.hasCredentialFindings) {
|
|
6541
|
+
console.log(` ${colors.cyan}Protect credentials:${RESET()} npx secretless-ai scan`);
|
|
6542
|
+
}
|
|
6543
|
+
if (context?.hasCodeVulns) {
|
|
6544
|
+
console.log(` ${colors.cyan}Auto-fix all issues:${RESET()} ${CLI_PREFIX} secure --fix`);
|
|
6545
|
+
}
|
|
6546
|
+
if (context?.hasFindings) {
|
|
6547
|
+
console.log(` ${colors.cyan}Full project audit:${RESET()} ${getFullScanHint()}`);
|
|
6548
|
+
}
|
|
6549
|
+
else if (context?.isCleanScan) {
|
|
6550
|
+
console.log(` ${colors.cyan}Governance scan:${RESET()} ${CLI_PREFIX} scan-soul ${target}`);
|
|
6551
|
+
console.log(` ${colors.cyan}Red-team test:${RESET()} ${CLI_PREFIX} attack --local`);
|
|
6552
|
+
}
|
|
6553
|
+
else {
|
|
6554
|
+
console.log(` ${colors.cyan}Deep scan:${RESET()} ${CLI_PREFIX} check ${target} --rescan`);
|
|
6555
|
+
}
|
|
6041
6556
|
console.log();
|
|
6042
6557
|
}
|
|
6043
6558
|
/**
|
|
@@ -6106,27 +6621,23 @@ async function suggestSimilarPackages(name) {
|
|
|
6106
6621
|
async function checkGitHubRepo(target, options) {
|
|
6107
6622
|
const { org, repo, cloneUrl } = parseGitHubTarget(target);
|
|
6108
6623
|
const displayName = `${org}/${repo}`;
|
|
6109
|
-
//
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6624
|
+
// Fetch registry data in parallel with clone (unless --no-registry)
|
|
6625
|
+
const registryPromise = options.registry === false ? Promise.resolve(null) : queryRegistry(displayName);
|
|
6626
|
+
// Registry-only mode (--no-scan): skip local scan
|
|
6627
|
+
if (options.scan === false) {
|
|
6628
|
+
const registryData = await registryPromise;
|
|
6629
|
+
if (registryData?.found) {
|
|
6113
6630
|
if (options.json) {
|
|
6114
6631
|
writeJsonStdout({ ...registryData, source: 'registry' });
|
|
6115
6632
|
return;
|
|
6116
6633
|
}
|
|
6117
|
-
|
|
6634
|
+
displayUnifiedCheck({ name: displayName, sourceLabel: 'GitHub', registry: registryData, verbose: !!options.verbose });
|
|
6118
6635
|
return;
|
|
6119
6636
|
}
|
|
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
|
-
}
|
|
6637
|
+
if (!options.json && !globalCiMode) {
|
|
6638
|
+
console.error(`No registry data found for ${displayName}. Running local scan...`);
|
|
6125
6639
|
}
|
|
6126
6640
|
}
|
|
6127
|
-
else if (options.rescan && !options.json && !globalCiMode) {
|
|
6128
|
-
console.error(`Forcing fresh local scan (--rescan)...`);
|
|
6129
|
-
}
|
|
6130
6641
|
// Step 2: Clone and scan
|
|
6131
6642
|
const { mkdtemp, rm } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
6132
6643
|
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
@@ -6157,6 +6668,8 @@ async function checkGitHubRepo(target, options) {
|
|
|
6157
6668
|
catch {
|
|
6158
6669
|
// NanoMind unavailable — use base scan results
|
|
6159
6670
|
}
|
|
6671
|
+
// Filter local-dev-only findings irrelevant to cloned repos
|
|
6672
|
+
filterLocalOnlyFindings(result, scanner);
|
|
6160
6673
|
const failed = result.findings.filter(f => !f.passed);
|
|
6161
6674
|
const critical = failed.filter(f => f.severity === 'critical');
|
|
6162
6675
|
const high = failed.filter(f => f.severity === 'high');
|
|
@@ -6174,19 +6687,21 @@ async function checkGitHubRepo(target, options) {
|
|
|
6174
6687
|
});
|
|
6175
6688
|
return;
|
|
6176
6689
|
}
|
|
6177
|
-
//
|
|
6178
|
-
const
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6690
|
+
// Await registry data (started in parallel with clone)
|
|
6691
|
+
const registryData = await registryPromise;
|
|
6692
|
+
// Display results using unified display
|
|
6693
|
+
displayUnifiedCheck({
|
|
6694
|
+
name: displayName,
|
|
6695
|
+
sourceLabel: 'GitHub',
|
|
6696
|
+
projectType: result.projectType,
|
|
6697
|
+
localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
|
|
6698
|
+
registry: registryData,
|
|
6699
|
+
verbose: !!options.verbose,
|
|
6700
|
+
});
|
|
6701
|
+
// Community contribution
|
|
6186
6702
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6187
6703
|
const scanCount = incrementScanCounter();
|
|
6188
6704
|
if (scanCount >= 3 && !hasContributeChoice()) {
|
|
6189
|
-
console.log();
|
|
6190
6705
|
console.log(` ${colors.dim}Your scans help other developers make safer choices.`);
|
|
6191
6706
|
console.log(` Sharing adds anonymized results to the OpenA2A trust registry`);
|
|
6192
6707
|
console.log(` so others can check packages before installing.${RESET()}`);
|
|
@@ -6201,7 +6716,7 @@ async function checkGitHubRepo(target, options) {
|
|
|
6201
6716
|
if (wantsToShare) {
|
|
6202
6717
|
const ok = await publishToRegistry(displayName, result);
|
|
6203
6718
|
if (ok) {
|
|
6204
|
-
console.error(
|
|
6719
|
+
console.error(`\n ${colors.green}Thanks for sharing! Future scans will auto-contribute.${RESET()}\n`);
|
|
6205
6720
|
}
|
|
6206
6721
|
else {
|
|
6207
6722
|
queuePendingScan(displayName, result);
|
|
@@ -6215,7 +6730,6 @@ async function checkGitHubRepo(target, options) {
|
|
|
6215
6730
|
queuePendingScan(displayName, result);
|
|
6216
6731
|
}
|
|
6217
6732
|
}
|
|
6218
|
-
printCheckNextSteps(displayName);
|
|
6219
6733
|
if (critical.length > 0 || high.length > 0)
|
|
6220
6734
|
process.exit(1);
|
|
6221
6735
|
}
|
|
@@ -6317,6 +6831,8 @@ async function checkPyPiPackage(target, options) {
|
|
|
6317
6831
|
catch {
|
|
6318
6832
|
// NanoMind unavailable -- use base scan results
|
|
6319
6833
|
}
|
|
6834
|
+
// Filter local-dev-only findings irrelevant to downloaded packages
|
|
6835
|
+
filterLocalOnlyFindings(result, scanner);
|
|
6320
6836
|
const failed = result.findings.filter(f => !f.passed);
|
|
6321
6837
|
const critical = failed.filter(f => f.severity === 'critical');
|
|
6322
6838
|
const high = failed.filter(f => f.severity === 'high');
|
|
@@ -6335,32 +6851,18 @@ async function checkPyPiPackage(target, options) {
|
|
|
6335
6851
|
});
|
|
6336
6852
|
return;
|
|
6337
6853
|
}
|
|
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);
|
|
6854
|
+
// Display results using unified display
|
|
6855
|
+
// Query registry for trust context (PyPI packages have pip: prefix in registry)
|
|
6856
|
+
const registryData = options.registry === false ? null : await queryRegistry(`pip:${name}`);
|
|
6857
|
+
displayUnifiedCheck({
|
|
6858
|
+
name,
|
|
6859
|
+
sourceLabel: 'PyPI',
|
|
6860
|
+
projectType: result.projectType,
|
|
6861
|
+
version: meta.info.version,
|
|
6862
|
+
localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
|
|
6863
|
+
registry: registryData,
|
|
6864
|
+
verbose: !!options.verbose,
|
|
6865
|
+
});
|
|
6364
6866
|
if (critical.length > 0 || high.length > 0)
|
|
6365
6867
|
process.exit(1);
|
|
6366
6868
|
}
|
|
@@ -6484,6 +6986,8 @@ async function checkRawUrl(url, options) {
|
|
|
6484
6986
|
catch {
|
|
6485
6987
|
// NanoMind unavailable — use base scan results
|
|
6486
6988
|
}
|
|
6989
|
+
// Filter local-dev-only findings irrelevant to downloaded URLs
|
|
6990
|
+
filterLocalOnlyFindings(result, scanner);
|
|
6487
6991
|
const failed = result.findings.filter(f => !f.passed);
|
|
6488
6992
|
const critical = failed.filter(f => f.severity === 'critical');
|
|
6489
6993
|
const high = failed.filter(f => f.severity === 'high');
|
|
@@ -6502,14 +7006,14 @@ async function checkRawUrl(url, options) {
|
|
|
6502
7006
|
});
|
|
6503
7007
|
return;
|
|
6504
7008
|
}
|
|
6505
|
-
// Display results
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
7009
|
+
// Display results using unified display
|
|
7010
|
+
displayUnifiedCheck({
|
|
7011
|
+
name: displayName,
|
|
7012
|
+
sourceLabel: 'URL',
|
|
7013
|
+
projectType: result.projectType,
|
|
7014
|
+
localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
|
|
7015
|
+
verbose: !!options.verbose,
|
|
7016
|
+
});
|
|
6513
7017
|
// Community contribution (auto-share if opted in, no first-time prompt for URLs)
|
|
6514
7018
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6515
7019
|
if (isContributeEnabled()) {
|
|
@@ -6519,7 +7023,6 @@ async function checkRawUrl(url, options) {
|
|
|
6519
7023
|
queuePendingScan(displayName, result);
|
|
6520
7024
|
}
|
|
6521
7025
|
}
|
|
6522
|
-
printCheckNextSteps(displayName);
|
|
6523
7026
|
if (critical.length > 0 || high.length > 0)
|
|
6524
7027
|
process.exit(1);
|
|
6525
7028
|
}
|
|
@@ -6544,30 +7047,24 @@ async function checkRawUrl(url, options) {
|
|
|
6544
7047
|
}
|
|
6545
7048
|
}
|
|
6546
7049
|
async function checkNpmPackage(name, options) {
|
|
6547
|
-
//
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
7050
|
+
// Fetch registry data in parallel with download+scan (unless --no-registry)
|
|
7051
|
+
const registryPromise = options.registry === false ? Promise.resolve(null) : queryRegistry(name);
|
|
7052
|
+
// Registry-only mode (--no-scan): skip local scan
|
|
7053
|
+
if (options.scan === false) {
|
|
7054
|
+
const registryData = await registryPromise;
|
|
7055
|
+
if (registryData?.found) {
|
|
6552
7056
|
if (options.json) {
|
|
6553
7057
|
writeJsonStdout({ ...registryData, source: 'registry' });
|
|
6554
7058
|
return;
|
|
6555
7059
|
}
|
|
6556
|
-
|
|
7060
|
+
displayUnifiedCheck({ name, registry: registryData, verbose: !!options.verbose });
|
|
6557
7061
|
return;
|
|
6558
7062
|
}
|
|
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
|
-
}
|
|
7063
|
+
if (!options.json && !globalCiMode) {
|
|
7064
|
+
console.error(`No registry data found for ${name}. Running local scan...`);
|
|
6565
7065
|
}
|
|
6566
7066
|
}
|
|
6567
|
-
|
|
6568
|
-
console.error(`Forcing fresh local scan (--rescan)...`);
|
|
6569
|
-
}
|
|
6570
|
-
// Step 2: Download and scan
|
|
7067
|
+
// Download and scan
|
|
6571
7068
|
const { mkdtemp, rm } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
6572
7069
|
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
6573
7070
|
const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
@@ -6583,7 +7080,35 @@ async function checkNpmPackage(name, options) {
|
|
|
6583
7080
|
const { stdout } = await execAsync('npm', ['pack', name, '--pack-destination', tempDir], { timeout: 60000 });
|
|
6584
7081
|
const tarball = stdout.trim().split('\n').pop();
|
|
6585
7082
|
await execAsync('tar', ['xzf', join(tempDir, tarball), '-C', tempDir], { timeout: 30000 });
|
|
6586
|
-
|
|
7083
|
+
// npm tarballs normally extract to 'package/', but some packages (e.g. @types/*)
|
|
7084
|
+
// may use a different directory name. Detect the actual extracted directory.
|
|
7085
|
+
const { readdir, stat } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
7086
|
+
let packageDir = join(tempDir, 'package');
|
|
7087
|
+
try {
|
|
7088
|
+
await stat(packageDir);
|
|
7089
|
+
}
|
|
7090
|
+
catch {
|
|
7091
|
+
// 'package/' doesn't exist — find the extracted directory (skip the .tgz file)
|
|
7092
|
+
const entries = await readdir(tempDir);
|
|
7093
|
+
const dirs = [];
|
|
7094
|
+
for (const entry of entries) {
|
|
7095
|
+
if (entry.endsWith('.tgz') || entry.endsWith('.tar.gz'))
|
|
7096
|
+
continue;
|
|
7097
|
+
const s = await stat(join(tempDir, entry));
|
|
7098
|
+
if (s.isDirectory())
|
|
7099
|
+
dirs.push(entry);
|
|
7100
|
+
}
|
|
7101
|
+
if (dirs.length === 1) {
|
|
7102
|
+
packageDir = join(tempDir, dirs[0]);
|
|
7103
|
+
}
|
|
7104
|
+
else if (dirs.length === 0) {
|
|
7105
|
+
throw new Error(`Tarball extraction produced no directory in ${tempDir}`);
|
|
7106
|
+
}
|
|
7107
|
+
else {
|
|
7108
|
+
// Multiple dirs — pick the first non-hidden one
|
|
7109
|
+
packageDir = join(tempDir, dirs.find(d => !d.startsWith('.')) || dirs[0]);
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
6587
7112
|
// Run full HMA scan + NanoMind (same pipeline as `secure`)
|
|
6588
7113
|
const scanner = new index_1.HardeningScanner();
|
|
6589
7114
|
const result = await scanner.scan({ targetDir: packageDir, autoFix: false });
|
|
@@ -6599,11 +7124,11 @@ async function checkNpmPackage(name, options) {
|
|
|
6599
7124
|
catch {
|
|
6600
7125
|
// NanoMind unavailable — use base scan results
|
|
6601
7126
|
}
|
|
7127
|
+
// Filter local-dev-only findings irrelevant to downloaded packages
|
|
7128
|
+
filterLocalOnlyFindings(result, scanner);
|
|
6602
7129
|
const failed = result.findings.filter(f => !f.passed);
|
|
6603
7130
|
const critical = failed.filter(f => f.severity === 'critical');
|
|
6604
7131
|
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
7132
|
if (options.json) {
|
|
6608
7133
|
writeJsonStdout({
|
|
6609
7134
|
name,
|
|
@@ -6616,19 +7141,20 @@ async function checkNpmPackage(name, options) {
|
|
|
6616
7141
|
});
|
|
6617
7142
|
return;
|
|
6618
7143
|
}
|
|
6619
|
-
//
|
|
6620
|
-
const
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
7144
|
+
// Await registry data (started in parallel with download)
|
|
7145
|
+
const registryData = await registryPromise;
|
|
7146
|
+
// Display results using unified display
|
|
7147
|
+
displayUnifiedCheck({
|
|
7148
|
+
name,
|
|
7149
|
+
projectType: result.projectType,
|
|
7150
|
+
localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
|
|
7151
|
+
registry: registryData,
|
|
7152
|
+
verbose: !!options.verbose,
|
|
7153
|
+
});
|
|
7154
|
+
// Community contribution (after 3 scans, interactive only)
|
|
6628
7155
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6629
7156
|
const scanCount = incrementScanCounter();
|
|
6630
7157
|
if (scanCount >= 3 && !hasContributeChoice()) {
|
|
6631
|
-
console.log();
|
|
6632
7158
|
console.log(` ${colors.dim}Your scans help other developers make safer choices.`);
|
|
6633
7159
|
console.log(` Sharing adds anonymized results to the OpenA2A trust registry`);
|
|
6634
7160
|
console.log(` so others can check packages before installing.${RESET()}`);
|
|
@@ -6643,10 +7169,9 @@ async function checkNpmPackage(name, options) {
|
|
|
6643
7169
|
if (wantsToShare) {
|
|
6644
7170
|
const ok = await publishToRegistry(name, result);
|
|
6645
7171
|
if (ok) {
|
|
6646
|
-
console.error(
|
|
7172
|
+
console.error(`\n ${colors.green}Thanks for sharing! Future scans will auto-contribute.${RESET()}\n`);
|
|
6647
7173
|
}
|
|
6648
7174
|
else {
|
|
6649
|
-
// Silent — queue locally and retry on next scan
|
|
6650
7175
|
queuePendingScan(name, result);
|
|
6651
7176
|
}
|
|
6652
7177
|
}
|
|
@@ -6659,7 +7184,6 @@ async function checkNpmPackage(name, options) {
|
|
|
6659
7184
|
queuePendingScan(name, result);
|
|
6660
7185
|
}
|
|
6661
7186
|
}
|
|
6662
|
-
printCheckNextSteps(name);
|
|
6663
7187
|
if (critical.length > 0 || high.length > 0)
|
|
6664
7188
|
process.exit(1);
|
|
6665
7189
|
}
|
|
@@ -6667,21 +7191,10 @@ async function checkNpmPackage(name, options) {
|
|
|
6667
7191
|
const message = err instanceof Error ? err.message : String(err);
|
|
6668
7192
|
// Clean npm error messages
|
|
6669
7193
|
if (message.includes('404') || message.includes('Not Found')) {
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
if (suggestions.length > 0) {
|
|
6675
|
-
console.error(`\nDid you mean?`);
|
|
6676
|
-
for (const s of suggestions) {
|
|
6677
|
-
console.error(` ${s}`);
|
|
6678
|
-
}
|
|
6679
|
-
console.error();
|
|
6680
|
-
}
|
|
6681
|
-
}
|
|
6682
|
-
catch {
|
|
6683
|
-
// Search failed — just show the original error
|
|
6684
|
-
}
|
|
7194
|
+
// Throw a typed error so the router can fall through to skill check
|
|
7195
|
+
const notFound = new Error(`NPM_NOT_FOUND:${name}`);
|
|
7196
|
+
notFound.name = 'NpmNotFoundError';
|
|
7197
|
+
throw notFound;
|
|
6685
7198
|
}
|
|
6686
7199
|
else {
|
|
6687
7200
|
console.error(`Error: ${message}`);
|