agentaudit 3.0.0 → 3.1.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 +212 -0
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -602,6 +602,210 @@ async function scanRepo(url) {
|
|
|
602
602
|
return { slug, url, info, files: files.length, findings, registryData, duration };
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
// ── Discover local MCP configs ──────────────────────────
|
|
606
|
+
|
|
607
|
+
function findMcpConfigs() {
|
|
608
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
609
|
+
const platform = process.platform;
|
|
610
|
+
|
|
611
|
+
// All known MCP config locations
|
|
612
|
+
const candidates = [
|
|
613
|
+
// Claude Desktop
|
|
614
|
+
{ name: 'Claude Desktop', path: path.join(home, '.claude', 'mcp.json') },
|
|
615
|
+
{ name: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
|
|
616
|
+
{ name: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json') },
|
|
617
|
+
{ name: 'Claude Desktop', path: path.join(home, '.config', 'claude', 'claude_desktop_config.json') },
|
|
618
|
+
// Cursor
|
|
619
|
+
{ name: 'Cursor', path: path.join(home, '.cursor', 'mcp.json') },
|
|
620
|
+
// Windsurf / Codeium
|
|
621
|
+
{ name: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json') },
|
|
622
|
+
// VS Code
|
|
623
|
+
{ name: 'VS Code', path: path.join(home, '.vscode', 'mcp.json') },
|
|
624
|
+
// Continue.dev
|
|
625
|
+
{ name: 'Continue', path: path.join(home, '.continue', 'config.json') },
|
|
626
|
+
];
|
|
627
|
+
|
|
628
|
+
// Also scan workspace .cursor/mcp.json, .vscode/mcp.json in cwd
|
|
629
|
+
const cwd = process.cwd();
|
|
630
|
+
candidates.push(
|
|
631
|
+
{ name: 'Cursor (project)', path: path.join(cwd, '.cursor', 'mcp.json') },
|
|
632
|
+
{ name: 'VS Code (project)', path: path.join(cwd, '.vscode', 'mcp.json') },
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const found = [];
|
|
636
|
+
for (const c of candidates) {
|
|
637
|
+
if (fs.existsSync(c.path)) {
|
|
638
|
+
try {
|
|
639
|
+
const content = JSON.parse(fs.readFileSync(c.path, 'utf8'));
|
|
640
|
+
found.push({ ...c, content });
|
|
641
|
+
} catch {}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return found;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function extractServersFromConfig(config) {
|
|
648
|
+
// Handle both { mcpServers: {...} } and { servers: {...} } formats
|
|
649
|
+
const servers = config.mcpServers || config.servers || {};
|
|
650
|
+
const result = [];
|
|
651
|
+
|
|
652
|
+
for (const [name, serverConfig] of Object.entries(servers)) {
|
|
653
|
+
const info = {
|
|
654
|
+
name,
|
|
655
|
+
command: serverConfig.command || null,
|
|
656
|
+
args: serverConfig.args || [],
|
|
657
|
+
url: serverConfig.url || null,
|
|
658
|
+
sourceUrl: null,
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// Try to extract source URL from args (common patterns)
|
|
662
|
+
const allArgs = [info.command, ...info.args].filter(Boolean).join(' ');
|
|
663
|
+
|
|
664
|
+
// npx package-name → npm package
|
|
665
|
+
const npxMatch = allArgs.match(/npx\s+(?:-y\s+)?(@?[a-z0-9][\w./-]*)/i);
|
|
666
|
+
if (npxMatch) info.npmPackage = npxMatch[1];
|
|
667
|
+
|
|
668
|
+
// node /path/to/something → try to find package.json
|
|
669
|
+
const nodePathMatch = allArgs.match(/node\s+["']?([^"'\s]+)/);
|
|
670
|
+
if (nodePathMatch) {
|
|
671
|
+
const scriptPath = nodePathMatch[1];
|
|
672
|
+
// Walk up to find package.json with repository
|
|
673
|
+
let dir = path.dirname(path.resolve(scriptPath));
|
|
674
|
+
for (let i = 0; i < 5; i++) {
|
|
675
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
676
|
+
if (fs.existsSync(pkgPath)) {
|
|
677
|
+
try {
|
|
678
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
679
|
+
if (pkg.repository?.url) {
|
|
680
|
+
info.sourceUrl = pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
681
|
+
}
|
|
682
|
+
if (pkg.name) info.npmPackage = pkg.name;
|
|
683
|
+
} catch {}
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
const parent = path.dirname(dir);
|
|
687
|
+
if (parent === dir) break;
|
|
688
|
+
dir = parent;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// python/uvx with package name
|
|
693
|
+
const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
|
|
694
|
+
if (pyMatch) info.pyPackage = pyMatch[1];
|
|
695
|
+
|
|
696
|
+
result.push(info);
|
|
697
|
+
}
|
|
698
|
+
return result;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function serverSlug(server) {
|
|
702
|
+
// Try to derive a slug for registry lookup
|
|
703
|
+
if (server.npmPackage) return server.npmPackage.replace(/^@/, '').replace(/\//g, '-');
|
|
704
|
+
if (server.pyPackage) return server.pyPackage.replace(/[^a-z0-9-]/gi, '-');
|
|
705
|
+
return server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function discoverCommand() {
|
|
709
|
+
console.log(` ${c.bold}Discovering local MCP servers...${c.reset}`);
|
|
710
|
+
console.log();
|
|
711
|
+
|
|
712
|
+
const configs = findMcpConfigs();
|
|
713
|
+
|
|
714
|
+
if (configs.length === 0) {
|
|
715
|
+
console.log(` ${c.yellow}No MCP configurations found.${c.reset}`);
|
|
716
|
+
console.log(` ${c.dim}Searched: Claude Desktop, Cursor, Windsurf, VS Code${c.reset}`);
|
|
717
|
+
console.log();
|
|
718
|
+
console.log(` ${c.dim}MCP config locations:${c.reset}`);
|
|
719
|
+
console.log(` ${c.dim} Claude: ~/.claude/mcp.json${c.reset}`);
|
|
720
|
+
console.log(` ${c.dim} Cursor: ~/.cursor/mcp.json${c.reset}`);
|
|
721
|
+
console.log(` ${c.dim} Windsurf: ~/.codeium/windsurf/mcp_config.json${c.reset}`);
|
|
722
|
+
console.log(` ${c.dim} VS Code: ~/.vscode/mcp.json${c.reset}`);
|
|
723
|
+
console.log();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
let totalServers = 0;
|
|
728
|
+
let checkedServers = 0;
|
|
729
|
+
let auditedServers = 0;
|
|
730
|
+
let unauditedServers = 0;
|
|
731
|
+
|
|
732
|
+
for (const config of configs) {
|
|
733
|
+
const servers = extractServersFromConfig(config.content);
|
|
734
|
+
const serverCount = servers.length;
|
|
735
|
+
totalServers += serverCount;
|
|
736
|
+
|
|
737
|
+
const countLabel = serverCount === 0
|
|
738
|
+
? `${c.dim}no servers${c.reset}`
|
|
739
|
+
: `found ${c.bold}${serverCount}${c.reset} server${serverCount > 1 ? 's' : ''}`;
|
|
740
|
+
|
|
741
|
+
console.log(`${icons.bullet} Scanning ${c.bold}${config.name}${c.reset} ${c.dim}${config.path}${c.reset} ${countLabel}`);
|
|
742
|
+
|
|
743
|
+
if (serverCount === 0) {
|
|
744
|
+
console.log();
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
console.log();
|
|
749
|
+
|
|
750
|
+
for (let i = 0; i < servers.length; i++) {
|
|
751
|
+
const server = servers[i];
|
|
752
|
+
const isLast = i === servers.length - 1;
|
|
753
|
+
const branch = isLast ? icons.treeLast : icons.tree;
|
|
754
|
+
const pipe = isLast ? ' ' : `${icons.pipe} `;
|
|
755
|
+
|
|
756
|
+
const slug = serverSlug(server);
|
|
757
|
+
checkedServers++;
|
|
758
|
+
|
|
759
|
+
// Registry lookup
|
|
760
|
+
const registryData = await checkRegistry(slug);
|
|
761
|
+
|
|
762
|
+
// Also try with server name directly
|
|
763
|
+
let regData = registryData;
|
|
764
|
+
if (!regData && slug !== server.name.toLowerCase()) {
|
|
765
|
+
regData = await checkRegistry(server.name.toLowerCase());
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Determine source display
|
|
769
|
+
let sourceLabel = '';
|
|
770
|
+
if (server.npmPackage) sourceLabel = `${c.dim}npm:${server.npmPackage}${c.reset}`;
|
|
771
|
+
else if (server.pyPackage) sourceLabel = `${c.dim}pip:${server.pyPackage}${c.reset}`;
|
|
772
|
+
else if (server.command) sourceLabel = `${c.dim}${[server.command, ...server.args.slice(0, 2)].join(' ')}${c.reset}`;
|
|
773
|
+
|
|
774
|
+
if (regData) {
|
|
775
|
+
auditedServers++;
|
|
776
|
+
const riskScore = regData.risk_score ?? regData.latest_risk_score ?? 0;
|
|
777
|
+
const hasOfficial = regData.has_official_audit;
|
|
778
|
+
console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
|
|
779
|
+
console.log(`${pipe} ${riskBadge(riskScore)} Risk ${riskScore} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
|
|
780
|
+
} else {
|
|
781
|
+
unauditedServers++;
|
|
782
|
+
console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
|
|
783
|
+
console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: agentaudit scan <source-url>${c.reset}`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (server.sourceUrl) {
|
|
787
|
+
console.log(`${pipe} ${c.dim}source: ${server.sourceUrl}${c.reset}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
console.log();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Summary
|
|
795
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
796
|
+
console.log(` ${c.bold}Summary${c.reset} ${totalServers} server${totalServers !== 1 ? 's' : ''} across ${configs.length} config${configs.length !== 1 ? 's' : ''}`);
|
|
797
|
+
console.log();
|
|
798
|
+
if (auditedServers > 0) console.log(` ${icons.safe} ${c.green}${auditedServers} audited${c.reset}`);
|
|
799
|
+
if (unauditedServers > 0) console.log(` ${icons.caution} ${c.yellow}${unauditedServers} not audited${c.reset}`);
|
|
800
|
+
console.log();
|
|
801
|
+
|
|
802
|
+
if (unauditedServers > 0) {
|
|
803
|
+
console.log(` ${c.dim}To audit unaudited servers, run:${c.reset}`);
|
|
804
|
+
console.log(` ${c.cyan}agentaudit scan <github-url>${c.reset}`);
|
|
805
|
+
console.log();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
605
809
|
// ── Check command ───────────────────────────────────────
|
|
606
810
|
|
|
607
811
|
async function checkPackage(name) {
|
|
@@ -633,11 +837,13 @@ async function main() {
|
|
|
633
837
|
banner();
|
|
634
838
|
console.log(` ${c.bold}Usage:${c.reset}`);
|
|
635
839
|
console.log(` agentaudit setup Register + configure API key`);
|
|
840
|
+
console.log(` agentaudit discover Find & check local MCP servers`);
|
|
636
841
|
console.log(` agentaudit scan <repo-url> [repo-url...] Scan repositories`);
|
|
637
842
|
console.log(` agentaudit check <package-name> Look up in registry`);
|
|
638
843
|
console.log();
|
|
639
844
|
console.log(` ${c.bold}Examples:${c.reset}`);
|
|
640
845
|
console.log(` agentaudit setup`);
|
|
846
|
+
console.log(` agentaudit discover`);
|
|
641
847
|
console.log(` agentaudit scan https://github.com/owner/repo`);
|
|
642
848
|
console.log(` agentaudit scan repo1.git repo2.git repo3.git`);
|
|
643
849
|
console.log(` agentaudit check fastmcp`);
|
|
@@ -655,6 +861,11 @@ async function main() {
|
|
|
655
861
|
return;
|
|
656
862
|
}
|
|
657
863
|
|
|
864
|
+
if (command === 'discover') {
|
|
865
|
+
await discoverCommand();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
658
869
|
if (command === 'check') {
|
|
659
870
|
if (targets.length === 0) {
|
|
660
871
|
console.log(` ${c.red}Error: package name required${c.reset}`);
|
|
@@ -667,6 +878,7 @@ async function main() {
|
|
|
667
878
|
if (command === 'scan') {
|
|
668
879
|
if (targets.length === 0) {
|
|
669
880
|
console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
|
|
881
|
+
console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit discover${c.dim} to find & check locally installed MCP servers${c.reset}`);
|
|
670
882
|
process.exit(1);
|
|
671
883
|
}
|
|
672
884
|
|