agentaudit 3.7.0 → 3.8.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.
Files changed (2) hide show
  1. package/cli.mjs +194 -8
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -93,6 +93,111 @@ function askQuestion(question) {
93
93
  return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer.trim()); }));
94
94
  }
95
95
 
96
+ /**
97
+ * Interactive multi-select in terminal. No dependencies.
98
+ * items: [{ label, sublabel?, value, checked? }]
99
+ * Returns: array of selected values
100
+ */
101
+ function multiSelect(items, { title = 'Select items', hint = 'Space=toggle ↑↓=move a=all n=none Enter=confirm' } = {}) {
102
+ return new Promise((resolve) => {
103
+ if (!process.stdin.isTTY) {
104
+ // Non-interactive: return all items
105
+ resolve(items.map(i => i.value));
106
+ return;
107
+ }
108
+
109
+ const selected = new Set(items.filter(i => i.checked).map((_, idx) => idx));
110
+ let cursor = 0;
111
+
112
+ const render = () => {
113
+ // Move cursor up to overwrite previous render
114
+ process.stdout.write(`\x1b[${items.length + 3}A\x1b[J`);
115
+ draw();
116
+ };
117
+
118
+ const draw = () => {
119
+ console.log(` ${c.bold}${title}${c.reset} ${c.dim}(${selected.size}/${items.length} selected)${c.reset}`);
120
+ console.log(` ${c.dim}${hint}${c.reset}`);
121
+ console.log();
122
+ for (let i = 0; i < items.length; i++) {
123
+ const item = items[i];
124
+ const isCursor = i === cursor;
125
+ const isSelected = selected.has(i);
126
+ const pointer = isCursor ? `${c.cyan}❯${c.reset}` : ' ';
127
+ const checkbox = isSelected ? `${c.green}◉${c.reset}` : `${c.dim}○${c.reset}`;
128
+ const label = isCursor ? `${c.bold}${item.label}${c.reset}` : item.label;
129
+ const sub = item.sublabel ? ` ${c.dim}${item.sublabel}${c.reset}` : '';
130
+ console.log(` ${pointer} ${checkbox} ${label}${sub}`);
131
+ }
132
+ };
133
+
134
+ // Initial draw
135
+ draw();
136
+
137
+ process.stdin.setRawMode(true);
138
+ process.stdin.resume();
139
+ process.stdin.setEncoding('utf8');
140
+
141
+ const onData = (key) => {
142
+ // Ctrl+C
143
+ if (key === '\x03') {
144
+ process.stdin.setRawMode(false);
145
+ process.stdin.pause();
146
+ process.stdin.removeListener('data', onData);
147
+ console.log();
148
+ process.exit(0);
149
+ }
150
+
151
+ // Enter
152
+ if (key === '\r' || key === '\n') {
153
+ process.stdin.setRawMode(false);
154
+ process.stdin.pause();
155
+ process.stdin.removeListener('data', onData);
156
+ resolve(items.filter((_, i) => selected.has(i)).map(i => i.value));
157
+ return;
158
+ }
159
+
160
+ // Space — toggle
161
+ if (key === ' ') {
162
+ if (selected.has(cursor)) selected.delete(cursor);
163
+ else selected.add(cursor);
164
+ render();
165
+ return;
166
+ }
167
+
168
+ // a — select all
169
+ if (key === 'a') {
170
+ for (let i = 0; i < items.length; i++) selected.add(i);
171
+ render();
172
+ return;
173
+ }
174
+
175
+ // n — select none
176
+ if (key === 'n') {
177
+ selected.clear();
178
+ render();
179
+ return;
180
+ }
181
+
182
+ // Arrow up / k
183
+ if (key === '\x1b[A' || key === 'k') {
184
+ cursor = (cursor - 1 + items.length) % items.length;
185
+ render();
186
+ return;
187
+ }
188
+
189
+ // Arrow down / j
190
+ if (key === '\x1b[B' || key === 'j') {
191
+ cursor = (cursor + 1) % items.length;
192
+ render();
193
+ return;
194
+ }
195
+ };
196
+
197
+ process.stdin.on('data', onData);
198
+ });
199
+ }
200
+
96
201
  async function registerAgent(agentName) {
97
202
  const res = await fetch(`${REGISTRY_URL}/api/register`, {
98
203
  method: 'POST',
@@ -638,6 +743,11 @@ function findMcpConfigs() {
638
743
  { name: 'Continue', path: path.join(home, '.continue', 'config.json') },
639
744
  ];
640
745
 
746
+ // Also check AGENTAUDIT_TEST_CONFIG env for testing
747
+ if (process.env.AGENTAUDIT_TEST_CONFIG) {
748
+ candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG });
749
+ }
750
+
641
751
  // Also scan workspace .cursor/mcp.json, .vscode/mcp.json in cwd
642
752
  const cwd = process.cwd();
643
753
  candidates.push(
@@ -731,6 +841,22 @@ function serverSlug(server) {
731
841
  return server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
732
842
  }
733
843
 
844
+ async function searchGitHub(query) {
845
+ try {
846
+ const res = await fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&per_page=1`, {
847
+ signal: AbortSignal.timeout(5000),
848
+ headers: { 'Accept': 'application/vnd.github+json' },
849
+ });
850
+ if (res.ok) {
851
+ const data = await res.json();
852
+ if (data.items?.length > 0) {
853
+ return data.items[0].html_url;
854
+ }
855
+ }
856
+ } catch {}
857
+ return null;
858
+ }
859
+
734
860
  async function resolveSourceUrl(server) {
735
861
  // Already have it
736
862
  if (server.sourceUrl) return server.sourceUrl;
@@ -750,7 +876,9 @@ async function resolveSourceUrl(server) {
750
876
  }
751
877
  }
752
878
  } catch {}
753
- // Fallback: npm page
879
+ // Fallback: try GitHub search for the package name
880
+ const ghUrl = await searchGitHub(server.npmPackage);
881
+ if (ghUrl) return ghUrl;
754
882
  return `https://www.npmjs.com/package/${server.npmPackage}`;
755
883
  }
756
884
 
@@ -767,6 +895,9 @@ async function resolveSourceUrl(server) {
767
895
  if (source && source.startsWith('http')) return source;
768
896
  }
769
897
  } catch {}
898
+ // Fallback: GitHub search
899
+ const ghUrl = await searchGitHub(server.pyPackage);
900
+ if (ghUrl) return ghUrl;
770
901
  return `https://pypi.org/project/${server.pyPackage}/`;
771
902
  }
772
903
 
@@ -808,6 +939,7 @@ async function resolveSourceUrl(server) {
808
939
 
809
940
  async function discoverCommand(options = {}) {
810
941
  const autoScan = options.scan || false;
942
+ const interactiveAudit = options.audit || false;
811
943
 
812
944
  console.log(` ${c.bold}Discovering local MCP servers...${c.reset}`);
813
945
  console.log();
@@ -915,16 +1047,28 @@ async function discoverCommand(options = {}) {
915
1047
  if (unauditedServers > 0) console.log(` ${icons.caution} ${c.yellow}${unauditedServers} not audited${c.reset}`);
916
1048
  console.log();
917
1049
 
918
- // --scan: automatically scan all servers with resolved source URLs
1050
+ // --scan: automatically scan all servers with resolved source URLs (git-cloneable only)
919
1051
  if (autoScan) {
920
- const scanTargets = allServersWithUrls.filter(s => s.sourceUrl && s.sourceUrl.startsWith('http'));
921
- if (scanTargets.length > 0) {
1052
+ const isCloneable = (url) => /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org)\//i.test(url);
1053
+ const scanTargets = allServersWithUrls.filter(s => s.sourceUrl && isCloneable(s.sourceUrl));
1054
+ // Deduplicate by sourceUrl
1055
+ const seen = new Set();
1056
+ const dedupedTargets = scanTargets.filter(s => {
1057
+ if (seen.has(s.sourceUrl)) return false;
1058
+ seen.add(s.sourceUrl);
1059
+ return true;
1060
+ });
1061
+ const skipped = allServersWithUrls.filter(s => s.sourceUrl && !isCloneable(s.sourceUrl));
1062
+ if (dedupedTargets.length > 0) {
922
1063
  console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
923
- console.log(` ${c.bold}${icons.scan} Auto-scanning ${scanTargets.length} server${scanTargets.length !== 1 ? 's' : ''}...${c.reset}`);
1064
+ console.log(` ${c.bold}${icons.scan} Auto-scanning ${dedupedTargets.length} server${dedupedTargets.length !== 1 ? 's' : ''}...${c.reset}`);
1065
+ if (skipped.length > 0) {
1066
+ console.log(` ${c.dim}(${skipped.length} skipped — no cloneable source URL)${c.reset}`);
1067
+ }
924
1068
  console.log();
925
1069
 
926
1070
  const scanResults = [];
927
- for (const target of scanTargets) {
1071
+ for (const target of dedupedTargets) {
928
1072
  const result = await scanRepo(target.sourceUrl);
929
1073
  if (result) scanResults.push({ ...result, serverName: target.name });
930
1074
  }
@@ -963,6 +1107,45 @@ async function discoverCommand(options = {}) {
963
1107
  console.log(` ${c.dim}No scannable source URLs found.${c.reset}`);
964
1108
  console.log();
965
1109
  }
1110
+ } else if (interactiveAudit && allServersWithUrls.length > 0) {
1111
+ // Interactive multi-select for audit
1112
+ const isCloneable = (url) => /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org)\//i.test(url);
1113
+ const auditCandidates = [];
1114
+ const seen = new Set();
1115
+ for (const s of allServersWithUrls) {
1116
+ if (!s.sourceUrl || !isCloneable(s.sourceUrl)) continue;
1117
+ if (seen.has(s.sourceUrl)) continue;
1118
+ seen.add(s.sourceUrl);
1119
+ auditCandidates.push(s);
1120
+ }
1121
+
1122
+ if (auditCandidates.length > 0) {
1123
+ console.log();
1124
+ const items = auditCandidates.map(s => ({
1125
+ label: s.name,
1126
+ sublabel: s.hasAudit ? `${c.green}✔ audited${c.reset} ${s.sourceUrl}` : s.sourceUrl,
1127
+ value: s,
1128
+ checked: !s.hasAudit, // Pre-select unaudited
1129
+ }));
1130
+
1131
+ const selected = await multiSelect(items, {
1132
+ title: 'Select servers to audit',
1133
+ hint: 'Space=toggle ↑↓=move a=all n=none Enter=confirm',
1134
+ });
1135
+
1136
+ if (selected.length > 0) {
1137
+ console.log();
1138
+ console.log(` ${c.bold}Auditing ${selected.length} server${selected.length !== 1 ? 's' : ''}...${c.reset}`);
1139
+ console.log();
1140
+ for (const s of selected) {
1141
+ await auditRepo(s.sourceUrl);
1142
+ console.log();
1143
+ }
1144
+ } else {
1145
+ console.log();
1146
+ console.log(` ${c.dim}No servers selected.${c.reset}`);
1147
+ }
1148
+ }
966
1149
  } else if (unauditedServers > 0) {
967
1150
  if (unauditedWithUrls.length > 0) {
968
1151
  console.log(` ${c.dim}To audit unaudited servers:${c.reset}`);
@@ -974,7 +1157,8 @@ async function discoverCommand(options = {}) {
974
1157
  console.log(` ${c.cyan}agentaudit audit <source-url>${c.reset}`);
975
1158
  }
976
1159
  console.log();
977
- console.log(` ${c.dim}Or run ${c.cyan}agentaudit discover --scan${c.dim} to auto-scan all servers${c.reset}`);
1160
+ console.log(` ${c.dim}Or run ${c.cyan}agentaudit discover --scan${c.dim} to quick-scan all servers${c.reset}`);
1161
+ console.log(` ${c.dim}Or run ${c.cyan}agentaudit discover --audit${c.dim} to select & deep-audit interactively${c.reset}`);
978
1162
  console.log();
979
1163
  }
980
1164
  }
@@ -1267,6 +1451,7 @@ async function main() {
1267
1451
  console.log();
1268
1452
  console.log(` ${c.cyan}agentaudit discover${c.reset} Find local MCP servers + check registry`);
1269
1453
  console.log(` ${c.cyan}agentaudit discover --scan${c.reset} Discover + auto-scan all servers`);
1454
+ console.log(` ${c.cyan}agentaudit discover --audit${c.reset} Discover + select servers to deep-audit`);
1270
1455
  console.log(` ${c.cyan}agentaudit scan${c.reset} <url> [url...] Quick static scan (regex, local)`);
1271
1456
  console.log(` ${c.cyan}agentaudit audit${c.reset} <url> [url...] Deep LLM-powered security audit`);
1272
1457
  console.log(` ${c.cyan}agentaudit check${c.reset} <name> Look up package in registry`);
@@ -1311,7 +1496,8 @@ async function main() {
1311
1496
 
1312
1497
  if (command === 'discover') {
1313
1498
  const scanFlag = targets.includes('--scan') || targets.includes('-s');
1314
- await discoverCommand({ scan: scanFlag });
1499
+ const auditFlag = targets.includes('--audit') || targets.includes('-a');
1500
+ await discoverCommand({ scan: scanFlag, audit: auditFlag });
1315
1501
  return;
1316
1502
  }
1317
1503
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {