Demande de Benjamin, 2026-10-08 : « il faut que le scan se fasse au build pour empêcher le build ». Jusqu'ici, le scan ne tournait que dans la publication, après la fusion : une PR n'était jamais scannée, et une dépendance vulnérable passait la revue. - Toute référence : construction + scan bloquant. Seules develop et vX.Y.Z publient ; une PR, une autre branche ou main s'arrêtent après le scan, sous un tag local « controle-<sha> » jamais poussé. - Identifiants du registre facultatifs : lus et exigés seulement si la référence publie. Une PR ne se connecte pas au registre. - REGISTRY_AUTH_FILE n'est exporté que pour publier : pointé sur un fichier absent, il faisait échouer buildah jusque dans l'export local vers le scan (constaté au banc, « credential file is not accessible »). - Sortie « publie » ; « tags » vide quand rien n'est publié. Banc, 2026-10-08, les deux étapes extraites du YAML et enchaînées : A. PR, sans identifiants, saine → code 0, registre vide B. PR, sans identifiants, vulnérable → code 1, image refusée C. develop, identifiants → :dev et :<sha> publiés D. develop, sans identifiants → code 1, erreur explicite E. main → code 0, rien publié Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VyxLNGHojdXeRMQgWFgzRV
252 lines
12 KiB
YAML
252 lines
12 KiB
YAML
name: 'Build, scan & push'
|
|
description: >-
|
|
Construit une image avec buildah, sans démon ni socket, la passe au scan de
|
|
vulnérabilités BLOQUANT, et — sur develop ou un tag vX.Y.Z seulement — la
|
|
publie dans le registre de Gitea (ADR 0002 et 0003 de RF_CICD). Ne déploie
|
|
rien : la filière dev tire l'image, la production se déploie sur ordre.
|
|
|
|
# Sur TOUTE référence — PR comprise — l'image est construite et scannée : une
|
|
# CVE HIGH ou CRITICAL corrigeable fait échouer le job, avant toute revue de
|
|
# fusion (RF_CICD#7, demande de Benjamin du 2026-10-08 : « le scan doit se faire
|
|
# au build pour empêcher le build »). Ensuite seulement, selon la référence :
|
|
#
|
|
# poussée sur develop → publie :dev et :<sha court, 12 caractères>
|
|
# tag vX.Y.Z → publie :vX.Y.Z, orthographié comme le tag git
|
|
# toute autre référence → ne publie RIEN : construction et scan seulement
|
|
#
|
|
# Une PR, une branche de fonctionnalité, une poussée sur main (ADR 0002) ne
|
|
# publient donc jamais : elles ne peuvent ni écraser :dev, ni produire une image
|
|
# sous un nom que personne n'a choisi. Et elles n'ont pas besoin des
|
|
# identifiants du registre — l'action ne les demande que pour publier.
|
|
#
|
|
# Il n'y a volontairement aucun tag flottant de production (pas de :prod, pas
|
|
# de :latest), et rien n'est élagué.
|
|
|
|
inputs:
|
|
image:
|
|
description: >-
|
|
Nom complet de l'image, sans tag — doit commencer par le registre, ex.
|
|
gitea.raggaroth-factory.fr/raggaroth-factory/scrum-board-backend
|
|
required: true
|
|
dockerfile:
|
|
description: Chemin du Dockerfile/Containerfile à construire.
|
|
required: true
|
|
context:
|
|
description: Contexte de construction.
|
|
required: false
|
|
default: '.'
|
|
registry:
|
|
description: Hôte du registre.
|
|
required: false
|
|
default: gitea.raggaroth-factory.fr
|
|
registry_user:
|
|
description: Compte qui publie dans le registre. Exigé seulement quand la référence publie.
|
|
required: false
|
|
default: ''
|
|
registry_password:
|
|
description: >-
|
|
Jeton du compte qui publie. Par un secret Gitea Actions, jamais en clair.
|
|
Exigé seulement quand la référence publie.
|
|
required: false
|
|
default: ''
|
|
|
|
outputs:
|
|
tags:
|
|
description: Les tags publiés, séparés par des espaces (vide si rien n'est publié).
|
|
value: ${{ steps.publish.outputs.tags }}
|
|
publie:
|
|
description: « true » si l'image a été publiée, « false » si elle a seulement été construite et scannée.
|
|
value: ${{ steps.tags.outputs.publier }}
|
|
digest:
|
|
description: L'empreinte (sha256:…) de l'image publiée — c'est elle que la filière dev compare.
|
|
value: ${{ steps.publish.outputs.digest }}
|
|
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
# Les entrées passent TOUJOURS par l'environnement, jamais par « ${{ }} »
|
|
# dans le script : une expression est recopiée telle quelle dans le texte
|
|
# bash avant son exécution, et une valeur contenant « "; … » deviendrait du
|
|
# code. L'ancienne version interpolait ainsi le mot de passe du registre
|
|
# dans un « echo ».
|
|
- name: Tags de la filière
|
|
id: tags
|
|
shell: bash
|
|
env:
|
|
INPUT_IMAGE: ${{ inputs.image }}
|
|
INPUT_REGISTRY: ${{ inputs.registry }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
case "$INPUT_IMAGE" in
|
|
"$INPUT_REGISTRY"/*) ;;
|
|
*) echo "::error::l'image « $INPUT_IMAGE » n'est pas dans le registre « $INPUT_REGISTRY » : les identifiants seraient envoyés au mauvais hôte, ou l'image poussée sans eux"
|
|
exit 1 ;;
|
|
esac
|
|
|
|
# GITHUB_REF et non GITHUB_REF_NAME : seul le premier distingue une
|
|
# branche « v1 » d'un tag « v1 ».
|
|
PUBLIER=true
|
|
case "$GITHUB_REF" in
|
|
refs/heads/develop)
|
|
TAGS="dev ${GITHUB_SHA:0:12}" ;;
|
|
refs/tags/v*)
|
|
VERSION=${GITHUB_REF#refs/tags/}
|
|
# Un tag d'image n'accepte que [A-Za-z0-9_.-] : on exige une
|
|
# version lisible plutôt que de corriger en silence ce qui a été
|
|
# posé, pour que le tag git et le tag d'image restent identiques.
|
|
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
|
|
echo "::error::tag « $VERSION » : attendu vX.Y.Z (ou vX.Y.Z-rc.1)"
|
|
exit 1
|
|
fi
|
|
TAGS="$VERSION" ;;
|
|
*)
|
|
# Un tag LOCAL, jamais poussé : il ne sert qu'à nommer l'image le
|
|
# temps de la scanner.
|
|
TAGS="controle-${GITHUB_SHA:0:12}"; PUBLIER=false ;;
|
|
esac
|
|
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
|
|
echo "publier=$PUBLIER" >> "$GITHUB_OUTPUT"
|
|
if [ "$PUBLIER" = true ]; then
|
|
echo "« $GITHUB_REF » : construction, scan, puis publication de : $TAGS"
|
|
else
|
|
echo "« $GITHUB_REF » : construction et scan seulement — cette référence ne publie rien (ADR 0002)"
|
|
fi
|
|
|
|
- name: Construire, scanner, publier
|
|
id: publish
|
|
shell: bash
|
|
env:
|
|
INPUT_IMAGE: ${{ inputs.image }}
|
|
INPUT_DOCKERFILE: ${{ inputs.dockerfile }}
|
|
INPUT_CONTEXT: ${{ inputs.context }}
|
|
INPUT_REGISTRY: ${{ inputs.registry }}
|
|
INPUT_TAGS: ${{ steps.tags.outputs.tags }}
|
|
PUBLIER: ${{ steps.tags.outputs.publier }}
|
|
REGISTRY_USER: ${{ inputs.registry_user }}
|
|
REGISTRY_PASSWORD: ${{ inputs.registry_password }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
for OUTIL in buildah trivy; do
|
|
if ! command -v "$OUTIL" >/dev/null; then
|
|
echo "::error::$OUTIL absent de l'image de job : le runner doit exécuter ce job dans une image qui le porte (voir README de RF_ACTIONS)"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# « --storage-driver vfs » sur CHAQUE appel, et pas par la variable
|
|
# STORAGE_DRIVER : constaté le 2026-10-07 sur buildah 1.33.7, avec la
|
|
# seule variable, un « build » a laissé un stockage overlay que l'appel
|
|
# suivant a refusé d'ouvrir (« 'overlay' is not supported over
|
|
# overlayfs »). Avec le drapeau explicite, build, images et push
|
|
# passent. vfs est le seul pilote qui tienne
|
|
# dans un conteneur de job sans privilège ni /dev/fuse (ADR 0003).
|
|
b() { buildah --storage-driver vfs "$@"; }
|
|
|
|
# Fichier d'authentification explicite, dans le répertoire temporaire du
|
|
# job, et effacé en sortie quoi qu'il arrive. Le conteneur est jeté à la
|
|
# fin du job, mais un jeton n'a pas à lui survivre d'une étape.
|
|
FICHIER_AUTH="${RUNNER_TEMP:-/tmp}/build-push-auth.json"
|
|
IMAGE_OCI="${RUNNER_TEMP:-/tmp}/build-push-image-oci"
|
|
trap 'rm -rf -- "$FICHIER_AUTH" "$IMAGE_OCI"' EXIT
|
|
|
|
# Connexion au registre SEULEMENT si la référence publie : une PR n'a
|
|
# pas à manipuler le jeton de publication, ni même à le recevoir.
|
|
# printf est une primitive du shell : le jeton ne passe dans l'argv
|
|
# d'aucun processus, donc pas dans /proc/<pid>/cmdline.
|
|
# REGISTRY_AUTH_FILE n'est exporté QUE si l'on publie : pointé sur un
|
|
# fichier inexistant, il fait échouer buildah jusque dans l'export local
|
|
# de l'image vers le scan (constaté le 2026-10-08, « credential file is
|
|
# not accessible »).
|
|
if [ "$PUBLIER" = true ]; then
|
|
export REGISTRY_AUTH_FILE="$FICHIER_AUTH"
|
|
if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_PASSWORD" ]; then
|
|
echo "::error::cette référence publie, mais registry_user / registry_password sont vides — secrets REGISTRY_USER / REGISTRY_PASSWORD du dépôt ?"
|
|
exit 1
|
|
fi
|
|
printf '%s' "$REGISTRY_PASSWORD" \
|
|
| b login --username "$REGISTRY_USER" --password-stdin "$INPUT_REGISTRY"
|
|
fi
|
|
|
|
TAG_ARGS=()
|
|
for TAG in $INPUT_TAGS; do TAG_ARGS+=(--tag "$INPUT_IMAGE:$TAG"); done
|
|
|
|
# --isolation chroot : pas d'espace de noms imbriqué à créer pour les
|
|
# RUN, c'est ce qui permet de construire sans privilège (ADR 0003).
|
|
# Les étiquettes OCI disent de quel commit vient l'image : c'est ce que
|
|
# lira un « deployer.sh --constat » pour dire ce qui tourne.
|
|
b build \
|
|
--isolation chroot \
|
|
--file "$INPUT_DOCKERFILE" \
|
|
--label "org.opencontainers.image.revision=$GITHUB_SHA" \
|
|
--label "org.opencontainers.image.source=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \
|
|
"${TAG_ARGS[@]}" \
|
|
"$INPUT_CONTEXT"
|
|
|
|
# ── Scan de vulnérabilités, BLOQUANT, avant toute publication ──────────
|
|
# Seuil décidé le 2026-10-07 (RF_CICD#7) : une CVE HIGH ou CRITICAL
|
|
# pour laquelle un correctif est publié fait échouer le job, comme un
|
|
# test rouge. Le reste — dont les CVE sans correctif, contre lesquelles
|
|
# personne ne peut rien — est rapporté sans bloquer. Le seuil n'est PAS
|
|
# une entrée de l'action : un dépôt ne peut pas l'abaisser pour lui seul.
|
|
# Une CVE acceptée sciemment va dans .trivyignore.yaml, à la racine du
|
|
# dépôt appelant, avec une justification et une date d'expiration.
|
|
#
|
|
# Trivy lit l'image construite sous forme de RÉPERTOIRE OCI : il n'y a
|
|
# pas de démon à interroger, et rien n'est publié avant son verdict.
|
|
# Un répertoire et non une archive : « --input » n'accepte que l'archive
|
|
# au format Docker (manifest.json) ou le répertoire OCI (index.json) —
|
|
# une archive OCI est refusée (constaté, trivy 0.75.0).
|
|
PREMIER_TAG=${INPUT_TAGS%% *}
|
|
b push "$INPUT_IMAGE:$PREMIER_TAG" "oci:$IMAGE_OCI"
|
|
|
|
IGNORE=()
|
|
FICHIER_IGNORE="${GITHUB_WORKSPACE:-.}/.trivyignore.yaml"
|
|
if [ -f "$FICHIER_IGNORE" ]; then
|
|
# Chaque exception porte un « statement » (pourquoi) et un
|
|
# « expired_at » (jusqu'à quand) : à l'échéance, Trivy cesse de
|
|
# l'appliquer et la CVE bloque de nouveau. Une exception sans date
|
|
# serait une CVE acceptée pour toujours, sans que personne l'ait dit.
|
|
if ! awk '
|
|
/^[[:space:]]*-[[:space:]]*id:/ { n++; id[n]=$0 }
|
|
/^[[:space:]]*statement:/ { s[n]=1 }
|
|
/^[[:space:]]*expired_at:/ { e[n]=1 }
|
|
END { ok=1; for (i=1;i<=n;i++) if (!s[i] || !e[i]) { print " incomplète :" id[i]; ok=0 } exit !ok }
|
|
' "$FICHIER_IGNORE"; then
|
|
echo "::error::.trivyignore.yaml : chaque exception doit porter « statement » et « expired_at »"
|
|
exit 1
|
|
fi
|
|
IGNORE=(--ignorefile "$FICHIER_IGNORE")
|
|
echo "Exceptions lues dans .trivyignore.yaml"
|
|
fi
|
|
|
|
# Rapport complet des HIGH/CRITICAL, corrigeables ou non — pour voir,
|
|
# pas pour juger. Puis le verdict, sur les seules corrigeables.
|
|
echo "::group::Vulnérabilités HIGH et CRITICAL, corrigeables ou non (rapport)"
|
|
trivy image --quiet --scanners vuln --severity HIGH,CRITICAL \
|
|
"${IGNORE[@]}" --format table --exit-code 0 --input "$IMAGE_OCI"
|
|
echo "::endgroup::"
|
|
if ! trivy image --quiet --skip-db-update --scanners vuln --severity HIGH,CRITICAL \
|
|
--ignore-unfixed "${IGNORE[@]}" --format table --exit-code 1 --input "$IMAGE_OCI"; then
|
|
echo "::error::CVE HIGH ou CRITICAL corrigeable dans l'image : elle est REFUSÉE, et rien n'est publié. Corriger (monter l'empreinte de la base, ou la version du paquet), ou l'accepter sciemment dans .trivyignore.yaml, avec justification et date d'expiration."
|
|
exit 1
|
|
fi
|
|
echo "Scan : aucune CVE HIGH ou CRITICAL corrigeable — image acceptée"
|
|
|
|
if [ "$PUBLIER" != true ]; then
|
|
echo "tags=" >> "$GITHUB_OUTPUT"
|
|
echo "Construction et scan réussis — « $GITHUB_REF » ne publie rien."
|
|
exit 0
|
|
fi
|
|
|
|
DIGEST_FILE="${RUNNER_TEMP:-/tmp}/build-push-digest"
|
|
for TAG in $INPUT_TAGS; do
|
|
b push --digestfile "$DIGEST_FILE" "$INPUT_IMAGE:$TAG" "docker://$INPUT_IMAGE:$TAG"
|
|
echo "Publié : $INPUT_IMAGE:$TAG@$(cat "$DIGEST_FILE")"
|
|
done
|
|
echo "digest=$(cat "$DIGEST_FILE")" >> "$GITHUB_OUTPUT"
|
|
echo "tags=$INPUT_TAGS" >> "$GITHUB_OUTPUT"
|
|
|
|
b logout "$INPUT_REGISTRY" || true
|