agentaudit 3.12.10 → 3.12.12
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 +226 -34
- package/package.json +1 -1
- package/prompts/audit-prompt.md +31 -1
package/cli.mjs
CHANGED
|
@@ -36,6 +36,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
36
36
|
const SKILL_DIR = path.resolve(__dirname);
|
|
37
37
|
const REGISTRY_URL = 'https://agentaudit.dev';
|
|
38
38
|
|
|
39
|
+
// ── Global error handlers — catch unhandled errors and exit cleanly ────
|
|
40
|
+
process.on('uncaughtException', (err) => {
|
|
41
|
+
process.stderr.write(`\nagentaudit: fatal error — ${err.message || err}\n`);
|
|
42
|
+
if (process.argv.includes('--debug')) process.stderr.write(`${err.stack || ''}\n`);
|
|
43
|
+
process.exit(2);
|
|
44
|
+
});
|
|
45
|
+
process.on('unhandledRejection', (reason) => {
|
|
46
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
47
|
+
process.stderr.write(`\nagentaudit: unhandled promise rejection — ${msg}\n`);
|
|
48
|
+
if (process.argv.includes('--debug') && reason instanceof Error) process.stderr.write(`${reason.stack || ''}\n`);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
39
52
|
// ── Global flags (set in main before command routing) ────
|
|
40
53
|
let jsonMode = false;
|
|
41
54
|
let quietMode = false;
|
|
@@ -367,21 +380,23 @@ function multiSelect(items, { title = 'Select items', hint = 'Space=toggle ↑
|
|
|
367
380
|
process.stdin.resume();
|
|
368
381
|
process.stdin.setEncoding('utf8');
|
|
369
382
|
|
|
383
|
+
const cleanup = () => {
|
|
384
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
385
|
+
process.stdin.pause();
|
|
386
|
+
process.stdin.removeListener('data', onData);
|
|
387
|
+
};
|
|
388
|
+
|
|
370
389
|
const onData = (key) => {
|
|
371
|
-
// Ctrl+C
|
|
390
|
+
// Ctrl+C — restore terminal state and exit cleanly
|
|
372
391
|
if (key === '\x03') {
|
|
373
|
-
|
|
374
|
-
process.stdin.pause();
|
|
375
|
-
process.stdin.removeListener('data', onData);
|
|
392
|
+
cleanup();
|
|
376
393
|
console.log();
|
|
377
|
-
process.
|
|
394
|
+
process.exit(0);
|
|
378
395
|
}
|
|
379
396
|
|
|
380
397
|
// Enter
|
|
381
398
|
if (key === '\r' || key === '\n') {
|
|
382
|
-
|
|
383
|
-
process.stdin.pause();
|
|
384
|
-
process.stdin.removeListener('data', onData);
|
|
399
|
+
cleanup();
|
|
385
400
|
resolve(items.filter((_, i) => selected.has(i)).map(i => i.value));
|
|
386
401
|
return;
|
|
387
402
|
}
|
|
@@ -1001,23 +1016,34 @@ function formatApiError(error, provider, statusCode) {
|
|
|
1001
1016
|
return null;
|
|
1002
1017
|
}
|
|
1003
1018
|
|
|
1019
|
+
/**
|
|
1020
|
+
* Validate that a parsed object looks like a valid audit report.
|
|
1021
|
+
* Must have at least: findings (array) and one of skill_slug/risk_score/result.
|
|
1022
|
+
*/
|
|
1023
|
+
function isValidReportSchema(obj) {
|
|
1024
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
1025
|
+
if (!Array.isArray(obj.findings)) return false;
|
|
1026
|
+
// Must have at least one identifying field
|
|
1027
|
+
if (!('skill_slug' in obj) && !('risk_score' in obj) && !('result' in obj)) return false;
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1004
1031
|
function extractJSON(text) {
|
|
1005
1032
|
// 1. Try parsing the entire text as JSON directly
|
|
1006
|
-
try {
|
|
1007
|
-
|
|
1033
|
+
try {
|
|
1034
|
+
const parsed = JSON.parse(text.trim());
|
|
1035
|
+
if (isValidReportSchema(parsed)) return parsed;
|
|
1036
|
+
} catch {}
|
|
1037
|
+
|
|
1008
1038
|
// 2. Strip markdown code fences — try last fence first (report is usually at the end)
|
|
1009
1039
|
const fenceMatches = [...text.matchAll(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/g)];
|
|
1010
1040
|
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
1011
|
-
try {
|
|
1041
|
+
try {
|
|
1012
1042
|
const parsed = JSON.parse(fenceMatches[i][1].trim());
|
|
1013
|
-
if (
|
|
1043
|
+
if (isValidReportSchema(parsed)) return parsed;
|
|
1014
1044
|
} catch {}
|
|
1015
1045
|
}
|
|
1016
|
-
|
|
1017
|
-
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
1018
|
-
try { return JSON.parse(fenceMatches[i][1].trim()); } catch {}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1046
|
+
|
|
1021
1047
|
// 3. Find ALL balanced top-level { ... } blocks, try each (prefer largest valid one)
|
|
1022
1048
|
const blocks = [];
|
|
1023
1049
|
let searchFrom = 0;
|
|
@@ -1045,9 +1071,12 @@ function extractJSON(text) {
|
|
|
1045
1071
|
// Try largest block first (the report JSON is usually the biggest)
|
|
1046
1072
|
blocks.sort((a, b) => b.length - a.length);
|
|
1047
1073
|
for (const block of blocks) {
|
|
1048
|
-
try {
|
|
1074
|
+
try {
|
|
1075
|
+
const parsed = JSON.parse(block);
|
|
1076
|
+
if (isValidReportSchema(parsed)) return parsed;
|
|
1077
|
+
} catch {}
|
|
1049
1078
|
}
|
|
1050
|
-
|
|
1079
|
+
|
|
1051
1080
|
return null;
|
|
1052
1081
|
}
|
|
1053
1082
|
|
|
@@ -1067,8 +1096,15 @@ const SKIP_EXTENSIONS = new Set([
|
|
|
1067
1096
|
'.dylib', '.dll', '.exe', '.bin', '.dat', '.db', '.sqlite',
|
|
1068
1097
|
]);
|
|
1069
1098
|
|
|
1070
|
-
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
|
|
1099
|
+
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }, _visitedPaths = new Set()) {
|
|
1071
1100
|
if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
|
|
1101
|
+
|
|
1102
|
+
// Symlink loop protection: resolve real path and track visited directories
|
|
1103
|
+
let realDir;
|
|
1104
|
+
try { realDir = fs.realpathSync(dir); } catch { return collected; }
|
|
1105
|
+
if (_visitedPaths.has(realDir)) return collected;
|
|
1106
|
+
_visitedPaths.add(realDir);
|
|
1107
|
+
|
|
1072
1108
|
let entries;
|
|
1073
1109
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
1074
1110
|
catch { return collected; }
|
|
@@ -1077,15 +1113,24 @@ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
|
|
|
1077
1113
|
if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
|
|
1078
1114
|
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
1079
1115
|
const fullPath = path.join(dir, entry.name);
|
|
1116
|
+
|
|
1117
|
+
// Skip symlinks that point to directories (prevent symlink traversal attacks)
|
|
1118
|
+
if (entry.isSymbolicLink()) {
|
|
1119
|
+
try {
|
|
1120
|
+
const target = fs.realpathSync(fullPath);
|
|
1121
|
+
if (fs.statSync(target).isDirectory()) continue; // skip symlinked dirs entirely
|
|
1122
|
+
} catch { continue; }
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1080
1125
|
if (entry.isDirectory()) {
|
|
1081
1126
|
// Special: scan .github/workflows/ (security-critical CI/CD files)
|
|
1082
1127
|
if (entry.name === '.github') {
|
|
1083
1128
|
const wfDir = path.join(fullPath, 'workflows');
|
|
1084
|
-
try { if (fs.statSync(wfDir).isDirectory()) collectFiles(wfDir, relPath + '/workflows', collected, totalSize); } catch {}
|
|
1129
|
+
try { if (fs.statSync(wfDir).isDirectory()) collectFiles(wfDir, relPath + '/workflows', collected, totalSize, _visitedPaths); } catch {}
|
|
1085
1130
|
continue;
|
|
1086
1131
|
}
|
|
1087
1132
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
1088
|
-
collectFiles(fullPath, relPath, collected, totalSize);
|
|
1133
|
+
collectFiles(fullPath, relPath, collected, totalSize, _visitedPaths);
|
|
1089
1134
|
} else {
|
|
1090
1135
|
const ext = path.extname(entry.name).toLowerCase();
|
|
1091
1136
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
@@ -2745,6 +2790,30 @@ function checkContextLimit(model, systemPrompt, userMessage) {
|
|
|
2745
2790
|
return null;
|
|
2746
2791
|
}
|
|
2747
2792
|
|
|
2793
|
+
/**
|
|
2794
|
+
* Safely parse JSON from a fetch response. If the response is not JSON
|
|
2795
|
+
* (e.g. HTML error page from a 502/503), returns {error: {message: ...}}
|
|
2796
|
+
* which the callLlm error handling paths already handle.
|
|
2797
|
+
*/
|
|
2798
|
+
async function safeJsonParse(res, llmConfig) {
|
|
2799
|
+
const contentType = res.headers.get('content-type') || '';
|
|
2800
|
+
// Read body as text first — we can only consume the stream once
|
|
2801
|
+
let body;
|
|
2802
|
+
try { body = await res.text(); } catch { body = ''; }
|
|
2803
|
+
|
|
2804
|
+
if (!res.ok && !contentType.includes('application/json')) {
|
|
2805
|
+
// Non-JSON error response (e.g. HTML from a proxy/gateway)
|
|
2806
|
+
const preview = body.slice(0, 200).replace(/<[^>]+>/g, '').trim();
|
|
2807
|
+
return { error: { message: `HTTP ${res.status} from ${llmConfig.provider}${preview ? ': ' + preview : ''}` } };
|
|
2808
|
+
}
|
|
2809
|
+
try {
|
|
2810
|
+
return JSON.parse(body);
|
|
2811
|
+
} catch (parseErr) {
|
|
2812
|
+
const preview = body.slice(0, 200).replace(/<[^>]+>/g, '').trim();
|
|
2813
|
+
return { error: { message: `Invalid JSON from ${llmConfig.provider} (HTTP ${res.status}): ${preview || parseErr.message}` } };
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2748
2817
|
async function callLlm(llmConfig, systemPrompt, userMessage) {
|
|
2749
2818
|
const apiKey = process.env[llmConfig.key];
|
|
2750
2819
|
if (!apiKey) return { error: `Missing API key: ${llmConfig.key}` };
|
|
@@ -2769,7 +2838,7 @@ async function callLlm(llmConfig, systemPrompt, userMessage) {
|
|
|
2769
2838
|
body: JSON.stringify({ model: llmConfig.model, max_tokens: 16384, system: systemPrompt, messages: [{ role: 'user', content: userMessage }] }),
|
|
2770
2839
|
signal: AbortSignal.timeout(180_000),
|
|
2771
2840
|
});
|
|
2772
|
-
data = await res
|
|
2841
|
+
data = await safeJsonParse(res, llmConfig);
|
|
2773
2842
|
if (data.error) {
|
|
2774
2843
|
const friendly = formatApiError(data.error, llmConfig.provider, res.status);
|
|
2775
2844
|
return { error: friendly?.text || data.error.message || JSON.stringify(data.error), hint: friendly?.hint, duration: Date.now() - start };
|
|
@@ -2789,7 +2858,10 @@ async function callLlm(llmConfig, systemPrompt, userMessage) {
|
|
|
2789
2858
|
}
|
|
2790
2859
|
return { report, text: _text, duration: Date.now() - start, truncated: data.stop_reason === 'max_tokens' };
|
|
2791
2860
|
} else if (llmConfig.type === 'gemini') {
|
|
2792
|
-
|
|
2861
|
+
// NOTE: Google's Gemini API requires the API key as a URL query parameter.
|
|
2862
|
+
// This is by design (their auth model). We never log the full URL to avoid key leakage.
|
|
2863
|
+
const geminiUrl = `${llmConfig.url}/${llmConfig.model}:generateContent?key=${apiKey}`;
|
|
2864
|
+
const res = await fetch(geminiUrl, {
|
|
2793
2865
|
method: 'POST',
|
|
2794
2866
|
headers: { 'Content-Type': 'application/json' },
|
|
2795
2867
|
body: JSON.stringify({
|
|
@@ -2799,7 +2871,7 @@ async function callLlm(llmConfig, systemPrompt, userMessage) {
|
|
|
2799
2871
|
}),
|
|
2800
2872
|
signal: AbortSignal.timeout(180_000),
|
|
2801
2873
|
});
|
|
2802
|
-
data = await res
|
|
2874
|
+
data = await safeJsonParse(res, llmConfig);
|
|
2803
2875
|
if (data.error) {
|
|
2804
2876
|
const friendly = formatApiError(data.error, llmConfig.provider, res.status);
|
|
2805
2877
|
return { error: friendly?.text || data.error.message || JSON.stringify(data.error), hint: friendly?.hint, duration: Date.now() - start };
|
|
@@ -2827,7 +2899,7 @@ async function callLlm(llmConfig, systemPrompt, userMessage) {
|
|
|
2827
2899
|
body: JSON.stringify({ model: llmConfig.model, max_tokens: 16384, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }] }),
|
|
2828
2900
|
signal: AbortSignal.timeout(180_000),
|
|
2829
2901
|
});
|
|
2830
|
-
data = await res
|
|
2902
|
+
data = await safeJsonParse(res, llmConfig);
|
|
2831
2903
|
if (data.error) {
|
|
2832
2904
|
const friendly = formatApiError(data.error, llmConfig.provider, res.status);
|
|
2833
2905
|
return { error: friendly?.text || data.error.message || JSON.stringify(data.error), hint: friendly?.hint, duration: Date.now() - start };
|
|
@@ -2919,7 +2991,23 @@ function enrichFindings(report, files, pkgInfo) {
|
|
|
2919
2991
|
report.max_severity = report.findings.length > 0 ? maxSev : 'none';
|
|
2920
2992
|
}
|
|
2921
2993
|
|
|
2994
|
+
const VALID_SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'info']);
|
|
2995
|
+
|
|
2922
2996
|
for (const finding of report.findings) {
|
|
2997
|
+
// 0. Validate & sanitize finding fields
|
|
2998
|
+
// Severity: must be one of the known values
|
|
2999
|
+
const sev = (finding.severity || '').toLowerCase();
|
|
3000
|
+
finding.severity = VALID_SEVERITIES.has(sev) ? sev : 'medium';
|
|
3001
|
+
// Line number: must be a positive integer
|
|
3002
|
+
if (finding.line != null) {
|
|
3003
|
+
const lineNum = parseInt(finding.line, 10);
|
|
3004
|
+
finding.line = (Number.isFinite(lineNum) && lineNum > 0) ? lineNum : undefined;
|
|
3005
|
+
}
|
|
3006
|
+
// File path: reject suspicious characters (null bytes, .., protocol schemes)
|
|
3007
|
+
if (finding.file && (/[\x00]|\.\.[\\/]|^[a-z]+:\/\//i.test(finding.file))) {
|
|
3008
|
+
finding.file = undefined;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
2923
3011
|
// 1. Fill cwe_id from pattern_id lookup
|
|
2924
3012
|
if (!finding.cwe_id || finding.cwe_id === '') {
|
|
2925
3013
|
const prefix = (finding.pattern_id || '').replace(/_\d+$/, '');
|
|
@@ -3648,12 +3736,19 @@ async function remoteAudit(url) {
|
|
|
3648
3736
|
|
|
3649
3737
|
for (const part of parts) {
|
|
3650
3738
|
const eventMatch = part.match(/^event:\s*(.+)/m);
|
|
3651
|
-
|
|
3652
|
-
|
|
3739
|
+
if (!eventMatch) continue;
|
|
3740
|
+
// Accumulate all data: lines per SSE spec (data fields can span multiple lines)
|
|
3741
|
+
const dataLines = [];
|
|
3742
|
+
for (const line of part.split('\n')) {
|
|
3743
|
+
const dm = line.match(/^data:\s?(.*)/);
|
|
3744
|
+
if (dm) dataLines.push(dm[1]);
|
|
3745
|
+
}
|
|
3746
|
+
if (dataLines.length === 0) continue;
|
|
3747
|
+
const dataStr = dataLines.join('\n');
|
|
3653
3748
|
|
|
3654
3749
|
const event = eventMatch[1].trim();
|
|
3655
3750
|
let data;
|
|
3656
|
-
try { data = JSON.parse(
|
|
3751
|
+
try { data = JSON.parse(dataStr); } catch { continue; }
|
|
3657
3752
|
|
|
3658
3753
|
switch (event) {
|
|
3659
3754
|
case 'step': {
|
|
@@ -4864,13 +4959,23 @@ async function main() {
|
|
|
4864
4959
|
` agentaudit consensus fastmcp --json`,
|
|
4865
4960
|
],
|
|
4866
4961
|
history: [
|
|
4867
|
-
`${c.bold}agentaudit history${c.reset} [
|
|
4962
|
+
`${c.bold}agentaudit history${c.reset} [show|upload] [n]`,
|
|
4868
4963
|
``,
|
|
4869
4964
|
`Show your local audit history. Results are stored in ~/.config/agentaudit/history/`,
|
|
4870
4965
|
`after every audit run. No internet connection required.`,
|
|
4871
4966
|
``,
|
|
4967
|
+
`${c.bold}Subcommands:${c.reset}`,
|
|
4968
|
+
` history List all local audits (numbered)`,
|
|
4969
|
+
` history show <n> Show full report details for entry #n`,
|
|
4970
|
+
` history upload <n> Retry upload of entry #n to agentaudit.dev`,
|
|
4971
|
+
``,
|
|
4872
4972
|
`${c.bold}Options:${c.reset}`,
|
|
4873
4973
|
` --json Machine-readable JSON output`,
|
|
4974
|
+
``,
|
|
4975
|
+
`${c.bold}Examples:${c.reset}`,
|
|
4976
|
+
` agentaudit history`,
|
|
4977
|
+
` agentaudit history show 1`,
|
|
4978
|
+
` agentaudit history upload 1`,
|
|
4874
4979
|
],
|
|
4875
4980
|
activity: [
|
|
4876
4981
|
`${c.bold}agentaudit activity${c.reset} [options]`,
|
|
@@ -5033,13 +5138,96 @@ async function main() {
|
|
|
5033
5138
|
}
|
|
5034
5139
|
if (command === 'history') {
|
|
5035
5140
|
banner();
|
|
5141
|
+
const subCmd = targets[0];
|
|
5036
5142
|
const entries = loadHistory(30);
|
|
5037
|
-
|
|
5143
|
+
|
|
5144
|
+
if (entries.length === 0 && !subCmd) {
|
|
5038
5145
|
console.log(` ${c.dim}No local audit history yet. Run ${c.cyan}agentaudit audit <url>${c.dim} to start.${c.reset}`);
|
|
5039
5146
|
console.log();
|
|
5040
5147
|
return;
|
|
5041
5148
|
}
|
|
5042
5149
|
|
|
5150
|
+
// history show <n> — show full report details
|
|
5151
|
+
if (subCmd === 'show') {
|
|
5152
|
+
const idx = parseInt(targets[1], 10) - 1;
|
|
5153
|
+
if (isNaN(idx) || idx < 0 || idx >= entries.length) {
|
|
5154
|
+
console.log(` ${c.red}Invalid index.${c.reset} Use a number from 1 to ${entries.length}.`);
|
|
5155
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit history${c.dim} to see the list.${c.reset}`);
|
|
5156
|
+
return;
|
|
5157
|
+
}
|
|
5158
|
+
const entry = entries[idx];
|
|
5159
|
+
if (jsonMode) {
|
|
5160
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
5161
|
+
return;
|
|
5162
|
+
}
|
|
5163
|
+
console.log(sectionHeader(`Report: ${entry.skill_slug || 'unknown'}`));
|
|
5164
|
+
console.log();
|
|
5165
|
+
console.log(` Source ${c.bold}${entry.source_url || '?'}${c.reset}`);
|
|
5166
|
+
console.log(` Model ${c.bold}${entry.audit_model || '?'}${c.reset} ${c.dim}(${entry.audit_provider || '?'})${c.reset}`);
|
|
5167
|
+
console.log(` Risk ${riskBadge(entry.risk_score ?? 0)}`);
|
|
5168
|
+
console.log(` Result ${entry.result || '?'}`);
|
|
5169
|
+
console.log(` Files ${entry.files_scanned || '?'} ${c.dim}Duration: ${entry.audit_duration_ms ? (entry.audit_duration_ms / 1000).toFixed(1) + 's' : '?'}${c.reset}`);
|
|
5170
|
+
console.log(` Tokens ${c.dim}in: ${entry.input_tokens || '?'} out: ${entry.output_tokens || '?'}${c.reset}`);
|
|
5171
|
+
console.log(` File ${c.dim}${entry._file}${c.reset}`);
|
|
5172
|
+
console.log();
|
|
5173
|
+
if (entry.findings && entry.findings.length > 0) {
|
|
5174
|
+
console.log(sectionHeader(`Findings (${entry.findings.length})`));
|
|
5175
|
+
console.log();
|
|
5176
|
+
for (const f of entry.findings) {
|
|
5177
|
+
const sc = severityColor(f.severity);
|
|
5178
|
+
console.log(` ${sc}┃${c.reset} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
|
|
5179
|
+
if (f.file) console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
|
|
5180
|
+
if (f.description) console.log(` ${sc}┃${c.reset} ${c.dim}${f.description.slice(0, 200)}${c.reset}`);
|
|
5181
|
+
console.log();
|
|
5182
|
+
}
|
|
5183
|
+
} else {
|
|
5184
|
+
console.log(` ${c.green}No findings.${c.reset}`);
|
|
5185
|
+
console.log();
|
|
5186
|
+
}
|
|
5187
|
+
return;
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
// history upload <n> — retry upload of a local report
|
|
5191
|
+
if (subCmd === 'upload') {
|
|
5192
|
+
const idx = parseInt(targets[1], 10) - 1;
|
|
5193
|
+
if (isNaN(idx) || idx < 0 || idx >= entries.length) {
|
|
5194
|
+
console.log(` ${c.red}Invalid index.${c.reset} Use a number from 1 to ${entries.length}.`);
|
|
5195
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit history${c.dim} to see the list.${c.reset}`);
|
|
5196
|
+
return;
|
|
5197
|
+
}
|
|
5198
|
+
const entry = entries[idx];
|
|
5199
|
+
const creds = loadCredentials();
|
|
5200
|
+
if (!creds) {
|
|
5201
|
+
console.log(` ${c.red}Not logged in.${c.reset} Run ${c.cyan}agentaudit login${c.reset} first.`);
|
|
5202
|
+
return;
|
|
5203
|
+
}
|
|
5204
|
+
process.stdout.write(` Uploading ${c.bold}${entry.skill_slug}${c.reset} (${entry.audit_model || '?'})...`);
|
|
5205
|
+
try {
|
|
5206
|
+
const reportCopy = { ...entry };
|
|
5207
|
+
delete reportCopy._file;
|
|
5208
|
+
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
5209
|
+
method: 'POST',
|
|
5210
|
+
headers: { 'Authorization': `Bearer ${creds.api_key}`, 'Content-Type': 'application/json' },
|
|
5211
|
+
body: JSON.stringify(reportCopy),
|
|
5212
|
+
signal: AbortSignal.timeout(30_000),
|
|
5213
|
+
});
|
|
5214
|
+
if (res.ok) {
|
|
5215
|
+
const data = await res.json();
|
|
5216
|
+
console.log(` ${c.green}done${c.reset} ${c.dim}(report #${data.report_id})${c.reset}`);
|
|
5217
|
+
console.log(` ${c.dim}${REGISTRY_URL}/packages/${entry.skill_slug}${c.reset}`);
|
|
5218
|
+
} else {
|
|
5219
|
+
const errBody = await res.text().catch(() => '');
|
|
5220
|
+
console.log(` ${c.red}failed (HTTP ${res.status})${c.reset}`);
|
|
5221
|
+
if (errBody) console.log(` ${c.dim}${errBody.slice(0, 300)}${c.reset}`);
|
|
5222
|
+
}
|
|
5223
|
+
} catch (e) {
|
|
5224
|
+
console.log(` ${c.red}failed: ${e.message}${c.reset}`);
|
|
5225
|
+
}
|
|
5226
|
+
console.log();
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
|
|
5230
|
+
// Default: list all entries
|
|
5043
5231
|
if (jsonMode) {
|
|
5044
5232
|
console.log(JSON.stringify(entries, null, 2));
|
|
5045
5233
|
return;
|
|
@@ -5048,7 +5236,8 @@ async function main() {
|
|
|
5048
5236
|
console.log(sectionHeader(`Local History (${entries.length})`));
|
|
5049
5237
|
console.log();
|
|
5050
5238
|
|
|
5051
|
-
for (
|
|
5239
|
+
for (let i = 0; i < entries.length; i++) {
|
|
5240
|
+
const entry = entries[i];
|
|
5052
5241
|
const slug = entry.skill_slug || 'unknown';
|
|
5053
5242
|
const risk = entry.risk_score ?? '?';
|
|
5054
5243
|
const sev = entry.max_severity || 'none';
|
|
@@ -5056,10 +5245,13 @@ async function main() {
|
|
|
5056
5245
|
const model = entry.audit_model || '?';
|
|
5057
5246
|
const fc = entry.findings?.length || 0;
|
|
5058
5247
|
const ts = entry._file?.slice(0, 10) || '';
|
|
5059
|
-
|
|
5060
|
-
console.log(` ${sc}┃${c.reset} ${c.
|
|
5248
|
+
const num = `${c.dim}${String(i + 1).padStart(2)}.${c.reset}`;
|
|
5249
|
+
console.log(` ${num} ${sc}┃${c.reset} ${c.bold}${slug.padEnd(30)}${c.reset} ${riskBadge(risk)} ${c.dim}${model}${c.reset}`);
|
|
5250
|
+
console.log(` ${sc}┃${c.reset} ${c.dim}${ts} ${fc} findings ${sev.toUpperCase()}${c.reset}`);
|
|
5061
5251
|
console.log();
|
|
5062
5252
|
}
|
|
5253
|
+
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}`);
|
|
5254
|
+
console.log();
|
|
5063
5255
|
return;
|
|
5064
5256
|
}
|
|
5065
5257
|
if (command === 'activity' || command === 'my') {
|
package/package.json
CHANGED
package/prompts/audit-prompt.md
CHANGED
|
@@ -237,6 +237,35 @@ A package that integrates multiple APIs requiring multiple credentials is a feat
|
|
|
237
237
|
- Test files with deliberate vulnerabilities
|
|
238
238
|
- Negation contexts ("never use eval"), install docs (`sudo apt`)
|
|
239
239
|
|
|
240
|
+
### ❌ Opt-In Features with Safety Warnings ≠ Default Vulnerabilities
|
|
241
|
+
If a feature must be EXPLICITLY enabled (via env var, config flag, CLI option) AND the naming/docs warn about risks, this is NOT a vulnerability in the default configuration.
|
|
242
|
+
```
|
|
243
|
+
❌ FALSE POSITIVE: MCP server has ENABLE_UNSAFE_SSE_TRANSPORT env var (default: unset/disabled) → NOT Critical (at most LOW/by_design)
|
|
244
|
+
❌ FALSE POSITIVE: Helm chart has useLegacyRules: false with documented "not recommended for production" → NOT a finding (defaults are safe)
|
|
245
|
+
❌ FALSE POSITIVE: Debug mode available via DEBUG=true env var → NOT a finding (operator must enable it)
|
|
246
|
+
✅ TRUE POSITIVE: SSE transport enabled by default without authentication → IS a finding (default is insecure)
|
|
247
|
+
✅ TRUE POSITIVE: Admin panel accessible without auth unless DISABLE_ADMIN=true → IS a finding (default is insecure)
|
|
248
|
+
```
|
|
249
|
+
**Key distinction:** "Vulnerable if operator explicitly opts in" (LOW/by_design) vs "Vulnerable by default" (HIGH/CRITICAL). Count the prerequisites — each explicit opt-in step REDUCES severity.
|
|
250
|
+
|
|
251
|
+
### ❌ Secure Code Patterns ≠ Injection Vulnerabilities
|
|
252
|
+
These code patterns are SECURE and must NOT be flagged:
|
|
253
|
+
```
|
|
254
|
+
❌ FALSE POSITIVE: execFileSync("kubectl", cmdArgs) where cmdArgs is an array → NOT shell injection (array args bypass shell)
|
|
255
|
+
❌ FALSE POSITIVE: execFile(command, [arg1, arg2]) → NOT command injection (no shell interpolation)
|
|
256
|
+
❌ FALSE POSITIVE: subprocess.run(["git", "clone", url]) → NOT injection (list form, no shell=True)
|
|
257
|
+
✅ TRUE POSITIVE: exec(`kubectl ${userInput}`) → IS command injection (string concatenation with shell)
|
|
258
|
+
✅ TRUE POSITIVE: execSync("git clone " + url) → IS command injection (string concatenation)
|
|
259
|
+
```
|
|
260
|
+
**Key distinction:** Array-based process spawning (`execFile`/`execFileSync` with args array, `subprocess.run` with list) does NOT use a shell and CANNOT be injected. Only string-based execution (`exec`, `execSync`, `shell=True`) is vulnerable.
|
|
261
|
+
|
|
262
|
+
### ❌ Never Fabricate Code That Doesn't Exist
|
|
263
|
+
If you cannot find the EXACT code pattern in the provided source files, do NOT report it. Specifically:
|
|
264
|
+
- Do NOT invent HTTP headers (e.g., `Access-Control-Allow-Origin: *`) that are not in the source code
|
|
265
|
+
- Do NOT assume a file contains code based on its name — VERIFY by reading it
|
|
266
|
+
- Do NOT report line numbers you haven't verified against actual file content
|
|
267
|
+
- If a vulnerability would exist in a dependency (e.g., Express defaults, MCP SDK) but NOT in the scanned package's code, it is NOT a finding for this package
|
|
268
|
+
|
|
240
269
|
## 3.3 Core-Functionality-Exemption (Hard Rule)
|
|
241
270
|
|
|
242
271
|
If the pattern is in the Package Profile's "Expected Behaviors" list:
|
|
@@ -272,8 +301,9 @@ For each candidate finding, evaluate:
|
|
|
272
301
|
- **None** (requires code modification) → likely NOT a finding
|
|
273
302
|
|
|
274
303
|
### Attack Complexity
|
|
275
|
-
- **Low**: No special conditions, works out of the box
|
|
304
|
+
- **Low**: No special conditions, works out of the box with default configuration
|
|
276
305
|
- **High**: Requires specific config, race conditions, chained exploits → cap at MEDIUM unless catastrophic impact
|
|
306
|
+
- **Opt-in required**: Vulnerability only exists if operator explicitly enables a feature (env var, config flag) → cap at LOW. Each required opt-in step reduces severity by one level.
|
|
277
307
|
|
|
278
308
|
### Privileges & Interaction Required
|
|
279
309
|
- More prerequisites → lower realistic severity
|