agentaudit 3.10.6 → 3.10.7
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 +32 -5
- package/index.mjs +43 -3
- package/package.json +1 -1
- package/prompts/audit-prompt.md +2 -3
package/cli.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import fs from 'fs';
|
|
26
26
|
import os from 'os';
|
|
27
27
|
import path from 'path';
|
|
28
|
+
import crypto from 'crypto';
|
|
28
29
|
import { execSync, execFileSync } from 'child_process';
|
|
29
30
|
import { createInterface } from 'readline';
|
|
30
31
|
import { fileURLToPath } from 'url';
|
|
@@ -818,7 +819,7 @@ const SKIP_DIRS = new Set([
|
|
|
818
819
|
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
|
|
819
820
|
'.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
|
|
820
821
|
'test', 'tests', '__tests__', 'spec', 'specs', 'docs', 'doc',
|
|
821
|
-
'examples', 'example', 'fixtures', '.
|
|
822
|
+
'examples', 'example', 'fixtures', '.vscode', '.idea',
|
|
822
823
|
'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
|
|
823
824
|
]);
|
|
824
825
|
const SKIP_EXTENSIONS = new Set([
|
|
@@ -839,6 +840,12 @@ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
|
|
|
839
840
|
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
840
841
|
const fullPath = path.join(dir, entry.name);
|
|
841
842
|
if (entry.isDirectory()) {
|
|
843
|
+
// Special: scan .github/workflows/ (security-critical CI/CD files)
|
|
844
|
+
if (entry.name === '.github') {
|
|
845
|
+
const wfDir = path.join(fullPath, 'workflows');
|
|
846
|
+
try { if (fs.statSync(wfDir).isDirectory()) collectFiles(wfDir, relPath + '/workflows', collected, totalSize); } catch {}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
842
849
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
843
850
|
collectFiles(fullPath, relPath, collected, totalSize);
|
|
844
851
|
} else {
|
|
@@ -1794,7 +1801,7 @@ async function auditRepo(url) {
|
|
|
1794
1801
|
`Audit this package: **${slug}** (${url})`,
|
|
1795
1802
|
``,
|
|
1796
1803
|
`After analysis, respond with ONLY a valid JSON object. No markdown fences, no explanation, no text before or after. Just the raw JSON:`,
|
|
1797
|
-
`{ "skill_slug": "${slug}", "source_url": "${url}", "package_type": "<mcp-server|agent-skill|library|cli-tool>",`,
|
|
1804
|
+
`{ "skill_slug": "${slug}", "source_url": "${url}", "package_type": "<mcp-server|agent-skill|library|cli-tool|other>",`,
|
|
1798
1805
|
` "risk_score": <0-100>, "result": "<safe|caution|unsafe>", "max_severity": "<none|low|medium|high|critical>",`,
|
|
1799
1806
|
` "findings_count": <n>, "findings": [{ "pattern_id": "CMD_INJECT_001", "title": "...", "severity": "...", "category": "...",`,
|
|
1800
1807
|
` "cwe_id": "CWE-78", "description": "...", "file": "...", "line": <n>, "content": "...", "remediation": "...",`,
|
|
@@ -1952,9 +1959,22 @@ async function auditRepo(url) {
|
|
|
1952
1959
|
return null;
|
|
1953
1960
|
}
|
|
1954
1961
|
|
|
1955
|
-
//
|
|
1962
|
+
// Provenance: compute BEFORE cleanup (needs repoPath on disk)
|
|
1963
|
+
let commitSha = '';
|
|
1964
|
+
try {
|
|
1965
|
+
commitSha = execSync('git rev-parse HEAD', { cwd: repoPath, encoding: 'utf8' }).trim();
|
|
1966
|
+
} catch { /* shallow clone without HEAD — unlikely but safe */ }
|
|
1967
|
+
const sourceHash = crypto.createHash('sha256').update(
|
|
1968
|
+
files.slice().sort((a, b) => a.path.localeCompare(b.path))
|
|
1969
|
+
.map(f => f.path + '\n' + f.content).join('\n')
|
|
1970
|
+
).digest('hex');
|
|
1971
|
+
// Code-based type detection (uses files array in memory + repoPath for context)
|
|
1972
|
+
const pkgInfo = detectPackageInfo(repoPath, files);
|
|
1973
|
+
const detectedType = pkgInfo.type === 'unknown' ? 'other' : pkgInfo.type;
|
|
1974
|
+
|
|
1975
|
+
// Cleanup repo (safe now — provenance data captured above)
|
|
1956
1976
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1957
|
-
|
|
1977
|
+
|
|
1958
1978
|
if (!report) {
|
|
1959
1979
|
console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
|
|
1960
1980
|
console.log(` ${c.dim}Hint: run with --debug to see the raw LLM response${c.reset}`);
|
|
@@ -1965,14 +1985,21 @@ async function auditRepo(url) {
|
|
|
1965
1985
|
}
|
|
1966
1986
|
return null;
|
|
1967
1987
|
}
|
|
1968
|
-
|
|
1988
|
+
|
|
1969
1989
|
// Force slug from URL — never trust LLM-provided skill_slug
|
|
1970
1990
|
report.skill_slug = slug;
|
|
1971
1991
|
|
|
1992
|
+
// Force package_type from code detection — never trust LLM-provided type
|
|
1993
|
+
report.package_type = detectedType;
|
|
1994
|
+
|
|
1972
1995
|
// Add scan metadata for benchmarking
|
|
1973
1996
|
report.audit_duration_ms = Date.now() - start;
|
|
1974
1997
|
report.files_scanned = files.length;
|
|
1975
1998
|
|
|
1999
|
+
// Set provenance data
|
|
2000
|
+
if (commitSha) report.commit_sha = commitSha;
|
|
2001
|
+
report.source_hash = sourceHash;
|
|
2002
|
+
|
|
1976
2003
|
// Display results
|
|
1977
2004
|
console.log();
|
|
1978
2005
|
const riskScore = report.risk_score || 0;
|
package/index.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import fs from 'fs';
|
|
28
28
|
import os from 'os';
|
|
29
29
|
import path from 'path';
|
|
30
|
+
import crypto from 'crypto';
|
|
30
31
|
import { execSync, execFileSync } from 'child_process';
|
|
31
32
|
import { fileURLToPath } from 'url';
|
|
32
33
|
|
|
@@ -39,7 +40,7 @@ const SKIP_DIRS = new Set([
|
|
|
39
40
|
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
|
|
40
41
|
'.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
|
|
41
42
|
'test', 'tests', '__tests__', 'spec', 'specs', 'docs', 'doc',
|
|
42
|
-
'examples', 'example', 'fixtures', '.
|
|
43
|
+
'examples', 'example', 'fixtures', '.vscode', '.idea',
|
|
43
44
|
'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
|
|
44
45
|
]);
|
|
45
46
|
const SKIP_EXTENSIONS = new Set([
|
|
@@ -101,6 +102,12 @@ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
|
|
|
101
102
|
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
102
103
|
const fullPath = path.join(dir, entry.name);
|
|
103
104
|
if (entry.isDirectory()) {
|
|
105
|
+
// Special: scan .github/workflows/ (security-critical CI/CD files)
|
|
106
|
+
if (entry.name === '.github') {
|
|
107
|
+
const wfDir = path.join(fullPath, 'workflows');
|
|
108
|
+
try { if (fs.statSync(wfDir).isDirectory()) collectFiles(wfDir, relPath + '/workflows', collected, totalSize); } catch {}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
104
111
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
105
112
|
collectFiles(fullPath, relPath, collected, totalSize);
|
|
106
113
|
} else {
|
|
@@ -122,6 +129,23 @@ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
|
|
|
122
129
|
return collected;
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
// ── Package Detection ────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
function detectPackageInfo(repoPath, files) {
|
|
135
|
+
const info = { type: 'unknown' };
|
|
136
|
+
const allContent = files.map(f => f.content).join('\n');
|
|
137
|
+
if (allContent.includes('@modelcontextprotocol') || allContent.includes('FastMCP') || allContent.includes('mcp.server') || allContent.includes('mcp_server')) {
|
|
138
|
+
info.type = 'mcp-server';
|
|
139
|
+
} else if (files.some(f => f.path.toLowerCase() === 'skill.md')) {
|
|
140
|
+
info.type = 'agent-skill';
|
|
141
|
+
} else if (allContent.includes('#!/usr/bin/env') || allContent.includes('argparse') || allContent.includes('commander')) {
|
|
142
|
+
info.type = 'cli-tool';
|
|
143
|
+
} else {
|
|
144
|
+
info.type = 'library';
|
|
145
|
+
}
|
|
146
|
+
return info;
|
|
147
|
+
}
|
|
148
|
+
|
|
125
149
|
// ── Repo Helpers ────────────────────────────────────────
|
|
126
150
|
|
|
127
151
|
function validateGitUrl(url) {
|
|
@@ -431,6 +455,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
431
455
|
const slug = slugFromUrl(source_url);
|
|
432
456
|
const auditPrompt = loadAuditPrompt();
|
|
433
457
|
|
|
458
|
+
// Compute provenance data
|
|
459
|
+
const pkgInfo = detectPackageInfo(repoPath, files);
|
|
460
|
+
const detectedType = pkgInfo.type === 'unknown' ? 'other' : pkgInfo.type;
|
|
461
|
+
let commitSha = '';
|
|
462
|
+
try { commitSha = execSync('git rev-parse HEAD', { cwd: repoPath, encoding: 'utf8' }).trim(); } catch {}
|
|
463
|
+
const hashInput = files.slice().sort((a, b) => a.path.localeCompare(b.path))
|
|
464
|
+
.map(f => f.path + '\n' + f.content).join('\n');
|
|
465
|
+
const sourceHash = crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
466
|
+
|
|
434
467
|
let codeBlock = '';
|
|
435
468
|
for (const file of files) {
|
|
436
469
|
codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
@@ -441,11 +474,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
441
474
|
``,
|
|
442
475
|
`**Source:** ${source_url}`,
|
|
443
476
|
`**Files collected:** ${files.length}`,
|
|
477
|
+
`**Detected type:** ${detectedType}`,
|
|
478
|
+
commitSha ? `**Commit:** ${commitSha}` : '',
|
|
479
|
+
`**Source hash:** ${sourceHash}`,
|
|
444
480
|
``,
|
|
445
481
|
`## Your Task`,
|
|
446
482
|
``,
|
|
447
483
|
`1. Analyze the source code below using the 3-pass audit methodology`,
|
|
448
484
|
`2. Call \`submit_report\` with your findings as JSON`,
|
|
485
|
+
`3. IMPORTANT: Include the pre-computed provenance fields exactly as shown below`,
|
|
449
486
|
``,
|
|
450
487
|
`## Report Format`,
|
|
451
488
|
``,
|
|
@@ -454,7 +491,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
454
491
|
`{`,
|
|
455
492
|
` "skill_slug": "${slug}",`,
|
|
456
493
|
` "source_url": "${source_url}",`,
|
|
457
|
-
` "package_type": "
|
|
494
|
+
` "package_type": "${detectedType}",`,
|
|
495
|
+
` "audit_model": "<your-model-id, e.g. claude-sonnet-4-20250514>",`,
|
|
496
|
+
commitSha ? ` "commit_sha": "${commitSha}",` : '',
|
|
497
|
+
` "source_hash": "${sourceHash}",`,
|
|
458
498
|
` "risk_score": <0-100>,`,
|
|
459
499
|
` "result": "<safe|caution|unsafe>",`,
|
|
460
500
|
` "max_severity": "<none|low|medium|high|critical>",`,
|
|
@@ -483,7 +523,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
483
523
|
`## Source Code`,
|
|
484
524
|
``,
|
|
485
525
|
codeBlock,
|
|
486
|
-
].join('\n');
|
|
526
|
+
].filter(Boolean).join('\n');
|
|
487
527
|
|
|
488
528
|
return { content: [{ type: 'text', text: response }] };
|
|
489
529
|
} catch (err) {
|
package/package.json
CHANGED
package/prompts/audit-prompt.md
CHANGED
|
@@ -27,7 +27,7 @@ PACKAGE PROFILE:
|
|
|
27
27
|
- Name: <package name>
|
|
28
28
|
- Purpose: <one sentence describing what this package does>
|
|
29
29
|
- Category: <one of the categories below>
|
|
30
|
-
- Package Type: <one of: mcp-server, agent-skill, library, cli-tool,
|
|
30
|
+
- Package Type: <one of: mcp-server, agent-skill, library, cli-tool, other>
|
|
31
31
|
- Expected Behaviors: <5-10 things this package SHOULD do given its purpose>
|
|
32
32
|
- Abnormal for Category: <5-10 things that would be suspicious for this category>
|
|
33
33
|
- Trust Boundaries: <where does external input enter? LLM tool args, HTTP requests, CLI args, file uploads, stdin, none>
|
|
@@ -47,8 +47,7 @@ Determine the `package_type` using these signals (check in order, first match wi
|
|
|
47
47
|
| "mcp" in package name AND has server/transport code | `mcp-server` |
|
|
48
48
|
| Has `bin` field in `package.json` (standalone CLI) | `cli-tool` |
|
|
49
49
|
| Is a reusable SDK/framework (no server, no CLI entry) | `library` |
|
|
50
|
-
|
|
|
51
|
-
| Source URL contains `pypi.org` | `pip-package` |
|
|
50
|
+
| None of the above match | `other` |
|
|
52
51
|
|
|
53
52
|
**Include `package_type` in your JSON report** as a top-level field (see Report Format).
|
|
54
53
|
|