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/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(`Verify a skill before installing
170
+ .description(`Check if a package or skill is safe
171
171
 
172
- Analyzes skill safety by checking:
173
- Publisher identity via DNS TXT records
174
- Permissions requested (filesystem, network, shell)
175
- Revocation status against global blocklist
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 server-filesystem
182
- $ hackmyagent check @publisher/skill --verbose
183
- $ hackmyagent check @publisher/skill --json`)
184
- .argument('<skill>', 'Skill identifier or local path (e.g., @publisher/skill, ./SKILL.md, ./agent-dir/)')
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 () => {