agentaudit 3.0.0 → 3.2.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/README.md +26 -7
- package/cli.mjs +468 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,22 +169,41 @@ The MCP server finds credentials automatically from:
|
|
|
169
169
|
## CLI Reference
|
|
170
170
|
|
|
171
171
|
```
|
|
172
|
-
agentaudit
|
|
173
|
-
agentaudit scan <url> [url...]
|
|
172
|
+
agentaudit discover Find local MCP servers + check registry
|
|
173
|
+
agentaudit scan <url> [url...] Quick static scan (regex, ~2s)
|
|
174
|
+
agentaudit audit <url> [url...] Deep LLM-powered audit (~30s)
|
|
174
175
|
agentaudit check <name> Look up package in registry
|
|
175
|
-
agentaudit
|
|
176
|
+
agentaudit setup Register + configure API key
|
|
176
177
|
```
|
|
177
178
|
|
|
179
|
+
### `scan` vs `audit`
|
|
180
|
+
|
|
181
|
+
| | `scan` | `audit` |
|
|
182
|
+
|--|--------|---------|
|
|
183
|
+
| **How** | Regex-based static analysis | LLM 3-pass analysis (UNDERSTAND → DETECT → CLASSIFY) |
|
|
184
|
+
| **Speed** | ~2 seconds | ~30 seconds |
|
|
185
|
+
| **Depth** | Pattern matching | Semantic code understanding |
|
|
186
|
+
| **Needs API key** | No | Yes (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) |
|
|
187
|
+
| **Upload to registry** | No | Yes (with `agentaudit setup`) |
|
|
188
|
+
|
|
189
|
+
Use `scan` for quick checks, `audit` for thorough analysis.
|
|
190
|
+
|
|
178
191
|
### Examples
|
|
179
192
|
|
|
180
193
|
```bash
|
|
181
|
-
#
|
|
194
|
+
# Discover all MCP servers on your machine
|
|
195
|
+
agentaudit discover
|
|
196
|
+
|
|
197
|
+
# Quick scan
|
|
182
198
|
agentaudit scan https://github.com/jlowin/fastmcp
|
|
183
199
|
|
|
184
|
-
#
|
|
185
|
-
agentaudit
|
|
200
|
+
# Deep audit (requires ANTHROPIC_API_KEY or OPENAI_API_KEY)
|
|
201
|
+
agentaudit audit https://github.com/jlowin/fastmcp
|
|
202
|
+
|
|
203
|
+
# Export audit for manual LLM review (no API key needed)
|
|
204
|
+
agentaudit audit https://github.com/owner/repo --export
|
|
186
205
|
|
|
187
|
-
# Check registry
|
|
206
|
+
# Check registry
|
|
188
207
|
agentaudit check mongodb-mcp-server
|
|
189
208
|
```
|
|
190
209
|
|
package/cli.mjs
CHANGED
|
@@ -602,6 +602,440 @@ 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 audit <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
|
+
|
|
809
|
+
// ── Audit command (deep LLM-powered) ────────────────────
|
|
810
|
+
|
|
811
|
+
function loadAuditPrompt() {
|
|
812
|
+
const promptPath = path.join(SKILL_DIR, 'prompts', 'audit-prompt.md');
|
|
813
|
+
if (fs.existsSync(promptPath)) return fs.readFileSync(promptPath, 'utf8');
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function auditRepo(url) {
|
|
818
|
+
const start = Date.now();
|
|
819
|
+
const slug = slugFromUrl(url);
|
|
820
|
+
|
|
821
|
+
console.log(`${icons.scan} ${c.bold}Auditing ${slug}${c.reset} ${c.dim}${url}${c.reset}`);
|
|
822
|
+
console.log(`${icons.pipe} ${c.dim}Deep LLM-powered analysis (3-pass: UNDERSTAND → DETECT → CLASSIFY)${c.reset}`);
|
|
823
|
+
console.log();
|
|
824
|
+
|
|
825
|
+
// Step 1: Clone
|
|
826
|
+
process.stdout.write(` ${c.dim}[1/4]${c.reset} Cloning repository...`);
|
|
827
|
+
const tmpDir = fs.mkdtempSync('/tmp/agentaudit-');
|
|
828
|
+
const repoPath = path.join(tmpDir, 'repo');
|
|
829
|
+
try {
|
|
830
|
+
execSync(`git clone --depth 1 "${url}" "${repoPath}" 2>/dev/null`, {
|
|
831
|
+
timeout: 30_000, stdio: 'pipe',
|
|
832
|
+
});
|
|
833
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
834
|
+
} catch {
|
|
835
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Step 2: Collect files
|
|
840
|
+
process.stdout.write(` ${c.dim}[2/4]${c.reset} Collecting source files...`);
|
|
841
|
+
const files = collectFiles(repoPath);
|
|
842
|
+
console.log(` ${c.green}${files.length} files${c.reset}`);
|
|
843
|
+
|
|
844
|
+
// Step 3: Build audit payload
|
|
845
|
+
process.stdout.write(` ${c.dim}[3/4]${c.reset} Preparing audit payload...`);
|
|
846
|
+
const auditPrompt = loadAuditPrompt();
|
|
847
|
+
|
|
848
|
+
let codeBlock = '';
|
|
849
|
+
for (const file of files) {
|
|
850
|
+
codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
851
|
+
}
|
|
852
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
853
|
+
|
|
854
|
+
// Step 4: LLM Analysis
|
|
855
|
+
// Check for API keys to determine which LLM to use
|
|
856
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
857
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
858
|
+
|
|
859
|
+
if (!anthropicKey && !openaiKey) {
|
|
860
|
+
// No LLM API key — output the prepared audit for piping or MCP use
|
|
861
|
+
console.log();
|
|
862
|
+
console.log(` ${c.yellow}No LLM API key found.${c.reset} To run the audit automatically, set one of:`);
|
|
863
|
+
console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
864
|
+
console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
|
|
865
|
+
console.log();
|
|
866
|
+
console.log(` ${c.bold}Alternatives:${c.reset}`);
|
|
867
|
+
console.log(` ${c.dim}1.${c.reset} Use the MCP server in Claude/Cursor — your agent runs the audit automatically`);
|
|
868
|
+
console.log(` ${c.dim}2.${c.reset} Export for manual review: ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
|
|
869
|
+
console.log();
|
|
870
|
+
|
|
871
|
+
// Check if --export flag
|
|
872
|
+
if (process.argv.includes('--export')) {
|
|
873
|
+
const exportPath = path.join(process.cwd(), `audit-${slug}.md`);
|
|
874
|
+
const exportContent = [
|
|
875
|
+
`# Security Audit: ${slug}`,
|
|
876
|
+
`**Source:** ${url}`,
|
|
877
|
+
`**Files:** ${files.length}`,
|
|
878
|
+
``,
|
|
879
|
+
`## Audit Instructions`,
|
|
880
|
+
``,
|
|
881
|
+
auditPrompt || '(audit prompt not found)',
|
|
882
|
+
``,
|
|
883
|
+
`## Report Format`,
|
|
884
|
+
``,
|
|
885
|
+
`After analysis, produce a JSON report:`,
|
|
886
|
+
'```json',
|
|
887
|
+
`{ "skill_slug": "${slug}", "source_url": "${url}", "risk_score": 0, "result": "safe", "findings": [] }`,
|
|
888
|
+
'```',
|
|
889
|
+
``,
|
|
890
|
+
`## Source Code`,
|
|
891
|
+
``,
|
|
892
|
+
codeBlock,
|
|
893
|
+
].join('\n');
|
|
894
|
+
fs.writeFileSync(exportPath, exportContent);
|
|
895
|
+
console.log(` ${icons.safe} Exported to ${c.bold}${exportPath}${c.reset}`);
|
|
896
|
+
console.log(` ${c.dim}Paste this into any LLM (Claude, ChatGPT, etc.) for analysis${c.reset}`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Cleanup
|
|
900
|
+
try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// We have an API key — run LLM audit
|
|
905
|
+
process.stdout.write(` ${c.dim}[4/4]${c.reset} Running LLM analysis...`);
|
|
906
|
+
|
|
907
|
+
const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
|
|
908
|
+
const userMessage = [
|
|
909
|
+
`Audit this package: **${slug}** (${url})`,
|
|
910
|
+
``,
|
|
911
|
+
`After analysis, respond with ONLY a JSON object (no markdown, no explanation):`,
|
|
912
|
+
'```',
|
|
913
|
+
`{ "skill_slug": "${slug}", "source_url": "${url}", "package_type": "<mcp-server|agent-skill|library|cli-tool>",`,
|
|
914
|
+
` "risk_score": <0-100>, "result": "<safe|caution|unsafe>", "max_severity": "<none|low|medium|high|critical>",`,
|
|
915
|
+
` "findings_count": <n>, "findings": [{ "id": "...", "title": "...", "severity": "...", "category": "...",`,
|
|
916
|
+
` "description": "...", "file": "...", "line": <n>, "remediation": "...", "confidence": "...", "is_by_design": false }] }`,
|
|
917
|
+
'```',
|
|
918
|
+
``,
|
|
919
|
+
`## Source Code`,
|
|
920
|
+
codeBlock,
|
|
921
|
+
].join('\n');
|
|
922
|
+
|
|
923
|
+
let report = null;
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
if (anthropicKey) {
|
|
927
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
928
|
+
method: 'POST',
|
|
929
|
+
headers: {
|
|
930
|
+
'x-api-key': anthropicKey,
|
|
931
|
+
'anthropic-version': '2023-06-01',
|
|
932
|
+
'content-type': 'application/json',
|
|
933
|
+
},
|
|
934
|
+
body: JSON.stringify({
|
|
935
|
+
model: 'claude-sonnet-4-20250514',
|
|
936
|
+
max_tokens: 8192,
|
|
937
|
+
system: systemPrompt,
|
|
938
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
939
|
+
}),
|
|
940
|
+
signal: AbortSignal.timeout(120_000),
|
|
941
|
+
});
|
|
942
|
+
const data = await res.json();
|
|
943
|
+
const text = data.content?.[0]?.text || '';
|
|
944
|
+
// Extract JSON from response
|
|
945
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
946
|
+
if (jsonMatch) report = JSON.parse(jsonMatch[0]);
|
|
947
|
+
} else if (openaiKey) {
|
|
948
|
+
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
949
|
+
method: 'POST',
|
|
950
|
+
headers: {
|
|
951
|
+
'Authorization': `Bearer ${openaiKey}`,
|
|
952
|
+
'Content-Type': 'application/json',
|
|
953
|
+
},
|
|
954
|
+
body: JSON.stringify({
|
|
955
|
+
model: 'gpt-4o',
|
|
956
|
+
max_tokens: 8192,
|
|
957
|
+
messages: [
|
|
958
|
+
{ role: 'system', content: systemPrompt },
|
|
959
|
+
{ role: 'user', content: userMessage },
|
|
960
|
+
],
|
|
961
|
+
}),
|
|
962
|
+
signal: AbortSignal.timeout(120_000),
|
|
963
|
+
});
|
|
964
|
+
const data = await res.json();
|
|
965
|
+
const text = data.choices?.[0]?.message?.content || '';
|
|
966
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
967
|
+
if (jsonMatch) report = JSON.parse(jsonMatch[0]);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
console.log(` ${c.green}done${c.reset} ${c.dim}(${elapsed(start)})${c.reset}`);
|
|
971
|
+
} catch (err) {
|
|
972
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
973
|
+
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
974
|
+
try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Cleanup repo
|
|
979
|
+
try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
|
|
980
|
+
|
|
981
|
+
if (!report) {
|
|
982
|
+
console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Display results
|
|
987
|
+
console.log();
|
|
988
|
+
const riskScore = report.risk_score || 0;
|
|
989
|
+
console.log(` ${riskBadge(riskScore)} Risk ${riskScore}/100 ${c.bold}${report.result || 'unknown'}${c.reset}`);
|
|
990
|
+
console.log();
|
|
991
|
+
|
|
992
|
+
if (report.findings && report.findings.length > 0) {
|
|
993
|
+
console.log(` ${c.bold}Findings (${report.findings.length})${c.reset}`);
|
|
994
|
+
console.log();
|
|
995
|
+
for (const f of report.findings) {
|
|
996
|
+
const sc = severityColor(f.severity);
|
|
997
|
+
console.log(` ${severityIcon(f.severity)} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${f.title}`);
|
|
998
|
+
if (f.file) console.log(` ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
|
|
999
|
+
if (f.description) console.log(` ${c.dim}${f.description.slice(0, 120)}${c.reset}`);
|
|
1000
|
+
console.log();
|
|
1001
|
+
}
|
|
1002
|
+
} else {
|
|
1003
|
+
console.log(` ${c.green}No findings — package looks clean.${c.reset}`);
|
|
1004
|
+
console.log();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Upload to registry
|
|
1008
|
+
const creds = loadCredentials();
|
|
1009
|
+
if (creds) {
|
|
1010
|
+
process.stdout.write(` Uploading report to registry...`);
|
|
1011
|
+
try {
|
|
1012
|
+
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
1013
|
+
method: 'POST',
|
|
1014
|
+
headers: {
|
|
1015
|
+
'Authorization': `Bearer ${creds.api_key}`,
|
|
1016
|
+
'Content-Type': 'application/json',
|
|
1017
|
+
},
|
|
1018
|
+
body: JSON.stringify(report),
|
|
1019
|
+
signal: AbortSignal.timeout(15_000),
|
|
1020
|
+
});
|
|
1021
|
+
if (res.ok) {
|
|
1022
|
+
const data = await res.json();
|
|
1023
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
1024
|
+
console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${slug}${c.reset}`);
|
|
1025
|
+
} else {
|
|
1026
|
+
console.log(` ${c.yellow}failed (HTTP ${res.status})${c.reset}`);
|
|
1027
|
+
}
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
console.log(` ${c.yellow}failed${c.reset}`);
|
|
1030
|
+
}
|
|
1031
|
+
} else {
|
|
1032
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to upload reports to the registry${c.reset}`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
console.log();
|
|
1036
|
+
return report;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
605
1039
|
// ── Check command ───────────────────────────────────────
|
|
606
1040
|
|
|
607
1041
|
async function checkPackage(name) {
|
|
@@ -611,7 +1045,7 @@ async function checkPackage(name) {
|
|
|
611
1045
|
const data = await checkRegistry(name);
|
|
612
1046
|
if (!data) {
|
|
613
1047
|
console.log(` ${c.yellow}Not found${c.reset} — package "${name}" hasn't been audited yet.`);
|
|
614
|
-
console.log(` ${c.dim}Run: agentaudit
|
|
1048
|
+
console.log(` ${c.dim}Run: agentaudit audit <repo-url> for a deep LLM audit${c.reset}`);
|
|
615
1049
|
return;
|
|
616
1050
|
}
|
|
617
1051
|
|
|
@@ -631,15 +1065,22 @@ async function main() {
|
|
|
631
1065
|
|
|
632
1066
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
633
1067
|
banner();
|
|
634
|
-
console.log(` ${c.bold}
|
|
635
|
-
console.log(
|
|
636
|
-
console.log(` agentaudit
|
|
637
|
-
console.log(` agentaudit
|
|
1068
|
+
console.log(` ${c.bold}Commands:${c.reset}`);
|
|
1069
|
+
console.log();
|
|
1070
|
+
console.log(` ${c.cyan}agentaudit discover${c.reset} Find local MCP servers + check registry`);
|
|
1071
|
+
console.log(` ${c.cyan}agentaudit scan${c.reset} <url> [url...] Quick static scan (regex, local)`);
|
|
1072
|
+
console.log(` ${c.cyan}agentaudit audit${c.reset} <url> [url...] Deep LLM-powered security audit`);
|
|
1073
|
+
console.log(` ${c.cyan}agentaudit check${c.reset} <name> Look up package in registry`);
|
|
1074
|
+
console.log(` ${c.cyan}agentaudit setup${c.reset} Register + configure API key`);
|
|
1075
|
+
console.log();
|
|
1076
|
+
console.log(` ${c.bold}scan${c.reset} vs ${c.bold}audit${c.reset}:`);
|
|
1077
|
+
console.log(` ${c.dim}scan = fast regex-based static analysis (~2s)${c.reset}`);
|
|
1078
|
+
console.log(` ${c.dim}audit = deep LLM analysis with 3-pass methodology (~30s)${c.reset}`);
|
|
638
1079
|
console.log();
|
|
639
1080
|
console.log(` ${c.bold}Examples:${c.reset}`);
|
|
640
|
-
console.log(` agentaudit
|
|
1081
|
+
console.log(` agentaudit discover`);
|
|
641
1082
|
console.log(` agentaudit scan https://github.com/owner/repo`);
|
|
642
|
-
console.log(` agentaudit
|
|
1083
|
+
console.log(` agentaudit audit https://github.com/owner/repo`);
|
|
643
1084
|
console.log(` agentaudit check fastmcp`);
|
|
644
1085
|
console.log();
|
|
645
1086
|
process.exit(0);
|
|
@@ -655,6 +1096,11 @@ async function main() {
|
|
|
655
1096
|
return;
|
|
656
1097
|
}
|
|
657
1098
|
|
|
1099
|
+
if (command === 'discover') {
|
|
1100
|
+
await discoverCommand();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
658
1104
|
if (command === 'check') {
|
|
659
1105
|
if (targets.length === 0) {
|
|
660
1106
|
console.log(` ${c.red}Error: package name required${c.reset}`);
|
|
@@ -667,6 +1113,8 @@ async function main() {
|
|
|
667
1113
|
if (command === 'scan') {
|
|
668
1114
|
if (targets.length === 0) {
|
|
669
1115
|
console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
|
|
1116
|
+
console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit discover${c.dim} to find & check locally installed MCP servers${c.reset}`);
|
|
1117
|
+
console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit audit <url>${c.dim} for a deep LLM-powered audit${c.reset}`);
|
|
670
1118
|
process.exit(1);
|
|
671
1119
|
}
|
|
672
1120
|
|
|
@@ -682,6 +1130,19 @@ async function main() {
|
|
|
682
1130
|
return;
|
|
683
1131
|
}
|
|
684
1132
|
|
|
1133
|
+
if (command === 'audit') {
|
|
1134
|
+
const urls = targets.filter(t => !t.startsWith('--'));
|
|
1135
|
+
if (urls.length === 0) {
|
|
1136
|
+
console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
|
|
1137
|
+
process.exit(1);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
for (const url of urls) {
|
|
1141
|
+
await auditRepo(url);
|
|
1142
|
+
}
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
685
1146
|
console.log(` ${c.red}Unknown command: ${command}${c.reset}`);
|
|
686
1147
|
console.log(` ${c.dim}Run agentaudit --help for usage${c.reset}`);
|
|
687
1148
|
process.exit(1);
|