agentaudit 3.9.40 → 3.9.42
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 +53 -21
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -525,12 +525,20 @@ function extractJSON(text) {
|
|
|
525
525
|
|
|
526
526
|
const MAX_FILE_SIZE = 50_000;
|
|
527
527
|
const MAX_TOTAL_SIZE = 300_000;
|
|
528
|
+
// Directories safe to skip: dependencies, caches, build artifacts, editor config.
|
|
529
|
+
// SECURITY RULE: If a directory can contain source code, workflow files, or
|
|
530
|
+
// prose (prompt injection), it MUST be scanned.
|
|
531
|
+
// Reviewed 2026-02-17:
|
|
532
|
+
// - RESTORED: test/tests/__tests__/spec/specs/e2e (malware in test hooks)
|
|
533
|
+
// - RESTORED: .github (workflow injection, supply chain attacks)
|
|
534
|
+
// - RESTORED: examples/example (hidden backdoors in "example" code)
|
|
535
|
+
// - RESTORED: docs/doc (prompt injection in documentation)
|
|
536
|
+
// - RESTORED: fixtures (test data can contain malicious payloads)
|
|
528
537
|
const SKIP_DIRS = new Set([
|
|
529
|
-
'node_modules', '.git', '__pycache__', '.venv', 'venv', '
|
|
530
|
-
'
|
|
531
|
-
'
|
|
532
|
-
'
|
|
533
|
-
'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
|
|
538
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'vendor',
|
|
539
|
+
'dist', 'build', '.next', '.nuxt',
|
|
540
|
+
'coverage', '.pytest_cache', '.mypy_cache', '.tox', '.eggs', 'htmlcov',
|
|
541
|
+
'.vscode', '.idea',
|
|
534
542
|
]);
|
|
535
543
|
const SKIP_EXTENSIONS = new Set([
|
|
536
544
|
'.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff',
|
|
@@ -539,31 +547,33 @@ const SKIP_EXTENSIONS = new Set([
|
|
|
539
547
|
'.dylib', '.dll', '.exe', '.bin', '.dat', '.db', '.sqlite',
|
|
540
548
|
'.snap', '.patch', '.diff', '.log', '.csv', '.tsv', '.parquet',
|
|
541
549
|
]);
|
|
542
|
-
// Files
|
|
543
|
-
//
|
|
544
|
-
//
|
|
550
|
+
// Files safe to skip: ONLY inert line-based config that cannot execute code
|
|
551
|
+
// and cannot contain prompt injections (no prose, no markdown, no scripts).
|
|
552
|
+
// SECURITY RULE: When in doubt, SCAN IT. False positives > false negatives.
|
|
553
|
+
// Reviewed 2026-02-17: All .md files removed from skip list (prompt injection vector).
|
|
554
|
+
// All executable configs removed (malware vector). Only line-based dotfiles remain.
|
|
545
555
|
const SKIP_FILES = new Set([
|
|
546
|
-
'license', 'license.md', 'license.txt', 'licence', 'licence.md',
|
|
547
|
-
'changelog.md', 'changelog', 'changes.md', 'history.md',
|
|
548
|
-
'contributing.md', 'contributors.md', 'authors', 'authors.md',
|
|
549
|
-
'code_of_conduct.md', 'security.md', 'funding.yml',
|
|
550
556
|
'.gitignore', '.gitattributes', '.npmignore', '.dockerignore',
|
|
551
557
|
'.editorconfig', '.browserslistrc', '.nvmrc', '.node-version',
|
|
552
558
|
'.prettierignore', '.eslintignore',
|
|
553
559
|
]);
|
|
554
560
|
|
|
555
|
-
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
|
|
556
|
-
if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
|
|
561
|
+
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0, truncated: false, skippedPaths: [] }) {
|
|
562
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) { totalSize.truncated = true; return collected; }
|
|
557
563
|
let entries;
|
|
558
564
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
559
565
|
catch { return collected; }
|
|
560
566
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
561
567
|
for (const entry of entries) {
|
|
562
|
-
if (totalSize.bytes >= MAX_TOTAL_SIZE)
|
|
568
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) { totalSize.truncated = true; totalSize.skippedPaths.push(relPath); continue; }
|
|
563
569
|
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
564
570
|
const fullPath = path.join(dir, entry.name);
|
|
571
|
+
// SECURITY: Never follow symlinks — attacker could link to /etc/passwd or ~/.ssh/
|
|
572
|
+
if (entry.isSymbolicLink()) continue;
|
|
565
573
|
if (entry.isDirectory()) {
|
|
566
|
-
|
|
574
|
+
// Allow .github (workflow security), skip other dot-dirs (editor/system config)
|
|
575
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
576
|
+
if (entry.name.startsWith('.') && entry.name !== '.github') continue;
|
|
567
577
|
collectFiles(fullPath, relPath, collected, totalSize);
|
|
568
578
|
} else {
|
|
569
579
|
const ext = path.extname(entry.name).toLowerCase();
|
|
@@ -1451,8 +1461,15 @@ async function auditRepo(url) {
|
|
|
1451
1461
|
|
|
1452
1462
|
// Step 2: Collect files
|
|
1453
1463
|
process.stdout.write(` ${c.dim}[2/4]${c.reset} Collecting source files...`);
|
|
1454
|
-
const
|
|
1464
|
+
const _collectMeta = { bytes: 0, truncated: false, skippedPaths: [] };
|
|
1465
|
+
const files = collectFiles(repoPath, '', [], _collectMeta);
|
|
1455
1466
|
console.log(` ${c.green}${files.length} files${c.reset}`);
|
|
1467
|
+
if (_collectMeta.truncated) {
|
|
1468
|
+
console.log(` ${c.yellow}⚠ Size limit reached (${(MAX_TOTAL_SIZE / 1000).toFixed(0)}KB) — ${_collectMeta.skippedPaths.length} files NOT collected:${c.reset}`);
|
|
1469
|
+
const shown = _collectMeta.skippedPaths.slice(0, 5);
|
|
1470
|
+
for (const p of shown) console.log(` ${c.dim} • ${p}${c.reset}`);
|
|
1471
|
+
if (_collectMeta.skippedPaths.length > 5) console.log(` ${c.dim} ... and ${_collectMeta.skippedPaths.length - 5} more${c.reset}`);
|
|
1472
|
+
}
|
|
1456
1473
|
|
|
1457
1474
|
// Step 3: Build audit payload
|
|
1458
1475
|
process.stdout.write(` ${c.dim}[3/4]${c.reset} Preparing audit payload...`);
|
|
@@ -1463,13 +1480,24 @@ async function auditRepo(url) {
|
|
|
1463
1480
|
// ~15k tokens per chunk for code → fits comfortably in 32k+ context models
|
|
1464
1481
|
// with room for system prompt (~2k tokens) + output (4k tokens)
|
|
1465
1482
|
const MAX_CHUNK_CHARS = 60_000;
|
|
1483
|
+
// Sort files by directory to keep related files in the same chunk.
|
|
1484
|
+
// This preserves cross-file context (imports, shared modules) within each pass.
|
|
1485
|
+
const sortedFiles = [...files].sort((a, b) => {
|
|
1486
|
+
const dirA = a.path.includes('/') ? a.path.substring(0, a.path.lastIndexOf('/')) : '';
|
|
1487
|
+
const dirB = b.path.includes('/') ? b.path.substring(0, b.path.lastIndexOf('/')) : '';
|
|
1488
|
+
return dirA.localeCompare(dirB) || a.path.localeCompare(b.path);
|
|
1489
|
+
});
|
|
1466
1490
|
const chunks = []; // array of code block strings
|
|
1491
|
+
const chunkFileNames = []; // track which files are in each chunk for error reporting
|
|
1467
1492
|
let currentChunk = '';
|
|
1468
1493
|
let currentChars = 0;
|
|
1469
|
-
|
|
1494
|
+
let currentFiles = [];
|
|
1495
|
+
for (const file of sortedFiles) {
|
|
1470
1496
|
const entry = `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
1471
1497
|
if (currentChars + entry.length > MAX_CHUNK_CHARS && currentChars > 0) {
|
|
1472
1498
|
chunks.push(currentChunk);
|
|
1499
|
+
chunkFileNames.push([...currentFiles]);
|
|
1500
|
+
currentFiles = [];
|
|
1473
1501
|
currentChunk = '';
|
|
1474
1502
|
currentChars = 0;
|
|
1475
1503
|
}
|
|
@@ -1482,8 +1510,9 @@ async function auditRepo(url) {
|
|
|
1482
1510
|
currentChunk += entry;
|
|
1483
1511
|
currentChars += entry.length;
|
|
1484
1512
|
}
|
|
1513
|
+
currentFiles.push(file.path);
|
|
1485
1514
|
}
|
|
1486
|
-
if (currentChunk) chunks.push(currentChunk);
|
|
1515
|
+
if (currentChunk) { chunks.push(currentChunk); chunkFileNames.push([...currentFiles]); }
|
|
1487
1516
|
|
|
1488
1517
|
const needsMultiPass = chunks.length > 1;
|
|
1489
1518
|
if (needsMultiPass) {
|
|
@@ -1604,7 +1633,7 @@ async function auditRepo(url) {
|
|
|
1604
1633
|
const modelInfo = modelData.data?.find(m => m.id === actualModel);
|
|
1605
1634
|
if (modelInfo?.context_length) {
|
|
1606
1635
|
const ctx = modelInfo.context_length;
|
|
1607
|
-
outputTokenBudget = ctx >= 128_000 ? 8192 : ctx >= 64_000 ? 4096 : ctx >= 32_000 ? 2048 :
|
|
1636
|
+
outputTokenBudget = ctx >= 128_000 ? 8192 : ctx >= 64_000 ? 4096 : ctx >= 32_000 ? 2048 : 2048;
|
|
1608
1637
|
if (process.argv.includes('--debug')) {
|
|
1609
1638
|
console.log(` ${c.dim} Model context: ${ctx.toLocaleString()} tokens → max_tokens: ${outputTokenBudget}${c.reset}`);
|
|
1610
1639
|
}
|
|
@@ -1744,8 +1773,11 @@ async function auditRepo(url) {
|
|
|
1744
1773
|
const result = await callLLM(chunks[i], `pass ${i + 1}`);
|
|
1745
1774
|
|
|
1746
1775
|
if (result.error) {
|
|
1776
|
+
const failedFiles = chunkFileNames[i] || [];
|
|
1747
1777
|
console.log(` ${c.red}failed${c.reset} ${c.dim}(${result.error.slice(0, 80)})${c.reset}`);
|
|
1748
|
-
|
|
1778
|
+
if (failedFiles.length > 0) {
|
|
1779
|
+
console.log(` ${c.yellow}⚠ ${failedFiles.length} files NOT analyzed:${c.reset} ${c.dim}${failedFiles.slice(0, 5).join(', ')}${failedFiles.length > 5 ? ` (+${failedFiles.length - 5} more)` : ''}${c.reset}`);
|
|
1780
|
+
}
|
|
1749
1781
|
continue;
|
|
1750
1782
|
}
|
|
1751
1783
|
|