tlc-claude-code 1.4.7 → 1.4.9
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/docker-compose.dev.yml +6 -3
- package/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Builder Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for safe parameterized query building.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
createQueryBuilder,
|
|
10
|
+
select,
|
|
11
|
+
insert,
|
|
12
|
+
update,
|
|
13
|
+
del,
|
|
14
|
+
SecurityError,
|
|
15
|
+
} from './query-builder.js';
|
|
16
|
+
|
|
17
|
+
describe('query-builder', () => {
|
|
18
|
+
describe('select', () => {
|
|
19
|
+
it('builds SELECT with simple WHERE', () => {
|
|
20
|
+
const result = select('users')
|
|
21
|
+
.columns(['id', 'name', 'email'])
|
|
22
|
+
.where('id', '=', 1)
|
|
23
|
+
.build();
|
|
24
|
+
|
|
25
|
+
expect(result.sql).toBe('SELECT id, name, email FROM users WHERE id = $1');
|
|
26
|
+
expect(result.params).toEqual([1]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('builds SELECT with multiple WHERE conditions', () => {
|
|
30
|
+
const result = select('users')
|
|
31
|
+
.columns(['*'])
|
|
32
|
+
.where('status', '=', 'active')
|
|
33
|
+
.where('age', '>=', 18)
|
|
34
|
+
.build();
|
|
35
|
+
|
|
36
|
+
expect(result.sql).toBe('SELECT * FROM users WHERE status = $1 AND age >= $2');
|
|
37
|
+
expect(result.params).toEqual(['active', 18]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('builds SELECT with OR conditions', () => {
|
|
41
|
+
const result = select('users')
|
|
42
|
+
.columns(['*'])
|
|
43
|
+
.where('role', '=', 'admin')
|
|
44
|
+
.orWhere('role', '=', 'superadmin')
|
|
45
|
+
.build();
|
|
46
|
+
|
|
47
|
+
expect(result.sql).toContain('OR');
|
|
48
|
+
expect(result.params).toEqual(['admin', 'superadmin']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('builds SELECT with IN clause', () => {
|
|
52
|
+
const result = select('users')
|
|
53
|
+
.columns(['*'])
|
|
54
|
+
.whereIn('id', [1, 2, 3])
|
|
55
|
+
.build();
|
|
56
|
+
|
|
57
|
+
expect(result.sql).toBe('SELECT * FROM users WHERE id IN ($1, $2, $3)');
|
|
58
|
+
expect(result.params).toEqual([1, 2, 3]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('builds SELECT with ORDER BY', () => {
|
|
62
|
+
const result = select('users')
|
|
63
|
+
.columns(['*'])
|
|
64
|
+
.orderBy('created_at', 'DESC')
|
|
65
|
+
.build();
|
|
66
|
+
|
|
67
|
+
expect(result.sql).toContain('ORDER BY created_at DESC');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('builds SELECT with LIMIT and OFFSET', () => {
|
|
71
|
+
const result = select('users')
|
|
72
|
+
.columns(['*'])
|
|
73
|
+
.limit(10)
|
|
74
|
+
.offset(20)
|
|
75
|
+
.build();
|
|
76
|
+
|
|
77
|
+
expect(result.sql).toContain('LIMIT $1');
|
|
78
|
+
expect(result.sql).toContain('OFFSET $2');
|
|
79
|
+
expect(result.params).toContain(10);
|
|
80
|
+
expect(result.params).toContain(20);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects string concatenation in WHERE value', () => {
|
|
84
|
+
expect(() => {
|
|
85
|
+
select('users')
|
|
86
|
+
.columns(['*'])
|
|
87
|
+
.whereRaw("name = '" + "'; DROP TABLE users;--")
|
|
88
|
+
.build();
|
|
89
|
+
}).toThrow(SecurityError);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('rejects unwhitelisted table name', () => {
|
|
93
|
+
const builder = createQueryBuilder({
|
|
94
|
+
allowedTables: ['users', 'posts'],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(() => {
|
|
98
|
+
builder.select('admin_secrets').columns(['*']).build();
|
|
99
|
+
}).toThrow(SecurityError);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rejects unwhitelisted column name', () => {
|
|
103
|
+
const builder = createQueryBuilder({
|
|
104
|
+
allowedColumns: { users: ['id', 'name', 'email'] },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(() => {
|
|
108
|
+
builder.select('users').columns(['password_hash']).build();
|
|
109
|
+
}).toThrow(SecurityError);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('insert', () => {
|
|
114
|
+
it('builds INSERT with values', () => {
|
|
115
|
+
const result = insert('users')
|
|
116
|
+
.values({ name: 'John', email: 'john@example.com' })
|
|
117
|
+
.build();
|
|
118
|
+
|
|
119
|
+
expect(result.sql).toBe('INSERT INTO users (name, email) VALUES ($1, $2)');
|
|
120
|
+
expect(result.params).toEqual(['John', 'john@example.com']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('builds INSERT with multiple rows', () => {
|
|
124
|
+
const result = insert('users')
|
|
125
|
+
.values([
|
|
126
|
+
{ name: 'John', email: 'john@example.com' },
|
|
127
|
+
{ name: 'Jane', email: 'jane@example.com' },
|
|
128
|
+
])
|
|
129
|
+
.build();
|
|
130
|
+
|
|
131
|
+
expect(result.sql).toContain('VALUES ($1, $2), ($3, $4)');
|
|
132
|
+
expect(result.params).toHaveLength(4);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('builds INSERT with RETURNING clause', () => {
|
|
136
|
+
const result = insert('users')
|
|
137
|
+
.values({ name: 'John' })
|
|
138
|
+
.returning(['id'])
|
|
139
|
+
.build();
|
|
140
|
+
|
|
141
|
+
expect(result.sql).toContain('RETURNING id');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('escapes column names with special characters', () => {
|
|
145
|
+
const result = insert('users')
|
|
146
|
+
.values({ 'user-name': 'John' })
|
|
147
|
+
.build();
|
|
148
|
+
|
|
149
|
+
expect(result.sql).toContain('"user-name"');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('rejects SQL in column names', () => {
|
|
153
|
+
expect(() => {
|
|
154
|
+
insert('users')
|
|
155
|
+
.values({ 'name; DROP TABLE users;--': 'John' })
|
|
156
|
+
.build();
|
|
157
|
+
}).toThrow(SecurityError);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('update', () => {
|
|
162
|
+
it('builds UPDATE with SET and WHERE', () => {
|
|
163
|
+
const result = update('users')
|
|
164
|
+
.set({ name: 'John', status: 'active' })
|
|
165
|
+
.where('id', '=', 1)
|
|
166
|
+
.build();
|
|
167
|
+
|
|
168
|
+
expect(result.sql).toBe('UPDATE users SET name = $1, status = $2 WHERE id = $3');
|
|
169
|
+
expect(result.params).toEqual(['John', 'active', 1]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('requires WHERE clause by default', () => {
|
|
173
|
+
expect(() => {
|
|
174
|
+
update('users')
|
|
175
|
+
.set({ status: 'deleted' })
|
|
176
|
+
.build();
|
|
177
|
+
}).toThrow(SecurityError);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('allows UPDATE without WHERE when explicitly unsafe', () => {
|
|
181
|
+
const result = update('users')
|
|
182
|
+
.set({ status: 'inactive' })
|
|
183
|
+
.unsafe()
|
|
184
|
+
.build();
|
|
185
|
+
|
|
186
|
+
expect(result.sql).toBe('UPDATE users SET status = $1');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('builds UPDATE with RETURNING', () => {
|
|
190
|
+
const result = update('users')
|
|
191
|
+
.set({ name: 'John' })
|
|
192
|
+
.where('id', '=', 1)
|
|
193
|
+
.returning(['id', 'name'])
|
|
194
|
+
.build();
|
|
195
|
+
|
|
196
|
+
expect(result.sql).toContain('RETURNING id, name');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('delete', () => {
|
|
201
|
+
it('builds DELETE with WHERE', () => {
|
|
202
|
+
const result = del('users')
|
|
203
|
+
.where('id', '=', 1)
|
|
204
|
+
.build();
|
|
205
|
+
|
|
206
|
+
expect(result.sql).toBe('DELETE FROM users WHERE id = $1');
|
|
207
|
+
expect(result.params).toEqual([1]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('requires WHERE clause by default', () => {
|
|
211
|
+
expect(() => {
|
|
212
|
+
del('users').build();
|
|
213
|
+
}).toThrow(SecurityError);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('allows DELETE without WHERE when explicitly unsafe', () => {
|
|
217
|
+
const result = del('users').unsafe().build();
|
|
218
|
+
expect(result.sql).toBe('DELETE FROM users');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('builds DELETE with multiple conditions', () => {
|
|
222
|
+
const result = del('users')
|
|
223
|
+
.where('status', '=', 'deleted')
|
|
224
|
+
.where('deleted_at', '<', '2024-01-01')
|
|
225
|
+
.build();
|
|
226
|
+
|
|
227
|
+
expect(result.sql).toContain('AND');
|
|
228
|
+
expect(result.params).toHaveLength(2);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('SQL keyword safety', () => {
|
|
233
|
+
it('treats SQL keywords in values as data', () => {
|
|
234
|
+
const result = insert('users')
|
|
235
|
+
.values({ name: 'SELECT * FROM users' })
|
|
236
|
+
.build();
|
|
237
|
+
|
|
238
|
+
expect(result.params).toContain('SELECT * FROM users');
|
|
239
|
+
expect(result.sql).not.toContain('SELECT * FROM users');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('treats DROP TABLE in values as data', () => {
|
|
243
|
+
const result = update('users')
|
|
244
|
+
.set({ bio: 'DROP TABLE users;' })
|
|
245
|
+
.where('id', '=', 1)
|
|
246
|
+
.build();
|
|
247
|
+
|
|
248
|
+
expect(result.params).toContain('DROP TABLE users;');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('treats UNION in values as data', () => {
|
|
252
|
+
const result = select('users')
|
|
253
|
+
.columns(['*'])
|
|
254
|
+
.where('name', '=', "' UNION SELECT * FROM passwords--")
|
|
255
|
+
.build();
|
|
256
|
+
|
|
257
|
+
expect(result.params).toContain("' UNION SELECT * FROM passwords--");
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('dialect support', () => {
|
|
262
|
+
it('uses PostgreSQL parameter style ($1)', () => {
|
|
263
|
+
const builder = createQueryBuilder({ dialect: 'postgresql' });
|
|
264
|
+
const result = builder.select('users')
|
|
265
|
+
.columns(['*'])
|
|
266
|
+
.where('id', '=', 1)
|
|
267
|
+
.build();
|
|
268
|
+
|
|
269
|
+
expect(result.sql).toContain('$1');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('uses MySQL parameter style (?)', () => {
|
|
273
|
+
const builder = createQueryBuilder({ dialect: 'mysql' });
|
|
274
|
+
const result = builder.select('users')
|
|
275
|
+
.columns(['*'])
|
|
276
|
+
.where('id', '=', 1)
|
|
277
|
+
.build();
|
|
278
|
+
|
|
279
|
+
expect(result.sql).toContain('?');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('uses SQLite parameter style (?)', () => {
|
|
283
|
+
const builder = createQueryBuilder({ dialect: 'sqlite' });
|
|
284
|
+
const result = builder.select('users')
|
|
285
|
+
.columns(['*'])
|
|
286
|
+
.where('id', '=', 1)
|
|
287
|
+
.build();
|
|
288
|
+
|
|
289
|
+
expect(result.sql).toContain('?');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('identifier escaping', () => {
|
|
294
|
+
it('escapes table names with special characters', () => {
|
|
295
|
+
const result = select('user-data')
|
|
296
|
+
.columns(['*'])
|
|
297
|
+
.build();
|
|
298
|
+
|
|
299
|
+
expect(result.sql).toContain('"user-data"');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('escapes reserved word table names', () => {
|
|
303
|
+
const result = select('order')
|
|
304
|
+
.columns(['*'])
|
|
305
|
+
.build();
|
|
306
|
+
|
|
307
|
+
expect(result.sql).toContain('"order"');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('handles schema-qualified table names', () => {
|
|
311
|
+
const result = select('public.users')
|
|
312
|
+
.columns(['*'])
|
|
313
|
+
.build();
|
|
314
|
+
|
|
315
|
+
expect(result.sql).toContain('public.users');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret Detector Module
|
|
3
|
+
*
|
|
4
|
+
* Detects hardcoded secrets, API keys, and credentials in code.
|
|
5
|
+
* Helps prevent OWASP A02: Cryptographic Failures and A07: Auth Failures
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Built-in secret detection patterns
|
|
10
|
+
* All patterns use simple, non-backtracking regex
|
|
11
|
+
*/
|
|
12
|
+
const BUILT_IN_PATTERNS = [
|
|
13
|
+
// AWS Access Key (always starts with AKIA, exactly 20 chars)
|
|
14
|
+
{
|
|
15
|
+
name: 'aws_access_key',
|
|
16
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
17
|
+
severity: 'critical',
|
|
18
|
+
},
|
|
19
|
+
// AWS Secret Key (40 char base64-like, starts with wJalrX typically)
|
|
20
|
+
{
|
|
21
|
+
name: 'aws_secret_key',
|
|
22
|
+
pattern: /(?:secret(?:Access)?Key|aws_secret)\s*[=:]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
23
|
+
severity: 'critical',
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// GitHub tokens (prefixed with ghp_, gho_, ghu_, ghr_, ghs_)
|
|
27
|
+
{
|
|
28
|
+
name: 'github_token',
|
|
29
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{36}/g,
|
|
30
|
+
severity: 'critical',
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Stripe secret key (sk_live_ or sk_test_, 4-32 chars after prefix for tests)
|
|
34
|
+
{
|
|
35
|
+
name: 'stripe_secret_key',
|
|
36
|
+
pattern: /sk_live_[A-Za-z0-9]{4,32}/g,
|
|
37
|
+
severity: 'critical',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'stripe_test_key',
|
|
41
|
+
pattern: /sk_test_[A-Za-z0-9]{4,32}/g,
|
|
42
|
+
severity: 'high',
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Private keys (PEM format headers)
|
|
46
|
+
{
|
|
47
|
+
name: 'private_key',
|
|
48
|
+
pattern: /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g,
|
|
49
|
+
severity: 'critical',
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// JWT tokens - detect by eyJ prefix (Base64 for {"alg" or {"typ")
|
|
53
|
+
{
|
|
54
|
+
name: 'jwt_token',
|
|
55
|
+
pattern: /eyJ[A-Za-z0-9_-]{20,}\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g,
|
|
56
|
+
severity: 'high',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Database connection strings with embedded credentials
|
|
60
|
+
{
|
|
61
|
+
name: 'connection_string',
|
|
62
|
+
pattern: /(?:postgresql|mysql|mongodb(?:\+srv)?|redis):\/\/\w+:\w+@[^\s"']+/gi,
|
|
63
|
+
severity: 'critical',
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Password assignments (simple detection)
|
|
67
|
+
{
|
|
68
|
+
name: 'password',
|
|
69
|
+
pattern: /(?:password|passwd|pass|db_password|secret)\s*[:=]\s*["'][^"']{4,30}["']/gi,
|
|
70
|
+
severity: 'high',
|
|
71
|
+
excludePatterns: [/process\.env/i, /\$\{/],
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Patterns that indicate false positives
|
|
77
|
+
* These are checked against the LINE, not just the matched value
|
|
78
|
+
*/
|
|
79
|
+
const FALSE_POSITIVE_PATTERNS = [
|
|
80
|
+
/process\.env/i,
|
|
81
|
+
/\$\{[^}]+\}/,
|
|
82
|
+
/YOUR_[A-Z_]+_HERE/i,
|
|
83
|
+
/PLACEHOLDER/i,
|
|
84
|
+
/-----BEGIN\s+PUBLIC\s+KEY-----/,
|
|
85
|
+
/-----BEGIN\s+CERTIFICATE-----/,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Patterns checked only against the matched value (not the full line)
|
|
90
|
+
*/
|
|
91
|
+
const VALUE_FALSE_POSITIVE_PATTERNS = [
|
|
92
|
+
/^x{4,}$/i, // Only if the entire value is just x's
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Detect secrets in code content
|
|
97
|
+
* @param {string} content - Code content to scan
|
|
98
|
+
* @param {Object} options - Detection options
|
|
99
|
+
* @returns {Object} Detection results
|
|
100
|
+
*/
|
|
101
|
+
export function detectSecrets(content, options = {}) {
|
|
102
|
+
const {
|
|
103
|
+
patterns = BUILT_IN_PATTERNS,
|
|
104
|
+
ignoreTestValues = false,
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
const findings = [];
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
|
|
110
|
+
for (const patternDef of patterns) {
|
|
111
|
+
const { name, pattern, severity, excludePatterns } = patternDef;
|
|
112
|
+
|
|
113
|
+
// Create a fresh regex instance with global flag
|
|
114
|
+
const flags = pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g';
|
|
115
|
+
const regex = new RegExp(pattern.source, flags);
|
|
116
|
+
|
|
117
|
+
let match;
|
|
118
|
+
while ((match = regex.exec(content)) !== null) {
|
|
119
|
+
const matchStart = match.index;
|
|
120
|
+
const matchValue = match[0];
|
|
121
|
+
|
|
122
|
+
// Find line number
|
|
123
|
+
let lineNumber = 1;
|
|
124
|
+
let charCount = 0;
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
if (charCount + lines[i].length >= matchStart) {
|
|
127
|
+
lineNumber = i + 1;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
charCount += lines[i].length + 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const line = lines[lineNumber - 1] || '';
|
|
134
|
+
|
|
135
|
+
// Check for false positives
|
|
136
|
+
let isFalsePositive = false;
|
|
137
|
+
|
|
138
|
+
// Check pattern-specific exclusions
|
|
139
|
+
if (excludePatterns) {
|
|
140
|
+
for (const excl of excludePatterns) {
|
|
141
|
+
if (excl.test(line)) {
|
|
142
|
+
isFalsePositive = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check global false positive patterns (against line)
|
|
149
|
+
if (!isFalsePositive) {
|
|
150
|
+
for (const fp of FALSE_POSITIVE_PATTERNS) {
|
|
151
|
+
if (fp.test(line)) {
|
|
152
|
+
isFalsePositive = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check value-specific false positive patterns
|
|
159
|
+
if (!isFalsePositive) {
|
|
160
|
+
for (const fp of VALUE_FALSE_POSITIVE_PATTERNS) {
|
|
161
|
+
if (fp.test(matchValue)) {
|
|
162
|
+
isFalsePositive = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Skip test values if configured
|
|
169
|
+
if (!isFalsePositive && ignoreTestValues) {
|
|
170
|
+
if (/test|example|sample|demo/i.test(matchValue)) {
|
|
171
|
+
isFalsePositive = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!isFalsePositive) {
|
|
176
|
+
findings.push({
|
|
177
|
+
type: name,
|
|
178
|
+
line: lineNumber,
|
|
179
|
+
column: matchStart - content.lastIndexOf('\n', matchStart - 1),
|
|
180
|
+
severity: severity || 'medium',
|
|
181
|
+
snippet: line.trim().substring(0, 100),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
findings,
|
|
189
|
+
hasSecrets: findings.length > 0,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Scan a file for secrets
|
|
195
|
+
*/
|
|
196
|
+
export async function scanFile(filePath, options = {}) {
|
|
197
|
+
const { content } = options;
|
|
198
|
+
|
|
199
|
+
if (!content) {
|
|
200
|
+
throw new Error('Content must be provided');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = detectSecrets(content, options);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
file: filePath,
|
|
207
|
+
findings: result.findings.map((f) => ({ ...f, file: filePath })),
|
|
208
|
+
hasSecrets: result.hasSecrets,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Scan a directory for secrets
|
|
214
|
+
*/
|
|
215
|
+
export async function scanDirectory(dirPath, options = {}) {
|
|
216
|
+
const { files = {}, ignore = [] } = options;
|
|
217
|
+
|
|
218
|
+
const allFindings = [];
|
|
219
|
+
let filesWithSecrets = 0;
|
|
220
|
+
let totalFiles = 0;
|
|
221
|
+
|
|
222
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
223
|
+
const relativePath = filePath.replace(dirPath, '').replace(/^\//, '');
|
|
224
|
+
|
|
225
|
+
// Check ignore patterns
|
|
226
|
+
let shouldIgnore = false;
|
|
227
|
+
for (const pattern of ignore) {
|
|
228
|
+
if (relativePath.includes(pattern.replace('/**', '').replace('/*', ''))) {
|
|
229
|
+
shouldIgnore = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (shouldIgnore) continue;
|
|
235
|
+
|
|
236
|
+
totalFiles++;
|
|
237
|
+
const result = await scanFile(filePath, { content, ...options });
|
|
238
|
+
|
|
239
|
+
if (result.hasSecrets) {
|
|
240
|
+
filesWithSecrets++;
|
|
241
|
+
allFindings.push(...result.findings);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
directory: dirPath,
|
|
247
|
+
totalFiles,
|
|
248
|
+
filesWithSecrets,
|
|
249
|
+
findings: allFindings,
|
|
250
|
+
hasSecrets: allFindings.length > 0,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Create a custom secret detector
|
|
256
|
+
*/
|
|
257
|
+
export function createSecretDetector(options = {}) {
|
|
258
|
+
const { patterns = [], builtInPatterns = true } = options;
|
|
259
|
+
|
|
260
|
+
const activePatterns = builtInPatterns
|
|
261
|
+
? [...BUILT_IN_PATTERNS, ...patterns]
|
|
262
|
+
: patterns;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
patterns: activePatterns,
|
|
266
|
+
|
|
267
|
+
detect(content, detectOptions = {}) {
|
|
268
|
+
return detectSecrets(content, { ...detectOptions, patterns: this.patterns });
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
addPattern(pattern) {
|
|
272
|
+
this.patterns.push(pattern);
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Add a custom pattern to an existing detector
|
|
279
|
+
*/
|
|
280
|
+
export function addCustomPattern(detector, pattern) {
|
|
281
|
+
if (!detector || !detector.patterns) {
|
|
282
|
+
throw new Error('Invalid detector');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
detector.patterns.push({
|
|
286
|
+
name: pattern.name,
|
|
287
|
+
pattern: pattern.pattern,
|
|
288
|
+
severity: pattern.severity || 'medium',
|
|
289
|
+
});
|
|
290
|
+
}
|