@lhi/tdd-audit 1.5.0 → 1.8.1

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/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @lhi/tdd-audit
2
2
 
3
- Security skill installer for **Claude Code, Gemini CLI, Cursor, Codex, and OpenCode**. Patches vulnerabilities using a Red-Green-Refactor exploit-test protocol — you prove the hole exists, apply the fix, and prove it's closed.
3
+ > **v1.8.0** — Security skill installer for **Claude Code, Gemini CLI, Cursor, Codex, and OpenCode**. Patches vulnerabilities using a Red-Green-Refactor exploit-test protocol — you prove the hole exists, apply the fix, and prove it's closed.
4
4
 
5
5
  ## What happens on install
6
6
 
7
7
  Running the installer does five things immediately:
8
8
 
9
- 1. **Scans your codebase** for 29 vulnerability patterns (SQL injection, IDOR, XSS, command injection, path traversal, broken auth, JWT alg:none, ReDoS, timing-unsafe comparisons, and more) and prints findings to stdout
9
+ 1. **Scans your codebase** for 34 vulnerability patterns across OWASP Top 10, mobile, agentic AI, and prompt/skill files prints a severity-ranked findings report to stdout
10
10
  2. **Scaffolds `__tests__/security/`** with a framework-matched boilerplate exploit test
11
11
  3. **Adds `test:security`** to your `package.json` scripts (Node.js projects)
12
12
  4. **Creates `.github/workflows/security-tests.yml`** so the CI gate exists from day one
@@ -31,25 +31,25 @@ node index.js
31
31
  | Claude Code | `npx @lhi/tdd-audit --local --claude` |
32
32
  | Gemini CLI / Codex / OpenCode | `npx @lhi/tdd-audit --local` |
33
33
  | With pre-commit hook | add `--with-hooks` |
34
- | Scan only (no install) | `npx @lhi/tdd-audit --scan-only` |
34
+ | Scan only (no install) | `npx @lhi/tdd-audit --scan` |
35
35
 
36
36
  ### All flags
37
37
 
38
38
  | Flag | Description |
39
39
  |---|---|
40
- | `--local` | Install skill files to the current project directory instead of `~` |
40
+ | `--local` | Install skill files into the current project instead of `~` |
41
41
  | `--claude` | Use `.claude/` instead of `.agents/` as the skill directory |
42
42
  | `--with-hooks` | Install a pre-commit hook that blocks commits if security tests fail |
43
43
  | `--skip-scan` | Skip the automatic vulnerability scan on install |
44
- | `--scan-only` | Run the vulnerability scan without installing anything |
44
+ | `--scan` / `--scan-only` | Run the vulnerability scan without installing anything |
45
45
 
46
- ### Framework Detection
46
+ ### Framework detection
47
47
 
48
48
  The installer automatically detects your project's test framework and scaffolds the right boilerplate:
49
49
 
50
50
  | Detected | Boilerplate | `test:security` command |
51
51
  |---|---|---|
52
- | `jest` / `supertest` | `sample.exploit.test.js` | `jest --testPathPattern=__tests__/security` |
52
+ | `jest` / `supertest` | `sample.exploit.test.js` | `jest --testPathPatterns=__tests__/security` |
53
53
  | `vitest` | `sample.exploit.test.vitest.js` | `vitest run __tests__/security` |
54
54
  | `mocha` | `sample.exploit.test.js` | `mocha '__tests__/security/**/*.spec.js'` |
55
55
  | `pytest.ini` / `pyproject.toml` | `sample.exploit.test.pytest.py` | `pytest tests/security/ -v` |
@@ -65,33 +65,46 @@ Once installed, trigger the autonomous audit in your agent:
65
65
  ```
66
66
 
67
67
  The agent will:
68
- 1. Scan the codebase and present a severity-ranked findings report (CRITICAL / HIGH / MEDIUM / LOW)
69
- 2. Wait for your confirmation before making any changes
70
- 3. For each confirmed vulnerability, apply the full Red-Green-Refactor loop:
68
+
69
+ 1. Detect your tech stack and scope the scan to relevant patterns only
70
+ 2. Scan the codebase and present a severity-ranked findings report (CRITICAL / HIGH / MEDIUM / LOW)
71
+ 3. **Wait for your confirmation** before making any changes
72
+ 4. For each confirmed vulnerability, apply the full Red-Green-Refactor loop:
71
73
  - **Red** — write an exploit test that fails, proving the vulnerability exists
72
74
  - **Green** — apply the targeted patch, making the test pass
73
75
  - **Refactor** — run the full suite to confirm no regressions
74
- 4. Deliver a final Remediation Summary table
76
+ 5. Apply proactive hardening controls (security headers, rate limiting, `npm audit`, secret history scan)
77
+ 6. Deliver a final Remediation Summary table
75
78
 
76
79
  The agent works one vulnerability at a time and does not advance until the current one is fully proven closed.
77
80
 
78
- ## Vulnerability Scanner
81
+ Pass `--scan` in your prompt to get the Audit Report only, without any code changes.
82
+
83
+ ## Vulnerability scanner
79
84
 
80
- The built-in scanner catches 29 patterns across OWASP Top 10 + mobile + agentic AI stacks:
85
+ The built-in scanner catches **34 patterns** across OWASP Top 10, mobile, agentic AI, and prompt/skill files:
81
86
 
82
87
  | Category | Patterns |
83
88
  |---|---|
84
- | Injection | SQL Injection, Command Injection, NoSQL Injection, Template Injection, LDAP |
85
- | Broken Auth | JWT alg:none, Broken Auth, Timing-Unsafe Comparison, Hardcoded Secret, Secret Fallback |
89
+ | Injection | SQL Injection, Command Injection, NoSQL Injection, Template Injection |
90
+ | Broken Auth | JWT Alg None, Broken Auth, Timing-Unsafe Comparison, Hardcoded Secret, Secret Fallback |
86
91
  | XSS / Output | XSS, eval() Injection, Open Redirect |
87
92
  | Crypto | Weak Crypto (MD5/SHA1), Insecure Random, TLS Bypass |
88
93
  | Server-side | SSRF, Path Traversal, XXE, Insecure Deserialization |
89
94
  | Assignment | Mass Assignment, Prototype Pollution |
90
95
  | Mobile | Sensitive Storage, WebView JS Bridge, Deep Link Injection, Android Debuggable |
91
- | Config | CORS Wildcard, Cleartext Traffic, Config Secrets |
92
- | New (v1.5) | JWT Alg None, Timing-Unsafe Comparison, ReDoS |
96
+ | Config / Infra | CORS Wildcard, Cleartext Traffic, Config Secrets, ReDoS |
97
+ | Agentic / Prompt | Deprecated CSRF Package (`csurf`), Unpinned npx MCP Server, Cleartext URL in Prompt |
98
+
99
+ ### Scanner behaviour
93
100
 
94
- ## Running security tests manually
101
+ - **Test files are flagged but labelled** — findings in `__tests__/`, `tests/`, `spec/`, or `*.test.*` files are shown with a `[test file]` badge. Patterns that mark `skipInTests: true` (e.g. Hardcoded Secret, Sensitive Log, Cleartext Traffic) are further tagged `likelyFalsePositive` and separated at the bottom of the report.
102
+ - **Prompt/skill files get their own scan** — `.md` files inside `prompts/`, `skills/`, `.claude/`, `workflows/`, plus `CLAUDE.md` and `SKILL.md`, are scanned for prompt-specific anti-patterns. Matches inside backtick code spans are suppressed to avoid noise from documentation examples.
103
+ - **`audit_status: safe` exemption** — any prompt file with `audit_status: safe` in its YAML frontmatter is skipped and listed separately so you can verify exemptions are intentional.
104
+ - **Binary and oversized files skipped** — files larger than 512 KB or containing null bytes are skipped to prevent OOM.
105
+ - **Symlinks skipped** — symlinks are never followed, preventing directory-escape on M-series Macs and shared filesystems.
106
+
107
+ ## Running security tests
95
108
 
96
109
  ```bash
97
110
  # Node.js
@@ -102,20 +115,30 @@ pytest tests/security/ -v
102
115
 
103
116
  # Go
104
117
  go test ./security/... -v
118
+
119
+ # Flutter
120
+ flutter test test/security/
105
121
  ```
106
122
 
107
123
  ## CI/CD
108
124
 
109
- The installer creates `.github/workflows/security-tests.yml` for your stack. It runs on every pull request targeting `main` — any exploit test that regresses will block the merge.
125
+ The installer creates framework-matched workflow files under `.github/workflows/`. Both `security-tests.yml` and `ci.yml` include:
126
+
127
+ - SHA-pinned `uses:` references on every action (supply chain hardening)
128
+ - `npm audit --audit-level=high` (or equivalent) to catch vulnerable dependencies
129
+ - The security exploit test suite on every push and pull request
110
130
 
111
- To add this gate to an existing CI pipeline manually:
131
+ To add the security gate to an existing pipeline manually:
112
132
 
113
133
  ```yaml
134
+ - name: Dependency audit
135
+ run: npm audit --audit-level=high
136
+
114
137
  - name: Run security exploit tests
115
- run: npm run test:security # or pytest tests/security/, or go test ./security/...
138
+ run: npm run test:security # or pytest tests/security/, flutter test test/security/
116
139
  ```
117
140
 
118
- ## Pre-commit Hook
141
+ ## Pre-commit hook
119
142
 
120
143
  The `--with-hooks` flag appends a security gate to `.git/hooks/pre-commit`. Commits are blocked if any exploit test fails:
121
144
 
@@ -123,7 +146,37 @@ The `--with-hooks` flag appends a security gate to `.git/hooks/pre-commit`. Comm
123
146
  ❌ Security tests failed. Commit blocked.
124
147
  ```
125
148
 
126
- The hook is non-destructive — it appends to any existing hook content rather than overwriting it.
149
+ The hook is non-destructive — it appends to existing hook content rather than overwriting it.
150
+
151
+ ## Agentic AI security (ASI01–ASI10)
152
+
153
+ When the project contains AI agent code, MCP configurations, or `CLAUDE.md` files, the scanner also checks for agentic-specific vulnerabilities:
154
+
155
+ | ID | Vulnerability | Risk |
156
+ |---|---|---|
157
+ | ASI01 | Prompt injection via tool output | Malicious content in web/file reads hijacks agent behaviour |
158
+ | ASI02 | CLAUDE.md / instructions file injection | Attacker-controlled system prompts override agent identity |
159
+ | ASI03 | MCP server supply chain (unpinned `npx`) | Compromised package version exfiltrates secrets |
160
+ | ASI04 | Excessive tool permissions | Agent can write files or run shell when only read is needed |
161
+ | ASI05 | Secrets in tool call arguments | Tokens/passwords logged by external tools |
162
+ | ASI06 | Unvalidated agent action execution | Agent runs irreversible actions without user confirmation |
163
+ | ASI07 | Insecure direct agent communication | Sub-agent messages trusted without verification |
164
+ | ASI08 | GitHub Actions command injection | `github.event.*` interpolated directly into `run:` steps |
165
+ | ASI09 | Unpinned GitHub Actions (supply chain) | Mutable `@v4` / `@main` tags can be hijacked |
166
+ | ASI10 | Secrets in workflow environment | Secrets printed to logs or embedded in curl URLs |
167
+
168
+ See [`docs/agentic-ai-security.md`](docs/agentic-ai-security.md) for grep patterns, examples, and fixes.
169
+
170
+ ## Documentation
171
+
172
+ | File | Contents |
173
+ |---|---|
174
+ | [`docs/scanner.md`](docs/scanner.md) | How the scanner works — architecture, detection logic, false-positive handling |
175
+ | [`docs/vulnerability-patterns.md`](docs/vulnerability-patterns.md) | All 34 patterns with descriptions, grep signatures, and fix pointers |
176
+ | [`docs/tdd-protocol.md`](docs/tdd-protocol.md) | The Red-Green-Refactor protocol in full, with framework templates |
177
+ | [`docs/agentic-ai-security.md`](docs/agentic-ai-security.md) | ASI01–ASI10 agentic AI vulnerability reference |
178
+ | [`docs/hardening.md`](docs/hardening.md) | Phase 4 proactive hardening controls |
179
+ | [`docs/ci-cd.md`](docs/ci-cd.md) | CI/CD integration guide for all supported stacks |
127
180
 
128
181
  ## License
129
182
 
package/SKILL.md CHANGED
@@ -53,7 +53,7 @@ jobs:
53
53
  security-tests:
54
54
  runs-on: ubuntu-latest
55
55
  steps:
56
- - uses: actions/checkout@v3
56
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
57
57
  - name: Install dependencies
58
58
  run: npm ci
59
59
  - name: Run Security Exploit Tests
package/lib/scanner.js CHANGED
@@ -45,6 +45,9 @@ const VULN_PATTERNS = [
45
45
  ];
46
46
 
47
47
  const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go', '.dart']);
48
+
49
+ /** Maximum file size to read before skipping (512 KB). Prevents OOM on large generated files. */
50
+ const MAX_SCAN_FILE_BYTES = 512 * 1024;
48
51
  const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', '__pycache__', 'venv', '.venv', 'vendor', '.expo', '.dart_tool', '.pub-cache']);
49
52
 
50
53
  // ─── Prompt / Skill Patterns ──────────────────────────────────────────────────
@@ -214,14 +217,22 @@ function hasSafeAuditStatus(lines) {
214
217
  }
215
218
 
216
219
  /**
217
- * Returns true if the match at matchIndex falls inside a backtick code span.
218
- * Used to suppress PROMPT_PATTERN hits on pattern-documentation table rows.
220
+ * Returns true if the match at matchIndex falls inside a *closed* backtick
221
+ * code span on the same line. A code span is closed only when there is an
222
+ * odd number of backticks before the match AND at least one closing backtick
223
+ * after it on the same line. A lone, unmatched backtick before the pattern
224
+ * does NOT constitute a code span and must NOT suppress the finding.
219
225
  * @param {string} line
220
226
  * @param {number} matchIndex - character index of the match start
221
227
  */
222
228
  function isInsideBackticks(line, matchIndex) {
223
229
  const before = line.slice(0, matchIndex);
224
- return (before.match(/`/g) || []).length % 2 === 1;
230
+ const after = line.slice(matchIndex);
231
+ const backticksBefore = (before.match(/`/g) || []).length;
232
+ const backticksAfter = (after.match(/`/g) || []).length;
233
+ // Suppress only when the span is properly closed: odd opening count + at
234
+ // least one closing backtick exists after the match position.
235
+ return backticksBefore % 2 === 1 && backticksAfter >= 1;
225
236
  }
226
237
 
227
238
  /**
@@ -234,16 +245,32 @@ function isCommentLine(line) {
234
245
 
235
246
  /**
236
247
  * Scan all prompt/skill .md files in projectDir for prompt-specific patterns.
248
+ *
249
+ * Returns a findings array with a non-enumerable `.exempted` property — an
250
+ * array of relative paths for files skipped via `audit_status: safe`. Using
251
+ * a non-enumerable property preserves full backward compatibility: spread,
252
+ * toEqual([]), and quickScan's `...scanPromptFiles()` all continue to work.
253
+ *
237
254
  * @param {string} projectDir - project root
238
- * @returns {Array} findings
255
+ * @returns {Array} findings (with non-enumerable .exempted: string[])
239
256
  */
240
257
  function scanPromptFiles(projectDir) {
241
258
  const findings = [];
259
+ const exempted = [];
242
260
  for (const filePath of walkMdFiles(projectDir)) {
243
261
  if (!isPromptFile(filePath, projectDir)) continue;
244
262
  let lines;
245
- try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
246
- if (hasSafeAuditStatus(lines)) continue;
263
+ try {
264
+ // SEC-06: read first, then check length — eliminates statSync/readFileSync TOCTOU race.
265
+ const content = fs.readFileSync(filePath, 'utf8');
266
+ if (content.length > MAX_SCAN_FILE_BYTES) continue;
267
+ if (content.includes('\0')) continue; // skip binary files (mirrors quickScan guard)
268
+ lines = content.split('\n');
269
+ } catch { continue; }
270
+ if (hasSafeAuditStatus(lines)) {
271
+ exempted.push(path.relative(projectDir, filePath));
272
+ continue;
273
+ }
247
274
  for (let i = 0; i < lines.length; i++) {
248
275
  for (const p of PROMPT_PATTERNS) {
249
276
  const match = p.pattern.exec(lines[i]);
@@ -262,6 +289,8 @@ function scanPromptFiles(projectDir) {
262
289
  }
263
290
  }
264
291
  }
292
+ // Attach exempted as non-enumerable so spread / toEqual([]) are unaffected.
293
+ Object.defineProperty(findings, 'exempted', { value: exempted, enumerable: false, configurable: true });
265
294
  return findings;
266
295
  }
267
296
 
@@ -339,8 +368,10 @@ function quickScan(projectDir) {
339
368
  const inTest = isTestFile(filePath, projectDir);
340
369
  let content;
341
370
  // L1 fix: guard against binary / non-UTF-8 files
371
+ // SEC-06: read first, then check length — eliminates statSync/readFileSync TOCTOU race.
342
372
  try {
343
373
  content = fs.readFileSync(filePath, 'utf8');
374
+ if (content.length > MAX_SCAN_FILE_BYTES) continue;
344
375
  } catch {
345
376
  continue;
346
377
  }
@@ -372,38 +403,47 @@ function quickScan(projectDir) {
372
403
 
373
404
  /**
374
405
  * Print a human-readable findings report to stdout.
375
- * @param {Array} findings
406
+ * @param {Array} findings - array of finding objects
407
+ * @param {string[]} [exempted=[]] - relative paths of files skipped via audit_status:safe
376
408
  */
377
- function printFindings(findings) {
409
+ function printFindings(findings, exempted = []) {
378
410
  if (findings.length === 0) {
379
411
  console.log(' ✅ No obvious vulnerability patterns detected.\n');
380
- return;
381
- }
382
- const real = findings.filter(f => !f.likelyFalsePositive);
383
- const noisy = findings.filter(f => f.likelyFalsePositive);
384
-
385
- const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
386
- for (const f of real) (bySeverity[f.severity] || bySeverity.LOW).push(f);
387
- const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
388
-
389
- console.log(`\n Found ${real.length} potential issue(s)${noisy.length ? ` (+${noisy.length} in test files — see below)` : ''}:\n`);
390
- for (const [sev, list] of Object.entries(bySeverity)) {
391
- if (!list.length) continue;
392
- for (const f of list) {
393
- const testBadge = f.inTestFile ? ' [test file]' : '';
394
- console.log(` ${icons[sev]} [${sev}] ${f.name} — ${f.file}:${f.line}${testBadge}`);
395
- console.log(` ${f.snippet}`);
412
+ } else {
413
+ const real = findings.filter(f => !f.likelyFalsePositive);
414
+ const noisy = findings.filter(f => f.likelyFalsePositive);
415
+
416
+ const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
417
+ for (const f of real) (bySeverity[f.severity] || bySeverity.LOW).push(f);
418
+ const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
419
+
420
+ console.log(`\n Found ${real.length} potential issue(s)${noisy.length ? ` (+${noisy.length} in test files — see below)` : ''}:\n`);
421
+ for (const [sev, list] of Object.entries(bySeverity)) {
422
+ if (!list.length) continue;
423
+ for (const f of list) {
424
+ const testBadge = f.inTestFile ? ' [test file]' : '';
425
+ console.log(` ${icons[sev]} [${sev}] ${f.name} ${f.file}:${f.line}${testBadge}`);
426
+ console.log(` ${f.snippet}`);
427
+ }
396
428
  }
397
- }
398
429
 
399
- if (noisy.length) {
400
- console.log('\n ⚪ Likely intentional (in test files — verify manually):');
401
- for (const f of noisy) {
402
- console.log(` ${f.name} — ${f.file}:${f.line}`);
430
+ if (noisy.length) {
431
+ console.log('\n ⚪ Likely intentional (in test files — verify manually):');
432
+ for (const f of noisy) {
433
+ console.log(` ${f.name} — ${f.file}:${f.line}`);
434
+ }
403
435
  }
436
+
437
+ console.log('\n Run /tdd-audit in your agent to remediate.\n');
404
438
  }
405
439
 
406
- console.log('\n Run /tdd-audit in your agent to remediate.\n');
440
+ if (exempted.length) {
441
+ console.log(' ⚠️ Files skipped via audit_status:safe (verify these exemptions are intentional):');
442
+ for (const p of exempted) {
443
+ console.log(` ${p}`);
444
+ }
445
+ console.log('');
446
+ }
407
447
  }
408
448
 
409
449
  module.exports = {
@@ -411,6 +451,7 @@ module.exports = {
411
451
  PROMPT_PATTERNS,
412
452
  SCAN_EXTENSIONS,
413
453
  SKIP_DIRS,
454
+ MAX_SCAN_FILE_BYTES,
414
455
  detectFramework,
415
456
  detectAppFramework,
416
457
  detectTestBaseDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/tdd-audit",
3
- "version": "1.5.0",
3
+ "version": "1.8.1",
4
4
  "description": "Security skill installer for Claude Code, Gemini CLI, Cursor, Codex, and OpenCode. Patches vulnerabilities using a Red-Green-Refactor exploit-test protocol.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,5 +1,11 @@
1
1
  ---
2
2
  description: Run the complete TDD Remediation Autonomous Audit
3
+ risk: low
4
+ source: personal
5
+ date_added: "2024-01-01"
6
+ audited_by: lcanady
7
+ last_audited: "2026-03-25"
8
+ audit_status: safe
3
9
  ---
4
10
  Please use the TDD Remediation Protocol Auto-Audit skill (located in the `skills/tdd-remediation` folder) to secure this repository.
5
11