clckernel 1.2.6
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/AGENTS.md +124 -0
- package/README.es.md +166 -0
- package/README.md +167 -0
- package/bin/cli.js +12 -0
- package/docs/Journal/001-adr-clckernel-governance.md +18 -0
- package/package.json +44 -0
- package/src/adapters/astro.js +67 -0
- package/src/adapters/base.js +109 -0
- package/src/adapters/django.js +70 -0
- package/src/adapters/fastapi.js +69 -0
- package/src/adapters/go.js +50 -0
- package/src/adapters/laravel.js +52 -0
- package/src/adapters/nextjs.js +72 -0
- package/src/adapters/rails.js +54 -0
- package/src/adapters/rust.js +51 -0
- package/src/catalog.js +217 -0
- package/src/config.js +250 -0
- package/src/detector.js +85 -0
- package/src/doctor.js +125 -0
- package/src/generator.js +473 -0
- package/src/index.js +52 -0
- package/src/technologies/detector.js +243 -0
- package/src/technologies/guards/_shared.js +66 -0
- package/src/ui.js +101 -0
- package/src/yaml.js +173 -0
- package/test/adapters/astro.test.js +102 -0
- package/test/adapters/base.test.js +86 -0
- package/test/adapters/django.test.js +95 -0
- package/test/adapters/fastapi.test.js +88 -0
- package/test/adapters/go.test.js +81 -0
- package/test/adapters/laravel.test.js +106 -0
- package/test/adapters/nextjs.test.js +113 -0
- package/test/adapters/rails.test.js +108 -0
- package/test/adapters/rust.test.js +83 -0
- package/test/catalog.test.js +170 -0
- package/test/config.test.js +512 -0
- package/test/detector.test.js +345 -0
- package/test/doctor.test.js +302 -0
- package/test/generator.test.js +419 -0
- package/test/guards_new.test.js +95 -0
- package/test/helpers.js +36 -0
- package/test/integration/cli-flow.test.js +270 -0
- package/test/technologies/celery_guard.test.js +185 -0
- package/test/technologies/detector.test.js +342 -0
- package/test/technologies/docker_guard.test.js +187 -0
- package/test/technologies/integration.test.js +123 -0
- package/test/technologies/postgres_guard.test.js +87 -0
- package/test/technologies/redis_guard.test.js +128 -0
- package/tools/audit.js +219 -0
- package/tools/audit.py +273 -0
- package/tools/celery_guard.py +209 -0
- package/tools/check_a11y.js +109 -0
- package/tools/check_api_contracts.js +139 -0
- package/tools/check_architecture.js +139 -0
- package/tools/check_architecture.py +264 -0
- package/tools/check_custom.js +163 -0
- package/tools/check_custom.py +395 -0
- package/tools/check_db_efficiency.py +190 -0
- package/tools/check_migrations.py +223 -0
- package/tools/check_performance.js +142 -0
- package/tools/check_responsive.js +131 -0
- package/tools/check_scope.py +180 -0
- package/tools/check_seo.js +139 -0
- package/tools/check_storybook.js +108 -0
- package/tools/check_ui_reuse.js +135 -0
- package/tools/docker_guard.py +210 -0
- package/tools/postgres_guard.py +192 -0
- package/tools/redis_guard.py +197 -0
- package/tools/scan_secrets.js +109 -0
- package/tools/scan_secrets.py +153 -0
- package/tools/verify_tdd.py +212 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — TechnologyDetector unit tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { describe, it } = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { TechnologyDetector } = require('../../src/technologies/detector');
|
|
9
|
+
const { createTmpProject, teardown } = require('../helpers');
|
|
10
|
+
|
|
11
|
+
describe('TechnologyDetector', () => {
|
|
12
|
+
describe('detect() — Celery', () => {
|
|
13
|
+
it('detects Celery via requirements.txt', () => {
|
|
14
|
+
const dir = createTmpProject({
|
|
15
|
+
'requirements.txt': 'celery>=5.3.0\nredis>=5.0.0',
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
const detector = new TechnologyDetector();
|
|
19
|
+
const result = detector.detect(dir);
|
|
20
|
+
assert.ok(result.includes('celery'), 'should detect celery from requirements.txt');
|
|
21
|
+
} finally {
|
|
22
|
+
teardown(dir);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('detects Celery via pyproject.toml', () => {
|
|
27
|
+
const dir = createTmpProject({
|
|
28
|
+
'pyproject.toml': '[project]\nname = "myapp"\ndependencies = ["celery>=5.3"]',
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
const detector = new TechnologyDetector();
|
|
32
|
+
const result = detector.detect(dir);
|
|
33
|
+
assert.ok(result.includes('celery'), 'should detect celery from pyproject.toml');
|
|
34
|
+
} finally {
|
|
35
|
+
teardown(dir);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('detects Celery via celery.py file', () => {
|
|
40
|
+
const dir = createTmpProject({
|
|
41
|
+
'celery.py': 'from celery import Celery\napp = Celery()',
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const detector = new TechnologyDetector();
|
|
45
|
+
const result = detector.detect(dir);
|
|
46
|
+
assert.ok(result.includes('celery'), 'should detect celery from celery.py');
|
|
47
|
+
} finally {
|
|
48
|
+
teardown(dir);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('detects Celery via celeryconfig.py', () => {
|
|
53
|
+
const dir = createTmpProject({
|
|
54
|
+
'celeryconfig.py': 'broker_url = "redis://localhost"',
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const detector = new TechnologyDetector();
|
|
58
|
+
const result = detector.detect(dir);
|
|
59
|
+
assert.ok(result.includes('celery'), 'should detect celery from celeryconfig.py');
|
|
60
|
+
} finally {
|
|
61
|
+
teardown(dir);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('detects Celery via celery_app.py', () => {
|
|
66
|
+
const dir = createTmpProject({
|
|
67
|
+
'celery_app.py': 'from celery import Celery\napp = Celery()',
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
const detector = new TechnologyDetector();
|
|
71
|
+
const result = detector.detect(dir);
|
|
72
|
+
assert.ok(result.includes('celery'), 'should detect celery from celery_app.py');
|
|
73
|
+
} finally {
|
|
74
|
+
teardown(dir);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('detect() — Redis', () => {
|
|
80
|
+
it('detects Redis via requirements.txt', () => {
|
|
81
|
+
const dir = createTmpProject({
|
|
82
|
+
'requirements.txt': 'redis>=5.0.0\ncelery>=5.3',
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
const detector = new TechnologyDetector();
|
|
86
|
+
const result = detector.detect(dir);
|
|
87
|
+
assert.ok(result.includes('redis'), 'should detect redis from requirements.txt');
|
|
88
|
+
} finally {
|
|
89
|
+
teardown(dir);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('detects Redis via pyproject.toml', () => {
|
|
94
|
+
const dir = createTmpProject({
|
|
95
|
+
'pyproject.toml': '[project]\ndependencies = ["redis>=5.0"]',
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
const detector = new TechnologyDetector();
|
|
99
|
+
const result = detector.detect(dir);
|
|
100
|
+
assert.ok(result.includes('redis'), 'should detect redis from pyproject.toml');
|
|
101
|
+
} finally {
|
|
102
|
+
teardown(dir);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('detects Redis via redis.conf', () => {
|
|
107
|
+
const dir = createTmpProject({
|
|
108
|
+
'redis.conf': 'port 6379\nbind 127.0.0.1',
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
const detector = new TechnologyDetector();
|
|
112
|
+
const result = detector.detect(dir);
|
|
113
|
+
assert.ok(result.includes('redis'), 'should detect redis from redis.conf');
|
|
114
|
+
} finally {
|
|
115
|
+
teardown(dir);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('detects Redis via redis:// in .env', () => {
|
|
120
|
+
const dir = createTmpProject({
|
|
121
|
+
'.env': 'REDIS_URL=redis://localhost:6379/0\nSECRET_KEY=xyz',
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
const detector = new TechnologyDetector();
|
|
125
|
+
const result = detector.detect(dir);
|
|
126
|
+
assert.ok(result.includes('redis'), 'should detect redis from .env');
|
|
127
|
+
} finally {
|
|
128
|
+
teardown(dir);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('detects Redis via .env.development', () => {
|
|
133
|
+
const dir = createTmpProject({
|
|
134
|
+
'.env.development': 'REDIS_URL=redis://localhost:6379/0',
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
const detector = new TechnologyDetector();
|
|
138
|
+
const result = detector.detect(dir);
|
|
139
|
+
assert.ok(result.includes('redis'), 'should detect redis from .env.development');
|
|
140
|
+
} finally {
|
|
141
|
+
teardown(dir);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('detect() — PostgreSQL', () => {
|
|
147
|
+
it('detects PostgreSQL via psycopg2 in requirements.txt', () => {
|
|
148
|
+
const dir = createTmpProject({
|
|
149
|
+
'requirements.txt': 'psycopg2>=2.9.0\ncelery>=5.3',
|
|
150
|
+
});
|
|
151
|
+
try {
|
|
152
|
+
const detector = new TechnologyDetector();
|
|
153
|
+
const result = detector.detect(dir);
|
|
154
|
+
assert.ok(result.includes('postgresql'), 'should detect postgresql from psycopg2');
|
|
155
|
+
} finally {
|
|
156
|
+
teardown(dir);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('detects PostgreSQL via asyncpg in requirements.txt', () => {
|
|
161
|
+
const dir = createTmpProject({
|
|
162
|
+
'requirements.txt': 'asyncpg>=0.29.0',
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
const detector = new TechnologyDetector();
|
|
166
|
+
const result = detector.detect(dir);
|
|
167
|
+
assert.ok(result.includes('postgresql'), 'should detect postgresql from asyncpg');
|
|
168
|
+
} finally {
|
|
169
|
+
teardown(dir);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('detects PostgreSQL via psycopg2-binary in requirements.txt', () => {
|
|
174
|
+
const dir = createTmpProject({
|
|
175
|
+
'requirements.txt': 'psycopg2-binary>=2.9.0',
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
const detector = new TechnologyDetector();
|
|
179
|
+
const result = detector.detect(dir);
|
|
180
|
+
assert.ok(result.includes('postgresql'), 'should detect postgresql from psycopg2-binary');
|
|
181
|
+
} finally {
|
|
182
|
+
teardown(dir);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('detects PostgreSQL via pyproject.toml', () => {
|
|
187
|
+
const dir = createTmpProject({
|
|
188
|
+
'pyproject.toml': '[project]\ndependencies = ["asyncpg>=0.29"]',
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
const detector = new TechnologyDetector();
|
|
192
|
+
const result = detector.detect(dir);
|
|
193
|
+
assert.ok(result.includes('postgresql'), 'should detect postgresql from pyproject.toml');
|
|
194
|
+
} finally {
|
|
195
|
+
teardown(dir);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('detect() — Docker', () => {
|
|
201
|
+
it('detects Docker via Dockerfile', () => {
|
|
202
|
+
const dir = createTmpProject({
|
|
203
|
+
'Dockerfile': 'FROM node:18-alpine\nRUN echo "hello"',
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
const detector = new TechnologyDetector();
|
|
207
|
+
const result = detector.detect(dir);
|
|
208
|
+
assert.ok(result.includes('docker'), 'should detect docker from Dockerfile');
|
|
209
|
+
} finally {
|
|
210
|
+
teardown(dir);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('detects Docker via docker-compose.yml', () => {
|
|
215
|
+
const dir = createTmpProject({
|
|
216
|
+
'docker-compose.yml': 'version: "3.8"\nservices:\n web:\n image: node:18',
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
const detector = new TechnologyDetector();
|
|
220
|
+
const result = detector.detect(dir);
|
|
221
|
+
assert.ok(result.includes('docker'), 'should detect docker from docker-compose.yml');
|
|
222
|
+
} finally {
|
|
223
|
+
teardown(dir);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('detect() — case insensitivity', () => {
|
|
229
|
+
it('detects celery in mixed-case requirements.txt', () => {
|
|
230
|
+
const dir = createTmpProject({
|
|
231
|
+
'requirements.txt': 'CELERY>=5.3.0\nREDIS>=5.0.0',
|
|
232
|
+
});
|
|
233
|
+
try {
|
|
234
|
+
const detector = new TechnologyDetector();
|
|
235
|
+
const result = detector.detect(dir);
|
|
236
|
+
assert.ok(result.includes('celery'), 'should detect celery case-insensitively');
|
|
237
|
+
assert.ok(result.includes('redis'), 'should detect redis case-insensitively');
|
|
238
|
+
} finally {
|
|
239
|
+
teardown(dir);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('detects CELERY in pyproject.toml', () => {
|
|
244
|
+
const dir = createTmpProject({
|
|
245
|
+
'pyproject.toml': '[project]\ndependencies = ["CELERY>=5.3"]',
|
|
246
|
+
});
|
|
247
|
+
try {
|
|
248
|
+
const detector = new TechnologyDetector();
|
|
249
|
+
const result = detector.detect(dir);
|
|
250
|
+
assert.ok(result.includes('celery'), 'should detect CELERY case-insensitively');
|
|
251
|
+
} finally {
|
|
252
|
+
teardown(dir);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('detect() — multiple technologies', () => {
|
|
258
|
+
it('detects all four technologies at once', () => {
|
|
259
|
+
const dir = createTmpProject({
|
|
260
|
+
'requirements.txt': 'celery>=5.3\nredis>=5.0\npsycopg2-binary>=2.9',
|
|
261
|
+
'Dockerfile': 'FROM python:3.11-alpine',
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
const detector = new TechnologyDetector();
|
|
265
|
+
const result = detector.detect(dir);
|
|
266
|
+
assert.ok(result.includes('celery'), 'should detect celery');
|
|
267
|
+
assert.ok(result.includes('redis'), 'should detect redis');
|
|
268
|
+
assert.ok(result.includes('postgresql'), 'should detect postgresql');
|
|
269
|
+
assert.ok(result.includes('docker'), 'should detect docker');
|
|
270
|
+
assert.strictEqual(result.length, 4, 'should detect exactly 4 technologies');
|
|
271
|
+
} finally {
|
|
272
|
+
teardown(dir);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('detects two technologies (celery + redis)', () => {
|
|
277
|
+
const dir = createTmpProject({
|
|
278
|
+
'requirements.txt': 'celery>=5.3\nredis>=5.0',
|
|
279
|
+
});
|
|
280
|
+
try {
|
|
281
|
+
const detector = new TechnologyDetector();
|
|
282
|
+
const result = detector.detect(dir);
|
|
283
|
+
assert.ok(result.includes('celery'), 'should detect celery');
|
|
284
|
+
assert.ok(result.includes('redis'), 'should detect redis');
|
|
285
|
+
assert.strictEqual(result.length, 2, 'should detect exactly 2 technologies');
|
|
286
|
+
} finally {
|
|
287
|
+
teardown(dir);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('detect() — no technology found', () => {
|
|
293
|
+
it('returns empty array when no tech signatures are present', () => {
|
|
294
|
+
const dir = createTmpProject({
|
|
295
|
+
'README.md': '# My Project\nJust a simple project.',
|
|
296
|
+
'package.json': JSON.stringify({ name: 'myapp', version: '1.0.0' }),
|
|
297
|
+
});
|
|
298
|
+
try {
|
|
299
|
+
const detector = new TechnologyDetector();
|
|
300
|
+
const result = detector.detect(dir);
|
|
301
|
+
assert.ok(Array.isArray(result), 'should return an array');
|
|
302
|
+
assert.strictEqual(result.length, 0, 'should return empty array');
|
|
303
|
+
} finally {
|
|
304
|
+
teardown(dir);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('returns empty array for non-existent directory', () => {
|
|
309
|
+
const detector = new TechnologyDetector();
|
|
310
|
+
const result = detector.detect('/non/existent/path');
|
|
311
|
+
assert.ok(Array.isArray(result), 'should return an array');
|
|
312
|
+
assert.strictEqual(result.length, 0, 'should return empty array');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('getToolsForTech()', () => {
|
|
317
|
+
it('returns tool manifest for all four technologies', () => {
|
|
318
|
+
const detector = new TechnologyDetector();
|
|
319
|
+
const tools = detector.getToolsForTech(['celery', 'redis', 'postgresql', 'docker']);
|
|
320
|
+
assert.strictEqual(tools.length, 4, 'should return 4 tools');
|
|
321
|
+
const names = tools.map((t) => t.name);
|
|
322
|
+
assert.ok(names.includes('celery_guard'));
|
|
323
|
+
assert.ok(names.includes('redis_guard'));
|
|
324
|
+
assert.ok(names.includes('postgres_guard'));
|
|
325
|
+
assert.ok(names.includes('docker_guard'));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('returns empty array for empty input', () => {
|
|
329
|
+
const detector = new TechnologyDetector();
|
|
330
|
+
const tools = detector.getToolsForTech([]);
|
|
331
|
+
assert.strictEqual(tools.length, 0, 'should return empty array');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('celery_guard has correct structure', () => {
|
|
335
|
+
const detector = new TechnologyDetector();
|
|
336
|
+
const tools = detector.getToolsForTech(['celery']);
|
|
337
|
+
assert.strictEqual(tools[0].name, 'celery_guard');
|
|
338
|
+
assert.strictEqual(tools[0].language, 'py');
|
|
339
|
+
assert.strictEqual(tools[0].path, 'celery_guard.py');
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — Docker guard integration tests.
|
|
3
|
+
* Tests docker_guard.py via subprocess against fixture projects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it } = require('node:test');
|
|
7
|
+
const assert = require('node:assert');
|
|
8
|
+
const { spawn, spawnSync } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { createTmpProject, teardown } = require('../helpers');
|
|
12
|
+
|
|
13
|
+
function runDockerGuard(targetDir) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const script = path.join(__dirname, '../../tools/docker_guard.py');
|
|
16
|
+
const proc = spawn('python3', [script, targetDir], { cwd: targetDir });
|
|
17
|
+
let stdout = '';
|
|
18
|
+
proc.stdout.on('data', (d) => (stdout += d));
|
|
19
|
+
proc.on('close', () => {
|
|
20
|
+
try {
|
|
21
|
+
resolve(JSON.parse(stdout));
|
|
22
|
+
} catch {
|
|
23
|
+
reject(new Error(`Failed to parse: ${stdout}`));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
proc.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function initGitRepo(dir) {
|
|
31
|
+
fs.writeFileSync(path.join(dir, '.gitkeep'), '');
|
|
32
|
+
spawnSync('git', ['init'], { cwd: dir });
|
|
33
|
+
spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dir });
|
|
34
|
+
spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir });
|
|
35
|
+
spawnSync('git', ['add', '.'], { cwd: dir });
|
|
36
|
+
spawnSync('git', ['commit', '-m', 'initial'], { cwd: dir });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('docker_guard', () => {
|
|
40
|
+
it('skips when no git repo', async () => {
|
|
41
|
+
const dir = createTmpProject({});
|
|
42
|
+
try {
|
|
43
|
+
const result = await runDockerGuard(dir);
|
|
44
|
+
assert.strictEqual(result.skipped, true);
|
|
45
|
+
} finally {
|
|
46
|
+
teardown(dir);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('finds violations: missing USER directive', async () => {
|
|
51
|
+
const dir = createTmpProject({});
|
|
52
|
+
initGitRepo(dir);
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(dir, 'Dockerfile'),
|
|
56
|
+
`FROM node:18-alpine\nRUN apk add --no-cache python3\nCMD ["node", "server.js"]\n`
|
|
57
|
+
);
|
|
58
|
+
spawnSync('git', ['add', 'Dockerfile'], { cwd: dir });
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await runDockerGuard(dir);
|
|
62
|
+
assert.strictEqual(result.exit_code, 1);
|
|
63
|
+
const issues = result.data.findings.map((f) => f.issue);
|
|
64
|
+
assert.ok(issues.includes('missing_user'), 'should detect missing USER directive');
|
|
65
|
+
} finally {
|
|
66
|
+
teardown(dir);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('finds violations: USER runs as root', async () => {
|
|
71
|
+
const dir = createTmpProject({});
|
|
72
|
+
initGitRepo(dir);
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
path.join(dir, 'Dockerfile'),
|
|
76
|
+
`FROM node:18-alpine\nUSER root\nCMD ["node", "server.js"]\n`
|
|
77
|
+
);
|
|
78
|
+
spawnSync('git', ['add', 'Dockerfile'], { cwd: dir });
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await runDockerGuard(dir);
|
|
82
|
+
assert.strictEqual(result.exit_code, 1);
|
|
83
|
+
const issues = result.data.findings.map((f) => f.issue);
|
|
84
|
+
assert.ok(issues.includes('user_is_root'), 'should detect root USER');
|
|
85
|
+
} finally {
|
|
86
|
+
teardown(dir);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('finds violations: missing HEALTHCHECK', async () => {
|
|
91
|
+
const dir = createTmpProject({});
|
|
92
|
+
initGitRepo(dir);
|
|
93
|
+
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
path.join(dir, 'Dockerfile'),
|
|
96
|
+
`FROM node:18-alpine\nUSER node\nCMD ["node", "server.js"]\n`
|
|
97
|
+
);
|
|
98
|
+
spawnSync('git', ['add', 'Dockerfile'], { cwd: dir });
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await runDockerGuard(dir);
|
|
102
|
+
assert.strictEqual(result.exit_code, 1);
|
|
103
|
+
const issues = result.data.findings.map((f) => f.issue);
|
|
104
|
+
assert.ok(issues.includes('missing_healthcheck'), 'should detect missing HEALTHCHECK');
|
|
105
|
+
} finally {
|
|
106
|
+
teardown(dir);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('finds violations: ENV secret exposure', async () => {
|
|
111
|
+
const dir = createTmpProject({});
|
|
112
|
+
initGitRepo(dir);
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
path.join(dir, 'Dockerfile'),
|
|
116
|
+
`FROM node:18-alpine\nENV AWS_SECRET_ACCESS_KEY=abcd1234\nENV DB_PASSWORD=secret\nUSER node\nHEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1\nCMD ["node", "server.js"]\n`
|
|
117
|
+
);
|
|
118
|
+
spawnSync('git', ['add', 'Dockerfile'], { cwd: dir });
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await runDockerGuard(dir);
|
|
122
|
+
assert.strictEqual(result.exit_code, 1);
|
|
123
|
+
const issues = result.data.findings.map((f) => f.issue);
|
|
124
|
+
assert.ok(issues.includes('env_secret'), 'should detect ENV secrets');
|
|
125
|
+
} finally {
|
|
126
|
+
teardown(dir);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('finds violations: docker-compose env secret', async () => {
|
|
131
|
+
const dir = createTmpProject({});
|
|
132
|
+
initGitRepo(dir);
|
|
133
|
+
|
|
134
|
+
fs.writeFileSync(
|
|
135
|
+
path.join(dir, 'docker-compose.yml'),
|
|
136
|
+
`version: "3.8"\nservices:\n web:\n image: node:18\n environment:\n - DB_PASSWORD="secret123"\n - API_KEY="key456"\n`
|
|
137
|
+
);
|
|
138
|
+
spawnSync('git', ['add', 'docker-compose.yml'], { cwd: dir });
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const result = await runDockerGuard(dir);
|
|
142
|
+
assert.strictEqual(result.exit_code, 1);
|
|
143
|
+
const issues = result.data.findings.map((f) => f.issue);
|
|
144
|
+
assert.ok(issues.includes('compose_env_secret'), 'should detect compose env secrets');
|
|
145
|
+
} finally {
|
|
146
|
+
teardown(dir);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('no violations: properly configured Dockerfile', async () => {
|
|
151
|
+
const dir = createTmpProject({});
|
|
152
|
+
initGitRepo(dir);
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
path.join(dir, 'Dockerfile'),
|
|
156
|
+
`FROM node:18-alpine\nUSER node\nHEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1\nCMD ["node", "server.js"]\n`
|
|
157
|
+
);
|
|
158
|
+
spawnSync('git', ['add', 'Dockerfile'], { cwd: dir });
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await runDockerGuard(dir);
|
|
162
|
+
assert.strictEqual(result.exit_code, 0);
|
|
163
|
+
assert.strictEqual(result.data.findings.length, 0, 'should have no findings');
|
|
164
|
+
} finally {
|
|
165
|
+
teardown(dir);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('no violations: no docker files in project', async () => {
|
|
170
|
+
const dir = createTmpProject({});
|
|
171
|
+
initGitRepo(dir);
|
|
172
|
+
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
path.join(dir, 'main.py'),
|
|
175
|
+
`def hello():\n return "world"\n`
|
|
176
|
+
);
|
|
177
|
+
spawnSync('git', ['add', 'main.py'], { cwd: dir });
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const result = await runDockerGuard(dir);
|
|
181
|
+
assert.strictEqual(result.exit_code, 0);
|
|
182
|
+
assert.strictEqual(result.data.findings.length, 0, 'should have no findings');
|
|
183
|
+
} finally {
|
|
184
|
+
teardown(dir);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — Technology detection integration tests.
|
|
3
|
+
* Tests the full autoDetectStack pipeline with Phase-1 technology signatures.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it } = require('node:test');
|
|
7
|
+
const assert = require('node:assert');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { autoDetectStack } = require('../../src/detector');
|
|
10
|
+
const { createTmpProject, teardown } = require('../helpers');
|
|
11
|
+
|
|
12
|
+
describe('TechnologyDetector integration', () => {
|
|
13
|
+
describe('autoDetectStack — techTools for Celery project', () => {
|
|
14
|
+
it('populates techTools with celery_guard for a Django+Celery project', () => {
|
|
15
|
+
const dir = createTmpProject({
|
|
16
|
+
'manage.py': '',
|
|
17
|
+
'requirements.txt': 'celery>=5.3\ndjango>=4.0\npsycopg2-binary',
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
const config = autoDetectStack(dir);
|
|
21
|
+
assert.ok(config.techTools, 'techTools should be populated');
|
|
22
|
+
const names = config.techTools.map((t) => t.name);
|
|
23
|
+
assert.ok(names.includes('celery_guard'), 'should include celery_guard');
|
|
24
|
+
assert.ok(names.includes('postgres_guard'), 'should include postgres_guard');
|
|
25
|
+
assert.strictEqual(names.includes('redis_guard'), false, 'should not include redis_guard');
|
|
26
|
+
assert.strictEqual(names.includes('docker_guard'), false, 'should not include docker_guard');
|
|
27
|
+
} finally {
|
|
28
|
+
teardown(dir);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('populates techTools with all four guards for full-stack project', () => {
|
|
33
|
+
const dir = createTmpProject({
|
|
34
|
+
'manage.py': '',
|
|
35
|
+
'requirements.txt': 'celery>=5.3\nredis>=5.0\npsycopg2-binary\ndjango>=4.0',
|
|
36
|
+
'Dockerfile': 'FROM python:3.11-alpine',
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
const config = autoDetectStack(dir);
|
|
40
|
+
assert.ok(Array.isArray(config.techTools), 'techTools should be an array');
|
|
41
|
+
const names = config.techTools.map((t) => t.name);
|
|
42
|
+
assert.strictEqual(names.includes('celery_guard'), true, 'should include celery_guard');
|
|
43
|
+
assert.strictEqual(names.includes('redis_guard'), true, 'should include redis_guard');
|
|
44
|
+
assert.strictEqual(names.includes('postgres_guard'), true, 'should include postgres_guard');
|
|
45
|
+
assert.strictEqual(names.includes('docker_guard'), true, 'should include docker_guard');
|
|
46
|
+
assert.strictEqual(config.techTools.length, 4, 'should have exactly 4 tech tools');
|
|
47
|
+
} finally {
|
|
48
|
+
teardown(dir);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('populates techTools with docker_guard for Docker-only detection', () => {
|
|
53
|
+
const dir = createTmpProject({
|
|
54
|
+
'Dockerfile': 'FROM node:18-alpine\nCMD ["node", "server.js"]',
|
|
55
|
+
'package.json': JSON.stringify({ name: 'myapp', version: '1.0.0' }),
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
const config = autoDetectStack(dir);
|
|
59
|
+
const names = config.techTools.map((t) => t.name);
|
|
60
|
+
assert.ok(names.includes('docker_guard'), 'should include docker_guard');
|
|
61
|
+
assert.strictEqual(config.techTools.length, 1, 'should have exactly 1 tech tool');
|
|
62
|
+
} finally {
|
|
63
|
+
teardown(dir);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns empty techTools for project with no Phase-1 signatures', () => {
|
|
68
|
+
const dir = createTmpProject({
|
|
69
|
+
'package.json': JSON.stringify({ name: 'myapp', version: '1.0.0' }),
|
|
70
|
+
'README.md': '# My Project',
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
const config = autoDetectStack(dir);
|
|
74
|
+
assert.ok(Array.isArray(config.techTools), 'techTools should be an array');
|
|
75
|
+
assert.strictEqual(config.techTools.length, 0, 'techTools should be empty');
|
|
76
|
+
} finally {
|
|
77
|
+
teardown(dir);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns empty techTools for non-existent directory', () => {
|
|
82
|
+
const config = autoDetectStack('/non/existent/path');
|
|
83
|
+
assert.ok(Array.isArray(config.techTools));
|
|
84
|
+
assert.strictEqual(config.techTools.length, 0, 'techTools should be empty');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('tool entries have correct language, path, command, and description', () => {
|
|
88
|
+
const dir = createTmpProject({
|
|
89
|
+
'Dockerfile': 'FROM node:18-alpine',
|
|
90
|
+
'package.json': JSON.stringify({ name: 'myapp' }),
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
const config = autoDetectStack(dir);
|
|
94
|
+
const dockerTool = config.techTools.find((t) => t.name === 'docker_guard');
|
|
95
|
+
assert.ok(dockerTool, 'should have docker_guard tool');
|
|
96
|
+
assert.strictEqual(dockerTool.language, 'py', 'language should be py');
|
|
97
|
+
assert.strictEqual(dockerTool.path, 'docker_guard.py', 'path should be docker_guard.py');
|
|
98
|
+
assert.ok(dockerTool.command.startsWith('python3 tools/'), 'command should run python3');
|
|
99
|
+
assert.ok(dockerTool.description.length > 0, 'description should be non-empty');
|
|
100
|
+
} finally {
|
|
101
|
+
teardown(dir);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('Redis + Docker project returns redis_guard and docker_guard', () => {
|
|
106
|
+
const dir = createTmpProject({
|
|
107
|
+
'requirements.txt': 'redis>=5.0\ncelery>=5.3',
|
|
108
|
+
'Dockerfile': 'FROM python:3.11-slim',
|
|
109
|
+
'docker-compose.yml': 'version: "3.8"\nservices:\n app:\n build: .',
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
const config = autoDetectStack(dir);
|
|
113
|
+
const names = config.techTools.map((t) => t.name);
|
|
114
|
+
assert.ok(names.includes('celery_guard'), 'should include celery_guard');
|
|
115
|
+
assert.ok(names.includes('redis_guard'), 'should include redis_guard');
|
|
116
|
+
assert.ok(names.includes('docker_guard'), 'should include docker_guard');
|
|
117
|
+
assert.strictEqual(names.includes('postgres_guard'), false, 'should not include postgres_guard');
|
|
118
|
+
} finally {
|
|
119
|
+
teardown(dir);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|