Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,173 @@ advisory, timestamp, and run number, in the same format as `trace --debug`:
uv run python -m code_audit eval --debug
```

## Dockerizing an advisory's repository

```sh
uv run python -m code_audit dockerize GHSA-jfh8-c2jp-5v3q
```

When the advisory's repository looks like a web application and has no working
Dockerfile or compose file, this generates one, verifies that it actually builds and
runs, and writes it to `./output/<ghsa_id>/`. It is a separate, explicitly opt-in
pipeline: it never runs as part of `trace`, and it requires `git` and Docker on the
machine running it, since it clones and executes real code from the repository being
audited.

`dockerize` is meant to run once an advisory's commits are already known, not to
re-investigate commit provenance itself: a deterministic-only pass over the advisory's
own references can miss cases the full trace agent resolves. For example,
GHSA-v98v-ff95-f3cp (n8n) references three different commits with no merged pull
request to confirm which one actually fixed it; the deterministic pass correctly
returns no fixing commit rather than guessing among them, and only the full trace
agent's reasoning can pick the right one. So before doing anything else, `dockerize`
needs to know the fixing commit. With none of the three flags below given, it asks
interactively:

```
Trace this advisory now to resolve its commits? [y/N]:
```

Answering yes runs the same deterministic pass plus full trace agent `trace` uses, and
uses the result directly. Answering no prompts for either a fixing commit SHA pasted
directly, or a path to a trace result JSON file (the same JSON `trace` prints to
stdout); a bare SHA is refetched through the GitHub API for full detail. For
non-interactive use (scripts, CI), three mutually exclusive flags skip the prompt
entirely:

```sh
uv run python -m code_audit dockerize GHSA-jfh8-c2jp-5v3q --trace
uv run python -m code_audit dockerize GHSA-jfh8-c2jp-5v3q --trace-result trace.json
uv run python -m code_audit dockerize GHSA-jfh8-c2jp-5v3q --fixing-commit c77b3cb39312b83b053d23a2158b99ac7de44dd3
```

Once the fixing commit is known (however it was resolved), the rest of the pipeline
runs. Because a full run can take minutes with otherwise no other output, every
long-running step prints a short line to stderr as it starts and finishes: resolving
the target commit, tracing (if chosen), cloning, classifying the repository, checking
for existing artifacts, each generation turn, the build-and-start phase, the health
check, and each repair attempt. Concretely, the pipeline:

1. Resolves the commit to build: the git tag for the advisory's last affected version
when one can be found (this does not depend on the fixing commit at all), otherwise
the parent of the fixing commit resolved above. If neither can be resolved, it exits
with an explanation rather than guessing a commit.
2. Shallow-clones the repository at exactly that commit into a temporary directory.
3. Classifies the repository as an application or a library, in two tiers. Tier 1 is
file-based signals (an entrypoint like `manage.py` or a `scripts.start` in
`package.json` versus a build-backend-only `pyproject.toml` or a `setup.py` with
nothing to run) and is trusted outright once its signals clearly favor one side.
When they do not, a Tier 2 Anthropic call settles it from a compact digest (the
file tree, any root-level manifest file, and a README excerpt) instead of another
hand-added deterministic rule for the next packaging convention that turns up. Most
GHSA advisories target libraries, not applications, so this step exists to skip
those rather than generate a Dockerfile for something that is never run as a
service.
4. Searches the whole checkout for an existing Dockerfile, compose file, `docker/`
directory, or `.devcontainer/`. A compose file only counts as already present when
some service actually builds from the repository or references its image; a compose
file that only defines infrastructure (a database, a cache) does not.
5. If generation is needed, an Anthropic agent reads the repository's files and produces
a Dockerfile (and a compose.yml, only if the application needs more than one service).
6. Builds and runs the result in an isolated, sandboxed compose project. A small helper
container joins the same isolated network and polls the application over HTTP from
inside it until the application answers or a timeout is hit. Whether verification
succeeds or fails, the containers, images, and temporary checkout are always removed
afterward.
7. If verification fails and repair attempts remain (`--repair-attempts`, default 2),
the failure is classified deterministically (the build image failed to pull,
a dependency failed to resolve or install, the build succeeded but the container
never started, or the container started but the health check timed out) and fed
back to the agent together with the previous Dockerfile/compose.yml and the actual
build/run log, asking it to fix that specific failure rather than start over. This
repeats until verification succeeds or repair attempts run out.

The sandboxing on that last step, in plain terms:

- **No bind mounts from the host.** Any volume the generated compose declares that
points at a host path is stripped before it runs; only Docker-managed named volumes
are kept.
- **No extra privileges.** Every container runs with `cap_drop: [ALL]` and no added
capabilities, and `privileged`, `cap_add`, and `network_mode: host` are all stripped
if the generated compose sets any of them.
- **No network access once running.** Every service, including the application, joins
one Docker network created with `internal: true` for the verification run, which
blocks all outbound traffic to the internet. This only restricts the *running*
containers: the build phase (`docker build`) still has normal network access, since
installing dependencies needs it. An internal network also has no route from the
host, so a small helper container (not the generated application) joins the same
network and checks over HTTP from inside it; nothing is published to the host.
- **Bounded resources and time.** Each container is capped at 512MB of memory, 1 CPU,
and 256 processes; the build-and-start phase and the health check both have a hard
wall-clock timeout, so a hung or resource-hungry container cannot stall the command
indefinitely. The build-and-start timeout defaults to 300 seconds but is configurable
with `--build-timeout`, since a large monorepo can legitimately need much longer than
a typical small application.
- **No exposed secrets.** The `docker compose` subprocess is given a minimal,
explicitly allowlisted environment (`PATH`, `HOME`, and the handful of `DOCKER_*`
variables the CLI itself needs to find the right daemon), not the full host
environment with a couple of names removed. Nothing else on the host, including
unrelated secrets the tool never touches otherwise, is reachable through the
generated compose file's variable interpolation.

This is meaningfully more trust than the rest of the tool extends: `trace` only ever
reads data through the GitHub API, while `dockerize` builds and executes code from the
audited repository, which is itself the subject of a security advisory. Treat the
sandboxing above as containment for a real build-and-run step, not as a guarantee that
nothing in the repository can do anything unwanted.

On success, the command prints the resolved target commit and the output path. On
failure (an unresolvable commit, a repository that does not look like a web
application, existing artifacts already in place, a generation failure, or a
verification failure) it prints the reason and exits non-zero; it never retries or
discards a failed attempt silently. A verification failure also copies the generated
Dockerfile/compose to the output path and writes the full, untruncated build output to
`build.log` there: the terminal-facing summary is only the last 500 lines, which can
hide the actual cause of a failure (a truncated tail once hid a real dependency
install error behind a generic "command failed with exit code 1").

Add `--force` to generate and verify even when the repository already has a
Dockerfile or compose file, purely to exercise generation and verification end to
end against a repository you know is already containerized. It still skips a
repository the classifier identifies as a library: `--force` only bypasses the
already-present check, never the application-vs-library judgment. When it does
bypass that check, the command prints a note that the existing artifact is being
overwritten in this run's temporary checkout only, never in the real repository,
since nothing here is pushed upstream:

```sh
uv run python -m code_audit dockerize GHSA-jfh8-c2jp-5v3q --force
```

Add `--build-timeout` (seconds, default 300) to raise the build-and-start timeout for a
repository that legitimately needs longer, for example a large monorepo compiling a Go
backend alongside a large frontend bundle in the same image:

```sh
uv run python -m code_audit dockerize GHSA-jfh8-c2jp-5v3q --build-timeout 900
```

Adjust `--repair-attempts` (default 2) to change how many extra generate-and-verify
tries run after a verification failure before giving up. Each attempt is a full
generate-plus-verify cycle: a fresh Anthropic generation call and another sandboxed
build and health check, so cost and time scale with it. Set it to `0` to go back to a
single, one-shot attempt:

```sh
uv run python -m code_audit dockerize GHSA-jfh8-c2jp-5v3q --repair-attempts 0
```

Add `--debug` to record the generation agent's turns as JSON Lines under `debug/`, in
the same format `trace --debug` uses: one line per turn (thinking summary, tool calls,
truncated tool results, stop reason), and on the terminal turn the raw response text
before it is validated, which is useful for seeing exactly what the model returned when
generation fails (for example, an empty or `FROM`-less Dockerfile, which is rejected
rather than left for Docker itself to fail on with a much less specific error):

```sh
uv run python -m code_audit dockerize GHSA-jfh8-c2jp-5v3q --debug
```

## Development

```sh
Expand Down
34 changes: 34 additions & 0 deletions output/GHSA-mqpq-2p68-46fv/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
FROM python:3.10-slim-bullseye

ENV PYTHONUNBUFFERED=1 \
PYCURL_SSL_LIBRARY=openssl \
LANG=C.UTF-8

# libcurl4-openssl-dev + build tools are required to compile pycurl from source
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libcurl4-openssl-dev \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . .

# Patch the default config so the web UI binds on all interfaces
# instead of localhost only (which is unreachable from outside the container)
RUN sed -i \
's/ip host : "IP address" = localhost/ip host : "IP address" = 0.0.0.0/' \
src/pyload/core/config/default.cfg

# Install the core package from local source then pull in the
# companion web-UI package from PyPI (pyload.webui namespace package)
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
pip install --no-cache-dir . && \
pip install --no-cache-dir pyload-ng-webui

# Directories used at runtime (config + downloads)
RUN mkdir -p /root/.pyload /downloads

EXPOSE 8000

CMD ["python", "-m", "pyload"]
Loading
Loading