hackmyagent 0.9.4 → 0.9.6
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/attack/scanner.d.ts.map +1 -1
- package/dist/attack/scanner.js +6 -3
- package/dist/attack/scanner.js.map +1 -1
- package/dist/attack/types.d.ts +2 -0
- package/dist/attack/types.d.ts.map +1 -1
- package/dist/attack/types.js.map +1 -1
- package/dist/benchmarks/oasb-1.d.ts +1 -1
- package/dist/benchmarks/oasb-1.d.ts.map +1 -1
- package/dist/benchmarks/oasb-1.js +3 -3
- package/dist/benchmarks/oasb-1.js.map +1 -1
- package/dist/cli.js +289 -125
- package/dist/cli.js.map +1 -1
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +42 -15
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/registry/client.d.ts +1 -0
- package/dist/registry/client.d.ts.map +1 -1
- package/dist/registry/client.js +26 -3
- package/dist/registry/client.js.map +1 -1
- package/dist/scanner/external-scanner.js +1 -1
- package/dist/scanner/external-scanner.js.map +1 -1
- package/dist/semantic/deep-scan.js +1 -1
- package/dist/semantic/structural/credential-context.js +1 -1
- package/dist/semantic/structural/credential-context.js.map +1 -1
- package/dist/soul/index.d.ts +2 -2
- package/dist/soul/index.d.ts.map +1 -1
- package/dist/soul/index.js +2 -1
- package/dist/soul/index.js.map +1 -1
- package/dist/soul/scanner.d.ts +39 -4
- package/dist/soul/scanner.d.ts.map +1 -1
- package/dist/soul/scanner.js +365 -107
- package/dist/soul/scanner.js.map +1 -1
- package/dist/soul/templates.d.ts +4 -0
- package/dist/soul/templates.d.ts.map +1 -1
- package/dist/soul/templates.js +249 -0
- package/dist/soul/templates.js.map +1 -1
- package/package.json +1 -1
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('
|
|
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: '
|
|
94
|
-
medium: { symbol: '
|
|
95
|
-
high: { symbol: '
|
|
96
|
-
critical: { symbol: '
|
|
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
|
-
|
|
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(`├─
|
|
157
|
+
console.log(`├─ [+] Verified via DNS`);
|
|
134
158
|
if (result.publisher.domain) {
|
|
135
|
-
console.log(`├─
|
|
159
|
+
console.log(`├─ Domain: ${result.publisher.domain}`);
|
|
136
160
|
}
|
|
137
161
|
if (result.publisher.verifiedAt && options.verbose) {
|
|
138
|
-
console.log(`└─
|
|
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(`├─
|
|
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(`├─
|
|
188
|
+
console.log(`├─ [+] ${perm}`);
|
|
165
189
|
}
|
|
166
190
|
for (const perm of result.permissions.reviewNeeded) {
|
|
167
|
-
console.log(`├─
|
|
191
|
+
console.log(`├─ [~] ${perm} (review needed)`);
|
|
168
192
|
}
|
|
169
193
|
for (const perm of result.permissions.dangerous) {
|
|
170
|
-
console.log(`├─
|
|
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(`└─
|
|
202
|
+
console.log(`└─ [!!] Revoked: ${result.revocation.reason}`);
|
|
179
203
|
}
|
|
180
204
|
else {
|
|
181
|
-
console.log(`└─
|
|
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: '
|
|
202
|
-
high: { symbol: '
|
|
203
|
-
medium: { symbol: '
|
|
204
|
-
low: { symbol: '
|
|
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) :
|
|
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) :
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
1417
|
+
'Not Passing': colors.red,
|
|
1394
1418
|
};
|
|
1395
1419
|
// Header
|
|
1396
|
-
console.log(`\n
|
|
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
|
|
1415
|
-
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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:
|
|
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
|
-
|
|
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
|
|
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(`\
|
|
1621
|
+
console.log(`\nScanning ${targetDir} (dry-run)...\n`);
|
|
1591
1622
|
}
|
|
1592
1623
|
else {
|
|
1593
|
-
console.log(`\
|
|
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
|
-
|
|
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 === '
|
|
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
|
-
|
|
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}
|
|
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:
|
|
1806
|
-
|
|
1807
|
-
if (
|
|
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 (
|
|
1845
|
-
|
|
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: '
|
|
1866
|
-
high: { symbol: '
|
|
1867
|
-
medium: { symbol: '
|
|
1868
|
-
low: { symbol: '
|
|
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: '
|
|
2003
|
+
level: 'Critical',
|
|
1920
2004
|
color: colors.brightRed,
|
|
1921
|
-
description:
|
|
2005
|
+
description: `${criticalCount} critical finding(s) with recommended fixes available.`,
|
|
1922
2006
|
};
|
|
1923
2007
|
}
|
|
1924
2008
|
if (highCount > 0) {
|
|
1925
2009
|
return {
|
|
1926
|
-
level: '
|
|
2010
|
+
level: 'High',
|
|
1927
2011
|
color: colors.red,
|
|
1928
|
-
description:
|
|
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: '
|
|
2017
|
+
level: 'Moderate',
|
|
1934
2018
|
color: colors.yellow,
|
|
1935
|
-
description: 'Some
|
|
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: '
|
|
2030
|
+
level: 'Low',
|
|
1940
2031
|
color: colors.green,
|
|
1941
|
-
description: '
|
|
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(`\
|
|
2063
|
+
console.log(`\nOpenClaw Security Report`);
|
|
1973
2064
|
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
1974
2065
|
if (options.dryRun) {
|
|
1975
|
-
console.log(
|
|
2066
|
+
console.log(`Scanning ${targetDir} (dry-run - previewing fixes)...\n`);
|
|
1976
2067
|
}
|
|
1977
2068
|
else if (options.fix) {
|
|
1978
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
2057
|
-
console.log(`${colors.yellow}
|
|
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),
|
|
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(`\
|
|
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
|
-
|
|
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}
|
|
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(`\
|
|
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}
|
|
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(`\
|
|
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:
|
|
2403
|
-
const shouldReport = options.
|
|
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 (
|
|
2437
|
-
|
|
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(`
|
|
2596
|
+
console.log(` [+] ${r.payload.id}: ${r.payload.name}`);
|
|
2499
2597
|
}
|
|
2500
2598
|
console.log();
|
|
2501
2599
|
}
|
|
2502
2600
|
}
|
|
2503
2601
|
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
2504
|
-
|
|
2505
|
-
|
|
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
|
-
|
|
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}[!!]
|
|
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
|
|
@@ -3591,6 +3703,35 @@ function gradeColor(grade) {
|
|
|
3591
3703
|
case 'F': return colors.brightRed;
|
|
3592
3704
|
}
|
|
3593
3705
|
}
|
|
3706
|
+
function levelColor(level) {
|
|
3707
|
+
switch (level) {
|
|
3708
|
+
case 'hardened': return colors.green;
|
|
3709
|
+
case 'standard': return colors.green;
|
|
3710
|
+
case 'developing': return colors.yellow;
|
|
3711
|
+
case 'initial': return colors.cyan;
|
|
3712
|
+
case 'not-started': return colors.reset;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
function levelLabel(level) {
|
|
3716
|
+
switch (level) {
|
|
3717
|
+
case 'hardened': return 'Hardened';
|
|
3718
|
+
case 'standard': return 'Standard';
|
|
3719
|
+
case 'developing': return 'Developing';
|
|
3720
|
+
case 'initial': return 'Initial';
|
|
3721
|
+
case 'not-started': return 'Not Started';
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Detect how the CLI was invoked to suggest correct command prefix.
|
|
3726
|
+
*/
|
|
3727
|
+
function getCommandPrefix() {
|
|
3728
|
+
const execPath = process.argv[1] || '';
|
|
3729
|
+
if (execPath.includes('npx') || execPath.includes('.npm/_npx') ||
|
|
3730
|
+
execPath.includes('node_modules/.bin')) {
|
|
3731
|
+
return 'npx hackmyagent';
|
|
3732
|
+
}
|
|
3733
|
+
return 'hackmyagent';
|
|
3734
|
+
}
|
|
3594
3735
|
// Domain percentage bar for text output
|
|
3595
3736
|
function domainBar(pct) {
|
|
3596
3737
|
if (pct >= 80)
|
|
@@ -3606,38 +3747,36 @@ program
|
|
|
3606
3747
|
.description(`Scan behavioral governance coverage
|
|
3607
3748
|
|
|
3608
3749
|
Analyzes SOUL.md (or equivalent governance file) for coverage
|
|
3609
|
-
across
|
|
3750
|
+
across 9 behavioral governance domains with 72 security controls.
|
|
3610
3751
|
|
|
3611
3752
|
Searches for governance files in priority order:
|
|
3612
3753
|
SOUL.md > system-prompt.md > SYSTEM_PROMPT.md > .cursorrules
|
|
3613
3754
|
> .github/copilot-instructions.md > CLAUDE.md > .clinerules
|
|
3614
3755
|
> instructions.md > constitution.md > agent-config.yaml
|
|
3615
3756
|
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3757
|
+
Agent profiles filter domains by agent purpose:
|
|
3758
|
+
conversational: Injection, Hardcoded, Honesty, Harm Avoidance
|
|
3759
|
+
code-assistant: + Trust, Data
|
|
3760
|
+
tool-agent: + Capability, Oversight
|
|
3761
|
+
autonomous: + Agentic Safety
|
|
3762
|
+
orchestrator: All 9 domains
|
|
3621
3763
|
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
Conformance levels:
|
|
3626
|
-
none: one or more critical controls missing
|
|
3627
|
-
essential: all critical controls pass, score < 60
|
|
3628
|
-
standard: all critical controls pass, score >= 60
|
|
3629
|
-
hardened: all critical controls pass, score >= 75
|
|
3764
|
+
Maturity levels:
|
|
3765
|
+
Hardened (80+), Standard (60-79), Developing (40-59),
|
|
3766
|
+
Initial (1-39), Not Started (0)
|
|
3630
3767
|
|
|
3631
3768
|
Examples:
|
|
3632
3769
|
$ hackmyagent scan-soul Scan current directory
|
|
3633
3770
|
$ hackmyagent scan-soul ./my-agent Scan specific directory
|
|
3634
3771
|
$ hackmyagent scan-soul --json Machine-readable output
|
|
3635
3772
|
$ hackmyagent scan-soul --verbose Show all controls
|
|
3773
|
+
$ hackmyagent scan-soul --profile conversational Override profile
|
|
3636
3774
|
$ hackmyagent scan-soul --deep Enable LLM semantic analysis`)
|
|
3637
3775
|
.argument('[directory]', 'Directory to scan (defaults to current directory)', '.')
|
|
3638
3776
|
.option('--json', 'Output as JSON')
|
|
3639
3777
|
.option('-v, --verbose', 'Show individual control results')
|
|
3640
3778
|
.option('--tier <tier>', 'Override agent tier detection (BASIC, TOOL-USING, AGENTIC, MULTI-AGENT)')
|
|
3779
|
+
.option('--profile <profile>', 'Override agent profile (conversational, code-assistant, tool-agent, autonomous, orchestrator, custom)')
|
|
3641
3780
|
.option('--fail-below <score>', 'Exit 1 if score below threshold (0-100)')
|
|
3642
3781
|
.option('--deep', 'Enable LLM semantic analysis for ambiguous controls (requires claude CLI or ANTHROPIC_API_KEY)')
|
|
3643
3782
|
.action(async (directory, options) => {
|
|
@@ -3647,15 +3786,17 @@ Examples:
|
|
|
3647
3786
|
process.stderr.write(`Error: Directory '${targetDir}' does not exist.\n`);
|
|
3648
3787
|
process.exit(1);
|
|
3649
3788
|
}
|
|
3789
|
+
const prefix = getCommandPrefix();
|
|
3650
3790
|
const scanner = new index_1.SoulScanner();
|
|
3651
3791
|
const result = await scanner.scanSoul(targetDir, {
|
|
3652
3792
|
verbose: options.verbose,
|
|
3653
3793
|
tier: options.tier,
|
|
3794
|
+
profile: options.profile,
|
|
3654
3795
|
deepAnalysis: options.deep,
|
|
3655
3796
|
});
|
|
3656
3797
|
// JSON output
|
|
3657
3798
|
if (options.json) {
|
|
3658
|
-
|
|
3799
|
+
writeJsonStdout(result);
|
|
3659
3800
|
// Check fail threshold
|
|
3660
3801
|
if (options.failBelow) {
|
|
3661
3802
|
const threshold = parseInt(options.failBelow, 10);
|
|
@@ -3676,9 +3817,27 @@ Examples:
|
|
|
3676
3817
|
process.stdout.write(` Searched: ${['SOUL.md', 'system-prompt.md', 'CLAUDE.md', '...'].join(', ')}\n`);
|
|
3677
3818
|
}
|
|
3678
3819
|
const tierLabel = result.tierForced ? `${result.agentTier} (--tier flag)` : `${result.agentTier} (auto-detected)`;
|
|
3679
|
-
|
|
3820
|
+
const profileLabel = result.profileForced ? `${result.agentProfile} (--profile flag)` : `${result.agentProfile} (auto-detected)`;
|
|
3821
|
+
process.stdout.write(`Agent Tier: ${tierLabel}\n`);
|
|
3822
|
+
process.stdout.write(`Agent Profile: ${profileLabel}\n`);
|
|
3823
|
+
if (result.skippedDomains.length > 0) {
|
|
3824
|
+
process.stdout.write(`Skipped Domains: ${result.skippedDomains.join(', ')}\n`);
|
|
3825
|
+
}
|
|
3826
|
+
process.stdout.write('\n');
|
|
3680
3827
|
process.stdout.write('Domain Scores:\n');
|
|
3681
3828
|
for (const domain of result.domains) {
|
|
3829
|
+
if (domain.skippedByProfile) {
|
|
3830
|
+
if (options.verbose) {
|
|
3831
|
+
const label = (domain.domain + ':').padEnd(26);
|
|
3832
|
+
process.stdout.write(` ${label}${colors.reset}-- (skipped by profile)${colors.reset}\n`);
|
|
3833
|
+
}
|
|
3834
|
+
continue;
|
|
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
|
+
}
|
|
3682
3841
|
const pctColor = domainBar(domain.percentage);
|
|
3683
3842
|
const label = (domain.domain + ':').padEnd(26);
|
|
3684
3843
|
process.stdout.write(` ${label}${pctColor}${domain.passed}/${domain.total} (${domain.percentage}%)${colors.reset}\n`);
|
|
@@ -3693,9 +3852,9 @@ Examples:
|
|
|
3693
3852
|
}
|
|
3694
3853
|
}
|
|
3695
3854
|
process.stdout.write('\n');
|
|
3696
|
-
// Score and
|
|
3697
|
-
const
|
|
3698
|
-
process.stdout.write(`Governance Score: ${
|
|
3855
|
+
// Score and level (progress-oriented)
|
|
3856
|
+
const lc = levelColor(result.level);
|
|
3857
|
+
process.stdout.write(`Governance Score: ${lc}${result.score}/100 [${levelLabel(result.level)}]${colors.reset}\n`);
|
|
3699
3858
|
// Conformance level
|
|
3700
3859
|
if (result.conformance === 'none') {
|
|
3701
3860
|
process.stdout.write(`Conformance: ${colors.red}NONE${colors.reset} -- critical control missing (${result.criticalMissing.join(', ')})\n`);
|
|
@@ -3714,11 +3873,12 @@ Examples:
|
|
|
3714
3873
|
const llmUpgraded = result.deepAnalysisResults.filter((e) => e.llmPassed).length;
|
|
3715
3874
|
process.stdout.write(`Deep Analysis: ${llmUpgraded} control${llmUpgraded === 1 ? '' : 's'} upgraded by LLM semantic analysis\n`);
|
|
3716
3875
|
}
|
|
3717
|
-
// Path forward
|
|
3876
|
+
// Path forward (recovery-oriented, not punitive)
|
|
3718
3877
|
const missing = result.totalControls - result.totalPassed;
|
|
3719
3878
|
if (missing > 0) {
|
|
3720
|
-
|
|
3721
|
-
process.stdout.write(
|
|
3879
|
+
const recoverable = Math.min(100 - result.score, 100);
|
|
3880
|
+
process.stdout.write(`\n Path forward: +${recoverable} recoverable by addressing ${missing} control${missing === 1 ? '' : 's'}`);
|
|
3881
|
+
process.stdout.write(`\n Run '${colors.cyan}${prefix} harden-soul${colors.reset}' to remediate.\n`);
|
|
3722
3882
|
}
|
|
3723
3883
|
else {
|
|
3724
3884
|
process.stdout.write(`\n${colors.green}All ${result.totalControls} governance controls covered.${colors.reset}\n`);
|
|
@@ -3744,6 +3904,8 @@ program
|
|
|
3744
3904
|
|
|
3745
3905
|
Runs scan-soul internally to identify missing controls, then generates
|
|
3746
3906
|
template content for each missing domain. Existing content is preserved.
|
|
3907
|
+
Supports iterative hardening: if a domain heading exists but controls
|
|
3908
|
+
fail within it, appends targeted remediation for those controls.
|
|
3747
3909
|
|
|
3748
3910
|
Modes:
|
|
3749
3911
|
Default: Append missing sections to SOUL.md (or create it)
|
|
@@ -3756,6 +3918,7 @@ Examples:
|
|
|
3756
3918
|
$ hackmyagent harden-soul --json Machine-readable output`)
|
|
3757
3919
|
.argument('[directory]', 'Directory to harden (defaults to current directory)', '.')
|
|
3758
3920
|
.option('--dry-run', 'Preview changes without modifying files')
|
|
3921
|
+
.option('--profile <profile>', 'Override agent profile (conversational, code-assistant, tool-agent, autonomous, orchestrator, custom)')
|
|
3759
3922
|
.option('--json', 'Output as JSON')
|
|
3760
3923
|
.action(async (directory, options) => {
|
|
3761
3924
|
try {
|
|
@@ -3764,8 +3927,9 @@ Examples:
|
|
|
3764
3927
|
process.stderr.write(`Error: Directory '${targetDir}' does not exist.\n`);
|
|
3765
3928
|
process.exit(1);
|
|
3766
3929
|
}
|
|
3930
|
+
const prefix = getCommandPrefix();
|
|
3767
3931
|
const scanner = new index_1.SoulScanner();
|
|
3768
|
-
const result = await scanner.hardenSoul(targetDir, { dryRun: options.dryRun });
|
|
3932
|
+
const result = await scanner.hardenSoul(targetDir, { dryRun: options.dryRun, profile: options.profile });
|
|
3769
3933
|
// JSON output
|
|
3770
3934
|
if (options.json) {
|
|
3771
3935
|
// Exclude full content from JSON to keep it concise
|
|
@@ -3776,13 +3940,13 @@ Examples:
|
|
|
3776
3940
|
dryRun: result.dryRun,
|
|
3777
3941
|
existedBefore: result.existedBefore,
|
|
3778
3942
|
};
|
|
3779
|
-
|
|
3943
|
+
writeJsonStdout(jsonResult);
|
|
3780
3944
|
return;
|
|
3781
3945
|
}
|
|
3782
3946
|
// Text output
|
|
3783
3947
|
if (result.sectionsAdded.length === 0) {
|
|
3784
3948
|
process.stdout.write(`\n${colors.green}All governance domains already have sections in ${result.file}.${colors.reset}\n`);
|
|
3785
|
-
process.stdout.write(`Run '
|
|
3949
|
+
process.stdout.write(`Run '${prefix} scan-soul --verbose' to see individual control coverage.\n\n`);
|
|
3786
3950
|
return;
|
|
3787
3951
|
}
|
|
3788
3952
|
if (result.dryRun) {
|
|
@@ -3817,7 +3981,7 @@ Examples:
|
|
|
3817
3981
|
process.stdout.write(` ${colors.green}+${colors.reset} ${section}\n`);
|
|
3818
3982
|
}
|
|
3819
3983
|
process.stdout.write(`Controls covered: +${result.controlsAdded}\n\n`);
|
|
3820
|
-
process.stdout.write(`Run '${colors.cyan}
|
|
3984
|
+
process.stdout.write(`Run '${colors.cyan}${prefix} scan-soul${colors.reset}' to verify coverage.\n\n`);
|
|
3821
3985
|
}
|
|
3822
3986
|
}
|
|
3823
3987
|
catch (error) {
|