docker-host-audit 0.1.1__tar.gz

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.
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: docker-host-audit
3
+ Version: 0.1.1
4
+ Summary: Read-only security audit for Docker hosts: flags root containers, exposed ports, secrets in env, privileged mode and mounted docker sockets.
5
+ Author: Petr Hromádka
6
+ Author-email: Petr Hromádka <109349500+petrhromadka@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ <div align="center">
12
+
13
+ # 🛡️ container-audit
14
+
15
+ **Read-only security audit for Docker hosts.**
16
+
17
+ Point it at your containers and it reports the risky configuration — root users, exposed ports,
18
+ secrets in environment variables, privileged mode, dangerous capabilities, mounted Docker sockets,
19
+ shared host namespaces, outdated images and missing hardening — as a clean text, JSON or Markdown report.
20
+
21
+ [![CI](https://github.com/petrhromadka/container-audit/actions/workflows/ci.yml/badge.svg)](https://github.com/petrhromadka/container-audit/actions/workflows/ci.yml)
22
+ ![defensive](https://img.shields.io/badge/purpose-defensive-blue)
23
+ ![read-only](https://img.shields.io/badge/access-read--only-brightgreen)
24
+ ![python](https://img.shields.io/badge/python-3.12%2B-informational)
25
+ ![license](https://img.shields.io/badge/license-MIT-lightgrey)
26
+
27
+ [Quick start](#-quick-start) · [What it checks](#-what-it-checks) · [Usage](#-usage) · [How it works](#-how-it-works) · [Safety](#-safety)
28
+
29
+ </div>
30
+
31
+ > [!NOTE]
32
+ > **Defensive and read-only.** container-audit only runs `docker ps` and `docker inspect` (or reads
33
+ > a saved copy of that JSON). It never starts, stops, or modifies a container, and it never sends
34
+ > data anywhere.
35
+
36
+ ---
37
+
38
+ ## 🚀 Quick start
39
+
40
+ No Docker needed to try it — the repo ships a sample `docker inspect` dump:
41
+
42
+ ```bash
43
+ git clone https://github.com/petrhromadka/container-audit
44
+ cd container-audit
45
+ uv run container-audit --input tests/fixtures/sample_inspect.json
46
+ ```
47
+
48
+ You'll see eleven findings on the intentionally-insecure `bad-db` container and a clean bill of health
49
+ for the hardened `good-web` one. To scan your own host instead, just run `container-audit` — it
50
+ reads live containers via `docker ps` / `docker inspect`.
51
+
52
+ ---
53
+
54
+ ## 🔍 What it checks
55
+
56
+ Severity key: 🔴 critical · 🟠 high · 🟡 medium · 🔵 low. Each finding carries a title, a plain-language
57
+ explanation, short **redacted** evidence, and a one-line remediation.
58
+
59
+ ### Privilege &amp; escape
60
+
61
+ | | Check id | What | Why it matters |
62
+ | :-: | --- | --- | --- |
63
+ | 🔴 | `docker-socket-mount` | `/var/run/docker.sock` is mounted in | Socket access is effectively root on the host: it can start any other container. |
64
+ | 🔴 | `privileged` | Container runs `--privileged` | Disables most isolation: full device access and all capabilities. |
65
+ | 🟠 | `root-user` | Runs as root (no `USER`, or `0`/`root`) | A process escape or file-permission bug has root inside the container. |
66
+ | 🟠 | `dangerous-caps` | Caps like `SYS_ADMIN`, `NET_ADMIN`, `SYS_PTRACE` added | Common container-escape building blocks. |
67
+ | 🟠 | `sensitive-host-mount` | Host paths (`/`, `/etc`, `/proc`, `/root`, …) bind-mounted | A compromised container can read or alter host config and credentials. |
68
+
69
+ ### Namespaces &amp; exposure
70
+
71
+ | | Check id | What | Why it matters |
72
+ | :-: | --- | --- | --- |
73
+ | 🟠 | `host-pid` | Shares the host PID namespace (`--pid=host`) | It can see and signal every process on the host. |
74
+ | 🟠 | `host-network` | Shares the host network namespace (`--network=host`) | Network isolation and port mapping are bypassed. |
75
+ | 🟡 | `port-all-interfaces` | Port published on `0.0.0.0` / `::` | Reachable from every network the host is on, not just where you intend. |
76
+ | 🟡 | `host-ipc` | Shares the host IPC namespace (`--ipc=host`) | Shared memory and semaphores cross the container boundary. |
77
+ | 🔵 | `host-uts` | Shares the host UTS namespace (`--uts=host`) | It can change the host's hostname. |
78
+
79
+ ### Secrets &amp; hardening
80
+
81
+ | | Check id | What | Why it matters |
82
+ | :-: | --- | --- | --- |
83
+ | 🟠 | `secret-in-env` | Env var that looks like a secret, or a URL with embedded credentials | Secrets in env vars leak via `docker inspect`, logs and child processes. |
84
+ | 🟡 | `no-new-privileges-missing` | `no-new-privileges:true` is not set | setuid binaries inside the container can still escalate privileges. |
85
+ | 🔵 | `writable-rootfs` | Root filesystem is writable (not `read_only`) | An attacker can persist changes and tooling. |
86
+ | 🟡 | `stale-image` | Image was built more than 180 days ago | Old images usually miss security patches released since; rebuild or pull a current one. |
87
+ | 🔵 | `unpinned-image` | Image uses `:latest`, no tag, or no digest | Pulls aren't reproducible; upstream can silently change what runs. |
88
+ | 🔵 | `unlimited-memory` | No memory limit set | A single container can exhaust host RAM and take the host down. |
89
+
90
+ ---
91
+
92
+ ## 📦 Install
93
+
94
+ Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/). The `docker` CLI is only needed to audit
95
+ a live host; auditing a saved file with `--input` does not need it.
96
+
97
+ ```bash
98
+ uv tool install . # install as a tool from a checkout
99
+ uvx --from . container-audit # or run without installing
100
+ uv run container-audit # or run inside the project environment
101
+ uv run python -m ca # module form (use this if the console script is blocked)
102
+ ```
103
+
104
+ > On Windows, Smart App Control may block the generated `container-audit.exe` launcher. Run the
105
+ > tool as a module instead: `python -m ca` (or `uv run python -m ca`).
106
+
107
+ Once published to PyPI: `pipx install docker-host-audit` (then run `container-audit`), or `uvx docker-host-audit`.
108
+
109
+ ### Run with Docker
110
+
111
+ The image ships the tool (the Docker CLI is intentionally not bundled); feed it a saved inspect dump
112
+ on stdin:
113
+
114
+ ```bash
115
+ docker build -t container-audit .
116
+ docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) | docker run --rm -i container-audit --input /dev/stdin --fail-on high
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 💻 Usage
122
+
123
+ ```bash
124
+ container-audit # audit running containers
125
+ container-audit --include-stopped # include stopped ones too
126
+ container-audit --format json # machine-readable output
127
+ container-audit --format markdown > report.md # for an issue / PR / wiki
128
+
129
+ docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > sample.json # save containers + their images
130
+ container-audit --input sample.json --fail-on high # audit the dump; non-zero exit for CI
131
+ ```
132
+
133
+ | Flag | Meaning |
134
+ | --- | --- |
135
+ | `--input FILE` | Read `docker inspect` JSON from `FILE` instead of calling Docker. |
136
+ | `--format FORMAT` | Output format: `text` (default), `json` or `markdown`. |
137
+ | `--include-stopped` | Also inspect containers that are not running. |
138
+ | `--include-host` | Include the real hostname in the report (off by default, so output is safe to share). |
139
+ | `--min-severity LEVEL` | Hide findings below `LEVEL` (`info`, `low`, `medium`, `high`, `critical`). Default `info` (show all). |
140
+ | `--fail-on LEVEL` | Exit non-zero if any shown finding is at or above `LEVEL` (`none`, `low`, `medium`, `high`, `critical`). Default `none`. |
141
+ | `--version` | Print the version and exit. |
142
+
143
+ **Exit codes:** `0` clean (nothing reached `--fail-on`) · `1` a finding met `--fail-on` · `2` could
144
+ not run (Docker unavailable, or a missing/invalid `--input` file).
145
+
146
+ Image age comes from `docker inspect --type image`. If an image was deleted while its container
147
+ still runs, or the dump has no image objects, the age is unknown and `stale-image` skips that
148
+ container. Images from reproducible builds (distroless, ko, Nix) carry a fixed 1970 date and are
149
+ skipped too.
150
+
151
+ ### Coolify and other self-hosted PaaS
152
+
153
+ It works unchanged on a [Coolify](https://coolify.io) host. Expect `docker-socket-mount` on Coolify's
154
+ own `coolify-proxy` (Traefik) and `coolify-sentinel` containers: they need the socket by design. Review
155
+ everything else, especially apps you deployed from public templates, which often publish database
156
+ ports on `0.0.0.0` and keep passwords in environment variables.
157
+
158
+ ### Use in CI
159
+
160
+ ```yaml
161
+ - name: Audit containers
162
+ run: |
163
+ docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > containers.json
164
+ uvx --from git+https://github.com/petrhromadka/container-audit \
165
+ container-audit --input containers.json --fail-on high
166
+ ```
167
+
168
+ ---
169
+
170
+ ## ⚙️ How it works
171
+
172
+ ```text
173
+ docker ps / docker inspect -> parse into Container objects -> run checks -> render report
174
+ (or --input file) (pure functions) text / json / markdown
175
+ ```
176
+
177
+ 1. **Collect** — list containers with `docker ps` and read their config with `docker inspect` (or
178
+ parse a saved copy via `--input`).
179
+ 2. **Check** — each check is a pure function over the parsed containers that yields findings. Checks
180
+ never talk to Docker and never change anything.
181
+ 3. **Report** — findings are sorted by severity and rendered as text, JSON or Markdown; with
182
+ `--fail-on`, the exit code reflects the worst severity found.
183
+
184
+ <details>
185
+ <summary>📋 Example report (from the bundled sample fixture)</summary>
186
+
187
+ ```text
188
+ container-audit report
189
+ Host: (redacted)
190
+ Scanned: 2 container(s)
191
+ Timestamp: 2026-11-05T19:32:19+00:00
192
+ Findings: 11 (critical=2, high=3, medium=4, low=2, info=0)
193
+
194
+ [CRITICAL] docker-socket-mount bad-db - Access to the Docker socket grants control of the Docker daemon, and therefore of the host.
195
+ evidence: /var/run/docker.sock -> /var/run/docker.sock (rw)
196
+ remediation: Remove the socket mount or use a restricted docker-socket-proxy.
197
+ [CRITICAL] privileged bad-db - Privileged mode grants all capabilities and host device access, so a compromise is effectively a host compromise.
198
+ evidence: HostConfig.Privileged=true
199
+ remediation: Remove `privileged: true`; add only the capabilities needed.
200
+ [HIGH] dangerous-caps bad-db - These capabilities allow administrative or host-affecting operations and weaken container isolation.
201
+ evidence: cap_add: SYS_ADMIN
202
+ remediation: Remove these from `cap_add`; grant only what is required.
203
+ [HIGH] root-user bad-db - The main process runs as UID 0, widening the impact of an escape.
204
+ evidence: Config.User=''
205
+ remediation: Set a non-root USER in the image or `user:` in compose.
206
+ [HIGH] secret-in-env bad-db - Environment variable DB_PASSWORD looks like a secret and has a literal value; it is visible via docker inspect and to anyone who can read the container configuration.
207
+ evidence: DB_PASSWORD=***
208
+ remediation: Use Docker secrets or an env file kept outside the image.
209
+ [MEDIUM] host-ipc bad-db - The container can access host shared memory and IPC objects, weakening isolation.
210
+ evidence: HostConfig.IpcMode=host
211
+ remediation: Remove `ipc: host`; do not share the host IPC namespace.
212
+ [MEDIUM] no-new-privileges-missing bad-db - Without no-new-privileges, setuid binaries inside the container can escalate privileges.
213
+ evidence: HostConfig.SecurityOpt lacks no-new-privileges
214
+ remediation: Add `security_opt: ["no-new-privileges:true"]`.
215
+ [MEDIUM] port-all-interfaces bad-db - The port is reachable from every network the host is attached to.
216
+ evidence: 0.0.0.0:5432 -> 5432/tcp
217
+ remediation: Bind to 127.0.0.1 (e.g. 127.0.0.1:<port>:<port>) unless public.
218
+ [MEDIUM] stale-image bad-db - The image is 1390 days old, so it likely lacks security patches released since it was built.
219
+ evidence: image built 2023-01-15 (1390 days ago)
220
+ remediation: Pull or rebuild the image on a current base and recreate the container; automate updates (e.g. Renovate, Dependabot).
221
+ [LOW] unlimited-memory bad-db - With no memory limit, one container can exhaust the host's RAM.
222
+ evidence: HostConfig.Memory=0
223
+ remediation: Set a memory limit (`mem_limit` in compose).
224
+ [LOW] writable-rootfs bad-db - A writable root filesystem lets an attacker persist changes.
225
+ evidence: HostConfig.ReadonlyRootfs=false
226
+ remediation: Set `read_only: true` and use tmpfs or volumes for writable paths.
227
+ ```
228
+
229
+ The second container, `good-web`, is hardened (non-root, read-only rootfs, memory limit,
230
+ `no-new-privileges`, port bound to `127.0.0.1`) and produces no findings.
231
+
232
+ </details>
233
+
234
+ ---
235
+
236
+ ## 🔒 Safety
237
+
238
+ - **Read-only.** The only Docker commands used are `docker ps` and `docker inspect`. Nothing is
239
+ started, stopped, restarted, or reconfigured.
240
+ - **Secret values are never printed.** For env vars that look like secrets, only the variable
241
+ **name** is reported, with the value masked to `***` — no characters of the value appear in any
242
+ output format.
243
+ - **Hostname hidden by default.** Reports show `(redacted)` as the host unless you pass
244
+ `--include-host`.
245
+ - **Output is sanitized.** Control and terminal-escape characters from container metadata are
246
+ stripped, so a report is safe to print in a terminal or paste into Markdown.
247
+ - **No network use, no runtime dependencies.** The tool never transmits data anywhere.
248
+ - Reports can still reveal your container names, image names, ports and mount paths — review one
249
+ before sharing it publicly.
250
+
251
+ ---
252
+
253
+ ## 🧰 Development
254
+
255
+ ```bash
256
+ uv sync # create the environment
257
+ uv run ruff check . # lint
258
+ uv run ruff format --check . # formatting
259
+ uv run ty check src # type check
260
+ uv run pytest # tests
261
+ ```
262
+
263
+ Pre-commit hooks (ruff, gitleaks, whitespace fixes) live in `.pre-commit-config.yaml`; install them
264
+ with [`prek`](https://github.com/j178/prek) or `pre-commit`. CI runs the same checks plus a gitleaks
265
+ scan over the full history.
266
+
267
+ To add a check, write a function that takes `list[Container]` and yields `Finding` objects, then
268
+ register it in the `CHECKS` list of the relevant module under `src/ca/checks/`.
269
+
270
+ ---
271
+
272
+ ## 📄 License
273
+
274
+ [MIT](LICENSE) © 2026 Petr Hromádka
@@ -0,0 +1,264 @@
1
+ <div align="center">
2
+
3
+ # 🛡️ container-audit
4
+
5
+ **Read-only security audit for Docker hosts.**
6
+
7
+ Point it at your containers and it reports the risky configuration — root users, exposed ports,
8
+ secrets in environment variables, privileged mode, dangerous capabilities, mounted Docker sockets,
9
+ shared host namespaces, outdated images and missing hardening — as a clean text, JSON or Markdown report.
10
+
11
+ [![CI](https://github.com/petrhromadka/container-audit/actions/workflows/ci.yml/badge.svg)](https://github.com/petrhromadka/container-audit/actions/workflows/ci.yml)
12
+ ![defensive](https://img.shields.io/badge/purpose-defensive-blue)
13
+ ![read-only](https://img.shields.io/badge/access-read--only-brightgreen)
14
+ ![python](https://img.shields.io/badge/python-3.12%2B-informational)
15
+ ![license](https://img.shields.io/badge/license-MIT-lightgrey)
16
+
17
+ [Quick start](#-quick-start) · [What it checks](#-what-it-checks) · [Usage](#-usage) · [How it works](#-how-it-works) · [Safety](#-safety)
18
+
19
+ </div>
20
+
21
+ > [!NOTE]
22
+ > **Defensive and read-only.** container-audit only runs `docker ps` and `docker inspect` (or reads
23
+ > a saved copy of that JSON). It never starts, stops, or modifies a container, and it never sends
24
+ > data anywhere.
25
+
26
+ ---
27
+
28
+ ## 🚀 Quick start
29
+
30
+ No Docker needed to try it — the repo ships a sample `docker inspect` dump:
31
+
32
+ ```bash
33
+ git clone https://github.com/petrhromadka/container-audit
34
+ cd container-audit
35
+ uv run container-audit --input tests/fixtures/sample_inspect.json
36
+ ```
37
+
38
+ You'll see eleven findings on the intentionally-insecure `bad-db` container and a clean bill of health
39
+ for the hardened `good-web` one. To scan your own host instead, just run `container-audit` — it
40
+ reads live containers via `docker ps` / `docker inspect`.
41
+
42
+ ---
43
+
44
+ ## 🔍 What it checks
45
+
46
+ Severity key: 🔴 critical · 🟠 high · 🟡 medium · 🔵 low. Each finding carries a title, a plain-language
47
+ explanation, short **redacted** evidence, and a one-line remediation.
48
+
49
+ ### Privilege &amp; escape
50
+
51
+ | | Check id | What | Why it matters |
52
+ | :-: | --- | --- | --- |
53
+ | 🔴 | `docker-socket-mount` | `/var/run/docker.sock` is mounted in | Socket access is effectively root on the host: it can start any other container. |
54
+ | 🔴 | `privileged` | Container runs `--privileged` | Disables most isolation: full device access and all capabilities. |
55
+ | 🟠 | `root-user` | Runs as root (no `USER`, or `0`/`root`) | A process escape or file-permission bug has root inside the container. |
56
+ | 🟠 | `dangerous-caps` | Caps like `SYS_ADMIN`, `NET_ADMIN`, `SYS_PTRACE` added | Common container-escape building blocks. |
57
+ | 🟠 | `sensitive-host-mount` | Host paths (`/`, `/etc`, `/proc`, `/root`, …) bind-mounted | A compromised container can read or alter host config and credentials. |
58
+
59
+ ### Namespaces &amp; exposure
60
+
61
+ | | Check id | What | Why it matters |
62
+ | :-: | --- | --- | --- |
63
+ | 🟠 | `host-pid` | Shares the host PID namespace (`--pid=host`) | It can see and signal every process on the host. |
64
+ | 🟠 | `host-network` | Shares the host network namespace (`--network=host`) | Network isolation and port mapping are bypassed. |
65
+ | 🟡 | `port-all-interfaces` | Port published on `0.0.0.0` / `::` | Reachable from every network the host is on, not just where you intend. |
66
+ | 🟡 | `host-ipc` | Shares the host IPC namespace (`--ipc=host`) | Shared memory and semaphores cross the container boundary. |
67
+ | 🔵 | `host-uts` | Shares the host UTS namespace (`--uts=host`) | It can change the host's hostname. |
68
+
69
+ ### Secrets &amp; hardening
70
+
71
+ | | Check id | What | Why it matters |
72
+ | :-: | --- | --- | --- |
73
+ | 🟠 | `secret-in-env` | Env var that looks like a secret, or a URL with embedded credentials | Secrets in env vars leak via `docker inspect`, logs and child processes. |
74
+ | 🟡 | `no-new-privileges-missing` | `no-new-privileges:true` is not set | setuid binaries inside the container can still escalate privileges. |
75
+ | 🔵 | `writable-rootfs` | Root filesystem is writable (not `read_only`) | An attacker can persist changes and tooling. |
76
+ | 🟡 | `stale-image` | Image was built more than 180 days ago | Old images usually miss security patches released since; rebuild or pull a current one. |
77
+ | 🔵 | `unpinned-image` | Image uses `:latest`, no tag, or no digest | Pulls aren't reproducible; upstream can silently change what runs. |
78
+ | 🔵 | `unlimited-memory` | No memory limit set | A single container can exhaust host RAM and take the host down. |
79
+
80
+ ---
81
+
82
+ ## 📦 Install
83
+
84
+ Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/). The `docker` CLI is only needed to audit
85
+ a live host; auditing a saved file with `--input` does not need it.
86
+
87
+ ```bash
88
+ uv tool install . # install as a tool from a checkout
89
+ uvx --from . container-audit # or run without installing
90
+ uv run container-audit # or run inside the project environment
91
+ uv run python -m ca # module form (use this if the console script is blocked)
92
+ ```
93
+
94
+ > On Windows, Smart App Control may block the generated `container-audit.exe` launcher. Run the
95
+ > tool as a module instead: `python -m ca` (or `uv run python -m ca`).
96
+
97
+ Once published to PyPI: `pipx install docker-host-audit` (then run `container-audit`), or `uvx docker-host-audit`.
98
+
99
+ ### Run with Docker
100
+
101
+ The image ships the tool (the Docker CLI is intentionally not bundled); feed it a saved inspect dump
102
+ on stdin:
103
+
104
+ ```bash
105
+ docker build -t container-audit .
106
+ docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) | docker run --rm -i container-audit --input /dev/stdin --fail-on high
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 💻 Usage
112
+
113
+ ```bash
114
+ container-audit # audit running containers
115
+ container-audit --include-stopped # include stopped ones too
116
+ container-audit --format json # machine-readable output
117
+ container-audit --format markdown > report.md # for an issue / PR / wiki
118
+
119
+ docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > sample.json # save containers + their images
120
+ container-audit --input sample.json --fail-on high # audit the dump; non-zero exit for CI
121
+ ```
122
+
123
+ | Flag | Meaning |
124
+ | --- | --- |
125
+ | `--input FILE` | Read `docker inspect` JSON from `FILE` instead of calling Docker. |
126
+ | `--format FORMAT` | Output format: `text` (default), `json` or `markdown`. |
127
+ | `--include-stopped` | Also inspect containers that are not running. |
128
+ | `--include-host` | Include the real hostname in the report (off by default, so output is safe to share). |
129
+ | `--min-severity LEVEL` | Hide findings below `LEVEL` (`info`, `low`, `medium`, `high`, `critical`). Default `info` (show all). |
130
+ | `--fail-on LEVEL` | Exit non-zero if any shown finding is at or above `LEVEL` (`none`, `low`, `medium`, `high`, `critical`). Default `none`. |
131
+ | `--version` | Print the version and exit. |
132
+
133
+ **Exit codes:** `0` clean (nothing reached `--fail-on`) · `1` a finding met `--fail-on` · `2` could
134
+ not run (Docker unavailable, or a missing/invalid `--input` file).
135
+
136
+ Image age comes from `docker inspect --type image`. If an image was deleted while its container
137
+ still runs, or the dump has no image objects, the age is unknown and `stale-image` skips that
138
+ container. Images from reproducible builds (distroless, ko, Nix) carry a fixed 1970 date and are
139
+ skipped too.
140
+
141
+ ### Coolify and other self-hosted PaaS
142
+
143
+ It works unchanged on a [Coolify](https://coolify.io) host. Expect `docker-socket-mount` on Coolify's
144
+ own `coolify-proxy` (Traefik) and `coolify-sentinel` containers: they need the socket by design. Review
145
+ everything else, especially apps you deployed from public templates, which often publish database
146
+ ports on `0.0.0.0` and keep passwords in environment variables.
147
+
148
+ ### Use in CI
149
+
150
+ ```yaml
151
+ - name: Audit containers
152
+ run: |
153
+ docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > containers.json
154
+ uvx --from git+https://github.com/petrhromadka/container-audit \
155
+ container-audit --input containers.json --fail-on high
156
+ ```
157
+
158
+ ---
159
+
160
+ ## ⚙️ How it works
161
+
162
+ ```text
163
+ docker ps / docker inspect -> parse into Container objects -> run checks -> render report
164
+ (or --input file) (pure functions) text / json / markdown
165
+ ```
166
+
167
+ 1. **Collect** — list containers with `docker ps` and read their config with `docker inspect` (or
168
+ parse a saved copy via `--input`).
169
+ 2. **Check** — each check is a pure function over the parsed containers that yields findings. Checks
170
+ never talk to Docker and never change anything.
171
+ 3. **Report** — findings are sorted by severity and rendered as text, JSON or Markdown; with
172
+ `--fail-on`, the exit code reflects the worst severity found.
173
+
174
+ <details>
175
+ <summary>📋 Example report (from the bundled sample fixture)</summary>
176
+
177
+ ```text
178
+ container-audit report
179
+ Host: (redacted)
180
+ Scanned: 2 container(s)
181
+ Timestamp: 2026-11-05T19:32:19+00:00
182
+ Findings: 11 (critical=2, high=3, medium=4, low=2, info=0)
183
+
184
+ [CRITICAL] docker-socket-mount bad-db - Access to the Docker socket grants control of the Docker daemon, and therefore of the host.
185
+ evidence: /var/run/docker.sock -> /var/run/docker.sock (rw)
186
+ remediation: Remove the socket mount or use a restricted docker-socket-proxy.
187
+ [CRITICAL] privileged bad-db - Privileged mode grants all capabilities and host device access, so a compromise is effectively a host compromise.
188
+ evidence: HostConfig.Privileged=true
189
+ remediation: Remove `privileged: true`; add only the capabilities needed.
190
+ [HIGH] dangerous-caps bad-db - These capabilities allow administrative or host-affecting operations and weaken container isolation.
191
+ evidence: cap_add: SYS_ADMIN
192
+ remediation: Remove these from `cap_add`; grant only what is required.
193
+ [HIGH] root-user bad-db - The main process runs as UID 0, widening the impact of an escape.
194
+ evidence: Config.User=''
195
+ remediation: Set a non-root USER in the image or `user:` in compose.
196
+ [HIGH] secret-in-env bad-db - Environment variable DB_PASSWORD looks like a secret and has a literal value; it is visible via docker inspect and to anyone who can read the container configuration.
197
+ evidence: DB_PASSWORD=***
198
+ remediation: Use Docker secrets or an env file kept outside the image.
199
+ [MEDIUM] host-ipc bad-db - The container can access host shared memory and IPC objects, weakening isolation.
200
+ evidence: HostConfig.IpcMode=host
201
+ remediation: Remove `ipc: host`; do not share the host IPC namespace.
202
+ [MEDIUM] no-new-privileges-missing bad-db - Without no-new-privileges, setuid binaries inside the container can escalate privileges.
203
+ evidence: HostConfig.SecurityOpt lacks no-new-privileges
204
+ remediation: Add `security_opt: ["no-new-privileges:true"]`.
205
+ [MEDIUM] port-all-interfaces bad-db - The port is reachable from every network the host is attached to.
206
+ evidence: 0.0.0.0:5432 -> 5432/tcp
207
+ remediation: Bind to 127.0.0.1 (e.g. 127.0.0.1:<port>:<port>) unless public.
208
+ [MEDIUM] stale-image bad-db - The image is 1390 days old, so it likely lacks security patches released since it was built.
209
+ evidence: image built 2023-01-15 (1390 days ago)
210
+ remediation: Pull or rebuild the image on a current base and recreate the container; automate updates (e.g. Renovate, Dependabot).
211
+ [LOW] unlimited-memory bad-db - With no memory limit, one container can exhaust the host's RAM.
212
+ evidence: HostConfig.Memory=0
213
+ remediation: Set a memory limit (`mem_limit` in compose).
214
+ [LOW] writable-rootfs bad-db - A writable root filesystem lets an attacker persist changes.
215
+ evidence: HostConfig.ReadonlyRootfs=false
216
+ remediation: Set `read_only: true` and use tmpfs or volumes for writable paths.
217
+ ```
218
+
219
+ The second container, `good-web`, is hardened (non-root, read-only rootfs, memory limit,
220
+ `no-new-privileges`, port bound to `127.0.0.1`) and produces no findings.
221
+
222
+ </details>
223
+
224
+ ---
225
+
226
+ ## 🔒 Safety
227
+
228
+ - **Read-only.** The only Docker commands used are `docker ps` and `docker inspect`. Nothing is
229
+ started, stopped, restarted, or reconfigured.
230
+ - **Secret values are never printed.** For env vars that look like secrets, only the variable
231
+ **name** is reported, with the value masked to `***` — no characters of the value appear in any
232
+ output format.
233
+ - **Hostname hidden by default.** Reports show `(redacted)` as the host unless you pass
234
+ `--include-host`.
235
+ - **Output is sanitized.** Control and terminal-escape characters from container metadata are
236
+ stripped, so a report is safe to print in a terminal or paste into Markdown.
237
+ - **No network use, no runtime dependencies.** The tool never transmits data anywhere.
238
+ - Reports can still reveal your container names, image names, ports and mount paths — review one
239
+ before sharing it publicly.
240
+
241
+ ---
242
+
243
+ ## 🧰 Development
244
+
245
+ ```bash
246
+ uv sync # create the environment
247
+ uv run ruff check . # lint
248
+ uv run ruff format --check . # formatting
249
+ uv run ty check src # type check
250
+ uv run pytest # tests
251
+ ```
252
+
253
+ Pre-commit hooks (ruff, gitleaks, whitespace fixes) live in `.pre-commit-config.yaml`; install them
254
+ with [`prek`](https://github.com/j178/prek) or `pre-commit`. CI runs the same checks plus a gitleaks
255
+ scan over the full history.
256
+
257
+ To add a check, write a function that takes `list[Container]` and yields `Finding` objects, then
258
+ register it in the `CHECKS` list of the relevant module under `src/ca/checks/`.
259
+
260
+ ---
261
+
262
+ ## 📄 License
263
+
264
+ [MIT](LICENSE) © 2026 Petr Hromádka
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "docker-host-audit"
3
+ version = "0.1.1"
4
+ description = "Read-only security audit for Docker hosts: flags root containers, exposed ports, secrets in env, privileged mode and mounted docker sockets."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ dependencies = []
9
+
10
+ [[project.authors]]
11
+ name = "Petr Hromádka"
12
+ email = "109349500+petrhromadka@users.noreply.github.com"
13
+
14
+ [project.scripts]
15
+ container-audit = "ca.cli:main"
16
+ docker-host-audit = "ca.cli:main"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.12.23,<0.13.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [tool.uv.build-backend]
23
+ module-name = "ca"
24
+
25
+ [tool.ruff]
26
+ line-length = 100
27
+ target-version = "py312"
28
+
29
+ [tool.ruff.lint]
30
+ select = [
31
+ "E",
32
+ "F",
33
+ "W",
34
+ "I",
35
+ "B",
36
+ "UP",
37
+ "SIM",
38
+ "RUF",
39
+ "S",
40
+ "PTH",
41
+ ]
42
+ ignore = [
43
+ "S603",
44
+ "S607",
45
+ ]
46
+
47
+ [tool.ruff.lint.per-file-ignores]
48
+ "tests/**" = [
49
+ "S101",
50
+ "S105",
51
+ "S106",
52
+ ]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ addopts = "-q"
57
+
58
+ [dependency-groups]
59
+ dev = [
60
+ "pytest>=9.2.0",
61
+ "ruff>=0.17.4",
62
+ "ty>=0.0.98",
63
+ ]
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "docker-host-audit"
3
+ version = "0.1.1"
4
+ description = "Read-only security audit for Docker hosts: flags root containers, exposed ports, secrets in env, privileged mode and mounted docker sockets."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [{ name = "Petr Hromádka", email = "109349500+petrhromadka@users.noreply.github.com" }]
8
+ requires-python = ">=3.12"
9
+ dependencies = []
10
+
11
+ [project.scripts]
12
+ container-audit = "ca.cli:main"
13
+ docker-host-audit = "ca.cli:main"
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.12.23,<0.13.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [tool.uv.build-backend]
20
+ module-name = "ca"
21
+
22
+ [tool.ruff]
23
+ line-length = 100
24
+ target-version = "py312"
25
+
26
+ [tool.ruff.lint]
27
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "S", "PTH"]
28
+ ignore = ["S603", "S607"]
29
+
30
+ [tool.ruff.lint.per-file-ignores]
31
+ "tests/**" = ["S101", "S105", "S106"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+ addopts = "-q"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pytest>=9.2.0",
40
+ "ruff>=0.17.4",
41
+ "ty>=0.0.98",
42
+ ]
@@ -0,0 +1,3 @@
1
+ """container-audit: read-only security audit for Docker hosts."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,6 @@
1
+ """Allow `python -m ca`."""
2
+
3
+ from ca.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,12 @@
1
+ """Check registry. Each submodule exposes `CHECKS: list[CheckFn]`; we aggregate them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ca.models import CheckFn
6
+
7
+
8
+ def get_checks() -> list[CheckFn]:
9
+ """Every registered check, across all submodules."""
10
+ from ca.checks import exposure, hardening, images, privilege
11
+
12
+ return [*privilege.CHECKS, *exposure.CHECKS, *hardening.CHECKS, *images.CHECKS]