hackmyagent 0.9.5 → 0.9.7

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
@@ -41,6 +41,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
41
41
  const commander_1 = require("commander");
42
42
  const index_1 = require("./index");
43
43
  const program = new commander_1.Command();
44
+ // Write JSON to stdout synchronously with retry for pipe backpressure.
45
+ // process.stdout.write() is async and gets truncated when process.exit()
46
+ // runs before the stream flushes. fs.writeFileSync(1, ...) can fail with
47
+ // EAGAIN on non-blocking pipes when the buffer (64KB on macOS) fills up.
48
+ // This function writes in chunks with retry to handle both cases.
49
+ function writeJsonStdout(data) {
50
+ const fs = require('fs');
51
+ const buf = Buffer.from(JSON.stringify(data, null, 2) + '\n');
52
+ let offset = 0;
53
+ while (offset < buf.length) {
54
+ try {
55
+ const written = fs.writeSync(1, buf, offset, buf.length - offset);
56
+ offset += written;
57
+ }
58
+ catch (e) {
59
+ if (e && typeof e === 'object' && 'code' in e && e.code === 'EAGAIN') {
60
+ // Pipe buffer full — spin-wait briefly then retry
61
+ continue;
62
+ }
63
+ throw e;
64
+ }
65
+ }
66
+ }
44
67
  // Check for NO_COLOR env or non-TTY to disable colors by default
45
68
  const noColorEnv = process.env.NO_COLOR !== undefined || process.stdout.isTTY === false;
46
69
  // Color codes - will be cleared if --no-color is passed
@@ -50,14 +73,15 @@ let colors = {
50
73
  red: '\x1b[31m',
51
74
  brightRed: '\x1b[91m',
52
75
  cyan: '\x1b[36m',
76
+ dim: '\x1b[2m',
53
77
  reset: '\x1b[0m',
54
78
  };
55
79
  if (noColorEnv) {
56
- colors = { green: '', yellow: '', red: '', brightRed: '', cyan: '', reset: '' };
80
+ colors = { green: '', yellow: '', red: '', brightRed: '', cyan: '', dim: '', reset: '' };
57
81
  }
58
82
  // Deprecation warning for removed HMAC auth
59
83
  if (process.env.HMA_COMMUNITY_SECRET) {
60
- console.error('Warning: HMA_COMMUNITY_SECRET is deprecated and no longer used. Scan tokens are now issued automatically.');
84
+ console.error('Note: HMA_COMMUNITY_SECRET is deprecated and no longer used. Scan tokens are now issued automatically.');
61
85
  }
62
86
  program
63
87
  .name('hackmyagent')
@@ -85,15 +109,15 @@ Examples:
85
109
  .hook('preAction', (thisCommand) => {
86
110
  const opts = thisCommand.opts();
87
111
  if (opts.color === false) {
88
- colors = { green: '', yellow: '', red: '', brightRed: '', cyan: '', reset: '' };
112
+ colors = { green: '', yellow: '', red: '', brightRed: '', cyan: '', dim: '', reset: '' };
89
113
  }
90
114
  });
91
115
  // Risk level colors and symbols
92
116
  const RISK_DISPLAY = {
93
- low: { symbol: '', color: () => colors.green },
94
- medium: { symbol: '⚠️', color: () => colors.yellow },
95
- high: { symbol: '🔴', color: () => colors.red },
96
- critical: { symbol: '🚨', color: () => colors.brightRed },
117
+ low: { symbol: '[+]', color: () => colors.green },
118
+ medium: { symbol: '[~]', color: () => colors.yellow },
119
+ high: { symbol: '[!]', color: () => colors.red },
120
+ critical: { symbol: '[!!]', color: () => colors.brightRed },
97
121
  };
98
122
  const RESET = () => colors.reset;
99
123
  program
@@ -122,7 +146,7 @@ Examples:
122
146
  skipDnsVerification: options.offline,
123
147
  });
124
148
  if (options.json) {
125
- console.log(JSON.stringify(result, null, 2));
149
+ writeJsonStdout(result);
126
150
  return;
127
151
  }
128
152
  const risk = RISK_DISPLAY[result.risk];
@@ -130,19 +154,19 @@ Examples:
130
154
  // Publisher info
131
155
  console.log(`Publisher: @${result.publisher.name}`);
132
156
  if (result.publisher.verified) {
133
- console.log(`├─ Verified via DNS`);
157
+ console.log(`├─ [+] Verified via DNS`);
134
158
  if (result.publisher.domain) {
135
- console.log(`├─ 🌐 Domain: ${result.publisher.domain}`);
159
+ console.log(`├─ Domain: ${result.publisher.domain}`);
136
160
  }
137
161
  if (result.publisher.verifiedAt && options.verbose) {
138
- console.log(`└─ 📅 Verified at: ${result.publisher.verifiedAt.toISOString()}`);
162
+ console.log(`└─ Verified at: ${result.publisher.verifiedAt.toISOString()}`);
139
163
  }
140
164
  else {
141
165
  console.log(`└─ Method: DNS TXT record`);
142
166
  }
143
167
  }
144
168
  else {
145
- console.log(`├─ Not verified`);
169
+ console.log(`├─ [-] Not verified`);
146
170
  if (result.publisher.failureReason && options.verbose) {
147
171
  console.log(`└─ Reason: ${result.publisher.failureReason}`);
148
172
  }
@@ -161,13 +185,13 @@ Examples:
161
185
  }
162
186
  else {
163
187
  for (const perm of result.permissions.safe) {
164
- console.log(`├─ ${perm}`);
188
+ console.log(`├─ [+] ${perm}`);
165
189
  }
166
190
  for (const perm of result.permissions.reviewNeeded) {
167
- console.log(`├─ ⚠️ ${perm} (review needed)`);
191
+ console.log(`├─ [~] ${perm} (review needed)`);
168
192
  }
169
193
  for (const perm of result.permissions.dangerous) {
170
- console.log(`├─ ${perm} (DANGEROUS)`);
194
+ console.log(`├─ [!] ${perm} (elevated risk)`);
171
195
  }
172
196
  console.log(`└─ Risk score: ${result.permissions.riskScore}/100`);
173
197
  }
@@ -175,10 +199,10 @@ Examples:
175
199
  // Revocation
176
200
  console.log('Revocation:');
177
201
  if (result.revocation.revoked) {
178
- console.log(`└─ 🚨 REVOKED: ${result.revocation.reason}`);
202
+ console.log(`└─ [!!] Revoked: ${result.revocation.reason}`);
179
203
  }
180
204
  else {
181
- console.log(`└─ Not on blocklist`);
205
+ console.log(`└─ [+] Not on blocklist`);
182
206
  }
183
207
  console.log();
184
208
  // Verbose details
@@ -198,10 +222,10 @@ Examples:
198
222
  });
199
223
  // Severity colors and symbols for secure command
200
224
  const SEVERITY_DISPLAY = {
201
- critical: { symbol: '🔴', color: () => colors.brightRed },
202
- high: { symbol: '🟠', color: () => colors.red },
203
- medium: { symbol: '🟡', color: () => colors.yellow },
204
- low: { symbol: '🟢', color: () => colors.green },
225
+ critical: { symbol: '[!!]', color: () => colors.brightRed },
226
+ high: { symbol: '[!]', color: () => colors.red },
227
+ medium: { symbol: '[~]', color: () => colors.yellow },
228
+ low: { symbol: '[.]', color: () => colors.green },
205
229
  };
206
230
  function groupFindingsBySeverity(findings) {
207
231
  const grouped = {
@@ -316,7 +340,7 @@ function generateBenchmarkReport(findings, level, categoryFilter) {
316
340
  const l3Compliance = l3Total > 0 ? Math.round((l3Passed / l3Total) * 100) : 100;
317
341
  const totalScored = l1Total + l2Total + l3Total;
318
342
  const totalPassed = l1Passed + l2Passed + l3Passed;
319
- const overallCompliance = totalScored > 0 ? Math.round((totalPassed / totalScored) * 100) : 100;
343
+ const overallCompliance = totalScored > 0 ? Math.round((totalPassed / totalScored) * 100) : 0;
320
344
  // Group results by category
321
345
  const categoryResults = [];
322
346
  for (const category of index_1.OASB_1_CATEGORIES) {
@@ -328,7 +352,7 @@ function generateBenchmarkReport(findings, level, categoryFilter) {
328
352
  const passed = catControls.filter((r) => r.status === 'passed').length;
329
353
  const failed = catControls.filter((r) => r.status === 'failed').length;
330
354
  const unverified = catControls.filter((r) => r.status === 'unverified').length;
331
- const compliance = (passed + failed) > 0 ? Math.round((passed / (passed + failed)) * 100) : 100;
355
+ const compliance = (passed + failed) > 0 ? Math.round((passed / (passed + failed)) * 100) : 0;
332
356
  categoryResults.push({
333
357
  category: category.name,
334
358
  compliance,
@@ -442,14 +466,14 @@ function generateHtmlReport(result) {
442
466
  'Compliant': '#22c55e',
443
467
  'Passing': '#eab308',
444
468
  'Needs Improvement': '#f97316',
445
- 'Failing': '#ef4444',
469
+ 'Not Passing': '#ef4444',
446
470
  }[result.rating] || '#94a3b8';
447
471
  const ratingBg = {
448
472
  'Certified': 'rgba(34, 197, 94, 0.15)',
449
473
  'Compliant': 'rgba(34, 197, 94, 0.15)',
450
474
  'Passing': 'rgba(234, 179, 8, 0.15)',
451
475
  'Needs Improvement': 'rgba(249, 115, 22, 0.15)',
452
- 'Failing': 'rgba(239, 68, 68, 0.15)',
476
+ 'Not Passing': 'rgba(239, 68, 68, 0.15)',
453
477
  }[result.rating] || 'rgba(148, 163, 184, 0.15)';
454
478
  // Generate donut chart SVG
455
479
  const donutRadius = 70;
@@ -1390,10 +1414,10 @@ function printBenchmarkReport(result, verbose) {
1390
1414
  'Compliant': colors.green,
1391
1415
  'Passing': colors.yellow,
1392
1416
  'Needs Improvement': colors.yellow,
1393
- 'Failing': colors.red,
1417
+ 'Not Passing': colors.red,
1394
1418
  };
1395
1419
  // Header
1396
- console.log(`\n📋 ${result.benchmark} v${result.version}`);
1420
+ console.log(`\n${result.benchmark} v${result.version}`);
1397
1421
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
1398
1422
  // Level and rating
1399
1423
  const levelNames = {
@@ -1411,13 +1435,18 @@ function printBenchmarkReport(result, verbose) {
1411
1435
  // Category breakdown
1412
1436
  console.log(`Categories:`);
1413
1437
  for (const catResult of result.categories) {
1414
- const statusIcon = catResult.failed === 0 ? '✅' : (catResult.passed > 0 ? '🟡' : '❌');
1415
- console.log(` ${statusIcon} ${catResult.category}: ${catResult.passed}/${catResult.passed + catResult.failed} (${catResult.compliance}%)`);
1438
+ const total = catResult.passed + catResult.failed;
1439
+ if (total === 0) {
1440
+ console.log(` [.] ${catResult.category}: N/A (no controls at this level)`);
1441
+ continue;
1442
+ }
1443
+ const statusIcon = catResult.failed === 0 ? '[+]' : (catResult.passed > 0 ? '[~]' : '[-]');
1444
+ console.log(` ${statusIcon} ${catResult.category}: ${catResult.passed}/${total} (${catResult.compliance}%)`);
1416
1445
  // Show failed controls
1417
1446
  if (verbose || catResult.failed > 0) {
1418
1447
  for (const ctrl of catResult.controls) {
1419
1448
  if (ctrl.status === 'failed') {
1420
- console.log(` ${ctrl.controlId}: ${ctrl.name}`);
1449
+ console.log(` [-] ${ctrl.controlId}: ${ctrl.name}`);
1421
1450
  if (verbose) {
1422
1451
  for (const finding of ctrl.findings) {
1423
1452
  console.log(` └─ ${finding}`);
@@ -1425,7 +1454,7 @@ function printBenchmarkReport(result, verbose) {
1425
1454
  }
1426
1455
  }
1427
1456
  else if (verbose && ctrl.status === 'passed') {
1428
- console.log(` ${ctrl.controlId}: ${ctrl.name}`);
1457
+ console.log(` [+] ${ctrl.controlId}: ${ctrl.name}`);
1429
1458
  }
1430
1459
  else if (verbose && ctrl.status === 'unverified') {
1431
1460
  // Look up the original control to determine why it's unverified
@@ -1435,7 +1464,7 @@ function printBenchmarkReport(result, verbose) {
1435
1464
  const reason = originalControl && (originalControl.verification === 'manual' || originalControl.verification === 'forward')
1436
1465
  ? 'manual/forward'
1437
1466
  : 'no scanner data';
1438
- console.log(` ${ctrl.controlId}: ${ctrl.name} (${reason})`);
1467
+ console.log(` [?] ${ctrl.controlId}: ${ctrl.name} (${reason})`);
1439
1468
  }
1440
1469
  }
1441
1470
  }
@@ -1445,7 +1474,7 @@ function printBenchmarkReport(result, verbose) {
1445
1474
  // Compliance breakdown by level
1446
1475
  if (verbose) {
1447
1476
  console.log(`\nCompliance by level: L1=${result.l1Compliance}% L2=${result.l2Compliance}% L3=${result.l3Compliance}%`);
1448
- console.log(`Legend: = Manual/Forward verification required`);
1477
+ console.log(`Legend: [?] = Manual/Forward verification required`);
1449
1478
  }
1450
1479
  // Show appropriate next step based on current level
1451
1480
  if (result.level === 'L1') {
@@ -1472,9 +1501,12 @@ function resolvePackageName(targetDir) {
1472
1501
  }
1473
1502
  }
1474
1503
  catch { /* ignore */ }
1475
- // Fallback: use directory name
1504
+ // Fallback: use directory name, resolving "." to the actual directory name
1476
1505
  const path = require('path');
1477
- return path.basename(targetDir);
1506
+ const resolved = path.resolve(targetDir);
1507
+ const name = path.basename(resolved);
1508
+ // Skip names that are clearly not package names
1509
+ return name && name !== '.' && name !== '..' ? name : null;
1478
1510
  }
1479
1511
  function resolvePackageVersion(targetDir) {
1480
1512
  try {
@@ -1513,7 +1545,6 @@ Output formats (--format):
1513
1545
  json Machine-readable JSON
1514
1546
  sarif GitHub Security tab / IDE integration
1515
1547
  html Shareable compliance report
1516
- asp Agent Security Profile (our format)
1517
1548
 
1518
1549
  Severities: critical, high, medium, low
1519
1550
  Exit code 1 if critical/high issues found (or non-compliant in benchmark mode).
@@ -1533,7 +1564,7 @@ Examples:
1533
1564
  .option('--dry-run', 'Preview fixes without applying them (use with --fix)')
1534
1565
  .option('--ignore <checks>', 'Comma-separated check IDs to skip (e.g., CRED-001,GIT-002)')
1535
1566
  .option('--json', 'Output as JSON (deprecated: use --format json)')
1536
- .option('-f, --format <format>', 'Output format: text, json, sarif, html, asp (default: text)', 'text')
1567
+ .option('-f, --format <format>', 'Output format: text, json, sarif, html (default: text)', 'text')
1537
1568
  .option('-o, --output <file>', 'Write output to file instead of stdout')
1538
1569
  .option('--fail-below <percent>', 'Exit 1 if compliance below threshold (0-100)')
1539
1570
  .option('-v, --verbose', 'Show all checks including passed ones')
@@ -1587,10 +1618,10 @@ Examples:
1587
1618
  // Only show progress for text output
1588
1619
  if (format === 'text') {
1589
1620
  if (options.dryRun) {
1590
- console.log(`\n🔍 Scanning ${targetDir} (dry-run)...\n`);
1621
+ console.log(`\nScanning ${targetDir} (dry-run)...\n`);
1591
1622
  }
1592
1623
  else {
1593
- console.log(`\n🔍 Scanning ${targetDir}...\n`);
1624
+ console.log(`\nScanning ${targetDir}...\n`);
1594
1625
  }
1595
1626
  }
1596
1627
  // Deep mode progress display
@@ -1624,7 +1655,7 @@ Examples:
1624
1655
  const govScore = govResult.score;
1625
1656
  const compositeScore = Math.round((infraScore + govScore) / 2);
1626
1657
  if (format === 'json') {
1627
- console.log(JSON.stringify({
1658
+ const jsonOutput = JSON.stringify({
1628
1659
  benchmark: 'OASB-2',
1629
1660
  infraScore,
1630
1661
  govScore,
@@ -1632,7 +1663,15 @@ Examples:
1632
1663
  conformance: govResult.conformance,
1633
1664
  infraResult,
1634
1665
  govResult,
1635
- }, null, 2));
1666
+ }, null, 2);
1667
+ if (options.output) {
1668
+ require('fs').writeFileSync(options.output, jsonOutput);
1669
+ console.error(`Report written to ${options.output}`);
1670
+ }
1671
+ else {
1672
+ const fs = require('fs');
1673
+ fs.writeFileSync(1, jsonOutput + '\n');
1674
+ }
1636
1675
  }
1637
1676
  else {
1638
1677
  process.stdout.write('\nOASB v2 Composite Security Assessment\n');
@@ -1700,13 +1739,22 @@ Examples:
1700
1739
  process.exit(1);
1701
1740
  }
1702
1741
  // Exit with non-zero if failing or needs improvement (default behavior)
1703
- if (failBelow === undefined && (benchmarkResult.rating === 'Failing' || benchmarkResult.rating === 'Needs Improvement')) {
1742
+ if (failBelow === undefined && (benchmarkResult.rating === 'Not Passing' || benchmarkResult.rating === 'Needs Improvement')) {
1704
1743
  process.exit(1);
1705
1744
  }
1706
1745
  return;
1707
1746
  }
1708
1747
  if (format === 'json') {
1709
- console.log(JSON.stringify(result, null, 2));
1748
+ if (options.output) {
1749
+ require('fs').writeFileSync(options.output, JSON.stringify(result, null, 2) + '\n');
1750
+ console.error(`Report written to ${options.output}`);
1751
+ }
1752
+ else {
1753
+ writeJsonStdout(result);
1754
+ }
1755
+ const critHigh = result.findings.filter((f) => !f.passed && !f.fixed && (f.severity === 'critical' || f.severity === 'high'));
1756
+ if (critHigh.length > 0)
1757
+ process.exitCode = 1;
1710
1758
  return;
1711
1759
  }
1712
1760
  // Handle SARIF/HTML/ASP for non-benchmark mode
@@ -1763,7 +1811,12 @@ Examples:
1763
1811
  scoreExtra += ` (${sa.cachedResults} cached)`;
1764
1812
  }
1765
1813
  }
1766
- console.log(`${projectTypeLabel} | Score: ${result.score}/${result.maxScore}${scoreExtra}\n`);
1814
+ console.log(`${projectTypeLabel} | Score: ${result.score}/${result.maxScore}${scoreExtra}`);
1815
+ if (issues.length > 0) {
1816
+ const recoverable = Math.min(result.maxScore - result.score, result.maxScore);
1817
+ console.log(` Path forward: +${recoverable} recoverable by addressing ${issues.length} issue${issues.length === 1 ? '' : 's'}`);
1818
+ }
1819
+ console.log('');
1767
1820
  // No issues? Say so and exit
1768
1821
  if (issues.length === 0 && fixedFindings.length === 0) {
1769
1822
  console.log(`${colors.green}No issues found.${RESET()}\n`);
@@ -1786,8 +1839,39 @@ Examples:
1786
1839
  if (finding.fix) {
1787
1840
  console.log(` ${colors.cyan}Fix:${RESET()} ${finding.fix}`);
1788
1841
  }
1842
+ if (options.verbose) {
1843
+ console.log(` ${colors.dim}Check: ${finding.checkId} | Category: ${finding.category}${RESET()}`);
1844
+ if (finding.file) {
1845
+ console.log(` ${colors.dim}File: ${finding.file}${finding.line ? ` (line ${finding.line})` : ''}${RESET()}`);
1846
+ }
1847
+ if (finding.message && finding.message !== finding.description) {
1848
+ console.log(` ${colors.dim}Detail: ${finding.message}${RESET()}`);
1849
+ }
1850
+ if (finding.details && Object.keys(finding.details).length > 0) {
1851
+ for (const [key, value] of Object.entries(finding.details)) {
1852
+ console.log(` ${colors.dim}${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}${RESET()}`);
1853
+ }
1854
+ }
1855
+ }
1789
1856
  console.log();
1790
1857
  }
1858
+ // Severity breakdown summary
1859
+ const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
1860
+ for (const f of issues) {
1861
+ severityCounts[f.severity]++;
1862
+ }
1863
+ const summaryParts = [];
1864
+ if (severityCounts.critical > 0)
1865
+ summaryParts.push(`${colors.brightRed}Critical: ${severityCounts.critical}${RESET()}`);
1866
+ if (severityCounts.high > 0)
1867
+ summaryParts.push(`${colors.red}High: ${severityCounts.high}${RESET()}`);
1868
+ if (severityCounts.medium > 0)
1869
+ summaryParts.push(`${colors.yellow}Medium: ${severityCounts.medium}${RESET()}`);
1870
+ if (severityCounts.low > 0)
1871
+ summaryParts.push(`${colors.green}Low: ${severityCounts.low}${RESET()}`);
1872
+ if (summaryParts.length > 0) {
1873
+ console.log(`${summaryParts.join(' | ')}\n`);
1874
+ }
1791
1875
  }
1792
1876
  // Print fixed findings
1793
1877
  if (fixedFindings.length > 0) {
@@ -1802,9 +1886,9 @@ Examples:
1802
1886
  console.log(`Undo: hackmyagent rollback ${directory}\n`);
1803
1887
  }
1804
1888
  }
1805
- // Registry reporting: auto-publish to community endpoint by default
1806
- const shouldReport = options.registryReport || (options.registry !== false);
1807
- if (shouldReport) {
1889
+ // Registry reporting: only when explicitly requested via --version-id (CI) or --registry-report
1890
+ // Community contributions are handled by the opena2a CLI wrapper, not HMA directly
1891
+ if (options.versionId || options.registryReport) {
1808
1892
  try {
1809
1893
  const core = await Promise.resolve().then(() => __importStar(require('./index')));
1810
1894
  const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://registry.opena2a.org';
@@ -1841,8 +1925,8 @@ Examples:
1841
1925
  }
1842
1926
  }
1843
1927
  }
1844
- catch (reportErr) {
1845
- console.error(`Registry: failed to report scan results: ${reportErr.message || reportErr}`);
1928
+ catch (_reportErr) {
1929
+ // Silently ignore registry errors - they are not relevant to local scan results
1846
1930
  }
1847
1931
  }
1848
1932
  // Star prompt (interactive TTY only, text format only)
@@ -1862,10 +1946,10 @@ Examples:
1862
1946
  });
1863
1947
  // Severity display for external scan findings
1864
1948
  const FINDING_SEVERITY_DISPLAY = {
1865
- critical: { symbol: '🔴', color: () => colors.brightRed },
1866
- high: { symbol: '🟠', color: () => colors.red },
1867
- medium: { symbol: '🟡', color: () => colors.yellow },
1868
- low: { symbol: '🟢', color: () => colors.green },
1949
+ critical: { symbol: '[!!]', color: () => colors.brightRed },
1950
+ high: { symbol: '[!]', color: () => colors.red },
1951
+ medium: { symbol: '[~]', color: () => colors.yellow },
1952
+ low: { symbol: '[.]', color: () => colors.green },
1869
1953
  };
1870
1954
  function groupExternalFindingsBySeverity(findings) {
1871
1955
  const grouped = {
@@ -1916,29 +2000,36 @@ function assessRiskLevel(findings) {
1916
2000
  const mediumCount = findings.filter((f) => f.severity === 'medium').length;
1917
2001
  if (criticalCount > 0) {
1918
2002
  return {
1919
- level: 'CRITICAL',
2003
+ level: 'Critical',
1920
2004
  color: colors.brightRed,
1921
- description: 'Immediate action required. Your OpenClaw installation has critical vulnerabilities.',
2005
+ description: `${criticalCount} critical finding(s) with recommended fixes available.`,
1922
2006
  };
1923
2007
  }
1924
2008
  if (highCount > 0) {
1925
2009
  return {
1926
- level: 'HIGH',
2010
+ level: 'High',
1927
2011
  color: colors.red,
1928
- description: 'Significant risks detected. Address high-severity issues promptly.',
2012
+ description: `${highCount} high-severity finding(s) detected. Fixes available below.`,
1929
2013
  };
1930
2014
  }
1931
2015
  if (mediumCount > 0) {
1932
2016
  return {
1933
- level: 'MODERATE',
2017
+ level: 'Moderate',
1934
2018
  color: colors.yellow,
1935
- description: 'Some issues found. Review and address when possible.',
2019
+ description: 'Some findings detected. Review the recommendations below.',
2020
+ };
2021
+ }
2022
+ if (findings.length === 0) {
2023
+ return {
2024
+ level: 'None',
2025
+ color: colors.dim,
2026
+ description: 'No OpenClaw configuration detected. Run `hackmyagent secure` for a full scan.',
1936
2027
  };
1937
2028
  }
1938
2029
  return {
1939
- level: 'LOW',
2030
+ level: 'Low',
1940
2031
  color: colors.green,
1941
- description: 'Your OpenClaw installation appears well-secured.',
2032
+ description: 'No critical or high findings detected.',
1942
2033
  };
1943
2034
  }
1944
2035
  program
@@ -1969,13 +2060,13 @@ Examples:
1969
2060
  try {
1970
2061
  const targetDir = detectOpenClawDirectory(directory);
1971
2062
  if (!options.json) {
1972
- console.log(`\n🦞 OpenClaw Security Report`);
2063
+ console.log(`\nOpenClaw Security Report`);
1973
2064
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
1974
2065
  if (options.dryRun) {
1975
- console.log(`🔍 Scanning ${targetDir} (dry-run - previewing fixes)...\n`);
2066
+ console.log(`Scanning ${targetDir} (dry-run - previewing fixes)...\n`);
1976
2067
  }
1977
2068
  else if (options.fix) {
1978
- console.log(`🔧 Scanning and fixing ${targetDir}...\n`);
2069
+ console.log(`Scanning and fixing ${targetDir}...\n`);
1979
2070
  console.log(`${colors.yellow}Auto-fix will:${RESET()}`);
1980
2071
  console.log(` • Bind gateway to 127.0.0.1 (local-only)`);
1981
2072
  console.log(` • Replace plaintext tokens with env var references`);
@@ -1984,7 +2075,7 @@ Examples:
1984
2075
  console.log(`\n${colors.cyan}A backup will be created for rollback if needed.${RESET()}\n`);
1985
2076
  }
1986
2077
  else {
1987
- console.log(`🔍 Scanning ${targetDir}...\n`);
2078
+ console.log(`Scanning ${targetDir}...\n`);
1988
2079
  }
1989
2080
  }
1990
2081
  const scanner = new index_1.HardeningScanner();
@@ -2009,7 +2100,7 @@ Examples:
2009
2100
  passed: passedFindings.length,
2010
2101
  findings: allOpenClawFindings,
2011
2102
  };
2012
- console.log(JSON.stringify(jsonOutput, null, 2));
2103
+ writeJsonStdout(jsonOutput);
2013
2104
  return;
2014
2105
  }
2015
2106
  // Risk assessment
@@ -2020,7 +2111,7 @@ Examples:
2020
2111
  console.log(`Checks: ${allOpenClawFindings.length} total | ${issues.length} issues | ${fixedFindings.length} fixed | ${passedFindings.length} passed\n`);
2021
2112
  // Show issues
2022
2113
  if (issues.length > 0) {
2023
- console.log(`${colors.red}Issues Found:${RESET()}\n`);
2114
+ console.log(`${colors.red}Findings:${RESET()}\n`);
2024
2115
  for (const finding of issues) {
2025
2116
  const display = SEVERITY_DISPLAY[finding.severity];
2026
2117
  const location = finding.file
@@ -2028,13 +2119,14 @@ Examples:
2028
2119
  ? `${finding.file}:${finding.line}`
2029
2120
  : finding.file
2030
2121
  : '';
2031
- console.log(`${display.color()}${display.symbol} [${finding.checkId}] ${finding.severity.toUpperCase()}${RESET()}`);
2122
+ const sevLabel = finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1);
2123
+ console.log(`${display.color()}${display.symbol} [${finding.checkId}] ${sevLabel}${RESET()}`);
2032
2124
  console.log(` ${finding.description}`);
2033
2125
  if (location) {
2034
2126
  console.log(` File: ${location}`);
2035
2127
  }
2036
2128
  if (finding.fix) {
2037
- console.log(` ${colors.cyan}Fix:${RESET()} ${finding.fix}`);
2129
+ console.log(` ${colors.cyan}Recommended fix:${RESET()} ${finding.fix}`);
2038
2130
  }
2039
2131
  console.log();
2040
2132
  }
@@ -2044,7 +2136,7 @@ Examples:
2044
2136
  }
2045
2137
  // Show fixed findings
2046
2138
  if (fixedFindings.length > 0) {
2047
- console.log(`${colors.green}Auto-Remediation Applied:${RESET()}\n`);
2139
+ console.log(`${colors.green}Auto-Remediation Applied:${RESET()}\n`);
2048
2140
  for (const finding of fixedFindings) {
2049
2141
  console.log(` ${colors.green}✓${RESET()} [${finding.checkId}] ${finding.name}`);
2050
2142
  if (finding.fixMessage) {
@@ -2053,8 +2145,8 @@ Examples:
2053
2145
  }
2054
2146
  console.log();
2055
2147
  if (result.backupPath) {
2056
- console.log(`${colors.yellow}📁 Backup created:${RESET()} ${result.backupPath}`);
2057
- console.log(`${colors.yellow}↩️ To rollback:${RESET()} hackmyagent rollback ${targetDir}`);
2148
+ console.log(`${colors.yellow}Backup created:${RESET()} ${result.backupPath}`);
2149
+ console.log(`${colors.yellow}To rollback:${RESET()} hackmyagent rollback ${targetDir}`);
2058
2150
  console.log();
2059
2151
  console.log(`${colors.cyan}Note:${RESET()} If you replaced tokens with env vars, set OPENCLAW_AUTH_TOKEN`);
2060
2152
  console.log(` in your environment before starting OpenClaw.\n`);
@@ -2091,7 +2183,7 @@ Detects externally exposed:
2091
2183
  • API keys in responses
2092
2184
  • Debug/admin interfaces
2093
2185
 
2094
- Scoring: A (90-100), B (80-89), C (70-79), D (60-69), F (<60)
2186
+ Scoring: A (90-100), B (80-89), C (70-79), D (60-69), Needs Improvement (<60)
2095
2187
  Exit code 1 if critical/high issues found.
2096
2188
 
2097
2189
  Examples:
@@ -2106,7 +2198,7 @@ Examples:
2106
2198
  .option('-v, --verbose', 'Show detailed finding information')
2107
2199
  .action(async (target, options) => {
2108
2200
  try {
2109
- console.log(`\n🔍 Scanning ${target}...\n`);
2201
+ console.log(`\nScanning ${target}...\n`);
2110
2202
  const scanner = new index_1.ExternalScanner();
2111
2203
  const customPorts = options.ports
2112
2204
  ? options.ports.split(',').map((p) => parseInt(p.trim(), 10))
@@ -2116,7 +2208,7 @@ Examples:
2116
2208
  timeout: parseInt(options.timeout ?? '5000', 10),
2117
2209
  });
2118
2210
  if (options.json) {
2119
- console.log(JSON.stringify(result, null, 2));
2211
+ writeJsonStdout(result);
2120
2212
  return;
2121
2213
  }
2122
2214
  // Print header
@@ -2132,7 +2224,7 @@ Examples:
2132
2224
  console.log(`Open Ports: ${result.openPorts.length > 0 ? result.openPorts.join(', ') : 'None detected'}`);
2133
2225
  console.log(`Duration: ${result.duration}ms\n`);
2134
2226
  if (result.findings.length === 0) {
2135
- console.log(`${colors.green} No security issues found!${RESET()}\n`);
2227
+ console.log(`${colors.green}[+] No security issues found!${RESET()}\n`);
2136
2228
  return;
2137
2229
  }
2138
2230
  // Group findings by severity
@@ -2183,10 +2275,10 @@ Examples:
2183
2275
  .action(async (directory) => {
2184
2276
  try {
2185
2277
  const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
2186
- console.log(`\n🔄 Rolling back changes in ${targetDir}...\n`);
2278
+ console.log(`\nRolling back changes in ${targetDir}...\n`);
2187
2279
  const scanner = new index_1.HardeningScanner();
2188
2280
  await scanner.rollback(targetDir);
2189
- console.log(`${colors.green} Rollback successful!${RESET()}`);
2281
+ console.log(`${colors.green}[+] Rollback successful!${RESET()}`);
2190
2282
  console.log(' All auto-fix changes have been reverted.\n');
2191
2283
  }
2192
2284
  catch (error) {
@@ -2220,12 +2312,6 @@ Target types:
2220
2312
  a2a A2A agent messaging endpoint (/a2a/message)
2221
2313
  local Local simulation (no API calls)
2222
2314
 
2223
- Target types:
2224
- api OpenAI/Anthropic chat completions (default)
2225
- mcp MCP JSON-RPC server (tools/call, tools/list)
2226
- a2a A2A agent messaging endpoint (/a2a/message)
2227
- local Local simulation (no API calls)
2228
-
2229
2315
  Examples:
2230
2316
  $ hackmyagent attack https://api.example.com/v1/chat
2231
2317
  $ hackmyagent attack https://api.example.com --intensity aggressive
@@ -2253,6 +2339,7 @@ Examples:
2253
2339
  .option('--stop-on-success', 'Stop after first successful attack')
2254
2340
  .option('--payload-file <path>', 'JSON file with custom attack payloads')
2255
2341
  .option('--fail-on-vulnerable [severity]', 'Exit code 1 if vulnerabilities found (optional: critical/high/medium/low)')
2342
+ .option('--json', 'Output as JSON (shorthand for --format json)')
2256
2343
  .option('-f, --format <format>', 'Output format: text, json, sarif, html', 'text')
2257
2344
  .option('-o, --output <file>', 'Write output to file')
2258
2345
  .option('-v, --verbose', 'Show detailed output for each payload')
@@ -2319,9 +2406,20 @@ Examples:
2319
2406
  apiFormat = 'a2a';
2320
2407
  }
2321
2408
  // Build target
2409
+ // When --local is used, treat the argument as a directory path, not a URL
2410
+ let localPath;
2411
+ if (targetType === 'local' && targetUrl) {
2412
+ const path = require('path');
2413
+ const fs = require('fs');
2414
+ const resolved = path.resolve(targetUrl);
2415
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
2416
+ localPath = resolved;
2417
+ }
2418
+ }
2322
2419
  const target = {
2323
- url: targetUrl || '',
2420
+ url: localPath ? '' : (targetUrl || ''),
2324
2421
  type: targetType,
2422
+ localPath,
2325
2423
  headers: Object.keys(headers).length > 0 ? headers : undefined,
2326
2424
  apiFormat: apiFormat,
2327
2425
  model: options.model,
@@ -2330,9 +2428,9 @@ Examples:
2330
2428
  a2aSender: options.a2aSender,
2331
2429
  a2aRecipient: options.a2aRecipient,
2332
2430
  };
2333
- // Validate format
2431
+ // Validate format (--json is shorthand for --format json)
2334
2432
  const validFormats = ['text', 'json', 'sarif', 'html'];
2335
- const format = options.format || 'text';
2433
+ const format = options.json ? 'json' : (options.format || 'text');
2336
2434
  if (!validFormats.includes(format)) {
2337
2435
  console.error(`Error: Invalid format '${format}'. Use: ${validFormats.join(', ')}`);
2338
2436
  process.exit(1);
@@ -2350,9 +2448,9 @@ Examples:
2350
2448
  }
2351
2449
  // Show header for text output
2352
2450
  if (format === 'text') {
2353
- console.log(`\n⚔️ HackMyAgent Attack Mode`);
2451
+ console.log(`\nHackMyAgent Attack Mode`);
2354
2452
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
2355
- console.log(`Target: ${target.type === 'local' ? 'Local Simulation' : targetUrl}`);
2453
+ console.log(`Target: ${target.type === 'local' ? (localPath ? `Local Directory: ${localPath}` : 'Local Simulation') : targetUrl}`);
2356
2454
  console.log(`Intensity: ${intensity}`);
2357
2455
  if (customPayloads) {
2358
2456
  console.log(`Payloads: ${customPayloads.length} custom (from file)`);
@@ -2399,8 +2497,8 @@ Examples:
2399
2497
  console.log(output);
2400
2498
  }
2401
2499
  }
2402
- // Registry reporting: auto-publish to community endpoint by default
2403
- const shouldReport = options.registryReport || (options.registry !== false);
2500
+ // Registry reporting: only when explicitly requested via --version-id (CI) or --registry-report
2501
+ const shouldReport = targetType !== 'local' && (options.versionId || options.registryReport);
2404
2502
  if (shouldReport) {
2405
2503
  try {
2406
2504
  const core = await Promise.resolve().then(() => __importStar(require('./index')));
@@ -2433,8 +2531,8 @@ Examples:
2433
2531
  }
2434
2532
  }
2435
2533
  }
2436
- catch (reportErr) {
2437
- console.error(`Registry: failed to report scan results: ${reportErr.message || reportErr}`);
2534
+ catch (_reportErr) {
2535
+ // Silently ignore registry errors - they are not relevant to local scan results
2438
2536
  }
2439
2537
  }
2440
2538
  // Exit with non-zero based on fail policy
@@ -2469,7 +2567,7 @@ function printAttackReport(report, verbose) {
2469
2567
  if (stats.total === 0)
2470
2568
  continue;
2471
2569
  const catInfo = index_1.ATTACK_CATEGORIES[cat];
2472
- const icon = stats.successful > 0 ? '' : '';
2570
+ const icon = stats.successful > 0 ? '[-]' : '[+]';
2473
2571
  console.log(` ${icon} ${catInfo.name}: ${stats.successful}/${stats.total} successful`);
2474
2572
  }
2475
2573
  console.log();
@@ -2495,14 +2593,28 @@ function printAttackReport(report, verbose) {
2495
2593
  if (blocked.length > 0) {
2496
2594
  console.log(`${colors.green}Blocked Attacks (${blocked.length}):${RESET()}`);
2497
2595
  for (const r of blocked) {
2498
- console.log(` ${r.payload.id}: ${r.payload.name}`);
2596
+ console.log(` [+] ${r.payload.id}: ${r.payload.name}`);
2499
2597
  }
2500
2598
  console.log();
2501
2599
  }
2502
2600
  }
2503
2601
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
2504
- console.log(`\nUse --verbose for detailed attack results.`);
2505
- console.log(`Use --intensity aggressive for advanced attacks.\n`);
2602
+ // Inconclusive explanation (when there are inconclusive results)
2603
+ if (report.summary.inconclusive > 0) {
2604
+ console.log(`Note: ${report.summary.inconclusive} result(s) were inconclusive -- no clear success or block`);
2605
+ console.log(`indicators matched the simulated response.`);
2606
+ if (report.targetType === 'local') {
2607
+ console.log(`Run against a live endpoint (without --local) for active testing with real responses.`);
2608
+ }
2609
+ console.log();
2610
+ }
2611
+ if (!verbose) {
2612
+ console.log(`\nUse --verbose for detailed attack results.`);
2613
+ }
2614
+ if (report.intensity !== 'aggressive') {
2615
+ console.log(`Use --intensity aggressive for advanced attacks.`);
2616
+ }
2617
+ console.log();
2506
2618
  }
2507
2619
  // Generate SARIF output for attack results
2508
2620
  function generateAttackSarif(report) {
@@ -3460,7 +3572,7 @@ Examples:
3460
3572
  remediations: r.remediations,
3461
3573
  })),
3462
3574
  };
3463
- console.log(JSON.stringify(jsonOutput, null, 2));
3575
+ writeJsonStdout(jsonOutput);
3464
3576
  if (pluginErrors > 0)
3465
3577
  process.exit(2);
3466
3578
  return;
@@ -3510,7 +3622,7 @@ Examples:
3510
3622
  console.log(`Run 'hackmyagent secure' for a full hardening scan.\n`);
3511
3623
  // Warn if scan is incomplete due to plugin errors
3512
3624
  if (pluginErrors > 0) {
3513
- console.log(`\n${colors.brightRed}[!!] WARNING: ${pluginErrors} plugin(s) failed scan results are incomplete${RESET()}`);
3625
+ console.log(`\n${colors.brightRed}[!!] Note: ${pluginErrors} plugin(s) failed -- scan results are incomplete${RESET()}`);
3514
3626
  console.log(` Re-run with --verbose for details.\n`);
3515
3627
  }
3516
3628
  // Exit with non-zero if critical/high issues remain or scan is incomplete
@@ -3635,7 +3747,7 @@ program
3635
3747
  .description(`Scan behavioral governance coverage
3636
3748
 
3637
3749
  Analyzes SOUL.md (or equivalent governance file) for coverage
3638
- across 8 behavioral governance domains with 68 security controls.
3750
+ across 9 behavioral governance domains with 72 security controls.
3639
3751
 
3640
3752
  Searches for governance files in priority order:
3641
3753
  SOUL.md > system-prompt.md > SYSTEM_PROMPT.md > .cursorrules
@@ -3643,11 +3755,11 @@ Searches for governance files in priority order:
3643
3755
  > instructions.md > constitution.md > agent-config.yaml
3644
3756
 
3645
3757
  Agent profiles filter domains by agent purpose:
3646
- conversational: Injection, Hardcoded, Honesty
3758
+ conversational: Injection, Hardcoded, Honesty, Harm Avoidance
3647
3759
  code-assistant: + Trust, Data
3648
3760
  tool-agent: + Capability, Oversight
3649
3761
  autonomous: + Agentic Safety
3650
- orchestrator: All 8 domains
3762
+ orchestrator: All 9 domains
3651
3763
 
3652
3764
  Maturity levels:
3653
3765
  Hardened (80+), Standard (60-79), Developing (40-59),
@@ -3684,7 +3796,7 @@ Examples:
3684
3796
  });
3685
3797
  // JSON output
3686
3798
  if (options.json) {
3687
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
3799
+ writeJsonStdout(result);
3688
3800
  // Check fail threshold
3689
3801
  if (options.failBelow) {
3690
3802
  const threshold = parseInt(options.failBelow, 10);
@@ -3721,6 +3833,11 @@ Examples:
3721
3833
  }
3722
3834
  continue;
3723
3835
  }
3836
+ if (domain.skippedByTier) {
3837
+ const label = (domain.domain + ':').padEnd(26);
3838
+ process.stdout.write(` ${label}${colors.reset}-- (not applicable at ${result.agentTier} tier)${colors.reset}\n`);
3839
+ continue;
3840
+ }
3724
3841
  const pctColor = domainBar(domain.percentage);
3725
3842
  const label = (domain.domain + ':').padEnd(26);
3726
3843
  process.stdout.write(` ${label}${pctColor}${domain.passed}/${domain.total} (${domain.percentage}%)${colors.reset}\n`);
@@ -3823,7 +3940,7 @@ Examples:
3823
3940
  dryRun: result.dryRun,
3824
3941
  existedBefore: result.existedBefore,
3825
3942
  };
3826
- process.stdout.write(JSON.stringify(jsonResult, null, 2) + '\n');
3943
+ writeJsonStdout(jsonResult);
3827
3944
  return;
3828
3945
  }
3829
3946
  // Text output