hackmyagent 0.15.7 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,10 @@ 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
189
+ $ hackmyagent check pip:requests
190
+ $ hackmyagent check pypi:flask
188
191
  $ hackmyagent check modelcontextprotocol/servers --json`)
189
- .argument('<target>', 'npm package name, local path, or skill identifier')
192
+ .argument('<target>', 'npm package, PyPI package (pip: or pypi: prefix), local path, GitHub repo, or skill identifier')
190
193
  .option('-v, --verbose', 'Show detailed verification info')
191
194
  .option('--json', 'Output as JSON (for scripting/CI)')
192
195
  .option('--offline', 'Skip DNS verification (offline mode)')
@@ -244,6 +247,11 @@ Examples:
244
247
  process.exit(1);
245
248
  return;
246
249
  }
250
+ // PyPI package: download, run full HMA scan, clean up
251
+ if (looksLikePyPiPackage(skill)) {
252
+ await checkPyPiPackage(skill, options);
253
+ return;
254
+ }
247
255
  // GitHub repo: clone, run full HMA scan, clean up
248
256
  if (looksLikeGitHubRepo(skill)) {
249
257
  await checkGitHubRepo(skill, options);
@@ -343,6 +351,43 @@ const SEVERITY_DISPLAY = {
343
351
  medium: { symbol: '[~]', color: () => colors.yellow },
344
352
  low: { symbol: '[.]', color: () => colors.green },
345
353
  };
354
+ /**
355
+ * Display check command findings with optional verbose details.
356
+ * When verbose is true, shows checkId, category, file location, and fix/guidance for each finding.
357
+ */
358
+ function displayCheckFindings(failed, verbose) {
359
+ if (failed.length > 0) {
360
+ console.log();
361
+ const limit = verbose ? failed.length : 15;
362
+ for (const f of failed.slice(0, limit)) {
363
+ const sev = SEVERITY_DISPLAY[f.severity];
364
+ const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
365
+ console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
366
+ if (verbose) {
367
+ console.log(` ${colors.dim}Check: ${f.checkId}${RESET()}`);
368
+ if (f.category) {
369
+ console.log(` ${colors.dim}Category: ${f.category}${RESET()}`);
370
+ }
371
+ if (f.file) {
372
+ const location = f.line ? `${f.file}:${f.line}` : f.file;
373
+ console.log(` ${colors.dim}File: ${location}${RESET()}`);
374
+ }
375
+ if (f.fix) {
376
+ console.log(` ${colors.cyan}Fix: ${f.fix}${RESET()}`);
377
+ }
378
+ if (f.guidance) {
379
+ console.log(` ${colors.dim}Guidance: ${f.guidance}${RESET()}`);
380
+ }
381
+ }
382
+ }
383
+ if (failed.length > limit) {
384
+ console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
385
+ }
386
+ }
387
+ else {
388
+ console.log(`\n ${colors.green}No security issues found.${RESET()}`);
389
+ }
390
+ }
346
391
  function groupFindingsBySeverity(findings) {
347
392
  const grouped = {
348
393
  critical: [],
@@ -1774,7 +1819,7 @@ program
1774
1819
  .command('secure')
1775
1820
  .description(`Scan and harden your agent setup
1776
1821
 
1777
- Performs 204 security checks across 60 categories:
1822
+ Performs ${CHECK_COUNT} security checks across 60 categories:
1778
1823
  • Credentials: API key exposure, secrets in configs
1779
1824
  • MCP: Server configs, tool permissions, secrets
1780
1825
  • Network: TLS, interface bindings, CORS
@@ -2776,188 +2821,6 @@ Examples:
2776
2821
  process.exit(1);
2777
2822
  }
2778
2823
  });
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
2824
  program
2962
2825
  .command('scan')
2963
2826
  .description(`Scan external target for exposed MCP endpoints
@@ -4534,7 +4397,7 @@ Examples:
4534
4397
  console.log(`\n Detected: ${result.tool}\n`);
4535
4398
  console.log(` Added HackMyAgent MCP server to ${result.configPath}\n`);
4536
4399
  console.log(` Available tools in ${result.tool}:`);
4537
- console.log(` hackmyagent_scan — 204 checks + structural analysis`);
4400
+ console.log(` hackmyagent_scan — ${CHECK_COUNT} checks + structural analysis`);
4538
4401
  console.log(` hackmyagent_deep_scan — Full analysis with LLM reasoning`);
4539
4402
  console.log(` hackmyagent_analyze_file — Analyze a single file`);
4540
4403
  console.log(` hackmyagent_benchmark — OASB-1 compliance assessment\n`);
@@ -5373,12 +5236,39 @@ program
5373
5236
  // Fallback: static explanation from check metadata
5374
5237
  const checkId = findingId.toUpperCase();
5375
5238
  const staticExplanations = {
5239
+ // Credential checks
5376
5240
  '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
5241
  'CRED-002': 'OpenAI API key pattern detected (sk-...). Move to environment variable OPENAI_API_KEY.',
5378
5242
  'CRED-003': 'Anthropic API key pattern detected (sk-ant-...). Move to environment variable ANTHROPIC_API_KEY.',
5379
5243
  '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.',
5244
+ // MCP checks
5245
+ '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.',
5246
+ // Skill checks
5247
+ 'SKILL-005': 'External endpoint in skill capability declaration. Verify the endpoint is trusted and uses HTTPS.',
5248
+ // Governance checks
5249
+ '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.',
5250
+ '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.',
5251
+ '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.',
5252
+ // Permission checks
5253
+ '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.',
5254
+ '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.',
5255
+ 'PERM-003': 'Execution permissions too permissive. The agent can spawn arbitrary processes. Restrict executable permissions to specific, required binaries only.',
5256
+ // SOUL checks
5257
+ 'SOUL-001': 'No SOUL.md file found. SOUL.md defines the agent identity, mission, and behavioral constraints. Run `hackmyagent secure --fix` to generate one.',
5258
+ 'SOUL-002': 'SOUL.md missing identity section. The agent lacks a declared identity, making impersonation easier. Add name, version, and publisher fields.',
5259
+ 'SOUL-003': 'SOUL.md missing behavioral boundaries. Without explicit limits, the agent may perform unintended actions. Add a boundaries section listing prohibited behaviors.',
5260
+ // Privacy checks
5261
+ '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.',
5262
+ // Data checks
5263
+ '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.',
5264
+ '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.',
5265
+ // Injection checks
5266
+ '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.',
5267
+ '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.',
5268
+ // Attestation checks
5269
+ '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.',
5270
+ // Supply chain checks
5271
+ '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
5272
  };
5383
5273
  const explanation = staticExplanations[checkId];
5384
5274
  if (explanation) {
@@ -5754,6 +5644,18 @@ program
5754
5644
  // ============================================================================
5755
5645
  // npm package scanning helpers (used by `check <package>`)
5756
5646
  // ============================================================================
5647
+ /**
5648
+ * Detect whether a string looks like a PyPI package reference.
5649
+ *
5650
+ * Requires an explicit prefix:
5651
+ * - pip:package-name
5652
+ * - pypi:package-name
5653
+ *
5654
+ * Bare names are NOT auto-detected as PyPI (they fall through to npm).
5655
+ */
5656
+ function looksLikePyPiPackage(target) {
5657
+ return target.startsWith('pip:') || target.startsWith('pypi:');
5658
+ }
5757
5659
  /**
5758
5660
  * Detect whether a string looks like an npm package name rather than
5759
5661
  * a hostname, IP address, or local path.
@@ -6124,21 +6026,7 @@ async function checkGitHubRepo(target, options) {
6124
6026
  console.log(` Type: ${result.projectType}`);
6125
6027
  console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
6126
6028
  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
- }
6029
+ displayCheckFindings(failed, !!options.verbose);
6142
6030
  // Step 3: Community contribution
6143
6031
  if (process.stdin.isTTY && !globalCiMode) {
6144
6032
  const scanCount = incrementScanCounter();
@@ -6202,6 +6090,139 @@ async function checkGitHubRepo(target, options) {
6202
6090
  * Download an npm package, run full HMA secure scan, display results, clean up.
6203
6091
  * Checks the registry first; only downloads if data is missing or stale.
6204
6092
  */
6093
+ /**
6094
+ * Download a PyPI package, scan it with HMA + NanoMind, and display results.
6095
+ * Accepts targets prefixed with pip: or pypi: (e.g. pip:requests, pypi:flask).
6096
+ */
6097
+ async function checkPyPiPackage(target, options) {
6098
+ // Strip prefix to get the bare package name
6099
+ const name = target.replace(/^(pip|pypi):/, '');
6100
+ const { mkdtemp, rm, readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
6101
+ const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
6102
+ const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
6103
+ const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
6104
+ if (!options.json && !globalCiMode) {
6105
+ console.error(`Downloading ${name} from PyPI...`);
6106
+ }
6107
+ const tempDir = await mkdtemp(join(tmpdir(), 'hma-check-pypi-'));
6108
+ try {
6109
+ // Fetch package metadata from PyPI JSON API
6110
+ const metaRes = await fetch(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`);
6111
+ if (!metaRes.ok) {
6112
+ if (metaRes.status === 404) {
6113
+ console.error(`Error: Package "${name}" not found on PyPI.`);
6114
+ }
6115
+ else {
6116
+ console.error(`Error: PyPI API returned ${metaRes.status} for "${name}".`);
6117
+ }
6118
+ process.exit(1);
6119
+ }
6120
+ const meta = await metaRes.json();
6121
+ // Prefer sdist (source tarball) for scanning; fall back to first wheel
6122
+ const sdist = meta.urls.find((u) => u.packagetype === 'sdist');
6123
+ const wheel = meta.urls.find((u) => u.packagetype === 'bdist_wheel');
6124
+ const dist = sdist || wheel || meta.urls[0];
6125
+ if (!dist) {
6126
+ console.error(`Error: No downloadable distribution found for "${name}" on PyPI.`);
6127
+ process.exit(1);
6128
+ }
6129
+ // Download the archive
6130
+ const archiveRes = await fetch(dist.url);
6131
+ if (!archiveRes.ok) {
6132
+ throw new Error(`Failed to download ${dist.filename}: HTTP ${archiveRes.status}`);
6133
+ }
6134
+ const archiveBuffer = Buffer.from(await archiveRes.arrayBuffer());
6135
+ const archivePath = join(tempDir, dist.filename);
6136
+ const { writeFileSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
6137
+ writeFileSync(archivePath, archiveBuffer);
6138
+ // Extract
6139
+ const extractDir = join(tempDir, 'package');
6140
+ const { mkdirSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
6141
+ mkdirSync(extractDir, { recursive: true });
6142
+ if (dist.filename.endsWith('.tar.gz') || dist.filename.endsWith('.tgz')) {
6143
+ execFileSync('tar', ['xzf', archivePath, '-C', extractDir, '--strip-components=1'], { timeout: 30000 });
6144
+ }
6145
+ else if (dist.filename.endsWith('.zip') || dist.filename.endsWith('.whl')) {
6146
+ execFileSync('unzip', ['-q', '-o', archivePath, '-d', extractDir], { timeout: 30000 });
6147
+ }
6148
+ else {
6149
+ throw new Error(`Unsupported archive format: ${dist.filename}`);
6150
+ }
6151
+ // Run full HMA scan + NanoMind (same pipeline as checkNpmPackage)
6152
+ const scanner = new index_1.HardeningScanner();
6153
+ const result = await scanner.scan({ targetDir: extractDir, autoFix: false });
6154
+ // Run NanoMind semantic analysis and re-filter
6155
+ try {
6156
+ const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6157
+ const nmResult = await orchestrateNanoMind(extractDir, result.findings, { silent: true });
6158
+ const refiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, extractDir);
6159
+ const projectType = result.projectType || 'library';
6160
+ result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6161
+ result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
6162
+ }
6163
+ catch {
6164
+ // NanoMind unavailable -- use base scan results
6165
+ }
6166
+ const failed = result.findings.filter(f => !f.passed);
6167
+ const critical = failed.filter(f => f.severity === 'critical');
6168
+ const high = failed.filter(f => f.severity === 'high');
6169
+ const medium = failed.filter(f => f.severity === 'medium');
6170
+ const low = failed.filter(f => f.severity === 'low');
6171
+ if (options.json) {
6172
+ writeJsonStdout({
6173
+ name,
6174
+ type: 'pypi-package',
6175
+ source: 'local-scan',
6176
+ version: meta.info.version,
6177
+ projectType: result.projectType,
6178
+ score: result.score,
6179
+ maxScore: result.maxScore,
6180
+ findings: result.findings,
6181
+ });
6182
+ return;
6183
+ }
6184
+ // Display results
6185
+ const scoreRatio = result.score / result.maxScore;
6186
+ const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
6187
+ console.log(`\n ${name} (PyPI)`);
6188
+ console.log(` Version: ${meta.info.version}`);
6189
+ console.log(` Type: ${result.projectType}`);
6190
+ console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
6191
+ console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
6192
+ if (failed.length > 0) {
6193
+ console.log();
6194
+ const limit = options.verbose ? failed.length : 15;
6195
+ for (const f of failed.slice(0, limit)) {
6196
+ const sev = SEVERITY_DISPLAY[f.severity];
6197
+ const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
6198
+ console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
6199
+ }
6200
+ if (failed.length > limit) {
6201
+ console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
6202
+ }
6203
+ }
6204
+ else {
6205
+ console.log(`\n ${colors.green}No security issues found.${RESET()}`);
6206
+ }
6207
+ console.log(`\n Full project scan: ${CLI_PREFIX} secure <dir>`);
6208
+ console.log();
6209
+ if (critical.length > 0 || high.length > 0)
6210
+ process.exit(1);
6211
+ }
6212
+ catch (err) {
6213
+ const message = err instanceof Error ? err.message : String(err);
6214
+ if (message.includes('not found on PyPI')) {
6215
+ console.error(`Error: ${message}`);
6216
+ }
6217
+ else {
6218
+ console.error(`Error scanning PyPI package "${name}": ${message}`);
6219
+ }
6220
+ process.exit(1);
6221
+ }
6222
+ finally {
6223
+ await rm(tempDir, { recursive: true, force: true });
6224
+ }
6225
+ }
6205
6226
  async function checkNpmPackage(name, options) {
6206
6227
  // Step 1: Check registry for existing trust data
6207
6228
  if (!options.offline) {
@@ -6279,21 +6300,7 @@ async function checkNpmPackage(name, options) {
6279
6300
  console.log(` Type: ${result.projectType}`);
6280
6301
  console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
6281
6302
  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
- }
6303
+ displayCheckFindings(failed, !!options.verbose);
6297
6304
  // Step 3: Community contribution (after 3 scans, interactive only)
6298
6305
  if (process.stdin.isTTY && !globalCiMode) {
6299
6306
  const scanCount = incrementScanCounter();