@lhi/tdd-audit 1.5.0 → 1.8.2

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.
@@ -0,0 +1,267 @@
1
+ # Phase 4 — Proactive Hardening
2
+
3
+ Phase 4 runs after all known vulnerabilities are patched. It applies defence-in-depth controls that make future vulnerabilities harder to introduce and easier to catch.
4
+
5
+ Apply each control independently. Confirm the test suite stays green after each.
6
+
7
+ ---
8
+
9
+ ## 4a. Security headers (Helmet)
10
+
11
+ ```bash
12
+ npm install helmet
13
+ ```
14
+
15
+ Apply as the **first** middleware, before any routes:
16
+
17
+ ```javascript
18
+ const helmet = require('helmet');
19
+ app.use(helmet());
20
+ ```
21
+
22
+ For **Next.js**, add to `next.config.js`:
23
+
24
+ ```javascript
25
+ const securityHeaders = [
26
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
27
+ { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
28
+ { key: 'X-XSS-Protection', value: '1; mode=block' },
29
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
30
+ { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
31
+ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
32
+ ];
33
+
34
+ module.exports = {
35
+ async headers() {
36
+ return [{ source: '/(.*)', headers: securityHeaders }];
37
+ },
38
+ };
39
+ ```
40
+
41
+ **Verify:** `curl -I https://localhost:3000/` — confirm headers are present.
42
+
43
+ ---
44
+
45
+ ## 4b. Content Security Policy (CSP)
46
+
47
+ ```javascript
48
+ app.use(
49
+ helmet.contentSecurityPolicy({
50
+ directives: {
51
+ defaultSrc: ["'self'"],
52
+ scriptSrc: ["'self'"], // no 'unsafe-inline' — use nonces
53
+ styleSrc: ["'self'", "'unsafe-inline'"],
54
+ imgSrc: ["'self'", 'data:', 'https:'],
55
+ connectSrc: ["'self'"],
56
+ fontSrc: ["'self'"],
57
+ objectSrc: ["'none'"],
58
+ frameAncestors: ["'none'"], // equivalent to X-Frame-Options: DENY
59
+ upgradeInsecureRequests: [],
60
+ },
61
+ })
62
+ );
63
+ ```
64
+
65
+ Validate your policy at `https://csp-evaluator.withgoogle.com/`.
66
+
67
+ ---
68
+
69
+ ## 4c. CSRF protection
70
+
71
+ For cookie-based sessions (not pure JWT / Authorization header flows):
72
+
73
+ ```javascript
74
+ // csrf-csrf (csurf is deprecated since March 2023)
75
+ const { doubleCsrf } = require('csrf-csrf');
76
+
77
+ const { generateToken, doubleCsrfProtection } = doubleCsrf({
78
+ getSecret: () => process.env.CSRF_SECRET,
79
+ cookieName: '__Host-psifi.x-csrf-token',
80
+ cookieOptions: { sameSite: 'strict', secure: true },
81
+ });
82
+
83
+ app.use(doubleCsrfProtection);
84
+ app.get('/form', (req, res) => res.render('form', { csrfToken: generateToken(req, res) }));
85
+ ```
86
+
87
+ For SPAs using `fetch`, set `SameSite=Strict` on the session cookie:
88
+
89
+ ```javascript
90
+ res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'strict' });
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 4d. Rate limiting
96
+
97
+ | Route type | Recommended limit |
98
+ |---|---|
99
+ | `/login`, `/register`, `/forgot-password` | 10 requests / 15 min / IP |
100
+ | `/api/` general endpoints | 100 requests / 1 min / IP |
101
+ | File upload endpoints | 5 requests / 1 min / IP |
102
+ | Password reset confirmation | 5 requests / 15 min / IP |
103
+
104
+ ```javascript
105
+ const rateLimit = require('express-rate-limit');
106
+
107
+ const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10 });
108
+ const apiLimiter = rateLimit({ windowMs: 60 * 1000, max: 100 });
109
+
110
+ app.use('/api/', apiLimiter);
111
+ app.post('/api/auth/login', authLimiter, loginHandler);
112
+ app.post('/api/auth/register', authLimiter, registerHandler);
113
+ ```
114
+
115
+ Quick grep to find unprotected POST routes:
116
+
117
+ ```bash
118
+ grep -rn "app\.post\|router\.post" src/ --include="*.js" | grep -v "limiter\|rateLimit"
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 4e. Dependency vulnerability audit
124
+
125
+ ```bash
126
+ # Node.js
127
+ npm audit --audit-level=high
128
+ npm audit fix # auto-fix where safe
129
+
130
+ # Python
131
+ pip install pip-audit && pip-audit
132
+
133
+ # Go
134
+ go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...
135
+
136
+ # Flutter / Dart
137
+ flutter pub outdated
138
+ ```
139
+
140
+ The live `ci.yml` and `security-tests.yml` workflows both run `npm audit --audit-level=high` on every push and pull request (added in v1.8.0).
141
+
142
+ ---
143
+
144
+ ## 4f. Secret history scan
145
+
146
+ ```bash
147
+ # trufflehog (recommended)
148
+ npx trufflehog git file://. --only-verified
149
+
150
+ # gitleaks
151
+ brew install gitleaks
152
+ gitleaks detect --source . -v
153
+ ```
154
+
155
+ If secrets are found in history:
156
+ 1. Rotate the secret immediately — treat it as compromised
157
+ 2. Use `git filter-repo` to rewrite history
158
+ 3. Force-push and have all team members re-clone
159
+
160
+ Prevent future secret commits via pre-commit hook:
161
+
162
+ ```bash
163
+ npx gitleaks protect --staged -v
164
+ # or use: npx @lhi/tdd-audit --with-hooks
165
+ ```
166
+
167
+ ---
168
+
169
+ ## 4g. Production error handling
170
+
171
+ ```javascript
172
+ // Express — place last, after all routes
173
+ app.use((err, req, res, next) => {
174
+ const isDev = process.env.NODE_ENV !== 'production';
175
+ console.error(err); // log internally — never expose to client
176
+ res.status(err.status || 500).json({
177
+ error: isDev ? err.message : 'Internal server error',
178
+ ...(isDev && { stack: err.stack }),
179
+ });
180
+ });
181
+ ```
182
+
183
+ ```python
184
+ # FastAPI
185
+ @app.exception_handler(Exception)
186
+ async def generic_exception_handler(request, exc):
187
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
188
+ return JSONResponse(status_code=500, content={"error": "Internal server error"})
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 4h. Subresource Integrity (SRI)
194
+
195
+ For third-party scripts or stylesheets loaded via CDN:
196
+
197
+ ```html
198
+ <script
199
+ src="https://cdn.example.com/lib.min.js"
200
+ integrity="sha384-<hash>"
201
+ crossorigin="anonymous"
202
+ ></script>
203
+ ```
204
+
205
+ Generate integrity hashes at `https://www.srihash.org/`.
206
+
207
+ ---
208
+
209
+ ## 4i. GitHub Actions supply chain hardening
210
+
211
+ Pin every `uses:` to a full commit SHA:
212
+
213
+ ```yaml
214
+ # Vulnerable — mutable tag
215
+ - uses: actions/checkout@v4
216
+
217
+ # Safe — SHA-locked, tag as comment
218
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
219
+ ```
220
+
221
+ Grep for unpinned actions:
222
+
223
+ ```bash
224
+ grep -rn "uses:.*@v\|uses:.*@main\|uses:.*@master" .github/workflows/
225
+ ```
226
+
227
+ Workflow inputs that inject into `run:` steps:
228
+
229
+ ```yaml
230
+ # Vulnerable
231
+ run: echo "${{ github.event.pull_request.title }}"
232
+
233
+ # Safe
234
+ env:
235
+ PR_TITLE: ${{ github.event.pull_request.title }}
236
+ run: echo "$PR_TITLE"
237
+ ```
238
+
239
+ ---
240
+
241
+ ## 4j. Agentic AI controls
242
+
243
+ - `CLAUDE.md` under version control; reviewed on every commit; no user-supplied content
244
+ - MCP servers pinned to exact versions or local installs (see [ASI03](agentic-ai-security.md#asi03--mcp-server-supply-chain-risk))
245
+ - Agent tool permissions scoped to minimum required; no `bash` when only `read` is needed
246
+ - Tool outputs sanitized before injecting into prompt context (see [ASI01](agentic-ai-security.md#asi01--prompt-injection-via-tool-output))
247
+
248
+ ---
249
+
250
+ ## Hardening verification checklist
251
+
252
+ - [ ] `helmet()` applied before all routes; `X-Content-Type-Options: nosniff` in every response
253
+ - [ ] CSP header present; validated with csp-evaluator
254
+ - [ ] CSRF protection on all state-mutating routes (or `SameSite=Strict` cookies)
255
+ - [ ] Rate limiting on auth routes — 429 returned after threshold
256
+ - [ ] `npm audit` / `pip-audit` / `govulncheck` shows 0 HIGH/CRITICAL findings
257
+ - [ ] `gitleaks` / `trufflehog` shows no verified secrets in history
258
+ - [ ] Production error handler returns generic messages; no stack traces in 5xx responses
259
+ - [ ] SRI hashes on all third-party CDN resources
260
+ - [ ] `*.env` in `.gitignore`; no `.env` committed to git
261
+ - [ ] All cookies: `httpOnly: true`, `secure: true`, `sameSite: 'strict'` or `'lax'`
262
+ - [ ] All GitHub Actions `uses:` pinned to full commit SHAs
263
+ - [ ] No `github.event.*` interpolated directly into `run:` steps
264
+ - [ ] No secrets inline in workflow `run:` commands or URLs
265
+ - [ ] `CLAUDE.md` in version control and reviewed; no user-supplied content
266
+ - [ ] MCP servers pinned to exact versions or local installs
267
+ - [ ] Agent tool permissions scoped to minimum required
@@ -0,0 +1,161 @@
1
+ # Scanner Architecture
2
+
3
+ `lib/scanner.js` is the core engine behind `npx @lhi/tdd-audit --scan` and the auto-audit skill. It is a pure Node.js module with no runtime dependencies — only `fs` and `path`.
4
+
5
+ ---
6
+
7
+ ## Entry points
8
+
9
+ | Export | Purpose |
10
+ |---|---|
11
+ | `quickScan(projectDir)` | Walk all source files and return a findings array |
12
+ | `scanPromptFiles(projectDir)` | Walk all `.md` prompt/skill files and check for prompt-specific patterns |
13
+ | `scanAppConfig(projectDir)` | Check `app.json` / `app.config.*` for embedded secrets |
14
+ | `scanAndroidManifest(projectDir)` | Check `AndroidManifest.xml` for `android:debuggable="true"` |
15
+ | `printFindings(findings, exempted)` | Format and print a findings report to stdout |
16
+ | `detectFramework(dir)` | Detect the test framework (`jest`, `vitest`, `mocha`, `pytest`, `go`, `flutter`) |
17
+ | `detectAppFramework(dir)` | Detect the UI framework (`nextjs`, `expo`, `react-native`, `react`, `flutter`) |
18
+ | `detectTestBaseDir(dir, framework)` | Locate the test root (`__tests__`, `tests`, `test`, `spec`) |
19
+
20
+ ---
21
+
22
+ ## How `quickScan` works
23
+
24
+ ```
25
+ projectDir
26
+ └─ walkFiles() — yields .js/.ts/.jsx/.tsx/.mjs/.py/.go/.dart files
27
+ └─ for each file:
28
+ 1. Read file content (read-first, check length after — no TOCTOU)
29
+ 2. Skip if content.length > 512 KB
30
+ 3. Skip if file contains null bytes (binary guard)
31
+ 4. For each line × each VULN_PATTERN:
32
+ – If pattern matches, push finding with severity / name / file / line / snippet
33
+ – inTestFile: true if path is under a test directory
34
+ – likelyFalsePositive: true if inTestFile && pattern.skipInTests
35
+ └─ scanAppConfig() — checks app.json / app.config.* for secret patterns
36
+ └─ scanAndroidManifest() — checks android:debuggable
37
+ └─ scanPromptFiles() — walks .md files in prompt directories
38
+ ```
39
+
40
+ All four result sets are merged into one array and returned to the caller.
41
+
42
+ ---
43
+
44
+ ## File walking
45
+
46
+ ### `walkFiles(dir)`
47
+
48
+ Yields scannable source files (`SCAN_EXTENSIONS`). Skips:
49
+
50
+ - **`SKIP_DIRS`**: `node_modules`, `.git`, `dist`, `build`, `.next`, `out`, `__pycache__`, `venv`, `.venv`, `vendor`, `.expo`, `.dart_tool`, `.pub-cache`
51
+ - **Symlinks** — never followed, preventing escape from the project root on shared/M-series filesystems
52
+
53
+ ### `walkMdFiles(dir)`
54
+
55
+ Same skip rules, yields `.md` files only. Used by `scanPromptFiles`.
56
+
57
+ ---
58
+
59
+ ## Scanned extensions
60
+
61
+ `.js` `.ts` `.jsx` `.tsx` `.mjs` `.py` `.go` `.dart`
62
+
63
+ YAML, JSON, XML, and shell files are not scanned by the code scanner. CI workflow files (`.yml`) are scanned separately when explicitly passed to the ASI08/ASI09 grep patterns during an agent-driven audit.
64
+
65
+ ---
66
+
67
+ ## Test file detection
68
+
69
+ `isTestFile(filePath, projectDir)` returns `true` for any file that matches:
70
+
71
+ | Pattern | Example |
72
+ |---|---|
73
+ | `*.test.js` / `*.spec.ts` | `auth.test.ts` |
74
+ | `*_test.dart` | `login_test.dart` |
75
+ | Path contains `__tests__/` or `tests/` | `__tests__/unit/scanner.test.js` |
76
+ | Path contains `spec/` | `spec/api/users_spec.rb` |
77
+ | Filename starts with `test_` | `test_helpers.js` |
78
+
79
+ Findings in test files are always reported (they may contain real vulnerabilities), but:
80
+ - They carry `inTestFile: true` in the finding object
81
+ - If the matched pattern has `skipInTests: true`, `likelyFalsePositive` is set to `true` and the finding is separated into a secondary "verify manually" section of the report
82
+
83
+ ---
84
+
85
+ ## Prompt file detection
86
+
87
+ `isPromptFile(filePath, projectDir)` returns `true` for:
88
+
89
+ | Condition | Example |
90
+ |---|---|
91
+ | Filename is in `PROMPT_FILE_NAMES` | `CLAUDE.md`, `SKILL.md`, `.cursorrules`, `.clinerules` |
92
+ | First path segment is in `PROMPT_DIRS` | `prompts/`, `skills/`, `.claude/`, `workflows/` |
93
+
94
+ ### `audit_status: safe` exemption
95
+
96
+ If a prompt file's YAML frontmatter contains `audit_status: safe`, it is skipped entirely. The relative path is collected into an `exempted` array and displayed at the bottom of the `printFindings` report so you can verify exemptions are intentional.
97
+
98
+ ```markdown
99
+ ---
100
+ name: my-prompt
101
+ audit_status: safe
102
+ ---
103
+ ```
104
+
105
+ This mechanism allows prompt authors to document intentional examples of vulnerable patterns (e.g., showing what `csurf` looks like before migration) without generating false positives on every scan.
106
+
107
+ ### Backtick suppression
108
+
109
+ Matches inside a properly closed backtick code span on the same line are suppressed. This prevents table rows like:
110
+
111
+ ```markdown
112
+ | `"command": "npx"` in MCP config | HIGH | ...
113
+ ```
114
+
115
+ from triggering the `Unpinned npx MCP Server` pattern.
116
+
117
+ The rule: suppress when there is an **odd** number of backticks before the match AND at least one closing backtick after it on the same line.
118
+
119
+ ---
120
+
121
+ ## Finding object schema
122
+
123
+ ```javascript
124
+ {
125
+ severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW',
126
+ name: string, // pattern display name, e.g. "SQL Injection"
127
+ file: string, // relative path from projectDir
128
+ line: number, // 1-indexed line number
129
+ snippet: string, // first 80 chars of the matched line (trimmed)
130
+ inTestFile: boolean,
131
+ likelyFalsePositive: boolean,
132
+ }
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Adding a new pattern
138
+
139
+ All vulnerability patterns live in the `VULN_PATTERNS` array in `lib/scanner.js`. Each entry is:
140
+
141
+ ```javascript
142
+ {
143
+ name: 'Display Name', // shown in the report
144
+ severity: 'HIGH', // CRITICAL | HIGH | MEDIUM | LOW
145
+ pattern: /regex/i, // matched against each line of each file
146
+ skipInTests: true, // optional — mark likelyFalsePositive when matched in test files
147
+ }
148
+ ```
149
+
150
+ Prompt-specific patterns live in `PROMPT_PATTERNS`:
151
+
152
+ ```javascript
153
+ {
154
+ name: 'Display Name',
155
+ severity: 'HIGH',
156
+ pattern: /regex/,
157
+ skipCommentLine: true, // optional — suppress matches on lines starting with // or #
158
+ }
159
+ ```
160
+
161
+ After adding a pattern, add a corresponding unit test in `__tests__/unit/scanner.test.js` with both a true-positive and a false-positive case.
@@ -0,0 +1,184 @@
1
+ # TDD Remediation Protocol
2
+
3
+ Security patching without tests is guesswork. The Red-Green-Refactor loop turns every vulnerability into a provable, reproducible closure: you prove the hole exists, you close it, and you prove it is closed.
4
+
5
+ ---
6
+
7
+ ## The three phases
8
+
9
+ ```
10
+ RED → write the exploit test → it MUST fail (vulnerability confirmed)
11
+ GREEN → apply the patch → test MUST pass (vulnerability closed)
12
+ REFACTOR → run the full suite → all MUST pass (no regressions)
13
+ ```
14
+
15
+ **Do not move to the next vulnerability until the current one completes all three phases.**
16
+
17
+ ---
18
+
19
+ ## Phase 1 — Red (Exploit)
20
+
21
+ Write a test that actively attempts the breach. The test must fail on the **security assertion**, not just crash the app.
22
+
23
+ ```javascript
24
+ // Wrong Red: test fails because the app throws 500
25
+ expect(res.status).toBe(403); // ← fails because app returned 500
26
+
27
+ // Correct Red: test fails because the vulnerability is open
28
+ expect(res.status).toBe(403); // ← fails because app returned 200 with data
29
+ ```
30
+
31
+ Place the test in your security test directory (`__tests__/security/`, `tests/security/`, or `test/security/`) so it is picked up by the `test:security` CI job.
32
+
33
+ ### Framework templates
34
+
35
+ **Jest / Supertest (Node.js)**
36
+ ```javascript
37
+ const request = require('supertest');
38
+ const app = require('../../app');
39
+
40
+ describe('[VulnType] — Red Phase', () => {
41
+ it('SHOULD block [exploit description]', async () => {
42
+ const res = await request(app)
43
+ .post('/api/vulnerable-endpoint')
44
+ .send({ input: '<exploit payload>' });
45
+
46
+ expect(res.status).toBe(403); // currently 200 — MUST fail (Red)
47
+ expect(res.body.data).not.toContain('<exploit payload>');
48
+ });
49
+ });
50
+ ```
51
+
52
+ **PyTest (Python)**
53
+ ```python
54
+ def test_exploit_blocked(client, attacker_token):
55
+ response = client.post(
56
+ '/api/vulnerable-endpoint',
57
+ json={'input': '<exploit payload>'},
58
+ headers={'Authorization': f'Bearer {attacker_token}'}
59
+ )
60
+ assert response.status_code == 403 # currently 200 — RED
61
+ ```
62
+
63
+ **Vitest + Testing Library (React / Next.js)**
64
+ ```typescript
65
+ test('SHOULD NOT store auth token in localStorage', async () => {
66
+ render(<LoginForm />);
67
+ fireEvent.submit(screen.getByRole('form'));
68
+ await waitFor(() => {
69
+ expect(localStorage.getItem('token')).toBeNull(); // currently set — RED
70
+ });
71
+ });
72
+ ```
73
+
74
+ **flutter_test (Flutter)**
75
+ ```dart
76
+ test('SHOULD NOT store auth token in SharedPreferences', () async {
77
+ SharedPreferences.setMockInitialValues({});
78
+ await simulateLogin(username: 'user', password: 'password');
79
+ final prefs = await SharedPreferences.getInstance();
80
+ expect(prefs.getString('token'), isNull); // currently stored — RED
81
+ });
82
+ ```
83
+
84
+ See [`prompts/red-phase.md`](../prompts/red-phase.md) for vulnerability-specific exploit strategies.
85
+
86
+ ---
87
+
88
+ ## Phase 2 — Green (Patch)
89
+
90
+ Apply the **minimum code change** that makes the exploit test pass. A targeted fix is safer than a rewrite.
91
+
92
+ 1. Identify the root cause — a 500 error is not a security fix
93
+ 2. Apply the narrowest patch that closes the vulnerability
94
+ 3. Run `npm run test:security` — the exploit test must now pass
95
+ 4. If the test still fails, the patch is incomplete — do not advance
96
+
97
+ See [`prompts/green-phase.md`](../prompts/green-phase.md) for vulnerability-specific patch strategies with before/after code examples covering:
98
+
99
+ - IDOR / tenant isolation
100
+ - XSS and `dangerouslySetInnerHTML`
101
+ - SQL injection (parameterized queries)
102
+ - Command injection (argument arrays)
103
+ - Path traversal (resolve + bounds check)
104
+ - Broken auth (JWT middleware)
105
+ - Next.js API route auth
106
+ - React Native / Expo sensitive storage migration
107
+ - Flutter sensitive storage migration
108
+ - SSRF (URL allowlist)
109
+ - Open redirect (relative-only)
110
+ - NoSQL injection (operator sanitization)
111
+ - Mass assignment (field allowlisting)
112
+ - Prototype pollution (key sanitization)
113
+ - Weak crypto (bcrypt/argon2)
114
+ - Missing rate limiting
115
+ - Missing security headers (Helmet)
116
+ - TLS bypass removal
117
+
118
+ ---
119
+
120
+ ## Phase 3 — Refactor (Regression)
121
+
122
+ Run the **full** test suite — security tests plus all pre-existing functional and integration tests.
123
+
124
+ ```bash
125
+ npm test # Node.js
126
+ pytest # Python
127
+ go test ./... # Go
128
+ flutter test # Flutter
129
+ ```
130
+
131
+ **If any pre-existing test now fails, stop and revert.** Return to Phase 2 with a narrower approach. A security fix that breaks functionality is a failed fix.
132
+
133
+ ### Regression checklist
134
+
135
+ - [ ] Happy-path flows still work — legitimate users can access their own resources
136
+ - [ ] Error messages are safe — no stack traces or internal paths in error responses
137
+ - [ ] Auth bypass not introduced — the fix doesn't open a new unprotected code path
138
+ - [ ] No secrets committed — patch doesn't hardcode keys or tokens
139
+ - [ ] No debug logging left — remove any `console.log` added during patching
140
+
141
+ See [`prompts/refactor-phase.md`](../prompts/refactor-phase.md) for the full framework-specific regression checklist.
142
+
143
+ ---
144
+
145
+ ## Phase 4 — Hardening (Proactive)
146
+
147
+ After all vulnerabilities are remediated, apply defence-in-depth controls that make future vulnerabilities harder to introduce. See [`docs/hardening.md`](hardening.md) for the full guide.
148
+
149
+ Summary of controls:
150
+ - **Security headers** — `helmet()` applied before all routes; explicit CSP
151
+ - **CSRF protection** — `csrf-csrf` double-submit pattern (not deprecated `csurf`)
152
+ - **Rate limiting** — `express-rate-limit` on auth routes
153
+ - **Dependency audit** — `npm audit --audit-level=high` in CI
154
+ - **Secret history scan** — `gitleaks` / `trufflehog` to catch committed secrets
155
+ - **Error handling** — generic 500 messages in production, no stack traces
156
+ - **SRI** — subresource integrity hashes on third-party CDN assets
157
+ - **GitHub Actions pinning** — every `uses:` locked to a full commit SHA
158
+
159
+ ---
160
+
161
+ ## When to revert and retry
162
+
163
+ Revert the patch (`git checkout -- <file>`) and return to Phase 2 if:
164
+
165
+ - A functional test fails after applying the security fix
166
+ - The fix introduces a new 401/403 for a legitimate user flow
167
+ - Performance degrades measurably (e.g., O(n) queries replacing O(1))
168
+
169
+ When you retry, describe the constraint: *"The previous fix broke X — find a narrower approach that still closes the vulnerability."*
170
+
171
+ ---
172
+
173
+ ## Remediation Summary format
174
+
175
+ After all vulnerabilities are addressed, the agent outputs a table:
176
+
177
+ ```
178
+ ## Remediation Summary
179
+
180
+ | Vulnerability | File | Status | Test File | Fix Applied |
181
+ |---|---|---|---|---|
182
+ | SQLi | src/routes/users.js:34 | ✅ Fixed | __tests__/security/sqli-users.test.js | Parameterized query |
183
+ | IDOR | src/controllers/docs.js:87 | ✅ Fixed | __tests__/security/idor-docs.test.js | Ownership check added |
184
+ ```