hackmyagent 0.11.8 → 0.11.9

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
6
  [![Tests](https://img.shields.io/badge/tests-1051%20passing-brightgreen)](https://github.com/opena2a-org/hackmyagent)
7
7
 
8
- **187 security checks for AI agents. Find what can go wrong before an attacker does.**
8
+ **199 security checks for AI agents. Find what can go wrong before an attacker does.**
9
9
 
10
10
  Security scanner and red-team toolkit for Claude Code, Cursor, VS Code, and any MCP server setup.
11
11
 
@@ -31,7 +31,7 @@ npx opena2a-cli review
31
31
 
32
32
  **Attack testing** -- 115 adversarial payloads across 11 categories (prompt injection, data exfiltration, jailbreak, MCP exploitation, supply chain, memory weaponization, A2A protocol attacks, context window attacks).
33
33
 
34
- **Static analysis** -- 187 security checks across 39 categories covering credentials, MCP configs, OpenClaw/NemoClaw, Unicode steganography, CVE detection, governance, supply chain, memory poisoning, agent identity, and sandbox escape patterns.
34
+ **Static analysis** -- 199 security checks across 60 categories covering credentials, MCP configs, OpenClaw/NemoClaw, Unicode steganography, CVE detection, governance, supply chain, memory poisoning, agent identity, and sandbox escape patterns.
35
35
 
36
36
  <details>
37
37
  <summary>Attack testing details (115 payloads)</summary>
@@ -49,7 +49,7 @@ npx opena2a-cli review
49
49
  </details>
50
50
 
51
51
  <details>
52
- <summary>Static analysis details (187 checks)</summary>
52
+ <summary>Static analysis details (199 checks)</summary>
53
53
 
54
54
  - **Unicode steganography** -- invisible codepoints, zero-width chars, bidi attacks, homoglyph confusables, GlassWorm decoders ([real-world: os-info-checker-es6 npm attack, May 2025](https://thehackernews.com/2025/05/malicious-npm-package-leverages-unicode.html))
55
55
  - **Hardcoded credentials** -- API keys, tokens, and passwords in source or config files
@@ -65,7 +65,7 @@ npx opena2a-cli review
65
65
 
66
66
  </details>
67
67
 
68
- 187 checks across 39 categories. 115 attack payloads. No flags needed.
68
+ 199 checks across 60 categories. 115 attack payloads. No flags needed.
69
69
 
70
70
  ---
71
71
 
@@ -107,10 +107,9 @@ npm install --save-dev hackmyagent
107
107
 
108
108
  Step-by-step guides for common workflows:
109
109
 
110
- - **[Scan my agent](docs/use-cases/scan-my-agent.md)** -- Run all 187 checks and auto-fix findings (5 min)
110
+ - **[Scan my agent](docs/use-cases/scan-my-agent.md)** -- Run all 199 checks and auto-fix findings (5 min)
111
111
  - **[Red-team MCP servers](docs/use-cases/red-team-mcp.md)** -- Test MCP servers with adversarial payloads (10 min)
112
- - **[Secure OpenClaw](docs/use-cases/openclaw-security.md)** -- OpenClaw-specific checks, CVE detection, ClawHavoc IOC scanning (10 min)
113
- - **Secure NemoClaw** -- Scan NVIDIA NemoClaw sandbox installations for credential exposure, network misconfig, and sandbox escape vectors (5 min)
112
+ - **[Secure OpenClaw](docs/use-cases/openclaw-security.md)** -- Auto-detected when OpenClaw files are present. Includes CVE detection and ClawHavoc IOC scanning (10 min)
114
113
  - **[CI/CD pipeline](docs/use-cases/ci-pipeline.md)** -- GitHub Actions with JSON/SARIF output (5 min)
115
114
 
116
115
  ---
@@ -280,16 +279,9 @@ hackmyagent harden-soul --dry-run # preview without writing
280
279
 
281
280
  ---
282
281
 
283
- ### `hackmyagent secure-nemoclaw` -- NemoClaw Sandbox Scanner
282
+ ### OpenClaw and NemoClaw Detection
284
283
 
285
- Scan NVIDIA NemoClaw installations for credential exposure, network misconfiguration, blueprint integrity issues, sandbox escape vectors, and inherited OpenClaw vulnerabilities. 28 checks across 6 categories.
286
-
287
- ```bash
288
- hackmyagent secure-nemoclaw # scan auto-detected directory
289
- hackmyagent secure-nemoclaw ~/.nemoclaw # scan specific directory
290
- hackmyagent secure-nemoclaw --json # JSON output for CI
291
- hackmyagent secure-nemoclaw --verbose # show all checks including passed
292
- ```
284
+ `hackmyagent secure` auto-detects OpenClaw and NemoClaw installations by looking for `.openclaw/`, `.moltbot/`, `.nemoclaw/`, `openclaw.json`, and `openclaw.plugin.json`. When detected, it automatically runs platform-specific checks (28 NemoClaw checks, 34 OpenClaw checks) alongside the standard 199 security checks. No separate commands needed.
293
285
 
294
286
 
295
287
  ---
package/dist/cli.js CHANGED
@@ -82,6 +82,19 @@ function resolveCliPrefix() {
82
82
  return 'hackmyagent';
83
83
  }
84
84
  const CLI_PREFIX = resolveCliPrefix();
85
+ /**
86
+ * Validate that a registry URL uses HTTPS.
87
+ * Allows http://localhost for local development.
88
+ * Rejects all other non-HTTPS URLs to prevent credential leakage.
89
+ */
90
+ function validateRegistryUrl(url) {
91
+ if (url && !url.startsWith('https://') && !url.startsWith('http://localhost')) {
92
+ console.error('Error: Registry URL must use HTTPS. Got: ' + url);
93
+ console.error('Only https:// URLs and http://localhost are allowed.');
94
+ process.exit(1);
95
+ }
96
+ return url;
97
+ }
85
98
  // Check for NO_COLOR env or non-TTY to disable colors by default
86
99
  const noColorEnv = process.env.NO_COLOR !== undefined || !process.stdout.isTTY;
87
100
  // Color codes - will be cleared if --no-color is passed
@@ -105,7 +118,7 @@ program
105
118
  .name('hackmyagent')
106
119
  .description(`Find it. Break it. Fix it.
107
120
 
108
- The hacker's toolkit for AI agents. 187 security checks, 115 attack
121
+ The hacker's toolkit for AI agents. 199 security checks, 115 attack
109
122
  payloads, auto-fix with rollback, and OASB benchmark compliance.
110
123
 
111
124
  Documentation: https://hackmyagent.com/docs
@@ -114,10 +127,10 @@ Updates (v${index_1.VERSION}):
114
127
  - NemoClaw sandbox scanner (28 installation checks)
115
128
  - 10 new static analysis patterns (NEMO series)
116
129
  - Community trust contributions
117
- - 187 checks across 39 categories
130
+ - 199 checks across 60 categories
118
131
 
119
132
  Examples:
120
- $ hackmyagent secure Find vulnerabilities (187 checks)
133
+ $ hackmyagent secure Find vulnerabilities (199 checks)
121
134
  $ hackmyagent attack --local Break it with 115 attack payloads
122
135
  $ hackmyagent secure --fix Fix issues automatically
123
136
  $ hackmyagent fix-all Run all security plugins
@@ -126,7 +139,7 @@ Examples:
126
139
  .option('--no-color', 'Disable colored output (also respects NO_COLOR env)');
127
140
  program.addHelpText('beforeAll', `
128
141
  Quick start:
129
- $ hackmyagent secure Scan current directory (187 checks)
142
+ $ hackmyagent secure Scan current directory (199 checks)
130
143
  $ hackmyagent fix-all --with-aim Auto-fix + create agent identity
131
144
  $ hackmyagent attack Red-team your agent
132
145
  `);
@@ -1540,6 +1553,66 @@ function resolvePackageVersion(targetDir) {
1540
1553
  catch { /* ignore */ }
1541
1554
  return null;
1542
1555
  }
1556
+ /**
1557
+ * Resolve package name from pyproject.toml (Python projects).
1558
+ */
1559
+ function resolvePackageNamePyproject(targetDir) {
1560
+ try {
1561
+ const fs = require('fs');
1562
+ const path = require('path');
1563
+ const pyprojectPath = path.join(targetDir, 'pyproject.toml');
1564
+ if (fs.existsSync(pyprojectPath)) {
1565
+ const content = fs.readFileSync(pyprojectPath, 'utf-8');
1566
+ // Match [project] section's name field
1567
+ const nameMatch = content.match(/\[project\][\s\S]*?name\s*=\s*"([^"]+)"/);
1568
+ if (nameMatch)
1569
+ return nameMatch[1];
1570
+ // Also try [tool.poetry] section
1571
+ const poetryMatch = content.match(/\[tool\.poetry\][\s\S]*?name\s*=\s*"([^"]+)"/);
1572
+ if (poetryMatch)
1573
+ return poetryMatch[1];
1574
+ }
1575
+ }
1576
+ catch { /* ignore */ }
1577
+ return null;
1578
+ }
1579
+ /**
1580
+ * Resolve package version from pyproject.toml (Python projects).
1581
+ */
1582
+ function resolvePackageVersionPyproject(targetDir) {
1583
+ try {
1584
+ const fs = require('fs');
1585
+ const path = require('path');
1586
+ const pyprojectPath = path.join(targetDir, 'pyproject.toml');
1587
+ if (fs.existsSync(pyprojectPath)) {
1588
+ const content = fs.readFileSync(pyprojectPath, 'utf-8');
1589
+ const versionMatch = content.match(/\[project\][\s\S]*?version\s*=\s*"([^"]+)"/);
1590
+ if (versionMatch)
1591
+ return versionMatch[1];
1592
+ const poetryMatch = content.match(/\[tool\.poetry\][\s\S]*?version\s*=\s*"([^"]+)"/);
1593
+ if (poetryMatch)
1594
+ return poetryMatch[1];
1595
+ }
1596
+ }
1597
+ catch { /* ignore */ }
1598
+ return null;
1599
+ }
1600
+ /**
1601
+ * Resolve the repository URL from the git remote 'origin'.
1602
+ */
1603
+ function resolveRepoUrl(targetDir) {
1604
+ try {
1605
+ const { execSync } = require('child_process');
1606
+ const url = execSync('git remote get-url origin', {
1607
+ cwd: targetDir,
1608
+ encoding: 'utf-8',
1609
+ stdio: ['pipe', 'pipe', 'pipe'],
1610
+ }).trim();
1611
+ return url || null;
1612
+ }
1613
+ catch { /* ignore */ }
1614
+ return null;
1615
+ }
1543
1616
  /**
1544
1617
  * Handle community contribution after a scan completes.
1545
1618
  *
@@ -1622,13 +1695,13 @@ program
1622
1695
  .command('secure')
1623
1696
  .description(`Scan and harden your agent setup
1624
1697
 
1625
- Performs 187 security checks across 39 categories:
1698
+ Performs 199 security checks across 60 categories:
1626
1699
  • Credentials: API key exposure, secrets in configs
1627
1700
  • MCP: Server configs, tool permissions, secrets
1628
1701
  • Network: TLS, interface bindings, CORS
1629
1702
  • Prompt: Injection defenses, role protection
1630
1703
  • Encryption: At-rest encryption, secure hashing
1631
- • And 25 more categories...
1704
+ • And 54 more categories...
1632
1705
 
1633
1706
  Benchmark mode (--benchmark):
1634
1707
  oasb-1 OASB-1 infrastructure compliance (L1/L2/L3 levels)
@@ -1669,11 +1742,13 @@ Examples:
1669
1742
  .option('-l, --level <level>', 'Benchmark level: L1 (Essential), L2 (Standard), L3 (Hardened)', 'L1')
1670
1743
  .option('-c, --category <name>', 'Filter to specific benchmark category')
1671
1744
  .option('--deep', 'Enable LLM-powered semantic analysis (requires ANTHROPIC_API_KEY)')
1745
+ .option('--scan-depth <depth>', 'CAAT scan depth: quick (config+creds only), standard (default), deep (+ LLM analysis)', 'standard')
1746
+ .option('--ci-publish', 'Submit scan results to registry CI endpoint (requires CI_SCAN_HMAC_SECRET env)')
1672
1747
  .option('--publish', 'Push scan results to the OpenA2A Registry')
1673
1748
  .option('--registry-report', 'Post results to OpenA2A Registry')
1674
1749
  .option('--no-registry', 'Skip auto-publishing results to OpenA2A Registry')
1675
1750
  .option('--version-id <id>', 'Registry version ID to report against')
1676
- .option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)', process.env.REGISTRY_URL || 'https://api.oa2a.org')
1751
+ .option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)', validateRegistryUrl(process.env.REGISTRY_URL || 'https://api.oa2a.org'))
1677
1752
  .option('--registry-key <key>', 'Registry API key (default: REGISTRY_API_KEY env)')
1678
1753
  .option('--contribute', 'Share anonymized scan findings with OpenA2A Registry (overrides config)')
1679
1754
  .option('--no-contribute', 'Do not share findings for this scan (overrides config)')
@@ -1733,8 +1808,15 @@ Examples:
1733
1808
  console.log(`\nScanning ${targetDir}...\n`);
1734
1809
  }
1735
1810
  }
1736
- // Deep mode progress display
1737
- const isDeep = options.deep ?? false;
1811
+ // Validate scan depth
1812
+ const validDepths = ['quick', 'standard', 'deep'];
1813
+ const scanDepth = (options.scanDepth || 'standard');
1814
+ if (!validDepths.includes(scanDepth)) {
1815
+ console.error(`Error: Invalid scan depth '${options.scanDepth}'. Use: ${validDepths.join(', ')}`);
1816
+ process.exit(1);
1817
+ }
1818
+ // Deep mode: --deep flag OR --scan-depth deep
1819
+ const isDeep = options.deep ?? (scanDepth === 'deep');
1738
1820
  const onProgress = isDeep && format === 'text'
1739
1821
  ? (msg) => process.stdout.write(msg)
1740
1822
  : undefined;
@@ -1745,6 +1827,9 @@ Examples:
1745
1827
  console.log(` npx ${CLI_PREFIX} init-mcp\n`);
1746
1828
  }
1747
1829
  }
1830
+ if (scanDepth === 'quick' && format === 'text') {
1831
+ console.log(`Scan depth: quick (config checks + credential detection only)\n`);
1832
+ }
1748
1833
  const scanner = new index_1.HardeningScanner();
1749
1834
  const scanStartMs = Date.now();
1750
1835
  const result = await scanner.scan({
@@ -1753,6 +1838,7 @@ Examples:
1753
1838
  dryRun: options.dryRun ?? false,
1754
1839
  ignore: ignoreList,
1755
1840
  deep: isDeep,
1841
+ scanDepth,
1756
1842
  cliName: CLI_PREFIX,
1757
1843
  onProgress,
1758
1844
  });
@@ -1862,7 +1948,7 @@ Examples:
1862
1948
  if (options.publish && options.registry !== false) {
1863
1949
  try {
1864
1950
  const { publishScanResults } = await Promise.resolve().then(() => __importStar(require('./registry/publish')));
1865
- const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org';
1951
+ const registryUrl = validateRegistryUrl(options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org');
1866
1952
  const packageName = resolvePackageName(targetDir);
1867
1953
  if (packageName) {
1868
1954
  const publishData = {
@@ -2060,7 +2146,7 @@ Examples:
2060
2146
  if (options.versionId || options.registryReport) {
2061
2147
  try {
2062
2148
  const core = await Promise.resolve().then(() => __importStar(require('./index')));
2063
- const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org';
2149
+ const registryUrl = validateRegistryUrl(options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org');
2064
2150
  if (options.versionId) {
2065
2151
  // Authenticated path: existing behavior (version-id + API key)
2066
2152
  const registryKey = options.registryKey || process.env.REGISTRY_API_KEY;
@@ -2107,7 +2193,7 @@ Examples:
2107
2193
  else if (options.publish) {
2108
2194
  try {
2109
2195
  const { publishScanResults, formatPublishOutput } = await Promise.resolve().then(() => __importStar(require('./registry/publish')));
2110
- const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org';
2196
+ const registryUrl = validateRegistryUrl(options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org');
2111
2197
  const packageName = resolvePackageName(targetDir);
2112
2198
  if (!packageName) {
2113
2199
  console.error('\nCould not determine package name. Publish requires a package.json with a name field.');
@@ -2139,6 +2225,97 @@ Examples:
2139
2225
  console.error('Scan results are still available locally.');
2140
2226
  }
2141
2227
  }
2228
+ // CI publish: submit results to registry CAAT pipeline endpoint
2229
+ if (options.ciPublish) {
2230
+ const hmacSecret = process.env.CI_SCAN_HMAC_SECRET;
2231
+ if (!hmacSecret) {
2232
+ console.error('\nError: --ci-publish requires the CI_SCAN_HMAC_SECRET environment variable.');
2233
+ process.exit(1);
2234
+ }
2235
+ try {
2236
+ const { RegistryClient } = await Promise.resolve().then(() => __importStar(require('./registry/client')));
2237
+ const { computeTreeHash } = await Promise.resolve().then(() => __importStar(require('./registry/publish')));
2238
+ const registryUrl = validateRegistryUrl(options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org');
2239
+ const packageName = resolvePackageName(targetDir) || resolvePackageNamePyproject(targetDir);
2240
+ const packageVersion = resolvePackageVersion(targetDir) || resolvePackageVersionPyproject(targetDir);
2241
+ const repoUrl = resolveRepoUrl(targetDir);
2242
+ if (!packageName) {
2243
+ console.error('\nCould not determine package name from package.json or pyproject.toml.');
2244
+ }
2245
+ else if (!repoUrl) {
2246
+ console.error('\nCould not determine repo URL from git remote. Ensure a git remote is configured.');
2247
+ }
2248
+ else {
2249
+ // Compute CAAT tree hash
2250
+ let contentHash = '';
2251
+ try {
2252
+ contentHash = computeTreeHash(targetDir);
2253
+ }
2254
+ catch {
2255
+ console.error('Warning: Could not compute tree hash. Using empty hash.');
2256
+ }
2257
+ // Count severity
2258
+ const failed = result.findings.filter((f) => !f.passed && !f.fixed);
2259
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
2260
+ for (const f of failed) {
2261
+ if (f.severity === 'critical')
2262
+ counts.critical++;
2263
+ else if (f.severity === 'high')
2264
+ counts.high++;
2265
+ else if (f.severity === 'medium')
2266
+ counts.medium++;
2267
+ else if (f.severity === 'low')
2268
+ counts.low++;
2269
+ }
2270
+ const status = (counts.critical > 0 || counts.high > 0) ? 'failed'
2271
+ : (counts.medium > 0 || counts.low > 0) ? 'warnings' : 'passed';
2272
+ const client = new RegistryClient({ registryUrl, apiKey: '' });
2273
+ const scanId = `hma-ci-${Date.now()}`;
2274
+ // Get scanner version from package.json
2275
+ let scannerVersion = 'unknown';
2276
+ try {
2277
+ const hmaPackagePath = require('path').resolve(__dirname, '../package.json');
2278
+ scannerVersion = require(hmaPackagePath).version || 'unknown';
2279
+ }
2280
+ catch { /* ignore */ }
2281
+ const ciResult = await client.submitCIScanResult({
2282
+ packageName,
2283
+ packageType: undefined,
2284
+ version: packageVersion ?? undefined,
2285
+ repoUrl,
2286
+ scanId,
2287
+ status,
2288
+ criticalCount: counts.critical,
2289
+ highCount: counts.high,
2290
+ mediumCount: counts.medium,
2291
+ lowCount: counts.low,
2292
+ contentHash,
2293
+ scannerVersion,
2294
+ hmacSecret,
2295
+ rawReport: {
2296
+ generator: 'hackmyagent',
2297
+ totalFindings: result.findings.length,
2298
+ failedFindings: failed.length,
2299
+ scanDepth,
2300
+ },
2301
+ });
2302
+ if (format === 'text') {
2303
+ console.log(`\nCI scan result submitted to registry.`);
2304
+ console.log(` Scan ID: ${scanId}`);
2305
+ console.log(` Valid: ${ciResult.valid}`);
2306
+ console.log(` Trust impact: ${ciResult.trustImpact}\n`);
2307
+ }
2308
+ else if (format === 'json') {
2309
+ console.error(JSON.stringify({ ciPublish: { scanId, ...ciResult } }, null, 2));
2310
+ }
2311
+ }
2312
+ }
2313
+ catch (ciErr) {
2314
+ const msg = ciErr instanceof Error ? ciErr.message : 'unknown error';
2315
+ console.error(`\nFailed to submit CI scan result: ${msg}`);
2316
+ console.error('Scan results are still available locally.');
2317
+ }
2318
+ }
2142
2319
  // Community contribution: share anonymized findings with OpenA2A Registry
2143
2320
  await handleContribution(options.contribute, targetDir, result.findings, scanDurationMs, options.registryUrl, format);
2144
2321
  // Star prompt (interactive TTY only, text format only)
@@ -2738,7 +2915,7 @@ Examples:
2738
2915
  .option('--registry-report', 'Post results to OpenA2A Registry')
2739
2916
  .option('--no-registry', 'Skip auto-publishing results to OpenA2A Registry')
2740
2917
  .option('--version-id <id>', 'Registry version ID to report against')
2741
- .option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)', process.env.REGISTRY_URL || 'https://api.oa2a.org')
2918
+ .option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)', validateRegistryUrl(process.env.REGISTRY_URL || 'https://api.oa2a.org'))
2742
2919
  .option('--registry-key <key>', 'Registry API key (default: REGISTRY_API_KEY env)')
2743
2920
  .action(async (targetUrl, options) => {
2744
2921
  try {
@@ -2894,7 +3071,7 @@ Examples:
2894
3071
  if (shouldReport) {
2895
3072
  try {
2896
3073
  const core = await Promise.resolve().then(() => __importStar(require('./index')));
2897
- const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org';
3074
+ const registryUrl = validateRegistryUrl(options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org');
2898
3075
  if (options.versionId) {
2899
3076
  // Authenticated path: existing behavior (version-id + API key)
2900
3077
  const registryKey = options.registryKey || process.env.REGISTRY_API_KEY;
@@ -2941,7 +3118,7 @@ Examples:
2941
3118
  else if (options.publish && targetType !== 'local') {
2942
3119
  try {
2943
3120
  const { publishScanResults, formatPublishOutput } = await Promise.resolve().then(() => __importStar(require('./registry/publish')));
2944
- const regUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org';
3121
+ const regUrl = validateRegistryUrl(options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org');
2945
3122
  const packageName = target.url || targetUrl || 'unknown';
2946
3123
  if (format === 'text') {
2947
3124
  console.log('\nPublishing results to registry...\n');
@@ -4126,7 +4303,7 @@ Examples:
4126
4303
  console.log(`\n Detected: ${result.tool}\n`);
4127
4304
  console.log(` Added HackMyAgent MCP server to ${result.configPath}\n`);
4128
4305
  console.log(` Available tools in ${result.tool}:`);
4129
- console.log(` hackmyagent_scan — 187 checks + structural analysis`);
4306
+ console.log(` hackmyagent_scan — 199 checks + structural analysis`);
4130
4307
  console.log(` hackmyagent_deep_scan — Full analysis with LLM reasoning`);
4131
4308
  console.log(` hackmyagent_analyze_file — Analyze a single file`);
4132
4309
  console.log(` hackmyagent_benchmark — OASB-1 compliance assessment\n`);
@@ -4215,7 +4392,7 @@ Examples:
4215
4392
  .option('--fail-below <score>', 'Exit 1 if score below threshold (0-100)')
4216
4393
  .option('--deep', 'Enable LLM semantic analysis for ambiguous controls (requires claude CLI or ANTHROPIC_API_KEY)')
4217
4394
  .option('--publish', 'Push scan results to the OpenA2A Registry')
4218
- .option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)', process.env.REGISTRY_URL || 'https://api.oa2a.org')
4395
+ .option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)', validateRegistryUrl(process.env.REGISTRY_URL || 'https://api.oa2a.org'))
4219
4396
  .option('--contribute', 'Share anonymized scan findings with OpenA2A Registry (overrides config)')
4220
4397
  .option('--no-contribute', 'Do not share findings for this scan (overrides config)')
4221
4398
  .option('--ci', 'CI mode: suppress interactive prompts, exit non-zero on findings')
@@ -4248,7 +4425,7 @@ Examples:
4248
4425
  if (options.publish) {
4249
4426
  try {
4250
4427
  const { publishScanResults } = await Promise.resolve().then(() => __importStar(require('./registry/publish')));
4251
- const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org';
4428
+ const registryUrl = validateRegistryUrl(options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org');
4252
4429
  const packageName = resolvePackageName(targetDir);
4253
4430
  if (packageName) {
4254
4431
  const publishData = {
@@ -4363,7 +4540,7 @@ Examples:
4363
4540
  if (options.publish) {
4364
4541
  try {
4365
4542
  const { publishScanResults, formatPublishOutput } = await Promise.resolve().then(() => __importStar(require('./registry/publish')));
4366
- const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org';
4543
+ const registryUrl = validateRegistryUrl(options.registryUrl || process.env.REGISTRY_URL || 'https://api.oa2a.org');
4367
4544
  const packageName = resolvePackageName(targetDir);
4368
4545
  if (!packageName) {
4369
4546
  process.stderr.write('Could not determine package name. Publish requires a package.json with a name field.\n');
@@ -4739,10 +4916,10 @@ Examples:
4739
4916
  .option('--audit <file>', 'Audit a dependency file (package.json or requirements.txt)')
4740
4917
  .option('--batch <names...>', 'Batch trust lookup for multiple packages')
4741
4918
  .option('--min-trust <level>', 'Minimum trust level threshold (0-4)', '3')
4742
- .option('--registry-url <url>', 'Registry base URL', REGISTRY_DEFAULT_URL)
4919
+ .option('--registry-url <url>', 'Registry base URL', validateRegistryUrl(REGISTRY_DEFAULT_URL))
4743
4920
  .option('--json', 'Output as JSON')
4744
4921
  .action(async (packageName, opts) => {
4745
- const registryUrl = opts.registryUrl.replace(/\/+$/, '');
4922
+ const registryUrl = validateRegistryUrl(opts.registryUrl).replace(/\/+$/, '');
4746
4923
  const minTrust = parseInt(opts.minTrust, 10);
4747
4924
  if (isNaN(minTrust) || minTrust < 0 || minTrust > 4) {
4748
4925
  process.stderr.write('Error: --min-trust must be a number between 0 and 4\n');
@@ -4821,5 +4998,36 @@ Examples:
4821
4998
  process.exit(1);
4822
4999
  }
4823
5000
  });
5001
+ program
5002
+ .command('check-metadata')
5003
+ .description('Export metadata for all security checks by scanning test fixtures (JSON)')
5004
+ .option('-d, --directory <dir>', 'Directory to scan for check metadata extraction')
5005
+ .action(async (options) => {
5006
+ const { getAttackClass } = require('./hardening/taxonomy');
5007
+ const targetDir = options.directory || process.cwd();
5008
+ // Run a real scan to collect all check metadata from findings
5009
+ const scanner = new index_1.HardeningScanner();
5010
+ const result = await scanner.scan({ targetDir, autoFix: false, scanDepth: 'deep' });
5011
+ const metadata = {};
5012
+ for (const finding of result.findings) {
5013
+ if (!metadata[finding.checkId]) {
5014
+ metadata[finding.checkId] = {
5015
+ checkId: finding.checkId,
5016
+ name: finding.name,
5017
+ category: finding.category,
5018
+ attackClass: getAttackClass(finding.checkId) || '',
5019
+ severity: finding.severity,
5020
+ fix: finding.fix || '',
5021
+ guidance: finding.guidance || '',
5022
+ };
5023
+ }
5024
+ }
5025
+ writeJsonStdout({ totalChecks: Object.keys(metadata).length, checks: metadata });
5026
+ });
5027
+ // Show help and exit 0 when no arguments provided
5028
+ if (process.argv.length <= 2) {
5029
+ program.outputHelp();
5030
+ process.exit(0);
5031
+ }
4824
5032
  program.parse();
4825
5033
  //# sourceMappingURL=cli.js.map