agentaudit 3.12.9 → 3.12.10

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/cli.mjs +462 -22
  3. 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 + CLI**
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
- console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
3288
- console.log();
3289
- console.log(` ${c.bold}Set an API key${c.reset} (e.g. ${c.cyan}export OPENROUTER_API_KEY=sk-or-...${c.reset})`);
3290
- console.log(` ${c.dim}Run "agentaudit model" to configure provider + model interactively${c.reset}`);
3291
- console.log();
3292
- console.log(` ${c.bold}Or export for manual review:${c.reset} ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
3293
- console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed)${c.reset}`);
3294
- console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
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 Run deep LLM audit instead (same as \`agentaudit audit\`)`,
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). Requires an LLM API key.`,
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: [
@@ -4795,6 +5178,20 @@ async function main() {
4795
5178
  if (creds) {
4796
5179
  console.log(` Account ${c.bold}${creds.agent_name}${c.reset} ${c.green}✔ logged in${c.reset}`);
4797
5180
  console.log(` ${c.dim} Key: ${creds.api_key.slice(0, 12)}...${c.reset}`);
5181
+ // Remote scan quota
5182
+ try {
5183
+ const quotaRes = await fetch(`${REGISTRY_URL}/api/scan`, {
5184
+ headers: { 'Authorization': `Bearer ${creds.api_key}` },
5185
+ signal: AbortSignal.timeout(5_000),
5186
+ });
5187
+ if (quotaRes.ok) {
5188
+ const quota = await quotaRes.json();
5189
+ const resetLabel = quota.resets_in_ms
5190
+ ? ` ${c.dim}(resets in ${Math.ceil(quota.resets_in_ms / 3600000)}h)${c.reset}`
5191
+ : '';
5192
+ console.log(` Remote ${c.bold}${quota.remaining}${c.reset} of ${quota.limit} free scans remaining${resetLabel}`);
5193
+ }
5194
+ } catch {}
4798
5195
  } else {
4799
5196
  console.log(` Account ${c.yellow}not configured${c.reset} ${c.dim}— run ${c.cyan}agentaudit setup${c.dim} to create one${c.reset}`);
4800
5197
  }
@@ -5279,12 +5676,23 @@ async function main() {
5279
5676
  return;
5280
5677
  }
5281
5678
 
5282
- // --deep redirects to audit flow
5679
+ // --deep redirects to audit flow (--remote supported)
5283
5680
  if (deepFlag) {
5681
+ const auditFn = remoteFlag ? remoteAudit : auditRepo;
5284
5682
  let hasFindings = false;
5683
+ const allReports = [];
5285
5684
  for (const url of urls) {
5286
- const report = await auditRepo(url);
5287
- if (report?.findings?.length > 0) hasFindings = true;
5685
+ const report = await auditFn(url);
5686
+ if (Array.isArray(report)) {
5687
+ allReports.push(...report.filter(Boolean));
5688
+ if (report.some(r => r?.findings?.length > 0)) hasFindings = true;
5689
+ } else if (report) {
5690
+ allReports.push(report);
5691
+ if (report.findings?.length > 0) hasFindings = true;
5692
+ }
5693
+ }
5694
+ if (outputFormat === 'sarif') {
5695
+ console.log(JSON.stringify(toSarif(allReports), null, 2));
5288
5696
  }
5289
5697
  process.exitCode = hasFindings ? 1 : 0;
5290
5698
  return;
@@ -5298,7 +5706,12 @@ async function main() {
5298
5706
  else hadErrors = true;
5299
5707
  }
5300
5708
 
5301
- if (jsonMode) {
5709
+ if (outputFormat === 'sarif') {
5710
+ const sarif = toSarif(results.map(r => ({
5711
+ findings: (r.findings || []).map(f => ({ ...f, pattern_id: f.id })),
5712
+ })));
5713
+ console.log(JSON.stringify(sarif, null, 2));
5714
+ } else if (jsonMode || outputFormat === 'json') {
5302
5715
  const jsonOut = results.map(r => ({
5303
5716
  slug: r.slug,
5304
5717
  url: r.url,
@@ -5332,17 +5745,44 @@ async function main() {
5332
5745
  process.exitCode = 2;
5333
5746
  return;
5334
5747
  }
5335
-
5748
+
5749
+ // --remote: use server-side scan
5750
+ if (remoteFlag) {
5751
+ let hasFindings = false;
5752
+ const allReports = [];
5753
+ for (const url of urls) {
5754
+ const result = await remoteAudit(url);
5755
+ if (result) {
5756
+ allReports.push(result);
5757
+ if (result.findings?.length > 0) hasFindings = true;
5758
+ }
5759
+ }
5760
+ if (outputFormat === 'sarif') {
5761
+ console.log(JSON.stringify(toSarif(allReports), null, 2));
5762
+ }
5763
+ process.exitCode = hasFindings ? 1 : 0;
5764
+ return;
5765
+ }
5766
+
5336
5767
  let hasFindings = false;
5768
+ const allReports = [];
5337
5769
  for (const url of urls) {
5338
5770
  const result = await auditRepo(url);
5339
5771
  // Multi-model returns array, single-model returns object
5340
5772
  if (Array.isArray(result)) {
5773
+ allReports.push(...result.filter(Boolean));
5341
5774
  if (result.some(r => r?.findings?.length > 0)) hasFindings = true;
5342
- } else if (result?.findings?.length > 0) {
5343
- hasFindings = true;
5775
+ } else if (result) {
5776
+ allReports.push(result);
5777
+ if (result.findings?.length > 0) hasFindings = true;
5344
5778
  }
5345
5779
  }
5780
+
5781
+ if (outputFormat === 'sarif') {
5782
+ const sarif = toSarif(allReports);
5783
+ console.log(JSON.stringify(sarif, null, 2));
5784
+ }
5785
+
5346
5786
  process.exitCode = hasFindings ? 1 : 0;
5347
5787
  return;
5348
5788
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.12.9",
4
- "description": "Security scanner for AI packages — MCP server + CLI",
3
+ "version": "3.12.10",
4
+ "description": "Security scanner for AI agent packages — CLI + MCP server",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agentaudit": "cli.mjs"