configsentry 0.0.18 → 0.0.20

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
@@ -4,6 +4,37 @@
4
4
 
5
5
  Developer-first guardrails for **docker-compose.yml** (security + ops footguns).
6
6
 
7
+ ## 60-second quickstart
8
+
9
+ ### Local (npx)
10
+
11
+ ```bash
12
+ npx configsentry ./docker-compose.yml
13
+ ```
14
+
15
+ ### GitHub Action (minimal)
16
+
17
+ ```yml
18
+ - uses: alfredMorgenstern/configsentry@v0.0.18
19
+ with:
20
+ target: .
21
+ ```
22
+
23
+ ### GitHub Code Scanning (SARIF upload)
24
+
25
+ ```yml
26
+ permissions:
27
+ contents: read
28
+ security-events: write
29
+
30
+ - uses: alfredMorgenstern/configsentry@v0.0.18
31
+ with:
32
+ target: .
33
+ sarif: true
34
+ upload-sarif: true
35
+ fail-on-findings: false
36
+ ```
37
+
7
38
  ## What it does
8
39
  ConfigSentry reads a Compose file and flags common **high-impact** mistakes:
9
40
  - privileged containers (`privileged: true`)
@@ -11,6 +42,7 @@ ConfigSentry reads a Compose file and flags common **high-impact** mistakes:
11
42
  - host namespaces (`network_mode: host`, `pid: host`, `ipc: host`)
12
43
  - unconfined security profiles (`security_opt: ["seccomp=unconfined"]` / `apparmor:unconfined`)
13
44
  - Docker socket mounts (`/var/run/docker.sock`)
45
+ - sensitive host mounts (`/etc`, `/proc`, `/sys`)
14
46
  - sensitive ports exposed publicly (e.g. `5432:5432` instead of `127.0.0.1:5432:5432`)
15
47
  - missing `restart:` policy
16
48
  - missing `healthcheck:`
@@ -31,19 +63,19 @@ npx configsentry ./docker-compose.yml
31
63
  ```bash
32
64
  npm install
33
65
  npm run build
34
- node dist/cli.js ./docker-compose.yml
66
+ node dist/cli.js --target ./docker-compose.yml
35
67
  ```
36
68
 
37
69
  ### JSON output (CI / tooling)
38
70
 
39
71
  ```bash
40
- node dist/cli.js ./docker-compose.yml --json
72
+ node dist/cli.js --target ./docker-compose.yml --json
41
73
  ```
42
74
 
43
75
  ### SARIF output (GitHub Code Scanning)
44
76
 
45
77
  ```bash
46
- node dist/cli.js ./docker-compose.yml --sarif > configsentry.sarif.json
78
+ node dist/cli.js --target ./docker-compose.yml --sarif > configsentry.sarif.json
47
79
  ```
48
80
 
49
81
  ## Baselines (incremental adoption)
@@ -51,15 +83,23 @@ node dist/cli.js ./docker-compose.yml --sarif > configsentry.sarif.json
51
83
  Generate a baseline (captures current findings):
52
84
 
53
85
  ```bash
54
- node dist/cli.js ./docker-compose.yml --write-baseline .configsentry-baseline.json
86
+ node dist/cli.js --target ./docker-compose.yml --write-baseline .configsentry-baseline.json
55
87
  ```
56
88
 
57
89
  Then suppress baseline findings in CI:
58
90
 
59
91
  ```bash
60
- node dist/cli.js ./docker-compose.yml --baseline .configsentry-baseline.json
92
+ node dist/cli.js --target ./docker-compose.yml --baseline .configsentry-baseline.json
61
93
  ```
62
94
 
95
+ ## Docs
96
+
97
+ - GitHub Action usage examples: [`docs/action-usage.md`](docs/action-usage.md)
98
+ - Baselines (incremental adoption): [`docs/baselines.md`](docs/baselines.md)
99
+ - Compatibility & scope: [`docs/compatibility.md`](docs/compatibility.md)
100
+ - Troubleshooting / FAQ: [`docs/troubleshooting.md`](docs/troubleshooting.md)
101
+ - Launch pack (links + demo assets): [`docs/launch-pack.md`](docs/launch-pack.md)
102
+
63
103
  ## Use in GitHub Actions (copy/paste)
64
104
 
65
105
  More examples: [`docs/action-usage.md`](docs/action-usage.md)
@@ -81,7 +121,7 @@ jobs:
81
121
 
82
122
  - run: npm ci
83
123
  - run: npm run build
84
- - run: node dist/cli.js ./docker-compose.yml
124
+ - run: node dist/cli.js --target ./docker-compose.yml
85
125
  ```
86
126
 
87
127
  ### Option B: use the ConfigSentry composite action
@@ -111,7 +151,9 @@ jobs:
111
151
  # security-events: write
112
152
  ```
113
153
 
114
- > Tip: pin to a tag (like `v0.0.13`) for reproducible builds.
154
+ **Note (consumer repos):** your repo does **not** need a `package-lock.json`. The action installs/builds ConfigSentry from the action package itself.
155
+
156
+ > Tip: pin to a tag (like `v0.0.18`) for reproducible builds.
115
157
 
116
158
  ## Exit codes
117
159
  - `0` no findings
@@ -121,7 +163,7 @@ jobs:
121
163
  ## Example
122
164
 
123
165
  ```bash
124
- node dist/cli.js ./example.docker-compose.yml
166
+ node dist/cli.js --target ./example.docker-compose.yml
125
167
  ```
126
168
 
127
169
  ## Next steps
package/dist/cli.js CHANGED
@@ -8,15 +8,49 @@ import { runRules } from './rules.js';
8
8
  import { findingsToSarif } from './sarif.js';
9
9
  import { resolveTargets } from './scan.js';
10
10
  import { applyBaseline, loadBaseline, writeBaseline } from './baseline.js';
11
+ function parseArgs(argv) {
12
+ const args = argv.slice(2);
13
+ const help = args.includes('-h') || args.includes('--help');
14
+ const version = args.includes('-v') || args.includes('--version');
15
+ const json = args.includes('--json');
16
+ const sarif = args.includes('--sarif');
17
+ const output = json ? 'json' : sarif ? 'sarif' : 'pretty';
18
+ const baselineIdx = args.indexOf('--baseline');
19
+ const baselinePath = baselineIdx >= 0 ? args[baselineIdx + 1] : undefined;
20
+ const writeBaselineIdx = args.indexOf('--write-baseline');
21
+ const writeBaselinePath = writeBaselineIdx >= 0 ? args[writeBaselineIdx + 1] : undefined;
22
+ // Prefer explicit flag (matches the GitHub Action input)
23
+ const targetIdx = args.indexOf('--target');
24
+ const targetFromFlag = targetIdx >= 0 ? args[targetIdx + 1] : undefined;
25
+ // Back-compat: first positional arg
26
+ const targetFromPositional = args.find((a) => !a.startsWith('-'));
27
+ const target = targetFromFlag ?? targetFromPositional;
28
+ return { args, help, version, output, baselinePath, writeBaselinePath, target };
29
+ }
11
30
  function usage() {
12
- console.log(`ConfigSentry (MVP)\n\nUsage:\n configsentry <file-or-dir> [--json|--sarif] [--baseline <file>] [--write-baseline <file>]\n\nOutput:\n --json machine-readable findings\n --sarif SARIF 2.1.0 (for GitHub code scanning)\n\nBaselines:\n --baseline <file> suppress findings present in a baseline file\n --write-baseline <file> write baseline file for current findings and exit 0\n\nExit codes:\n 0 = no findings (after baseline suppression)
31
+ console.log(`ConfigSentry (MVP)
32
+
33
+ Usage:
34
+ configsentry <file-or-dir> [--json|--sarif] [--baseline <file>] [--write-baseline <file>]
35
+ configsentry --target <file-or-dir> [--json|--sarif] [--baseline <file>] [--write-baseline <file>]
36
+
37
+ Output:
38
+ --json machine-readable findings
39
+ --sarif SARIF 2.1.0 (for GitHub code scanning)
40
+
41
+ Baselines:
42
+ --baseline <file> suppress findings present in a baseline file
43
+ --write-baseline <file> write baseline file for current findings and exit 0
44
+
45
+ Exit codes:
46
+ 0 = no findings (after baseline suppression)
13
47
  2 = findings present
14
48
  1 = error
15
49
  `);
16
50
  }
17
51
  async function main() {
18
- const args = process.argv.slice(2);
19
- if (args.includes('-v') || args.includes('--version')) {
52
+ const { args, help, version, output, baselinePath, writeBaselinePath, target } = parseArgs(process.argv);
53
+ if (version) {
20
54
  try {
21
55
  const here = path.dirname(fileURLToPath(import.meta.url));
22
56
  const pkgPath = path.resolve(here, '../package.json');
@@ -29,21 +63,19 @@ async function main() {
29
63
  }
30
64
  process.exit(0);
31
65
  }
32
- if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
66
+ if (args.length === 0 || help) {
33
67
  usage();
34
68
  process.exit(0);
35
69
  }
36
- const json = args.includes('--json');
37
- const sarif = args.includes('--sarif');
38
- if (json && sarif) {
70
+ if (output === 'json' && args.includes('--sarif')) {
71
+ // should be impossible due to parseArgs, but keep a clear message
72
+ console.error('Error: choose only one output mode: --json or --sarif');
73
+ process.exit(1);
74
+ }
75
+ if (output === 'sarif' && args.includes('--json')) {
39
76
  console.error('Error: choose only one output mode: --json or --sarif');
40
77
  process.exit(1);
41
78
  }
42
- const baselineIdx = args.indexOf('--baseline');
43
- const baselinePath = baselineIdx >= 0 ? args[baselineIdx + 1] : undefined;
44
- const writeBaselineIdx = args.indexOf('--write-baseline');
45
- const writeBaselinePath = writeBaselineIdx >= 0 ? args[writeBaselineIdx + 1] : undefined;
46
- const target = args.find((a) => !a.startsWith('-'));
47
79
  if (!target) {
48
80
  usage();
49
81
  process.exit(1);
@@ -73,10 +105,10 @@ async function main() {
73
105
  console.log(`Wrote baseline: ${path.resolve(writeBaselinePath)} (${allFindings.length} finding(s))`);
74
106
  process.exit(0);
75
107
  }
76
- if (json) {
108
+ if (output === 'json') {
77
109
  console.log(JSON.stringify({ targetPaths, findings, suppressedCount: suppressed.length }, null, 2));
78
110
  }
79
- else if (sarif) {
111
+ else if (output === 'sarif') {
80
112
  console.log(JSON.stringify(findingsToSarif(findings), null, 2));
81
113
  }
82
114
  else {
package/dist/rules.js CHANGED
@@ -113,9 +113,66 @@ export function runRules(compose, targetPath) {
113
113
  // Rule: docker socket mount
114
114
  const volumes = Array.isArray(svc?.volumes) ? svc.volumes : [];
115
115
  for (const v of volumes) {
116
- if (typeof v !== 'string')
116
+ // Support both short syntax (string) and long syntax (object).
117
+ // Long syntax example:
118
+ // - type: bind
119
+ // source: /etc
120
+ // target: /host-etc
121
+ let raw = '';
122
+ let hostPath;
123
+ if (typeof v === 'string') {
124
+ raw = v;
125
+ hostPath = v.split(':')[0];
126
+ }
127
+ else if (v && typeof v === 'object') {
128
+ raw = JSON.stringify(v);
129
+ const type = String(v.type ?? '').toLowerCase();
130
+ if (type === '' || type === 'bind') {
131
+ hostPath = v.source ?? v.src;
132
+ }
133
+ }
134
+ else {
117
135
  continue;
118
- if (v.includes('/var/run/docker.sock')) {
136
+ }
137
+ // Rule: sensitive host path mounts
138
+ // Only consider bind mounts where the host path is absolute.
139
+ if (typeof hostPath === 'string' && hostPath.startsWith('/')) {
140
+ const hp = hostPath.replace(/\/$/, '');
141
+ if (hp === '/etc' || hp.startsWith('/etc/')) {
142
+ findings.push({
143
+ id: 'compose.host-etc-mount',
144
+ title: 'Sensitive host path mounted (/etc)',
145
+ severity: 'high',
146
+ message: `Service '${serviceName}' mounts host /etc into the container ('${raw}').`,
147
+ service: serviceName,
148
+ path: `${targetPath}#services.${serviceName}.volumes`,
149
+ suggestion: 'Avoid mounting /etc. If you only need a single config file, mount that file explicitly read-only.'
150
+ });
151
+ }
152
+ if (hp === '/proc' || hp.startsWith('/proc/')) {
153
+ findings.push({
154
+ id: 'compose.host-proc-mount',
155
+ title: 'Sensitive host path mounted (/proc)',
156
+ severity: 'high',
157
+ message: `Service '${serviceName}' mounts host /proc into the container ('${raw}').`,
158
+ service: serviceName,
159
+ path: `${targetPath}#services.${serviceName}.volumes`,
160
+ suggestion: 'Avoid mounting /proc. If you need host metrics, prefer safer exporters or explicit APIs.'
161
+ });
162
+ }
163
+ if (hp === '/sys' || hp.startsWith('/sys/')) {
164
+ findings.push({
165
+ id: 'compose.host-sys-mount',
166
+ title: 'Sensitive host path mounted (/sys)',
167
+ severity: 'high',
168
+ message: `Service '${serviceName}' mounts host /sys into the container ('${raw}').`,
169
+ service: serviceName,
170
+ path: `${targetPath}#services.${serviceName}.volumes`,
171
+ suggestion: 'Avoid mounting /sys. If hardware/host introspection is required, isolate the container and mount only specific needed subpaths read-only.'
172
+ });
173
+ }
174
+ }
175
+ if (raw.includes('/var/run/docker.sock')) {
119
176
  findings.push({
120
177
  id: 'compose.docker-socket',
121
178
  title: 'Docker socket mounted',
@@ -126,23 +183,23 @@ export function runRules(compose, targetPath) {
126
183
  suggestion: 'Avoid mounting the docker socket. If you need it, isolate the runner and treat it as privileged infrastructure.'
127
184
  });
128
185
  }
129
- if (v.startsWith('/:') || v.startsWith('/:/')) {
186
+ if (raw.startsWith('/:') || raw.startsWith('/:/') || hostPath === '/') {
130
187
  findings.push({
131
188
  id: 'compose.host-root-mount',
132
189
  title: 'Host root mounted',
133
190
  severity: 'high',
134
- message: `Service '${serviceName}' appears to mount the host root filesystem ('${v}').`,
191
+ message: `Service '${serviceName}' appears to mount the host root filesystem ('${raw}').`,
135
192
  service: serviceName,
136
193
  path: `${targetPath}#services.${serviceName}.volumes`,
137
194
  suggestion: 'Avoid mounting /. Mount only specific directories required by the app.'
138
195
  });
139
196
  }
140
- if (v.startsWith('/dev:/dev') || v.startsWith('/dev/:/dev')) {
197
+ if (raw.startsWith('/dev:/dev') || raw.startsWith('/dev/:/dev') || hostPath === '/dev') {
141
198
  findings.push({
142
199
  id: 'compose.host-dev-mount',
143
200
  title: 'Host /dev mounted into container',
144
201
  severity: 'high',
145
- message: `Service '${serviceName}' mounts host /dev into the container ('${v}'), which can enable device access and privilege escalation.`,
202
+ message: `Service '${serviceName}' mounts host /dev into the container ('${raw}'), which can enable device access and privilege escalation.`,
146
203
  service: serviceName,
147
204
  path: `${targetPath}#services.${serviceName}.volumes`,
148
205
  suggestion: 'Avoid mounting /dev. If hardware access is required, map only the specific device(s) needed via devices:.'
@@ -204,6 +261,19 @@ export function runRules(compose, targetPath) {
204
261
  suggestion: 'Set user: "1000:1000" (or a dedicated UID/GID) and ensure the image supports running unprivileged.'
205
262
  });
206
263
  }
264
+ // Rule: filesystem not read-only (hardening)
265
+ // Low severity because many images expect write access unless explicitly designed for read-only.
266
+ if (svc?.read_only !== true) {
267
+ findings.push({
268
+ id: 'compose.missing-read-only',
269
+ title: 'Filesystem not set to read-only',
270
+ severity: 'low',
271
+ message: `Service '${serviceName}' does not set read_only: true (container filesystem is writable by default).`,
272
+ service: serviceName,
273
+ path: `${targetPath}#services.${serviceName}.read_only`,
274
+ suggestion: 'Consider setting read_only: true + add explicit writable mounts (e.g. tmpfs:/tmp or a data volume) if the app supports it.'
275
+ });
276
+ }
207
277
  // Rule: exposed sensitive ports
208
278
  const ports = normalizePorts(svc?.ports);
209
279
  for (const p of ports) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configsentry",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Developer-first guardrails for docker-compose.yml (security + ops footguns).",
5
5
  "type": "module",
6
6
  "license": "MIT",