agentaudit 3.10.6 → 3.10.7

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