agentaudit 3.12.9 → 3.12.11
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 +1 -1
- package/cli.mjs +564 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
# 🛡️ AgentAudit
|
|
8
8
|
|
|
9
|
-
**Security scanner for AI packages — MCP server
|
|
9
|
+
**Security scanner for AI agent packages — CLI + MCP server**
|
|
10
10
|
|
|
11
11
|
Scan MCP servers, AI skills, and packages for vulnerabilities, prompt injection,
|
|
12
12
|
and supply chain attacks. Powered by regex static analysis and deep LLM audits.
|
package/cli.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* profile Your profile — rank, points, audit stats
|
|
22
22
|
* help [command] Show help
|
|
23
23
|
*
|
|
24
|
-
* Flags: --json, --quiet, --no-color, --no-upload, --model, --export, --debug
|
|
24
|
+
* Flags: --json, --quiet, --no-color, --no-upload, --model, --export, --format, --debug
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import fs from 'fs';
|
|
@@ -2985,7 +2985,120 @@ function enrichFindings(report, files, pkgInfo) {
|
|
|
2985
2985
|
return report;
|
|
2986
2986
|
}
|
|
2987
2987
|
|
|
2988
|
+
// ── SARIF 2.1.0 output ────────────────────────────────
|
|
2989
|
+
|
|
2990
|
+
function toSarif(reports) {
|
|
2991
|
+
if (!reports || (Array.isArray(reports) && reports.length === 0)) {
|
|
2992
|
+
reports = [];
|
|
2993
|
+
}
|
|
2994
|
+
const version = getVersion();
|
|
2995
|
+
const LEVEL_MAP = { critical: 'error', high: 'error', medium: 'warning', low: 'note', info: 'note' };
|
|
2996
|
+
const SCORE_MAP = { critical: '9.5', high: '8.0', medium: '5.5', low: '2.0', info: '0.5' };
|
|
2997
|
+
const rules = [];
|
|
2998
|
+
const results = [];
|
|
2999
|
+
const ruleIndex = new Map();
|
|
3000
|
+
|
|
3001
|
+
for (const report of (Array.isArray(reports) ? reports : [reports]).filter(Boolean)) {
|
|
3002
|
+
for (const f of (report.findings || [])) {
|
|
3003
|
+
const ruleId = f.pattern_id || f.id || 'UNKNOWN';
|
|
3004
|
+
const sev = (f.severity || 'medium').toLowerCase();
|
|
3005
|
+
|
|
3006
|
+
if (!ruleIndex.has(ruleId)) {
|
|
3007
|
+
ruleIndex.set(ruleId, rules.length);
|
|
3008
|
+
const tags = ['security'];
|
|
3009
|
+
if (f.cwe_id) tags.push(f.cwe_id.toLowerCase());
|
|
3010
|
+
if (f.category) tags.push(f.category);
|
|
3011
|
+
rules.push({
|
|
3012
|
+
id: ruleId,
|
|
3013
|
+
shortDescription: { text: f.title || ruleId },
|
|
3014
|
+
fullDescription: { text: f.description || f.title || '' },
|
|
3015
|
+
helpUri: f.cwe_id
|
|
3016
|
+
? `https://cwe.mitre.org/data/definitions/${f.cwe_id.replace('CWE-', '')}.html`
|
|
3017
|
+
: `https://agentaudit.dev`,
|
|
3018
|
+
defaultConfiguration: { level: LEVEL_MAP[sev] || 'warning' },
|
|
3019
|
+
properties: { 'security-severity': SCORE_MAP[sev] || '5.5', tags },
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
const result = {
|
|
3024
|
+
ruleId,
|
|
3025
|
+
ruleIndex: ruleIndex.get(ruleId),
|
|
3026
|
+
level: LEVEL_MAP[sev] || 'warning',
|
|
3027
|
+
message: { text: [f.title, f.description].filter(Boolean).join(': ') },
|
|
3028
|
+
locations: [],
|
|
3029
|
+
};
|
|
3030
|
+
|
|
3031
|
+
const filePath = f.file || f.file_path;
|
|
3032
|
+
const lineNum = f.line || f.line_start;
|
|
3033
|
+
if (filePath) {
|
|
3034
|
+
const loc = {
|
|
3035
|
+
physicalLocation: {
|
|
3036
|
+
artifactLocation: { uri: filePath, uriBaseId: '%SRCROOT%' },
|
|
3037
|
+
},
|
|
3038
|
+
};
|
|
3039
|
+
if (lineNum) {
|
|
3040
|
+
loc.physicalLocation.region = { startLine: lineNum };
|
|
3041
|
+
}
|
|
3042
|
+
const snippet = f.content || f.snippet || f.code_snippet;
|
|
3043
|
+
if (snippet) {
|
|
3044
|
+
loc.physicalLocation.region = loc.physicalLocation.region || {};
|
|
3045
|
+
loc.physicalLocation.region.snippet = { text: snippet };
|
|
3046
|
+
}
|
|
3047
|
+
result.locations.push(loc);
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
if (f.remediation) {
|
|
3051
|
+
result.fixes = [{ description: { text: f.remediation } }];
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
if (f.by_design) {
|
|
3055
|
+
result.suppressions = [{ kind: 'inSource', justification: 'Marked as by-design' }];
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
if (filePath && lineNum) {
|
|
3059
|
+
const hash = crypto.createHash('sha256')
|
|
3060
|
+
.update(`${ruleId}:${filePath}:${lineNum}`)
|
|
3061
|
+
.digest('hex').slice(0, 16);
|
|
3062
|
+
result.partialFingerprints = { primaryLocationLineHash: hash };
|
|
3063
|
+
} else {
|
|
3064
|
+
// Fallback fingerprint from rule + title for findings without file/line
|
|
3065
|
+
const hash = crypto.createHash('sha256')
|
|
3066
|
+
.update(`${ruleId}:${f.title || ''}`)
|
|
3067
|
+
.digest('hex').slice(0, 16);
|
|
3068
|
+
result.partialFingerprints = { primaryLocationLineHash: hash };
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
results.push(result);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
return {
|
|
3076
|
+
version: '2.1.0',
|
|
3077
|
+
$schema: 'https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json',
|
|
3078
|
+
runs: [{
|
|
3079
|
+
tool: {
|
|
3080
|
+
driver: {
|
|
3081
|
+
name: 'AgentAudit',
|
|
3082
|
+
semanticVersion: version,
|
|
3083
|
+
informationUri: 'https://agentaudit.dev',
|
|
3084
|
+
rules,
|
|
3085
|
+
},
|
|
3086
|
+
},
|
|
3087
|
+
results,
|
|
3088
|
+
}],
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
|
|
2988
3092
|
async function auditRepo(url) {
|
|
3093
|
+
// In quiet mode (SARIF/JSON), redirect all progress output to stderr
|
|
3094
|
+
// so stdout only contains clean machine-readable data
|
|
3095
|
+
const _origConsoleLog = console.log;
|
|
3096
|
+
const _origStdoutWrite = process.stdout.write;
|
|
3097
|
+
if (quietMode) {
|
|
3098
|
+
console.log = console.error;
|
|
3099
|
+
process.stdout.write = process.stderr.write.bind(process.stderr);
|
|
3100
|
+
}
|
|
3101
|
+
try {
|
|
2989
3102
|
const start = Date.now();
|
|
2990
3103
|
|
|
2991
3104
|
// Support local directories
|
|
@@ -3283,15 +3396,52 @@ async function auditRepo(url) {
|
|
|
3283
3396
|
}
|
|
3284
3397
|
|
|
3285
3398
|
if (!activeLlm) {
|
|
3399
|
+
// Check if user is logged in — offer remote scan as fallback
|
|
3400
|
+
const _creds = loadCredentials();
|
|
3401
|
+
if (_creds && process.stdin.isTTY && !process.argv.includes('--export')) {
|
|
3402
|
+
console.log();
|
|
3403
|
+
console.log(` ${c.yellow}No LLM API key configured.${c.reset}`);
|
|
3404
|
+
console.log();
|
|
3405
|
+
// Fetch quota for display
|
|
3406
|
+
let quotaLabel = '3/day free';
|
|
3407
|
+
try {
|
|
3408
|
+
const qr = await fetch(`${REGISTRY_URL}/api/scan`, {
|
|
3409
|
+
headers: { 'Authorization': `Bearer ${_creds.api_key}` },
|
|
3410
|
+
signal: AbortSignal.timeout(5_000),
|
|
3411
|
+
});
|
|
3412
|
+
if (qr.ok) {
|
|
3413
|
+
const q = await qr.json();
|
|
3414
|
+
quotaLabel = `${q.remaining}/${q.limit} free remaining`;
|
|
3415
|
+
}
|
|
3416
|
+
} catch {}
|
|
3417
|
+
console.log(` ${c.cyan}1${c.reset} Use agentaudit.dev ${c.dim}(${quotaLabel})${c.reset}`);
|
|
3418
|
+
console.log(` ${c.cyan}2${c.reset} Configure local LLM ${c.dim}(agentaudit model)${c.reset}`);
|
|
3419
|
+
console.log();
|
|
3420
|
+
const _choice = await askQuestion(` Choice ${c.dim}(1/2, default: 1):${c.reset} `);
|
|
3421
|
+
console.log();
|
|
3422
|
+
if (_choice.trim() === '2') {
|
|
3423
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit model${c.dim} to configure your LLM provider and API key.${c.reset}`);
|
|
3424
|
+
console.log();
|
|
3425
|
+
return null;
|
|
3426
|
+
}
|
|
3427
|
+
// Default: remote audit
|
|
3428
|
+
return await remoteAudit(url);
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// Not logged in or non-interactive
|
|
3286
3432
|
console.log();
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3433
|
+
if (!_creds) {
|
|
3434
|
+
console.log(` ${c.yellow}No LLM API key found.${c.reset} To run a deep audit, you need either:`);
|
|
3435
|
+
console.log();
|
|
3436
|
+
console.log(` ${c.bold}1.${c.reset} An LLM API key: ${c.cyan}agentaudit model${c.reset}`);
|
|
3437
|
+
console.log(` ${c.bold}2.${c.reset} A free account: ${c.cyan}agentaudit login${c.reset} ${c.dim}(3 free remote scans/day)${c.reset}`);
|
|
3438
|
+
} else {
|
|
3439
|
+
console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
|
|
3440
|
+
console.log();
|
|
3441
|
+
console.log(` ${c.bold}Set an API key${c.reset} (e.g. ${c.cyan}export OPENROUTER_API_KEY=sk-or-...${c.reset})`);
|
|
3442
|
+
console.log(` ${c.dim}Run "agentaudit model" to configure provider + model interactively${c.reset}`);
|
|
3443
|
+
console.log(` ${c.dim}Or use ${c.cyan}agentaudit audit ${url} --remote${c.dim} for a free server-side scan${c.reset}`);
|
|
3444
|
+
}
|
|
3295
3445
|
console.log();
|
|
3296
3446
|
if (process.argv.includes('--export')) {
|
|
3297
3447
|
const exportPath = path.join(process.cwd(), `audit-${slug}.md`);
|
|
@@ -3409,6 +3559,215 @@ async function auditRepo(url) {
|
|
|
3409
3559
|
|
|
3410
3560
|
console.log();
|
|
3411
3561
|
return report;
|
|
3562
|
+
|
|
3563
|
+
} finally {
|
|
3564
|
+
console.log = _origConsoleLog;
|
|
3565
|
+
process.stdout.write = _origStdoutWrite;
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
// ── Remote Audit (server-side free scan via SSE) ────────
|
|
3570
|
+
|
|
3571
|
+
async function remoteAudit(url) {
|
|
3572
|
+
// 1. Check credentials
|
|
3573
|
+
const creds = loadCredentials();
|
|
3574
|
+
if (!creds) {
|
|
3575
|
+
console.log();
|
|
3576
|
+
console.log(` ${c.red}Not logged in.${c.reset} Remote scans require an agentaudit.dev account.`);
|
|
3577
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit login${c.dim} to sign in (free).${c.reset}`);
|
|
3578
|
+
console.log();
|
|
3579
|
+
return null;
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
const authHeaders = { 'Authorization': `Bearer ${creds.api_key}`, 'Content-Type': 'application/json' };
|
|
3583
|
+
|
|
3584
|
+
// 2. Check quota
|
|
3585
|
+
if (!quietMode) {
|
|
3586
|
+
try {
|
|
3587
|
+
const quotaRes = await fetch(`${REGISTRY_URL}/api/scan`, {
|
|
3588
|
+
headers: authHeaders,
|
|
3589
|
+
signal: AbortSignal.timeout(10_000),
|
|
3590
|
+
});
|
|
3591
|
+
if (quotaRes.ok) {
|
|
3592
|
+
const quota = await quotaRes.json();
|
|
3593
|
+
if (quota.remaining <= 0) {
|
|
3594
|
+
console.log();
|
|
3595
|
+
console.log(` ${c.red}Rate limit reached${c.reset} — 0 of ${quota.limit} free remote scans remaining.`);
|
|
3596
|
+
console.log(` ${c.dim}Configure a local LLM for unlimited scans: ${c.cyan}agentaudit model${c.reset}`);
|
|
3597
|
+
console.log();
|
|
3598
|
+
return null;
|
|
3599
|
+
}
|
|
3600
|
+
console.log(` ${c.dim}Remote scans: ${quota.remaining} of ${quota.limit} remaining today${c.reset}`);
|
|
3601
|
+
}
|
|
3602
|
+
} catch {
|
|
3603
|
+
// Quota check failed — continue, the POST will catch it
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
// 3. Start SSE stream
|
|
3608
|
+
if (!quietMode) {
|
|
3609
|
+
console.log();
|
|
3610
|
+
console.log(sectionHeader('Remote Audit'));
|
|
3611
|
+
console.log(` ${c.dim}Server: ${REGISTRY_URL} • Model: Gemini 2.5 Flash${c.reset}`);
|
|
3612
|
+
console.log();
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
const startTime = Date.now();
|
|
3616
|
+
let report = null;
|
|
3617
|
+
|
|
3618
|
+
try {
|
|
3619
|
+
const res = await fetch(`${REGISTRY_URL}/api/scan`, {
|
|
3620
|
+
method: 'POST',
|
|
3621
|
+
headers: authHeaders,
|
|
3622
|
+
body: JSON.stringify({ url }),
|
|
3623
|
+
signal: AbortSignal.timeout(90_000),
|
|
3624
|
+
});
|
|
3625
|
+
|
|
3626
|
+
if (!res.ok) {
|
|
3627
|
+
let errBody;
|
|
3628
|
+
try { errBody = await res.json(); } catch { errBody = { error: `HTTP ${res.status}` }; }
|
|
3629
|
+
console.log(` ${c.red}${errBody.message || errBody.error || `Server error (${res.status})`}${c.reset}`);
|
|
3630
|
+
console.log();
|
|
3631
|
+
return null;
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
// 4. Parse SSE stream
|
|
3635
|
+
const reader = res.body.getReader();
|
|
3636
|
+
const decoder = new TextDecoder();
|
|
3637
|
+
let buffer = '';
|
|
3638
|
+
const findings = [];
|
|
3639
|
+
let currentStep = '';
|
|
3640
|
+
|
|
3641
|
+
while (true) {
|
|
3642
|
+
const { done, value } = await reader.read();
|
|
3643
|
+
if (done) break;
|
|
3644
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3645
|
+
|
|
3646
|
+
const parts = buffer.split('\n\n');
|
|
3647
|
+
buffer = parts.pop(); // keep incomplete chunk
|
|
3648
|
+
|
|
3649
|
+
for (const part of parts) {
|
|
3650
|
+
const eventMatch = part.match(/^event:\s*(.+)/m);
|
|
3651
|
+
const dataMatch = part.match(/^data:\s*(.+)/m);
|
|
3652
|
+
if (!eventMatch || !dataMatch) continue;
|
|
3653
|
+
|
|
3654
|
+
const event = eventMatch[1].trim();
|
|
3655
|
+
let data;
|
|
3656
|
+
try { data = JSON.parse(dataMatch[1]); } catch { continue; }
|
|
3657
|
+
|
|
3658
|
+
switch (event) {
|
|
3659
|
+
case 'step': {
|
|
3660
|
+
if (quietMode) break;
|
|
3661
|
+
const icon = data.status === 'done' ? `${c.green}✔${c.reset}` : `${c.cyan}◌${c.reset}`;
|
|
3662
|
+
const detail = data.detail ? ` ${c.dim}(${data.detail})${c.reset}` : '';
|
|
3663
|
+
// Clear previous line if updating same step
|
|
3664
|
+
if (currentStep && data.status === 'done') {
|
|
3665
|
+
process.stdout.write(`\r\x1b[K`);
|
|
3666
|
+
}
|
|
3667
|
+
if (data.status === 'done') {
|
|
3668
|
+
console.log(` ${icon} ${data.label}${detail}`);
|
|
3669
|
+
currentStep = '';
|
|
3670
|
+
} else {
|
|
3671
|
+
process.stdout.write(`\r ${icon} ${data.label}${detail}`);
|
|
3672
|
+
currentStep = data.label;
|
|
3673
|
+
}
|
|
3674
|
+
break;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
case 'finding': {
|
|
3678
|
+
findings.push(data);
|
|
3679
|
+
break;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
case 'cached': {
|
|
3683
|
+
if (!quietMode) {
|
|
3684
|
+
console.log(` ${c.cyan}ℹ${c.reset} Using cached result from ${c.bold}${data.scanned_ago}${c.reset}`);
|
|
3685
|
+
}
|
|
3686
|
+
break;
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
case 'result': {
|
|
3690
|
+
report = {
|
|
3691
|
+
cached: data.cached,
|
|
3692
|
+
result: data.result,
|
|
3693
|
+
risk_score: data.risk_score,
|
|
3694
|
+
trust_score: data.trust_score,
|
|
3695
|
+
findings_count: data.findings_count,
|
|
3696
|
+
max_severity: data.max_severity,
|
|
3697
|
+
slug: data.slug,
|
|
3698
|
+
url: data.url,
|
|
3699
|
+
findings: findings,
|
|
3700
|
+
audit_model: 'google/gemini-2.5-flash',
|
|
3701
|
+
audit_provider: 'agentaudit.dev',
|
|
3702
|
+
source_url: url,
|
|
3703
|
+
skill_slug: data.slug,
|
|
3704
|
+
audit_duration_ms: Date.now() - startTime,
|
|
3705
|
+
};
|
|
3706
|
+
break;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
case 'error': {
|
|
3710
|
+
if (currentStep) {
|
|
3711
|
+
process.stdout.write(`\r\x1b[K`);
|
|
3712
|
+
currentStep = '';
|
|
3713
|
+
}
|
|
3714
|
+
console.log(` ${c.red}${data.message || 'Server error'}${c.reset}`);
|
|
3715
|
+
break;
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
case 'done':
|
|
3719
|
+
break;
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
} catch (err) {
|
|
3724
|
+
if (err.name === 'TimeoutError' || err.name === 'AbortError') {
|
|
3725
|
+
console.log(` ${c.red}Timeout — server took too long to respond.${c.reset}`);
|
|
3726
|
+
} else {
|
|
3727
|
+
console.log(` ${c.red}Connection error: ${err.message}${c.reset}`);
|
|
3728
|
+
}
|
|
3729
|
+
console.log();
|
|
3730
|
+
return null;
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
if (!report) {
|
|
3734
|
+
console.log(` ${c.red}No result received from server.${c.reset}`);
|
|
3735
|
+
console.log();
|
|
3736
|
+
return null;
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
// 5. Display results
|
|
3740
|
+
if (!quietMode) {
|
|
3741
|
+
console.log();
|
|
3742
|
+
console.log(sectionHeader('Result'));
|
|
3743
|
+
console.log(` ${riskBadge(report.risk_score || 0)}`);
|
|
3744
|
+
console.log();
|
|
3745
|
+
|
|
3746
|
+
if (findings.length > 0) {
|
|
3747
|
+
console.log(sectionHeader(`Findings (${findings.length})`));
|
|
3748
|
+
console.log();
|
|
3749
|
+
for (const f of findings) {
|
|
3750
|
+
const sc = severityColor(f.severity);
|
|
3751
|
+
console.log(` ${sc}┃${c.reset} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
|
|
3752
|
+
if (f.file) console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
|
|
3753
|
+
console.log();
|
|
3754
|
+
}
|
|
3755
|
+
} else {
|
|
3756
|
+
console.log(` ${c.green}No findings — package looks clean.${c.reset}`);
|
|
3757
|
+
console.log();
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
console.log(` ${c.dim}Report: ${REGISTRY_URL}/packages/${report.slug}${c.reset}`);
|
|
3761
|
+
console.log(` ${c.dim}Duration: ${elapsed(startTime)}${c.reset}`);
|
|
3762
|
+
console.log();
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// JSON output
|
|
3766
|
+
if (jsonMode && !quietMode) {
|
|
3767
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
return report;
|
|
3412
3771
|
}
|
|
3413
3772
|
|
|
3414
3773
|
// ── Check command ───────────────────────────────────────
|
|
@@ -4286,16 +4645,32 @@ async function main() {
|
|
|
4286
4645
|
jsonMode = rawArgs.includes('--json');
|
|
4287
4646
|
quietMode = rawArgs.includes('--quiet') || rawArgs.includes('-q');
|
|
4288
4647
|
// --no-color already handled at top level for `c` object
|
|
4289
|
-
|
|
4290
|
-
// Strip global flags from args (including --model <value>)
|
|
4291
|
-
const globalFlags = new Set(['--json', '--quiet', '-q', '--no-color', '--no-upload']);
|
|
4648
|
+
|
|
4649
|
+
// Strip global flags from args (including --model <value>, --format <value>)
|
|
4650
|
+
const globalFlags = new Set(['--json', '--quiet', '-q', '--no-color', '--no-upload', '--remote']);
|
|
4292
4651
|
let args = rawArgs.filter(a => !globalFlags.has(a));
|
|
4293
4652
|
// Remove --model <value> and --models <value> pairs
|
|
4294
4653
|
const modelIdx = args.indexOf('--model');
|
|
4295
4654
|
if (modelIdx !== -1) args.splice(modelIdx, 2);
|
|
4296
4655
|
const modelsIdx = args.indexOf('--models');
|
|
4297
4656
|
if (modelsIdx !== -1) args.splice(modelsIdx, 2);
|
|
4298
|
-
|
|
4657
|
+
// Remove --format <value> pair
|
|
4658
|
+
const formatIdx = args.indexOf('--format');
|
|
4659
|
+
const formatFlag = formatIdx !== -1 ? args.splice(formatIdx, 2)[1] : null;
|
|
4660
|
+
// --json is alias for --format json
|
|
4661
|
+
const outputFormat = formatFlag || (jsonMode ? 'json' : null);
|
|
4662
|
+
// Validate --format value
|
|
4663
|
+
if (outputFormat && !['json', 'sarif'].includes(outputFormat)) {
|
|
4664
|
+
console.error(` ${c.red}Unknown format: ${outputFormat}${c.reset}`);
|
|
4665
|
+
console.error(` ${c.dim}Supported formats: json, sarif${c.reset}`);
|
|
4666
|
+
process.exitCode = 2; return;
|
|
4667
|
+
}
|
|
4668
|
+
// SARIF mode: suppress console output so only clean JSON goes to stdout
|
|
4669
|
+
if (outputFormat === 'sarif') { quietMode = true; jsonMode = true; }
|
|
4670
|
+
|
|
4671
|
+
// --remote: use server-side scan instead of local LLM
|
|
4672
|
+
const remoteFlag = rawArgs.includes('--remote');
|
|
4673
|
+
|
|
4299
4674
|
// Detect per-command --help BEFORE stripping (e.g. `agentaudit model --help`)
|
|
4300
4675
|
const wantsHelp = args.includes('--help') || args.includes('-h');
|
|
4301
4676
|
// Strip --help/-h from args for routing
|
|
@@ -4331,29 +4706,37 @@ async function main() {
|
|
|
4331
4706
|
`(command injection, eval, hardcoded secrets, path traversal, etc.)`,
|
|
4332
4707
|
``,
|
|
4333
4708
|
`${c.bold}Options:${c.reset}`,
|
|
4334
|
-
` --deep
|
|
4709
|
+
` --deep Run deep LLM audit instead (same as \`agentaudit audit\`)`,
|
|
4710
|
+
` --remote Use agentaudit.dev server for --deep (no LLM key needed)`,
|
|
4711
|
+
` --format sarif Output results as SARIF 2.1.0 (for GitHub Code Scanning)`,
|
|
4335
4712
|
``,
|
|
4336
4713
|
`${c.bold}Examples:${c.reset}`,
|
|
4337
4714
|
` agentaudit scan https://github.com/owner/repo`,
|
|
4338
4715
|
` agentaudit scan https://github.com/a/b https://github.com/c/d`,
|
|
4339
4716
|
` agentaudit scan https://github.com/owner/repo --deep`,
|
|
4717
|
+
` agentaudit scan https://github.com/owner/repo --deep --remote`,
|
|
4718
|
+
` agentaudit scan https://github.com/owner/repo --format sarif > results.sarif`,
|
|
4340
4719
|
],
|
|
4341
4720
|
audit: [
|
|
4342
4721
|
`${c.bold}agentaudit audit${c.reset} <url> [url...] [options]`,
|
|
4343
4722
|
``,
|
|
4344
|
-
`Deep LLM-powered 3-pass security audit (~30s)
|
|
4723
|
+
`Deep LLM-powered 3-pass security audit (~30s).`,
|
|
4345
4724
|
``,
|
|
4346
4725
|
`${c.bold}Options:${c.reset}`,
|
|
4726
|
+
` --remote Use agentaudit.dev server (no LLM key needed, 3/day free)`,
|
|
4347
4727
|
` --model <name> Override LLM model for this run`,
|
|
4348
4728
|
` --models <a,b,c> Multi-model audit (parallel calls, consensus comparison)`,
|
|
4349
4729
|
` --no-upload Skip uploading report to registry`,
|
|
4350
4730
|
` --export Export audit payload as markdown (for manual LLM review)`,
|
|
4731
|
+
` --format sarif Output results as SARIF 2.1.0 (for GitHub Code Scanning)`,
|
|
4351
4732
|
` --debug Show raw LLM response on parse errors`,
|
|
4352
4733
|
``,
|
|
4353
4734
|
`${c.bold}Examples:${c.reset}`,
|
|
4354
4735
|
` agentaudit audit https://github.com/owner/repo`,
|
|
4736
|
+
` agentaudit audit https://github.com/owner/repo --remote`,
|
|
4355
4737
|
` agentaudit audit https://github.com/owner/repo --model gpt-4o`,
|
|
4356
4738
|
` agentaudit audit https://github.com/owner/repo --models gemini-2.5-flash,claude-sonnet-4-20250514`,
|
|
4739
|
+
` agentaudit audit https://github.com/owner/repo --format sarif > results.sarif`,
|
|
4357
4740
|
` agentaudit audit https://github.com/owner/repo --export`,
|
|
4358
4741
|
],
|
|
4359
4742
|
lookup: [
|
|
@@ -4481,13 +4864,23 @@ async function main() {
|
|
|
4481
4864
|
` agentaudit consensus fastmcp --json`,
|
|
4482
4865
|
],
|
|
4483
4866
|
history: [
|
|
4484
|
-
`${c.bold}agentaudit history${c.reset} [
|
|
4867
|
+
`${c.bold}agentaudit history${c.reset} [show|upload] [n]`,
|
|
4485
4868
|
``,
|
|
4486
4869
|
`Show your local audit history. Results are stored in ~/.config/agentaudit/history/`,
|
|
4487
4870
|
`after every audit run. No internet connection required.`,
|
|
4488
4871
|
``,
|
|
4872
|
+
`${c.bold}Subcommands:${c.reset}`,
|
|
4873
|
+
` history List all local audits (numbered)`,
|
|
4874
|
+
` history show <n> Show full report details for entry #n`,
|
|
4875
|
+
` history upload <n> Retry upload of entry #n to agentaudit.dev`,
|
|
4876
|
+
``,
|
|
4489
4877
|
`${c.bold}Options:${c.reset}`,
|
|
4490
4878
|
` --json Machine-readable JSON output`,
|
|
4879
|
+
``,
|
|
4880
|
+
`${c.bold}Examples:${c.reset}`,
|
|
4881
|
+
` agentaudit history`,
|
|
4882
|
+
` agentaudit history show 1`,
|
|
4883
|
+
` agentaudit history upload 1`,
|
|
4491
4884
|
],
|
|
4492
4885
|
activity: [
|
|
4493
4886
|
`${c.bold}agentaudit activity${c.reset} [options]`,
|
|
@@ -4650,13 +5043,96 @@ async function main() {
|
|
|
4650
5043
|
}
|
|
4651
5044
|
if (command === 'history') {
|
|
4652
5045
|
banner();
|
|
5046
|
+
const subCmd = targets[0];
|
|
4653
5047
|
const entries = loadHistory(30);
|
|
4654
|
-
|
|
5048
|
+
|
|
5049
|
+
if (entries.length === 0 && !subCmd) {
|
|
4655
5050
|
console.log(` ${c.dim}No local audit history yet. Run ${c.cyan}agentaudit audit <url>${c.dim} to start.${c.reset}`);
|
|
4656
5051
|
console.log();
|
|
4657
5052
|
return;
|
|
4658
5053
|
}
|
|
4659
5054
|
|
|
5055
|
+
// history show <n> — show full report details
|
|
5056
|
+
if (subCmd === 'show') {
|
|
5057
|
+
const idx = parseInt(targets[1], 10) - 1;
|
|
5058
|
+
if (isNaN(idx) || idx < 0 || idx >= entries.length) {
|
|
5059
|
+
console.log(` ${c.red}Invalid index.${c.reset} Use a number from 1 to ${entries.length}.`);
|
|
5060
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit history${c.dim} to see the list.${c.reset}`);
|
|
5061
|
+
return;
|
|
5062
|
+
}
|
|
5063
|
+
const entry = entries[idx];
|
|
5064
|
+
if (jsonMode) {
|
|
5065
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
5066
|
+
return;
|
|
5067
|
+
}
|
|
5068
|
+
console.log(sectionHeader(`Report: ${entry.skill_slug || 'unknown'}`));
|
|
5069
|
+
console.log();
|
|
5070
|
+
console.log(` Source ${c.bold}${entry.source_url || '?'}${c.reset}`);
|
|
5071
|
+
console.log(` Model ${c.bold}${entry.audit_model || '?'}${c.reset} ${c.dim}(${entry.audit_provider || '?'})${c.reset}`);
|
|
5072
|
+
console.log(` Risk ${riskBadge(entry.risk_score ?? 0)}`);
|
|
5073
|
+
console.log(` Result ${entry.result || '?'}`);
|
|
5074
|
+
console.log(` Files ${entry.files_scanned || '?'} ${c.dim}Duration: ${entry.audit_duration_ms ? (entry.audit_duration_ms / 1000).toFixed(1) + 's' : '?'}${c.reset}`);
|
|
5075
|
+
console.log(` Tokens ${c.dim}in: ${entry.input_tokens || '?'} out: ${entry.output_tokens || '?'}${c.reset}`);
|
|
5076
|
+
console.log(` File ${c.dim}${entry._file}${c.reset}`);
|
|
5077
|
+
console.log();
|
|
5078
|
+
if (entry.findings && entry.findings.length > 0) {
|
|
5079
|
+
console.log(sectionHeader(`Findings (${entry.findings.length})`));
|
|
5080
|
+
console.log();
|
|
5081
|
+
for (const f of entry.findings) {
|
|
5082
|
+
const sc = severityColor(f.severity);
|
|
5083
|
+
console.log(` ${sc}┃${c.reset} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
|
|
5084
|
+
if (f.file) console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
|
|
5085
|
+
if (f.description) console.log(` ${sc}┃${c.reset} ${c.dim}${f.description.slice(0, 200)}${c.reset}`);
|
|
5086
|
+
console.log();
|
|
5087
|
+
}
|
|
5088
|
+
} else {
|
|
5089
|
+
console.log(` ${c.green}No findings.${c.reset}`);
|
|
5090
|
+
console.log();
|
|
5091
|
+
}
|
|
5092
|
+
return;
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
// history upload <n> — retry upload of a local report
|
|
5096
|
+
if (subCmd === 'upload') {
|
|
5097
|
+
const idx = parseInt(targets[1], 10) - 1;
|
|
5098
|
+
if (isNaN(idx) || idx < 0 || idx >= entries.length) {
|
|
5099
|
+
console.log(` ${c.red}Invalid index.${c.reset} Use a number from 1 to ${entries.length}.`);
|
|
5100
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit history${c.dim} to see the list.${c.reset}`);
|
|
5101
|
+
return;
|
|
5102
|
+
}
|
|
5103
|
+
const entry = entries[idx];
|
|
5104
|
+
const creds = loadCredentials();
|
|
5105
|
+
if (!creds) {
|
|
5106
|
+
console.log(` ${c.red}Not logged in.${c.reset} Run ${c.cyan}agentaudit login${c.reset} first.`);
|
|
5107
|
+
return;
|
|
5108
|
+
}
|
|
5109
|
+
process.stdout.write(` Uploading ${c.bold}${entry.skill_slug}${c.reset} (${entry.audit_model || '?'})...`);
|
|
5110
|
+
try {
|
|
5111
|
+
const reportCopy = { ...entry };
|
|
5112
|
+
delete reportCopy._file;
|
|
5113
|
+
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
5114
|
+
method: 'POST',
|
|
5115
|
+
headers: { 'Authorization': `Bearer ${creds.api_key}`, 'Content-Type': 'application/json' },
|
|
5116
|
+
body: JSON.stringify(reportCopy),
|
|
5117
|
+
signal: AbortSignal.timeout(30_000),
|
|
5118
|
+
});
|
|
5119
|
+
if (res.ok) {
|
|
5120
|
+
const data = await res.json();
|
|
5121
|
+
console.log(` ${c.green}done${c.reset} ${c.dim}(report #${data.report_id})${c.reset}`);
|
|
5122
|
+
console.log(` ${c.dim}${REGISTRY_URL}/packages/${entry.skill_slug}${c.reset}`);
|
|
5123
|
+
} else {
|
|
5124
|
+
const errBody = await res.text().catch(() => '');
|
|
5125
|
+
console.log(` ${c.red}failed (HTTP ${res.status})${c.reset}`);
|
|
5126
|
+
if (errBody) console.log(` ${c.dim}${errBody.slice(0, 300)}${c.reset}`);
|
|
5127
|
+
}
|
|
5128
|
+
} catch (e) {
|
|
5129
|
+
console.log(` ${c.red}failed: ${e.message}${c.reset}`);
|
|
5130
|
+
}
|
|
5131
|
+
console.log();
|
|
5132
|
+
return;
|
|
5133
|
+
}
|
|
5134
|
+
|
|
5135
|
+
// Default: list all entries
|
|
4660
5136
|
if (jsonMode) {
|
|
4661
5137
|
console.log(JSON.stringify(entries, null, 2));
|
|
4662
5138
|
return;
|
|
@@ -4665,7 +5141,8 @@ async function main() {
|
|
|
4665
5141
|
console.log(sectionHeader(`Local History (${entries.length})`));
|
|
4666
5142
|
console.log();
|
|
4667
5143
|
|
|
4668
|
-
for (
|
|
5144
|
+
for (let i = 0; i < entries.length; i++) {
|
|
5145
|
+
const entry = entries[i];
|
|
4669
5146
|
const slug = entry.skill_slug || 'unknown';
|
|
4670
5147
|
const risk = entry.risk_score ?? '?';
|
|
4671
5148
|
const sev = entry.max_severity || 'none';
|
|
@@ -4673,10 +5150,13 @@ async function main() {
|
|
|
4673
5150
|
const model = entry.audit_model || '?';
|
|
4674
5151
|
const fc = entry.findings?.length || 0;
|
|
4675
5152
|
const ts = entry._file?.slice(0, 10) || '';
|
|
4676
|
-
|
|
4677
|
-
console.log(` ${sc}┃${c.reset} ${c.
|
|
5153
|
+
const num = `${c.dim}${String(i + 1).padStart(2)}.${c.reset}`;
|
|
5154
|
+
console.log(` ${num} ${sc}┃${c.reset} ${c.bold}${slug.padEnd(30)}${c.reset} ${riskBadge(risk)} ${c.dim}${model}${c.reset}`);
|
|
5155
|
+
console.log(` ${sc}┃${c.reset} ${c.dim}${ts} ${fc} findings ${sev.toUpperCase()}${c.reset}`);
|
|
4678
5156
|
console.log();
|
|
4679
5157
|
}
|
|
5158
|
+
console.log(` ${c.dim}Tip: ${c.cyan}agentaudit history show <n>${c.dim} for details, ${c.cyan}history upload <n>${c.dim} to retry upload${c.reset}`);
|
|
5159
|
+
console.log();
|
|
4680
5160
|
return;
|
|
4681
5161
|
}
|
|
4682
5162
|
if (command === 'activity' || command === 'my') {
|
|
@@ -4795,6 +5275,20 @@ async function main() {
|
|
|
4795
5275
|
if (creds) {
|
|
4796
5276
|
console.log(` Account ${c.bold}${creds.agent_name}${c.reset} ${c.green}✔ logged in${c.reset}`);
|
|
4797
5277
|
console.log(` ${c.dim} Key: ${creds.api_key.slice(0, 12)}...${c.reset}`);
|
|
5278
|
+
// Remote scan quota
|
|
5279
|
+
try {
|
|
5280
|
+
const quotaRes = await fetch(`${REGISTRY_URL}/api/scan`, {
|
|
5281
|
+
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
5282
|
+
signal: AbortSignal.timeout(5_000),
|
|
5283
|
+
});
|
|
5284
|
+
if (quotaRes.ok) {
|
|
5285
|
+
const quota = await quotaRes.json();
|
|
5286
|
+
const resetLabel = quota.resets_in_ms
|
|
5287
|
+
? ` ${c.dim}(resets in ${Math.ceil(quota.resets_in_ms / 3600000)}h)${c.reset}`
|
|
5288
|
+
: '';
|
|
5289
|
+
console.log(` Remote ${c.bold}${quota.remaining}${c.reset} of ${quota.limit} free scans remaining${resetLabel}`);
|
|
5290
|
+
}
|
|
5291
|
+
} catch {}
|
|
4798
5292
|
} else {
|
|
4799
5293
|
console.log(` Account ${c.yellow}not configured${c.reset} ${c.dim}— run ${c.cyan}agentaudit setup${c.dim} to create one${c.reset}`);
|
|
4800
5294
|
}
|
|
@@ -5279,12 +5773,23 @@ async function main() {
|
|
|
5279
5773
|
return;
|
|
5280
5774
|
}
|
|
5281
5775
|
|
|
5282
|
-
// --deep redirects to audit flow
|
|
5776
|
+
// --deep redirects to audit flow (--remote supported)
|
|
5283
5777
|
if (deepFlag) {
|
|
5778
|
+
const auditFn = remoteFlag ? remoteAudit : auditRepo;
|
|
5284
5779
|
let hasFindings = false;
|
|
5780
|
+
const allReports = [];
|
|
5285
5781
|
for (const url of urls) {
|
|
5286
|
-
const report = await
|
|
5287
|
-
if (report
|
|
5782
|
+
const report = await auditFn(url);
|
|
5783
|
+
if (Array.isArray(report)) {
|
|
5784
|
+
allReports.push(...report.filter(Boolean));
|
|
5785
|
+
if (report.some(r => r?.findings?.length > 0)) hasFindings = true;
|
|
5786
|
+
} else if (report) {
|
|
5787
|
+
allReports.push(report);
|
|
5788
|
+
if (report.findings?.length > 0) hasFindings = true;
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
if (outputFormat === 'sarif') {
|
|
5792
|
+
console.log(JSON.stringify(toSarif(allReports), null, 2));
|
|
5288
5793
|
}
|
|
5289
5794
|
process.exitCode = hasFindings ? 1 : 0;
|
|
5290
5795
|
return;
|
|
@@ -5298,7 +5803,12 @@ async function main() {
|
|
|
5298
5803
|
else hadErrors = true;
|
|
5299
5804
|
}
|
|
5300
5805
|
|
|
5301
|
-
if (
|
|
5806
|
+
if (outputFormat === 'sarif') {
|
|
5807
|
+
const sarif = toSarif(results.map(r => ({
|
|
5808
|
+
findings: (r.findings || []).map(f => ({ ...f, pattern_id: f.id })),
|
|
5809
|
+
})));
|
|
5810
|
+
console.log(JSON.stringify(sarif, null, 2));
|
|
5811
|
+
} else if (jsonMode || outputFormat === 'json') {
|
|
5302
5812
|
const jsonOut = results.map(r => ({
|
|
5303
5813
|
slug: r.slug,
|
|
5304
5814
|
url: r.url,
|
|
@@ -5332,17 +5842,44 @@ async function main() {
|
|
|
5332
5842
|
process.exitCode = 2;
|
|
5333
5843
|
return;
|
|
5334
5844
|
}
|
|
5335
|
-
|
|
5845
|
+
|
|
5846
|
+
// --remote: use server-side scan
|
|
5847
|
+
if (remoteFlag) {
|
|
5848
|
+
let hasFindings = false;
|
|
5849
|
+
const allReports = [];
|
|
5850
|
+
for (const url of urls) {
|
|
5851
|
+
const result = await remoteAudit(url);
|
|
5852
|
+
if (result) {
|
|
5853
|
+
allReports.push(result);
|
|
5854
|
+
if (result.findings?.length > 0) hasFindings = true;
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
if (outputFormat === 'sarif') {
|
|
5858
|
+
console.log(JSON.stringify(toSarif(allReports), null, 2));
|
|
5859
|
+
}
|
|
5860
|
+
process.exitCode = hasFindings ? 1 : 0;
|
|
5861
|
+
return;
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5336
5864
|
let hasFindings = false;
|
|
5865
|
+
const allReports = [];
|
|
5337
5866
|
for (const url of urls) {
|
|
5338
5867
|
const result = await auditRepo(url);
|
|
5339
5868
|
// Multi-model returns array, single-model returns object
|
|
5340
5869
|
if (Array.isArray(result)) {
|
|
5870
|
+
allReports.push(...result.filter(Boolean));
|
|
5341
5871
|
if (result.some(r => r?.findings?.length > 0)) hasFindings = true;
|
|
5342
|
-
} else if (result
|
|
5343
|
-
|
|
5872
|
+
} else if (result) {
|
|
5873
|
+
allReports.push(result);
|
|
5874
|
+
if (result.findings?.length > 0) hasFindings = true;
|
|
5344
5875
|
}
|
|
5345
5876
|
}
|
|
5877
|
+
|
|
5878
|
+
if (outputFormat === 'sarif') {
|
|
5879
|
+
const sarif = toSarif(allReports);
|
|
5880
|
+
console.log(JSON.stringify(sarif, null, 2));
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5346
5883
|
process.exitCode = hasFindings ? 1 : 0;
|
|
5347
5884
|
return;
|
|
5348
5885
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentaudit",
|
|
3
|
-
"version": "3.12.
|
|
4
|
-
"description": "Security scanner for AI packages — MCP server
|
|
3
|
+
"version": "3.12.11",
|
|
4
|
+
"description": "Security scanner for AI agent packages — CLI + MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"agentaudit": "cli.mjs"
|