configsentry 0.0.26 → 0.0.28
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/dist/rules.js +133 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ Tip: for machine output use `--format json` / `--format sarif`.
|
|
|
102
102
|
|
|
103
103
|
## Docs
|
|
104
104
|
|
|
105
|
+
- Proof in the wild (public repo tests): [`repo-tests/`](repo-tests/)
|
|
105
106
|
- GitHub Action usage examples: [`docs/action-usage.md`](docs/action-usage.md)
|
|
106
107
|
- Baselines (incremental adoption): [`docs/baselines.md`](docs/baselines.md)
|
|
107
108
|
- Compatibility & scope: [`docs/compatibility.md`](docs/compatibility.md)
|
package/dist/rules.js
CHANGED
|
@@ -1,4 +1,43 @@
|
|
|
1
1
|
const SENSITIVE_PORTS = new Set([5432, 3306, 6379, 27017, 9200]);
|
|
2
|
+
const SECRET_KEY_RE = /(pass(word)?|secret|token|api[_-]?key|private[_-]?key)/i;
|
|
3
|
+
const PLACEHOLDER_VALUE_RE = /^(changeme|change-me|password|secret|token|example|example_password|yourpassword|your_password|replace_me|replace-me|TODO)$/i;
|
|
4
|
+
function extractEnv(svc) {
|
|
5
|
+
const env = svc?.environment;
|
|
6
|
+
const res = [];
|
|
7
|
+
// environment:
|
|
8
|
+
// KEY: value
|
|
9
|
+
// or
|
|
10
|
+
// environment:
|
|
11
|
+
// - KEY=value
|
|
12
|
+
// - KEY
|
|
13
|
+
if (env && typeof env === 'object' && !Array.isArray(env)) {
|
|
14
|
+
for (const [k, v] of Object.entries(env)) {
|
|
15
|
+
if (typeof k !== 'string')
|
|
16
|
+
continue;
|
|
17
|
+
if (v == null)
|
|
18
|
+
continue;
|
|
19
|
+
res.push({ key: k, value: String(v), raw: `${k}=${String(v)}` });
|
|
20
|
+
}
|
|
21
|
+
return res;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(env)) {
|
|
24
|
+
for (const item of env) {
|
|
25
|
+
if (typeof item !== 'string')
|
|
26
|
+
continue;
|
|
27
|
+
const idx = item.indexOf('=');
|
|
28
|
+
if (idx === -1) {
|
|
29
|
+
// KEY (value from environment at runtime) — not a hardcoded secret
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const key = item.slice(0, idx);
|
|
33
|
+
const value = item.slice(idx + 1);
|
|
34
|
+
if (!key)
|
|
35
|
+
continue;
|
|
36
|
+
res.push({ key, value, raw: item });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return res;
|
|
40
|
+
}
|
|
2
41
|
function normalizePorts(ports) {
|
|
3
42
|
if (!Array.isArray(ports))
|
|
4
43
|
return [];
|
|
@@ -224,6 +263,44 @@ export function runRules(compose, targetPath) {
|
|
|
224
263
|
});
|
|
225
264
|
}
|
|
226
265
|
}
|
|
266
|
+
// Rule: depends_on without healthchecks / service_healthy
|
|
267
|
+
// Compose nuance: service_healthy requires healthchecks and is not universally used.
|
|
268
|
+
// We keep this as a gentle reliability warning.
|
|
269
|
+
const dependsOn = svc?.depends_on;
|
|
270
|
+
if (dependsOn != null) {
|
|
271
|
+
// Collect dependency service names.
|
|
272
|
+
const deps = [];
|
|
273
|
+
if (Array.isArray(dependsOn)) {
|
|
274
|
+
for (const d of dependsOn)
|
|
275
|
+
if (typeof d === 'string')
|
|
276
|
+
deps.push(d);
|
|
277
|
+
}
|
|
278
|
+
else if (dependsOn && typeof dependsOn === 'object') {
|
|
279
|
+
for (const k of Object.keys(dependsOn))
|
|
280
|
+
deps.push(k);
|
|
281
|
+
}
|
|
282
|
+
// Detect whether any condition: service_healthy is used.
|
|
283
|
+
let hasServiceHealthy = false;
|
|
284
|
+
if (dependsOn && typeof dependsOn === 'object' && !Array.isArray(dependsOn)) {
|
|
285
|
+
for (const v of Object.values(dependsOn)) {
|
|
286
|
+
if (v && typeof v === 'object' && String(v.condition ?? '').toLowerCase() === 'service_healthy') {
|
|
287
|
+
hasServiceHealthy = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const depsMissingHealthchecks = deps.filter((d) => services?.[d]?.healthcheck == null);
|
|
292
|
+
if (depsMissingHealthchecks.length > 0 || !hasServiceHealthy) {
|
|
293
|
+
findings.push({
|
|
294
|
+
id: 'compose.depends-on-without-health',
|
|
295
|
+
title: 'depends_on without healthcheck gating',
|
|
296
|
+
severity: 'low',
|
|
297
|
+
message: `Service '${serviceName}' uses depends_on without robust healthcheck gating.`,
|
|
298
|
+
service: serviceName,
|
|
299
|
+
path: `${targetPath}#services.${serviceName}.depends_on`,
|
|
300
|
+
suggestion: "Prefer adding healthchecks and (where supported) depends_on: { <svc>: { condition: service_healthy } } to avoid startup race conditions."
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
227
304
|
// Rule: restart policy
|
|
228
305
|
if (svc?.restart == null) {
|
|
229
306
|
findings.push({
|
|
@@ -261,6 +338,27 @@ export function runRules(compose, targetPath) {
|
|
|
261
338
|
suggestion: 'Set user: "1000:1000" (or a dedicated UID/GID) and ensure the image supports running unprivileged.'
|
|
262
339
|
});
|
|
263
340
|
}
|
|
341
|
+
// Rule: hardcoded secrets in environment
|
|
342
|
+
for (const { key, value, raw } of extractEnv(svc)) {
|
|
343
|
+
if (!SECRET_KEY_RE.test(key))
|
|
344
|
+
continue;
|
|
345
|
+
const v = String(value).trim();
|
|
346
|
+
if (v.length === 0)
|
|
347
|
+
continue;
|
|
348
|
+
// ${VAR} / ${VAR:-default} / ${VAR?err}
|
|
349
|
+
if (v.startsWith('${') && v.endsWith('}'))
|
|
350
|
+
continue;
|
|
351
|
+
const placeholder = PLACEHOLDER_VALUE_RE.test(v) || v.toLowerCase().includes('changeme');
|
|
352
|
+
findings.push({
|
|
353
|
+
id: 'compose.hardcoded-secret',
|
|
354
|
+
title: 'Possible hardcoded secret in compose environment',
|
|
355
|
+
severity: placeholder ? 'medium' : 'high',
|
|
356
|
+
message: `Service '${serviceName}' sets '${key}' to a literal value in environment ('${raw}').`,
|
|
357
|
+
service: serviceName,
|
|
358
|
+
path: `${targetPath}#services.${serviceName}.environment`,
|
|
359
|
+
suggestion: 'Avoid committing secrets in docker-compose.yml. Prefer ${VAR} with a .env file (gitignored) or Docker secrets where supported.'
|
|
360
|
+
});
|
|
361
|
+
}
|
|
264
362
|
// Rule: filesystem not read-only (hardening)
|
|
265
363
|
// Low severity because many images expect write access unless explicitly designed for read-only.
|
|
266
364
|
if (svc?.read_only !== true) {
|
|
@@ -274,6 +372,41 @@ export function runRules(compose, targetPath) {
|
|
|
274
372
|
suggestion: 'Consider setting read_only: true + add explicit writable mounts (e.g. tmpfs:/tmp or a data volume) if the app supports it.'
|
|
275
373
|
});
|
|
276
374
|
}
|
|
375
|
+
// Rule: floating / unpinned image tags
|
|
376
|
+
const image = svc?.image;
|
|
377
|
+
if (typeof image === 'string' && image.trim() !== '') {
|
|
378
|
+
// If pinned by digest, it's reproducible.
|
|
379
|
+
if (!image.includes('@')) {
|
|
380
|
+
const lastSlash = image.lastIndexOf('/');
|
|
381
|
+
const lastColon = image.lastIndexOf(':');
|
|
382
|
+
const hasTag = lastColon > lastSlash;
|
|
383
|
+
if (!hasTag) {
|
|
384
|
+
findings.push({
|
|
385
|
+
id: 'compose.image-floating-tag',
|
|
386
|
+
title: 'Image tag not pinned',
|
|
387
|
+
severity: 'medium',
|
|
388
|
+
message: `Service '${serviceName}' uses an image without an explicit tag ('${image}').`,
|
|
389
|
+
service: serviceName,
|
|
390
|
+
path: `${targetPath}#services.${serviceName}.image`,
|
|
391
|
+
suggestion: "Pin the image to a version tag (e.g. 'nginx:1.27') or a digest (e.g. 'nginx@sha256:...') for reproducible deployments."
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
const tag = image.slice(lastColon + 1).trim();
|
|
396
|
+
if (tag.toLowerCase() === 'latest') {
|
|
397
|
+
findings.push({
|
|
398
|
+
id: 'compose.image-floating-tag',
|
|
399
|
+
title: 'Image tag not pinned',
|
|
400
|
+
severity: 'medium',
|
|
401
|
+
message: `Service '${serviceName}' uses a floating image tag ('${image}').`,
|
|
402
|
+
service: serviceName,
|
|
403
|
+
path: `${targetPath}#services.${serviceName}.image`,
|
|
404
|
+
suggestion: "Pin the image to a version tag (e.g. 'nginx:1.27') or a digest (e.g. 'nginx@sha256:...') for reproducible deployments."
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
277
410
|
// Rule: exposed sensitive ports
|
|
278
411
|
const ports = normalizePorts(svc?.ports);
|
|
279
412
|
for (const p of ports) {
|