Gate an image build on a CIS scan and a deviation register
Offline tests: Python 3.11+, standard library (PyYAML adds two register-file tests).
Live scan: Docker with Linux containers and internet access to build the scanner image and
pull the images you scan.

Reference solution: not included here, so the exercise does not hand you the answer.
Attempt the repair first. Then download image-benchmark-gate-solution.zip from the project
card on https://softwaresecurity.com/labs/#project-image-benchmark-gate and extract it into
this folder; it adds reference.py without changing your files, and the reference commands
below need it.

Goal
A platform team ships a base image built on ubuntu:24.04 and holds it to the CIS Ubuntu
Linux 24.04 LTS Benchmark, Level 1 - Server, as the SCAP Security Guide (SSG) implements it.
Some rules fail for reasons the team accepts for now. Those go in deviations.yaml with the
control, the reason, a compensating control, an owner, a different approver, an approval
date and an expiry. The gate in exercise.py reads the scan and the register and must fail
the build when:
- a rule failed, errored or came back unknown and has no register entry;
- the entry for a failing rule has expired;
- the scan used another profile, benchmark or SSG content version than the register names
  (a new content version adds and changes rules, so the register needs a fresh review);
- the scan evaluated no rules at all.
It also reports rules that came back notchecked (review those by hand) and entries for rules
that no longer fail (remove them).

Files
- scan-ubuntu2404-cis-l1.xml: the XCCDF 1.2 TestResult of a real scan, made with scan.sh
  below on 11 October 2026: ubuntu:24.04@sha256:534baea6a22c03a63003dbc8dbe78fe34bc0d7e595d9a9dc9834884ff530eb55,
  OpenSCAP 1.4.4, SSG 0.1.82, profile xccdf_org.ssgproject.content_profile_cis_level1_server
  (SSG aligns it to CIS Ubuntu Linux 24.04 LTS Benchmark v1.0.0). Result: 128 pass, 8 fail,
  2 notchecked, 270 notapplicable, 240 notselected. The full ARF report was 16.6 MB
  (SHA-256 1b3561d9e290ede3be52262633edd01365900743d26d9688fa45e142661545aa) because it
  embeds the whole SSG data stream; extract_testresult.py copied out the TestResult element
  with every child, attribute and value unchanged.
- deviations.yaml: four entries; the other four failures are fixed in example-image.Dockerfile.
- scan.sh: exports an image's filesystem and scans it in the scanner container.
- Dockerfile: the scanner image (Fedora 44 pinned by digest, openscap-scanner 1.4.4,
  scap-security-guide 0.1.82, PyYAML).

1. See the gaps offline [OFFLINE: Python only]
   python3 -m unittest -v test_gate.py
   Expected starter: 8 failures. It waives errors and unknowns, ignores expiry, accepts a
   scan of any profile or content version and matches register entries by substring, so an
   entry for "accounts_umask" waives every umask rule.
   Reference: EXERCISE_MODULE=reference python3 -m unittest -v test_gate.py

2. Repair exercise.py and run step 1 again.

3. Build the scanner and scan two images [LIVE: Docker and internet access]
   docker build -t image-scanner .
   sh scan.sh ubuntu:24.04@sha256:534baea6a22c03a63003dbc8dbe78fe34bc0d7e595d9a9dc9834884ff530eb55 ubuntu2404 cis_level1_server out-base
   docker build -t example-image -f example-image.Dockerfile .
   sh scan.sh "$(docker image inspect --format '{{.Id}}' example-image)" ubuntu2404 cis_level1_server out-hardened
   Each scan takes seconds. The scanner container has no network, and nothing from the
   host is mounted into it: the filesystem goes in on stdin and the results come out on
   stdout. oscap exits 2 when any rule fails, errors or is unknown; scan.sh treats that as
   a finished scan and leaves the judgement to the gate.

4. Gate both scans in the scanner image, which has PyYAML [LIVE: Docker]
   docker run --rm --network=none -v "$PWD:/w:ro" -w /w image-scanner python3 reference.py out-base/arf.xml deviations.yaml
   docker run --rm --network=none -v "$PWD:/w:ro" -w /w image-scanner python3 reference.py out-hardened/arf.xml deviations.yaml
   The same container also runs all 12 tests, including the two that read deviations.yaml:
   docker run --rm --network=none -v "$PWD:/w:ro" -w /w -e EXERCISE_MODULE=reference image-scanner python3 -m unittest -v test_gate.py

Execution status
Last run on 11 October 2026 with Docker 29.4.3 (Docker Desktop, Apple silicon). The base
image: 8 failures, 4 waived by the register, 4 unregistered (the Bash and /etc/profile
umask, /var/log permissions and the default "ubuntu" user's init files), gate FAIL. The
example image fixes those four: 132 pass, 4 waived, gate PASS. Both runs reported the same
two notchecked rules (no world-writable files, no files without a group), for which SSG
0.1.82 has no check on Ubuntu 24.04. Two of the waived rules check root's PATH by reading
the environment of running processes, which an image on disk does not have; that is why
they are deviations with a check of the image's configured PATH, not fixes.

Limits
SSG profiles trail the benchmarks they implement: in 0.1.82 the Ubuntu 24.04 profile follows
CIS v1.0.0, while CIS has published v2.0.0. Treat the SSG profile as the benchmark you
actually test against and say so in the register. CIS Benchmark PDFs are free for
non-commercial use; check CIS's terms before you redistribute benchmark text. A scan of an
image on disk cannot check running services, kernel parameters or mounts: scan the running
host or node too, and read the notapplicable list before you call an image compliant.

Dev container
.devcontainer/devcontainer.json opens this folder in VS Code Dev Containers or GitHub
Codespaces using the Dev Containers Python 3.13 image, pinned by digest, for the offline
steps. The scans need Docker on the host, so run them outside the container.
