@lhi/tdd-audit 1.16.0 → 1.18.0

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.
@@ -1,6 +1,104 @@
1
1
  # Vulnerability Patterns Reference
2
2
 
3
- All 57 patterns detected by `@lhi/tdd-audit` across 6 scanner modules. Source patterns are checked against `.js`, `.ts`, `.jsx`, `.tsx`, `.mjs`, `.py`, `.go`, `.dart`, `.yml`, and `.yaml` files line-by-line. Prompt/skill patterns are checked separately against `.md` files in agent configuration directories. Supply-chain patterns check `package.json`. NEXT_PUBLIC secret patterns also check `.env*` files.
3
+ All 79 patterns detected by `@lhi/tdd-audit` across 6 scanner modules. Source patterns are checked against `.js`, `.ts`, `.jsx`, `.tsx`, `.mjs`, `.py`, `.go`, `.dart`, `.yml`, and `.yaml` files line-by-line. **All `.md` files in the repo are also scanned** for AI/LLM vulnerabilities, prompt injection, skill anti-patterns, hidden unicode, and hardcoded keys (see Phase 0d). Supply-chain patterns check `package.json`. NEXT_PUBLIC secret patterns also check `.env*` files.
4
+
5
+ ---
6
+
7
+ ## Custom Patterns & Extensibility
8
+
9
+ `tdd-audit` is designed to be wrapped and rebranded by organizations that want to maintain their own pattern databases, plug in org-specific MCP services, or distribute a pre-configured version to their teams.
10
+
11
+ All extensibility is controlled by a single `.tdd-audit.json` file at the repo root. The CLI tool and the Claude Code skill both read this file at startup. CLI flags override file config; file config overrides built-in defaults.
12
+
13
+ ### `.tdd-audit.json` extensibility fields
14
+
15
+ ```jsonc
16
+ {
17
+ // ── Identity & branding ────────────────────────────────────────────────────
18
+ // Appears in reports, SECURITY.md, commit messages, and the README badge.
19
+ "org": "Daily Caller",
20
+ "project": "beltway-events",
21
+
22
+ // Badge & SARIF link — replaces the default @lhi/tdd-audit npm page.
23
+ // Set this to your internal tool URL, your org's security portal, or your
24
+ // fork of tdd-audit. Controls badge href, SARIF informationUri, and the
25
+ // "Run /tdd-audit to remediate" footer in scan output.
26
+ "tdd_site": "https://security.dailycaller.com",
27
+
28
+ // Badge label text — replaces "tdd-audit" in the shields.io badge.
29
+ // Useful when distributing a white-labeled or rebranded version.
30
+ "badge_label": "dc-audit",
31
+
32
+ // ── Pattern repositories ────────────────────────────────────────────────────
33
+ // Each repo is cloned/pulled at audit startup and RAG-indexed.
34
+ // Agents query the index before proposing any fix — prior solutions surface first.
35
+ // Only genuinely new patterns are contributed back; existing ones are updated.
36
+ "pattern_repos": [
37
+ {
38
+ "name": "caller-patterns",
39
+ "url": "git@git.dailycaller.com:DailyCaller/caller-patterns.git",
40
+ "local_path": "../caller-patterns",
41
+ "namespace": "patterns"
42
+ }
43
+ // Add more per-org or per-stack pattern repos here.
44
+ ],
45
+
46
+ // ── Extra skill directories ─────────────────────────────────────────────────
47
+ // Paths (relative to repo root) containing Claude Code skill folders.
48
+ // Each is linked into ~/.claude/skills/ at startup so their slash commands
49
+ // are available during the audit session.
50
+ "extra_skill_dirs": [
51
+ "../caller-audit"
52
+ // "../my-org-skills"
53
+ ],
54
+
55
+ // ── Extra repos ─────────────────────────────────────────────────────────────
56
+ // Cloned/pulled at startup for reference. Not RAG-indexed unless also listed
57
+ // in pattern_repos.
58
+ "extra_repos": [
59
+ // { "url": "...", "local_path": "..." }
60
+ ],
61
+
62
+ // ── MCP services ────────────────────────────────────────────────────────────
63
+ // Started before the first audit agent turn.
64
+ // Template vars available in args: ${project}, ${org}, ${cwd}.
65
+ "mcp_services": [
66
+ {
67
+ "name": "memory-bank",
68
+ "cwd": ".agent/skills/agent-memory",
69
+ "command": "npm run start-server",
70
+ "args": ["${project}", "${cwd}"]
71
+ }
72
+ ],
73
+
74
+ // ── Extra audit domains ─────────────────────────────────────────────────────
75
+ // Custom check tables beyond the six built-in scanner modules.
76
+ // Each points to a markdown file in the repo with vulnerability definitions
77
+ // in the same format as the tables in this document.
78
+ "extra_domains": [
79
+ // { "name": "supabase-rls", "prompt_file": "docs/patterns-rls.md" }
80
+ ]
81
+ }
82
+ ```
83
+
84
+ ### How the skill uses this config
85
+
86
+ When `/tdd-audit` is invoked as a Claude Code slash command, the skill reads `.tdd-audit.json` from the repo root before any audit phase runs:
87
+
88
+ 1. **Pattern repos** — each entry is cloned/pulled then RAG-indexed into its namespace. Agents query the index via `/rag-engineer retrieve` before proposing any fix. If a prior solution is found, the agent leads with it instead of deriving from scratch.
89
+ 2. **Extra skill dirs** — linked into `~/.claude/skills/` so their commands are available during the session.
90
+ 3. **MCP services** — started and awaited before the first agent turn. The memory bank, if configured, is queried at session start to pre-load prior audit findings and known patterns.
91
+ 4. **Extra domains** — loaded alongside the built-in six. Each domain's check table is given to its parallel audit agent.
92
+ 5. **Branding** — `org` and `project` appear in the audit report header, SECURITY.md, and pattern contribution PRs. `tdd_site` and `badge_label` control the README badge and all external links in scan output.
93
+
94
+ ### Wrapping tdd-audit
95
+
96
+ To distribute a pre-configured version to your team:
97
+
98
+ 1. Create a wrapper skill (e.g., `caller-audit`) that depends on `tdd-remediation`.
99
+ 2. Ship a `.tdd-audit.json` template in your wrapper's `references/` directory.
100
+ 3. Update your installer to copy `prompts/auto-audit.md` from tdd-audit and register it as `~/.claude/commands/tdd-audit.md` — the skill reads the same config file regardless of which installer registered it.
101
+ 4. Your team runs `/tdd-audit` as normal; it picks up your org's config automatically.
4
102
 
5
103
  ---
6
104
 
@@ -334,3 +432,145 @@ These patterns are checked against `.md` files in `prompts/`, `skills/`, `.claud
334
432
  **Grep signature:** `"postinstall": "curl https://..."`, `"preinstall": "wget http://..."`
335
433
  **Why it matters:** A postinstall script that shells out to `curl`/`wget` can silently exfiltrate environment variables, `.env` files, or SSH keys to an attacker's server the moment anyone installs your package or its parent.
336
434
  **Fix:** Remove network calls from lifecycle scripts. If data collection is needed, make it explicit and user-consented, never automatic on install.
435
+
436
+ ---
437
+
438
+ ## AI / LLM Advanced Patterns (v1.17.0+)
439
+
440
+ Patterns sourced from Semgrep ai-best-practices and OWASP LLM Top 10.
441
+
442
+ ### Hardcoded Gemini Key (CRITICAL)
443
+ **Grep signature:** `'AIza...'` (39 chars — Google API key format)
444
+ **Note:** `skipInTests: true`
445
+ **Why it matters:** Gemini/Google API keys grant access to all Google Cloud services associated with the account.
446
+ **Fix:** Use `process.env.GEMINI_API_KEY`. Revoke via Google Cloud Console immediately.
447
+
448
+ ### Hardcoded Cohere Key (CRITICAL)
449
+ **Grep signature:** 40-char alphanumeric string adjacent to `cohere`
450
+ **Note:** `skipInTests: true`
451
+ **Why it matters:** Committed Cohere key leaks billing access and all generation API capabilities.
452
+ **Fix:** Use environment variables. Rotate via dashboard.cohere.com.
453
+
454
+ ### Hardcoded Mistral Key (CRITICAL)
455
+ **Grep signature:** 32-char alphanumeric string adjacent to `mistral`
456
+ **Note:** `skipInTests: true`
457
+ **Why it matters:** Committed Mistral key leaks API billing and model access.
458
+ **Fix:** Use environment variables. Rotate via console.mistral.ai.
459
+
460
+ ### LLM Output to exec (CRITICAL)
461
+ **Grep signature:** `exec(response)`, `execSync(completion)`, `spawn(aiResult)`, `spawnSync(generated)`
462
+ **Why it matters:** Raw LLM text passed to a shell command enables RCE if the model is jailbroken, fine-tuned adversarially, or the response channel is intercepted.
463
+ **Fix:** Never pass LLM output to shell execution functions. Use a strict allowlist of safe commands.
464
+
465
+ ### Missing max_tokens (HIGH)
466
+ **Grep signature:** `messages.create({...})` or `chat.completions.create({...})` without `max_tokens:` in the options object
467
+ **Why it matters:** Without a token cap, a single API call can consume the entire model context window, leading to billing spikes or quota exhaustion.
468
+ **Fix:** Always set `max_tokens` (OpenAI/Anthropic) or `maxOutputTokens` (Gemini). Typical safe cap: 1024–4096 tokens.
469
+
470
+ ### Missing system message (MEDIUM)
471
+ **Grep signature:** `messages` array where first role is `user` with no `system` role present
472
+ **Why it matters:** Without a system message, the model has no safety guardrails, persona boundary, or scope restriction — making jailbreaks and scope creep much easier.
473
+ **Fix:** Always include a `{ role: 'system', content: '...' }` element as the first message.
474
+
475
+ ### MCP Credential in Response (HIGH)
476
+ **Grep signature:** `tool_result` / `toolResult` containing `password`, `secret`, `token`, `api_key`, `credential`
477
+ **Why it matters:** MCP tool results containing credentials are sent back to the LLM context and can be exfiltrated via prompt injection or logged.
478
+ **Fix:** Sanitize all MCP tool outputs before injecting into model context. Strip or redact credential-shaped strings.
479
+
480
+ ### Agent Unbounded Loop (HIGH)
481
+ **Grep signature:** `while(true)` containing `tool_use`, `tool_calls`, `function_call`, `runAgent`, or `agent.run`
482
+ **Why it matters:** An agentic loop with no iteration cap can exhaust compute resources, API quota, and time budgets — equivalent to a self-inflicted DoS.
483
+ **Fix:** Add an explicit iteration counter and max: `if (++iterations > MAX_ITERATIONS) throw new Error('Agent loop limit exceeded')`.
484
+
485
+ ### Unsafe Model Load (HIGH)
486
+ **Grep signature:** `torch.load(` without `weights_only=True`; `pickle.load(` from a URL or user-supplied path
487
+ **Why it matters:** PyTorch `torch.load()` and Python `pickle.load()` execute arbitrary code during deserialization. A malicious model file achieves full RCE.
488
+ **Fix:** Use `torch.load(path, weights_only=True)` (PyTorch ≥1.13). Never load models from user-supplied URLs.
489
+
490
+ ---
491
+
492
+ ## Node.js Advanced Patterns (v1.17.0+)
493
+
494
+ Patterns sourced from njsscan, Bearer CLI, and eslint-plugin-security.
495
+
496
+ ### Host Header Injection (HIGH)
497
+ **Grep signature:** `req.headers['host']`, `req.hostname`, or `req.get('host')` used in redirect URL, email link, or `href` construction
498
+ **Why it matters:** An attacker supplies a forged `Host:` header, redirecting victims to an attacker-controlled domain via password-reset emails or redirects.
499
+ **Fix:** Use a hard-coded trusted base URL from environment config (`process.env.BASE_URL`). Never derive it from the request.
500
+
501
+ ### Headless Browser SSRF (CRITICAL)
502
+ **Grep signature:** `page.goto(req.query.url)`, `wkhtmltopdf(req.body.url)`, `page.navigate(userInput)`
503
+ **Why it matters:** Server-side headless browsers have access to the internal network. Attacker can reach cloud metadata (`169.254.169.254`), internal services, or local files.
504
+ **Fix:** Validate URL against an allowlist of allowed hostnames. Block private IP ranges (`10.`, `172.16–31.`, `192.168.`, `127.`, `169.254.`).
505
+
506
+ ### Body Parser DoS (HIGH)
507
+ **Grep signature:** `express.json()` or `bodyParser.json()` with no arguments (no `limit:` option)
508
+ **Why it matters:** A single large request can exhaust Node.js process memory, DoSing the server.
509
+ **Fix:** Always set a `limit`: `express.json({ limit: '100kb' })`.
510
+
511
+ ### vm2 Deprecated (CRITICAL)
512
+ **Grep signature:** `require('vm2')` or `from 'vm2'`
513
+ **Why it matters:** The `vm2` library was publicly abandoned in May 2023 with unfixed sandbox-escape CVEs (CVE-2023-29017 CVSS 9.8, CVE-2023-32314 CVSS 9.8). Any code using it is vulnerable to host compromise.
514
+ **Fix:** Replace with `isolated-vm` for true V8 isolate sandboxing.
515
+
516
+ ### Pug Raw Output (HIGH)
517
+ **Grep signature:** `!{userValue}` in Pug templates
518
+ **Why it matters:** The `!{}` syntax renders content without HTML escaping — XSS if `userValue` is user-controlled.
519
+ **Fix:** Use `#{userValue}` (auto-escaped) instead of `!{userValue}`.
520
+
521
+ ### EJS Unescaped Output (HIGH)
522
+ **Grep signature:** `<%-` tag in EJS templates
523
+ **Why it matters:** `<%-` renders raw HTML — XSS if the variable is user-controlled.
524
+ **Fix:** Use `<%= %>` (auto-escaped) for all user-derived values.
525
+
526
+ ### Handlebars Triple-Stache (HIGH)
527
+ **Grep signature:** `{{{userValue}}}` in Handlebars templates
528
+ **Why it matters:** Triple-stache disables HTML escaping — XSS if `userValue` is user-controlled.
529
+ **Fix:** Use `{{userValue}}` (double-stache, auto-escaped) for all user-derived values.
530
+
531
+ ### postMessage No Origin (HIGH)
532
+ **Grep signature:** `addEventListener('message', handler)` where `handler` body does not reference `event.origin`
533
+ **Why it matters:** Without origin validation, any page (including attacker-controlled iframes) can send arbitrary messages to your handler.
534
+ **Fix:** Check `event.origin` against an explicit allowlist before processing any message data.
535
+
536
+ ### Dynamic Import User Input (HIGH)
537
+ **Grep signature:** `import(req.query.module)`, `import(req.params.name)`, `` import(`./plugins/${req.params.name}`) ``
538
+ **Why it matters:** User-controlled module paths enable path traversal to load arbitrary modules including `child_process` or `fs`.
539
+ **Fix:** Use a static allowlist Map of permitted imports keyed by user-friendly name.
540
+
541
+ ### JWT No Revocation (HIGH)
542
+ **Grep signature:** `jwt.sign({...}, secret, { expiresIn: '7d' })` with no token blocklist or session store
543
+ **Why it matters:** Long-lived JWTs with no revocation mechanism cannot be invalidated after theft or logout. The token remains valid until natural expiry.
544
+ **Fix:** Use short-lived access tokens (15 min) with a JTI blocklist in Redis, or use opaque session tokens instead.
545
+
546
+ ### X-Powered-By Exposed (MEDIUM)
547
+ **Grep signature:** `const app = express()` with no subsequent `disable('x-powered-by')` or `helmet()` call
548
+ **Why it matters:** `X-Powered-By: Express` advertises the framework and version, enabling targeted exploit selection.
549
+ **Fix:** Add `app.use(require('helmet')())` or `app.disable('x-powered-by')`.
550
+
551
+ ### GraphQL Introspection On (HIGH)
552
+ **Grep signature:** `introspection: true` in ApolloServer config
553
+ **Why it matters:** Introspection exposes the entire schema to unauthenticated clients — a reconnaissance goldmine for attackers.
554
+ **Fix:** Set `introspection: process.env.NODE_ENV !== 'production'`. Disable the playground in production.
555
+
556
+ ### GraphQL No Depth Limit (MEDIUM)
557
+ **Grep signature:** `new ApolloServer({...})` without `depthLimit` or `createDepthLimitPlugin`
558
+ **Why it matters:** Without depth limits, a deeply nested query can trigger O(n²) or worse resolver execution — DoS.
559
+ **Fix:** Add `createDepthLimitPlugin(7)` and a complexity limit plugin to all ApolloServer configurations.
560
+
561
+ ### Sequelize TLS Disabled (HIGH)
562
+ **Grep signature:** `dialectOptions: { ssl: false }` or `ssl: { rejectUnauthorized: false }` in Sequelize/pg/mysql2 config
563
+ **Why it matters:** Disables certificate validation on the database connection, exposing credentials and data to MitM.
564
+ **Fix:** Use `ssl: { require: true, rejectUnauthorized: true, ca: fs.readFileSync('./certs/ca.pem') }`.
565
+
566
+ ### Silent Exception Swallow (MEDIUM)
567
+ **Grep signature:** `catch(e) {}` — empty or comment-only catch blocks
568
+ **Note:** `skipInTests: true`
569
+ **Why it matters:** Silently discarded exceptions hide auth failures, validation errors, and crypto failures — making incidents invisible.
570
+ **Fix:** Always log and re-throw or return a safe error response from every catch block.
571
+
572
+ ### Insecure WebSocket URL (MEDIUM)
573
+ **Grep signature:** `new WebSocket('ws://...')` — non-localhost `ws://` URL
574
+ **Note:** `skipInTests: true`
575
+ **Why it matters:** Unencrypted WebSocket connections expose message content and credentials to network observers.
576
+ **Fix:** Always use `wss://` in production. Gate on `NODE_ENV`: `process.env.NODE_ENV === 'production' ? 'wss://...' : 'ws://localhost'`.
package/index.js CHANGED
@@ -11,17 +11,23 @@ const {
11
11
  quickScan,
12
12
  printFindings,
13
13
  } = require('./lib/scanner');
14
- const { toJson, toSarif, toText } = require('./lib/reporter');
15
14
  const { writeInitConfig, loadConfig, parseCliOverrides } = require('./lib/config');
16
15
  const { badgeLine, injectBadge } = require('./lib/badge');
17
16
 
18
17
  const args = process.argv.slice(2);
19
- const isLocal = args.includes('--local');
20
- const isClaude = args.includes('--claude');
21
- const withHooks = args.includes('--with-hooks');
22
- const skipScan = args.includes('--skip-scan');
23
- const scanOnly = args.includes('--scan-only') || args.includes('--scan');
24
- const isServe = args[0] === 'serve';
18
+ const isLocal = args.includes('--local');
19
+ const isClaude = args.includes('--claude');
20
+ const withHooks = args.includes('--with-hooks');
21
+ const skipScan = args.includes('--skip-scan');
22
+ const isServe = args[0] === 'serve';
23
+ const isAI = args.includes('--ai');
24
+ const allowWrites = args.includes('--allow-writes');
25
+ const isVerbose = args.includes('--verbose');
26
+
27
+ // --depth tier-1|tier-2|tier-3|tier-4 (output depth for --ai --json mode)
28
+ // tier-4 also auto-enables --allow-writes
29
+ const depthIdx = args.indexOf('--depth');
30
+ const depth = depthIdx !== -1 ? args[depthIdx + 1] : 'tier-1';
25
31
 
26
32
  // --json or --format json → structured JSON output
27
33
  // --format sarif → SARIF 2.1.0 output
@@ -72,25 +78,30 @@ if (isServe) {
72
78
  return; // server stays alive — do not fall through to installer
73
79
  }
74
80
 
75
- // ─── Scan-only early exit ─────────────────────────────────────────────────────
76
-
77
- if (scanOnly) {
78
- if (outputFormat !== 'text') process.stdout.write('\n🔍 Scanning...\n');
79
- else process.stdout.write('\n🔍 Scanning for vulnerability patterns...');
80
- const findings = quickScan(projectDir);
81
- const exempted = findings.exempted || [];
82
- if (outputFormat === 'json') {
83
- process.stdout.write('\n');
84
- console.log(JSON.stringify(toJson(findings, exempted), null, 2));
85
- } else if (outputFormat === 'sarif') {
86
- process.stdout.write('\n');
87
- console.log(JSON.stringify(toSarif(findings, projectDir), null, 2));
88
- } else {
89
- process.stdout.write('\n');
90
- printFindings(findings, exempted);
91
- }
92
- injectBadge(projectDir, badgeLine(findings, config.tdd_site));
93
- process.exit(0);
81
+ // ─── AI Audit mode early exit ─────────────────────────────────────────────────
82
+ // tdd-audit --ai [--scan-only] [--json | --format sarif] [--allow-writes]
83
+ // [--provider anthropic] [--model claude-opus-4-6] [--api-key sk-...]
84
+ // [--base-url https://...] [--config .tdd-audit.json] [--verbose]
85
+
86
+ if (isAI) {
87
+ const { runAudit } = require('./lib/auditor');
88
+ runAudit({
89
+ projectDir,
90
+ packageDir: __dirname,
91
+ provider: config.provider,
92
+ apiKey: config.apiKey,
93
+ model: config.model,
94
+ baseUrl: config.baseUrl,
95
+ outputFormat,
96
+ depth,
97
+ // tier-4 auto-enables writes; explicit --allow-writes also works
98
+ allowWrites: allowWrites || depth === 'tier-4',
99
+ verbose: isVerbose,
100
+ }).catch(err => {
101
+ console.error(`\n❌ AI Audit failed: ${err.message}`);
102
+ process.exit(1);
103
+ });
104
+ return;
94
105
  }
95
106
 
96
107
  // ─── Install Skill Files ──────────────────────────────────────────────────────