Skip to content
Merged
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
112 changes: 112 additions & 0 deletions .github/workflows/container-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Builds both split images (Containerfile.tools, Containerfile.runtime) on every PR and after
# each merge to development/main, and checks the properties the plan's "Done when" names:
# every installed binary at its pin, no Python stack or database in the tools image, the
# runtime's label names the toolchain hash, and a scan against an emptied database degrades
# instead of reporting zero.
name: container-images

on:
pull_request:
paths:
- "Containerfile.tools"
- "Containerfile.runtime"
- "config/external-tools.yaml"
- "scripts/build-toolchain-image.sh"
- "scripts/containerfile-build-args.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/container-images.yml"
merge_group:
push:
branches: [main, development]

concurrency:
group: container-images-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
show-progress: false
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
enable-cache: true
python-version: "3.12"
- run: uv sync --frozen
- name: Install podman
run: sudo apt-get update && sudo apt-get install -y podman

- name: Validate pins against the tools image's inventory
run: uv run python3 scripts/containerfile-build-args.py --check

- name: Build traust-toolchain
run: bash scripts/build-toolchain-image.sh --tools-only --tools-tag traust-toolchain:ci

- name: Every installed binary reports its pin
run: |
# The build's own smoke test (Containerfile.tools' final RUN) already asserted this
# inside the build; re-check from outside as an independent, external verification.
uv run python3 scripts/containerfile-build-args.py --env > /tmp/pins.env
source /tmp/pins.env
podman run --rm traust-toolchain:ci opengrep --version | grep -q "$OPENGREP_VERSION"
podman run --rm traust-toolchain:ci gitleaks version | grep -q "$GITLEAKS_VERSION"
podman run --rm traust-toolchain:ci syft --version | grep -q "$SYFT_VERSION"
podman run --rm traust-toolchain:ci grype --version | grep -q "$GRYPE_VERSION"
podman run --rm traust-toolchain:ci cosign version | grep -q "GitVersion:.*$COSIGN_VERSION"
podman run --rm traust-toolchain:ci govulncheck -version | grep -q "govulncheck@v$GOVULNCHECK_VERSION"
podman run --rm traust-toolchain:ci skopeo --version | grep -q " $SKOPEO_VERSION\$"
podman run --rm traust-toolchain:ci yara --version | grep -q "$YARA_VERSION"
podman run --rm --entrypoint /opt/pip-audit-venv/bin/pip-audit traust-toolchain:ci --version | grep -q "$PIP_AUDIT_VERSION"
podman run --rm traust-toolchain:ci go version | grep -q " go$GO_VERSION "

- name: No traust Python stack and no vulnerability database in traust-toolchain
run: |
! podman run --rm --entrypoint python3.11 traust-toolchain:ci -c "import traust_core" 2>/dev/null
test -z "$(podman run --rm --entrypoint find traust-toolchain:ci / -iname 'vulnerability.db' -o -ipath '*vulndb*' 2>/dev/null)"

- name: No build toolchain in traust-toolchain (it only downloads/compiles in builder stages)
run: |
# curl is deliberately not checked: ubi-minimal ships curl-minimal by default, part of
# the pristine base image, not something this build adds.
for t in gcc make automake autoconf libtool pkg-config; do
podman run --rm --entrypoint sh traust-toolchain:ci -c "! command -v $t" \
|| { echo "FATAL: $t leaked into the final tools image" >&2; exit 1; }
done
# Every native binary resolves against what's already in the base image (no missing
# shared library from a trimmed final stage).
for b in opengrep gitleaks syft grype cosign govulncheck skopeo yara yarac; do
podman run --rm --entrypoint sh traust-toolchain:ci -c "ldd \$(command -v $b) 2>&1" \
| grep -qi "not found" && { echo "FATAL: $b is missing a shared library" >&2; exit 1; }
done
echo "ok"

- name: Build traust-runtime FROM traust-toolchain by digest
run: |
DIGEST=$(podman image inspect traust-toolchain:ci --format '{{.Digest}}')
podman build -f Containerfile.runtime \
--build-arg "TOOLS_IMAGE=traust-toolchain@${DIGEST}" \
--build-arg "TOOLCHAIN_HASH=$(uv run python3 scripts/containerfile-build-args.py --pin-hash)" \
--build-arg "VCS_REF=${{ github.sha }}" \
-t traust-runtime:ci .

- name: The runtime label names the toolchain hash
run: |
HASH=$(uv run python3 scripts/containerfile-build-args.py --pin-hash)
podman inspect traust-runtime:ci --format '{{index .Config.Labels "traust.toolchain-hash"}}' | grep -qF "$HASH"

- name: The traust stack is installed and the CLI runs
run: podman run --rm traust-runtime:ci --help

- name: A scan against an emptied database degrades, never reports zero
run: |
mkdir -p /tmp/empty-feeds-cache/tool-dbs/grype
podman run --rm -v /tmp/empty-feeds-cache:/feeds:Z --entrypoint sh traust-runtime:ci -c '
GRYPE_DB_CACHE_DIR=/feeds/tool-dbs/grype GRYPE_DB_AUTO_UPDATE=false grype /opt/traust -o json >/dev/null 2>/tmp/err
rc=$?
cat /tmp/err >&2
test "$rc" -ne 0
'
80 changes: 80 additions & 0 deletions Containerfile.runtime
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Containerfile.runtime — traust-core + traust-engine + traust (pinned Git SHAs via uv.lock) on
# top of the pinned-tools image (Containerfile.tools). No scanner installs here — everything
# scanner-related comes from the tools base, at the digest the wrapper resolves. Labeled with
# both the commit this was built from and the toolchain hash it was built on, so a runner's
# label and a report's provenance can both be traced back to an exact pair of images.
#
# Build (always use the wrapper — it builds Containerfile.tools first, if needed, and passes its
# digest and pin-set hash here):
# ./scripts/build-toolchain-image.sh
#
# Rebuilt on every merge, regardless of whether the pinned tools changed.

ARG TOOLS_IMAGE
ARG TOOLCHAIN_HASH=unknown
ARG VCS_REF=unknown

FROM registry.access.redhat.com/ubi9/ubi-minimal@sha256:c4c285d42941ebb2bd4da878479008e1e4536bb599aad1dbe568f89de9053661 AS app-builder
RUN microdnf install -y --setopt=install_weak_deps=0 git tar gzip python3.11 python3.11-pip \
&& microdnf clean all
RUN python3.11 -m pip install --no-cache-dir uv==0.13.4

WORKDIR /opt/traust
# Only what `uv sync` needs to resolve and install the pinned dependency graph (traust-contracts,
# traust-ledger, traust-engine, traust-core — through traust's [tool.uv.sources] — plus traust's
# own runtime deps) and build traust itself as a wheel. The harness payload (harnessing/, config/,
# docs/, bin/) is copied straight into the runtime stage below: it's data, not a dependency.
COPY pyproject.toml uv.lock VERSION requirements.txt ./
COPY src/ ./src/
COPY LICENSE NOTICE ./
COPY LICENSES/ ./LICENSES/

# --no-editable: a wheel install, not a dev-mode link back to this build stage's source tree
# (which doesn't exist in the final image). The pinned sources in pyproject.toml are static
# https:// URLs (not runtime-supplied), so AGENTS.md rule S3 (GIT_ALLOW_PROTOCOL=https on
# non-literal URLs) doesn't apply here — and setting it would break uv's own internal
# cache-to-checkout step, which clones over the local `file://` transport.
RUN uv sync --frozen --no-dev --no-editable --python /usr/bin/python3.11

# --- Runtime: the pinned tools image, extended with the traust Python stack ---
FROM ${TOOLS_IMAGE} AS runtime
ARG TOOLCHAIN_HASH
ARG VCS_REF

LABEL name="traust-runtime" \
summary="Traust runtime on toolchain ${TOOLCHAIN_HASH}" \
description="traust-core, traust-engine and traust (pinned Git SHAs) on the \
traust-toolchain image at pin-set hash ${TOOLCHAIN_HASH}. Pull, mount a target, run a profile. \
For local, non-air-gapped use only; an air-gapped deployment mirrors this image and the \
traust-toolchain image it was built on, plus the feeds cache, separately." \
traust.toolchain-hash="${TOOLCHAIN_HASH}" \
traust.commit="${VCS_REF}" \
vendor="Red Hat, Inc."

USER 0
COPY --from=app-builder /opt/traust /opt/traust
RUN chown -R 1001:1001 /opt/traust

# Harness skill data — the same payload the pre-split image shipped, now layered on top of the
# tools base instead of installed alongside ad hoc scanner commands.
COPY harnessing/ /harness/harnessing/
COPY config/ /harness/config/
COPY docs/ /harness/docs/
COPY bin/ /harness/bin/
COPY VERSION /harness/
RUN chown -R 1001:1001 /harness
# Deployment config (corpus-config, product map, …) is mounted, never baked in.
ENV TRAUST_CONFIG_HOME=/harness/deploy-config
ENV HARNESS_ROOT=/harness
ENV PATH="/opt/traust/.venv/bin:${PATH}"

# --- Smoke test: the pinned Python stack imports and reports itself, as the runtime user ---
USER 1001
RUN python3.11 -c "from importlib.metadata import version as v; \
print({n: v(n) for n in ('traust', 'traust-engine', 'traust-ledger', 'traust-contracts', 'traust-core')})" \
&& python3.11 -m traust.cli --help >/dev/null \
&& echo "runtime image verified: traust stack importable, CLI runs"

WORKDIR /workspace
ENTRYPOINT ["python3.11", "-m", "traust.cli"]
CMD ["--help"]
146 changes: 0 additions & 146 deletions Containerfile.toolchain

This file was deleted.

Loading
Loading