Compare commits

...
Author SHA1 Message Date
james.maesandCodex 21517b25e7 fix: refresh generated JVM runtime packages on every build
Co-Authored-By: Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-19 07:50:52 -05:00
james.maes a3d680f130 Merge pull request #37 from KofTwentyTwo/main
chore: back-merge main into develop after 0.3.6
2026-10-13 17:24:24 -05:00
james.maes 57dffdd9a3 Merge pull request #36 from KofTwentyTwo/release/0.3.6
Release Munitor 0.3.6
2026-10-13 17:17:42 -05:00
james.maesandClaude Opus 5.5 9e2c45e79a docs(readme): adoption examples use 0.3.6
Signed-off-by: James Maes <james@kof22.com>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:11:51 -05:00
james.maes fbeec65cb5 Merge pull request #35 from KofTwentyTwo/feature/supplemental-push-boolean
fix: push supplemental images on release builds again (0.3.5 regression)
2026-10-13 17:11:47 -05:00
james.maesandClaude Opus 5.5 69898922be fix: push supplemental images on release builds again
CircleCI renders the boolean push parameter as 1/0 in the step
environment, but build_push_supplemental.sh only pushed when
PUSH_IMAGES was the string "true". Since 0.3.5 every release build
built and scanned supplemental images and never pushed them, so
deployments referencing them hit ImagePullBackOff (kof22 0.1.75).

PUSH_IMAGES now accepts true/1/yes/on and false/0/no/off/empty; unset
still means push, and an unrecognised value fails before any build.
Tests cover release-mode pushes (1, true, TRUE) of every supplemental
image, PR-mode non-pushes (0, false, empty), and the invalid value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-13 17:05:34 -05:00
james.maes 59eb5294fe Merge pull request #34 from KofTwentyTwo/main
chore: back-merge main into develop after 0.3.5
2026-10-13 15:44:21 -05:00
james.maes 16079c5ca8 Merge pull request #33 from KofTwentyTwo/release/0.3.5
Release Munitor 0.3.5
2026-10-13 15:37:54 -05:00
james.maesandClaude Opus 5.5 578c7ea0eb docs(readme): adoption examples use 0.3.5
Signed-off-by: James Maes <james@kof22.com>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:31:55 -05:00
james.maes 53c04937b0 Merge pull request #32 from KofTwentyTwo/feature/pr-image-scan
feat: scan container images in PR pipelines
2026-10-13 14:42:28 -05:00
james.maesandClaude Opus 5.5 b09ced056f feat: scan container images in PR pipelines
New munitor/docker_scan job (image-scan) in the pr-checks workflow of
java-webapp, gradle-webapp, node-api and node-webapp: builds the app
image and every supplemental image, runs the same Trivy 0.69.3
HIGH/CRITICAL --exit-code 1 scan as the release pipeline (same
.trivyignore), stores the JSON results, and never pushes.
build_push_supplemental gains push (default true, unchanged behavior).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:18:41 -05:00
james.maes fb232fb6fe Merge pull request #31 from KofTwentyTwo/main
chore: back-merge main into develop after 0.3.4
2026-10-12 06:24:42 -05:00
17 changed files with 486 additions and 18 deletions

No files matched your search

+1
View File
@@ -332,6 +332,7 @@ These components are shared across all modules -- you don't need to recreate the
| `secrets_scan` job | Gitleaks (all modules) |
| `sast_scan` job | Semgrep (all modules, opt-in) |
| `docker_build_push` job | Build + health check + Trivy scan + GHCR push |
| `docker_scan` job | PR image build + Trivy scan (including supplemental images), JSON artifacts, no publishing |
| `health_check` command | Container health check smoke test (all Docker modules) |
| `update_cd_repo` job | GitOps CD repo update |
| `github_release` job | Annotated git tags + GitHub Releases (auto-enabled with `contexts.github`) |
+15 -6
View File
@@ -6,7 +6,7 @@ Latin: *munitor* (fortifier, protector) -- KofTwentyTwo's unified CircleCI orb f
| --- | --- |
| **Tier** | Internal until classified -- listed under *Awaiting classification* in the KofTwentyTwo standards [MAINTAINERS.md](https://github.com/KofTwentyTwo/standards/blob/main/MAINTAINERS.md); Product-tier requirements are being adopted |
| **Standards** | Targets [KofTwentyTwo standards](https://github.com/KofTwentyTwo/standards) 1.0.0 |
| **Orb** | [`kof22/munitor`](https://circleci.com/developer/orbs/orb/kof22/munitor), latest stable `0.3.3` |
| **Orb** | [`kof22/munitor`](https://circleci.com/developer/orbs/orb/kof22/munitor), latest stable `0.3.6` |
| **Security** | [SECURITY.md](SECURITY.md) |
| **Contributing** | [CONTRIBUTING.md](CONTRIBUTING.md) |
@@ -70,16 +70,16 @@ Munitor validates branch names and will fail on non-conforming names.
Munitor is a public CircleCI orb: any CircleCI organization can use it without forking. This checklist covers what a new consumer needs, for example a Java/Maven web service. A QQQ application should also read the [QQQ adoption checklist](https://github.com/QRun-IO/qqq/blob/develop/docs/migration/adoption-checklist.adoc) (section 7 covers release tooling).
**Version.** Use `kof22/munitor@0.3.3` (or `@0.3` to float on the 0.3 line) in `.circleci/config.yml`, and the same value as `orb_version` in `.munitor.yml`. Keep the setup orb and the generated pipeline on the same line.
**Version.** Use `kof22/munitor@0.3.6` (or `@0.3` to float on the 0.3 line) in `.circleci/config.yml`, and the same value as `orb_version` in `.munitor.yml`. Keep the setup orb and the generated pipeline on the same line.
### 1. Repository files
- `.circleci/config.yml`: the setup config from [Quick Start](#quick-start), pinned to `kof22/munitor@0.3.3`.
- `.circleci/config.yml`: the setup config from [Quick Start](#quick-start), pinned to `kof22/munitor@0.3.6`.
- `.munitor.yml`: pick a [pipeline type](#pipeline-types). A Java service is `java-webapp`. KofTwentyTwo's Website-Backend is a working example; a generalized version:
```yaml
pipeline: java-webapp
orb_version: "0.3.3"
orb_version: "0.3.6"
image_name: my-service
java_version: "21"
docker:
@@ -372,7 +372,7 @@ contexts:
**What runs:** Maven build/test, Checkstyle, SpotBugs, PMD, JaCoCo coverage, OWASP + Trivy security, Docker build/push (Dockerfile auto-generated), gitleaks. OWASP dependency check requires the `nvd` context; if omitted, the security scan runs without NVD database enrichment.
**Dockerfile:** Auto-generated production-ready image using Eclipse Temurin JRE Alpine. Includes non-root user (uid 1001), JVM container tuning flags, Docker HEALTHCHECK, and configurable port via `health.port`. The pre-built JAR from CI is copied directly (no redundant Maven build stage).
**Dockerfile:** Auto-generated production-ready image using Eclipse Temurin JRE Alpine. Includes non-root user (uid 1001), JVM container tuning flags, Docker HEALTHCHECK, and configurable port via `health.port`. The pre-built JAR from CI is copied directly (no redundant Maven build stage). PR and release image builds pull the current base and bypass Docker layer caching for generated Java/Gradle runtime images, ensuring the Alpine package upgrade runs on every build. Node builds retain their layer cache. Trivy gates remain required.
### `terraform`
@@ -490,7 +490,7 @@ Each workflow runs on specific branches and includes jobs based on your config:
| Workflow | Branches | Jobs |
|----------|----------|------|
| `pr-checks` | `feature/*`, `hotfix/*` | Build, test, lint, coverage, security scans |
| `pr-checks` | `feature/*`, `hotfix/*` | Build, test, lint, coverage, security scans, container image scan (Docker modules) |
| `develop` | `develop` | All pr-checks + Docker build/push + CD update (dev env) |
| `staging` | `staging` | All pr-checks + Docker build/push + CD update (staging env) |
| `release-candidate` | `release/*` | All pr-checks + Docker, CD (staging), SonarCloud, SBOM, GitHub Release (pre-release) |
@@ -498,11 +498,20 @@ Each workflow runs on specific branches and includes jobs based on your config:
For `validate-cd-repo` and `argocd-apps`, all three workflows (`pr-checks`, `develop`, `release`) run the same 5-job set: yaml-lint, kustomize-validate, kubesec-scan, kube-linter, secrets-scan.
For `java-webapp`, `gradle-webapp`, `node-api`, and `node-webapp`, PRs run
`image-scan` after `build-and-test`: build the application and any supplemental
images locally, then fail on HIGH/CRITICAL CVEs with the same Trivy policy and
repository-root `.trivyignore` as release builds. JSON reports are saved as
artifacts. This job does not log in, publish images, reserve tags, or update CD.
Release candidates already scan through `docker-build-push` and do not run a
second image scan job.
### Job Dependency Graph (node-api pr-checks)
```
checkout
--> build-and-test
--> image-scan (local build + Trivy, including supplemental images)
--> code-quality
--> coverage
--> security-scan
+32
View File
@@ -1,5 +1,35 @@
# Munitor session state
## JVM runtime package freshness (2026-10-19)
Website-Backend release6258 and PR scans6266/6277 reused a cached apk upgrade
layer containing vulnerable libexpat. The shared docker_build command now pulls
the base and bypasses layer caching for generated Java/Gradle runtimes, while
Node caching and all Trivy gates remain intact. Narrow behavioral tests cover
both pipeline types, alternate/missing manifests, tags, provenance, and both
PR/release job routes. All eight focused shell cases passed via the coordinating
agent; ShellCheck and git diff --check passed. Full make/BATS/orb validation and
real-image scanning remain pending. Stable release and consumer adoption remain
pending; no unchanged vulnerable image should be retried.
## PR container scanning (2026-10-13)
Uncommitted work on `feature/pr-image-scan` adds `docker_scan` after
`build-and-test` in the four application PR templates. It restores the existing
feature/hotfix version metadata, builds locally, and applies the release Trivy
HIGH/CRITICAL policy from the repository root. Supplemental images use
`push: false`; the command defaults to publishing for existing callers. Main
and supplemental JSON reports are artifacts. Release candidates already scan
through `docker_build_push`, so no duplicate job was added.
Verified: native CircleCI orb pack/validate, ShellCheck on all source scripts
and the changed tests, template structure/filters/parameter wiring, packed
template synchronization, and `git diff --check`. Requested make and BATS runs
were attempted but Git Bash cannot start in this sandbox (CreateFileMapping,
Win32 error 5); yamllint is not installed. New behavioral tests therefore need
a Linux/CI run, including real Docker/Trivy integration. No commit or publish.
The Second Brain vault is outside writable roots; continuity is recorded here.
## Release 0.3.3 (2026-10-11)
PR22 (00f349c) fixes release version collisions. Website-Backend #65/#66 (0.1.24)
@@ -66,3 +96,5 @@ after Java installation. APT now receives noninteractive debconf and automatic
needrestart settings; a stubbed integration test verifies both APT invocations.
These fixes require normal feature-to-develop review and stable tag publication
from main before website consumers can select the new version. Nothing deployed.
Runtime freshness validation: coordinating agent completed make all (lint, pack, orb validation, scripts,277template assertions) and all7BATS tests in Ubuntu. Real consumer image scan remains required after release.
+5
View File
@@ -2,6 +2,11 @@
## Pending
- [ ] Release JVM runtime cache fix and adopt the stable orb in Website-Backend; verify fresh image Trivy before release.
- [ ] Run PR image-scan script/template/BATS tests on Linux and verify a consumer PR with Docker/Trivy before publishing.
- [x] Add PR-only image scans for the four application templates, supplemental scan-only mode, JSON artifacts, and regression tests (uncommitted).
- [x] Review publishing CI fixes and merge PR19 to develop.
- [x] Publish v0.3.2 from main.
- [ ] Publish v0.3.3 (atomic release versions); bump Website-Backend/Frontend pins to 0.3.3.
+11 -3
View File
@@ -1,8 +1,12 @@
description: >
Build, scan, and push supplemental Docker images alongside the main application image.
Each supplemental image shares the same version tag. Requires docker_push_ghcr to have
run first (for registry auth and Trivy installation).
Each supplemental image shares the same version tag. Requires run_trivy_image first
for Trivy installation, and docker_push_ghcr for registry auth only when push is true.
parameters:
push:
type: boolean
default: true
description: Publish scanned images to the registry. Disable for PR scans.
supplemental_images:
type: string
default: "[]"
@@ -16,9 +20,13 @@ parameters:
description: Container registry URL.
steps:
- run:
name: Build and push supplemental images
name: Build and scan supplemental images (optionally push)
environment:
PUSH_IMAGES: << parameters.push >>
SUPPLEMENTAL_IMAGES_JSON: << parameters.supplemental_images >>
IMAGE_NAME: << parameters.image_name >>
REGISTRY: << parameters.registry >>
command: << include(scripts/build_push_supplemental.sh) >>
- store_artifacts:
path: /tmp/trivy-supplemental-results
destination: trivy-supplemental-scans
+49
View File
@@ -0,0 +1,49 @@
description: >
Build and scan application and supplemental images without publishing.
Requires the source and version metadata workspace from build-and-test.
parameters:
image_name:
type: string
description: Docker image name (without registry prefix).
registry:
type: string
default: ghcr.io/KofTwentyTwo
description: Container registry URL.
resource_class:
type: enum
enum:
- medium
- large
default: large
description: Machine resource class.
docker_layer_caching:
type: boolean
default: true
description: Enable Docker Layer Caching for faster builds.
supplemental_images:
type: string
default: "[]"
description: JSON array of supplemental images to build and scan.
executor:
name: machine
resource_class: << parameters.resource_class >>
docker_layer_caching: << parameters.docker_layer_caching >>
steps:
- attach_workspace:
at: .
- run:
name: Restore version metadata
command: <<include(scripts/restore_version_metadata.sh)>>
- generate_dockerfile
- docker_build:
image_name: << parameters.image_name >>
registry: << parameters.registry >>
- restore_trivy_cache
- run_trivy_image:
image_tag: << parameters.registry >>/<< parameters.image_name >>:${DOCKER_TAG}
- save_trivy_cache
- build_push_supplemental:
supplemental_images: << parameters.supplemental_images >>
image_name: << parameters.image_name >>
registry: << parameters.registry >>
push: false
+28 -8
View File
@@ -27,6 +27,19 @@ IMAGE="${IMAGE_NAME:?IMAGE_NAME not set}"
REGISTRY="${REGISTRY:-ghcr.io/KofTwentyTwo}"
VERSION="${PROJECT_VERSION:?PROJECT_VERSION not set -- run export_version_vars first}"
ENV_TAG="${DOCKER_ENV_TAG:-}"
# CircleCI renders a boolean orb parameter as "1"/"0" in a step environment, not
# "true"/"false", so accept both spellings. Unset means push (release builds).
case "$(echo "${PUSH_IMAGES-true}" | tr '[:upper:]' '[:lower:]')" in
true | 1 | yes | on) PUSH_IMAGES=true ;;
false | 0 | no | off | "") PUSH_IMAGES=false ;;
*)
echo "ERROR: PUSH_IMAGES must be true/false (or 1/0), got '${PUSH_IMAGES}'" >&2
exit 1
;;
esac
# run_trivy_image installs here in a previous CircleCI step.
export PATH="${HOME}/bin:${PATH}"
mkdir -p /tmp/trivy-supplemental-results
COUNT=$(echo "${IMAGES_JSON}" | jq 'length')
echo "Found ${COUNT} supplemental image(s) to build."
@@ -76,26 +89,33 @@ for i in $(seq 0 $((COUNT - 1))); do
fi
docker build "${BUILD_ARGS[@]}" .
if [[ -n "${ENV_TAG}" ]]; then
if [[ "${PUSH_IMAGES}" == "true" && -n "${ENV_TAG}" ]]; then
ENV_FULL_TAG="${REGISTRY}/${DERIVED_NAME}:${ENV_TAG}"
docker tag "${FULL_TAG}" "${ENV_FULL_TAG}"
fi
# Trivy scan
echo "Scanning ${FULL_TAG} with Trivy ..."
trivy image --severity HIGH,CRITICAL --exit-code 1 "${FULL_TAG}"
trivy image --format json --output "/tmp/trivy-supplemental-results/${NAME}.json" \
--severity HIGH,CRITICAL --exit-code 1 "${FULL_TAG}" || {
EXIT_CODE=$?
trivy image --severity HIGH,CRITICAL "${FULL_TAG}"
exit "${EXIT_CODE}"
}
# Push
echo "Pushing ${FULL_TAG} ..."
docker push "${FULL_TAG}"
if [[ "${PUSH_IMAGES}" == "true" ]]; then
echo "Pushing ${FULL_TAG} ..."
docker push "${FULL_TAG}"
if [[ -n "${ENV_TAG}" ]]; then
echo "Pushing ${ENV_FULL_TAG} ..."
docker push "${ENV_FULL_TAG}"
if [[ -n "${ENV_TAG}" ]]; then
echo "Pushing ${ENV_FULL_TAG} ..."
docker push "${ENV_FULL_TAG}"
fi
fi
echo "--- Done: ${DERIVED_NAME} ---"
done
echo ""
echo "All supplemental images built, scanned, and pushed."
echo "All supplemental images built and scanned (push=${PUSH_IMAGES})."
+10 -1
View File
@@ -23,7 +23,16 @@ munitor_header "docker_build (${FULL_IMAGE}:${VERSION})"
munitor_check_tool docker --version
# Build with version tag; add env tag when set (e.g., develop, staging)
BUILD_CMD=(docker build -f "${DOCKERFILE}" -t "${FULL_IMAGE}:${VERSION}")
BUILD_CMD=(docker build --pull -f "${DOCKERFILE}" -t "${FULL_IMAGE}:${VERSION}")
# Generated JVM images only copy a prebuilt JAR. Re-run their package upgrades:
# a cached apk upgrade layer can retain vulnerable packages indefinitely.
CONFIG_FILE="${MUNITOR_CONFIG:-.munitor.yml}"
if [[ -f "${CONFIG_FILE}" ]]; then
munitor_check_tool yq --version
case "$(yq -r '.pipeline' "${CONFIG_FILE}")" in
java-webapp|gradle-webapp) BUILD_CMD+=(--no-cache) ;;
esac
fi
if [[ -n "${ENV_TAG}" ]]; then
BUILD_CMD+=(-t "${FULL_IMAGE}:${ENV_TAG}")
fi
+56
View File
@@ -366,6 +366,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
@@ -694,6 +708,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
@@ -1037,6 +1065,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
@@ -1406,6 +1448,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
@@ -16,6 +16,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
+14
View File
@@ -16,6 +16,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
+14
View File
@@ -31,6 +31,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
+14
View File
@@ -31,6 +31,20 @@ workflows:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/docker_scan:
name: image-scan
image_name: ${MUNITOR_IMAGE_NAME}
registry: ${MUNITOR_DOCKER_REGISTRY}
##IF_SUPPLEMENTAL##
supplemental_images: '${MUNITOR_SUPPLEMENTAL_IMAGES_JSON}'
##ENDIF_SUPPLEMENTAL##
requires:
- build-and-test
filters:
branches:
only:
- /feature\/.*/
- /hotfix\/.*/
- munitor/secrets_scan:
name: secrets-scan
filters:
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bats
@test "PR and release JVM builds refresh packages without disabling Node caching or Trivy" {
run bash "${BATS_TEST_DIRNAME}/test_docker_build.sh"
[ "$status" -eq 0 ]
}
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Assert the actual shared PR/release build command without a Docker daemon.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT
mkdir -p "${WORK}/bin" "${WORK}/repo"
cat > "${WORK}/bin/docker" <<'MOCK'
#!/usr/bin/env bash
[[ "$1" == --version ]] && exit 0
printf '%s\n' "$@" > "${CALL_LOG}"
MOCK
cat > "${WORK}/bin/yq" <<'MOCK'
#!/usr/bin/env bash
[[ "$1" == --version ]] && exit 0
[[ "$1" == -r && "$2" == .pipeline ]] || exit 2
sed -n 's/^pipeline: //p' "$3"
MOCK
chmod +x "${WORK}/bin/docker" "${WORK}/bin/yq"
export PATH="${WORK}/bin:${PATH}"
export CALL_LOG="${WORK}/calls" BASH_ENV="${WORK}/env"
export IMAGE_NAME=app REGISTRY=ghcr.io/example PROJECT_VERSION=1.2.3
export CIRCLE_SHA1=abc123 DOCKER_ENV_TAG=develop
cd "${WORK}/repo"
for pipeline in java-webapp gradle-webapp node-api node-webapp; do
printf 'pipeline: %s\n' "${pipeline}" > .munitor.yml
bash "${PROJECT_DIR}/src/scripts/docker_build.sh" > "${WORK}/output"
grep -qx -- '--pull' "${CALL_LOG}"
if [[ "${pipeline}" == java-webapp || "${pipeline}" == gradle-webapp ]]; then
grep -qx -- '--no-cache' "${CALL_LOG}"
else
! grep -qx -- '--no-cache' "${CALL_LOG}"
fi
grep -qx 'org.opencontainers.image.revision=abc123' "${CALL_LOG}"
grep -qx 'ghcr.io/example/app:1.2.3' "${CALL_LOG}"
grep -qx 'ghcr.io/example/app:develop' "${CALL_LOG}"
echo "PASS: ${pipeline} cache policy, provenance and tags"
done
mv .munitor.yml 'custom config.yml'
printf 'pipeline: java-webapp\n' > 'custom config.yml'
MUNITOR_CONFIG='custom config.yml' bash "${PROJECT_DIR}/src/scripts/docker_build.sh" > "${WORK}/output"
grep -qx -- '--no-cache' "${CALL_LOG}"
echo 'PASS: alternate manifest path'
bash "${PROJECT_DIR}/src/scripts/docker_build.sh" > "${WORK}/output"
grep -qx -- '--pull' "${CALL_LOG}"
! grep -qx -- '--no-cache' "${CALL_LOG}"
echo 'PASS: standalone build without manifest'
for job in docker_scan docker_build_push; do
grep -q '^ - docker_build:' "${PROJECT_DIR}/src/jobs/${job}.yml"
grep -q '^ - run_trivy_image:' "${PROJECT_DIR}/src/jobs/${job}.yml"
echo "PASS: ${job} uses shared builder and retains Trivy"
done
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# Exercise image scanning with local tool stubs; no Docker daemon or network needed.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_SCRIPTS="$(cd "${SCRIPT_DIR}/../src/scripts" && pwd)"
WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT
PASS=0
FAIL=0
pass() { echo "PASS"; PASS=$((PASS + 1)); }
fail() { echo "FAIL"; FAIL=$((FAIL + 1)); }
mkdir -p "${WORK}/bin" "${WORK}/home/bin" "${WORK}/repo"
cat > "${WORK}/bin/docker" <<'EOF'
#!/usr/bin/env bash
printf 'docker %s\n' "$*" >> "${CALL_LOG}"
EOF
cat > "${WORK}/home/bin/trivy" <<'EOF'
#!/usr/bin/env bash
[[ -f .trivyignore ]] || exit 99
printf 'trivy %s\n' "$*" >> "${CALL_LOG}"
# Redirect the production /tmp output path into the test workspace.
while [[ $# -gt 0 ]]; do
if [[ "$1" == --output ]]; then
printf '{"Results":[]}\n' > "${REPORT_DIR}/$(basename "$2")"
fi
shift
done
exit "${SCAN_EXIT:-0}"
EOF
chmod +x "${WORK}/bin/docker" "${WORK}/home/bin/trivy"
export PATH="${WORK}/bin:${PATH}" HOME="${WORK}/home"
export CALL_LOG="${WORK}/calls" REPORT_DIR="${WORK}"
export IMAGE_NAME=app REGISTRY=ghcr.io/example CIRCLE_SHA1=abcdef123456
export SUPPLEMENTAL_IMAGES_JSON='[{"name":"migrations","dockerfile":"Dockerfile.migrations"},{"name":"worker","base":"example/base:1"}]'
cd "${WORK}/repo"
touch .trivyignore
echo "=== PR image scan tests ==="
echo -n " TEST: feature metadata survives workspace restore and supplies Docker scan tag... "
export BASH_ENV="${WORK}/build-env" CIRCLE_BRANCH=feature/image-scan
export GITVERSION_MAJOR=1 GITVERSION_MINOR=2 GITVERSION_PATCH=3
export GITVERSION_SHA=abcdef123456
bash "${SRC_SCRIPTS}/export_version_vars.sh" > "${WORK}/version.log"
bash "${SRC_SCRIPTS}/persist_version_metadata.sh" >> "${WORK}/version.log"
export BASH_ENV="${WORK}/scan-env"
bash "${SRC_SCRIPTS}/restore_version_metadata.sh" >> "${WORK}/version.log"
bash "${SRC_SCRIPTS}/docker_build.sh" >> "${WORK}/version.log"
# shellcheck disable=SC1090
source "${BASH_ENV}"
export IMAGE_TAG='ghcr.io/example/app:${DOCKER_TAG}'
if bash "${SRC_SCRIPTS}/run_trivy_image.sh" >> "${WORK}/version.log" &&
grep -q 'trivy image .*--severity HIGH,CRITICAL --exit-code 1 ghcr.io/example/app:1.2.3-image-scan-abcdef123456-SNAPSHOT' "${CALL_LOG}"; then
pass
else
fail
fi
echo -n " TEST: push=false builds and scans every supplemental without publishing... "
: > "${CALL_LOG}"
echo 'export DOCKER_ENV_TAG=develop' >> "${BASH_ENV}"
if PUSH_IMAGES=false bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
[[ $(grep -c '^docker build ' "${CALL_LOG}") == 2 ]] &&
[[ $(grep -c '^trivy image .*--severity HIGH,CRITICAL --exit-code 1 ' "${CALL_LOG}") == 2 ]] &&
! grep -Eq '^docker (push|login|tag|buildx) ' "${CALL_LOG}" &&
[[ -s "${WORK}/migrations.json" && -s "${WORK}/worker.json" ]]; then
pass
else
fail
fi
echo -n " TEST: default push still publishes version and environment tags... "
: > "${CALL_LOG}"
unset PUSH_IMAGES
if bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
[[ $(grep -c '^docker push ' "${CALL_LOG}") == 4 ]]; then
pass
else
fail
fi
# Regression (0.3.5): CircleCI renders the boolean "push" parameter as "1"/"0" in
# the step environment, and release builds silently skipped the push.
for push in 1 true TRUE; do
echo -n " TEST: push=${push} (release builds) publishes every supplemental image... "
: > "${CALL_LOG}"
if PUSH_IMAGES="${push}" bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
[[ $(grep -c '^docker push ' "${CALL_LOG}") == 4 ]] &&
grep -q '^docker push ghcr.io/example/app-migrations:' "${CALL_LOG}" &&
grep -q '^docker push ghcr.io/example/app-worker:' "${CALL_LOG}" &&
grep -q 'push=true' "${WORK}/supplemental.log"; then
pass
else
fail
fi
done
for push in 0 false ""; do
echo -n " TEST: push='${push}' (PR scans) publishes nothing... "
: > "${CALL_LOG}"
if PUSH_IMAGES="${push}" bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" &&
! grep -Eq '^docker (push|tag) ' "${CALL_LOG}"; then
pass
else
fail
fi
done
echo -n " TEST: an unrecognised push value fails instead of guessing... "
: > "${CALL_LOG}"
if PUSH_IMAGES=maybe bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" 2>&1; then
fail
elif ! grep -Eq '^docker (build|tag|push) ' "${CALL_LOG}" && grep -q "must be true/false" "${WORK}/supplemental.log"; then
pass
else
fail
fi
for push in false true; do
echo -n " TEST: push=${push} fails on HIGH/CRITICAL findings before any push... "
: > "${CALL_LOG}"
rm -f "${WORK}/migrations.json"
if SCAN_EXIT=1 PUSH_IMAGES="${push}" bash "${SRC_SCRIPTS}/build_push_supplemental.sh" > "${WORK}/supplemental.log" 2>&1; then
fail
elif ! grep -q '^docker push ' "${CALL_LOG}" && [[ -s "${WORK}/migrations.json" ]]; then
pass
else
fail
fi
done
echo "Results: ${PASS} passed, ${FAIL} failed"
[[ ${FAIL} -eq 0 ]]
+30
View File
@@ -157,6 +157,36 @@ run_negative_context_test() {
echo "=== Template Rendering Tests ==="
# Container scans belong only to PR jobs; RC already builds and scans before release.
for module in java-webapp gradle-webapp node-api node-webapp; do
fixture="${FIXTURES_DIR}/${module}.munitor.yml"
run_context_test "${module}: PR builds and scans the image" \
"${fixture}" "pr-checks:" " develop:" 'munitor/docker_scan:'
run_context_test "${module}: image scan is named image-scan" \
"${fixture}" "munitor/docker_scan:" "munitor/secrets_scan:" 'name: image-scan'
run_context_test "${module}: image scan waits for build-and-test" \
"${fixture}" "munitor/docker_scan:" "munitor/secrets_scan:" '\- build-and-test'
run_negative_context_test "${module}: no duplicate scan outside PR workflow" \
"${fixture}" " develop:" "END_OF_CONFIG" 'munitor/docker_scan:'
run_context_test "${module}: RC already builds and scans" \
"${fixture}" "release-candidate:" "production:" 'munitor/docker_build_push:'
echo -n " TEST: ${module}: every image scan has exactly the PR branch filters... "
output=$(render_fixture "${fixture}")
filters=$(yq -o=json -I=0 '[.workflows[].jobs[] | select(has("munitor/docker_scan")) | ."munitor/docker_scan".filters.branches.only]' <<< "${output}")
if [[ "${filters}" == '[["/feature\\/.*/","/hotfix\\/.*/"]]' ]]; then
echo "PASS"
PASS=$((PASS + 1))
else
echo "FAIL (unexpected scan filters: ${filters})"
FAIL=$((FAIL + 1))
fi
done
run_context_test "supplemental images are wired into the PR image scan" \
"${FIXTURES_DIR}/java-webapp-supplemental.munitor.yml" \
"munitor/docker_scan:" "munitor/secrets_scan:" 'supplemental_images:.*migrations'
# Test java-webapp renders with correct values
run_test "java-webapp: renders image_name" \
"${FIXTURES_DIR}/java-webapp.munitor.yml" \