configsentry 0.0.10 → 0.0.12

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 CHANGED
@@ -1,10 +1,15 @@
1
1
  # ConfigSentry (MVP)
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/configsentry.svg)](https://www.npmjs.com/package/configsentry)
4
+
3
5
  Developer-first guardrails for **docker-compose.yml** (security + ops footguns).
4
6
 
5
7
  ## What it does
6
8
  ConfigSentry reads a Compose file and flags common **high-impact** mistakes:
7
9
  - privileged containers (`privileged: true`)
10
+ - dangerous capabilities (`cap_add: [ALL]`)
11
+ - host namespaces (`network_mode: host`, `pid: host`, `ipc: host`)
12
+ - unconfined security profiles (`security_opt: ["seccomp=unconfined"]` / `apparmor:unconfined`)
8
13
  - Docker socket mounts (`/var/run/docker.sock`)
9
14
  - sensitive ports exposed publicly (e.g. `5432:5432` instead of `127.0.0.1:5432:5432`)
10
15
  - missing `restart:` policy
@@ -15,7 +20,7 @@ Designed to be **CI-friendly** (non-zero exit code when findings exist).
15
20
 
16
21
  ## Quickstart
17
22
 
18
- ### Run via npx (after npm publish)
23
+ ### Run via npx
19
24
 
20
25
  ```bash
21
26
  npx configsentry ./docker-compose.yml
@@ -94,7 +99,7 @@ jobs:
94
99
  runs-on: ubuntu-latest
95
100
  steps:
96
101
  - uses: actions/checkout@v4
97
- - uses: alfredMorgenstern/configsentry@v0.0.9
102
+ - uses: alfredMorgenstern/configsentry@v0.0.13
98
103
  with:
99
104
  target: .
100
105
  # optional: baseline: .configsentry-baseline.json
package/dist/rules.js CHANGED
@@ -48,6 +48,68 @@ export function runRules(compose, targetPath) {
48
48
  suggestion: 'Remove privileged: true unless absolutely required; prefer adding only the needed capabilities.'
49
49
  });
50
50
  }
51
+ // Rule: cap_add: [ALL]
52
+ const capAdd = Array.isArray(svc?.cap_add) ? svc.cap_add : [];
53
+ if (capAdd.some((c) => String(c).toUpperCase() === 'ALL')) {
54
+ findings.push({
55
+ id: 'compose.cap-add-all',
56
+ title: 'Dangerous Linux capabilities (cap_add: ALL)',
57
+ severity: 'high',
58
+ message: `Service '${serviceName}' uses cap_add: [ALL], which is effectively privileged in many cases.`,
59
+ service: serviceName,
60
+ path: `${targetPath}#services.${serviceName}.cap_add`,
61
+ suggestion: 'Remove cap_add: ALL. Add only the specific capabilities required (e.g. NET_BIND_SERVICE) or redesign to avoid it.'
62
+ });
63
+ }
64
+ // Rule: host namespaces (network/pid/ipc)
65
+ if (svc?.network_mode === 'host') {
66
+ findings.push({
67
+ id: 'compose.network-host',
68
+ title: 'Host network namespace (network_mode: host)',
69
+ severity: 'high',
70
+ message: `Service '${serviceName}' uses network_mode: host, bypassing Docker network isolation.`,
71
+ service: serviceName,
72
+ path: `${targetPath}#services.${serviceName}.network_mode`,
73
+ suggestion: 'Avoid host networking. Prefer explicit port mappings or internal networks.'
74
+ });
75
+ }
76
+ if (svc?.pid === 'host') {
77
+ findings.push({
78
+ id: 'compose.pid-host',
79
+ title: 'Host PID namespace (pid: host)',
80
+ severity: 'high',
81
+ message: `Service '${serviceName}' uses pid: host, exposing host process namespace to the container.`,
82
+ service: serviceName,
83
+ path: `${targetPath}#services.${serviceName}.pid`,
84
+ suggestion: 'Avoid pid: host unless you are building low-level host tooling and understand the security implications.'
85
+ });
86
+ }
87
+ if (svc?.ipc === 'host') {
88
+ findings.push({
89
+ id: 'compose.ipc-host',
90
+ title: 'Host IPC namespace (ipc: host)',
91
+ severity: 'high',
92
+ message: `Service '${serviceName}' uses ipc: host, exposing host IPC namespace to the container.`,
93
+ service: serviceName,
94
+ path: `${targetPath}#services.${serviceName}.ipc`,
95
+ suggestion: 'Avoid ipc: host. Prefer explicit shared volumes or redesign if IPC sharing is required.'
96
+ });
97
+ }
98
+ // Rule: unconfined security profiles
99
+ const securityOpt = Array.isArray(svc?.security_opt) ? svc.security_opt : [];
100
+ const sec = securityOpt.map((x) => String(x).toLowerCase());
101
+ const hasUnconfined = sec.some((x) => x.includes('seccomp') && x.includes('unconfined')) || sec.some((x) => x.includes('apparmor') && x.includes('unconfined')) || sec.some((x) => x.includes('label:disable'));
102
+ if (hasUnconfined) {
103
+ findings.push({
104
+ id: 'compose.security-unconfined',
105
+ title: 'Security profile disabled (unconfined)',
106
+ severity: 'high',
107
+ message: `Service '${serviceName}' disables container security profiles via security_opt (${securityOpt.join(', ')}).`,
108
+ service: serviceName,
109
+ path: `${targetPath}#services.${serviceName}.security_opt`,
110
+ suggestion: 'Avoid unconfined security profiles. Remove the option or use a minimal custom seccomp/apparmor profile.'
111
+ });
112
+ }
51
113
  // Rule: docker socket mount
52
114
  const volumes = Array.isArray(svc?.volumes) ? svc.volumes : [];
53
115
  for (const v of volumes) {
@@ -16,3 +16,28 @@ test('detects docker socket mount', () => {
16
16
  const findings = runRules(compose, 'docker-compose.yml');
17
17
  assert.ok(findings.some((f) => f.id === 'compose.docker-socket' && f.service === 'runner'));
18
18
  });
19
+ test('detects cap_add: ALL', () => {
20
+ const compose = { services: { app: { cap_add: ['ALL'] } } };
21
+ const findings = runRules(compose, 'docker-compose.yml');
22
+ assert.ok(findings.some((f) => f.id === 'compose.cap-add-all' && f.service === 'app'));
23
+ });
24
+ test('detects network_mode: host', () => {
25
+ const compose = { services: { app: { network_mode: 'host' } } };
26
+ const findings = runRules(compose, 'docker-compose.yml');
27
+ assert.ok(findings.some((f) => f.id === 'compose.network-host' && f.service === 'app'));
28
+ });
29
+ test('detects pid: host', () => {
30
+ const compose = { services: { app: { pid: 'host' } } };
31
+ const findings = runRules(compose, 'docker-compose.yml');
32
+ assert.ok(findings.some((f) => f.id === 'compose.pid-host' && f.service === 'app'));
33
+ });
34
+ test('detects ipc: host', () => {
35
+ const compose = { services: { app: { ipc: 'host' } } };
36
+ const findings = runRules(compose, 'docker-compose.yml');
37
+ assert.ok(findings.some((f) => f.id === 'compose.ipc-host' && f.service === 'app'));
38
+ });
39
+ test('detects unconfined security_opt', () => {
40
+ const compose = { services: { app: { security_opt: ['seccomp=unconfined'] } } };
41
+ const findings = runRules(compose, 'docker-compose.yml');
42
+ assert.ok(findings.some((f) => f.id === 'compose.security-unconfined' && f.service === 'app'));
43
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configsentry",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Developer-first guardrails for docker-compose.yml (security + ops footguns).",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/rules.test.ts CHANGED
@@ -19,3 +19,33 @@ test('detects docker socket mount', () => {
19
19
  const findings = runRules(compose, 'docker-compose.yml');
20
20
  assert.ok(findings.some((f) => f.id === 'compose.docker-socket' && f.service === 'runner'));
21
21
  });
22
+
23
+ test('detects cap_add: ALL', () => {
24
+ const compose = { services: { app: { cap_add: ['ALL'] } } };
25
+ const findings = runRules(compose, 'docker-compose.yml');
26
+ assert.ok(findings.some((f) => f.id === 'compose.cap-add-all' && f.service === 'app'));
27
+ });
28
+
29
+ test('detects network_mode: host', () => {
30
+ const compose = { services: { app: { network_mode: 'host' } } };
31
+ const findings = runRules(compose, 'docker-compose.yml');
32
+ assert.ok(findings.some((f) => f.id === 'compose.network-host' && f.service === 'app'));
33
+ });
34
+
35
+ test('detects pid: host', () => {
36
+ const compose = { services: { app: { pid: 'host' } } };
37
+ const findings = runRules(compose, 'docker-compose.yml');
38
+ assert.ok(findings.some((f) => f.id === 'compose.pid-host' && f.service === 'app'));
39
+ });
40
+
41
+ test('detects ipc: host', () => {
42
+ const compose = { services: { app: { ipc: 'host' } } };
43
+ const findings = runRules(compose, 'docker-compose.yml');
44
+ assert.ok(findings.some((f) => f.id === 'compose.ipc-host' && f.service === 'app'));
45
+ });
46
+
47
+ test('detects unconfined security_opt', () => {
48
+ const compose = { services: { app: { security_opt: ['seccomp=unconfined'] } } };
49
+ const findings = runRules(compose, 'docker-compose.yml');
50
+ assert.ok(findings.some((f) => f.id === 'compose.security-unconfined' && f.service === 'app'));
51
+ });
package/src/rules.ts CHANGED
@@ -50,6 +50,71 @@ export function runRules(compose: any, targetPath: string): Finding[] {
50
50
  });
51
51
  }
52
52
 
53
+ // Rule: cap_add: [ALL]
54
+ const capAdd: any[] = Array.isArray(svc?.cap_add) ? svc.cap_add : [];
55
+ if (capAdd.some((c) => String(c).toUpperCase() === 'ALL')) {
56
+ findings.push({
57
+ id: 'compose.cap-add-all',
58
+ title: 'Dangerous Linux capabilities (cap_add: ALL)',
59
+ severity: 'high',
60
+ message: `Service '${serviceName}' uses cap_add: [ALL], which is effectively privileged in many cases.`,
61
+ service: serviceName,
62
+ path: `${targetPath}#services.${serviceName}.cap_add`,
63
+ suggestion: 'Remove cap_add: ALL. Add only the specific capabilities required (e.g. NET_BIND_SERVICE) or redesign to avoid it.'
64
+ });
65
+ }
66
+
67
+ // Rule: host namespaces (network/pid/ipc)
68
+ if (svc?.network_mode === 'host') {
69
+ findings.push({
70
+ id: 'compose.network-host',
71
+ title: 'Host network namespace (network_mode: host)',
72
+ severity: 'high',
73
+ message: `Service '${serviceName}' uses network_mode: host, bypassing Docker network isolation.`,
74
+ service: serviceName,
75
+ path: `${targetPath}#services.${serviceName}.network_mode`,
76
+ suggestion: 'Avoid host networking. Prefer explicit port mappings or internal networks.'
77
+ });
78
+ }
79
+ if (svc?.pid === 'host') {
80
+ findings.push({
81
+ id: 'compose.pid-host',
82
+ title: 'Host PID namespace (pid: host)',
83
+ severity: 'high',
84
+ message: `Service '${serviceName}' uses pid: host, exposing host process namespace to the container.`,
85
+ service: serviceName,
86
+ path: `${targetPath}#services.${serviceName}.pid`,
87
+ suggestion: 'Avoid pid: host unless you are building low-level host tooling and understand the security implications.'
88
+ });
89
+ }
90
+ if (svc?.ipc === 'host') {
91
+ findings.push({
92
+ id: 'compose.ipc-host',
93
+ title: 'Host IPC namespace (ipc: host)',
94
+ severity: 'high',
95
+ message: `Service '${serviceName}' uses ipc: host, exposing host IPC namespace to the container.`,
96
+ service: serviceName,
97
+ path: `${targetPath}#services.${serviceName}.ipc`,
98
+ suggestion: 'Avoid ipc: host. Prefer explicit shared volumes or redesign if IPC sharing is required.'
99
+ });
100
+ }
101
+
102
+ // Rule: unconfined security profiles
103
+ const securityOpt: any[] = Array.isArray(svc?.security_opt) ? svc.security_opt : [];
104
+ const sec = securityOpt.map((x) => String(x).toLowerCase());
105
+ const hasUnconfined = sec.some((x) => x.includes('seccomp') && x.includes('unconfined')) || sec.some((x) => x.includes('apparmor') && x.includes('unconfined')) || sec.some((x) => x.includes('label:disable'));
106
+ if (hasUnconfined) {
107
+ findings.push({
108
+ id: 'compose.security-unconfined',
109
+ title: 'Security profile disabled (unconfined)',
110
+ severity: 'high',
111
+ message: `Service '${serviceName}' disables container security profiles via security_opt (${securityOpt.join(', ')}).`,
112
+ service: serviceName,
113
+ path: `${targetPath}#services.${serviceName}.security_opt`,
114
+ suggestion: 'Avoid unconfined security profiles. Remove the option or use a minimal custom seccomp/apparmor profile.'
115
+ });
116
+ }
117
+
53
118
  // Rule: docker socket mount
54
119
  const volumes: any[] = Array.isArray(svc?.volumes) ? svc.volumes : [];
55
120
  for (const v of volumes) {