hackmyagent 0.15.7 → 0.16.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.
Files changed (47) hide show
  1. package/dist/.integrity-manifest.json +1 -1
  2. package/dist/arp/intelligence/nanomind-l1.d.ts +30 -0
  3. package/dist/arp/intelligence/nanomind-l1.d.ts.map +1 -1
  4. package/dist/arp/intelligence/nanomind-l1.js +115 -0
  5. package/dist/arp/intelligence/nanomind-l1.js.map +1 -1
  6. package/dist/cli.js +452 -244
  7. package/dist/cli.js.map +1 -1
  8. package/dist/hardening/scanner.d.ts.map +1 -1
  9. package/dist/hardening/scanner.js +125 -4
  10. package/dist/hardening/scanner.js.map +1 -1
  11. package/dist/hardening/taxonomy.d.ts +2 -0
  12. package/dist/hardening/taxonomy.d.ts.map +1 -1
  13. package/dist/hardening/taxonomy.js +5 -0
  14. package/dist/hardening/taxonomy.js.map +1 -1
  15. package/dist/nanomind-core/analyzers/stego-analyzer.d.ts +30 -0
  16. package/dist/nanomind-core/analyzers/stego-analyzer.d.ts.map +1 -0
  17. package/dist/nanomind-core/analyzers/stego-analyzer.js +533 -0
  18. package/dist/nanomind-core/analyzers/stego-analyzer.js.map +1 -0
  19. package/dist/nanomind-core/daemon-lifecycle.d.ts +28 -0
  20. package/dist/nanomind-core/daemon-lifecycle.d.ts.map +1 -0
  21. package/dist/nanomind-core/daemon-lifecycle.js +142 -0
  22. package/dist/nanomind-core/daemon-lifecycle.js.map +1 -0
  23. package/dist/nanomind-core/inference/tme-classifier.d.ts +3 -2
  24. package/dist/nanomind-core/inference/tme-classifier.d.ts.map +1 -1
  25. package/dist/nanomind-core/inference/tme-classifier.js +26 -16
  26. package/dist/nanomind-core/inference/tme-classifier.js.map +1 -1
  27. package/dist/nanomind-core/orchestrate.d.ts.map +1 -1
  28. package/dist/nanomind-core/orchestrate.js +11 -1
  29. package/dist/nanomind-core/orchestrate.js.map +1 -1
  30. package/dist/nanomind-core/scanner-bridge.d.ts.map +1 -1
  31. package/dist/nanomind-core/scanner-bridge.js +6 -0
  32. package/dist/nanomind-core/scanner-bridge.js.map +1 -1
  33. package/dist/plugins/credvault.d.ts.map +1 -1
  34. package/dist/plugins/credvault.js +25 -0
  35. package/dist/plugins/credvault.js.map +1 -1
  36. package/dist/semantic/nanomind-enhancer.d.ts.map +1 -1
  37. package/dist/semantic/nanomind-enhancer.js +206 -0
  38. package/dist/semantic/nanomind-enhancer.js.map +1 -1
  39. package/dist/telemetry/nanomind-feedback.d.ts +43 -0
  40. package/dist/telemetry/nanomind-feedback.d.ts.map +1 -0
  41. package/dist/telemetry/nanomind-feedback.js +104 -0
  42. package/dist/telemetry/nanomind-feedback.js.map +1 -0
  43. package/dist/telemetry/nanomind-telemetry.d.ts +48 -0
  44. package/dist/telemetry/nanomind-telemetry.d.ts.map +1 -0
  45. package/dist/telemetry/nanomind-telemetry.js +123 -0
  46. package/dist/telemetry/nanomind-telemetry.js.map +1 -0
  47. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -42,9 +42,11 @@ const commander_1 = require("commander");
42
42
  const index_1 = require("./index");
43
43
  const resolve_mcp_1 = require("./resolve-mcp");
44
44
  const wild_1 = require("./wild");
45
- const nemoclaw_scanner_1 = require("./hardening/nemoclaw-scanner");
46
45
  const program = new commander_1.Command();
47
46
  program.showHelpAfterError('(run with --help for usage)');
47
+ // Total security check count across all scanner modules.
48
+ // Update when adding new checks (verify with: grep -r "checkId:" src/hardening/ | grep -o "checkId: '[^']*'" | sort -u | wc -l)
49
+ const CHECK_COUNT = 208;
48
50
  // Write a string to stdout synchronously with retry for pipe backpressure.
49
51
  // process.stdout.write() is async and gets truncated when process.exit()
50
52
  // runs before the stream flushes. fs.writeFileSync(1, ...) can fail with
@@ -126,20 +128,19 @@ program
126
128
  .name('hackmyagent')
127
129
  .description(`Find it. Break it. Fix it.
128
130
 
129
- The hacker's toolkit for AI agents. 204 security checks, 115 attack
131
+ The hacker's toolkit for AI agents. ${CHECK_COUNT} security checks, ${index_1.PAYLOAD_STATS.total} attack
130
132
  payloads, auto-fix with rollback, and OASB benchmark compliance.
131
133
 
132
134
  Documentation: https://hackmyagent.com/docs
133
135
 
134
136
  Updates (v${index_1.VERSION}):
135
- - NemoClaw sandbox scanner (28 installation checks)
136
137
  - 10 new static analysis patterns (NEMO series)
137
138
  - Community trust contributions
138
- - 204 checks across 60 categories
139
+ - ${CHECK_COUNT} checks across 60 categories
139
140
 
140
141
  Examples:
141
- $ hackmyagent secure Find vulnerabilities (204 checks)
142
- $ hackmyagent attack --local Break it with 115 attack payloads
142
+ $ hackmyagent secure Find vulnerabilities (${CHECK_COUNT} checks)
143
+ $ hackmyagent attack --local Break it with ${index_1.PAYLOAD_STATS.total} attack payloads
143
144
  $ hackmyagent secure --fix Fix issues automatically
144
145
  $ hackmyagent fix-all Run all security plugins
145
146
  $ hackmyagent scan example.com Scan external infrastructure`)
@@ -147,7 +148,7 @@ Examples:
147
148
  .option('--no-color', 'Disable colored output (also respects NO_COLOR env)');
148
149
  program.addHelpText('beforeAll', `
149
150
  Quick start:
150
- $ hackmyagent secure Scan current directory (204 checks)
151
+ $ hackmyagent secure Scan current directory (${CHECK_COUNT} checks)
151
152
  $ hackmyagent fix-all --with-aim Auto-fix + create agent identity
152
153
  $ hackmyagent attack Red-team your agent
153
154
  `);
@@ -170,7 +171,7 @@ program
170
171
  .description(`Check if a package, repo, or skill is safe
171
172
 
172
173
  Accepts npm packages, GitHub repos, local paths, or skill identifiers:
173
- • npm package: downloads and runs full security analysis (204 checks + NanoMind)
174
+ • npm package: downloads and runs full security analysis (${CHECK_COUNT} checks + NanoMind)
174
175
  • GitHub repo: shallow clones and runs full security analysis
175
176
  • Local path: runs NanoMind semantic analysis
176
177
  • Skill identifier: verifies publisher, permissions, revocation
@@ -185,8 +186,12 @@ Examples:
185
186
  $ hackmyagent check https://github.com/punkpeye/awesome-mcp-servers
186
187
  $ hackmyagent check ./my-agent/
187
188
  $ hackmyagent check @publisher/skill --verbose
188
- $ hackmyagent check modelcontextprotocol/servers --json`)
189
- .argument('<target>', 'npm package name, local path, or skill identifier')
189
+ $ hackmyagent check pip:requests
190
+ $ hackmyagent check pypi:flask
191
+ $ hackmyagent check modelcontextprotocol/servers --json
192
+ $ hackmyagent check https://gitlab.com/org/repo
193
+ $ hackmyagent check https://example.com/agent-v1.tar.gz`)
194
+ .argument('<target>', 'npm package, PyPI package (pip: or pypi: prefix), local path, GitHub repo, or skill identifier')
190
195
  .option('-v, --verbose', 'Show detailed verification info')
191
196
  .option('--json', 'Output as JSON (for scripting/CI)')
192
197
  .option('--offline', 'Skip DNS verification (offline mode)')
@@ -244,11 +249,21 @@ Examples:
244
249
  process.exit(1);
245
250
  return;
246
251
  }
252
+ // PyPI package: download, run full HMA scan, clean up
253
+ if (looksLikePyPiPackage(skill)) {
254
+ await checkPyPiPackage(skill, options);
255
+ return;
256
+ }
247
257
  // GitHub repo: clone, run full HMA scan, clean up
248
258
  if (looksLikeGitHubRepo(skill)) {
249
259
  await checkGitHubRepo(skill, options);
250
260
  return;
251
261
  }
262
+ // Raw URL (non-GitHub): fetch/clone based on content type
263
+ if (looksLikeRawUrl(skill)) {
264
+ await checkRawUrl(skill, options);
265
+ return;
266
+ }
252
267
  // npm package name: download, run full HMA scan, clean up
253
268
  if (looksLikeNpmPackage(skill)) {
254
269
  await checkNpmPackage(skill, options);
@@ -343,6 +358,43 @@ const SEVERITY_DISPLAY = {
343
358
  medium: { symbol: '[~]', color: () => colors.yellow },
344
359
  low: { symbol: '[.]', color: () => colors.green },
345
360
  };
361
+ /**
362
+ * Display check command findings with optional verbose details.
363
+ * When verbose is true, shows checkId, category, file location, and fix/guidance for each finding.
364
+ */
365
+ function displayCheckFindings(failed, verbose) {
366
+ if (failed.length > 0) {
367
+ console.log();
368
+ const limit = verbose ? failed.length : 15;
369
+ for (const f of failed.slice(0, limit)) {
370
+ const sev = SEVERITY_DISPLAY[f.severity];
371
+ const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
372
+ console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
373
+ if (verbose) {
374
+ console.log(` ${colors.dim}Check: ${f.checkId}${RESET()}`);
375
+ if (f.category) {
376
+ console.log(` ${colors.dim}Category: ${f.category}${RESET()}`);
377
+ }
378
+ if (f.file) {
379
+ const location = f.line ? `${f.file}:${f.line}` : f.file;
380
+ console.log(` ${colors.dim}File: ${location}${RESET()}`);
381
+ }
382
+ if (f.fix) {
383
+ console.log(` ${colors.cyan}Fix: ${f.fix}${RESET()}`);
384
+ }
385
+ if (f.guidance) {
386
+ console.log(` ${colors.dim}Guidance: ${f.guidance}${RESET()}`);
387
+ }
388
+ }
389
+ }
390
+ if (failed.length > limit) {
391
+ console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
392
+ }
393
+ }
394
+ else {
395
+ console.log(`\n ${colors.green}No security issues found.${RESET()}`);
396
+ }
397
+ }
346
398
  function groupFindingsBySeverity(findings) {
347
399
  const grouped = {
348
400
  critical: [],
@@ -1774,7 +1826,7 @@ program
1774
1826
  .command('secure')
1775
1827
  .description(`Scan and harden your agent setup
1776
1828
 
1777
- Performs 204 security checks across 60 categories:
1829
+ Performs ${CHECK_COUNT} security checks across 60 categories:
1778
1830
  • Credentials: API key exposure, secrets in configs
1779
1831
  • MCP: Server configs, tool permissions, secrets
1780
1832
  • Network: TLS, interface bindings, CORS
@@ -2776,188 +2828,6 @@ Examples:
2776
2828
  process.exit(1);
2777
2829
  }
2778
2830
  });
2779
- // NemoClaw-specific helpers
2780
- const NEMOCLAW_CHECK_CATEGORIES = nemoclaw_scanner_1.NEMOCLAW_CATEGORIES;
2781
- function detectNemoClawDirectory(providedDir) {
2782
- const os = require('os');
2783
- const fs = require('fs');
2784
- const path = require('path');
2785
- if (providedDir && providedDir !== '') {
2786
- return providedDir.startsWith('/') ? providedDir : path.join(process.cwd(), providedDir);
2787
- }
2788
- const homeDir = os.homedir();
2789
- const candidates = [
2790
- path.join(homeDir, '.nemoclaw'),
2791
- path.join(homeDir, '.openshell'),
2792
- path.join(homeDir, '.openclaw'),
2793
- ];
2794
- for (const candidate of candidates) {
2795
- if (fs.existsSync(candidate)) {
2796
- return candidate;
2797
- }
2798
- }
2799
- return process.cwd();
2800
- }
2801
- function filterNemoClawFindings(findings) {
2802
- return findings.filter((f) => {
2803
- const checkId = f.checkId.toUpperCase();
2804
- return checkId.startsWith('HMA-NMC-');
2805
- });
2806
- }
2807
- function assessNemoClawRiskLevel(findings) {
2808
- const criticalCount = findings.filter((f) => f.severity === 'critical').length;
2809
- const highCount = findings.filter((f) => f.severity === 'high').length;
2810
- const mediumCount = findings.filter((f) => f.severity === 'medium').length;
2811
- if (criticalCount > 0) {
2812
- return {
2813
- level: 'Critical',
2814
- color: colors.brightRed,
2815
- description: `${criticalCount} critical finding(s) with recommended fixes available.`,
2816
- };
2817
- }
2818
- if (highCount > 0) {
2819
- return {
2820
- level: 'High',
2821
- color: colors.red,
2822
- description: `${highCount} high-severity finding(s) detected. Fixes available below.`,
2823
- };
2824
- }
2825
- if (mediumCount > 0) {
2826
- return {
2827
- level: 'Moderate',
2828
- color: colors.yellow,
2829
- description: 'Some findings detected. Review the recommendations below.',
2830
- };
2831
- }
2832
- if (findings.length === 0) {
2833
- return {
2834
- level: 'None',
2835
- color: colors.dim,
2836
- description: `No NemoClaw installation detected. Run \`${CLI_PREFIX} secure\` for a full scan.`,
2837
- };
2838
- }
2839
- return {
2840
- level: 'Low',
2841
- color: colors.green,
2842
- description: 'No critical or high findings detected.',
2843
- };
2844
- }
2845
- program
2846
- .command('secure-nemoclaw')
2847
- .description(`Security scan for NVIDIA NemoClaw installations
2848
-
2849
- Performs focused security checks for NemoClaw sandbox deployments:
2850
- - Secrets: NVIDIA API key exposure in configs, logs, Docker, shell history
2851
- - Network: Gateway/k3s/inference port binding, Docker socket, egress policies
2852
- - Skills: Blueprint integrity, skill verification, directory permissions
2853
- - Process: Sandbox privileges, seccomp/Landlock enforcement, root execution
2854
- - OpenClaw layer: Inherited misconfigs that survive NemoClaw sandboxing
2855
-
2856
- Auto-detects ~/.nemoclaw, ~/.openshell, or ~/.openclaw directories.
2857
- Exit code 1 if critical/high issues found.
2858
-
2859
- Examples:
2860
- $ hackmyagent secure-nemoclaw Scan auto-detected directory
2861
- $ hackmyagent secure-nemoclaw ~/.nemoclaw Scan specific directory
2862
- $ hackmyagent secure-nemoclaw --json JSON output for CI`)
2863
- .argument('[directory]', 'Directory to scan (default: ~/.nemoclaw or ~/.openshell)', '')
2864
- .option('--json', 'Output as JSON (for scripting/CI)')
2865
- .option('-v, --verbose', 'Show all checks including passed ones')
2866
- .action(async (directory, options) => {
2867
- try {
2868
- const targetDir = detectNemoClawDirectory(directory);
2869
- if (!options.json) {
2870
- console.log(`\nNemoClaw Security Report`);
2871
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
2872
- console.log(`Scanning ${targetDir}...\n`);
2873
- }
2874
- const scanner = new nemoclaw_scanner_1.NemoClawScanner();
2875
- const findings = await scanner.scan(targetDir, {});
2876
- // Enrich with taxonomy
2877
- const { enrichWithTaxonomy } = require('./hardening/taxonomy');
2878
- enrichWithTaxonomy(findings);
2879
- // NanoMind semantic analysis (defense-in-depth)
2880
- let mergedFindings = findings;
2881
- try {
2882
- const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
2883
- const nmResult = await orchestrateNanoMind(targetDir, findings, { silent: !!options.json });
2884
- mergedFindings = nmResult.mergedFindings;
2885
- }
2886
- catch { /* NanoMind unavailable */ }
2887
- const issues = mergedFindings.filter((f) => !f.passed);
2888
- const passedFindings = mergedFindings.filter((f) => f.passed);
2889
- if (options.json) {
2890
- const jsonOutput = {
2891
- target: targetDir,
2892
- riskLevel: assessNemoClawRiskLevel(issues).level,
2893
- totalChecks: mergedFindings.length,
2894
- issues: issues.length,
2895
- passed: passedFindings.length,
2896
- findings: mergedFindings,
2897
- };
2898
- writeJsonStdout(jsonOutput);
2899
- return;
2900
- }
2901
- // Risk assessment
2902
- const risk = assessNemoClawRiskLevel(issues);
2903
- console.log(`Risk Level: ${risk.color}${risk.level}${RESET()}`);
2904
- console.log(`${risk.description}\n`);
2905
- // Summary stats
2906
- console.log(`Checks: ${findings.length} total | ${issues.length} issues | ${passedFindings.length} passed\n`);
2907
- // Show issues
2908
- if (issues.length > 0) {
2909
- console.log(`${colors.red}Findings:${RESET()}\n`);
2910
- for (const finding of issues) {
2911
- const display = SEVERITY_DISPLAY[finding.severity];
2912
- const location = finding.file
2913
- ? finding.line
2914
- ? `${finding.file}:${finding.line}`
2915
- : finding.file
2916
- : '';
2917
- const sevLabel = finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1);
2918
- console.log(`${display.color()}${display.symbol} [${finding.checkId}] ${sevLabel}${RESET()}`);
2919
- console.log(` ${finding.description}`);
2920
- if (location) {
2921
- console.log(` File: ${location}`);
2922
- }
2923
- if (finding.fix) {
2924
- console.log(` ${colors.cyan}Recommended fix:${RESET()} ${finding.fix}`);
2925
- }
2926
- console.log();
2927
- }
2928
- }
2929
- else {
2930
- console.log(`${colors.green}No NemoClaw-specific issues found.${RESET()}\n`);
2931
- }
2932
- // Show passed checks in verbose mode
2933
- if (options.verbose && passedFindings.length > 0) {
2934
- console.log(`${colors.green}Passed Checks:${RESET()}`);
2935
- for (const finding of passedFindings) {
2936
- console.log(` ${colors.green}[ok]${RESET()} [${finding.checkId}] ${finding.name}`);
2937
- }
2938
- console.log();
2939
- }
2940
- // Shodan self-check guidance
2941
- if (issues.some((f) => f.category === 'network')) {
2942
- console.log(`${colors.yellow}Internet Exposure Check:${RESET()}`);
2943
- console.log(` Check if your instance is visible on Shodan:`);
2944
- console.log(` https://www.shodan.io/host/<YOUR-IP>`);
2945
- console.log(` Known NemoClaw dorks: port:18789, port:6443 ssl.cert.subject.cn:"k3s-serving"\n`);
2946
- }
2947
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
2948
- console.log(`Run '${CLI_PREFIX} secure-openclaw' for OpenClaw-specific checks.`);
2949
- console.log(`Run '${CLI_PREFIX} secure' for a full security scan.\n`);
2950
- // Exit with non-zero if critical/high issues remain
2951
- const criticalOrHigh = issues.filter((f) => f.severity === 'critical' || f.severity === 'high');
2952
- if (criticalOrHigh.length > 0) {
2953
- process.exit(1);
2954
- }
2955
- }
2956
- catch (error) {
2957
- console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
2958
- process.exit(1);
2959
- }
2960
- });
2961
2831
  program
2962
2832
  .command('scan')
2963
2833
  .description(`Scan external target for exposed MCP endpoints
@@ -4534,7 +4404,7 @@ Examples:
4534
4404
  console.log(`\n Detected: ${result.tool}\n`);
4535
4405
  console.log(` Added HackMyAgent MCP server to ${result.configPath}\n`);
4536
4406
  console.log(` Available tools in ${result.tool}:`);
4537
- console.log(` hackmyagent_scan — 204 checks + structural analysis`);
4407
+ console.log(` hackmyagent_scan — ${CHECK_COUNT} checks + structural analysis`);
4538
4408
  console.log(` hackmyagent_deep_scan — Full analysis with LLM reasoning`);
4539
4409
  console.log(` hackmyagent_analyze_file — Analyze a single file`);
4540
4410
  console.log(` hackmyagent_benchmark — OASB-1 compliance assessment\n`);
@@ -5328,26 +5198,44 @@ Examples:
5328
5198
  });
5329
5199
  program
5330
5200
  .command('check-metadata')
5331
- .description('Export metadata for all security checks by scanning test fixtures (JSON)')
5332
- .option('-d, --directory <dir>', 'Directory to scan for check metadata extraction')
5201
+ .description('Export metadata for all security checks (JSON)')
5202
+ .option('-d, --directory <dir>', 'Scan a specific directory to collect check metadata from findings')
5203
+ .option('--json', 'Output as JSON (default)')
5333
5204
  .action(async (options) => {
5334
- const { getAttackClass } = require('./hardening/taxonomy');
5335
- const targetDir = options.directory || process.cwd();
5336
- // Run a real scan to collect all check metadata from findings
5337
- const scanner = new index_1.HardeningScanner();
5338
- const result = await scanner.scan({ targetDir, autoFix: false, scanDepth: 'deep' });
5205
+ const { getAttackClass, getTaxonomyMap } = require('./hardening/taxonomy');
5206
+ // Build static registry from taxonomy map (covers all known checks)
5207
+ const taxMap = getTaxonomyMap();
5339
5208
  const metadata = {};
5340
- for (const finding of result.findings) {
5341
- if (!metadata[finding.checkId]) {
5342
- metadata[finding.checkId] = {
5343
- checkId: finding.checkId,
5344
- name: finding.name,
5345
- category: finding.category,
5346
- attackClass: getAttackClass(finding.checkId) || '',
5347
- severity: finding.severity,
5348
- fix: finding.fix || '',
5349
- guidance: finding.guidance || '',
5350
- };
5209
+ // Add all checks from taxonomy (the authoritative source of check IDs)
5210
+ for (const checkId of Object.keys(taxMap)) {
5211
+ const prefix = checkId.split('-').slice(0, -1).join('-') || checkId.split('-')[0];
5212
+ metadata[checkId] = {
5213
+ checkId,
5214
+ name: checkId,
5215
+ category: prefix.toLowerCase(),
5216
+ attackClass: taxMap[checkId] || '',
5217
+ severity: '',
5218
+ };
5219
+ }
5220
+ // If a directory is provided, enrich with actual finding data (names, severity, etc.)
5221
+ if (options.directory) {
5222
+ const scanner = new index_1.HardeningScanner();
5223
+ const result = await scanner.scan({ targetDir: options.directory, autoFix: false, scanDepth: 'deep' });
5224
+ for (const finding of result.findings) {
5225
+ if (metadata[finding.checkId]) {
5226
+ metadata[finding.checkId].name = finding.name;
5227
+ metadata[finding.checkId].category = finding.category;
5228
+ metadata[finding.checkId].severity = finding.severity;
5229
+ }
5230
+ else {
5231
+ metadata[finding.checkId] = {
5232
+ checkId: finding.checkId,
5233
+ name: finding.name,
5234
+ category: finding.category,
5235
+ attackClass: getAttackClass(finding.checkId) || '',
5236
+ severity: finding.severity,
5237
+ };
5238
+ }
5351
5239
  }
5352
5240
  }
5353
5241
  writeJsonStdout({ totalChecks: Object.keys(metadata).length, checks: metadata });
@@ -5373,12 +5261,39 @@ program
5373
5261
  // Fallback: static explanation from check metadata
5374
5262
  const checkId = findingId.toUpperCase();
5375
5263
  const staticExplanations = {
5264
+ // Credential checks
5376
5265
  'CRED-001': 'Hardcoded credential detected. API keys, tokens, or passwords are embedded directly in source code. Replace with environment variable references ($VAR_NAME) and rotate the exposed credential immediately.',
5377
5266
  'CRED-002': 'OpenAI API key pattern detected (sk-...). Move to environment variable OPENAI_API_KEY.',
5378
5267
  'CRED-003': 'Anthropic API key pattern detected (sk-ant-...). Move to environment variable ANTHROPIC_API_KEY.',
5379
5268
  'CRED-004': 'AWS credential pattern detected. Use AWS SDK credential chain or environment variables.',
5380
- 'MCP-001': 'MCP server running without TLS. Agent-to-server communication is unencrypted.',
5381
- 'SKILL-005': 'External endpoint in skill capability declaration. Verify the endpoint is trusted.',
5269
+ // MCP checks
5270
+ 'MCP-001': 'MCP server running without TLS. Agent-to-server communication is unencrypted. Enable TLS on the MCP server or use a reverse proxy with TLS termination.',
5271
+ // Skill checks
5272
+ 'SKILL-005': 'External endpoint in skill capability declaration. Verify the endpoint is trusted and uses HTTPS.',
5273
+ // Governance checks
5274
+ 'GOV-001': 'No governance policy found. Agents should declare behavioral constraints in a SOUL.md or governance file. Create a SOUL.md with mission, boundaries, and allowed actions.',
5275
+ 'GOV-002': 'Governance file lacks boundary definitions. Without explicit boundaries, the agent may act outside intended scope. Add "boundaries" or "constraints" sections to your governance file.',
5276
+ 'GOV-003': 'Governance file missing escalation policy. Define when and how the agent should escalate to a human. Add an escalation section with trigger conditions and contact methods.',
5277
+ // Permission checks
5278
+ 'PERM-001': 'Overly broad file system permissions detected. The agent has write access to directories outside its working scope. Restrict file permissions to the minimum required paths.',
5279
+ 'PERM-002': 'Network permissions not restricted. The agent can make outbound requests to any host. Define an allowlist of permitted domains in the agent configuration.',
5280
+ 'PERM-003': 'Execution permissions too permissive. The agent can spawn arbitrary processes. Restrict executable permissions to specific, required binaries only.',
5281
+ // SOUL checks
5282
+ 'SOUL-001': 'No SOUL.md file found. SOUL.md defines the agent identity, mission, and behavioral constraints. Run `hackmyagent secure --fix` to generate one.',
5283
+ 'SOUL-002': 'SOUL.md missing identity section. The agent lacks a declared identity, making impersonation easier. Add name, version, and publisher fields.',
5284
+ 'SOUL-003': 'SOUL.md missing behavioral boundaries. Without explicit limits, the agent may perform unintended actions. Add a boundaries section listing prohibited behaviors.',
5285
+ // Privacy checks
5286
+ 'PRIV-001': 'PII handling not declared. The agent processes data but has no privacy policy or data handling declaration. Add a data handling section specifying what data is collected, stored, and shared.',
5287
+ // Data checks
5288
+ 'DATA-001': 'Sensitive data logged to console or file. Credentials, tokens, or PII appear in log output. Sanitize log statements to redact sensitive values before output.',
5289
+ 'DATA-002': 'Data retention policy missing. The agent stores data without a defined retention or deletion policy. Define how long data is kept and when it is purged.',
5290
+ // Injection checks
5291
+ 'INJECT-001': 'No prompt injection defense detected. The agent does not validate or sanitize inputs against injection attacks. Add input validation and consider using a system prompt with injection resistance instructions.',
5292
+ 'INJECT-002': 'Indirect prompt injection surface found. External data (URLs, files, API responses) is passed to the LLM without sanitization. Sanitize or sandbox external content before including it in prompts.',
5293
+ // Attestation checks
5294
+ 'ATTEST-001': 'No attestation mechanism found. The agent cannot prove its identity or integrity to other agents. Implement agent attestation using signed identity tokens or SOUL.md signatures.',
5295
+ // Supply chain checks
5296
+ 'SUPPLY-001': 'Dependency with known vulnerability detected. A transitive or direct dependency has a published CVE. Update the affected package to a patched version.',
5382
5297
  };
5383
5298
  const explanation = staticExplanations[checkId];
5384
5299
  if (explanation) {
@@ -5754,6 +5669,18 @@ program
5754
5669
  // ============================================================================
5755
5670
  // npm package scanning helpers (used by `check <package>`)
5756
5671
  // ============================================================================
5672
+ /**
5673
+ * Detect whether a string looks like a PyPI package reference.
5674
+ *
5675
+ * Requires an explicit prefix:
5676
+ * - pip:package-name
5677
+ * - pypi:package-name
5678
+ *
5679
+ * Bare names are NOT auto-detected as PyPI (they fall through to npm).
5680
+ */
5681
+ function looksLikePyPiPackage(target) {
5682
+ return target.startsWith('pip:') || target.startsWith('pypi:');
5683
+ }
5757
5684
  /**
5758
5685
  * Detect whether a string looks like an npm package name rather than
5759
5686
  * a hostname, IP address, or local path.
@@ -5804,6 +5731,16 @@ function looksLikeGitHubRepo(target) {
5804
5731
  }
5805
5732
  return false;
5806
5733
  }
5734
+ /**
5735
+ * Detect whether a string is an HTTP(S) URL that is NOT a GitHub repo.
5736
+ * GitHub URLs are handled by looksLikeGitHubRepo; this catches everything else:
5737
+ * GitLab, Bitbucket, self-hosted git, raw tarballs, zip archives, single files, etc.
5738
+ */
5739
+ function looksLikeRawUrl(target) {
5740
+ if (looksLikeGitHubRepo(target))
5741
+ return false;
5742
+ return /^https?:\/\/.+/.test(target);
5743
+ }
5807
5744
  /**
5808
5745
  * Parse a GitHub target into org/repo and optional clone URL.
5809
5746
  * Returns { org, repo, cloneUrl }
@@ -6124,21 +6061,7 @@ async function checkGitHubRepo(target, options) {
6124
6061
  console.log(` Type: ${result.projectType}`);
6125
6062
  console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
6126
6063
  console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
6127
- if (failed.length > 0) {
6128
- console.log();
6129
- const limit = options.verbose ? failed.length : 15;
6130
- for (const f of failed.slice(0, limit)) {
6131
- const sev = SEVERITY_DISPLAY[f.severity];
6132
- const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
6133
- console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
6134
- }
6135
- if (failed.length > limit) {
6136
- console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
6137
- }
6138
- }
6139
- else {
6140
- console.log(`\n ${colors.green}No security issues found.${RESET()}`);
6141
- }
6064
+ displayCheckFindings(failed, !!options.verbose);
6142
6065
  // Step 3: Community contribution
6143
6066
  if (process.stdin.isTTY && !globalCiMode) {
6144
6067
  const scanCount = incrementScanCounter();
@@ -6202,6 +6125,305 @@ async function checkGitHubRepo(target, options) {
6202
6125
  * Download an npm package, run full HMA secure scan, display results, clean up.
6203
6126
  * Checks the registry first; only downloads if data is missing or stale.
6204
6127
  */
6128
+ /**
6129
+ * Download a PyPI package, scan it with HMA + NanoMind, and display results.
6130
+ * Accepts targets prefixed with pip: or pypi: (e.g. pip:requests, pypi:flask).
6131
+ */
6132
+ async function checkPyPiPackage(target, options) {
6133
+ // Strip prefix to get the bare package name
6134
+ const name = target.replace(/^(pip|pypi):/, '');
6135
+ const { mkdtemp, rm, readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
6136
+ const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
6137
+ const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
6138
+ const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
6139
+ if (!options.json && !globalCiMode) {
6140
+ console.error(`Downloading ${name} from PyPI...`);
6141
+ }
6142
+ const tempDir = await mkdtemp(join(tmpdir(), 'hma-check-pypi-'));
6143
+ try {
6144
+ // Fetch package metadata from PyPI JSON API
6145
+ const metaRes = await fetch(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`);
6146
+ if (!metaRes.ok) {
6147
+ if (metaRes.status === 404) {
6148
+ console.error(`Error: Package "${name}" not found on PyPI.`);
6149
+ }
6150
+ else {
6151
+ console.error(`Error: PyPI API returned ${metaRes.status} for "${name}".`);
6152
+ }
6153
+ process.exit(1);
6154
+ }
6155
+ const meta = await metaRes.json();
6156
+ // Prefer sdist (source tarball) for scanning; fall back to first wheel
6157
+ const sdist = meta.urls.find((u) => u.packagetype === 'sdist');
6158
+ const wheel = meta.urls.find((u) => u.packagetype === 'bdist_wheel');
6159
+ const dist = sdist || wheel || meta.urls[0];
6160
+ if (!dist) {
6161
+ console.error(`Error: No downloadable distribution found for "${name}" on PyPI.`);
6162
+ process.exit(1);
6163
+ }
6164
+ // Download the archive
6165
+ const archiveRes = await fetch(dist.url);
6166
+ if (!archiveRes.ok) {
6167
+ throw new Error(`Failed to download ${dist.filename}: HTTP ${archiveRes.status}`);
6168
+ }
6169
+ const archiveBuffer = Buffer.from(await archiveRes.arrayBuffer());
6170
+ const archivePath = join(tempDir, dist.filename);
6171
+ const { writeFileSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
6172
+ writeFileSync(archivePath, archiveBuffer);
6173
+ // Extract
6174
+ const extractDir = join(tempDir, 'package');
6175
+ const { mkdirSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
6176
+ mkdirSync(extractDir, { recursive: true });
6177
+ if (dist.filename.endsWith('.tar.gz') || dist.filename.endsWith('.tgz')) {
6178
+ execFileSync('tar', ['xzf', archivePath, '-C', extractDir, '--strip-components=1'], { timeout: 30000 });
6179
+ }
6180
+ else if (dist.filename.endsWith('.zip') || dist.filename.endsWith('.whl')) {
6181
+ execFileSync('unzip', ['-q', '-o', archivePath, '-d', extractDir], { timeout: 30000 });
6182
+ }
6183
+ else {
6184
+ throw new Error(`Unsupported archive format: ${dist.filename}`);
6185
+ }
6186
+ // Run full HMA scan + NanoMind (same pipeline as checkNpmPackage)
6187
+ const scanner = new index_1.HardeningScanner();
6188
+ const result = await scanner.scan({ targetDir: extractDir, autoFix: false });
6189
+ // Run NanoMind semantic analysis and re-filter
6190
+ try {
6191
+ const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6192
+ const nmResult = await orchestrateNanoMind(extractDir, result.findings, { silent: true });
6193
+ const refiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, extractDir);
6194
+ const projectType = result.projectType || 'library';
6195
+ result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6196
+ result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
6197
+ }
6198
+ catch {
6199
+ // NanoMind unavailable -- use base scan results
6200
+ }
6201
+ const failed = result.findings.filter(f => !f.passed);
6202
+ const critical = failed.filter(f => f.severity === 'critical');
6203
+ const high = failed.filter(f => f.severity === 'high');
6204
+ const medium = failed.filter(f => f.severity === 'medium');
6205
+ const low = failed.filter(f => f.severity === 'low');
6206
+ if (options.json) {
6207
+ writeJsonStdout({
6208
+ name,
6209
+ type: 'pypi-package',
6210
+ source: 'local-scan',
6211
+ version: meta.info.version,
6212
+ projectType: result.projectType,
6213
+ score: result.score,
6214
+ maxScore: result.maxScore,
6215
+ findings: result.findings,
6216
+ });
6217
+ return;
6218
+ }
6219
+ // Display results
6220
+ const scoreRatio = result.score / result.maxScore;
6221
+ const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
6222
+ console.log(`\n ${name} (PyPI)`);
6223
+ console.log(` Version: ${meta.info.version}`);
6224
+ console.log(` Type: ${result.projectType}`);
6225
+ console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
6226
+ console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
6227
+ if (failed.length > 0) {
6228
+ console.log();
6229
+ const limit = options.verbose ? failed.length : 15;
6230
+ for (const f of failed.slice(0, limit)) {
6231
+ const sev = SEVERITY_DISPLAY[f.severity];
6232
+ const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
6233
+ console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
6234
+ }
6235
+ if (failed.length > limit) {
6236
+ console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
6237
+ }
6238
+ }
6239
+ else {
6240
+ console.log(`\n ${colors.green}No security issues found.${RESET()}`);
6241
+ }
6242
+ console.log(`\n Full project scan: ${CLI_PREFIX} secure <dir>`);
6243
+ console.log();
6244
+ if (critical.length > 0 || high.length > 0)
6245
+ process.exit(1);
6246
+ }
6247
+ catch (err) {
6248
+ const message = err instanceof Error ? err.message : String(err);
6249
+ if (message.includes('not found on PyPI')) {
6250
+ console.error(`Error: ${message}`);
6251
+ }
6252
+ else {
6253
+ console.error(`Error scanning PyPI package "${name}": ${message}`);
6254
+ }
6255
+ process.exit(1);
6256
+ }
6257
+ finally {
6258
+ await rm(tempDir, { recursive: true, force: true });
6259
+ }
6260
+ }
6261
+ /**
6262
+ * Fetch a raw URL, detect its type (git repo, tarball, zip, or single file),
6263
+ * download to a temp dir, run full HMA + NanoMind scan, display results, clean up.
6264
+ */
6265
+ async function checkRawUrl(url, options) {
6266
+ const { mkdtemp, rm, writeFile, readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
6267
+ const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
6268
+ const { join, basename } = await Promise.resolve().then(() => __importStar(require('node:path')));
6269
+ const { execFile } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
6270
+ const { promisify } = await Promise.resolve().then(() => __importStar(require('node:util')));
6271
+ const execAsync = promisify(execFile);
6272
+ const tempDir = await mkdtemp(join(tmpdir(), 'hma-check-url-'));
6273
+ let scanDir = tempDir;
6274
+ let displayName = url;
6275
+ try {
6276
+ // Git clone for known forge URLs and .git suffix
6277
+ const isGitUrl = url.endsWith('.git')
6278
+ || /^https?:\/\/(gitlab\.com|bitbucket\.org|codeberg\.org|gitea\.com|sr\.ht)\//.test(url);
6279
+ if (isGitUrl) {
6280
+ const repoName = basename(url.replace(/\.git$/, '')) || 'repo';
6281
+ displayName = url.replace(/^https?:\/\//, '').replace(/\.git$/, '');
6282
+ if (!options.json && !globalCiMode) {
6283
+ console.error(`Cloning ${displayName}...`);
6284
+ }
6285
+ await execAsync('git', ['clone', '--depth', '1', '--single-branch', url, join(tempDir, repoName)], { timeout: 120000 });
6286
+ scanDir = join(tempDir, repoName);
6287
+ }
6288
+ else {
6289
+ // HTTP fetch — use HEAD to determine content type
6290
+ if (!options.json && !globalCiMode) {
6291
+ console.error(`Fetching ${url}...`);
6292
+ }
6293
+ const headRes = await fetch(url, { method: 'HEAD', redirect: 'follow' });
6294
+ if (!headRes.ok) {
6295
+ console.error(`Error: HTTP ${headRes.status} fetching "${url}".`);
6296
+ process.exit(1);
6297
+ }
6298
+ const contentType = headRes.headers.get('content-type') || '';
6299
+ const finalUrl = headRes.url;
6300
+ const fileName = basename(new URL(finalUrl).pathname) || 'download';
6301
+ const isArchive = /\.(tar\.gz|tgz|tar\.bz2|tar\.xz|zip)$/i.test(fileName)
6302
+ || contentType.includes('gzip')
6303
+ || contentType.includes('tar')
6304
+ || contentType.includes('zip')
6305
+ || contentType.includes('compressed');
6306
+ const bodyRes = await fetch(finalUrl, { redirect: 'follow' });
6307
+ if (!bodyRes.ok || !bodyRes.body) {
6308
+ console.error(`Error: Failed to download "${url}" (HTTP ${bodyRes.status}).`);
6309
+ process.exit(1);
6310
+ }
6311
+ const buffer = Buffer.from(await bodyRes.arrayBuffer());
6312
+ if (isArchive) {
6313
+ const archivePath = join(tempDir, fileName);
6314
+ await writeFile(archivePath, buffer);
6315
+ const extractDir = join(tempDir, 'extracted');
6316
+ await execAsync('mkdir', ['-p', extractDir]);
6317
+ if (/\.(tar\.gz|tgz)$/i.test(fileName) || contentType.includes('gzip') || contentType.includes('tar')) {
6318
+ await execAsync('tar', ['xzf', archivePath, '-C', extractDir], { timeout: 30000 });
6319
+ }
6320
+ else if (/\.tar\.bz2$/i.test(fileName)) {
6321
+ await execAsync('tar', ['xjf', archivePath, '-C', extractDir], { timeout: 30000 });
6322
+ }
6323
+ else if (/\.tar\.xz$/i.test(fileName)) {
6324
+ await execAsync('tar', ['xJf', archivePath, '-C', extractDir], { timeout: 30000 });
6325
+ }
6326
+ else if (/\.zip$/i.test(fileName)) {
6327
+ await execAsync('unzip', ['-q', archivePath, '-d', extractDir], { timeout: 30000 });
6328
+ }
6329
+ // If extraction produced a single directory, scan that
6330
+ const entries = await readdir(extractDir);
6331
+ if (entries.length === 1) {
6332
+ const { statSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
6333
+ const innerPath = join(extractDir, entries[0]);
6334
+ if (statSync(innerPath).isDirectory()) {
6335
+ scanDir = innerPath;
6336
+ }
6337
+ else {
6338
+ scanDir = extractDir;
6339
+ }
6340
+ }
6341
+ else {
6342
+ scanDir = extractDir;
6343
+ }
6344
+ displayName = fileName;
6345
+ }
6346
+ else {
6347
+ // Single file: save for scanning
6348
+ await writeFile(join(tempDir, fileName), buffer);
6349
+ scanDir = tempDir;
6350
+ displayName = fileName;
6351
+ }
6352
+ }
6353
+ // Run full HMA scan + NanoMind
6354
+ const scanner = new index_1.HardeningScanner();
6355
+ const result = await scanner.scan({ targetDir: scanDir, autoFix: false });
6356
+ try {
6357
+ const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6358
+ const nmResult = await orchestrateNanoMind(scanDir, result.findings, { silent: true });
6359
+ const refiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, scanDir);
6360
+ const projectType = result.projectType || 'library';
6361
+ result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6362
+ result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
6363
+ }
6364
+ catch {
6365
+ // NanoMind unavailable — use base scan results
6366
+ }
6367
+ const failed = result.findings.filter(f => !f.passed);
6368
+ const critical = failed.filter(f => f.severity === 'critical');
6369
+ const high = failed.filter(f => f.severity === 'high');
6370
+ const medium = failed.filter(f => f.severity === 'medium');
6371
+ const low = failed.filter(f => f.severity === 'low');
6372
+ if (options.json) {
6373
+ writeJsonStdout({
6374
+ name: displayName,
6375
+ url,
6376
+ type: 'raw-url',
6377
+ source: 'local-scan',
6378
+ projectType: result.projectType,
6379
+ score: result.score,
6380
+ maxScore: result.maxScore,
6381
+ findings: result.findings,
6382
+ });
6383
+ return;
6384
+ }
6385
+ // Display results
6386
+ const scoreRatio = result.score / result.maxScore;
6387
+ const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
6388
+ console.log(`\n ${displayName} ${colors.dim}(URL)${RESET()}`);
6389
+ console.log(` Type: ${result.projectType}`);
6390
+ console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
6391
+ console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
6392
+ displayCheckFindings(failed, !!options.verbose);
6393
+ // Community contribution (auto-share if opted in, no first-time prompt for URLs)
6394
+ if (process.stdin.isTTY && !globalCiMode) {
6395
+ if (isContributeEnabled()) {
6396
+ flushPendingScans();
6397
+ const ok = await publishToRegistry(displayName, result);
6398
+ if (!ok)
6399
+ queuePendingScan(displayName, result);
6400
+ }
6401
+ }
6402
+ console.log(`\n Full project scan: ${CLI_PREFIX} secure <dir>`);
6403
+ console.log();
6404
+ if (critical.length > 0 || high.length > 0)
6405
+ process.exit(1);
6406
+ }
6407
+ catch (err) {
6408
+ const message = err instanceof Error ? err.message : String(err);
6409
+ if (message.includes('128') || message.includes('not found') || message.includes('Repository not found')) {
6410
+ console.error(`Error: Could not clone repository from "${url}".`);
6411
+ console.error(`\nVerify the URL is accessible and contains a git repository.`);
6412
+ }
6413
+ else if (message.includes('timeout') || message.includes('Timeout')) {
6414
+ console.error(`Error: Fetching "${url}" timed out. The target may be too large.`);
6415
+ console.error(`\nTry downloading manually and scanning the local path:`);
6416
+ console.error(` ${CLI_PREFIX} check ./downloaded-dir/`);
6417
+ }
6418
+ else {
6419
+ console.error(`Error scanning URL: ${message}`);
6420
+ }
6421
+ process.exit(1);
6422
+ }
6423
+ finally {
6424
+ await rm(tempDir, { recursive: true, force: true });
6425
+ }
6426
+ }
6205
6427
  async function checkNpmPackage(name, options) {
6206
6428
  // Step 1: Check registry for existing trust data
6207
6429
  if (!options.offline) {
@@ -6279,21 +6501,7 @@ async function checkNpmPackage(name, options) {
6279
6501
  console.log(` Type: ${result.projectType}`);
6280
6502
  console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
6281
6503
  console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
6282
- if (failed.length > 0) {
6283
- console.log();
6284
- const limit = options.verbose ? failed.length : 15;
6285
- for (const f of failed.slice(0, limit)) {
6286
- const sev = SEVERITY_DISPLAY[f.severity];
6287
- const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
6288
- console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
6289
- }
6290
- if (failed.length > limit) {
6291
- console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
6292
- }
6293
- }
6294
- else {
6295
- console.log(`\n ${colors.green}No security issues found.${RESET()}`);
6296
- }
6504
+ displayCheckFindings(failed, !!options.verbose);
6297
6505
  // Step 3: Community contribution (after 3 scans, interactive only)
6298
6506
  if (process.stdin.isTTY && !globalCiMode) {
6299
6507
  const scanCount = incrementScanCounter();