hackmyagent 0.15.7 → 0.16.1
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/arp/intelligence/nanomind-l1.d.ts +30 -0
- package/dist/arp/intelligence/nanomind-l1.d.ts.map +1 -1
- package/dist/arp/intelligence/nanomind-l1.js +115 -0
- package/dist/arp/intelligence/nanomind-l1.js.map +1 -1
- package/dist/cli.js +452 -244
- package/dist/cli.js.map +1 -1
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +125 -4
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/hardening/taxonomy.d.ts +2 -0
- package/dist/hardening/taxonomy.d.ts.map +1 -1
- package/dist/hardening/taxonomy.js +5 -0
- package/dist/hardening/taxonomy.js.map +1 -1
- package/dist/nanomind-core/analyzers/stego-analyzer.d.ts +30 -0
- package/dist/nanomind-core/analyzers/stego-analyzer.d.ts.map +1 -0
- package/dist/nanomind-core/analyzers/stego-analyzer.js +533 -0
- package/dist/nanomind-core/analyzers/stego-analyzer.js.map +1 -0
- package/dist/nanomind-core/daemon-lifecycle.d.ts +28 -0
- package/dist/nanomind-core/daemon-lifecycle.d.ts.map +1 -0
- package/dist/nanomind-core/daemon-lifecycle.js +142 -0
- package/dist/nanomind-core/daemon-lifecycle.js.map +1 -0
- package/dist/nanomind-core/inference/tme-classifier.d.ts +3 -2
- package/dist/nanomind-core/inference/tme-classifier.d.ts.map +1 -1
- package/dist/nanomind-core/inference/tme-classifier.js +26 -16
- package/dist/nanomind-core/inference/tme-classifier.js.map +1 -1
- package/dist/nanomind-core/orchestrate.d.ts.map +1 -1
- package/dist/nanomind-core/orchestrate.js +11 -1
- package/dist/nanomind-core/orchestrate.js.map +1 -1
- package/dist/nanomind-core/scanner-bridge.d.ts.map +1 -1
- package/dist/nanomind-core/scanner-bridge.js +6 -0
- package/dist/nanomind-core/scanner-bridge.js.map +1 -1
- package/dist/plugins/credvault.d.ts.map +1 -1
- package/dist/plugins/credvault.js +25 -0
- package/dist/plugins/credvault.js.map +1 -1
- package/dist/semantic/nanomind-enhancer.d.ts.map +1 -1
- package/dist/semantic/nanomind-enhancer.js +206 -0
- package/dist/semantic/nanomind-enhancer.js.map +1 -1
- package/dist/telemetry/nanomind-feedback.d.ts +43 -0
- package/dist/telemetry/nanomind-feedback.d.ts.map +1 -0
- package/dist/telemetry/nanomind-feedback.js +104 -0
- package/dist/telemetry/nanomind-feedback.js.map +1 -0
- package/dist/telemetry/nanomind-telemetry.d.ts +48 -0
- package/dist/telemetry/nanomind-telemetry.d.ts.map +1 -0
- package/dist/telemetry/nanomind-telemetry.js +123 -0
- package/dist/telemetry/nanomind-telemetry.js.map +1 -0
- 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,12 @@ 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
|
|
188
|
-
$ hackmyagent check
|
|
189
|
-
|
|
189
|
+
$ hackmyagent check pip:requests
|
|
190
|
+
$ hackmyagent check pypi:flask
|
|
191
|
+
$ hackmyagent check modelcontextprotocol/servers --json
|
|
192
|
+
$ hackmyagent check https://gitlab.com/org/repo
|
|
193
|
+
$ hackmyagent check https://example.com/agent-v1.tar.gz`)
|
|
194
|
+
.argument('<target>', 'npm package, PyPI package (pip: or pypi: prefix), local path, GitHub repo, or skill identifier')
|
|
190
195
|
.option('-v, --verbose', 'Show detailed verification info')
|
|
191
196
|
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
192
197
|
.option('--offline', 'Skip DNS verification (offline mode)')
|
|
@@ -244,11 +249,21 @@ Examples:
|
|
|
244
249
|
process.exit(1);
|
|
245
250
|
return;
|
|
246
251
|
}
|
|
252
|
+
// PyPI package: download, run full HMA scan, clean up
|
|
253
|
+
if (looksLikePyPiPackage(skill)) {
|
|
254
|
+
await checkPyPiPackage(skill, options);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
247
257
|
// GitHub repo: clone, run full HMA scan, clean up
|
|
248
258
|
if (looksLikeGitHubRepo(skill)) {
|
|
249
259
|
await checkGitHubRepo(skill, options);
|
|
250
260
|
return;
|
|
251
261
|
}
|
|
262
|
+
// Raw URL (non-GitHub): fetch/clone based on content type
|
|
263
|
+
if (looksLikeRawUrl(skill)) {
|
|
264
|
+
await checkRawUrl(skill, options);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
252
267
|
// npm package name: download, run full HMA scan, clean up
|
|
253
268
|
if (looksLikeNpmPackage(skill)) {
|
|
254
269
|
await checkNpmPackage(skill, options);
|
|
@@ -343,6 +358,43 @@ const SEVERITY_DISPLAY = {
|
|
|
343
358
|
medium: { symbol: '[~]', color: () => colors.yellow },
|
|
344
359
|
low: { symbol: '[.]', color: () => colors.green },
|
|
345
360
|
};
|
|
361
|
+
/**
|
|
362
|
+
* Display check command findings with optional verbose details.
|
|
363
|
+
* When verbose is true, shows checkId, category, file location, and fix/guidance for each finding.
|
|
364
|
+
*/
|
|
365
|
+
function displayCheckFindings(failed, verbose) {
|
|
366
|
+
if (failed.length > 0) {
|
|
367
|
+
console.log();
|
|
368
|
+
const limit = verbose ? failed.length : 15;
|
|
369
|
+
for (const f of failed.slice(0, limit)) {
|
|
370
|
+
const sev = SEVERITY_DISPLAY[f.severity];
|
|
371
|
+
const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
|
|
372
|
+
console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
|
|
373
|
+
if (verbose) {
|
|
374
|
+
console.log(` ${colors.dim}Check: ${f.checkId}${RESET()}`);
|
|
375
|
+
if (f.category) {
|
|
376
|
+
console.log(` ${colors.dim}Category: ${f.category}${RESET()}`);
|
|
377
|
+
}
|
|
378
|
+
if (f.file) {
|
|
379
|
+
const location = f.line ? `${f.file}:${f.line}` : f.file;
|
|
380
|
+
console.log(` ${colors.dim}File: ${location}${RESET()}`);
|
|
381
|
+
}
|
|
382
|
+
if (f.fix) {
|
|
383
|
+
console.log(` ${colors.cyan}Fix: ${f.fix}${RESET()}`);
|
|
384
|
+
}
|
|
385
|
+
if (f.guidance) {
|
|
386
|
+
console.log(` ${colors.dim}Guidance: ${f.guidance}${RESET()}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (failed.length > limit) {
|
|
391
|
+
console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
346
398
|
function groupFindingsBySeverity(findings) {
|
|
347
399
|
const grouped = {
|
|
348
400
|
critical: [],
|
|
@@ -1774,7 +1826,7 @@ program
|
|
|
1774
1826
|
.command('secure')
|
|
1775
1827
|
.description(`Scan and harden your agent setup
|
|
1776
1828
|
|
|
1777
|
-
Performs
|
|
1829
|
+
Performs ${CHECK_COUNT} security checks across 60 categories:
|
|
1778
1830
|
• Credentials: API key exposure, secrets in configs
|
|
1779
1831
|
• MCP: Server configs, tool permissions, secrets
|
|
1780
1832
|
• Network: TLS, interface bindings, CORS
|
|
@@ -2776,188 +2828,6 @@ Examples:
|
|
|
2776
2828
|
process.exit(1);
|
|
2777
2829
|
}
|
|
2778
2830
|
});
|
|
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
2831
|
program
|
|
2962
2832
|
.command('scan')
|
|
2963
2833
|
.description(`Scan external target for exposed MCP endpoints
|
|
@@ -4534,7 +4404,7 @@ Examples:
|
|
|
4534
4404
|
console.log(`\n Detected: ${result.tool}\n`);
|
|
4535
4405
|
console.log(` Added HackMyAgent MCP server to ${result.configPath}\n`);
|
|
4536
4406
|
console.log(` Available tools in ${result.tool}:`);
|
|
4537
|
-
console.log(` hackmyagent_scan —
|
|
4407
|
+
console.log(` hackmyagent_scan — ${CHECK_COUNT} checks + structural analysis`);
|
|
4538
4408
|
console.log(` hackmyagent_deep_scan — Full analysis with LLM reasoning`);
|
|
4539
4409
|
console.log(` hackmyagent_analyze_file — Analyze a single file`);
|
|
4540
4410
|
console.log(` hackmyagent_benchmark — OASB-1 compliance assessment\n`);
|
|
@@ -5328,26 +5198,44 @@ Examples:
|
|
|
5328
5198
|
});
|
|
5329
5199
|
program
|
|
5330
5200
|
.command('check-metadata')
|
|
5331
|
-
.description('Export metadata for all security checks
|
|
5332
|
-
.option('-d, --directory <dir>', '
|
|
5201
|
+
.description('Export metadata for all security checks (JSON)')
|
|
5202
|
+
.option('-d, --directory <dir>', 'Scan a specific directory to collect check metadata from findings')
|
|
5203
|
+
.option('--json', 'Output as JSON (default)')
|
|
5333
5204
|
.action(async (options) => {
|
|
5334
|
-
const { getAttackClass } = require('./hardening/taxonomy');
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
const scanner = new index_1.HardeningScanner();
|
|
5338
|
-
const result = await scanner.scan({ targetDir, autoFix: false, scanDepth: 'deep' });
|
|
5205
|
+
const { getAttackClass, getTaxonomyMap } = require('./hardening/taxonomy');
|
|
5206
|
+
// Build static registry from taxonomy map (covers all known checks)
|
|
5207
|
+
const taxMap = getTaxonomyMap();
|
|
5339
5208
|
const metadata = {};
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5209
|
+
// Add all checks from taxonomy (the authoritative source of check IDs)
|
|
5210
|
+
for (const checkId of Object.keys(taxMap)) {
|
|
5211
|
+
const prefix = checkId.split('-').slice(0, -1).join('-') || checkId.split('-')[0];
|
|
5212
|
+
metadata[checkId] = {
|
|
5213
|
+
checkId,
|
|
5214
|
+
name: checkId,
|
|
5215
|
+
category: prefix.toLowerCase(),
|
|
5216
|
+
attackClass: taxMap[checkId] || '',
|
|
5217
|
+
severity: '',
|
|
5218
|
+
};
|
|
5219
|
+
}
|
|
5220
|
+
// If a directory is provided, enrich with actual finding data (names, severity, etc.)
|
|
5221
|
+
if (options.directory) {
|
|
5222
|
+
const scanner = new index_1.HardeningScanner();
|
|
5223
|
+
const result = await scanner.scan({ targetDir: options.directory, autoFix: false, scanDepth: 'deep' });
|
|
5224
|
+
for (const finding of result.findings) {
|
|
5225
|
+
if (metadata[finding.checkId]) {
|
|
5226
|
+
metadata[finding.checkId].name = finding.name;
|
|
5227
|
+
metadata[finding.checkId].category = finding.category;
|
|
5228
|
+
metadata[finding.checkId].severity = finding.severity;
|
|
5229
|
+
}
|
|
5230
|
+
else {
|
|
5231
|
+
metadata[finding.checkId] = {
|
|
5232
|
+
checkId: finding.checkId,
|
|
5233
|
+
name: finding.name,
|
|
5234
|
+
category: finding.category,
|
|
5235
|
+
attackClass: getAttackClass(finding.checkId) || '',
|
|
5236
|
+
severity: finding.severity,
|
|
5237
|
+
};
|
|
5238
|
+
}
|
|
5351
5239
|
}
|
|
5352
5240
|
}
|
|
5353
5241
|
writeJsonStdout({ totalChecks: Object.keys(metadata).length, checks: metadata });
|
|
@@ -5373,12 +5261,39 @@ program
|
|
|
5373
5261
|
// Fallback: static explanation from check metadata
|
|
5374
5262
|
const checkId = findingId.toUpperCase();
|
|
5375
5263
|
const staticExplanations = {
|
|
5264
|
+
// Credential checks
|
|
5376
5265
|
'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
5266
|
'CRED-002': 'OpenAI API key pattern detected (sk-...). Move to environment variable OPENAI_API_KEY.',
|
|
5378
5267
|
'CRED-003': 'Anthropic API key pattern detected (sk-ant-...). Move to environment variable ANTHROPIC_API_KEY.',
|
|
5379
5268
|
'CRED-004': 'AWS credential pattern detected. Use AWS SDK credential chain or environment variables.',
|
|
5380
|
-
|
|
5381
|
-
'
|
|
5269
|
+
// MCP checks
|
|
5270
|
+
'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.',
|
|
5271
|
+
// Skill checks
|
|
5272
|
+
'SKILL-005': 'External endpoint in skill capability declaration. Verify the endpoint is trusted and uses HTTPS.',
|
|
5273
|
+
// Governance checks
|
|
5274
|
+
'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.',
|
|
5275
|
+
'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.',
|
|
5276
|
+
'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.',
|
|
5277
|
+
// Permission checks
|
|
5278
|
+
'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.',
|
|
5279
|
+
'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.',
|
|
5280
|
+
'PERM-003': 'Execution permissions too permissive. The agent can spawn arbitrary processes. Restrict executable permissions to specific, required binaries only.',
|
|
5281
|
+
// SOUL checks
|
|
5282
|
+
'SOUL-001': 'No SOUL.md file found. SOUL.md defines the agent identity, mission, and behavioral constraints. Run `hackmyagent secure --fix` to generate one.',
|
|
5283
|
+
'SOUL-002': 'SOUL.md missing identity section. The agent lacks a declared identity, making impersonation easier. Add name, version, and publisher fields.',
|
|
5284
|
+
'SOUL-003': 'SOUL.md missing behavioral boundaries. Without explicit limits, the agent may perform unintended actions. Add a boundaries section listing prohibited behaviors.',
|
|
5285
|
+
// Privacy checks
|
|
5286
|
+
'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.',
|
|
5287
|
+
// Data checks
|
|
5288
|
+
'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.',
|
|
5289
|
+
'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.',
|
|
5290
|
+
// Injection checks
|
|
5291
|
+
'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.',
|
|
5292
|
+
'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.',
|
|
5293
|
+
// Attestation checks
|
|
5294
|
+
'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.',
|
|
5295
|
+
// Supply chain checks
|
|
5296
|
+
'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
5297
|
};
|
|
5383
5298
|
const explanation = staticExplanations[checkId];
|
|
5384
5299
|
if (explanation) {
|
|
@@ -5754,6 +5669,18 @@ program
|
|
|
5754
5669
|
// ============================================================================
|
|
5755
5670
|
// npm package scanning helpers (used by `check <package>`)
|
|
5756
5671
|
// ============================================================================
|
|
5672
|
+
/**
|
|
5673
|
+
* Detect whether a string looks like a PyPI package reference.
|
|
5674
|
+
*
|
|
5675
|
+
* Requires an explicit prefix:
|
|
5676
|
+
* - pip:package-name
|
|
5677
|
+
* - pypi:package-name
|
|
5678
|
+
*
|
|
5679
|
+
* Bare names are NOT auto-detected as PyPI (they fall through to npm).
|
|
5680
|
+
*/
|
|
5681
|
+
function looksLikePyPiPackage(target) {
|
|
5682
|
+
return target.startsWith('pip:') || target.startsWith('pypi:');
|
|
5683
|
+
}
|
|
5757
5684
|
/**
|
|
5758
5685
|
* Detect whether a string looks like an npm package name rather than
|
|
5759
5686
|
* a hostname, IP address, or local path.
|
|
@@ -5804,6 +5731,16 @@ function looksLikeGitHubRepo(target) {
|
|
|
5804
5731
|
}
|
|
5805
5732
|
return false;
|
|
5806
5733
|
}
|
|
5734
|
+
/**
|
|
5735
|
+
* Detect whether a string is an HTTP(S) URL that is NOT a GitHub repo.
|
|
5736
|
+
* GitHub URLs are handled by looksLikeGitHubRepo; this catches everything else:
|
|
5737
|
+
* GitLab, Bitbucket, self-hosted git, raw tarballs, zip archives, single files, etc.
|
|
5738
|
+
*/
|
|
5739
|
+
function looksLikeRawUrl(target) {
|
|
5740
|
+
if (looksLikeGitHubRepo(target))
|
|
5741
|
+
return false;
|
|
5742
|
+
return /^https?:\/\/.+/.test(target);
|
|
5743
|
+
}
|
|
5807
5744
|
/**
|
|
5808
5745
|
* Parse a GitHub target into org/repo and optional clone URL.
|
|
5809
5746
|
* Returns { org, repo, cloneUrl }
|
|
@@ -6124,21 +6061,7 @@ async function checkGitHubRepo(target, options) {
|
|
|
6124
6061
|
console.log(` Type: ${result.projectType}`);
|
|
6125
6062
|
console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
|
|
6126
6063
|
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
|
-
}
|
|
6064
|
+
displayCheckFindings(failed, !!options.verbose);
|
|
6142
6065
|
// Step 3: Community contribution
|
|
6143
6066
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6144
6067
|
const scanCount = incrementScanCounter();
|
|
@@ -6202,6 +6125,305 @@ async function checkGitHubRepo(target, options) {
|
|
|
6202
6125
|
* Download an npm package, run full HMA secure scan, display results, clean up.
|
|
6203
6126
|
* Checks the registry first; only downloads if data is missing or stale.
|
|
6204
6127
|
*/
|
|
6128
|
+
/**
|
|
6129
|
+
* Download a PyPI package, scan it with HMA + NanoMind, and display results.
|
|
6130
|
+
* Accepts targets prefixed with pip: or pypi: (e.g. pip:requests, pypi:flask).
|
|
6131
|
+
*/
|
|
6132
|
+
async function checkPyPiPackage(target, options) {
|
|
6133
|
+
// Strip prefix to get the bare package name
|
|
6134
|
+
const name = target.replace(/^(pip|pypi):/, '');
|
|
6135
|
+
const { mkdtemp, rm, readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
6136
|
+
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
6137
|
+
const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
6138
|
+
const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
6139
|
+
if (!options.json && !globalCiMode) {
|
|
6140
|
+
console.error(`Downloading ${name} from PyPI...`);
|
|
6141
|
+
}
|
|
6142
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'hma-check-pypi-'));
|
|
6143
|
+
try {
|
|
6144
|
+
// Fetch package metadata from PyPI JSON API
|
|
6145
|
+
const metaRes = await fetch(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`);
|
|
6146
|
+
if (!metaRes.ok) {
|
|
6147
|
+
if (metaRes.status === 404) {
|
|
6148
|
+
console.error(`Error: Package "${name}" not found on PyPI.`);
|
|
6149
|
+
}
|
|
6150
|
+
else {
|
|
6151
|
+
console.error(`Error: PyPI API returned ${metaRes.status} for "${name}".`);
|
|
6152
|
+
}
|
|
6153
|
+
process.exit(1);
|
|
6154
|
+
}
|
|
6155
|
+
const meta = await metaRes.json();
|
|
6156
|
+
// Prefer sdist (source tarball) for scanning; fall back to first wheel
|
|
6157
|
+
const sdist = meta.urls.find((u) => u.packagetype === 'sdist');
|
|
6158
|
+
const wheel = meta.urls.find((u) => u.packagetype === 'bdist_wheel');
|
|
6159
|
+
const dist = sdist || wheel || meta.urls[0];
|
|
6160
|
+
if (!dist) {
|
|
6161
|
+
console.error(`Error: No downloadable distribution found for "${name}" on PyPI.`);
|
|
6162
|
+
process.exit(1);
|
|
6163
|
+
}
|
|
6164
|
+
// Download the archive
|
|
6165
|
+
const archiveRes = await fetch(dist.url);
|
|
6166
|
+
if (!archiveRes.ok) {
|
|
6167
|
+
throw new Error(`Failed to download ${dist.filename}: HTTP ${archiveRes.status}`);
|
|
6168
|
+
}
|
|
6169
|
+
const archiveBuffer = Buffer.from(await archiveRes.arrayBuffer());
|
|
6170
|
+
const archivePath = join(tempDir, dist.filename);
|
|
6171
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
6172
|
+
writeFileSync(archivePath, archiveBuffer);
|
|
6173
|
+
// Extract
|
|
6174
|
+
const extractDir = join(tempDir, 'package');
|
|
6175
|
+
const { mkdirSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
6176
|
+
mkdirSync(extractDir, { recursive: true });
|
|
6177
|
+
if (dist.filename.endsWith('.tar.gz') || dist.filename.endsWith('.tgz')) {
|
|
6178
|
+
execFileSync('tar', ['xzf', archivePath, '-C', extractDir, '--strip-components=1'], { timeout: 30000 });
|
|
6179
|
+
}
|
|
6180
|
+
else if (dist.filename.endsWith('.zip') || dist.filename.endsWith('.whl')) {
|
|
6181
|
+
execFileSync('unzip', ['-q', '-o', archivePath, '-d', extractDir], { timeout: 30000 });
|
|
6182
|
+
}
|
|
6183
|
+
else {
|
|
6184
|
+
throw new Error(`Unsupported archive format: ${dist.filename}`);
|
|
6185
|
+
}
|
|
6186
|
+
// Run full HMA scan + NanoMind (same pipeline as checkNpmPackage)
|
|
6187
|
+
const scanner = new index_1.HardeningScanner();
|
|
6188
|
+
const result = await scanner.scan({ targetDir: extractDir, autoFix: false });
|
|
6189
|
+
// Run NanoMind semantic analysis and re-filter
|
|
6190
|
+
try {
|
|
6191
|
+
const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
|
|
6192
|
+
const nmResult = await orchestrateNanoMind(extractDir, result.findings, { silent: true });
|
|
6193
|
+
const refiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, extractDir);
|
|
6194
|
+
const projectType = result.projectType || 'library';
|
|
6195
|
+
result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
|
|
6196
|
+
result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
|
|
6197
|
+
}
|
|
6198
|
+
catch {
|
|
6199
|
+
// NanoMind unavailable -- use base scan results
|
|
6200
|
+
}
|
|
6201
|
+
const failed = result.findings.filter(f => !f.passed);
|
|
6202
|
+
const critical = failed.filter(f => f.severity === 'critical');
|
|
6203
|
+
const high = failed.filter(f => f.severity === 'high');
|
|
6204
|
+
const medium = failed.filter(f => f.severity === 'medium');
|
|
6205
|
+
const low = failed.filter(f => f.severity === 'low');
|
|
6206
|
+
if (options.json) {
|
|
6207
|
+
writeJsonStdout({
|
|
6208
|
+
name,
|
|
6209
|
+
type: 'pypi-package',
|
|
6210
|
+
source: 'local-scan',
|
|
6211
|
+
version: meta.info.version,
|
|
6212
|
+
projectType: result.projectType,
|
|
6213
|
+
score: result.score,
|
|
6214
|
+
maxScore: result.maxScore,
|
|
6215
|
+
findings: result.findings,
|
|
6216
|
+
});
|
|
6217
|
+
return;
|
|
6218
|
+
}
|
|
6219
|
+
// Display results
|
|
6220
|
+
const scoreRatio = result.score / result.maxScore;
|
|
6221
|
+
const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
|
|
6222
|
+
console.log(`\n ${name} (PyPI)`);
|
|
6223
|
+
console.log(` Version: ${meta.info.version}`);
|
|
6224
|
+
console.log(` Type: ${result.projectType}`);
|
|
6225
|
+
console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
|
|
6226
|
+
console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
|
|
6227
|
+
if (failed.length > 0) {
|
|
6228
|
+
console.log();
|
|
6229
|
+
const limit = options.verbose ? failed.length : 15;
|
|
6230
|
+
for (const f of failed.slice(0, limit)) {
|
|
6231
|
+
const sev = SEVERITY_DISPLAY[f.severity];
|
|
6232
|
+
const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
|
|
6233
|
+
console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
|
|
6234
|
+
}
|
|
6235
|
+
if (failed.length > limit) {
|
|
6236
|
+
console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
else {
|
|
6240
|
+
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
6241
|
+
}
|
|
6242
|
+
console.log(`\n Full project scan: ${CLI_PREFIX} secure <dir>`);
|
|
6243
|
+
console.log();
|
|
6244
|
+
if (critical.length > 0 || high.length > 0)
|
|
6245
|
+
process.exit(1);
|
|
6246
|
+
}
|
|
6247
|
+
catch (err) {
|
|
6248
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6249
|
+
if (message.includes('not found on PyPI')) {
|
|
6250
|
+
console.error(`Error: ${message}`);
|
|
6251
|
+
}
|
|
6252
|
+
else {
|
|
6253
|
+
console.error(`Error scanning PyPI package "${name}": ${message}`);
|
|
6254
|
+
}
|
|
6255
|
+
process.exit(1);
|
|
6256
|
+
}
|
|
6257
|
+
finally {
|
|
6258
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
/**
|
|
6262
|
+
* Fetch a raw URL, detect its type (git repo, tarball, zip, or single file),
|
|
6263
|
+
* download to a temp dir, run full HMA + NanoMind scan, display results, clean up.
|
|
6264
|
+
*/
|
|
6265
|
+
async function checkRawUrl(url, options) {
|
|
6266
|
+
const { mkdtemp, rm, writeFile, readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
6267
|
+
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
6268
|
+
const { join, basename } = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
6269
|
+
const { execFile } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
6270
|
+
const { promisify } = await Promise.resolve().then(() => __importStar(require('node:util')));
|
|
6271
|
+
const execAsync = promisify(execFile);
|
|
6272
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'hma-check-url-'));
|
|
6273
|
+
let scanDir = tempDir;
|
|
6274
|
+
let displayName = url;
|
|
6275
|
+
try {
|
|
6276
|
+
// Git clone for known forge URLs and .git suffix
|
|
6277
|
+
const isGitUrl = url.endsWith('.git')
|
|
6278
|
+
|| /^https?:\/\/(gitlab\.com|bitbucket\.org|codeberg\.org|gitea\.com|sr\.ht)\//.test(url);
|
|
6279
|
+
if (isGitUrl) {
|
|
6280
|
+
const repoName = basename(url.replace(/\.git$/, '')) || 'repo';
|
|
6281
|
+
displayName = url.replace(/^https?:\/\//, '').replace(/\.git$/, '');
|
|
6282
|
+
if (!options.json && !globalCiMode) {
|
|
6283
|
+
console.error(`Cloning ${displayName}...`);
|
|
6284
|
+
}
|
|
6285
|
+
await execAsync('git', ['clone', '--depth', '1', '--single-branch', url, join(tempDir, repoName)], { timeout: 120000 });
|
|
6286
|
+
scanDir = join(tempDir, repoName);
|
|
6287
|
+
}
|
|
6288
|
+
else {
|
|
6289
|
+
// HTTP fetch — use HEAD to determine content type
|
|
6290
|
+
if (!options.json && !globalCiMode) {
|
|
6291
|
+
console.error(`Fetching ${url}...`);
|
|
6292
|
+
}
|
|
6293
|
+
const headRes = await fetch(url, { method: 'HEAD', redirect: 'follow' });
|
|
6294
|
+
if (!headRes.ok) {
|
|
6295
|
+
console.error(`Error: HTTP ${headRes.status} fetching "${url}".`);
|
|
6296
|
+
process.exit(1);
|
|
6297
|
+
}
|
|
6298
|
+
const contentType = headRes.headers.get('content-type') || '';
|
|
6299
|
+
const finalUrl = headRes.url;
|
|
6300
|
+
const fileName = basename(new URL(finalUrl).pathname) || 'download';
|
|
6301
|
+
const isArchive = /\.(tar\.gz|tgz|tar\.bz2|tar\.xz|zip)$/i.test(fileName)
|
|
6302
|
+
|| contentType.includes('gzip')
|
|
6303
|
+
|| contentType.includes('tar')
|
|
6304
|
+
|| contentType.includes('zip')
|
|
6305
|
+
|| contentType.includes('compressed');
|
|
6306
|
+
const bodyRes = await fetch(finalUrl, { redirect: 'follow' });
|
|
6307
|
+
if (!bodyRes.ok || !bodyRes.body) {
|
|
6308
|
+
console.error(`Error: Failed to download "${url}" (HTTP ${bodyRes.status}).`);
|
|
6309
|
+
process.exit(1);
|
|
6310
|
+
}
|
|
6311
|
+
const buffer = Buffer.from(await bodyRes.arrayBuffer());
|
|
6312
|
+
if (isArchive) {
|
|
6313
|
+
const archivePath = join(tempDir, fileName);
|
|
6314
|
+
await writeFile(archivePath, buffer);
|
|
6315
|
+
const extractDir = join(tempDir, 'extracted');
|
|
6316
|
+
await execAsync('mkdir', ['-p', extractDir]);
|
|
6317
|
+
if (/\.(tar\.gz|tgz)$/i.test(fileName) || contentType.includes('gzip') || contentType.includes('tar')) {
|
|
6318
|
+
await execAsync('tar', ['xzf', archivePath, '-C', extractDir], { timeout: 30000 });
|
|
6319
|
+
}
|
|
6320
|
+
else if (/\.tar\.bz2$/i.test(fileName)) {
|
|
6321
|
+
await execAsync('tar', ['xjf', archivePath, '-C', extractDir], { timeout: 30000 });
|
|
6322
|
+
}
|
|
6323
|
+
else if (/\.tar\.xz$/i.test(fileName)) {
|
|
6324
|
+
await execAsync('tar', ['xJf', archivePath, '-C', extractDir], { timeout: 30000 });
|
|
6325
|
+
}
|
|
6326
|
+
else if (/\.zip$/i.test(fileName)) {
|
|
6327
|
+
await execAsync('unzip', ['-q', archivePath, '-d', extractDir], { timeout: 30000 });
|
|
6328
|
+
}
|
|
6329
|
+
// If extraction produced a single directory, scan that
|
|
6330
|
+
const entries = await readdir(extractDir);
|
|
6331
|
+
if (entries.length === 1) {
|
|
6332
|
+
const { statSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
6333
|
+
const innerPath = join(extractDir, entries[0]);
|
|
6334
|
+
if (statSync(innerPath).isDirectory()) {
|
|
6335
|
+
scanDir = innerPath;
|
|
6336
|
+
}
|
|
6337
|
+
else {
|
|
6338
|
+
scanDir = extractDir;
|
|
6339
|
+
}
|
|
6340
|
+
}
|
|
6341
|
+
else {
|
|
6342
|
+
scanDir = extractDir;
|
|
6343
|
+
}
|
|
6344
|
+
displayName = fileName;
|
|
6345
|
+
}
|
|
6346
|
+
else {
|
|
6347
|
+
// Single file: save for scanning
|
|
6348
|
+
await writeFile(join(tempDir, fileName), buffer);
|
|
6349
|
+
scanDir = tempDir;
|
|
6350
|
+
displayName = fileName;
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
// Run full HMA scan + NanoMind
|
|
6354
|
+
const scanner = new index_1.HardeningScanner();
|
|
6355
|
+
const result = await scanner.scan({ targetDir: scanDir, autoFix: false });
|
|
6356
|
+
try {
|
|
6357
|
+
const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
|
|
6358
|
+
const nmResult = await orchestrateNanoMind(scanDir, result.findings, { silent: true });
|
|
6359
|
+
const refiltered = await scanner.reapplyIgnoreFilters(nmResult.mergedFindings, scanDir);
|
|
6360
|
+
const projectType = result.projectType || 'library';
|
|
6361
|
+
result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
|
|
6362
|
+
result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
|
|
6363
|
+
}
|
|
6364
|
+
catch {
|
|
6365
|
+
// NanoMind unavailable — use base scan results
|
|
6366
|
+
}
|
|
6367
|
+
const failed = result.findings.filter(f => !f.passed);
|
|
6368
|
+
const critical = failed.filter(f => f.severity === 'critical');
|
|
6369
|
+
const high = failed.filter(f => f.severity === 'high');
|
|
6370
|
+
const medium = failed.filter(f => f.severity === 'medium');
|
|
6371
|
+
const low = failed.filter(f => f.severity === 'low');
|
|
6372
|
+
if (options.json) {
|
|
6373
|
+
writeJsonStdout({
|
|
6374
|
+
name: displayName,
|
|
6375
|
+
url,
|
|
6376
|
+
type: 'raw-url',
|
|
6377
|
+
source: 'local-scan',
|
|
6378
|
+
projectType: result.projectType,
|
|
6379
|
+
score: result.score,
|
|
6380
|
+
maxScore: result.maxScore,
|
|
6381
|
+
findings: result.findings,
|
|
6382
|
+
});
|
|
6383
|
+
return;
|
|
6384
|
+
}
|
|
6385
|
+
// Display results
|
|
6386
|
+
const scoreRatio = result.score / result.maxScore;
|
|
6387
|
+
const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
|
|
6388
|
+
console.log(`\n ${displayName} ${colors.dim}(URL)${RESET()}`);
|
|
6389
|
+
console.log(` Type: ${result.projectType}`);
|
|
6390
|
+
console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
|
|
6391
|
+
console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
|
|
6392
|
+
displayCheckFindings(failed, !!options.verbose);
|
|
6393
|
+
// Community contribution (auto-share if opted in, no first-time prompt for URLs)
|
|
6394
|
+
if (process.stdin.isTTY && !globalCiMode) {
|
|
6395
|
+
if (isContributeEnabled()) {
|
|
6396
|
+
flushPendingScans();
|
|
6397
|
+
const ok = await publishToRegistry(displayName, result);
|
|
6398
|
+
if (!ok)
|
|
6399
|
+
queuePendingScan(displayName, result);
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
console.log(`\n Full project scan: ${CLI_PREFIX} secure <dir>`);
|
|
6403
|
+
console.log();
|
|
6404
|
+
if (critical.length > 0 || high.length > 0)
|
|
6405
|
+
process.exit(1);
|
|
6406
|
+
}
|
|
6407
|
+
catch (err) {
|
|
6408
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6409
|
+
if (message.includes('128') || message.includes('not found') || message.includes('Repository not found')) {
|
|
6410
|
+
console.error(`Error: Could not clone repository from "${url}".`);
|
|
6411
|
+
console.error(`\nVerify the URL is accessible and contains a git repository.`);
|
|
6412
|
+
}
|
|
6413
|
+
else if (message.includes('timeout') || message.includes('Timeout')) {
|
|
6414
|
+
console.error(`Error: Fetching "${url}" timed out. The target may be too large.`);
|
|
6415
|
+
console.error(`\nTry downloading manually and scanning the local path:`);
|
|
6416
|
+
console.error(` ${CLI_PREFIX} check ./downloaded-dir/`);
|
|
6417
|
+
}
|
|
6418
|
+
else {
|
|
6419
|
+
console.error(`Error scanning URL: ${message}`);
|
|
6420
|
+
}
|
|
6421
|
+
process.exit(1);
|
|
6422
|
+
}
|
|
6423
|
+
finally {
|
|
6424
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6205
6427
|
async function checkNpmPackage(name, options) {
|
|
6206
6428
|
// Step 1: Check registry for existing trust data
|
|
6207
6429
|
if (!options.offline) {
|
|
@@ -6279,21 +6501,7 @@ async function checkNpmPackage(name, options) {
|
|
|
6279
6501
|
console.log(` Type: ${result.projectType}`);
|
|
6280
6502
|
console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
|
|
6281
6503
|
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
|
-
}
|
|
6504
|
+
displayCheckFindings(failed, !!options.verbose);
|
|
6297
6505
|
// Step 3: Community contribution (after 3 scans, interactive only)
|
|
6298
6506
|
if (process.stdin.isTTY && !globalCiMode) {
|
|
6299
6507
|
const scanCount = incrementScanCounter();
|