agentaudit 3.6.0 → 3.7.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.
Files changed (2) hide show
  1. package/cli.mjs +103 -6
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -638,6 +638,11 @@ function findMcpConfigs() {
638
638
  { name: 'Continue', path: path.join(home, '.continue', 'config.json') },
639
639
  ];
640
640
 
641
+ // Also check AGENTAUDIT_TEST_CONFIG env for testing
642
+ if (process.env.AGENTAUDIT_TEST_CONFIG) {
643
+ candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG });
644
+ }
645
+
641
646
  // Also scan workspace .cursor/mcp.json, .vscode/mcp.json in cwd
642
647
  const cwd = process.cwd();
643
648
  candidates.push(
@@ -731,6 +736,22 @@ function serverSlug(server) {
731
736
  return server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
732
737
  }
733
738
 
739
+ async function searchGitHub(query) {
740
+ try {
741
+ const res = await fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&per_page=1`, {
742
+ signal: AbortSignal.timeout(5000),
743
+ headers: { 'Accept': 'application/vnd.github+json' },
744
+ });
745
+ if (res.ok) {
746
+ const data = await res.json();
747
+ if (data.items?.length > 0) {
748
+ return data.items[0].html_url;
749
+ }
750
+ }
751
+ } catch {}
752
+ return null;
753
+ }
754
+
734
755
  async function resolveSourceUrl(server) {
735
756
  // Already have it
736
757
  if (server.sourceUrl) return server.sourceUrl;
@@ -750,7 +771,9 @@ async function resolveSourceUrl(server) {
750
771
  }
751
772
  }
752
773
  } catch {}
753
- // Fallback: npm page
774
+ // Fallback: try GitHub search for the package name
775
+ const ghUrl = await searchGitHub(server.npmPackage);
776
+ if (ghUrl) return ghUrl;
754
777
  return `https://www.npmjs.com/package/${server.npmPackage}`;
755
778
  }
756
779
 
@@ -767,6 +790,9 @@ async function resolveSourceUrl(server) {
767
790
  if (source && source.startsWith('http')) return source;
768
791
  }
769
792
  } catch {}
793
+ // Fallback: GitHub search
794
+ const ghUrl = await searchGitHub(server.pyPackage);
795
+ if (ghUrl) return ghUrl;
770
796
  return `https://pypi.org/project/${server.pyPackage}/`;
771
797
  }
772
798
 
@@ -806,7 +832,9 @@ async function resolveSourceUrl(server) {
806
832
  return null;
807
833
  }
808
834
 
809
- async function discoverCommand() {
835
+ async function discoverCommand(options = {}) {
836
+ const autoScan = options.scan || false;
837
+
810
838
  console.log(` ${c.bold}Discovering local MCP servers...${c.reset}`);
811
839
  console.log();
812
840
 
@@ -830,6 +858,7 @@ async function discoverCommand() {
830
858
  let auditedServers = 0;
831
859
  let unauditedServers = 0;
832
860
  const unauditedWithUrls = [];
861
+ const allServersWithUrls = []; // For --scan: all servers we can scan
833
862
 
834
863
  for (const config of configs) {
835
864
  const servers = extractServersFromConfig(config.content);
@@ -874,20 +903,23 @@ async function discoverCommand() {
874
903
  else if (server.url) sourceLabel = `${c.dim}${server.url.length > 60 ? server.url.slice(0, 57) + '...' : server.url}${c.reset}`;
875
904
  else if (server.command) sourceLabel = `${c.dim}${[server.command, ...server.args.slice(0, 2)].join(' ')}${c.reset}`;
876
905
 
906
+ // Always resolve source URL (needed for --scan)
907
+ const resolvedUrl = await resolveSourceUrl(server);
908
+
877
909
  if (regData) {
878
910
  auditedServers++;
879
911
  const riskScore = regData.risk_score ?? regData.latest_risk_score ?? 0;
880
912
  const hasOfficial = regData.has_official_audit;
881
913
  console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
882
914
  console.log(`${pipe} ${riskBadge(riskScore)} Risk ${riskScore} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
915
+ if (resolvedUrl) allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: true, regData });
883
916
  } else {
884
917
  unauditedServers++;
885
- // Resolve source URL
886
- const resolvedUrl = await resolveSourceUrl(server);
887
918
  console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
888
919
  if (resolvedUrl) {
889
920
  console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: ${c.cyan}agentaudit audit ${resolvedUrl}${c.reset}`);
890
921
  unauditedWithUrls.push({ name: server.name, sourceUrl: resolvedUrl });
922
+ allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: false });
891
923
  } else {
892
924
  console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Source URL unknown — check the package's GitHub/npm page${c.reset}`);
893
925
  }
@@ -909,7 +941,67 @@ async function discoverCommand() {
909
941
  if (unauditedServers > 0) console.log(` ${icons.caution} ${c.yellow}${unauditedServers} not audited${c.reset}`);
910
942
  console.log();
911
943
 
912
- if (unauditedServers > 0) {
944
+ // --scan: automatically scan all servers with resolved source URLs (git-cloneable only)
945
+ if (autoScan) {
946
+ const isCloneable = (url) => /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org)\//i.test(url);
947
+ const scanTargets = allServersWithUrls.filter(s => s.sourceUrl && isCloneable(s.sourceUrl));
948
+ // Deduplicate by sourceUrl
949
+ const seen = new Set();
950
+ const dedupedTargets = scanTargets.filter(s => {
951
+ if (seen.has(s.sourceUrl)) return false;
952
+ seen.add(s.sourceUrl);
953
+ return true;
954
+ });
955
+ const skipped = allServersWithUrls.filter(s => s.sourceUrl && !isCloneable(s.sourceUrl));
956
+ if (dedupedTargets.length > 0) {
957
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
958
+ console.log(` ${c.bold}${icons.scan} Auto-scanning ${dedupedTargets.length} server${dedupedTargets.length !== 1 ? 's' : ''}...${c.reset}`);
959
+ if (skipped.length > 0) {
960
+ console.log(` ${c.dim}(${skipped.length} skipped — no cloneable source URL)${c.reset}`);
961
+ }
962
+ console.log();
963
+
964
+ const scanResults = [];
965
+ for (const target of dedupedTargets) {
966
+ const result = await scanRepo(target.sourceUrl);
967
+ if (result) scanResults.push({ ...result, serverName: target.name });
968
+ }
969
+
970
+ if (scanResults.length > 1) {
971
+ // Print combined scan summary
972
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
973
+ console.log(` ${c.bold}Scan Summary${c.reset} ${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} scanned`);
974
+ console.log();
975
+
976
+ let totalFindings = 0;
977
+ let serversWithFindings = 0;
978
+
979
+ for (const r of scanResults) {
980
+ const findingCount = r.findings ? r.findings.length : 0;
981
+ totalFindings += findingCount;
982
+ if (findingCount > 0) serversWithFindings++;
983
+
984
+ const status = findingCount === 0
985
+ ? `${icons.safe} ${c.green}clean${c.reset}`
986
+ : `${icons.caution} ${c.yellow}${findingCount} finding${findingCount !== 1 ? 's' : ''}${c.reset}`;
987
+ console.log(` ${status} ${c.bold}${r.serverName || r.slug}${c.reset} ${c.dim}(${r.duration})${c.reset}`);
988
+ }
989
+
990
+ console.log();
991
+ if (serversWithFindings > 0) {
992
+ console.log(` ${c.yellow}${serversWithFindings}/${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} with findings (${totalFindings} total)${c.reset}`);
993
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit audit <url>${c.dim} for deep LLM analysis on flagged servers${c.reset}`);
994
+ } else {
995
+ console.log(` ${c.green}All servers passed quick scan${c.reset}`);
996
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit audit <url>${c.dim} for thorough LLM-powered analysis${c.reset}`);
997
+ }
998
+ console.log();
999
+ }
1000
+ } else {
1001
+ console.log(` ${c.dim}No scannable source URLs found.${c.reset}`);
1002
+ console.log();
1003
+ }
1004
+ } else if (unauditedServers > 0) {
913
1005
  if (unauditedWithUrls.length > 0) {
914
1006
  console.log(` ${c.dim}To audit unaudited servers:${c.reset}`);
915
1007
  for (const { name, sourceUrl } of unauditedWithUrls) {
@@ -920,6 +1012,8 @@ async function discoverCommand() {
920
1012
  console.log(` ${c.cyan}agentaudit audit <source-url>${c.reset}`);
921
1013
  }
922
1014
  console.log();
1015
+ console.log(` ${c.dim}Or run ${c.cyan}agentaudit discover --scan${c.dim} to auto-scan all servers${c.reset}`);
1016
+ console.log();
923
1017
  }
924
1018
  }
925
1019
 
@@ -1210,6 +1304,7 @@ async function main() {
1210
1304
  console.log(` ${c.bold}Commands:${c.reset}`);
1211
1305
  console.log();
1212
1306
  console.log(` ${c.cyan}agentaudit discover${c.reset} Find local MCP servers + check registry`);
1307
+ console.log(` ${c.cyan}agentaudit discover --scan${c.reset} Discover + auto-scan all servers`);
1213
1308
  console.log(` ${c.cyan}agentaudit scan${c.reset} <url> [url...] Quick static scan (regex, local)`);
1214
1309
  console.log(` ${c.cyan}agentaudit audit${c.reset} <url> [url...] Deep LLM-powered security audit`);
1215
1310
  console.log(` ${c.cyan}agentaudit check${c.reset} <name> Look up package in registry`);
@@ -1221,6 +1316,7 @@ async function main() {
1221
1316
  console.log();
1222
1317
  console.log(` ${c.bold}Examples:${c.reset}`);
1223
1318
  console.log(` agentaudit discover`);
1319
+ console.log(` agentaudit discover --scan`);
1224
1320
  console.log(` agentaudit scan https://github.com/owner/repo`);
1225
1321
  console.log(` agentaudit audit https://github.com/owner/repo`);
1226
1322
  console.log(` agentaudit check fastmcp`);
@@ -1252,7 +1348,8 @@ async function main() {
1252
1348
  }
1253
1349
 
1254
1350
  if (command === 'discover') {
1255
- await discoverCommand();
1351
+ const scanFlag = targets.includes('--scan') || targets.includes('-s');
1352
+ await discoverCommand({ scan: scanFlag });
1256
1353
  return;
1257
1354
  }
1258
1355
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.6.0",
3
+ "version": "3.7.1",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {