#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" IMAGE="${SECURITY_AUDIT_IMAGE:-govoplan/security-audit:local}" SCOPE="${SECURITY_AUDIT_SCOPE:-current}" MODE="${SECURITY_AUDIT_MODE:-full}" REPORTS_DIR="${SECURITY_AUDIT_REPORTS_DIR:-audit-reports}" REBUILD="${SECURITY_AUDIT_REBUILD:-0}" UPDATE="${SECURITY_AUDIT_UPDATE:-0}" BUILD_ONLY=0 SCRIPT_ARGS=() while [[ $# -gt 0 ]]; do case "$1" in --mode) MODE="${2:?missing mode}" shift 2 ;; --scope) SCOPE="${2:?missing scope}" shift 2 ;; --reports-dir) REPORTS_DIR="${2:?missing reports dir}" shift 2 ;; --rebuild) REBUILD=1 shift ;; --update) UPDATE=1 REBUILD=1 shift ;; --build-only) BUILD_ONLY=1 shift ;; -h|--help) cat <<'EOF' Usage: tools/checks/security-audit/run.sh [--mode quick|ci|full] [--scope current|govoplan] [--reports-dir DIR] [--rebuild] [--update] [--build-only] [--strict|--report-only] Builds or reuses the containerized GovOPlaN audit toolbox, then runs tools/checks/check-security-audit.sh inside it. Image caching: The image is tagged by a fingerprint of tools/checks/security-audit/Dockerfile and requirements-audit.txt. Existing matching images are reused. Flags: --rebuild Rebuild the fingerprinted image using Docker cache. --update Rebuild with --pull --no-cache to refresh base images/tools. --build-only Build or refresh the image, then exit without running an audit. EOF exit 0 ;; --strict|--report-only) SCRIPT_ARGS+=("$1") shift ;; *) SCRIPT_ARGS+=("$1") shift ;; esac done REBUILD="${REBUILD:-0}" UPDATE="${UPDATE:-0}" BUILD_ONLY="${BUILD_ONLY:-0}" declare -a DOCKER_CLI=() DOCKER_LOCATION="" if command -v docker >/dev/null 2>&1; then DOCKER_CLI=(docker) DOCKER_LOCATION="current environment" elif command -v flatpak-spawn >/dev/null 2>&1 \ && flatpak-spawn --host sh -lc 'command -v docker >/dev/null 2>&1'; then DOCKER_CLI=(flatpak-spawn --host docker) DOCKER_LOCATION="Flatpak host" else echo "Docker is not available in this environment or through a Flatpak host bridge." >&2 echo "Install Docker, expose its CLI/socket, or run this script from the host." >&2 exit 1 fi if ! DOCKER_VERSION_OUTPUT="$("${DOCKER_CLI[@]}" version --format '{{.Server.Version}}' 2>&1)"; then echo "Docker was found on the $DOCKER_LOCATION, but its daemon is not accessible:" >&2 printf ' %s\n' "$DOCKER_VERSION_OUTPUT" >&2 if [[ "$DOCKER_LOCATION" == "Flatpak host" && "$DOCKER_VERSION_OUTPUT" == *"permission denied"* ]]; then echo "Grant the host account access to the Docker socket, then fully log out and back in." >&2 echo "On a conventional rootful Docker installation: sudo usermod -aG docker \"$(id -un)\"" >&2 echo "Membership in the docker group is root-equivalent; use rootless Docker instead where that is preferred." >&2 fi exit 1 fi IMAGE_NAME="${IMAGE##*/}" if [[ "$IMAGE_NAME" == *:* ]]; then IMAGE_REPOSITORY="${IMAGE%:*}" IMAGE_ALIAS="$IMAGE" else IMAGE_REPOSITORY="$IMAGE" IMAGE_ALIAS="$IMAGE:local" fi IMAGE_FINGERPRINT="$( { sha256sum "$ROOT/tools/checks/security-audit/Dockerfile" sha256sum "$ROOT/requirements-audit.txt" sha256sum "$ROOT/tools/checks/security-audit/semgrep-govoplan.yml" sha256sum "$ROOT/tools/checks/check-version-alignment.py" } | sha256sum | cut -c1-16 )" FINGERPRINT_IMAGE="${SECURITY_AUDIT_FINGERPRINT_IMAGE:-$IMAGE_REPOSITORY:$IMAGE_FINGERPRINT}" build_image() { local -a build_args=(-t "$FINGERPRINT_IMAGE" -t "$IMAGE_ALIAS" -f "$ROOT/tools/checks/security-audit/Dockerfile") if [[ "$UPDATE" == "1" ]]; then build_args=(--pull --no-cache "${build_args[@]}") fi "${DOCKER_CLI[@]}" build "${build_args[@]}" "$ROOT" } if [[ "$REBUILD" == "1" ]] || ! "${DOCKER_CLI[@]}" image inspect "$FINGERPRINT_IMAGE" >/dev/null 2>&1; then echo "Building security audit toolbox image: $FINGERPRINT_IMAGE" build_image else echo "Reusing security audit toolbox image: $FINGERPRINT_IMAGE" if ! "${DOCKER_CLI[@]}" image inspect "$IMAGE_ALIAS" >/dev/null 2>&1; then "${DOCKER_CLI[@]}" tag "$FINGERPRINT_IMAGE" "$IMAGE_ALIAS" fi fi if [[ "$BUILD_ONLY" == "1" ]]; then "${DOCKER_CLI[@]}" image inspect "$FINGERPRINT_IMAGE" --format 'Built audit toolbox image {{.RepoTags}} {{.Id}}' exit 0 fi if [[ "$SCOPE" == "govoplan" ]]; then MOUNT_ROOT="$(dirname "$ROOT")" WORKDIR="/workspace/$(basename "$ROOT")" EXTRA_ENV=(-e "GOVOPLAN_REPOS_ROOT=/workspace") else MOUNT_ROOT="$ROOT" WORKDIR="/workspace" EXTRA_ENV=() fi "${DOCKER_CLI[@]}" run --rm \ --user "$(id -u):$(id -g)" \ -e HOME=/tmp/security-audit-home \ -e SECURITY_AUDIT_SCOPE="$SCOPE" \ -e SECURITY_AUDIT_MODE="$MODE" \ -e SECURITY_AUDIT_REPORTS_DIR="$REPORTS_DIR" \ -e SECURITY_AUDIT_FAIL_ON_FINDINGS="${SECURITY_AUDIT_FAIL_ON_FINDINGS:-0}" \ -e SECURITY_AUDIT_REQUIRE_TOOLS="${SECURITY_AUDIT_REQUIRE_TOOLS:-0}" \ -e SECURITY_AUDIT_TOOLBOX_FINGERPRINT="$IMAGE_FINGERPRINT" \ "${EXTRA_ENV[@]}" \ -v "$MOUNT_ROOT:/workspace" \ -w "$WORKDIR" \ "$FINGERPRINT_IMAGE" \ bash tools/checks/check-security-audit.sh --mode "$MODE" --scope "$SCOPE" --reports-dir "$REPORTS_DIR" "${SCRIPT_ARGS[@]}"