@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.
- package/README.md +76 -23
- package/SKILL.md +1 -1
- package/docs/agentic-ai-security.md +202 -0
- package/docs/ci-cd.md +169 -0
- package/docs/hardening.md +267 -0
- package/docs/scanner.md +161 -0
- package/docs/tdd-protocol.md +184 -0
- package/docs/vulnerability-patterns.md +200 -0
- package/lib/scanner.js +71 -30
- package/package.json +3 -2
- package/workflows/tdd-audit.md +6 -0
|
@@ -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
|
package/docs/scanner.md
ADDED
|
@@ -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
|
+
```
|