configsentry 0.0.26 → 0.0.27

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.
Files changed (2) hide show
  1. package/dist/rules.js +95 -0
  2. package/package.json +1 -1
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 [];
@@ -261,6 +300,27 @@ export function runRules(compose, targetPath) {
261
300
  suggestion: 'Set user: "1000:1000" (or a dedicated UID/GID) and ensure the image supports running unprivileged.'
262
301
  });
263
302
  }
303
+ // Rule: hardcoded secrets in environment
304
+ for (const { key, value, raw } of extractEnv(svc)) {
305
+ if (!SECRET_KEY_RE.test(key))
306
+ continue;
307
+ const v = String(value).trim();
308
+ if (v.length === 0)
309
+ continue;
310
+ // ${VAR} / ${VAR:-default} / ${VAR?err}
311
+ if (v.startsWith('${') && v.endsWith('}'))
312
+ continue;
313
+ const placeholder = PLACEHOLDER_VALUE_RE.test(v) || v.toLowerCase().includes('changeme');
314
+ findings.push({
315
+ id: 'compose.hardcoded-secret',
316
+ title: 'Possible hardcoded secret in compose environment',
317
+ severity: placeholder ? 'medium' : 'high',
318
+ message: `Service '${serviceName}' sets '${key}' to a literal value in environment ('${raw}').`,
319
+ service: serviceName,
320
+ path: `${targetPath}#services.${serviceName}.environment`,
321
+ suggestion: 'Avoid committing secrets in docker-compose.yml. Prefer ${VAR} with a .env file (gitignored) or Docker secrets where supported.'
322
+ });
323
+ }
264
324
  // Rule: filesystem not read-only (hardening)
265
325
  // Low severity because many images expect write access unless explicitly designed for read-only.
266
326
  if (svc?.read_only !== true) {
@@ -274,6 +334,41 @@ export function runRules(compose, targetPath) {
274
334
  suggestion: 'Consider setting read_only: true + add explicit writable mounts (e.g. tmpfs:/tmp or a data volume) if the app supports it.'
275
335
  });
276
336
  }
337
+ // Rule: floating / unpinned image tags
338
+ const image = svc?.image;
339
+ if (typeof image === 'string' && image.trim() !== '') {
340
+ // If pinned by digest, it's reproducible.
341
+ if (!image.includes('@')) {
342
+ const lastSlash = image.lastIndexOf('/');
343
+ const lastColon = image.lastIndexOf(':');
344
+ const hasTag = lastColon > lastSlash;
345
+ if (!hasTag) {
346
+ findings.push({
347
+ id: 'compose.image-floating-tag',
348
+ title: 'Image tag not pinned',
349
+ severity: 'medium',
350
+ message: `Service '${serviceName}' uses an image without an explicit tag ('${image}').`,
351
+ service: serviceName,
352
+ path: `${targetPath}#services.${serviceName}.image`,
353
+ suggestion: "Pin the image to a version tag (e.g. 'nginx:1.27') or a digest (e.g. 'nginx@sha256:...') for reproducible deployments."
354
+ });
355
+ }
356
+ else {
357
+ const tag = image.slice(lastColon + 1).trim();
358
+ if (tag.toLowerCase() === 'latest') {
359
+ findings.push({
360
+ id: 'compose.image-floating-tag',
361
+ title: 'Image tag not pinned',
362
+ severity: 'medium',
363
+ message: `Service '${serviceName}' uses a floating image tag ('${image}').`,
364
+ service: serviceName,
365
+ path: `${targetPath}#services.${serviceName}.image`,
366
+ suggestion: "Pin the image to a version tag (e.g. 'nginx:1.27') or a digest (e.g. 'nginx@sha256:...') for reproducible deployments."
367
+ });
368
+ }
369
+ }
370
+ }
371
+ }
277
372
  // Rule: exposed sensitive ports
278
373
  const ports = normalizePorts(svc?.ports);
279
374
  for (const p of ports) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configsentry",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "description": "Developer-first guardrails for docker-compose.yml (security + ops footguns).",
5
5
  "type": "module",
6
6
  "license": "MIT",