hackmyagent 0.14.2 → 0.15.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 +261 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -167,21 +167,23 @@ const RISK_DISPLAY = {
|
|
|
167
167
|
const RESET = () => colors.reset;
|
|
168
168
|
program
|
|
169
169
|
.command('check')
|
|
170
|
-
.description(`
|
|
170
|
+
.description(`Check if a package or skill is safe
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
•
|
|
174
|
-
•
|
|
175
|
-
•
|
|
172
|
+
Accepts npm packages, local paths, or skill identifiers:
|
|
173
|
+
• npm package: downloads and runs full security analysis (204 checks + NanoMind)
|
|
174
|
+
• Local path: runs NanoMind semantic analysis
|
|
175
|
+
• Skill identifier: verifies publisher, permissions, revocation
|
|
176
176
|
|
|
177
177
|
Risk levels: low, medium, high, critical
|
|
178
178
|
Exit code 1 if high/critical risk detected.
|
|
179
179
|
|
|
180
180
|
Examples:
|
|
181
|
-
$ hackmyagent check
|
|
182
|
-
$ hackmyagent check @
|
|
183
|
-
$ hackmyagent check @
|
|
184
|
-
|
|
181
|
+
$ hackmyagent check express
|
|
182
|
+
$ hackmyagent check @modelcontextprotocol/server-filesystem
|
|
183
|
+
$ hackmyagent check @modelcontextprotocol/server-filesystem --json
|
|
184
|
+
$ hackmyagent check ./my-agent/
|
|
185
|
+
$ hackmyagent check @publisher/skill --verbose`)
|
|
186
|
+
.argument('<target>', 'npm package name, local path, or skill identifier')
|
|
185
187
|
.option('-v, --verbose', 'Show detailed verification info')
|
|
186
188
|
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
187
189
|
.option('--offline', 'Skip DNS verification (offline mode)')
|
|
@@ -239,6 +241,11 @@ Examples:
|
|
|
239
241
|
process.exit(1);
|
|
240
242
|
return;
|
|
241
243
|
}
|
|
244
|
+
// npm package name: download, run full HMA scan, clean up
|
|
245
|
+
if (looksLikeNpmPackage(skill)) {
|
|
246
|
+
await checkNpmPackage(skill, options);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
242
249
|
// Registry lookup path (non-local identifier) with 10s timeout
|
|
243
250
|
const checkPromise = (0, index_1.checkSkill)(skill, {
|
|
244
251
|
skipDnsVerification: options.offline,
|
|
@@ -5736,6 +5743,251 @@ program
|
|
|
5736
5743
|
}
|
|
5737
5744
|
console.log(`\nYour skill is ready. Verify security with: hackmyagent secure ${outputDir}/`);
|
|
5738
5745
|
});
|
|
5746
|
+
// ============================================================================
|
|
5747
|
+
// npm package scanning helpers (used by `check <package>`)
|
|
5748
|
+
// ============================================================================
|
|
5749
|
+
/**
|
|
5750
|
+
* Detect whether a string looks like an npm package name rather than
|
|
5751
|
+
* a hostname, IP address, or local path.
|
|
5752
|
+
*
|
|
5753
|
+
* npm package names: express, @scope/name, lodash, my-pkg
|
|
5754
|
+
* NOT packages: example.com, 192.168.1.1, ./dir, /path, .
|
|
5755
|
+
*/
|
|
5756
|
+
function looksLikeNpmPackage(target) {
|
|
5757
|
+
// Local paths
|
|
5758
|
+
if (target.startsWith('.') || target.startsWith('/'))
|
|
5759
|
+
return false;
|
|
5760
|
+
// Scoped packages are always npm
|
|
5761
|
+
if (target.startsWith('@') && target.includes('/'))
|
|
5762
|
+
return true;
|
|
5763
|
+
// IPs
|
|
5764
|
+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(target))
|
|
5765
|
+
return false;
|
|
5766
|
+
// Hostnames have dots (example.com, sub.domain.org)
|
|
5767
|
+
if (target.includes('.'))
|
|
5768
|
+
return false;
|
|
5769
|
+
// What's left: bare names like express, lodash, hackmyagent
|
|
5770
|
+
// npm names are lowercase, may contain hyphens and digits
|
|
5771
|
+
return /^[a-z0-9][a-z0-9._-]*$/.test(target);
|
|
5772
|
+
}
|
|
5773
|
+
const REGISTRY_URL = 'https://api.oa2a.org';
|
|
5774
|
+
const STALE_SCAN_DAYS = 3;
|
|
5775
|
+
/**
|
|
5776
|
+
* Query the OpenA2A Registry for existing trust data.
|
|
5777
|
+
* Returns null on any error (network, 404, timeout).
|
|
5778
|
+
*/
|
|
5779
|
+
async function queryRegistry(name) {
|
|
5780
|
+
try {
|
|
5781
|
+
const params = new URLSearchParams({ name, includeProfile: 'true' });
|
|
5782
|
+
const response = await fetch(`${REGISTRY_URL}/api/v1/trust/query?${params}`, {
|
|
5783
|
+
method: 'GET',
|
|
5784
|
+
headers: { 'Accept': 'application/json', 'User-Agent': `hackmyagent/${index_1.VERSION}` },
|
|
5785
|
+
signal: AbortSignal.timeout(5000),
|
|
5786
|
+
});
|
|
5787
|
+
if (!response.ok)
|
|
5788
|
+
return null;
|
|
5789
|
+
const data = await response.json();
|
|
5790
|
+
if (!data.packageId)
|
|
5791
|
+
return null;
|
|
5792
|
+
return {
|
|
5793
|
+
found: true,
|
|
5794
|
+
name: data.name ?? name,
|
|
5795
|
+
trustScore: data.trustScore ?? 0,
|
|
5796
|
+
trustLevel: data.trustLevel ?? 0,
|
|
5797
|
+
verdict: data.verdict ?? 'unknown',
|
|
5798
|
+
scanStatus: data.scanStatus,
|
|
5799
|
+
lastScannedAt: data.lastScannedAt,
|
|
5800
|
+
packageType: data.packageType,
|
|
5801
|
+
};
|
|
5802
|
+
}
|
|
5803
|
+
catch {
|
|
5804
|
+
return null;
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
/**
|
|
5808
|
+
* Check if scan data is stale (older than STALE_SCAN_DAYS).
|
|
5809
|
+
*/
|
|
5810
|
+
function isScanStale(lastScannedAt) {
|
|
5811
|
+
if (!lastScannedAt)
|
|
5812
|
+
return true;
|
|
5813
|
+
const scanned = new Date(lastScannedAt);
|
|
5814
|
+
const now = new Date();
|
|
5815
|
+
const days = (now.getTime() - scanned.getTime()) / (1000 * 60 * 60 * 24);
|
|
5816
|
+
return days > STALE_SCAN_DAYS;
|
|
5817
|
+
}
|
|
5818
|
+
/**
|
|
5819
|
+
* Publish scan results to the community registry.
|
|
5820
|
+
*/
|
|
5821
|
+
async function publishToRegistry(name, result) {
|
|
5822
|
+
try {
|
|
5823
|
+
const response = await fetch(`${REGISTRY_URL}/api/v1/trust/scans`, {
|
|
5824
|
+
method: 'POST',
|
|
5825
|
+
headers: {
|
|
5826
|
+
'Content-Type': 'application/json',
|
|
5827
|
+
'User-Agent': `hackmyagent/${index_1.VERSION}`,
|
|
5828
|
+
},
|
|
5829
|
+
body: JSON.stringify({
|
|
5830
|
+
name,
|
|
5831
|
+
score: result.score,
|
|
5832
|
+
maxScore: result.maxScore,
|
|
5833
|
+
projectType: result.projectType,
|
|
5834
|
+
findings: result.findings.map(f => ({
|
|
5835
|
+
checkId: f.checkId,
|
|
5836
|
+
name: f.name,
|
|
5837
|
+
severity: f.severity,
|
|
5838
|
+
passed: f.passed,
|
|
5839
|
+
message: f.message,
|
|
5840
|
+
category: f.category,
|
|
5841
|
+
})),
|
|
5842
|
+
scanTimestamp: new Date().toISOString(),
|
|
5843
|
+
}),
|
|
5844
|
+
signal: AbortSignal.timeout(10000),
|
|
5845
|
+
});
|
|
5846
|
+
return response.ok;
|
|
5847
|
+
}
|
|
5848
|
+
catch {
|
|
5849
|
+
return false;
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
/**
|
|
5853
|
+
* Display registry trust data in the terminal.
|
|
5854
|
+
*/
|
|
5855
|
+
function displayRegistryResult(data) {
|
|
5856
|
+
const scoreRatio = data.trustScore;
|
|
5857
|
+
const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
|
|
5858
|
+
const score = Math.round(scoreRatio * 100);
|
|
5859
|
+
console.log(`\n ${data.name}`);
|
|
5860
|
+
console.log(` Type: ${data.packageType ?? 'unknown'}`);
|
|
5861
|
+
console.log(` Score: ${scoreColor}${score}/100${RESET()} (registry)`);
|
|
5862
|
+
console.log(` Verdict: ${data.verdict}`);
|
|
5863
|
+
if (data.lastScannedAt) {
|
|
5864
|
+
const days = Math.floor((Date.now() - new Date(data.lastScannedAt).getTime()) / (1000 * 60 * 60 * 24));
|
|
5865
|
+
console.log(` Scanned: ${days === 0 ? 'today' : days + ' day(s) ago'}`);
|
|
5866
|
+
}
|
|
5867
|
+
console.log();
|
|
5868
|
+
}
|
|
5869
|
+
/**
|
|
5870
|
+
* Download an npm package, run full HMA secure scan, display results, clean up.
|
|
5871
|
+
* Checks the registry first; only downloads if data is missing or stale.
|
|
5872
|
+
*/
|
|
5873
|
+
async function checkNpmPackage(name, options) {
|
|
5874
|
+
// Step 1: Check registry for existing trust data
|
|
5875
|
+
if (!options.offline) {
|
|
5876
|
+
const registryData = await queryRegistry(name);
|
|
5877
|
+
if (registryData?.found && !isScanStale(registryData.lastScannedAt)) {
|
|
5878
|
+
// Fresh data in registry — show it
|
|
5879
|
+
if (options.json) {
|
|
5880
|
+
writeJsonStdout({ ...registryData, source: 'registry' });
|
|
5881
|
+
return;
|
|
5882
|
+
}
|
|
5883
|
+
displayRegistryResult(registryData);
|
|
5884
|
+
return;
|
|
5885
|
+
}
|
|
5886
|
+
// Stale or missing — tell the user we're scanning
|
|
5887
|
+
if (registryData?.found && registryData.lastScannedAt) {
|
|
5888
|
+
if (!options.json && !globalCiMode) {
|
|
5889
|
+
const days = Math.floor((Date.now() - new Date(registryData.lastScannedAt).getTime()) / (1000 * 60 * 60 * 24));
|
|
5890
|
+
console.error(`\nRegistry data is ${days} day(s) old. Re-scanning...`);
|
|
5891
|
+
}
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5894
|
+
// Step 2: Download and scan
|
|
5895
|
+
const { mkdtemp, rm } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
5896
|
+
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
5897
|
+
const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
5898
|
+
const { execFile } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
5899
|
+
const { promisify } = await Promise.resolve().then(() => __importStar(require('node:util')));
|
|
5900
|
+
const execAsync = promisify(execFile);
|
|
5901
|
+
if (!options.json && !globalCiMode) {
|
|
5902
|
+
console.error(`Downloading ${name} from npm...`);
|
|
5903
|
+
}
|
|
5904
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'hma-check-'));
|
|
5905
|
+
try {
|
|
5906
|
+
// Download and extract
|
|
5907
|
+
const { stdout } = await execAsync('npm', ['pack', name, '--pack-destination', tempDir], { timeout: 60000 });
|
|
5908
|
+
const tarball = stdout.trim().split('\n').pop();
|
|
5909
|
+
await execAsync('tar', ['xzf', join(tempDir, tarball), '-C', tempDir], { timeout: 30000 });
|
|
5910
|
+
const packageDir = join(tempDir, 'package');
|
|
5911
|
+
// Run full HMA scan
|
|
5912
|
+
const scanner = new index_1.HardeningScanner();
|
|
5913
|
+
const result = await scanner.scan({ targetDir: packageDir, autoFix: false });
|
|
5914
|
+
const failed = result.findings.filter(f => !f.passed);
|
|
5915
|
+
const critical = failed.filter(f => f.severity === 'critical');
|
|
5916
|
+
const high = failed.filter(f => f.severity === 'high');
|
|
5917
|
+
const medium = failed.filter(f => f.severity === 'medium');
|
|
5918
|
+
const low = failed.filter(f => f.severity === 'low');
|
|
5919
|
+
if (options.json) {
|
|
5920
|
+
writeJsonStdout({
|
|
5921
|
+
name,
|
|
5922
|
+
type: 'npm-package',
|
|
5923
|
+
source: 'local-scan',
|
|
5924
|
+
projectType: result.projectType,
|
|
5925
|
+
score: result.score,
|
|
5926
|
+
maxScore: result.maxScore,
|
|
5927
|
+
findings: result.findings,
|
|
5928
|
+
});
|
|
5929
|
+
return;
|
|
5930
|
+
}
|
|
5931
|
+
// Display results
|
|
5932
|
+
const scoreRatio = result.score / result.maxScore;
|
|
5933
|
+
const scoreColor = scoreRatio >= 0.7 ? colors.green : scoreRatio >= 0.4 ? colors.yellow : colors.red;
|
|
5934
|
+
console.log(`\n ${name}`);
|
|
5935
|
+
console.log(` Type: ${result.projectType}`);
|
|
5936
|
+
console.log(` Score: ${scoreColor}${result.score}/${result.maxScore}${RESET()}`);
|
|
5937
|
+
console.log(` Findings: ${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low`);
|
|
5938
|
+
if (failed.length > 0) {
|
|
5939
|
+
console.log();
|
|
5940
|
+
const limit = options.verbose ? failed.length : 15;
|
|
5941
|
+
for (const f of failed.slice(0, limit)) {
|
|
5942
|
+
const sev = SEVERITY_DISPLAY[f.severity];
|
|
5943
|
+
const attackClass = f.attackClass ? ` (${f.attackClass})` : '';
|
|
5944
|
+
console.log(` ${sev.color()}${sev.symbol}${RESET()} ${f.name}: ${f.message}${colors.dim}${attackClass}${RESET()}`);
|
|
5945
|
+
}
|
|
5946
|
+
if (failed.length > limit) {
|
|
5947
|
+
console.log(`\n ... and ${failed.length - limit} more (use --verbose to see all)`);
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
else {
|
|
5951
|
+
console.log(`\n ${colors.green}No security issues found.${RESET()}`);
|
|
5952
|
+
}
|
|
5953
|
+
// Step 3: Offer to share with community (interactive only)
|
|
5954
|
+
if (process.stdin.isTTY && !globalCiMode) {
|
|
5955
|
+
const readline = await Promise.resolve().then(() => __importStar(require('node:readline')));
|
|
5956
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
5957
|
+
const answer = await new Promise(resolve => {
|
|
5958
|
+
rl.question(`\n Share this scan with the community? [Y/n] `, resolve);
|
|
5959
|
+
});
|
|
5960
|
+
rl.close();
|
|
5961
|
+
if (answer.trim().toLowerCase() !== 'n') {
|
|
5962
|
+
const ok = await publishToRegistry(name, result);
|
|
5963
|
+
if (ok) {
|
|
5964
|
+
console.error(` ${colors.green}Shared. Thank you for building trust in AI.${RESET()}`);
|
|
5965
|
+
}
|
|
5966
|
+
else {
|
|
5967
|
+
console.error(` ${colors.dim}Could not reach registry. Scan saved locally.${RESET()}`);
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
console.log(`\n Full project scan: ${CLI_PREFIX} secure <dir>`);
|
|
5972
|
+
console.log();
|
|
5973
|
+
if (critical.length > 0 || high.length > 0)
|
|
5974
|
+
process.exit(1);
|
|
5975
|
+
}
|
|
5976
|
+
catch (err) {
|
|
5977
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5978
|
+
// Clean npm error messages
|
|
5979
|
+
if (message.includes('404') || message.includes('Not Found')) {
|
|
5980
|
+
console.error(`Error: Package "${name}" not found on npm.`);
|
|
5981
|
+
}
|
|
5982
|
+
else {
|
|
5983
|
+
console.error(`Error: ${message}`);
|
|
5984
|
+
}
|
|
5985
|
+
process.exit(1);
|
|
5986
|
+
}
|
|
5987
|
+
finally {
|
|
5988
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5739
5991
|
// Self-securing: verify own integrity before running any command
|
|
5740
5992
|
// A security tool that doesn't verify itself is worse than no security tool
|
|
5741
5993
|
(async () => {
|