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.
- package/cli.mjs +194 -8
- 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:
|
|
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
|
|
921
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
-
|
|
1499
|
+
const auditFlag = targets.includes('--audit') || targets.includes('-a');
|
|
1500
|
+
await discoverCommand({ scan: scanFlag, audit: auditFlag });
|
|
1315
1501
|
return;
|
|
1316
1502
|
}
|
|
1317
1503
|
|