hackmyagent 0.15.6 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.integrity-manifest.json +1 -1
- package/dist/cli.js +232 -225
- package/dist/cli.js.map +1 -1
- package/dist/registry/client.d.ts +44 -19
- package/dist/registry/client.d.ts.map +1 -1
- package/dist/registry/client.js +71 -6
- package/dist/registry/client.js.map +1 -1
- package/dist/registry/publish.d.ts +4 -4
- package/dist/registry/publish.d.ts.map +1 -1
- package/dist/registry/publish.js +85 -92
- package/dist/registry/publish.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -42,9 +42,11 @@ const commander_1 = require("commander");
|
|
|
42
42
|
const index_1 = require("./index");
|
|
43
43
|
const resolve_mcp_1 = require("./resolve-mcp");
|
|
44
44
|
const wild_1 = require("./wild");
|
|
45
|
-
const nemoclaw_scanner_1 = require("./hardening/nemoclaw-scanner");
|
|
46
45
|
const program = new commander_1.Command();
|
|
47
46
|
program.showHelpAfterError('(run with --help for usage)');
|
|
47
|
+
// Total security check count across all scanner modules.
|
|
48
|
+
// Update when adding new checks (verify with: grep -r "checkId:" src/hardening/ | grep -o "checkId: '[^']*'" | sort -u | wc -l)
|
|
49
|
+
const CHECK_COUNT = 208;
|
|
48
50
|
// Write a string to stdout synchronously with retry for pipe backpressure.
|
|
49
51
|
// process.stdout.write() is async and gets truncated when process.exit()
|
|
50
52
|
// runs before the stream flushes. fs.writeFileSync(1, ...) can fail with
|
|
@@ -126,20 +128,19 @@ program
|
|
|
126
128
|
.name('hackmyagent')
|
|
127
129
|
.description(`Find it. Break it. Fix it.
|
|
128
130
|
|
|
129
|
-
The hacker's toolkit for AI agents.
|
|
131
|
+
The hacker's toolkit for AI agents. ${CHECK_COUNT} security checks, ${index_1.PAYLOAD_STATS.total} attack
|
|
130
132
|
payloads, auto-fix with rollback, and OASB benchmark compliance.
|
|
131
133
|
|
|
132
134
|
Documentation: https://hackmyagent.com/docs
|
|
133
135
|
|
|
134
136
|
Updates (v${index_1.VERSION}):
|
|
135
|
-
- NemoClaw sandbox scanner (28 installation checks)
|
|
136
137
|
- 10 new static analysis patterns (NEMO series)
|
|
137
138
|
- Community trust contributions
|
|
138
|
-
-
|
|
139
|
+
- ${CHECK_COUNT} checks across 60 categories
|
|
139
140
|
|
|
140
141
|
Examples:
|
|
141
|
-
$ hackmyagent secure Find vulnerabilities (
|
|
142
|
-
$ hackmyagent attack --local Break it with
|
|
142
|
+
$ hackmyagent secure Find vulnerabilities (${CHECK_COUNT} checks)
|
|
143
|
+
$ hackmyagent attack --local Break it with ${index_1.PAYLOAD_STATS.total} attack payloads
|
|
143
144
|
$ hackmyagent secure --fix Fix issues automatically
|
|
144
145
|
$ hackmyagent fix-all Run all security plugins
|
|
145
146
|
$ hackmyagent scan example.com Scan external infrastructure`)
|
|
@@ -147,7 +148,7 @@ Examples:
|
|
|
147
148
|
.option('--no-color', 'Disable colored output (also respects NO_COLOR env)');
|
|
148
149
|
program.addHelpText('beforeAll', `
|
|
149
150
|
Quick start:
|
|
150
|
-
$ hackmyagent secure Scan current directory (
|
|
151
|
+
$ hackmyagent secure Scan current directory (${CHECK_COUNT} checks)
|
|
151
152
|
$ hackmyagent fix-all --with-aim Auto-fix + create agent identity
|
|
152
153
|
$ hackmyagent attack Red-team your agent
|
|
153
154
|
`);
|
|
@@ -170,7 +171,7 @@ program
|
|
|
170
171
|
.description(`Check if a package, repo, or skill is safe
|
|
171
172
|
|
|
172
173
|
Accepts npm packages, GitHub repos, local paths, or skill identifiers:
|
|
173
|
-
• npm package: downloads and runs full security analysis (
|
|
174
|
+
• npm package: downloads and runs full security analysis (${CHECK_COUNT} checks + NanoMind)
|
|
174
175
|
• GitHub repo: shallow clones and runs full security analysis
|
|
175
176
|
• Local path: runs NanoMind semantic analysis
|
|
176
177
|
• Skill identifier: verifies publisher, permissions, revocation
|
|
@@ -185,8 +186,10 @@ Examples:
|
|
|
185
186
|
$ hackmyagent check https://github.com/punkpeye/awesome-mcp-servers
|
|
186
187
|
$ hackmyagent check ./my-agent/
|
|
187
188
|
$ hackmyagent check @publisher/skill --verbose
|
|
189
|
+
$ hackmyagent check pip:requests
|
|
190
|
+
$ hackmyagent check pypi:flask
|
|
188
191
|
$ hackmyagent check modelcontextprotocol/servers --json`)
|
|
189
|
-
.argument('<target>', 'npm package
|
|
192
|
+
.argument('<target>', 'npm package, PyPI package (pip: or pypi: prefix), local path, GitHub repo, or skill identifier')
|
|
190
193
|
.option('-v, --verbose', 'Show detailed verification info')
|
|
191
194
|
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
192
195
|
.option('--offline', 'Skip DNS verification (offline mode)')
|
|
@@ -244,6 +247,11 @@ Examples:
|
|
|
244
247
|
process.exit(1);
|
|
245
248
|
return;
|
|
246
249
|
}
|
|
250
|
+
// PyPI package: download, run full HMA scan, clean up
|
|
251
|
+
if (looksLikePyPiPackage(skill)) {
|
|
252
|
+
await checkPyPiPackage(skill, options);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
247
255
|
// GitHub repo: clone, run full HMA scan, clean up
|
|
248
256
|
if (looksLikeGitHubRepo(skill)) {
|
|
249
257
|
await checkGitHubRepo(skill, options);
|
|
@@ -343,6 +351,43 @@ const SEVERITY_DISPLAY = {
|
|
|
343
351
|
medium: { symbol: '[~]', color: () => colors.yellow },
|
|
344
352
|
low: { symbol: '[.]', color: () => colors.green },
|
|
345
353
|
};
|
|
354
|
+
/**
|
|
355
|
+
* Display check command findings with optional verbose details.
|
|
356
|
+
* When verbose is true, shows checkId, category, file location, and fix/guidance for each finding.
|
|
357
|
+
*/
|
|
358
|
+
function displayCheckFindings(failed, verbose) {
|
|
359
|
+
if (failed.length > 0) {
|
|
360
|
+
console.log();
|
|
361
|
+
const limit = verbose ? failed.length : 15;
|
|
362
|
+
for (const f of failed.slice(0, limit)) {
|
|
363
|
+
const sev = SEVERITY_DISPLAY[f.severity];
|
|
364
|
+
const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
|
|
365
|
+
console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
|
|
366
|
+
if (verbose) {
|
|
367
|
+
console.log(` ${colors.dim}Check: ${f.checkId}${RESET()}`);
|
|
368
|
+
if (f.category) {
|
|
369
|
+
console.log(` ${colors.dim}Category: ${f.category}${RESET()}`);
|
|
370
|
+
}
|
|
371
|
+
if (f.file) {
|
|
372
|
+
const location = f.line ? `${f.file}:${f.line}` : f.file;
|
|
373
|
+
console.log(` ${colors.dim}File: ${location}${RESET()}`);
|
|
374
|
+
}
|
|
375
|
+
if (f.fix) {
|
|
376
|
+
console.log(` ${colors.cyan}Fix: ${f.fix}${RESET()}`);
|
|
377
|
+
}
|
|
378
|
+
if (f.guidance) {
|
|
379
|
+
console.log(` ${colors.dim}Guidance: ${f.guidance}${RESET()}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (failed.length > limit) {
|
|
384
|
+
console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
346
391
|
function groupFindingsBySeverity(findings) {
|
|
347
392
|
const grouped = {
|
|
348
393
|
critical: [],
|
|
@@ -1774,7 +1819,7 @@ program
|
|
|
1774
1819
|
.command('secure')
|
|
1775
1820
|
.description(`Scan and harden your agent setup
|
|
1776
1821
|
|
|
1777
|
-
Performs
|
|
1822
|
+
Performs ${CHECK_COUNT} security checks across 60 categories:
|
|
1778
1823
|
• Credentials: API key exposure, secrets in configs
|
|
1779
1824
|
• MCP: Server configs, tool permissions, secrets
|
|
1780
1825
|
• Network: TLS, interface bindings, CORS
|
|
@@ -2776,188 +2821,6 @@ Examples:
|
|
|
2776
2821
|
process.exit(1);
|
|
2777
2822
|
}
|
|
2778
2823
|
});
|
|
2779
|
-
// NemoClaw-specific helpers
|
|
2780
|
-
const NEMOCLAW_CHECK_CATEGORIES = nemoclaw_scanner_1.NEMOCLAW_CATEGORIES;
|
|
2781
|
-
function detectNemoClawDirectory(providedDir) {
|
|
2782
|
-
const os = require('os');
|
|
2783
|
-
const fs = require('fs');
|
|
2784
|
-
const path = require('path');
|
|
2785
|
-
if (providedDir && providedDir !== '') {
|
|
2786
|
-
return providedDir.startsWith('/') ? providedDir : path.join(process.cwd(), providedDir);
|
|
2787
|
-
}
|
|
2788
|
-
const homeDir = os.homedir();
|
|
2789
|
-
const candidates = [
|
|
2790
|
-
path.join(homeDir, '.nemoclaw'),
|
|
2791
|
-
path.join(homeDir, '.openshell'),
|
|
2792
|
-
path.join(homeDir, '.openclaw'),
|
|
2793
|
-
];
|
|
2794
|
-
for (const candidate of candidates) {
|
|
2795
|
-
if (fs.existsSync(candidate)) {
|
|
2796
|
-
return candidate;
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
return process.cwd();
|
|
2800
|
-
}
|
|
2801
|
-
function filterNemoClawFindings(findings) {
|
|
2802
|
-
return findings.filter((f) => {
|
|
2803
|
-
const checkId = f.checkId.toUpperCase();
|
|
2804
|
-
return checkId.startsWith('HMA-NMC-');
|
|
2805
|
-
});
|
|
2806
|
-
}
|
|
2807
|
-
function assessNemoClawRiskLevel(findings) {
|
|
2808
|
-
const criticalCount = findings.filter((f) => f.severity === 'critical').length;
|
|
2809
|
-
const highCount = findings.filter((f) => f.severity === 'high').length;
|
|
2810
|
-
const mediumCount = findings.filter((f) => f.severity === 'medium').length;
|
|
2811
|
-
if (criticalCount > 0) {
|
|
2812
|
-
return {
|
|
2813
|
-
level: 'Critical',
|
|
2814
|
-
color: colors.brightRed,
|
|
2815
|
-
description: `${criticalCount} critical finding(s) with recommended fixes available.`,
|
|
2816
|
-
};
|
|
2817
|
-
}
|
|
2818
|
-
if (highCount > 0) {
|
|
2819
|
-
return {
|
|
2820
|
-
level: 'High',
|
|
2821
|
-
color: colors.red,
|
|
2822
|
-
description: `${highCount} high-severity finding(s) detected. Fixes available below.`,
|
|
2823
|
-
};
|
|
2824
|
-
}
|
|
2825
|
-
if (mediumCount > 0) {
|
|
2826
|
-
return {
|
|
2827
|
-
level: 'Moderate',
|
|
2828
|
-
color: colors.yellow,
|
|
2829
|
-
description: 'Some findings detected. Review the recommendations below.',
|
|
2830
|
-
};
|
|
2831
|
-
}
|
|
2832
|
-
if (findings.length === 0) {
|
|
2833
|
-
return {
|
|
2834
|
-
level: 'None',
|
|
2835
|
-
color: colors.dim,
|
|
2836
|
-
description: `No NemoClaw installation detected. Run \`${CLI_PREFIX} secure\` for a full scan.`,
|
|
2837
|
-
};
|
|
2838
|
-
}
|
|
2839
|
-
return {
|
|
2840
|
-
level: 'Low',
|
|
2841
|
-
color: colors.green,
|
|
2842
|
-
description: 'No critical or high findings detected.',
|
|
2843
|
-
};
|
|
2844
|
-
}
|
|
2845
|
-
program
|
|
2846
|
-
.command('secure-nemoclaw')
|
|
2847
|
-
.description(`Security scan for NVIDIA NemoClaw installations
|
|
2848
|
-
|
|
2849
|
-
Performs focused security checks for NemoClaw sandbox deployments:
|
|
2850
|
-
- Secrets: NVIDIA API key exposure in configs, logs, Docker, shell history
|
|
2851
|
-
- Network: Gateway/k3s/inference port binding, Docker socket, egress policies
|
|
2852
|
-
- Skills: Blueprint integrity, skill verification, directory permissions
|
|
2853
|
-
- Process: Sandbox privileges, seccomp/Landlock enforcement, root execution
|
|
2854
|
-
- OpenClaw layer: Inherited misconfigs that survive NemoClaw sandboxing
|
|
2855
|
-
|
|
2856
|
-
Auto-detects ~/.nemoclaw, ~/.openshell, or ~/.openclaw directories.
|
|
2857
|
-
Exit code 1 if critical/high issues found.
|
|
2858
|
-
|
|
2859
|
-
Examples:
|
|
2860
|
-
$ hackmyagent secure-nemoclaw Scan auto-detected directory
|
|
2861
|
-
$ hackmyagent secure-nemoclaw ~/.nemoclaw Scan specific directory
|
|
2862
|
-
$ hackmyagent secure-nemoclaw --json JSON output for CI`)
|
|
2863
|
-
.argument('[directory]', 'Directory to scan (default: ~/.nemoclaw or ~/.openshell)', '')
|
|
2864
|
-
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
2865
|
-
.option('-v, --verbose', 'Show all checks including passed ones')
|
|
2866
|
-
.action(async (directory, options) => {
|
|
2867
|
-
try {
|
|
2868
|
-
const targetDir = detectNemoClawDirectory(directory);
|
|
2869
|
-
if (!options.json) {
|
|
2870
|
-
console.log(`\nNemoClaw Security Report`);
|
|
2871
|
-
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
2872
|
-
console.log(`Scanning ${targetDir}...\n`);
|
|
2873
|
-
}
|
|
2874
|
-
const scanner = new nemoclaw_scanner_1.NemoClawScanner();
|
|
2875
|
-
const findings = await scanner.scan(targetDir, {});
|
|
2876
|
-
// Enrich with taxonomy
|
|
2877
|
-
const { enrichWithTaxonomy } = require('./hardening/taxonomy');
|
|
2878
|
-
enrichWithTaxonomy(findings);
|
|
2879
|
-
// NanoMind semantic analysis (defense-in-depth)
|
|
2880
|
-
let mergedFindings = findings;
|
|
2881
|
-
try {
|
|
2882
|
-
const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
|
|
2883
|
-
const nmResult = await orchestrateNanoMind(targetDir, findings, { silent: !!options.json });
|
|
2884
|
-
mergedFindings = nmResult.mergedFindings;
|
|
2885
|
-
}
|
|
2886
|
-
catch { /* NanoMind unavailable */ }
|
|
2887
|
-
const issues = mergedFindings.filter((f) => !f.passed);
|
|
2888
|
-
const passedFindings = mergedFindings.filter((f) => f.passed);
|
|
2889
|
-
if (options.json) {
|
|
2890
|
-
const jsonOutput = {
|
|
2891
|
-
target: targetDir,
|
|
2892
|
-
riskLevel: assessNemoClawRiskLevel(issues).level,
|
|
2893
|
-
totalChecks: mergedFindings.length,
|
|
2894
|
-
issues: issues.length,
|
|
2895
|
-
passed: passedFindings.length,
|
|
2896
|
-
findings: mergedFindings,
|
|
2897
|
-
};
|
|
2898
|
-
writeJsonStdout(jsonOutput);
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
// Risk assessment
|
|
2902
|
-
const risk = assessNemoClawRiskLevel(issues);
|
|
2903
|
-
console.log(`Risk Level: ${risk.color}${risk.level}${RESET()}`);
|
|
2904
|
-
console.log(`${risk.description}\n`);
|
|
2905
|
-
// Summary stats
|
|
2906
|
-
console.log(`Checks: ${findings.length} total | ${issues.length} issues | ${passedFindings.length} passed\n`);
|
|
2907
|
-
// Show issues
|
|
2908
|
-
if (issues.length > 0) {
|
|
2909
|
-
console.log(`${colors.red}Findings:${RESET()}\n`);
|
|
2910
|
-
for (const finding of issues) {
|
|
2911
|
-
const display = SEVERITY_DISPLAY[finding.severity];
|
|
2912
|
-
const location = finding.file
|
|
2913
|
-
? finding.line
|
|
2914
|
-
? `${finding.file}:${finding.line}`
|
|
2915
|
-
: finding.file
|
|
2916
|
-
: '';
|
|
2917
|
-
const sevLabel = finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1);
|
|
2918
|
-
console.log(`${display.color()}${display.symbol} [${finding.checkId}] ${sevLabel}${RESET()}`);
|
|
2919
|
-
console.log(` ${finding.description}`);
|
|
2920
|
-
if (location) {
|
|
2921
|
-
console.log(` File: ${location}`);
|
|
2922
|
-
}
|
|
2923
|
-
if (finding.fix) {
|
|
2924
|
-
console.log(` ${colors.cyan}Recommended fix:${RESET()} ${finding.fix}`);
|
|
2925
|
-
}
|
|
2926
|
-
console.log();
|
|
2927
|
-
}
|
|
2928
|
-
}
|
|
2929
|
-
else {
|
|
2930
|
-
console.log(`${colors.green}No NemoClaw-specific issues found.${RESET()}\n`);
|
|
2931
|
-
}
|
|
2932
|
-
// Show passed checks in verbose mode
|
|
2933
|
-
if (options.verbose && passedFindings.length > 0) {
|
|
2934
|
-
console.log(`${colors.green}Passed Checks:${RESET()}`);
|
|
2935
|
-
for (const finding of passedFindings) {
|
|
2936
|
-
console.log(` ${colors.green}[ok]${RESET()} [${finding.checkId}] ${finding.name}`);
|
|
2937
|
-
}
|
|
2938
|
-
console.log();
|
|
2939
|
-
}
|
|
2940
|
-
// Shodan self-check guidance
|
|
2941
|
-
if (issues.some((f) => f.category === 'network')) {
|
|
2942
|
-
console.log(`${colors.yellow}Internet Exposure Check:${RESET()}`);
|
|
2943
|
-
console.log(` Check if your instance is visible on Shodan:`);
|
|
2944
|
-
console.log(` https://www.shodan.io/host/<YOUR-IP>`);
|
|
2945
|
-
console.log(` Known NemoClaw dorks: port:18789, port:6443 ssl.cert.subject.cn:"k3s-serving"\n`);
|
|
2946
|
-
}
|
|
2947
|
-
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
2948
|
-
console.log(`Run '${CLI_PREFIX} secure-openclaw' for OpenClaw-specific checks.`);
|
|
2949
|
-
console.log(`Run '${CLI_PREFIX} secure' for a full security scan.\n`);
|
|
2950
|
-
// Exit with non-zero if critical/high issues remain
|
|
2951
|
-
const criticalOrHigh = issues.filter((f) => f.severity === 'critical' || f.severity === 'high');
|
|
2952
|
-
if (criticalOrHigh.length > 0) {
|
|
2953
|
-
process.exit(1);
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
catch (error) {
|
|
2957
|
-
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2958
|
-
process.exit(1);
|
|
2959
|
-
}
|
|
2960
|
-
});
|
|
2961
2824
|
program
|
|
2962
2825
|
.command('scan')
|
|
2963
2826
|
.description(`Scan external target for exposed MCP endpoints
|
|
@@ -4534,7 +4397,7 @@ Examples:
|
|
|
4534
4397
|
console.log(`\n Detected: ${result.tool}\n`);
|
|
4535
4398
|
console.log(` Added HackMyAgent MCP server to ${result.configPath}\n`);
|
|
4536
4399
|
console.log(` Available tools in ${result.tool}:`);
|
|
4537
|
-
console.log(` hackmyagent_scan —
|
|
4400
|
+
console.log(` hackmyagent_scan — ${CHECK_COUNT} checks + structural analysis`);
|
|
4538
4401
|
console.log(` hackmyagent_deep_scan — Full analysis with LLM reasoning`);
|
|
4539
4402
|
console.log(` hackmyagent_analyze_file — Analyze a single file`);
|
|
4540
4403
|
console.log(` hackmyagent_benchmark — OASB-1 compliance assessment\n`);
|
|
@@ -5373,12 +5236,39 @@ program
|
|
|
5373
5236
|
// Fallback: static explanation from check metadata
|
|
5374
5237
|
const checkId = findingId.toUpperCase();
|
|
5375
5238
|
const staticExplanations = {
|
|
5239
|
+
// Credential checks
|
|
5376
5240
|
'CRED-001': 'Hardcoded credential detected. API keys, tokens, or passwords are embedded directly in source code. Replace with environment variable references ($VAR_NAME) and rotate the exposed credential immediately.',
|
|
5377
5241
|
'CRED-002': 'OpenAI API key pattern detected (sk-...). Move to environment variable OPENAI_API_KEY.',
|
|
5378
5242
|
'CRED-003': 'Anthropic API key pattern detected (sk-ant-...). Move to environment variable ANTHROPIC_API_KEY.',
|
|
5379
5243
|
'CRED-004': 'AWS credential pattern detected. Use AWS SDK credential chain or environment variables.',
|
|
5380
|
-
|
|
5381
|
-
'
|
|
5244
|
+
// MCP checks
|
|
5245
|
+
'MCP-001': 'MCP server running without TLS. Agent-to-server communication is unencrypted. Enable TLS on the MCP server or use a reverse proxy with TLS termination.',
|
|
5246
|
+
// Skill checks
|
|
5247
|
+
'SKILL-005': 'External endpoint in skill capability declaration. Verify the endpoint is trusted and uses HTTPS.',
|
|
5248
|
+
// Governance checks
|
|
5249
|
+
'GOV-001': 'No governance policy found. Agents should declare behavioral constraints in a SOUL.md or governance file. Create a SOUL.md with mission, boundaries, and allowed actions.',
|
|
5250
|
+
'GOV-002': 'Governance file lacks boundary definitions. Without explicit boundaries, the agent may act outside intended scope. Add "boundaries" or "constraints" sections to your governance file.',
|
|
5251
|
+
'GOV-003': 'Governance file missing escalation policy. Define when and how the agent should escalate to a human. Add an escalation section with trigger conditions and contact methods.',
|
|
5252
|
+
// Permission checks
|
|
5253
|
+
'PERM-001': 'Overly broad file system permissions detected. The agent has write access to directories outside its working scope. Restrict file permissions to the minimum required paths.',
|
|
5254
|
+
'PERM-002': 'Network permissions not restricted. The agent can make outbound requests to any host. Define an allowlist of permitted domains in the agent configuration.',
|
|
5255
|
+
'PERM-003': 'Execution permissions too permissive. The agent can spawn arbitrary processes. Restrict executable permissions to specific, required binaries only.',
|
|
5256
|
+
// SOUL checks
|
|
5257
|
+
'SOUL-001': 'No SOUL.md file found. SOUL.md defines the agent identity, mission, and behavioral constraints. Run `hackmyagent secure --fix` to generate one.',
|
|
5258
|
+
'SOUL-002': 'SOUL.md missing identity section. The agent lacks a declared identity, making impersonation easier. Add name, version, and publisher fields.',
|
|
5259
|
+
'SOUL-003': 'SOUL.md missing behavioral boundaries. Without explicit limits, the agent may perform unintended actions. Add a boundaries section listing prohibited behaviors.',
|
|
5260
|
+
// Privacy checks
|
|
5261
|
+
'PRIV-001': 'PII handling not declared. The agent processes data but has no privacy policy or data handling declaration. Add a data handling section specifying what data is collected, stored, and shared.',
|
|
5262
|
+
// Data checks
|
|
5263
|
+
'DATA-001': 'Sensitive data logged to console or file. Credentials, tokens, or PII appear in log output. Sanitize log statements to redact sensitive values before output.',
|
|
5264
|
+
'DATA-002': 'Data retention policy missing. The agent stores data without a defined retention or deletion policy. Define how long data is kept and when it is purged.',
|
|
5265
|
+
// Injection checks
|
|
5266
|
+
'INJECT-001': 'No prompt injection defense detected. The agent does not validate or sanitize inputs against injection attacks. Add input validation and consider using a system prompt with injection resistance instructions.',
|
|
5267
|
+
'INJECT-002': 'Indirect prompt injection surface found. External data (URLs, files, API responses) is passed to the LLM without sanitization. Sanitize or sandbox external content before including it in prompts.',
|
|
5268
|
+
// Attestation checks
|
|
5269
|
+
'ATTEST-001': 'No attestation mechanism found. The agent cannot prove its identity or integrity to other agents. Implement agent attestation using signed identity tokens or SOUL.md signatures.',
|
|
5270
|
+
// Supply chain checks
|
|
5271
|
+
'SUPPLY-001': 'Dependency with known vulnerability detected. A transitive or direct dependency has a published CVE. Update the affected package to a patched version.',
|
|
5382
5272
|
};
|
|
5383
5273
|
const explanation = staticExplanations[checkId];
|
|
5384
5274
|
if (explanation) {
|
|
@@ -5754,6 +5644,18 @@ program
|
|
|
5754
5644
|
// ============================================================================
|
|
5755
5645
|
// npm package scanning helpers (used by `check <package>`)
|
|
5756
5646
|
// ============================================================================
|
|
5647
|
+
/**
|
|
5648
|
+
* Detect whether a string looks like a PyPI package reference.
|
|
5649
|
+
*
|
|
5650
|
+
* Requires an explicit prefix:
|
|
5651
|
+
* - pip:package-name
|
|
5652
|
+
* - pypi:package-name
|
|
5653
|
+
*
|
|
5654
|
+
* Bare names are NOT auto-detected as PyPI (they fall through to npm).
|
|
5655
|
+
*/
|
|
5656
|
+
function looksLikePyPiPackage(target) {
|
|
5657
|
+
return target.startsWith('pip:') || target.startsWith('pypi:');
|
|
5658
|
+
}
|
|
5757
5659
|
/**
|
|
5758
5660
|
* Detect whether a string looks like an npm package name rather than
|
|
5759
5661
|
* a hostname, IP address, or local path.
|
|
@@ -6124,21 +6026,7 @@ async function checkGitHubRepo(target, options) {
|
|
|
6124
6026
|
console.log(` Type: ${result.projectType}`);
|
|
6125
6027
|
console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
|
|
6126
6028
|
console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
|
|
6127
|
-
|
|
6128
|
-
console.log();
|
|
6129
|
-
const limit = options.verbose ? failed.length : 15;
|
|
6130
|
-
for (const f of failed.slice(0, limit)) {
|
|
6131
|
-
const sev = SEVERITY_DISPLAY[f.severity];
|
|
6132
|
-
const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
|
|
6133
|
-
console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
|
|
6134
|
-
}
|
|
6135
|
-
if (failed.length > limit) {
|
|
6136
|
-
console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
|
|
6137
|
-
}
|
|
6138
|
-
}
|
|
6139
|
-
else {
|
|
6140
|
-
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
6141
|
-
}
|
|
6029
|
+
displayCheckFindings(failed, !!options.verbose);
|
|
6142
6030
|
// Step 3: Community contribution
|
|
6143
6031
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6144
6032
|
const scanCount = incrementScanCounter();
|
|
@@ -6202,6 +6090,139 @@ async function checkGitHubRepo(target, options) {
|
|
|
6202
6090
|
* Download an npm package, run full HMA secure scan, display results, clean up.
|
|
6203
6091
|
* Checks the registry first; only downloads if data is missing or stale.
|
|
6204
6092
|
*/
|
|
6093
|
+
/**
|
|
6094
|
+
* Download a PyPI package, scan it with HMA + NanoMind, and display results.
|
|
6095
|
+
* Accepts targets prefixed with pip: or pypi: (e.g. pip:requests, pypi:flask).
|
|
6096
|
+
*/
|
|
6097
|
+
async function checkPyPiPackage(target, options) {
|
|
6098
|
+
// Strip prefix to get the bare package name
|
|
6099
|
+
const name = target.replace(/^(pip|pypi):/, '');
|
|
6100
|
+
const { mkdtemp, rm, readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
6101
|
+
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
6102
|
+
const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
6103
|
+
const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
6104
|
+
if (!options.json && !globalCiMode) {
|
|
6105
|
+
console.error(`Downloading ${name} from PyPI...`);
|
|
6106
|
+
}
|
|
6107
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'hma-check-pypi-'));
|
|
6108
|
+
try {
|
|
6109
|
+
// Fetch package metadata from PyPI JSON API
|
|
6110
|
+
const metaRes = await fetch(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`);
|
|
6111
|
+
if (!metaRes.ok) {
|
|
6112
|
+
if (metaRes.status === 404) {
|
|
6113
|
+
console.error(`Error: Package "${name}" not found on PyPI.`);
|
|
6114
|
+
}
|
|
6115
|
+
else {
|
|
6116
|
+
console.error(`Error: PyPI API returned ${metaRes.status} for "${name}".`);
|
|
6117
|
+
}
|
|
6118
|
+
process.exit(1);
|
|
6119
|
+
}
|
|
6120
|
+
const meta = await metaRes.json();
|
|
6121
|
+
// Prefer sdist (source tarball) for scanning; fall back to first wheel
|
|
6122
|
+
const sdist = meta.urls.find((u) => u.packagetype === 'sdist');
|
|
6123
|
+
const wheel = meta.urls.find((u) => u.packagetype === 'bdist_wheel');
|
|
6124
|
+
const dist = sdist || wheel || meta.urls[0];
|
|
6125
|
+
if (!dist) {
|
|
6126
|
+
console.error(`Error: No downloadable distribution found for "${name}" on PyPI.`);
|
|
6127
|
+
process.exit(1);
|
|
6128
|
+
}
|
|
6129
|
+
// Download the archive
|
|
6130
|
+
const archiveRes = await fetch(dist.url);
|
|
6131
|
+
if (!archiveRes.ok) {
|
|
6132
|
+
throw new Error(`Failed to download ${dist.filename}: HTTP ${archiveRes.status}`);
|
|
6133
|
+
}
|
|
6134
|
+
const archiveBuffer = Buffer.from(await archiveRes.arrayBuffer());
|
|
6135
|
+
const archivePath = join(tempDir, dist.filename);
|
|
6136
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
6137
|
+
writeFileSync(archivePath, archiveBuffer);
|
|
6138
|
+
// Extract
|
|
6139
|
+
const extractDir = join(tempDir, 'package');
|
|
6140
|
+
const { mkdirSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
6141
|
+
mkdirSync(extractDir, { recursive: true });
|
|
6142
|
+
if (dist.filename.endsWith('.tar.gz') || dist.filename.endsWith('.tgz')) {
|
|
6143
|
+
execFileSync('tar', ['xzf', archivePath, '-C', extractDir, '--strip-components=1'], { timeout: 30000 });
|
|
6144
|
+
}
|
|
6145
|
+
else if (dist.filename.endsWith('.zip') || dist.filename.endsWith('.whl')) {
|
|
6146
|
+
execFileSync('unzip', ['-q', '-o', archivePath, '-d', extractDir], { timeout: 30000 });
|
|
6147
|
+
}
|
|
6148
|
+
else {
|
|
6149
|
+
throw new Error(`Unsupported archive format: ${dist.filename}`);
|
|
6150
|
+
}
|
|
6151
|
+
// Run full HMA scan + NanoMind (same pipeline as checkNpmPackage)
|
|
6152
|
+
const scanner = new index_1.HardeningScanner();
|
|
6153
|
+
const result = await scanner.scan({ targetDir: extractDir, autoFix: false });
|
|
6154
|
+
// Run NanoMind semantic analysis and re-filter
|
|
6155
|
+
try {
|
|
6156
|
+
const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
|
|
6157
|
+
const nmResult = await orchestrateNanoMind(extractDir, result.findings, { silent: true });
|
|
6158
|
+
const refiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, extractDir);
|
|
6159
|
+
const projectType = result.projectType || 'library';
|
|
6160
|
+
result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
|
|
6161
|
+
result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
|
|
6162
|
+
}
|
|
6163
|
+
catch {
|
|
6164
|
+
// NanoMind unavailable -- use base scan results
|
|
6165
|
+
}
|
|
6166
|
+
const failed = result.findings.filter(f => !f.passed);
|
|
6167
|
+
const critical = failed.filter(f => f.severity === 'critical');
|
|
6168
|
+
const high = failed.filter(f => f.severity === 'high');
|
|
6169
|
+
const medium = failed.filter(f => f.severity === 'medium');
|
|
6170
|
+
const low = failed.filter(f => f.severity === 'low');
|
|
6171
|
+
if (options.json) {
|
|
6172
|
+
writeJsonStdout({
|
|
6173
|
+
name,
|
|
6174
|
+
type: 'pypi-package',
|
|
6175
|
+
source: 'local-scan',
|
|
6176
|
+
version: meta.info.version,
|
|
6177
|
+
projectType: result.projectType,
|
|
6178
|
+
score: result.score,
|
|
6179
|
+
maxScore: result.maxScore,
|
|
6180
|
+
findings: result.findings,
|
|
6181
|
+
});
|
|
6182
|
+
return;
|
|
6183
|
+
}
|
|
6184
|
+
// Display results
|
|
6185
|
+
const scoreRatio = result.score / result.maxScore;
|
|
6186
|
+
const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
|
|
6187
|
+
console.log(`\n ${name} (PyPI)`);
|
|
6188
|
+
console.log(` Version: ${meta.info.version}`);
|
|
6189
|
+
console.log(` Type: ${result.projectType}`);
|
|
6190
|
+
console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
|
|
6191
|
+
console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
|
|
6192
|
+
if (failed.length > 0) {
|
|
6193
|
+
console.log();
|
|
6194
|
+
const limit = options.verbose ? failed.length : 15;
|
|
6195
|
+
for (const f of failed.slice(0, limit)) {
|
|
6196
|
+
const sev = SEVERITY_DISPLAY[f.severity];
|
|
6197
|
+
const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
|
|
6198
|
+
console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
|
|
6199
|
+
}
|
|
6200
|
+
if (failed.length > limit) {
|
|
6201
|
+
console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
else {
|
|
6205
|
+
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
6206
|
+
}
|
|
6207
|
+
console.log(`\n Full project scan: ${CLI_PREFIX} secure <dir>`);
|
|
6208
|
+
console.log();
|
|
6209
|
+
if (critical.length > 0 || high.length > 0)
|
|
6210
|
+
process.exit(1);
|
|
6211
|
+
}
|
|
6212
|
+
catch (err) {
|
|
6213
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6214
|
+
if (message.includes('not found on PyPI')) {
|
|
6215
|
+
console.error(`Error: ${message}`);
|
|
6216
|
+
}
|
|
6217
|
+
else {
|
|
6218
|
+
console.error(`Error scanning PyPI package "${name}": ${message}`);
|
|
6219
|
+
}
|
|
6220
|
+
process.exit(1);
|
|
6221
|
+
}
|
|
6222
|
+
finally {
|
|
6223
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6205
6226
|
async function checkNpmPackage(name, options) {
|
|
6206
6227
|
// Step 1: Check registry for existing trust data
|
|
6207
6228
|
if (!options.offline) {
|
|
@@ -6279,21 +6300,7 @@ async function checkNpmPackage(name, options) {
|
|
|
6279
6300
|
console.log(` Type: ${result.projectType}`);
|
|
6280
6301
|
console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
|
|
6281
6302
|
console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
|
|
6282
|
-
|
|
6283
|
-
console.log();
|
|
6284
|
-
const limit = options.verbose ? failed.length : 15;
|
|
6285
|
-
for (const f of failed.slice(0, limit)) {
|
|
6286
|
-
const sev = SEVERITY_DISPLAY[f.severity];
|
|
6287
|
-
const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
|
|
6288
|
-
console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
|
|
6289
|
-
}
|
|
6290
|
-
if (failed.length > limit) {
|
|
6291
|
-
console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
|
|
6292
|
-
}
|
|
6293
|
-
}
|
|
6294
|
-
else {
|
|
6295
|
-
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
6296
|
-
}
|
|
6303
|
+
displayCheckFindings(failed, !!options.verbose);
|
|
6297
6304
|
// Step 3: Community contribution (after 3 scans, interactive only)
|
|
6298
6305
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6299
6306
|
const scanCount = incrementScanCounter();
|