hackmyagent 0.11.3 → 0.11.5
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 +27 -10
- package/dist/cli.js +232 -36
- package/dist/cli.js.map +1 -1
- package/dist/hardening/index.d.ts +1 -0
- package/dist/hardening/index.d.ts.map +1 -1
- package/dist/hardening/index.js +4 -1
- package/dist/hardening/index.js.map +1 -1
- package/dist/hardening/nemoclaw-scanner.d.ts +46 -0
- package/dist/hardening/nemoclaw-scanner.d.ts.map +1 -0
- package/dist/hardening/nemoclaw-scanner.js +1061 -0
- package/dist/hardening/nemoclaw-scanner.js.map +1 -0
- package/dist/hardening/scanner.d.ts +7 -0
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +740 -13
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/hardening/taxonomy.d.ts.map +1 -1
- package/dist/hardening/taxonomy.js +107 -65
- package/dist/hardening/taxonomy.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/telemetry/contribute.d.ts +58 -44
- package/dist/telemetry/contribute.d.ts.map +1 -1
- package/dist/telemetry/contribute.js +190 -96
- package/dist/telemetry/contribute.js.map +1 -1
- package/dist/telemetry/index.d.ts +2 -2
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +8 -2
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/opt-in.d.ts +22 -13
- package/dist/telemetry/opt-in.d.ts.map +1 -1
- package/dist/telemetry/opt-in.js +93 -102
- package/dist/telemetry/opt-in.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/hackmyagent)
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
|
-
[](https://github.com/opena2a-org/hackmyagent)
|
|
8
8
|
|
|
9
|
-
**
|
|
9
|
+
**173 security checks for AI agents. Find what can go wrong before an attacker does.**
|
|
10
10
|
|
|
11
11
|
Security scanner and red-team toolkit for Claude Code, Cursor, VS Code, and any MCP server setup.
|
|
12
12
|
|
|
@@ -30,14 +30,19 @@ npx opena2a-cli review
|
|
|
30
30
|
|
|
31
31
|
## What It Finds
|
|
32
32
|
|
|
33
|
-
**Attack testing:**
|
|
33
|
+
**Attack testing (115 payloads across 11 categories):**
|
|
34
34
|
- **Prompt injection** -- tests whether agents follow injected instructions from untrusted input
|
|
35
35
|
- **Data exfiltration** -- checks if agents can be tricked into leaking sensitive data to external endpoints
|
|
36
36
|
- **Jailbreak and context manipulation** -- probes agent guardrails with adversarial prompts
|
|
37
37
|
- **MCP exploitation** -- tests MCP servers for tool misuse, capability abuse, and unauthorized access
|
|
38
38
|
- **Capability abuse** -- verifies agents can't exceed their intended permissions
|
|
39
|
+
- **Supply chain attacks** -- dependency confusion, tool shadowing, package impersonation
|
|
40
|
+
- **Memory weaponization** -- persistent instruction injection via agent memory systems
|
|
41
|
+
- **A2A protocol attacks** -- identity spoofing, capability escalation in multi-agent communication
|
|
42
|
+
- **Context window attacks** -- token flooding, attention manipulation, context poisoning
|
|
39
43
|
|
|
40
|
-
**Static analysis:**
|
|
44
|
+
**Static analysis (173 checks across 34 categories):**
|
|
45
|
+
- **Unicode steganography** -- invisible codepoints (variation selectors, tag characters), zero-width characters (U+200B-200D), mid-file BOM injection, bidi override attacks (U+202A-202E), homoglyph confusables (Cyrillic/Greek/Fullwidth lookalikes), GlassWorm decoder patterns, and eval-on-invisible-payload detection. Scans JS, TS, Python, Markdown, YAML, JSON, and TOML files. ([real-world: os-info-checker-es6 npm attack, May 2025](https://thehackernews.com/2025/05/malicious-npm-package-leverages-unicode.html))
|
|
41
46
|
- **Hardcoded credentials** -- API keys, tokens, and passwords in source or config files
|
|
42
47
|
- **MCP server misconfigurations** -- open ports, root filesystem access, missing auth
|
|
43
48
|
- **AI agent CVE detection** -- scans for CVE-2026-25253 (OpenClaw WebSocket RCE), CVE-2026-25157, CVE-2026-24763, and ClawHavoc IOCs
|
|
@@ -45,8 +50,10 @@ npx opena2a-cli review
|
|
|
45
50
|
- **Governance gaps** -- missing SOUL.md, no capability policies, unsigned MCP servers
|
|
46
51
|
- **Credential scope drift** -- Google Maps keys accessing Gemini, AWS S3 keys reaching Bedrock
|
|
47
52
|
- **Supply chain risks** -- vulnerable dependencies, unsigned skills, tampered packages
|
|
53
|
+
- **Memory and RAG poisoning** -- persistent instruction injection, knowledge base contamination
|
|
54
|
+
- **Agent identity** -- missing cryptographic identity, capability claims without attestation
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
173 checks across 34 categories. 115 attack payloads. No flags needed.
|
|
50
57
|
|
|
51
58
|
---
|
|
52
59
|
|
|
@@ -69,7 +76,7 @@ npm install --save-dev hackmyagent
|
|
|
69
76
|
```
|
|
70
77
|
|
|
71
78
|
┌──────────────────────────────────────────┐
|
|
72
|
-
│ HackMyAgent v0.
|
|
79
|
+
│ HackMyAgent v0.11.4 — Security Scanner │
|
|
73
80
|
│ Found: 3 critical · 5 high · 12 medium │
|
|
74
81
|
│ │
|
|
75
82
|
│ CRED-001 critical Hardcoded API key in .env │
|
|
@@ -88,7 +95,7 @@ npm install --save-dev hackmyagent
|
|
|
88
95
|
|
|
89
96
|
Step-by-step guides for common workflows:
|
|
90
97
|
|
|
91
|
-
- **[Scan my agent](docs/use-cases/scan-my-agent.md)** -- Run all
|
|
98
|
+
- **[Scan my agent](docs/use-cases/scan-my-agent.md)** -- Run all 173 checks and auto-fix findings (5 min)
|
|
92
99
|
- **[Red-team MCP servers](docs/use-cases/red-team-mcp.md)** -- Test MCP servers with adversarial payloads (10 min)
|
|
93
100
|
- **[Secure OpenClaw](docs/use-cases/openclaw-security.md)** -- OpenClaw-specific checks, CVE detection, ClawHavoc IOC scanning (10 min)
|
|
94
101
|
- **[CI/CD pipeline](docs/use-cases/ci-pipeline.md)** -- GitHub Actions with JSON/SARIF output (5 min)
|
|
@@ -124,7 +131,7 @@ hackmyagent secure --publish # push results to OpenA2A Registry
|
|
|
124
131
|
|
|
125
132
|
|
|
126
133
|
<details>
|
|
127
|
-
<summary>All
|
|
134
|
+
<summary>All 34 security categories</summary>
|
|
128
135
|
|
|
129
136
|
| Category | Checks | What it detects |
|
|
130
137
|
|----------|--------|-----------------|
|
|
@@ -157,7 +164,11 @@ hackmyagent secure --publish # push results to OpenA2A Registry
|
|
|
157
164
|
| CONFIG | 9 | Insecure default settings |
|
|
158
165
|
| SUPPLY | 8 | Supply chain attack vectors |
|
|
159
166
|
| SKILL | 12 | Malicious skill/tool detection |
|
|
160
|
-
| HEARTBEAT |
|
|
167
|
+
| HEARTBEAT | 7 | Heartbeat/cron abuse |
|
|
168
|
+
| UNICODE-STEGO | 5 | Invisible codepoints, zero-width chars, bidi attacks, homoglyphs, GlassWorm decoders |
|
|
169
|
+
| MEM | 5 | Memory poisoning, context injection |
|
|
170
|
+
| RAG | 4 | RAG/knowledge base poisoning |
|
|
171
|
+
| AIM | 3 | Agent identity verification |
|
|
161
172
|
|
|
162
173
|
</details>
|
|
163
174
|
|
|
@@ -185,7 +196,7 @@ Use `--dry-run` to preview changes. Backups are created in `.hackmyagent-backup/
|
|
|
185
196
|
|
|
186
197
|
### `hackmyagent attack` -- Red Team
|
|
187
198
|
|
|
188
|
-
Test your AI agent with
|
|
199
|
+
Test your AI agent with 115 adversarial payloads across 11 attack categories.
|
|
189
200
|
|
|
190
201
|
```bash
|
|
191
202
|
hackmyagent attack --local # local simulation
|
|
@@ -205,6 +216,12 @@ hackmyagent attack https://api.example.com --fail-on-vulnerable medium # CI gat
|
|
|
205
216
|
| `data-exfiltration` | 11 | Extract sensitive data, system prompts, credentials |
|
|
206
217
|
| `capability-abuse` | 10 | Misuse agent tools for unintended actions |
|
|
207
218
|
| `context-manipulation` | 10 | Poison agent context or memory |
|
|
219
|
+
| `supply-chain` | 10 | Dependency confusion, package impersonation |
|
|
220
|
+
| `tool-shadow` | 10 | Tool shadowing, capability escalation |
|
|
221
|
+
| `mcp-exploitation` | 10 | MCP protocol abuse, tool injection |
|
|
222
|
+
| `memory-weaponization` | 10 | Persistent memory poisoning attacks |
|
|
223
|
+
| `a2a-attacks` | 10 | Agent-to-agent identity spoofing |
|
|
224
|
+
| `context-window` | 10 | Token flooding, attention manipulation |
|
|
208
225
|
|
|
209
226
|
> Only test systems you own or have written authorization to test.
|
|
210
227
|
|
package/dist/cli.js
CHANGED
|
@@ -41,6 +41,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
41
41
|
const commander_1 = require("commander");
|
|
42
42
|
const index_1 = require("./index");
|
|
43
43
|
const resolve_mcp_1 = require("./resolve-mcp");
|
|
44
|
+
const nemoclaw_scanner_1 = require("./hardening/nemoclaw-scanner");
|
|
44
45
|
const program = new commander_1.Command();
|
|
45
46
|
// Write JSON to stdout synchronously with retry for pipe backpressure.
|
|
46
47
|
// process.stdout.write() is async and gets truncated when process.exit()
|
|
@@ -1544,16 +1545,21 @@ function resolvePackageVersion(targetDir) {
|
|
|
1544
1545
|
* Determines whether to contribute based on:
|
|
1545
1546
|
* 1. --contribute / --no-contribute CLI flags (highest priority)
|
|
1546
1547
|
* 2. ~/.opena2a/config.json contribute.enabled setting
|
|
1547
|
-
* 3. Interactive opt-in prompt (first scan or scan #10)
|
|
1548
1548
|
*
|
|
1549
|
-
* If contributing,
|
|
1550
|
-
*
|
|
1549
|
+
* If contributing, queues an anonymized event to ~/.opena2a/contribute-queue.json
|
|
1550
|
+
* (compatible with @opena2a/contribute format) and flushes when threshold reached.
|
|
1551
|
+
*
|
|
1552
|
+
* Also records the scan and shows a delayed consent tip after the 3rd scan
|
|
1553
|
+
* if the user hasn't opted in or dismissed.
|
|
1551
1554
|
*/
|
|
1552
|
-
async function handleContribution(contributeFlag, targetDir, findings, registryUrl, format) {
|
|
1555
|
+
async function handleContribution(contributeFlag, targetDir, findings, durationMs, registryUrl, format) {
|
|
1553
1556
|
try {
|
|
1554
|
-
const { isContributeEnabled,
|
|
1555
|
-
//
|
|
1556
|
-
|
|
1557
|
+
const { isContributeEnabled, recordScanAndMaybeShowTip, buildScanEvent, queueAndMaybeFlush, } = await Promise.resolve().then(() => __importStar(require('./telemetry')));
|
|
1558
|
+
// Record scan count and maybe show the delayed consent tip
|
|
1559
|
+
const tip = recordScanAndMaybeShowTip();
|
|
1560
|
+
if (tip && format === 'text' && process.stdout.isTTY) {
|
|
1561
|
+
process.stdout.write(tip + '\n');
|
|
1562
|
+
}
|
|
1557
1563
|
// Determine whether to contribute
|
|
1558
1564
|
let shouldContribute;
|
|
1559
1565
|
if (contributeFlag === true) {
|
|
@@ -1566,35 +1572,19 @@ async function handleContribution(contributeFlag, targetDir, findings, registryU
|
|
|
1566
1572
|
}
|
|
1567
1573
|
else {
|
|
1568
1574
|
// Check config
|
|
1569
|
-
|
|
1570
|
-
if (configSetting === true) {
|
|
1571
|
-
shouldContribute = true;
|
|
1572
|
-
}
|
|
1573
|
-
else if (configSetting === false) {
|
|
1574
|
-
shouldContribute = false;
|
|
1575
|
-
}
|
|
1576
|
-
else {
|
|
1577
|
-
// Not configured -- prompt after 3 scans (interactive TTY only)
|
|
1578
|
-
if (format === 'text' && process.stdout.isTTY && shouldPromptContribute()) {
|
|
1579
|
-
shouldContribute = await showContributePrompt();
|
|
1580
|
-
}
|
|
1581
|
-
else {
|
|
1582
|
-
shouldContribute = false;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1575
|
+
shouldContribute = isContributeEnabled() === true;
|
|
1585
1576
|
}
|
|
1586
1577
|
if (!shouldContribute)
|
|
1587
1578
|
return;
|
|
1588
|
-
// Build and
|
|
1579
|
+
// Build and queue contribution event (non-blocking, flushes at threshold)
|
|
1589
1580
|
const packageName = resolvePackageName(targetDir);
|
|
1590
1581
|
if (!packageName)
|
|
1591
1582
|
return;
|
|
1592
|
-
const
|
|
1593
|
-
|
|
1594
|
-
if (
|
|
1595
|
-
|
|
1583
|
+
const event = buildScanEvent(packageName, targetDir, findings, durationMs);
|
|
1584
|
+
await queueAndMaybeFlush(event, registryUrl, format === 'text');
|
|
1585
|
+
if (format === 'text') {
|
|
1586
|
+
process.stdout.write('Queued anonymized scan summary for OpenA2A Registry (--no-contribute to opt out)\n');
|
|
1596
1587
|
}
|
|
1597
|
-
// Failures are silently ignored -- contribution is best-effort
|
|
1598
1588
|
}
|
|
1599
1589
|
catch {
|
|
1600
1590
|
// Non-fatal: contribution failure must never crash the scan
|
|
@@ -1606,7 +1596,7 @@ async function handleContribution(contributeFlag, targetDir, findings, registryU
|
|
|
1606
1596
|
* Converts SoulScanResult controls into SecurityFinding-like objects
|
|
1607
1597
|
* for the contribution module, then delegates to handleContribution.
|
|
1608
1598
|
*/
|
|
1609
|
-
async function handleSoulContribution(contributeFlag, targetDir, result, registryUrl, format) {
|
|
1599
|
+
async function handleSoulContribution(contributeFlag, targetDir, result, durationMs, registryUrl, format) {
|
|
1610
1600
|
// Convert soul controls into SecurityFinding-shaped objects
|
|
1611
1601
|
const findings = [];
|
|
1612
1602
|
for (const domain of result.domains) {
|
|
@@ -1625,7 +1615,7 @@ async function handleSoulContribution(contributeFlag, targetDir, result, registr
|
|
|
1625
1615
|
});
|
|
1626
1616
|
}
|
|
1627
1617
|
}
|
|
1628
|
-
await handleContribution(contributeFlag, targetDir, findings, registryUrl, format);
|
|
1618
|
+
await handleContribution(contributeFlag, targetDir, findings, durationMs, registryUrl, format);
|
|
1629
1619
|
}
|
|
1630
1620
|
program
|
|
1631
1621
|
.command('secure')
|
|
@@ -1686,9 +1676,18 @@ Examples:
|
|
|
1686
1676
|
.option('--registry-key <key>', 'Registry API key (default: REGISTRY_API_KEY env)')
|
|
1687
1677
|
.option('--contribute', 'Share anonymized scan findings with OpenA2A Registry (overrides config)')
|
|
1688
1678
|
.option('--no-contribute', 'Do not share findings for this scan (overrides config)')
|
|
1679
|
+
.option('--ci', 'CI mode: suppress interactive prompts, exit non-zero on findings')
|
|
1689
1680
|
.action(async (directory, options) => {
|
|
1690
1681
|
try {
|
|
1691
1682
|
const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
|
|
1683
|
+
// CI mode: force non-interactive defaults
|
|
1684
|
+
if (options.ci) {
|
|
1685
|
+
if (!options.format && !options.json)
|
|
1686
|
+
options.format = 'text';
|
|
1687
|
+
// In CI, never prompt -- only contribute if explicitly --contribute
|
|
1688
|
+
if (options.contribute === undefined)
|
|
1689
|
+
options.contribute = false;
|
|
1690
|
+
}
|
|
1692
1691
|
// Check if directory exists
|
|
1693
1692
|
if (!require('fs').existsSync(targetDir)) {
|
|
1694
1693
|
console.error(`Error: Directory '${targetDir}' does not exist.`);
|
|
@@ -1746,6 +1745,7 @@ Examples:
|
|
|
1746
1745
|
}
|
|
1747
1746
|
}
|
|
1748
1747
|
const scanner = new index_1.HardeningScanner();
|
|
1748
|
+
const scanStartMs = Date.now();
|
|
1749
1749
|
const result = await scanner.scan({
|
|
1750
1750
|
targetDir,
|
|
1751
1751
|
autoFix: options.fix ?? false,
|
|
@@ -1755,6 +1755,7 @@ Examples:
|
|
|
1755
1755
|
cliName: CLI_PREFIX,
|
|
1756
1756
|
onProgress,
|
|
1757
1757
|
});
|
|
1758
|
+
const scanDurationMs = Date.now() - scanStartMs;
|
|
1758
1759
|
// OASB-2 composite mode: infrastructure (50%) + governance (50%)
|
|
1759
1760
|
if (isOasb2) {
|
|
1760
1761
|
const infraResult = generateBenchmarkReport(result.allFindings || result.findings, level, options.category);
|
|
@@ -1890,7 +1891,7 @@ Examples:
|
|
|
1890
1891
|
writeJsonStdout(jsonOutput);
|
|
1891
1892
|
}
|
|
1892
1893
|
// Community contribution (non-blocking, runs in JSON mode too)
|
|
1893
|
-
await handleContribution(options.contribute, targetDir, result.findings, options.registryUrl, format);
|
|
1894
|
+
await handleContribution(options.contribute, targetDir, result.findings, scanDurationMs, options.registryUrl, format);
|
|
1894
1895
|
const critHigh = result.findings.filter((f) => !f.passed && !f.fixed && (f.severity === 'critical' || f.severity === 'high'));
|
|
1895
1896
|
if (critHigh.length > 0)
|
|
1896
1897
|
process.exitCode = 1;
|
|
@@ -2119,12 +2120,15 @@ Examples:
|
|
|
2119
2120
|
}
|
|
2120
2121
|
}
|
|
2121
2122
|
// Community contribution: share anonymized findings with OpenA2A Registry
|
|
2122
|
-
await handleContribution(options.contribute, targetDir, result.findings, options.registryUrl, format);
|
|
2123
|
+
await handleContribution(options.contribute, targetDir, result.findings, scanDurationMs, options.registryUrl, format);
|
|
2123
2124
|
// Star prompt (interactive TTY only, text format only)
|
|
2124
2125
|
if (process.stdout.isTTY) {
|
|
2125
2126
|
console.log(`${colors.cyan}Helpful?${RESET()} Star the project: https://github.com/opena2a-org/opena2a\n`);
|
|
2126
2127
|
}
|
|
2127
|
-
// Exit with non-zero if critical/high issues remain
|
|
2128
|
+
// Exit with non-zero if critical/high issues remain (or any issues in --ci mode)
|
|
2129
|
+
if (options.ci && issues.length > 0) {
|
|
2130
|
+
process.exit(1);
|
|
2131
|
+
}
|
|
2128
2132
|
const criticalOrHigh = issues.filter((f) => f.severity === 'critical' || f.severity === 'high');
|
|
2129
2133
|
if (criticalOrHigh.length > 0) {
|
|
2130
2134
|
process.exit(1);
|
|
@@ -2365,6 +2369,180 @@ Examples:
|
|
|
2365
2369
|
process.exit(1);
|
|
2366
2370
|
}
|
|
2367
2371
|
});
|
|
2372
|
+
// NemoClaw-specific helpers
|
|
2373
|
+
const NEMOCLAW_CHECK_CATEGORIES = nemoclaw_scanner_1.NEMOCLAW_CATEGORIES;
|
|
2374
|
+
function detectNemoClawDirectory(providedDir) {
|
|
2375
|
+
const os = require('os');
|
|
2376
|
+
const fs = require('fs');
|
|
2377
|
+
const path = require('path');
|
|
2378
|
+
if (providedDir && providedDir !== '') {
|
|
2379
|
+
return providedDir.startsWith('/') ? providedDir : path.join(process.cwd(), providedDir);
|
|
2380
|
+
}
|
|
2381
|
+
const homeDir = os.homedir();
|
|
2382
|
+
const candidates = [
|
|
2383
|
+
path.join(homeDir, '.nemoclaw'),
|
|
2384
|
+
path.join(homeDir, '.openshell'),
|
|
2385
|
+
path.join(homeDir, '.openclaw'),
|
|
2386
|
+
];
|
|
2387
|
+
for (const candidate of candidates) {
|
|
2388
|
+
if (fs.existsSync(candidate)) {
|
|
2389
|
+
return candidate;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return process.cwd();
|
|
2393
|
+
}
|
|
2394
|
+
function filterNemoClawFindings(findings) {
|
|
2395
|
+
return findings.filter((f) => {
|
|
2396
|
+
const checkId = f.checkId.toUpperCase();
|
|
2397
|
+
return checkId.startsWith('HMA-NMC-');
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
function assessNemoClawRiskLevel(findings) {
|
|
2401
|
+
const criticalCount = findings.filter((f) => f.severity === 'critical').length;
|
|
2402
|
+
const highCount = findings.filter((f) => f.severity === 'high').length;
|
|
2403
|
+
const mediumCount = findings.filter((f) => f.severity === 'medium').length;
|
|
2404
|
+
if (criticalCount > 0) {
|
|
2405
|
+
return {
|
|
2406
|
+
level: 'Critical',
|
|
2407
|
+
color: colors.brightRed,
|
|
2408
|
+
description: `${criticalCount} critical finding(s) with recommended fixes available.`,
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
if (highCount > 0) {
|
|
2412
|
+
return {
|
|
2413
|
+
level: 'High',
|
|
2414
|
+
color: colors.red,
|
|
2415
|
+
description: `${highCount} high-severity finding(s) detected. Fixes available below.`,
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
if (mediumCount > 0) {
|
|
2419
|
+
return {
|
|
2420
|
+
level: 'Moderate',
|
|
2421
|
+
color: colors.yellow,
|
|
2422
|
+
description: 'Some findings detected. Review the recommendations below.',
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
if (findings.length === 0) {
|
|
2426
|
+
return {
|
|
2427
|
+
level: 'None',
|
|
2428
|
+
color: colors.dim,
|
|
2429
|
+
description: `No NemoClaw installation detected. Run \`${CLI_PREFIX} secure\` for a full scan.`,
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
return {
|
|
2433
|
+
level: 'Low',
|
|
2434
|
+
color: colors.green,
|
|
2435
|
+
description: 'No critical or high findings detected.',
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
program
|
|
2439
|
+
.command('secure-nemoclaw')
|
|
2440
|
+
.description(`Security scan for NVIDIA NemoClaw installations
|
|
2441
|
+
|
|
2442
|
+
Performs focused security checks for NemoClaw sandbox deployments:
|
|
2443
|
+
- Secrets: NVIDIA API key exposure in configs, logs, Docker, shell history
|
|
2444
|
+
- Network: Gateway/k3s/inference port binding, Docker socket, egress policies
|
|
2445
|
+
- Skills: Blueprint integrity, skill verification, directory permissions
|
|
2446
|
+
- Process: Sandbox privileges, seccomp/Landlock enforcement, root execution
|
|
2447
|
+
- OpenClaw layer: Inherited misconfigs that survive NemoClaw sandboxing
|
|
2448
|
+
|
|
2449
|
+
Auto-detects ~/.nemoclaw, ~/.openshell, or ~/.openclaw directories.
|
|
2450
|
+
Exit code 1 if critical/high issues found.
|
|
2451
|
+
|
|
2452
|
+
Examples:
|
|
2453
|
+
$ hackmyagent secure-nemoclaw Scan auto-detected directory
|
|
2454
|
+
$ hackmyagent secure-nemoclaw ~/.nemoclaw Scan specific directory
|
|
2455
|
+
$ hackmyagent secure-nemoclaw --json JSON output for CI`)
|
|
2456
|
+
.argument('[directory]', 'Directory to scan (default: ~/.nemoclaw or ~/.openshell)', '')
|
|
2457
|
+
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
2458
|
+
.option('-v, --verbose', 'Show all checks including passed ones')
|
|
2459
|
+
.action(async (directory, options) => {
|
|
2460
|
+
try {
|
|
2461
|
+
const targetDir = detectNemoClawDirectory(directory);
|
|
2462
|
+
if (!options.json) {
|
|
2463
|
+
console.log(`\nNemoClaw Security Report`);
|
|
2464
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
2465
|
+
console.log(`Scanning ${targetDir}...\n`);
|
|
2466
|
+
}
|
|
2467
|
+
const scanner = new nemoclaw_scanner_1.NemoClawScanner();
|
|
2468
|
+
const findings = await scanner.scan(targetDir, {});
|
|
2469
|
+
// Enrich with taxonomy
|
|
2470
|
+
const { enrichWithTaxonomy } = require('./hardening/taxonomy');
|
|
2471
|
+
enrichWithTaxonomy(findings);
|
|
2472
|
+
const issues = findings.filter((f) => !f.passed);
|
|
2473
|
+
const passedFindings = findings.filter((f) => f.passed);
|
|
2474
|
+
if (options.json) {
|
|
2475
|
+
const jsonOutput = {
|
|
2476
|
+
target: targetDir,
|
|
2477
|
+
riskLevel: assessNemoClawRiskLevel(issues).level,
|
|
2478
|
+
totalChecks: findings.length,
|
|
2479
|
+
issues: issues.length,
|
|
2480
|
+
passed: passedFindings.length,
|
|
2481
|
+
findings: findings,
|
|
2482
|
+
};
|
|
2483
|
+
writeJsonStdout(jsonOutput);
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
// Risk assessment
|
|
2487
|
+
const risk = assessNemoClawRiskLevel(issues);
|
|
2488
|
+
console.log(`Risk Level: ${risk.color}${risk.level}${RESET()}`);
|
|
2489
|
+
console.log(`${risk.description}\n`);
|
|
2490
|
+
// Summary stats
|
|
2491
|
+
console.log(`Checks: ${findings.length} total | ${issues.length} issues | ${passedFindings.length} passed\n`);
|
|
2492
|
+
// Show issues
|
|
2493
|
+
if (issues.length > 0) {
|
|
2494
|
+
console.log(`${colors.red}Findings:${RESET()}\n`);
|
|
2495
|
+
for (const finding of issues) {
|
|
2496
|
+
const display = SEVERITY_DISPLAY[finding.severity];
|
|
2497
|
+
const location = finding.file
|
|
2498
|
+
? finding.line
|
|
2499
|
+
? `${finding.file}:${finding.line}`
|
|
2500
|
+
: finding.file
|
|
2501
|
+
: '';
|
|
2502
|
+
const sevLabel = finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1);
|
|
2503
|
+
console.log(`${display.color()}${display.symbol} [${finding.checkId}] ${sevLabel}${RESET()}`);
|
|
2504
|
+
console.log(` ${finding.description}`);
|
|
2505
|
+
if (location) {
|
|
2506
|
+
console.log(` File: ${location}`);
|
|
2507
|
+
}
|
|
2508
|
+
if (finding.fix) {
|
|
2509
|
+
console.log(` ${colors.cyan}Recommended fix:${RESET()} ${finding.fix}`);
|
|
2510
|
+
}
|
|
2511
|
+
console.log();
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
else {
|
|
2515
|
+
console.log(`${colors.green}No NemoClaw-specific issues found.${RESET()}\n`);
|
|
2516
|
+
}
|
|
2517
|
+
// Show passed checks in verbose mode
|
|
2518
|
+
if (options.verbose && passedFindings.length > 0) {
|
|
2519
|
+
console.log(`${colors.green}Passed Checks:${RESET()}`);
|
|
2520
|
+
for (const finding of passedFindings) {
|
|
2521
|
+
console.log(` ${colors.green}[ok]${RESET()} [${finding.checkId}] ${finding.name}`);
|
|
2522
|
+
}
|
|
2523
|
+
console.log();
|
|
2524
|
+
}
|
|
2525
|
+
// Shodan self-check guidance
|
|
2526
|
+
if (issues.some((f) => f.category === 'network')) {
|
|
2527
|
+
console.log(`${colors.yellow}Internet Exposure Check:${RESET()}`);
|
|
2528
|
+
console.log(` Check if your instance is visible on Shodan:`);
|
|
2529
|
+
console.log(` https://www.shodan.io/host/<YOUR-IP>`);
|
|
2530
|
+
console.log(` Known NemoClaw dorks: port:18789, port:6443 ssl.cert.subject.cn:"k3s-serving"\n`);
|
|
2531
|
+
}
|
|
2532
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
2533
|
+
console.log(`Run '${CLI_PREFIX} secure-openclaw' for OpenClaw-specific checks.`);
|
|
2534
|
+
console.log(`Run '${CLI_PREFIX} secure' for a full security scan.\n`);
|
|
2535
|
+
// Exit with non-zero if critical/high issues remain
|
|
2536
|
+
const criticalOrHigh = issues.filter((f) => f.severity === 'critical' || f.severity === 'high');
|
|
2537
|
+
if (criticalOrHigh.length > 0) {
|
|
2538
|
+
process.exit(1);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
catch (error) {
|
|
2542
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2543
|
+
process.exit(1);
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2368
2546
|
program
|
|
2369
2547
|
.command('scan')
|
|
2370
2548
|
.description(`Scan external target for exposed MCP endpoints
|
|
@@ -2390,7 +2568,9 @@ Examples:
|
|
|
2390
2568
|
.option('-v, --verbose', 'Show detailed finding information')
|
|
2391
2569
|
.action(async (target, options) => {
|
|
2392
2570
|
try {
|
|
2393
|
-
|
|
2571
|
+
if (!options.json) {
|
|
2572
|
+
console.log(`\nScanning ${target}...\n`);
|
|
2573
|
+
}
|
|
2394
2574
|
const scanner = new index_1.ExternalScanner();
|
|
2395
2575
|
const customPorts = options.ports
|
|
2396
2576
|
? options.ports.split(',').map((p) => parseInt(p.trim(), 10))
|
|
@@ -4018,21 +4198,29 @@ Examples:
|
|
|
4018
4198
|
.option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)', process.env.REGISTRY_URL || 'https://api.oa2a.org')
|
|
4019
4199
|
.option('--contribute', 'Share anonymized scan findings with OpenA2A Registry (overrides config)')
|
|
4020
4200
|
.option('--no-contribute', 'Do not share findings for this scan (overrides config)')
|
|
4201
|
+
.option('--ci', 'CI mode: suppress interactive prompts, exit non-zero on findings')
|
|
4021
4202
|
.action(async (directory, options) => {
|
|
4022
4203
|
try {
|
|
4023
4204
|
const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
|
|
4205
|
+
// CI mode: force non-interactive defaults
|
|
4206
|
+
if (options.ci) {
|
|
4207
|
+
if (options.contribute === undefined)
|
|
4208
|
+
options.contribute = false;
|
|
4209
|
+
}
|
|
4024
4210
|
if (!require('fs').existsSync(targetDir)) {
|
|
4025
4211
|
process.stderr.write(`Error: Directory '${targetDir}' does not exist.\n`);
|
|
4026
4212
|
process.exit(1);
|
|
4027
4213
|
}
|
|
4028
4214
|
const prefix = getCommandPrefix();
|
|
4029
4215
|
const scanner = new index_1.SoulScanner();
|
|
4216
|
+
const soulScanStartMs = Date.now();
|
|
4030
4217
|
const result = await scanner.scanSoul(targetDir, {
|
|
4031
4218
|
verbose: options.verbose,
|
|
4032
4219
|
tier: options.tier,
|
|
4033
4220
|
profile: options.profile,
|
|
4034
4221
|
deepAnalysis: options.deep,
|
|
4035
4222
|
});
|
|
4223
|
+
const soulScanDurationMs = Date.now() - soulScanStartMs;
|
|
4036
4224
|
// JSON output
|
|
4037
4225
|
if (options.json) {
|
|
4038
4226
|
// Run publish in JSON mode and include result in output
|
|
@@ -4070,6 +4258,7 @@ Examples:
|
|
|
4070
4258
|
process.exit(1);
|
|
4071
4259
|
}
|
|
4072
4260
|
}
|
|
4261
|
+
await handleSoulContribution(options.contribute, targetDir, result, soulScanDurationMs, options.registryUrl, 'json');
|
|
4073
4262
|
return;
|
|
4074
4263
|
}
|
|
4075
4264
|
// Text output
|
|
@@ -4183,7 +4372,14 @@ Examples:
|
|
|
4183
4372
|
}
|
|
4184
4373
|
// Community contribution: share anonymized findings with OpenA2A Registry
|
|
4185
4374
|
const soulFormat = options.json ? 'json' : 'text';
|
|
4186
|
-
await handleSoulContribution(options.contribute, targetDir, result, options.registryUrl, soulFormat);
|
|
4375
|
+
await handleSoulContribution(options.contribute, targetDir, result, soulScanDurationMs, options.registryUrl, soulFormat);
|
|
4376
|
+
// In CI mode, exit non-zero if any controls failed
|
|
4377
|
+
if (options.ci) {
|
|
4378
|
+
const failedControls = result.domains.flatMap(d => d.controls).filter(c => !c.passed);
|
|
4379
|
+
if (failedControls.length > 0) {
|
|
4380
|
+
process.exit(1);
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4187
4383
|
// Check fail threshold
|
|
4188
4384
|
if (options.failBelow) {
|
|
4189
4385
|
const threshold = parseInt(options.failBelow, 10);
|