# Security runtime and secret-scan / test-report primitives Scope: `runtime/security.Dockerfile`, `platform_ci/secret_scan.py` and `platform_ci/test_reports.py` with their tests. **Status: unqualified.** Nothing here has been built, scanned or published. The root owner builds, scans and publishes the image, and only then pins its digest. Until that happens the image must not be referenced by any workflow or catalog. ## Image `security.Dockerfile` derives from the published control image `git.kof22.com/koftwentytwo/platform-ci-control@sha256:11f8cb328e7fc7aca8e0c39ed979b33cb1db411a59460baae3e0d48281a15e3c` (digest-checked at build) and adds one file, `/usr/local/bin/gitleaks`, **rebuilt from source**. Why: the upstream prebuilt 8.21.2 binary was scanned at 28 HIGH/CRITICAL findings (26 Go standard library, 2 `golang.org/x/text`). Gitleaks' source, rules and fingerprints stay exactly the 8.21.2 commit; only the compiler and `golang.org/x/text` change. Nothing in the scanner's source or rules is edited. Rebuild pins (mirroring `runtime-qualification/security/rebuild-pins.json`, frozen by root; the recipe tests compare the Dockerfile only with the constants in `secret_scan.py` and do not read that file): | Item | Value | | --- | --- | | Gitleaks | 8.21.2, commit `43fae355e6fe4d99d2a7b240a224b85e2903aeb4` | | Source archive | `https://codeload.github.com/gitleaks/gitleaks/tar.gz/`, SHA-256 `9035d05f3aa6c28dbc06c9785b019b2c2331efdb91c626abe39381417d08ab41` | | Go toolchain | `golang:1.27.3-bookworm@sha256:e482b31b5d30c31f9e495f4b8cbdf41429b2d74955dfbcd9c0a904483d738390` | | `golang.org/x/text` | `v0.41.0` (fixes both library findings) | Build stages (only the last ships): 1. `gitleaks-source` (control image): downloads the archive with `urllib`, fails on a `hashlib` SHA-256 mismatch, extracts with `tarfile` `filter="data"`, requires exactly one top-level directory. 2. `gitleaks-build` (pinned Go image): `GOTOOLCHAIN=local`, `go get golang.org/x/text@v0.41.0`, then `CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags "-s -w -X /cmd.Version=8.21.2"` (`cmd.Version` is the `Version` variable of package `cmd`, defined in the frozen archive's `cmd/version.go`; module path read from `go list -m`). It checks that the binary's embedded Go is `go1.27.3`, that its `golang.org/x/text` dependency is `v0.41.0`, and that `gitleaks version` prints `8.21.2`. 3. Runtime: the control image plus that one binary. No platform or application source, and no Go, is shipped. Downloads and `go get` (Go module proxy and checksum database) happen at image build time only; nothing is installed or downloaded when a pipeline runs. The constants in `secret_scan.py` must equal the build args (a test compares them). The build's dependency and compiler facts are therefore: Go 1.27.3 (official image), x/text v0.41.0, source commit above. **Legacy fact only:** the old prebuilt `gitleaks_8.21.2_linux_x64.tar.gz` checksum `5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba` (from `tools.env`) describes the vulnerable binary that is no longer used. It verifies nothing about the rebuilt binary and is not a build input; the rebuilt binary is verified by the source archive checksum and the pinned Go image, and has no vendor-published binary checksum. **Build and scan proof (root-reported, frozen only; not independently verified here):** root built this recipe with the `cmd.Version` ldflag (the earlier `version.Version` flag was wrong), the binary prints `8.21.2`, and a Trivy scan of the rebuilt image found 0 HIGH/CRITICAL vulnerabilities and 0 secrets, against 28 for the old prebuilt binary. The Gitleaks source commit and rules are unchanged from 8.21.2. **Not yet published:** the image is not published and no digest is pinned, so it stays unqualified for workflow use until root publishes it and pins the digest. A real-binary history-scan smoke test is still owed. ## Secret scan contract ```python scan_history(source_directory, source_revision, report_path, run_command=run_scan_command, parent_environment=os.environ) -> SecretScanResult # SecretScanResult(passed, source_revision, failure, findings, exit_code, report_path, gitleaks_version) ``` - Preconditions, all failures (never exceptions): revision is exactly 40 lowercase hex; `HEAD` equals it; the clone is not shallow (`git rev-parse --is-shallow-repository` must print `false`); the binary is always `GITLEAKS_INSTALL_PATH` (`/usr/local/bin/gitleaks`, not configurable) and must report version 8.21.2 (a binary that cannot start fails the scan); `report_path` is inside the source checkout and outside `.git` and `.platform-ci`. - Scan: `gitleaks detect --source --no-banner --redact=100 --report-format json --report-path --exit-code=2 --log-opts="--full-history "`. Only that revision's ancestry is scanned. A stale report is deleted first. - Failure: any finding (report is authoritative over exit code 0), exit code 2, any other nonzero exit, or a missing, unreadable or non-list report. Gitleaks and Git output is captured and discarded; only counts and generic reasons are logged or returned, so no matched secret or body reaches a log. - Environment: only `PATH`, `LANG`, `LC_ALL`, `TMPDIR`, plus `GIT_CONFIG_NOSYSTEM`, `GIT_CONFIG_GLOBAL=/dev/null`, `GIT_NO_REPLACE_OBJECTS`, `GIT_TERMINAL_PROMPT=0`. `safe.directory` is the exact resolved source path: `-c` on each Git command, and `GIT_CONFIG_COUNT/KEY_0/VALUE_0` for the Git processes Gitleaks starts. - Exemptions: Gitleaks reads the repository's standard `.gitleaksignore` itself. There is no central owner/repository map and no baseline option; the approved exact fingerprints are moved into the repositories by the migration worker, and baseline contents are not changed here. - Runner: `run_scan_command(command, cwd, env) -> CommandOutput(exit_code, stdout)`, no shell; raises `SecretScanError` when a program cannot start (converted to a failed result by `scan_history`). ## Test-report contract ```python check_junit_reports(workspace_directory, report_paths) -> JUnitSummary # JUnitSummary(reports, tests, skipped, failures, errors, problems); properties executed, passed ``` - Reads only the declared files in one pass of the public `xml.parsers.expat` parser feeding `ElementTree.TreeBuilder` (the C `ElementTree.XMLParser` exposes no handlers and is not constructed); no process is started and tests are never rerun. - Rejects DOCTYPE, ENTITY and external entity references in UTF-8 and UTF-16 (expat handlers raise, nothing is expanded or fetched), expat errors as malformed XML, roots other than `testsuite`/`testsuites`, non-integer counts, and `skipped > tests`. - Fails on failures/errors attributes or on any `testcase` with a `failure` or `error` child. - Legacy Maven `--allow-empty` parity: zero reports, zero tests and all-skipped pass. No new zero or skip gate. - Containment: each path must resolve (through symlinks) to a regular file inside the workspace, outside `.git` and `.platform-ci`, else `TestReportError`. A missing declared report is packaging's responsibility, so it is an error here only as "not a regular file", never a pass. ## Tests From `candidate/`: `python -m unittest tests.test_secret_scan tests.test_test_reports` (all tests use a fake command runner or temporary files). Lint: `ruff check` and `ruff format --check`; typing: `pyright`. Strict `pyright` must also pass. **The author had no shell**, so these changes are unverified until root runs them.