@lhi/tdd-audit 1.1.1 → 1.2.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.
- package/README.md +1 -0
- package/SKILL.md +6 -1
- package/index.js +114 -19
- package/package.json +9 -2
- package/prompts/auto-audit.md +63 -0
- package/prompts/green-phase.md +164 -0
- package/prompts/red-phase.md +70 -0
- package/prompts/refactor-phase.md +16 -0
- package/templates/sample.exploit.test.dart +52 -0
- package/templates/sample.exploit.test.react.tsx +59 -0
- package/templates/workflows/security-tests.flutter.yml +26 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ node index.js
|
|
|
34
34
|
| `--claude` | Use `.claude/` instead of `.agents/` as the skill directory |
|
|
35
35
|
| `--with-hooks` | Install a pre-commit hook that blocks commits if security tests fail |
|
|
36
36
|
| `--skip-scan` | Skip the automatic vulnerability scan on install |
|
|
37
|
+
| `--scan-only` | Run the vulnerability scan without installing anything |
|
|
37
38
|
|
|
38
39
|
**Install to a Claude Code project with pre-commit protection:**
|
|
39
40
|
```bash
|
package/SKILL.md
CHANGED
|
@@ -9,7 +9,12 @@ Applying Test-Driven Development (TDD) to code that has already been generated r
|
|
|
9
9
|
|
|
10
10
|
## Autonomous Audit Mode
|
|
11
11
|
If the user asks you to "Run the TDD Remediation Auto-Audit" or asks you to implement this on your own:
|
|
12
|
-
1. **Explore**: Proactively use `Glob`, `Grep`, and `Read` to scan the repository. Focus on
|
|
12
|
+
1. **Explore**: Proactively use `Glob`, `Grep`, and `Read` to scan the repository. Focus on:
|
|
13
|
+
- **Backend/API**: `controllers/`, `routes/`, `api/`, `handlers/`, `middleware/`, `services/`, `models/`
|
|
14
|
+
- **React / Next.js**: `pages/api/`, `app/api/`, `components/`, `hooks/`, `context/`, `store/`
|
|
15
|
+
- **React Native / Expo**: `screens/`, `navigation/`, `app/`, `app.json`, `app.config.js`
|
|
16
|
+
- **Flutter / Dart**: `lib/screens/`, `lib/services/`, `lib/api/`, `lib/repositories/`, `pubspec.yaml`
|
|
17
|
+
Search for anti-patterns: unparameterized SQL queries, missing ownership checks, unsafe HTML rendering, command injection sinks, sensitive data in storage, TLS bypasses, hardcoded secrets. Full search patterns are in [auto-audit.md](./prompts/auto-audit.md).
|
|
13
18
|
2. **Plan**: Present a structured list of vulnerabilities (grouped by severity: CRITICAL / HIGH / MEDIUM / LOW) and get confirmation before making any changes.
|
|
14
19
|
3. **Self-Implement**: For *each* confirmed vulnerability, autonomously execute the complete 3-phase protocol:
|
|
15
20
|
- **[Phase 1 (Red)](./prompts/red-phase.md)**: Write the exploit test ensuring it fails.
|
package/index.js
CHANGED
|
@@ -9,17 +9,23 @@ const isLocal = args.includes('--local');
|
|
|
9
9
|
const isClaude = args.includes('--claude');
|
|
10
10
|
const withHooks = args.includes('--with-hooks');
|
|
11
11
|
const skipScan = args.includes('--skip-scan');
|
|
12
|
+
const scanOnly = args.includes('--scan-only');
|
|
12
13
|
|
|
13
14
|
const agentBaseDir = isLocal ? process.cwd() : os.homedir();
|
|
14
15
|
const agentDirName = isClaude ? '.claude' : '.agents';
|
|
15
16
|
const projectDir = process.cwd();
|
|
16
17
|
|
|
17
18
|
const targetSkillDir = path.join(agentBaseDir, agentDirName, 'skills', 'tdd-remediation');
|
|
18
|
-
const targetWorkflowDir =
|
|
19
|
+
const targetWorkflowDir = isClaude
|
|
20
|
+
? path.join(agentBaseDir, agentDirName, 'commands')
|
|
21
|
+
: path.join(agentBaseDir, agentDirName, 'workflows');
|
|
19
22
|
|
|
20
23
|
// ─── 1. Framework Detection ──────────────────────────────────────────────────
|
|
21
24
|
|
|
22
25
|
function detectFramework() {
|
|
26
|
+
// Flutter / Dart — check before package.json since a Flutter project may have both
|
|
27
|
+
if (fs.existsSync(path.join(projectDir, 'pubspec.yaml'))) return 'flutter';
|
|
28
|
+
|
|
23
29
|
const pkgPath = path.join(projectDir, 'package.json');
|
|
24
30
|
if (fs.existsSync(pkgPath)) {
|
|
25
31
|
try {
|
|
@@ -40,6 +46,25 @@ function detectFramework() {
|
|
|
40
46
|
return 'jest';
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
// Detect the UI framework for richer scan context (React, Next.js, RN, Expo, Flutter)
|
|
50
|
+
function detectAppFramework() {
|
|
51
|
+
if (fs.existsSync(path.join(projectDir, 'pubspec.yaml'))) return 'flutter';
|
|
52
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
53
|
+
if (fs.existsSync(pkgPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
56
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
57
|
+
if (deps.expo) return 'expo';
|
|
58
|
+
if (deps['react-native']) return 'react-native';
|
|
59
|
+
if (deps.next) return 'nextjs';
|
|
60
|
+
if (deps.react) return 'react';
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const appFramework = detectAppFramework();
|
|
67
|
+
|
|
43
68
|
const framework = detectFramework();
|
|
44
69
|
|
|
45
70
|
// ─── 2. Test Directory Detection ─────────────────────────────────────────────
|
|
@@ -68,10 +93,19 @@ const VULN_PATTERNS = [
|
|
|
68
93
|
{ name: 'XSS', severity: 'HIGH', pattern: /[^/]innerHTML\s*=(?!=)|dangerouslySetInnerHTML\s*=\s*\{\{|document\.write\s*\(|res\.send\s*\(`[^`]*\$\{req\./i },
|
|
69
94
|
{ name: 'Path Traversal', severity: 'HIGH', pattern: /(readFile|sendFile|createReadStream|open)\s*\(.*req\.(params|body|query)|path\.join\s*\([^)]*req\.(params|body|query)/i },
|
|
70
95
|
{ name: 'Broken Auth', severity: 'HIGH', pattern: /jwt\.decode\s*\((?![^;]*\.verify)|verify\s*:\s*false|secret\s*=\s*['"][a-z0-9]{1,20}['"]/i },
|
|
96
|
+
// Vibecoding / mobile stacks
|
|
97
|
+
{ name: 'Sensitive Storage', severity: 'HIGH', pattern: /(localStorage|AsyncStorage)\.setItem\s*\(\s*['"](token|password|secret|auth|jwt|api.?key)['"]/i },
|
|
98
|
+
{ name: 'TLS Bypass', severity: 'CRITICAL', pattern: /badCertificateCallback[^;]*=\s*true|rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0/i },
|
|
99
|
+
{ name: 'Hardcoded Secret', severity: 'CRITICAL', skipInTests: true, pattern: /(?:const|final|var|let|static)\s+(?:API_KEY|PRIVATE_KEY|SECRET_KEY|ACCESS_TOKEN|CLIENT_SECRET)\s*=\s*['"][A-Za-z0-9+/=_\-]{20,}['"]/i },
|
|
100
|
+
{ name: 'eval() Injection', severity: 'HIGH', pattern: /\beval\s*\([^)]*(?:route\.params|searchParams\.get|req\.(query|body)|params\[)/i },
|
|
101
|
+
// Common vibecoding anti-patterns
|
|
102
|
+
{ name: 'Insecure Random', severity: 'HIGH', pattern: /(?:token|sessionId|nonce|secret|csrf)\w*\s*=.*Math\.random\(\)|Math\.random\(\).*(?:token|session|nonce|secret)/i },
|
|
103
|
+
{ name: 'Sensitive Log', severity: 'MEDIUM', skipInTests: true, pattern: /console\.(log|info|debug)\([^)]*(?:token|password|secret|jwt|authorization|apiKey|api_key)/i },
|
|
104
|
+
{ name: 'Secret Fallback', severity: 'HIGH', pattern: /process\.env\.\w+\s*\|\|\s*['"][A-Za-z0-9+/=_\-]{10,}['"]/i },
|
|
71
105
|
];
|
|
72
106
|
|
|
73
|
-
const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go']);
|
|
74
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', '__pycache__', 'venv', '.venv', 'vendor']);
|
|
107
|
+
const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go', '.dart']);
|
|
108
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', '__pycache__', 'venv', '.venv', 'vendor', '.expo', '.dart_tool', '.pub-cache']);
|
|
75
109
|
|
|
76
110
|
function* walkFiles(dir) {
|
|
77
111
|
let entries;
|
|
@@ -84,9 +118,43 @@ function* walkFiles(dir) {
|
|
|
84
118
|
}
|
|
85
119
|
}
|
|
86
120
|
|
|
121
|
+
// Returns true for test/spec files — used to down-weight false-positive-prone patterns
|
|
122
|
+
function isTestFile(filePath) {
|
|
123
|
+
const rel = path.relative(projectDir, filePath).replace(/\\/g, '/');
|
|
124
|
+
return /[._-]test\.[a-z]+$|[._-]spec\.[a-z]+$|_test\.dart$|\/tests?\/|\/spec\/|\/test_/.test(rel);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Scan app.json / app.config.* for embedded secrets (common Expo vibecoding issue)
|
|
128
|
+
function scanAppConfig() {
|
|
129
|
+
const findings = [];
|
|
130
|
+
const configCandidates = ['app.json', 'app.config.js', 'app.config.ts'];
|
|
131
|
+
const secretPattern = /['"]?(?:apiKey|api_key|secret|privateKey|accessToken|clientSecret)['"]?\s*[:=]\s*['"][A-Za-z0-9+/=_\-]{20,}['"]/i;
|
|
132
|
+
|
|
133
|
+
for (const name of configCandidates) {
|
|
134
|
+
const filePath = path.join(projectDir, name);
|
|
135
|
+
if (!fs.existsSync(filePath)) continue;
|
|
136
|
+
let lines;
|
|
137
|
+
try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
if (secretPattern.test(lines[i])) {
|
|
140
|
+
findings.push({
|
|
141
|
+
severity: 'CRITICAL',
|
|
142
|
+
name: 'Config Secret',
|
|
143
|
+
file: name,
|
|
144
|
+
line: i + 1,
|
|
145
|
+
snippet: lines[i].trim().slice(0, 80),
|
|
146
|
+
inTestFile: false,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return findings;
|
|
152
|
+
}
|
|
153
|
+
|
|
87
154
|
function quickScan() {
|
|
88
155
|
const findings = [];
|
|
89
156
|
for (const filePath of walkFiles(projectDir)) {
|
|
157
|
+
const inTest = isTestFile(filePath);
|
|
90
158
|
let lines;
|
|
91
159
|
try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
|
|
92
160
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -98,13 +166,15 @@ function quickScan() {
|
|
|
98
166
|
file: path.relative(projectDir, filePath),
|
|
99
167
|
line: i + 1,
|
|
100
168
|
snippet: lines[i].trim().slice(0, 80),
|
|
169
|
+
inTestFile: inTest,
|
|
170
|
+
likelyFalsePositive: inTest && !!vuln.skipInTests,
|
|
101
171
|
});
|
|
102
172
|
break; // one finding per line
|
|
103
173
|
}
|
|
104
174
|
}
|
|
105
175
|
}
|
|
106
176
|
}
|
|
107
|
-
return findings;
|
|
177
|
+
return [...findings, ...scanAppConfig()];
|
|
108
178
|
}
|
|
109
179
|
|
|
110
180
|
function printFindings(findings) {
|
|
@@ -112,24 +182,47 @@ function printFindings(findings) {
|
|
|
112
182
|
console.log(' ✅ No obvious vulnerability patterns detected.\n');
|
|
113
183
|
return;
|
|
114
184
|
}
|
|
185
|
+
const real = findings.filter(f => !f.likelyFalsePositive);
|
|
186
|
+
const noisy = findings.filter(f => f.likelyFalsePositive);
|
|
187
|
+
|
|
115
188
|
const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
|
|
116
|
-
for (const f of
|
|
189
|
+
for (const f of real) (bySeverity[f.severity] || bySeverity.LOW).push(f);
|
|
117
190
|
const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
|
|
118
191
|
|
|
119
|
-
console.log(`\n Found ${
|
|
192
|
+
console.log(`\n Found ${real.length} potential issue(s)${noisy.length ? ` (+${noisy.length} in test files — see below)` : ''}:\n`);
|
|
120
193
|
for (const [sev, list] of Object.entries(bySeverity)) {
|
|
121
194
|
if (!list.length) continue;
|
|
122
195
|
for (const f of list) {
|
|
123
|
-
|
|
196
|
+
const testBadge = f.inTestFile ? ' [test file]' : '';
|
|
197
|
+
console.log(` ${icons[sev]} [${sev}] ${f.name} — ${f.file}:${f.line}${testBadge}`);
|
|
124
198
|
console.log(` ${f.snippet}`);
|
|
125
199
|
}
|
|
126
200
|
}
|
|
201
|
+
|
|
202
|
+
if (noisy.length) {
|
|
203
|
+
console.log('\n ⚪ Likely intentional (in test files — verify manually):');
|
|
204
|
+
for (const f of noisy) {
|
|
205
|
+
console.log(` ${f.name} — ${f.file}:${f.line}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
127
209
|
console.log('\n Run /tdd-audit in your agent to remediate.\n');
|
|
128
210
|
}
|
|
129
211
|
|
|
130
|
-
// ─── 4.
|
|
212
|
+
// ─── 4. Scan-only early exit ──────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
if (scanOnly) {
|
|
215
|
+
process.stdout.write('\n🔍 Scanning for vulnerability patterns...');
|
|
216
|
+
const findings = quickScan();
|
|
217
|
+
process.stdout.write('\n');
|
|
218
|
+
printFindings(findings);
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── 5. Install Skill Files ───────────────────────────────────────────────────
|
|
131
223
|
|
|
132
|
-
|
|
224
|
+
const appLabel = appFramework ? `, app: ${appFramework}` : '';
|
|
225
|
+
console.log(`\nInstalling TDD Remediation Skill (${isLocal ? 'local' : 'global'}, framework: ${framework}${appLabel}, test dir: ${testBaseDir}/)...\n`);
|
|
133
226
|
|
|
134
227
|
if (!fs.existsSync(targetSkillDir)) fs.mkdirSync(targetSkillDir, { recursive: true });
|
|
135
228
|
|
|
@@ -147,11 +240,12 @@ if (!fs.existsSync(targetTestDir)) {
|
|
|
147
240
|
}
|
|
148
241
|
|
|
149
242
|
const testTemplateMap = {
|
|
150
|
-
jest:
|
|
151
|
-
vitest:
|
|
152
|
-
mocha:
|
|
153
|
-
pytest:
|
|
154
|
-
go:
|
|
243
|
+
jest: 'sample.exploit.test.js',
|
|
244
|
+
vitest: 'sample.exploit.test.vitest.js',
|
|
245
|
+
mocha: 'sample.exploit.test.js',
|
|
246
|
+
pytest: 'sample.exploit.test.pytest.py',
|
|
247
|
+
go: 'sample.exploit.test.go',
|
|
248
|
+
flutter: 'sample.exploit.test.dart',
|
|
155
249
|
};
|
|
156
250
|
|
|
157
251
|
const testTemplateName = testTemplateMap[framework];
|
|
@@ -204,11 +298,12 @@ const ciWorkflowPath = path.join(ciWorkflowDir, 'security-tests.yml');
|
|
|
204
298
|
|
|
205
299
|
if (!fs.existsSync(ciWorkflowPath)) {
|
|
206
300
|
const ciTemplateMap = {
|
|
207
|
-
jest:
|
|
208
|
-
vitest:
|
|
209
|
-
mocha:
|
|
210
|
-
pytest:
|
|
211
|
-
go:
|
|
301
|
+
jest: 'security-tests.node.yml',
|
|
302
|
+
vitest: 'security-tests.node.yml',
|
|
303
|
+
mocha: 'security-tests.node.yml',
|
|
304
|
+
pytest: 'security-tests.python.yml',
|
|
305
|
+
go: 'security-tests.go.yml',
|
|
306
|
+
flutter: 'security-tests.flutter.yml',
|
|
212
307
|
};
|
|
213
308
|
const ciTemplatePath = path.join(__dirname, 'templates', 'workflows', ciTemplateMap[framework]);
|
|
214
309
|
if (fs.existsSync(ciTemplatePath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lhi/tdd-audit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Anti-Gravity Skill for TDD Remediation. Patches security vulnerabilities using a Red-Green-Refactor protocol with automated exploit tests.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,14 @@
|
|
|
31
31
|
"audit",
|
|
32
32
|
"claude",
|
|
33
33
|
"ai-agent",
|
|
34
|
-
"skill"
|
|
34
|
+
"skill",
|
|
35
|
+
"react",
|
|
36
|
+
"nextjs",
|
|
37
|
+
"react-native",
|
|
38
|
+
"expo",
|
|
39
|
+
"flutter",
|
|
40
|
+
"mobile-security",
|
|
41
|
+
"vibecoding"
|
|
35
42
|
],
|
|
36
43
|
"engines": {
|
|
37
44
|
"node": ">=16.7.0"
|
package/prompts/auto-audit.md
CHANGED
|
@@ -6,11 +6,29 @@ When invoked in Auto-Audit mode, proactively secure the user's entire repository
|
|
|
6
6
|
|
|
7
7
|
### 0a. Explore the Architecture
|
|
8
8
|
Use `Glob` and `Read` to understand the project structure. Focus on:
|
|
9
|
+
|
|
10
|
+
**Backend / API**
|
|
9
11
|
- `controllers/`, `routes/`, `api/`, `handlers/` — request entry points
|
|
10
12
|
- `services/`, `models/`, `db/`, `repositories/` — data access
|
|
11
13
|
- `middleware/`, `utils/`, `helpers/`, `lib/` — shared utilities
|
|
12
14
|
- Config files: `*.env`, `config.js`, `settings.py` — secrets and security settings
|
|
13
15
|
|
|
16
|
+
**React / Next.js**
|
|
17
|
+
- `pages/api/`, `app/api/` — Next.js API routes (check for missing auth)
|
|
18
|
+
- `components/`, `app/`, `pages/` — UI components (check for `dangerouslySetInnerHTML`, `eval`)
|
|
19
|
+
- `hooks/`, `context/`, `store/` — state management (check for sensitive data leakage)
|
|
20
|
+
|
|
21
|
+
**React Native / Expo**
|
|
22
|
+
- `screens/`, `navigation/`, `app/` — screen components (check `route.params` usage)
|
|
23
|
+
- `services/`, `api/`, `utils/` — API calls (check TLS config, token storage)
|
|
24
|
+
- `app.json`, `app.config.js` — Expo config (check for embedded keys)
|
|
25
|
+
|
|
26
|
+
**Flutter / Dart**
|
|
27
|
+
- `lib/screens/`, `lib/pages/`, `lib/views/` — UI layer
|
|
28
|
+
- `lib/services/`, `lib/api/`, `lib/repositories/` — data layer (check HTTP client config)
|
|
29
|
+
- `lib/utils/`, `lib/helpers/` — shared utilities
|
|
30
|
+
- `pubspec.yaml` — dependency audit
|
|
31
|
+
|
|
14
32
|
### 0b. Search for Anti-Patterns
|
|
15
33
|
Use `Grep` with the following patterns to surface candidates. Read the matched files to confirm before reporting.
|
|
16
34
|
|
|
@@ -71,6 +89,51 @@ router\.(post|put|delete) # mutation routes (check for rate-limit middleware)
|
|
|
71
89
|
app\.post\( # POST handlers (check for rate-limit middleware)
|
|
72
90
|
```
|
|
73
91
|
|
|
92
|
+
**Sensitive Storage (React / React Native / Expo)**
|
|
93
|
+
```
|
|
94
|
+
AsyncStorage\.setItem.*token # token stored in unencrypted AsyncStorage
|
|
95
|
+
localStorage\.setItem.*token # token stored in localStorage (XSS-accessible)
|
|
96
|
+
AsyncStorage\.setItem.*password # password stored in plain AsyncStorage
|
|
97
|
+
SecureStore vs AsyncStorage # confirm sensitive values use expo-secure-store
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**TLS / Certificate Bypass**
|
|
101
|
+
```
|
|
102
|
+
rejectUnauthorized.*false # Node.js TLS verification disabled
|
|
103
|
+
badCertificateCallback.*true # Dart/Flutter TLS bypass
|
|
104
|
+
NODE_TLS_REJECT_UNAUTHORIZED=0 # env-level TLS disable
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Hardcoded Secrets (vibecoded apps)**
|
|
108
|
+
```
|
|
109
|
+
API_KEY\s*=\s*['"][A-Za-z0-9]{20,} # hardcoded API key in source
|
|
110
|
+
PRIVATE_KEY\s*=\s*['"] # private key in source
|
|
111
|
+
SECRET_KEY\s*=\s*['"] # secret embedded in code
|
|
112
|
+
process\.env\.\w+\s*\|\|\s*['"] # env var with hardcoded fallback
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Next.js API Route Auth**
|
|
116
|
+
```
|
|
117
|
+
export.*async.*handler # Next.js API route — check for missing auth guard
|
|
118
|
+
export default.*req.*res # pages/api handler — verify authentication
|
|
119
|
+
getServerSideProps.*params # SSR with params — check for injection
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**React Native / Expo Navigation Injection**
|
|
123
|
+
```
|
|
124
|
+
route\.params\.\w+.*query # route param passed to DB/API query
|
|
125
|
+
route\.params\.\w+.*fetch # route param used in fetch URL
|
|
126
|
+
navigation\.navigate.*params # user-controlled navigation params
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Flutter / Dart**
|
|
130
|
+
```
|
|
131
|
+
http\.get\( # raw http call — check for TLS config
|
|
132
|
+
http\.post\( # raw http call — check for TLS config
|
|
133
|
+
SharedPreferences.*setString.*token # token in unencrypted SharedPreferences
|
|
134
|
+
Platform\.environment\[ # env access in Flutter — check for secrets
|
|
135
|
+
```
|
|
136
|
+
|
|
74
137
|
### 0c. Present Findings
|
|
75
138
|
Before touching any code, output a structured **Audit Report** with this format:
|
|
76
139
|
|
package/prompts/green-phase.md
CHANGED
|
@@ -208,3 +208,167 @@ function requireAuth(req, res, next) {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### React: XSS via dangerouslySetInnerHTML
|
|
215
|
+
|
|
216
|
+
**Root cause:** User-generated content is passed directly to `dangerouslySetInnerHTML` without sanitization.
|
|
217
|
+
|
|
218
|
+
**Fix:** Sanitize with DOMPurify before rendering. Never pass raw user input to `dangerouslySetInnerHTML`.
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
// BEFORE (vulnerable)
|
|
222
|
+
<div dangerouslySetInnerHTML={{ __html: userContent }} />
|
|
223
|
+
|
|
224
|
+
// AFTER
|
|
225
|
+
import DOMPurify from 'dompurify';
|
|
226
|
+
|
|
227
|
+
const clean = DOMPurify.sanitize(userContent, {
|
|
228
|
+
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
|
|
229
|
+
ALLOWED_ATTR: ['href'],
|
|
230
|
+
});
|
|
231
|
+
<div dangerouslySetInnerHTML={{ __html: clean }} />
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Install:** `npm install dompurify @types/dompurify`
|
|
235
|
+
For SSR (Next.js): `npm install isomorphic-dompurify` instead.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### Next.js: Missing Auth on API Routes
|
|
240
|
+
|
|
241
|
+
**Root cause:** API route handlers in `pages/api/` or `app/api/` are publicly accessible with no authentication check.
|
|
242
|
+
|
|
243
|
+
**Fix — Option A (per-route wrapper):**
|
|
244
|
+
```typescript
|
|
245
|
+
// lib/withAuth.ts
|
|
246
|
+
import jwt from 'jsonwebtoken';
|
|
247
|
+
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
|
|
248
|
+
|
|
249
|
+
export function withAuth(handler: NextApiHandler): NextApiHandler {
|
|
250
|
+
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
251
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
252
|
+
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
|
253
|
+
try {
|
|
254
|
+
(req as any).user = jwt.verify(token, process.env.JWT_SECRET!);
|
|
255
|
+
return handler(req, res);
|
|
256
|
+
} catch {
|
|
257
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// pages/api/user.ts
|
|
263
|
+
import { withAuth } from '../../lib/withAuth';
|
|
264
|
+
export default withAuth((req, res) => res.json({ user: (req as any).user }));
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Fix — Option B (global middleware, preferred for App Router):**
|
|
268
|
+
```typescript
|
|
269
|
+
// middleware.ts (root of project — protects all /api routes)
|
|
270
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
271
|
+
import { jwtVerify } from 'jose';
|
|
272
|
+
|
|
273
|
+
export async function middleware(request: NextRequest) {
|
|
274
|
+
const token = request.headers.get('authorization')?.split(' ')[1];
|
|
275
|
+
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
276
|
+
try {
|
|
277
|
+
await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET!));
|
|
278
|
+
return NextResponse.next();
|
|
279
|
+
} catch {
|
|
280
|
+
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const config = { matcher: '/api/:path*' };
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### React Native / Expo: Sensitive Storage Migration
|
|
290
|
+
|
|
291
|
+
**Root cause:** Auth tokens stored in `AsyncStorage` are unencrypted and readable on rooted/jailbroken devices.
|
|
292
|
+
|
|
293
|
+
**Fix:** Replace with `expo-secure-store`, which uses iOS Keychain and Android EncryptedSharedPreferences.
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
// BEFORE (vulnerable)
|
|
297
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
298
|
+
await AsyncStorage.setItem('token', userToken);
|
|
299
|
+
const token = await AsyncStorage.getItem('token');
|
|
300
|
+
|
|
301
|
+
// AFTER
|
|
302
|
+
import * as SecureStore from 'expo-secure-store';
|
|
303
|
+
await SecureStore.setItemAsync('token', userToken);
|
|
304
|
+
const token = await SecureStore.getItemAsync('token');
|
|
305
|
+
// On logout:
|
|
306
|
+
await SecureStore.deleteItemAsync('token');
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Install:** `npx expo install expo-secure-store`
|
|
310
|
+
**Note:** `SecureStore` is device-bound and not available in Expo Go web preview — check `SecureStore.isAvailableAsync()` for web fallbacks.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
### Flutter: Sensitive Storage Migration
|
|
315
|
+
|
|
316
|
+
**Root cause:** Auth tokens stored in `SharedPreferences` are plain text in app storage — readable on rooted/jailbroken devices.
|
|
317
|
+
|
|
318
|
+
**Fix:** Replace with `flutter_secure_storage`, which uses iOS Keychain and Android EncryptedSharedPreferences.
|
|
319
|
+
|
|
320
|
+
```dart
|
|
321
|
+
// BEFORE (vulnerable)
|
|
322
|
+
final prefs = await SharedPreferences.getInstance();
|
|
323
|
+
await prefs.setString('token', userToken);
|
|
324
|
+
final token = prefs.getString('token');
|
|
325
|
+
|
|
326
|
+
// AFTER
|
|
327
|
+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
328
|
+
|
|
329
|
+
const _storage = FlutterSecureStorage();
|
|
330
|
+
await _storage.write(key: 'token', value: userToken);
|
|
331
|
+
final token = await _storage.read(key: 'token');
|
|
332
|
+
// On logout:
|
|
333
|
+
await _storage.delete(key: 'token');
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**pubspec.yaml:**
|
|
337
|
+
```yaml
|
|
338
|
+
dependencies:
|
|
339
|
+
flutter_secure_storage: ^9.0.0
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
### TLS Bypass Fix (Node.js + Flutter/Dart)
|
|
345
|
+
|
|
346
|
+
**Root cause:** TLS certificate verification is explicitly disabled, allowing man-in-the-middle attacks.
|
|
347
|
+
|
|
348
|
+
**Fix:** Remove the bypass entirely. For internal CAs, provide the cert — don't disable verification.
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
// BEFORE (vulnerable — Node.js)
|
|
352
|
+
const https = require('https');
|
|
353
|
+
const agent = new https.Agent({ rejectUnauthorized: false }); // ❌
|
|
354
|
+
|
|
355
|
+
// AFTER — remove the override; default is rejectUnauthorized: true ✅
|
|
356
|
+
const agent = new https.Agent();
|
|
357
|
+
|
|
358
|
+
// For internal/self-signed CAs in staging environments:
|
|
359
|
+
// NODE_EXTRA_CA_CERTS=/path/to/internal-ca.crt node server.js
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```dart
|
|
363
|
+
// BEFORE (vulnerable — Flutter/Dart)
|
|
364
|
+
final client = HttpClient()
|
|
365
|
+
..badCertificateCallback = (cert, host, port) => true; // ❌
|
|
366
|
+
|
|
367
|
+
// AFTER — remove the callback (default validates certs) ✅
|
|
368
|
+
final client = HttpClient();
|
|
369
|
+
|
|
370
|
+
// For a private CA in integration tests only:
|
|
371
|
+
final context = SecurityContext()
|
|
372
|
+
..setTrustedCertificates('test/certs/ca.crt');
|
|
373
|
+
final client = HttpClient(context: context);
|
|
374
|
+
```
|
package/prompts/red-phase.md
CHANGED
|
@@ -120,3 +120,73 @@ def test_vuln_type_exploit(client, attacker_token):
|
|
|
120
120
|
)
|
|
121
121
|
assert response.status_code == 403 # currently 200 — RED
|
|
122
122
|
```
|
|
123
|
+
|
|
124
|
+
### React / Next.js (Vitest + Testing Library)
|
|
125
|
+
```typescript
|
|
126
|
+
// Sensitive storage: token must NOT land in localStorage
|
|
127
|
+
import { render, fireEvent, waitFor } from '@testing-library/react';
|
|
128
|
+
import LoginForm from '../../components/LoginForm';
|
|
129
|
+
|
|
130
|
+
test('SHOULD NOT store auth token in localStorage', async () => {
|
|
131
|
+
render(<LoginForm />);
|
|
132
|
+
fireEvent.submit(screen.getByRole('form'));
|
|
133
|
+
await waitFor(() => {
|
|
134
|
+
expect(localStorage.getItem('token')).toBeNull(); // currently set — RED
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// XSS: dangerouslySetInnerHTML must not accept unsanitized input
|
|
139
|
+
test('SHOULD sanitize user content before rendering', () => {
|
|
140
|
+
const xssPayload = '<script>alert(1)</script>';
|
|
141
|
+
const { container } = render(<CommentBody content={xssPayload} />);
|
|
142
|
+
expect(container.innerHTML).not.toContain('<script>'); // currently reflected — RED
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### React Native / Expo (Jest)
|
|
147
|
+
```javascript
|
|
148
|
+
// Route param injection: params must be validated before API use
|
|
149
|
+
import { renderRouter, screen } from 'expo-router/testing-library';
|
|
150
|
+
|
|
151
|
+
test('SHOULD NOT pass raw route params to API query', async () => {
|
|
152
|
+
const maliciousParam = "1 UNION SELECT * FROM users";
|
|
153
|
+
// Render the screen with a crafted route param
|
|
154
|
+
renderRouter({ initialUrl: `/item/${encodeURIComponent(maliciousParam)}` });
|
|
155
|
+
// Assert the API was NOT called with the raw param
|
|
156
|
+
expect(mockApiClient.getItem).not.toHaveBeenCalledWith(maliciousParam); // currently called — RED
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Sensitive storage: tokens must use SecureStore, not AsyncStorage
|
|
160
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
161
|
+
|
|
162
|
+
test('SHOULD NOT store token in plain AsyncStorage', async () => {
|
|
163
|
+
await simulateLogin({ username: 'user', password: 'pass' });
|
|
164
|
+
const stored = await AsyncStorage.getItem('token');
|
|
165
|
+
expect(stored).toBeNull(); // currently stored in plain AsyncStorage — RED
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Flutter / Dart (flutter_test)
|
|
170
|
+
```dart
|
|
171
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
172
|
+
import 'package:shared_preferences/shared_preferences.dart';
|
|
173
|
+
|
|
174
|
+
void main() {
|
|
175
|
+
// Sensitive storage: token must NOT be in unencrypted SharedPreferences
|
|
176
|
+
test('SHOULD NOT store auth token in SharedPreferences', () async {
|
|
177
|
+
SharedPreferences.setMockInitialValues({});
|
|
178
|
+
await simulateLogin(username: 'user', password: 'password');
|
|
179
|
+
final prefs = await SharedPreferences.getInstance();
|
|
180
|
+
expect(prefs.getString('token'), isNull,
|
|
181
|
+
reason: 'Tokens must not be in unencrypted SharedPreferences — use flutter_secure_storage'); // currently stored — RED
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// TLS bypass: HTTP client must not disable certificate validation
|
|
185
|
+
test('SHOULD enforce TLS certificate verification', () {
|
|
186
|
+
final client = buildHttpClient(); // the app's HTTP client factory
|
|
187
|
+
// Inspect that no badCertificateCallback bypasses verification
|
|
188
|
+
expect(client.badCertificateCallback, isNull,
|
|
189
|
+
reason: 'badCertificateCallback must not be set to bypass TLS'); // currently bypassed — RED
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
```
|
|
@@ -22,6 +22,22 @@ Go through this checklist before closing the vulnerability:
|
|
|
22
22
|
- [ ] **Performance acceptable** — the patch doesn't add unbounded DB queries or blocking I/O
|
|
23
23
|
- [ ] **No secrets in code** — patch doesn't hardcode keys, tokens, or credentials
|
|
24
24
|
|
|
25
|
+
**React / Next.js additions:**
|
|
26
|
+
- [ ] **`dangerouslySetInnerHTML` removed or wrapped** — confirm DOMPurify is imported and called before all remaining usages
|
|
27
|
+
- [ ] **Next.js middleware matcher is correct** — `/api/:path*` or tighter; public routes (health checks, webhooks) still reachable
|
|
28
|
+
- [ ] **`app.json` / `.env.local` clean** — no API keys or secrets committed; `*.env` is in `.gitignore`
|
|
29
|
+
|
|
30
|
+
**React Native / Expo additions:**
|
|
31
|
+
- [ ] **`AsyncStorage` fully migrated** — no remaining `setItem('token', ...)` calls; `expo-secure-store` in `package.json`
|
|
32
|
+
- [ ] **Offline token refresh still works** — `SecureStore.getItemAsync` is called in the right lifecycle (not before `SecureStore.isAvailableAsync()` on web)
|
|
33
|
+
- [ ] **Deep link params validated** — any `route.params` passed to API calls are sanitized or type-checked
|
|
34
|
+
|
|
35
|
+
**Flutter additions:**
|
|
36
|
+
- [ ] **`flutter_secure_storage` in `pubspec.yaml`** — dependency present and `flutter pub get` ran
|
|
37
|
+
- [ ] **No remaining `SharedPreferences` calls for sensitive keys** — grep for `prefs.getString('token')`, `prefs.setString('password', ...)`
|
|
38
|
+
- [ ] **TLS `badCertificateCallback` fully removed** — grep the entire `lib/` directory for `badCertificateCallback`
|
|
39
|
+
- [ ] **iOS entitlements updated if needed** — `flutter_secure_storage` requires Keychain Sharing capability on iOS
|
|
40
|
+
|
|
25
41
|
### Step 3: Clean the patch
|
|
26
42
|
- Remove any debugging `console.log` or `print` statements added during patching
|
|
27
43
|
- Extract reusable security logic into middleware or utility functions if it appears in more than one place
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/// TDD Remediation: Red Phase Sample Test (Flutter / Dart)
|
|
2
|
+
///
|
|
3
|
+
/// Replace the boilerplate below with the specific exploit you are verifying.
|
|
4
|
+
/// This test MUST fail initially (Red Phase). Once you apply the fix, it MUST pass (Green Phase).
|
|
5
|
+
///
|
|
6
|
+
/// Run with: flutter test test/security/
|
|
7
|
+
|
|
8
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
9
|
+
// import 'package:shared_preferences/shared_preferences.dart';
|
|
10
|
+
// import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
11
|
+
// import '../../lib/services/auth_service.dart'; // update with your auth service path
|
|
12
|
+
|
|
13
|
+
void main() {
|
|
14
|
+
group('Security Vulnerability Remediation - Red Phase', () {
|
|
15
|
+
|
|
16
|
+
// ── Example 1: Sensitive Storage ─────────────────────────────────────────
|
|
17
|
+
// test('SHOULD NOT store auth token in plain SharedPreferences', () async {
|
|
18
|
+
// SharedPreferences.setMockInitialValues({});
|
|
19
|
+
//
|
|
20
|
+
// // Act: simulate what the app does after login
|
|
21
|
+
// // await AuthService().login(username: 'user', password: 'pass');
|
|
22
|
+
//
|
|
23
|
+
// // Assert: token must NOT be in unencrypted SharedPreferences
|
|
24
|
+
// final prefs = await SharedPreferences.getInstance();
|
|
25
|
+
// expect(prefs.getString('token'), isNull,
|
|
26
|
+
// reason: 'Use flutter_secure_storage instead'); // currently stored — RED
|
|
27
|
+
// });
|
|
28
|
+
|
|
29
|
+
// ── Example 2: TLS Bypass ─────────────────────────────────────────────────
|
|
30
|
+
// test('SHOULD enforce TLS certificate verification', () {
|
|
31
|
+
// final client = buildHttpClient(); // your app's HTTP client factory
|
|
32
|
+
// expect(client.badCertificateCallback, isNull,
|
|
33
|
+
// reason: 'badCertificateCallback must not bypass TLS'); // currently bypassed — RED
|
|
34
|
+
// });
|
|
35
|
+
|
|
36
|
+
// ── Example 3: Navigation Param Injection ─────────────────────────────────
|
|
37
|
+
// test('SHOULD NOT use raw route params in API calls', () async {
|
|
38
|
+
// const maliciousId = "1; DROP TABLE users";
|
|
39
|
+
// // Simulate screen loading with a crafted route argument
|
|
40
|
+
// // final result = await ItemService().fetchItem(id: maliciousId);
|
|
41
|
+
// //
|
|
42
|
+
// // Assert the input was validated / rejected
|
|
43
|
+
// // expect(result, isNull); // currently fetches — RED
|
|
44
|
+
// });
|
|
45
|
+
|
|
46
|
+
test('PLACEHOLDER — replace with your exploit assertion', () {
|
|
47
|
+
// Remove this placeholder and uncomment one of the examples above.
|
|
48
|
+
expect(true, isTrue);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD Remediation: Red Phase Sample Test (React / Next.js — Testing Library + Vitest)
|
|
3
|
+
*
|
|
4
|
+
* For UI-layer security tests: XSS via dangerouslySetInnerHTML, sensitive data
|
|
5
|
+
* rendering, unauthenticated route access, client-side auth bypass, etc.
|
|
6
|
+
*
|
|
7
|
+
* Replace the boilerplate below with the specific exploit you are verifying.
|
|
8
|
+
* This test MUST fail initially (Red Phase). Once you apply the fix, it MUST pass (Green Phase).
|
|
9
|
+
*
|
|
10
|
+
* Run with: vitest run __tests__/security/
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
14
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
15
|
+
// import ComponentUnderTest from '../../components/ComponentUnderTest'; // update path
|
|
16
|
+
|
|
17
|
+
describe('Security Vulnerability Remediation - Red Phase (UI)', () => {
|
|
18
|
+
|
|
19
|
+
// ── Example 1: XSS via dangerouslySetInnerHTML ───────────────────────────
|
|
20
|
+
// it('SHOULD sanitize user content before rendering as HTML', () => {
|
|
21
|
+
// const xssPayload = '<script>window.__xss = true</script>';
|
|
22
|
+
// render(<CommentBody content={xssPayload} />);
|
|
23
|
+
//
|
|
24
|
+
// // Script tag must not be injected into the DOM
|
|
25
|
+
// expect(document.querySelector('script')).toBeNull(); // currently rendered — RED
|
|
26
|
+
// expect((window as any).__xss).toBeUndefined();
|
|
27
|
+
// });
|
|
28
|
+
|
|
29
|
+
// ── Example 2: Sensitive data must not appear in rendered output ─────────
|
|
30
|
+
// it('SHOULD NOT expose auth token in the DOM', () => {
|
|
31
|
+
// render(<UserProfile token="super-secret-jwt" />);
|
|
32
|
+
// expect(screen.queryByText('super-secret-jwt')).toBeNull(); // currently visible — RED
|
|
33
|
+
// });
|
|
34
|
+
|
|
35
|
+
// ── Example 3: Protected route must reject unauthenticated users ─────────
|
|
36
|
+
// it('SHOULD NOT render protected content without a valid session', () => {
|
|
37
|
+
// render(<ProtectedPage />, { wrapper: UnauthenticatedProvider });
|
|
38
|
+
// expect(screen.queryByRole('main')).toBeNull(); // currently renders — RED
|
|
39
|
+
// expect(screen.getByText(/sign in/i)).toBeInTheDocument();
|
|
40
|
+
// });
|
|
41
|
+
|
|
42
|
+
// ── Example 4: Form input must be sanitized before submission ────────────
|
|
43
|
+
// it('SHOULD strip script tags from form input before submit', async () => {
|
|
44
|
+
// const user = userEvent.setup();
|
|
45
|
+
// render(<CommentForm onSubmit={mockSubmit} />);
|
|
46
|
+
// await user.type(screen.getByRole('textbox'), '<script>alert(1)</script>');
|
|
47
|
+
// await user.click(screen.getByRole('button', { name: /submit/i }));
|
|
48
|
+
//
|
|
49
|
+
// expect(mockSubmit).toHaveBeenCalledWith(
|
|
50
|
+
// expect.not.objectContaining({ body: expect.stringContaining('<script>') })
|
|
51
|
+
// ); // currently passes raw payload — RED
|
|
52
|
+
// });
|
|
53
|
+
|
|
54
|
+
it('PLACEHOLDER — replace with your exploit assertion', () => {
|
|
55
|
+
// Remove this placeholder and uncomment one of the examples above.
|
|
56
|
+
expect(true).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Security Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
security-tests:
|
|
11
|
+
name: Exploit Test Suite
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: subosito/flutter-action@v2
|
|
18
|
+
with:
|
|
19
|
+
flutter-version: 'stable'
|
|
20
|
+
cache: true
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: flutter pub get
|
|
24
|
+
|
|
25
|
+
- name: Run security exploit tests
|
|
26
|
+
run: flutter test test/security/
|