hackmyagent 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/.integrity-manifest.json +1 -1
  2. package/dist/arp/engine/event-engine.d.ts.map +1 -1
  3. package/dist/arp/engine/event-engine.js +8 -6
  4. package/dist/arp/engine/event-engine.js.map +1 -1
  5. package/dist/arp/intelligence/coordinator.d.ts.map +1 -1
  6. package/dist/arp/intelligence/coordinator.js +17 -12
  7. package/dist/arp/intelligence/coordinator.js.map +1 -1
  8. package/dist/arp/intelligence/nanomind-l1.js +2 -2
  9. package/dist/arp/intelligence/nanomind-l1.js.map +1 -1
  10. package/dist/arp/interceptors/process.d.ts.map +1 -1
  11. package/dist/arp/interceptors/process.js +40 -5
  12. package/dist/arp/interceptors/process.js.map +1 -1
  13. package/dist/arp/proxy/server.d.ts +6 -0
  14. package/dist/arp/proxy/server.d.ts.map +1 -1
  15. package/dist/arp/proxy/server.js +40 -14
  16. package/dist/arp/proxy/server.js.map +1 -1
  17. package/dist/arp/telemetry/gtin.d.ts +1 -1
  18. package/dist/arp/telemetry/gtin.d.ts.map +1 -1
  19. package/dist/arp/telemetry/gtin.js +4 -0
  20. package/dist/arp/telemetry/gtin.js.map +1 -1
  21. package/dist/attack/payloads/index.d.ts +2 -1
  22. package/dist/attack/payloads/index.d.ts.map +1 -1
  23. package/dist/attack/payloads/index.js +5 -1
  24. package/dist/attack/payloads/index.js.map +1 -1
  25. package/dist/attack/payloads/policy-enforcement-integrity.d.ts +13 -0
  26. package/dist/attack/payloads/policy-enforcement-integrity.d.ts.map +1 -0
  27. package/dist/attack/payloads/policy-enforcement-integrity.js +217 -0
  28. package/dist/attack/payloads/policy-enforcement-integrity.js.map +1 -0
  29. package/dist/attack/scanner.d.ts.map +1 -1
  30. package/dist/attack/scanner.js +1 -0
  31. package/dist/attack/scanner.js.map +1 -1
  32. package/dist/attack/types.d.ts +1 -1
  33. package/dist/attack/types.d.ts.map +1 -1
  34. package/dist/attack/types.js +5 -0
  35. package/dist/attack/types.js.map +1 -1
  36. package/dist/attack-engine/types.d.ts +1 -1
  37. package/dist/attack-engine/types.d.ts.map +1 -1
  38. package/dist/attack-engine/types.js.map +1 -1
  39. package/dist/cli.js +279 -20
  40. package/dist/cli.js.map +1 -1
  41. package/dist/hardening/scanner.d.ts +18 -4
  42. package/dist/hardening/scanner.d.ts.map +1 -1
  43. package/dist/hardening/scanner.js +76 -18
  44. package/dist/hardening/scanner.js.map +1 -1
  45. package/dist/nanomind-core/inference/tme-classifier.d.ts.map +1 -1
  46. package/dist/nanomind-core/inference/tme-classifier.js +0 -1
  47. package/dist/nanomind-core/inference/tme-classifier.js.map +1 -1
  48. package/dist/nanomind-core/scanner-bridge.d.ts.map +1 -1
  49. package/dist/nanomind-core/scanner-bridge.js +8 -0
  50. package/dist/nanomind-core/scanner-bridge.js.map +1 -1
  51. package/package.json +1 -1
  52. package/dist/abgr/controls.d.ts +0 -35
  53. package/dist/abgr/controls.d.ts.map +0 -1
  54. package/dist/abgr/controls.js +0 -1058
  55. package/dist/abgr/controls.js.map +0 -1
  56. package/dist/abgr/detector.d.ts +0 -45
  57. package/dist/abgr/detector.d.ts.map +0 -1
  58. package/dist/abgr/detector.js +0 -175
  59. package/dist/abgr/detector.js.map +0 -1
  60. package/dist/abgr/index.d.ts +0 -24
  61. package/dist/abgr/index.d.ts.map +0 -1
  62. package/dist/abgr/index.js +0 -50
  63. package/dist/abgr/index.js.map +0 -1
  64. package/dist/abgr/scorer.d.ts +0 -36
  65. package/dist/abgr/scorer.d.ts.map +0 -1
  66. package/dist/abgr/scorer.js +0 -205
  67. package/dist/abgr/scorer.js.map +0 -1
  68. package/dist/abgr/templates.d.ts +0 -35
  69. package/dist/abgr/templates.d.ts.map +0 -1
  70. package/dist/abgr/templates.js +0 -668
  71. package/dist/abgr/templates.js.map +0 -1
  72. package/dist/abgr/tier.d.ts +0 -27
  73. package/dist/abgr/tier.d.ts.map +0 -1
  74. package/dist/abgr/tier.js +0 -115
  75. package/dist/abgr/tier.js.map +0 -1
  76. package/dist/abgr/types.d.ts +0 -59
  77. package/dist/abgr/types.d.ts.map +0 -1
  78. package/dist/abgr/types.js +0 -10
  79. package/dist/abgr/types.js.map +0 -1
  80. package/dist/agent-scan/checks.d.ts +0 -6
  81. package/dist/agent-scan/checks.d.ts.map +0 -1
  82. package/dist/agent-scan/checks.js +0 -93
  83. package/dist/agent-scan/checks.js.map +0 -1
  84. package/dist/agent-scan/index.d.ts +0 -10
  85. package/dist/agent-scan/index.d.ts.map +0 -1
  86. package/dist/agent-scan/index.js +0 -16
  87. package/dist/agent-scan/index.js.map +0 -1
  88. package/dist/agent-scan/scanner.d.ts +0 -31
  89. package/dist/agent-scan/scanner.d.ts.map +0 -1
  90. package/dist/agent-scan/scanner.js +0 -484
  91. package/dist/agent-scan/scanner.js.map +0 -1
  92. package/dist/agent-scan/types.d.ts +0 -63
  93. package/dist/agent-scan/types.d.ts.map +0 -1
  94. package/dist/agent-scan/types.js +0 -10
  95. package/dist/agent-scan/types.js.map +0 -1
  96. package/dist/hardening/llm-checks.d.ts +0 -18
  97. package/dist/hardening/llm-checks.d.ts.map +0 -1
  98. package/dist/hardening/llm-checks.js +0 -434
  99. package/dist/hardening/llm-checks.js.map +0 -1
  100. package/dist/hardening/mcp-tool-enum.d.ts +0 -45
  101. package/dist/hardening/mcp-tool-enum.d.ts.map +0 -1
  102. package/dist/hardening/mcp-tool-enum.js +0 -315
  103. package/dist/hardening/mcp-tool-enum.js.map +0 -1
  104. package/dist/hardening/shell-checks.d.ts +0 -21
  105. package/dist/hardening/shell-checks.d.ts.map +0 -1
  106. package/dist/hardening/shell-checks.js +0 -236
  107. package/dist/hardening/shell-checks.js.map +0 -1
  108. package/dist/nanomind-core/telemetry/auto-update.d.ts +0 -27
  109. package/dist/nanomind-core/telemetry/auto-update.d.ts.map +0 -1
  110. package/dist/nanomind-core/telemetry/auto-update.js +0 -129
  111. package/dist/nanomind-core/telemetry/auto-update.js.map +0 -1
  112. package/dist/nanomind-core/telemetry/client.d.ts +0 -66
  113. package/dist/nanomind-core/telemetry/client.d.ts.map +0 -1
  114. package/dist/nanomind-core/telemetry/client.js +0 -123
  115. package/dist/nanomind-core/telemetry/client.js.map +0 -1
  116. package/dist/nanomind-core/telemetry/config.d.ts +0 -33
  117. package/dist/nanomind-core/telemetry/config.d.ts.map +0 -1
  118. package/dist/nanomind-core/telemetry/config.js +0 -119
  119. package/dist/nanomind-core/telemetry/config.js.map +0 -1
  120. package/dist/nanomind-core/telemetry/index.d.ts +0 -15
  121. package/dist/nanomind-core/telemetry/index.d.ts.map +0 -1
  122. package/dist/nanomind-core/telemetry/index.js +0 -27
  123. package/dist/nanomind-core/telemetry/index.js.map +0 -1
  124. package/dist/registry/contribution.d.ts +0 -178
  125. package/dist/registry/contribution.d.ts.map +0 -1
  126. package/dist/registry/contribution.js +0 -272
  127. package/dist/registry/contribution.js.map +0 -1
package/dist/cli.js CHANGED
@@ -167,21 +167,23 @@ const RISK_DISPLAY = {
167
167
  const RESET = () => colors.reset;
168
168
  program
169
169
  .command('check')
170
- .description(`Verify a skill before installing
170
+ .description(`Check if a package or skill is safe
171
171
 
172
- Analyzes skill safety by checking:
173
- Publisher identity via DNS TXT records
174
- Permissions requested (filesystem, network, shell)
175
- Revocation status against global blocklist
172
+ Accepts npm packages, local paths, or skill identifiers:
173
+ npm package: downloads and runs full security analysis (204 checks + NanoMind)
174
+ Local path: runs NanoMind semantic analysis
175
+ Skill identifier: verifies publisher, permissions, revocation
176
176
 
177
177
  Risk levels: low, medium, high, critical
178
178
  Exit code 1 if high/critical risk detected.
179
179
 
180
180
  Examples:
181
- $ hackmyagent check server-filesystem
182
- $ hackmyagent check @publisher/skill --verbose
183
- $ hackmyagent check @publisher/skill --json`)
184
- .argument('<skill>', 'Skill identifier or local path (e.g., @publisher/skill, ./SKILL.md, ./agent-dir/)')
181
+ $ hackmyagent check express
182
+ $ hackmyagent check @modelcontextprotocol/server-filesystem
183
+ $ hackmyagent check @modelcontextprotocol/server-filesystem --json
184
+ $ hackmyagent check ./my-agent/
185
+ $ hackmyagent check @publisher/skill --verbose`)
186
+ .argument('<target>', 'npm package name, local path, or skill identifier')
185
187
  .option('-v, --verbose', 'Show detailed verification info')
186
188
  .option('--json', 'Output as JSON (for scripting/CI)')
187
189
  .option('--offline', 'Skip DNS verification (offline mode)')
@@ -239,6 +241,11 @@ Examples:
239
241
  process.exit(1);
240
242
  return;
241
243
  }
244
+ // npm package name: download, run full HMA scan, clean up
245
+ if (looksLikeNpmPackage(skill)) {
246
+ await checkNpmPackage(skill, options);
247
+ return;
248
+ }
242
249
  // Registry lookup path (non-local identifier) with 10s timeout
243
250
  const checkPromise = (0, index_1.checkSkill)(skill, {
244
251
  skipDnsVerification: options.offline,
@@ -1945,12 +1952,20 @@ Examples:
1945
1952
  deep: isDeep,
1946
1953
  silent: format !== 'text',
1947
1954
  });
1955
+ // Re-apply all filters after NanoMind merge (merge uses allFindings which is unfiltered)
1956
+ const refiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, targetDir);
1948
1957
  if (result.allFindings) {
1949
- result.allFindings = nmResult.mergedFindings;
1958
+ result.allFindings = refiltered;
1950
1959
  }
1951
1960
  if (result.findings) {
1952
- result.findings = nmResult.mergedFindings.filter((f) => !f.passed);
1961
+ // Re-apply the same gates as the original filter:
1962
+ // 1. Only failed checks 2. Has file path 3. Applies to project type
1963
+ const projectType = result.projectType || 'library';
1964
+ result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
1953
1965
  }
1966
+ // Recalculate score from filtered findings (score was set pre-NanoMind)
1967
+ const forScore = (result.findings || []).filter((f) => !f.passed && !f.fixed);
1968
+ result.score = scanner.calculateScore(forScore).score;
1954
1969
  }
1955
1970
  // Behavioral simulation: auto-runs on --deep, or when NanoMind detects ambiguity
1956
1971
  if (isDeep && format === 'text') {
@@ -2658,7 +2673,11 @@ Examples:
2658
2673
  try {
2659
2674
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
2660
2675
  const nmResult = await orchestrateNanoMind(targetDir, result.findings, { silent: !!options.json });
2661
- result.findings = nmResult.mergedFindings;
2676
+ // Re-apply .hmaignore filters and recalculate score after NanoMind merge
2677
+ const hRefiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, targetDir);
2678
+ result.findings = hRefiltered;
2679
+ const hForScore = hRefiltered.filter((f) => !f.passed && !f.fixed);
2680
+ result.score = scanner.calculateScore(hForScore).score;
2662
2681
  }
2663
2682
  catch { /* NanoMind unavailable */ }
2664
2683
  // Filter to OpenClaw-specific findings
@@ -3064,14 +3083,8 @@ program
3064
3083
  .command('attack')
3065
3084
  .description(`Adversarial security testing for AI agents
3066
3085
 
3067
- Red team your AI agent with ${index_1.PAYLOAD_STATS.total} attack payloads across 7 categories:
3068
- • Prompt Injection: ${index_1.PAYLOAD_STATS.byCategory['prompt-injection']} payloads
3069
- • Jailbreaking: ${index_1.PAYLOAD_STATS.byCategory['jailbreak']} payloads
3070
- • Data Exfiltration: ${index_1.PAYLOAD_STATS.byCategory['data-exfiltration']} payloads
3071
- • Capability Abuse: ${index_1.PAYLOAD_STATS.byCategory['capability-abuse']} payloads
3072
- • Context Manipulation: ${index_1.PAYLOAD_STATS.byCategory['context-manipulation']} payloads
3073
- • MCP Exploitation: ${index_1.PAYLOAD_STATS.byCategory['mcp-exploitation']} payloads
3074
- • A2A Attacks: ${index_1.PAYLOAD_STATS.byCategory['a2a-attack']} payloads
3086
+ Red team your AI agent with ${index_1.PAYLOAD_STATS.total} attack payloads across ${Object.keys(index_1.PAYLOAD_STATS.byCategory).length} categories:
3087
+ ${Object.entries(index_1.PAYLOAD_STATS.byCategory).map(([cat, count]) => ` • ${index_1.ATTACK_CATEGORIES[cat].name}: ${count} payloads`).join('\n')}
3075
3088
 
3076
3089
  Intensity levels (controls how many payloads run):
3077
3090
  passive Observation only (${index_1.PAYLOAD_STATS.byIntensity.passive} payloads)
@@ -3527,6 +3540,7 @@ function generateAttackHtmlReport(report) {
3527
3540
  'persistent-agent': 'PERSIST',
3528
3541
  'fake-tool': 'FAKETOOL',
3529
3542
  'context-lifecycle': 'LIFECYCLE',
3543
+ 'policy-enforcement-integrity': 'PEI',
3530
3544
  };
3531
3545
  // Donut chart for attack results
3532
3546
  const donutRadius = 60;
@@ -5729,6 +5743,251 @@ program
5729
5743
  }
5730
5744
  console.log(`\nYour skill is ready. Verify security with: hackmyagent secure ${outputDir}/`);
5731
5745
  });
5746
+ // ============================================================================
5747
+ // npm package scanning helpers (used by `check <package>`)
5748
+ // ============================================================================
5749
+ /**
5750
+ * Detect whether a string looks like an npm package name rather than
5751
+ * a hostname, IP address, or local path.
5752
+ *
5753
+ * npm package names: express, @scope/name, lodash, my-pkg
5754
+ * NOT packages: example.com, 192.168.1.1, ./dir, /path, .
5755
+ */
5756
+ function looksLikeNpmPackage(target) {
5757
+ // Local paths
5758
+ if (target.startsWith('.') || target.startsWith('/'))
5759
+ return false;
5760
+ // Scoped packages are always npm
5761
+ if (target.startsWith('@') && target.includes('/'))
5762
+ return true;
5763
+ // IPs
5764
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(target))
5765
+ return false;
5766
+ // Hostnames have dots (example.com, sub.domain.org)
5767
+ if (target.includes('.'))
5768
+ return false;
5769
+ // What's left: bare names like express, lodash, hackmyagent
5770
+ // npm names are lowercase, may contain hyphens and digits
5771
+ return /^[a-z0-9][a-z0-9._-]*$/.test(target);
5772
+ }
5773
+ const REGISTRY_URL = 'https://api.oa2a.org';
5774
+ const STALE_SCAN_DAYS = 3;
5775
+ /**
5776
+ * Query the OpenA2A Registry for existing trust data.
5777
+ * Returns null on any error (network, 404, timeout).
5778
+ */
5779
+ async function queryRegistry(name) {
5780
+ try {
5781
+ const params = new URLSearchParams({ name, includeProfile: 'true' });
5782
+ const response = await fetch(`${REGISTRY_URL}/api/v1/trust/query?${params}`, {
5783
+ method: 'GET',
5784
+ headers: { 'Accept': 'application/json', 'User-Agent': `hackmyagent/${index_1.VERSION}` },
5785
+ signal: AbortSignal.timeout(5000),
5786
+ });
5787
+ if (!response.ok)
5788
+ return null;
5789
+ const data = await response.json();
5790
+ if (!data.packageId)
5791
+ return null;
5792
+ return {
5793
+ found: true,
5794
+ name: data.name ?? name,
5795
+ trustScore: data.trustScore ?? 0,
5796
+ trustLevel: data.trustLevel ?? 0,
5797
+ verdict: data.verdict ?? 'unknown',
5798
+ scanStatus: data.scanStatus,
5799
+ lastScannedAt: data.lastScannedAt,
5800
+ packageType: data.packageType,
5801
+ };
5802
+ }
5803
+ catch {
5804
+ return null;
5805
+ }
5806
+ }
5807
+ /**
5808
+ * Check if scan data is stale (older than STALE_SCAN_DAYS).
5809
+ */
5810
+ function isScanStale(lastScannedAt) {
5811
+ if (!lastScannedAt)
5812
+ return true;
5813
+ const scanned = new Date(lastScannedAt);
5814
+ const now = new Date();
5815
+ const days = (now.getTime() - scanned.getTime()) / (1000 * 60 * 60 * 24);
5816
+ return days > STALE_SCAN_DAYS;
5817
+ }
5818
+ /**
5819
+ * Publish scan results to the community registry.
5820
+ */
5821
+ async function publishToRegistry(name, result) {
5822
+ try {
5823
+ const response = await fetch(`${REGISTRY_URL}/api/v1/trust/scans`, {
5824
+ method: 'POST',
5825
+ headers: {
5826
+ 'Content-Type': 'application/json',
5827
+ 'User-Agent': `hackmyagent/${index_1.VERSION}`,
5828
+ },
5829
+ body: JSON.stringify({
5830
+ name,
5831
+ score: result.score,
5832
+ maxScore: result.maxScore,
5833
+ projectType: result.projectType,
5834
+ findings: result.findings.map(f => ({
5835
+ checkId: f.checkId,
5836
+ name: f.name,
5837
+ severity: f.severity,
5838
+ passed: f.passed,
5839
+ message: f.message,
5840
+ category: f.category,
5841
+ })),
5842
+ scanTimestamp: new Date().toISOString(),
5843
+ }),
5844
+ signal: AbortSignal.timeout(10000),
5845
+ });
5846
+ return response.ok;
5847
+ }
5848
+ catch {
5849
+ return false;
5850
+ }
5851
+ }
5852
+ /**
5853
+ * Display registry trust data in the terminal.
5854
+ */
5855
+ function displayRegistryResult(data) {
5856
+ const scoreRatio = data.trustScore;
5857
+ const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
5858
+ const score = Math.round(scoreRatio * 100);
5859
+ console.log(`\n ${data.name}`);
5860
+ console.log(` Type: ${data.packageType ?? 'unknown'}`);
5861
+ console.log(` Score: ${scoreColor}${score}/100${RESET()} (registry)`);
5862
+ console.log(` Verdict: ${data.verdict}`);
5863
+ if (data.lastScannedAt) {
5864
+ const days = Math.floor((Date.now() - new Date(data.lastScannedAt).getTime()) / (1000 * 60 * 60 * 24));
5865
+ console.log(` Scanned: ${days === 0 ? 'today' : days + ' day(s) ago'}`);
5866
+ }
5867
+ console.log();
5868
+ }
5869
+ /**
5870
+ * Download an npm package, run full HMA secure scan, display results, clean up.
5871
+ * Checks the registry first; only downloads if data is missing or stale.
5872
+ */
5873
+ async function checkNpmPackage(name, options) {
5874
+ // Step 1: Check registry for existing trust data
5875
+ if (!options.offline) {
5876
+ const registryData = await queryRegistry(name);
5877
+ if (registryData?.found && !isScanStale(registryData.lastScannedAt)) {
5878
+ // Fresh data in registry — show it
5879
+ if (options.json) {
5880
+ writeJsonStdout({ ...registryData, source: 'registry' });
5881
+ return;
5882
+ }
5883
+ displayRegistryResult(registryData);
5884
+ return;
5885
+ }
5886
+ // Stale or missing — tell the user we're scanning
5887
+ if (registryData?.found && registryData.lastScannedAt) {
5888
+ if (!options.json && !globalCiMode) {
5889
+ const days = Math.floor((Date.now() - new Date(registryData.lastScannedAt).getTime()) / (1000 * 60 * 60 * 24));
5890
+ console.error(`\nRegistry data is ${days} day(s) old. Re-scanning...`);
5891
+ }
5892
+ }
5893
+ }
5894
+ // Step 2: Download and scan
5895
+ const { mkdtemp, rm } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
5896
+ const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
5897
+ const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
5898
+ const { execFile } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
5899
+ const { promisify } = await Promise.resolve().then(() => __importStar(require('node:util')));
5900
+ const execAsync = promisify(execFile);
5901
+ if (!options.json && !globalCiMode) {
5902
+ console.error(`Downloading ${name} from npm...`);
5903
+ }
5904
+ const tempDir = await mkdtemp(join(tmpdir(), 'hma-check-'));
5905
+ try {
5906
+ // Download and extract
5907
+ const { stdout } = await execAsync('npm', ['pack', name, '--pack-destination', tempDir], { timeout: 60000 });
5908
+ const tarball = stdout.trim().split('\n').pop();
5909
+ await execAsync('tar', ['xzf', join(tempDir, tarball), '-C', tempDir], { timeout: 30000 });
5910
+ const packageDir = join(tempDir, 'package');
5911
+ // Run full HMA scan
5912
+ const scanner = new index_1.HardeningScanner();
5913
+ const result = await scanner.scan({ targetDir: packageDir, autoFix: false });
5914
+ const failed = result.findings.filter(f => !f.passed);
5915
+ const critical = failed.filter(f => f.severity === 'critical');
5916
+ const high = failed.filter(f => f.severity === 'high');
5917
+ const medium = failed.filter(f => f.severity === 'medium');
5918
+ const low = failed.filter(f => f.severity === 'low');
5919
+ if (options.json) {
5920
+ writeJsonStdout({
5921
+ name,
5922
+ type: 'npm-package',
5923
+ source: 'local-scan',
5924
+ projectType: result.projectType,
5925
+ score: result.score,
5926
+ maxScore: result.maxScore,
5927
+ findings: result.findings,
5928
+ });
5929
+ return;
5930
+ }
5931
+ // Display results
5932
+ const scoreRatio = result.score / result.maxScore;
5933
+ const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
5934
+ console.log(`\n ${name}`);
5935
+ console.log(` Type: ${result.projectType}`);
5936
+ console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
5937
+ console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
5938
+ if (failed.length > 0) {
5939
+ console.log();
5940
+ const limit = options.verbose ? failed.length : 15;
5941
+ for (const f of failed.slice(0, limit)) {
5942
+ const sev = SEVERITY_DISPLAY[f.severity];
5943
+ const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
5944
+ console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
5945
+ }
5946
+ if (failed.length > limit) {
5947
+ console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
5948
+ }
5949
+ }
5950
+ else {
5951
+ console.log(`\n ${colors.green}No security issues found.${RESET()}`);
5952
+ }
5953
+ // Step 3: Offer to share with community (interactive only)
5954
+ if (process.stdin.isTTY && !globalCiMode) {
5955
+ const readline = await Promise.resolve().then(() => __importStar(require('node:readline')));
5956
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
5957
+ const answer = await new Promise(resolve => {
5958
+ rl.question(`\n Share this scan with the community? [Y/n] `, resolve);
5959
+ });
5960
+ rl.close();
5961
+ if (answer.trim().toLowerCase() !== 'n') {
5962
+ const ok = await publishToRegistry(name, result);
5963
+ if (ok) {
5964
+ console.error(` ${colors.green}Shared. Thank you for building trust in AI.${RESET()}`);
5965
+ }
5966
+ else {
5967
+ console.error(` ${colors.dim}Could not reach registry. Scan saved locally.${RESET()}`);
5968
+ }
5969
+ }
5970
+ }
5971
+ console.log(`\n Full project scan: ${CLI_PREFIX} secure <dir>`);
5972
+ console.log();
5973
+ if (critical.length > 0 || high.length > 0)
5974
+ process.exit(1);
5975
+ }
5976
+ catch (err) {
5977
+ const message = err instanceof Error ? err.message : String(err);
5978
+ // Clean npm error messages
5979
+ if (message.includes('404') || message.includes('Not Found')) {
5980
+ console.error(`Error: Package "${name}" not found on npm.`);
5981
+ }
5982
+ else {
5983
+ console.error(`Error: ${message}`);
5984
+ }
5985
+ process.exit(1);
5986
+ }
5987
+ finally {
5988
+ await rm(tempDir, { recursive: true, force: true });
5989
+ }
5990
+ }
5732
5991
  // Self-securing: verify own integrity before running any command
5733
5992
  // A security tool that doesn't verify itself is worse than no security tool
5734
5993
  (async () => {