configsentry 0.0.11 → 0.0.13
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 +4 -1
- package/dist/rules.js +91 -0
- package/dist/rules.test.js +35 -0
- package/package.json +1 -1
- package/src/rules.test.ts +42 -0
- package/src/rules.ts +94 -0
package/README.md
CHANGED
|
@@ -7,6 +7,9 @@ Developer-first guardrails for **docker-compose.yml** (security + ops footguns).
|
|
|
7
7
|
## What it does
|
|
8
8
|
ConfigSentry reads a Compose file and flags common **high-impact** mistakes:
|
|
9
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`)
|
|
10
13
|
- Docker socket mounts (`/var/run/docker.sock`)
|
|
11
14
|
- sensitive ports exposed publicly (e.g. `5432:5432` instead of `127.0.0.1:5432:5432`)
|
|
12
15
|
- missing `restart:` policy
|
|
@@ -96,7 +99,7 @@ jobs:
|
|
|
96
99
|
runs-on: ubuntu-latest
|
|
97
100
|
steps:
|
|
98
101
|
- uses: actions/checkout@v4
|
|
99
|
-
- uses: alfredMorgenstern/configsentry@v0.0.
|
|
102
|
+
- uses: alfredMorgenstern/configsentry@v0.0.13
|
|
100
103
|
with:
|
|
101
104
|
target: .
|
|
102
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) {
|
|
@@ -75,6 +137,35 @@ export function runRules(compose, targetPath) {
|
|
|
75
137
|
suggestion: 'Avoid mounting /. Mount only specific directories required by the app.'
|
|
76
138
|
});
|
|
77
139
|
}
|
|
140
|
+
if (v.startsWith('/dev:/dev') || v.startsWith('/dev/:/dev')) {
|
|
141
|
+
findings.push({
|
|
142
|
+
id: 'compose.host-dev-mount',
|
|
143
|
+
title: 'Host /dev mounted into container',
|
|
144
|
+
severity: 'high',
|
|
145
|
+
message: `Service '${serviceName}' mounts host /dev into the container ('${v}'), which can enable device access and privilege escalation.`,
|
|
146
|
+
service: serviceName,
|
|
147
|
+
path: `${targetPath}#services.${serviceName}.volumes`,
|
|
148
|
+
suggestion: 'Avoid mounting /dev. If hardware access is required, map only the specific device(s) needed via devices:.'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Rule: dangerous device mappings
|
|
153
|
+
const devices = Array.isArray(svc?.devices) ? svc.devices : [];
|
|
154
|
+
for (const d of devices) {
|
|
155
|
+
if (typeof d !== 'string')
|
|
156
|
+
continue;
|
|
157
|
+
const lower = d.toLowerCase();
|
|
158
|
+
if (lower.includes('/dev/mem') || lower.includes('/dev/kmem') || lower.includes('/dev/kmsg')) {
|
|
159
|
+
findings.push({
|
|
160
|
+
id: 'compose.dangerous-device',
|
|
161
|
+
title: 'Dangerous device mapped into container',
|
|
162
|
+
severity: 'high',
|
|
163
|
+
message: `Service '${serviceName}' maps a sensitive device into the container ('${d}').`,
|
|
164
|
+
service: serviceName,
|
|
165
|
+
path: `${targetPath}#services.${serviceName}.devices`,
|
|
166
|
+
suggestion: 'Avoid mapping kernel/memory/log devices into containers. If absolutely required, isolate the host and restrict container privileges.'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
78
169
|
}
|
|
79
170
|
// Rule: restart policy
|
|
80
171
|
if (svc?.restart == null) {
|
package/dist/rules.test.js
CHANGED
|
@@ -16,3 +16,38 @@ 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
|
+
});
|
|
44
|
+
test('detects host /dev mount', () => {
|
|
45
|
+
const compose = { services: { app: { volumes: ['/dev:/dev'] } } };
|
|
46
|
+
const findings = runRules(compose, 'docker-compose.yml');
|
|
47
|
+
assert.ok(findings.some((f) => f.id === 'compose.host-dev-mount' && f.service === 'app'));
|
|
48
|
+
});
|
|
49
|
+
test('detects dangerous device mapping', () => {
|
|
50
|
+
const compose = { services: { app: { devices: ['/dev/kmsg:/dev/kmsg'] } } };
|
|
51
|
+
const findings = runRules(compose, 'docker-compose.yml');
|
|
52
|
+
assert.ok(findings.some((f) => f.id === 'compose.dangerous-device' && f.service === 'app'));
|
|
53
|
+
});
|
package/package.json
CHANGED
package/src/rules.test.ts
CHANGED
|
@@ -19,3 +19,45 @@ 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
|
+
});
|
|
52
|
+
|
|
53
|
+
test('detects host /dev mount', () => {
|
|
54
|
+
const compose = { services: { app: { volumes: ['/dev:/dev'] } } };
|
|
55
|
+
const findings = runRules(compose, 'docker-compose.yml');
|
|
56
|
+
assert.ok(findings.some((f) => f.id === 'compose.host-dev-mount' && f.service === 'app'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('detects dangerous device mapping', () => {
|
|
60
|
+
const compose = { services: { app: { devices: ['/dev/kmsg:/dev/kmsg'] } } };
|
|
61
|
+
const findings = runRules(compose, 'docker-compose.yml');
|
|
62
|
+
assert.ok(findings.some((f) => f.id === 'compose.dangerous-device' && f.service === 'app'));
|
|
63
|
+
});
|
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) {
|
|
@@ -76,6 +141,35 @@ export function runRules(compose: any, targetPath: string): Finding[] {
|
|
|
76
141
|
suggestion: 'Avoid mounting /. Mount only specific directories required by the app.'
|
|
77
142
|
});
|
|
78
143
|
}
|
|
144
|
+
if (v.startsWith('/dev:/dev') || v.startsWith('/dev/:/dev')) {
|
|
145
|
+
findings.push({
|
|
146
|
+
id: 'compose.host-dev-mount',
|
|
147
|
+
title: 'Host /dev mounted into container',
|
|
148
|
+
severity: 'high',
|
|
149
|
+
message: `Service '${serviceName}' mounts host /dev into the container ('${v}'), which can enable device access and privilege escalation.`,
|
|
150
|
+
service: serviceName,
|
|
151
|
+
path: `${targetPath}#services.${serviceName}.volumes`,
|
|
152
|
+
suggestion: 'Avoid mounting /dev. If hardware access is required, map only the specific device(s) needed via devices:.'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Rule: dangerous device mappings
|
|
158
|
+
const devices: any[] = Array.isArray(svc?.devices) ? svc.devices : [];
|
|
159
|
+
for (const d of devices) {
|
|
160
|
+
if (typeof d !== 'string') continue;
|
|
161
|
+
const lower = d.toLowerCase();
|
|
162
|
+
if (lower.includes('/dev/mem') || lower.includes('/dev/kmem') || lower.includes('/dev/kmsg')) {
|
|
163
|
+
findings.push({
|
|
164
|
+
id: 'compose.dangerous-device',
|
|
165
|
+
title: 'Dangerous device mapped into container',
|
|
166
|
+
severity: 'high',
|
|
167
|
+
message: `Service '${serviceName}' maps a sensitive device into the container ('${d}').`,
|
|
168
|
+
service: serviceName,
|
|
169
|
+
path: `${targetPath}#services.${serviceName}.devices`,
|
|
170
|
+
suggestion: 'Avoid mapping kernel/memory/log devices into containers. If absolutely required, isolate the host and restrict container privileges.'
|
|
171
|
+
});
|
|
172
|
+
}
|
|
79
173
|
}
|
|
80
174
|
|
|
81
175
|
// Rule: restart policy
|