Metadata-Version: 2.4
Name: compose-harden
Version: 0.2.0
Summary: Auto-harden and audit any docker-compose.yml: read-only `scan` with a pass/fail score (JSON/SARIF for CI), and `fix` to apply cap_drop, no-new-privileges, read_only, and tmpfs with a human-readable audit report.
Author: joshua-michael
License: MIT
Project-URL: Homepage, https://github.com/joshua-michael/compose-harden
Project-URL: Issues, https://github.com/joshua-michael/compose-harden/issues
Keywords: docker,docker-compose,security,hardening,container-security,audit,devsecops,sarif,ci-cd
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: PyYAML>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# compose-harden

[![CI](https://github.com/joshua-michael/compose-harden/actions/workflows/ci.yml/badge.svg)](https://github.com/joshua-michael/compose-harden/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Scan any `docker-compose.yml` for container-hardening gaps, get a score,
then auto-fix what's safe to auto-fix — with a document explaining every
change, and every issue it noticed but refused to guess at.

```bash
$ compose-harden scan docker-compose.yml
```

```
Service: web
  ✖ cap_drop: ALL           -- no `cap_drop: [ALL]`
  ✖ no-new-privileges       -- missing `no-new-privileges` in security_opt
  ✖ read_only root filesystem -- root filesystem is writable
  ✔ not running privileged
  ✔ image version pinned
  ✖ non-root user set       -- no non-root `user:` configured
  ✔ not using host network
  ✔ seccomp not disabled

Score: 50/100  (8/16 checks passed)
```

```bash
$ compose-harden fix docker-compose.yml --dry-run
```

```diff
   depends_on:
     - db
+    cap_drop:
+      - ALL
+    security_opt:
+      - no-new-privileges:true
+    read_only: true
+    tmpfs:
+      - /tmp
```

...plus an `AUDIT.md` that explains *why* each line was added, in plain
language, service by service.

**Don't want to run it just to see what it does?** Real, generated
examples are checked into this repo:

| Input | Output |
|---|---|
| [`examples/docker-compose.yml`](examples/docker-compose.yml) | [`scan-output.txt`](examples/sample-output/scan-output.txt) · [`scan-output.json`](examples/sample-output/scan-output.json) · [`scan-output.sarif`](examples/sample-output/scan-output.sarif) |
| same file | [`docker-compose.hardened.yml`](examples/sample-output/docker-compose.hardened.yml) · [`AUDIT.md`](examples/sample-output/AUDIT.md) |

## Why this exists

There are great tools for **scanning** a Docker setup for problems
(`docker-bench-security`, Trivy, CrowdSec, Docker's own Hardened Images at
the base-image level). There's nothing that closes the loop end to end:

1. read an existing, real-world `docker-compose.yml`
2. **score** it against known hardening controls, in a format a CI
   pipeline can gate on
3. **apply** the ones that are safe to auto-fix, without breaking the
   file's comments or formatting
4. **explain**, in a document a non-security person can read, what changed
   and why — and just as importantly, what it *refused* to touch
   automatically and why

`compose-harden` does exactly that.

## What it checks

| Check | Auto-fixed by `fix`? | What it means |
|---|---|---|
| `cap_drop: [ALL]` | Yes | Drops every Linux capability, shrinking what a compromised process can do |
| `security_opt: [no-new-privileges:true]` | Yes | Blocks privilege escalation via setuid binaries |
| `read_only: true` | Yes | Root filesystem can't be written to at runtime |
| `tmpfs` scratch space | Yes (when `read_only` is active) | Keeps the app working under `read_only` |
| Not `privileged: true` | No — flagged only | Privileged mode disables nearly all isolation at once |
| Image version pinned | No — flagged only | `:latest`/no tag means the image can silently change under you |
| Non-root `user:` set | No — flagged only | The correct UID is image-specific; a wrong guess breaks startup |
| Not `network_mode: host` | No — flagged only | Host networking removes the container's network namespace entirely |
| seccomp not `unconfined` | No — flagged only | Only fires if seccomp was explicitly turned off; Docker's default profile is fine and isn't flagged |

Full rationale for each is in [`compose_harden/rules.py`](compose_harden/rules.py)
and gets reproduced in the generated `AUDIT.md` for the exact services it
touches. The four auto-fixable ones need no service-specific knowledge to
apply safely; the rest depend on things only the operator knows (why
privileged mode is on, what UID an image expects), so they're surfaced,
never guessed at.

Deliberately **not** in scope: healthchecks, restart policies, resource
limits. Those are operability/reliability concerns, not attack-surface
ones — folding them into a "security score" would make the score mean two
different things at once.

## CI/CD integration

`scan` is built to gate a pipeline:

```bash
compose-harden scan docker-compose.yml --min-score 80   # exits 1 if score < 80
compose-harden scan docker-compose.yml --json           # machine-readable
compose-harden scan docker-compose.yml --sarif out.sarif  # for GitHub Code Scanning
```

A ready-to-copy workflow that scans on every PR and uploads findings to
GitHub's Security tab is at
[`examples/github-actions/compose-harden.yml`](examples/github-actions/compose-harden.yml).

## Design principle: never re-serialize your YAML

Most tools that "fix" a YAML file parse it into a data structure, edit
the structure, and dump it back out — which silently strips comments,
reorders keys, and changes quoting style. `compose-harden fix` never does
this. It reads your file as plain text, decides what to change using a
read-only parse, and then makes **surgical line insertions** at the
correct indentation. Everything you didn't ask it to touch comes out
byte-for-byte identical.

## Install

```bash
pip install compose-harden   # once published
# or, from source:
git clone https://github.com/joshua-michael/compose-harden
cd compose-harden
pip install -e .
```

## Usage

```bash
# Read-only check + score, exit 1 if not fully clean -- safe to run anywhere
compose-harden scan docker-compose.yml

# Same, but gate CI at a lower bar while you migrate an existing project
compose-harden scan docker-compose.yml --min-score 80

# Show what fix would change + full audit report, write nothing
compose-harden fix docker-compose.yml --dry-run

# Write docker-compose.hardened.yml + AUDIT.md next to it
compose-harden fix docker-compose.yml

# Overwrite in place (keeps a .bak backup automatically)
compose-harden fix docker-compose.yml --in-place

# Skip a check/rule you don't want applied or scored
compose-harden scan docker-compose.yml --skip host_network
compose-harden fix docker-compose.yml --skip read_only --skip tmpfs

# Add extra writable paths some images need alongside read_only
compose-harden fix docker-compose.yml --extra-tmpfs /var/cache/nginx

# Custom output locations
compose-harden fix docker-compose.yml -o hardened.yml --audit-output SECURITY-CHANGES.md
```

Run `compose-harden scan --help` or `compose-harden fix --help` for the full flag list.

## How it decides what's safe to touch

- If a control is **already present and correct** → skipped, marked
  "already present" in the audit (the tool is idempotent — running it
  twice is a no-op the second time).
- If a control is **present but customized** (e.g. you already listed
  specific capabilities in `cap_drop` instead of `ALL`) → left untouched
  and flagged as a conflict for manual review, instead of overwriting your
  deliberate choice.
- If a control is **absent** → added, using the same indentation style as
  the rest of your file.
- `tmpfs` is only added when `read_only` is actually active for that
  service — there's no point adding writable scratch space you don't need.

## Limitations

- `fix` only edits `cap_drop`, `security_opt` (no-new-privileges),
  `read_only`, and `tmpfs`. It does not touch networking, port bindings,
  secrets, or resource limits — that's a deliberate scope boundary, not an
  oversight.
- Assumes conventional 2-space YAML indentation and a top-level
  `services:` map (the vast majority of real-world compose files). YAML
  anchors/aliases and `extends:` are not specially handled yet.
- This is a fast first pass, not a substitute for an actual security
  review or for Docker's own Hardened Images at the base-image layer —
  use both.

## Development

```bash
pip install -e ".[dev]"
pytest
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for how the rule/check engine is
structured before adding a new one.

## License

MIT — see [LICENSE](LICENSE).
