wip: contenido inicial + test de reusable workflow / composite action
This commit is contained in:
1 parent
e9fe7d0567
commit
262c0f271a
22 files changed
+1480
No files matched your search
@@ -0,0 +1,68 @@
|
||||
name: "Cosign — firmar y verificar por digest"
|
||||
description: >-
|
||||
Firma la imagen por digest (no por tag) y verifica esa firma como
|
||||
smoke test, en el mismo job. --tlog-upload=false: sin publicar en el
|
||||
transparency log público de Sigstore (registry privado, no tiene
|
||||
sentido anunciar públicamente qué se firmó — ver
|
||||
docs/devsecops/cosign.md en apps-registry para el razonamiento
|
||||
completo de esta decisión).
|
||||
IMPORTANTE: un composite action no puede leer `secrets` directamente
|
||||
— el workflow que lo llama tiene que pasarlos como `with:` (ver
|
||||
ejemplo en el README de este snippet).
|
||||
|
||||
inputs:
|
||||
image_name:
|
||||
description: "Nombre de la imagen sin tag/digest (ej. gitea.cruzcloud.net/devops/mi-app)."
|
||||
required: true
|
||||
digest:
|
||||
description: "Digest sha256:... a firmar (el output del paso que hace docker push)."
|
||||
required: true
|
||||
public_key_path:
|
||||
description: "Ruta al cosign.pub commiteado en el repo (para el smoke test de verificación)."
|
||||
required: true
|
||||
cosign_private_key:
|
||||
description: "Contenido de la llave privada de Cosign (pasar como \\${{ secrets.COSIGN_PRIVATE_KEY }} desde el caller)."
|
||||
required: true
|
||||
cosign_password:
|
||||
description: "Password de la llave privada (pasar como \\${{ secrets.COSIGN_PASSWORD }} desde el caller)."
|
||||
required: true
|
||||
cosign_version:
|
||||
description: "Versión de Cosign a instalar."
|
||||
required: false
|
||||
default: "3.1.3"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Instalar Cosign
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sSfL \
|
||||
"https://github.com/sigstore/cosign/releases/download/v${{ inputs.cosign_version }}/cosign-linux-amd64" \
|
||||
-o /tmp/cosign
|
||||
chmod +x /tmp/cosign
|
||||
/tmp/cosign version
|
||||
|
||||
- name: Firmar imagen (Cosign) — por digest
|
||||
shell: bash
|
||||
env:
|
||||
COSIGN_PRIVATE_KEY: ${{ inputs.cosign_private_key }}
|
||||
COSIGN_PASSWORD: ${{ inputs.cosign_password }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
/tmp/cosign sign \
|
||||
--key env://COSIGN_PRIVATE_KEY \
|
||||
--use-signing-config=false \
|
||||
--tlog-upload=false \
|
||||
--yes \
|
||||
"${{ inputs.image_name }}@${{ inputs.digest }}"
|
||||
|
||||
- name: Verificar firma (smoke test)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
/tmp/cosign verify \
|
||||
--key "${{ inputs.public_key_path }}" \
|
||||
--insecure-ignore-tlog=true \
|
||||
"${{ inputs.image_name }}@${{ inputs.digest }}"
|
||||
@@ -0,0 +1,52 @@
|
||||
name: "Gitleaks — escaneo de secretos"
|
||||
description: >-
|
||||
Escanea el árbol de archivos (no el historial de git) buscando
|
||||
secretos commiteados. Bloquea (exit 1) si encuentra algo. Extraído
|
||||
literal de los 3 pipelines reales del lab (frontend, commerce-backend,
|
||||
docs-portal) — idéntico en los tres salvo el "source".
|
||||
|
||||
inputs:
|
||||
source:
|
||||
description: "Directorio a escanear (ej. workloads/mi-app)."
|
||||
required: true
|
||||
version:
|
||||
description: "Versión de Gitleaks a instalar."
|
||||
required: false
|
||||
default: "8.21.2"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Instalar Gitleaks
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sSfL \
|
||||
"https://github.com/gitleaks/gitleaks/releases/download/v${{ inputs.version }}/gitleaks_${{ inputs.version }}_linux_x64.tar.gz" \
|
||||
-o /tmp/gitleaks.tar.gz
|
||||
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
|
||||
chmod +x /tmp/gitleaks
|
||||
/tmp/gitleaks version
|
||||
|
||||
# --no-git: escanea el árbol de archivos del checkout, no el log de
|
||||
# commits. Un scan histórico completo se corre aparte, manual, no
|
||||
# en cada push/PR (ver README de este snippet).
|
||||
- name: Escanear secretos en el árbol de archivos
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
/tmp/gitleaks detect \
|
||||
--source="${{ inputs.source }}" \
|
||||
--no-git \
|
||||
--redact \
|
||||
--report-format=json \
|
||||
--report-path=gitleaks-report.json \
|
||||
--exit-code=1
|
||||
|
||||
- name: Publicar reporte de Gitleaks
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gitleaks-report
|
||||
path: gitleaks-report.json
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,82 @@
|
||||
name: "Semgrep — SAST"
|
||||
description: >-
|
||||
Escaneo estático de código, modo auditoría (no bloquea) por default —
|
||||
mismo criterio que los 3 pipelines reales: primera vuelta para
|
||||
revisar hallazgos en conjunto antes de decidir qué reglas deberían
|
||||
bloquear. Corre nativo en un venv, no como contenedor aparte — el
|
||||
runner de Gitea ya ejecuta cada job dentro de un contenedor propio
|
||||
(sibling containers hablando con el Docker del host, no
|
||||
Docker-in-Docker), así que un "docker run -v" desde acá monta rutas
|
||||
que no existen en el host real y falla con "read-only file system".
|
||||
|
||||
inputs:
|
||||
target:
|
||||
description: "Directorio a escanear."
|
||||
required: true
|
||||
configs:
|
||||
description: >-
|
||||
Rulesets de Semgrep separados por coma, ej.
|
||||
"p/typescript,p/react,p/nextjs,p/security-audit". Elegí el
|
||||
combo según tu stack real — no copies el mismo set para todo
|
||||
(ver README de este snippet, tabla de qué usa cada arquetipo).
|
||||
required: true
|
||||
semgrep_version:
|
||||
description: "Versión de Semgrep a instalar."
|
||||
required: false
|
||||
default: "1.173.0"
|
||||
fail_on_findings:
|
||||
description: "'true' para bloquear el pipeline si hay hallazgos. Default 'false' (auditoría)."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Instalar Semgrep
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 -m venv /tmp/semgrep-venv
|
||||
/tmp/semgrep-venv/bin/pip install --quiet "semgrep==${{ inputs.semgrep_version }}"
|
||||
/tmp/semgrep-venv/bin/semgrep --version
|
||||
|
||||
- name: Escaneo SAST (Semgrep)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_ARGS=()
|
||||
IFS=',' read -ra CONFIGS <<< "${{ inputs.configs }}"
|
||||
for cfg in "${CONFIGS[@]}"; do
|
||||
CONFIG_ARGS+=(--config="${cfg}")
|
||||
done
|
||||
|
||||
ERROR_FLAG=()
|
||||
if [ "${{ inputs.fail_on_findings }}" = "true" ]; then
|
||||
ERROR_FLAG=(--error)
|
||||
fi
|
||||
|
||||
/tmp/semgrep-venv/bin/semgrep scan \
|
||||
"${CONFIG_ARGS[@]}" \
|
||||
"${ERROR_FLAG[@]}" \
|
||||
--json \
|
||||
--output=semgrep-report.json \
|
||||
"${{ inputs.target }}"
|
||||
|
||||
echo "=== Resumen Semgrep ==="
|
||||
/tmp/semgrep-venv/bin/python -c "
|
||||
import json
|
||||
data = json.load(open('semgrep-report.json'))
|
||||
results = data.get('results', [])
|
||||
print(f'Hallazgos: {len(results)}')
|
||||
for r in results:
|
||||
print(f\" [{r['extra']['severity']}] {r['check_id']} - {r['path']}:{r['start']['line']}\")
|
||||
"
|
||||
|
||||
- name: Publicar reporte de Semgrep
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: semgrep-report
|
||||
path: semgrep-report.json
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,50 @@
|
||||
name: "Syft — SBOM"
|
||||
description: >-
|
||||
Genera el SBOM (CycloneDX + SPDX) de la imagen que ya pasó el gate de
|
||||
CRITICAL de Trivy — describe exactamente qué paquetes (y en qué
|
||||
versión) quedaron adentro. CycloneDX es el formato más usado por
|
||||
herramientas de consulta/alertas de CVEs nuevas.
|
||||
|
||||
inputs:
|
||||
image_ref:
|
||||
description: "Referencia completa imagen:tag a describir."
|
||||
required: true
|
||||
artifact_suffix:
|
||||
description: "Sufijo para el nombre del artifact publicado (ej. la versión)."
|
||||
required: true
|
||||
syft_version:
|
||||
description: "Versión de Syft a instalar."
|
||||
required: false
|
||||
default: "1.51.0"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Instalar Syft
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sSfL \
|
||||
"https://github.com/anchore/syft/releases/download/v${{ inputs.syft_version }}/syft_${{ inputs.syft_version }}_linux_amd64.tar.gz" \
|
||||
-o /tmp/syft.tar.gz
|
||||
tar -xzf /tmp/syft.tar.gz -C /tmp syft
|
||||
chmod +x /tmp/syft
|
||||
/tmp/syft version
|
||||
|
||||
- name: Generar SBOM (Syft)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
/tmp/syft "${{ inputs.image_ref }}" \
|
||||
-o cyclonedx-json=sbom.cdx.json \
|
||||
-o spdx-json=sbom.spdx.json
|
||||
|
||||
- name: Publicar SBOM
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: sbom-${{ inputs.artifact_suffix }}
|
||||
path: |
|
||||
sbom.cdx.json
|
||||
sbom.spdx.json
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,47 @@
|
||||
name: "Trivy IaC — escaneo de manifiestos Kubernetes"
|
||||
description: >-
|
||||
Escanea manifiestos Kubernetes buscando misconfiguraciones (root,
|
||||
falta de resource limits, falta de probes, etc). Informativo por
|
||||
default (no bloquea) — mismo criterio que los 3 pipelines reales del
|
||||
lab mientras se revisan los hallazgos antes de decidir qué debería
|
||||
bloquear.
|
||||
|
||||
inputs:
|
||||
target:
|
||||
description: "Archivo o directorio de manifiestos a escanear."
|
||||
required: true
|
||||
trivy_version:
|
||||
description: "Versión de Trivy a instalar (compartida con trivy-image-step si corrés ambos en el mismo job)."
|
||||
required: false
|
||||
default: "0.74.0"
|
||||
fail_on_findings:
|
||||
description: "'true' para bloquear el pipeline si hay hallazgos. Default 'false' (auditoría)."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Instalar Trivy
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sSfL \
|
||||
"https://github.com/aquasecurity/trivy/releases/download/v${{ inputs.trivy_version }}/trivy_${{ inputs.trivy_version }}_Linux-64bit.tar.gz" \
|
||||
-o /tmp/trivy.tar.gz
|
||||
tar -xzf /tmp/trivy.tar.gz -C /tmp trivy
|
||||
chmod +x /tmp/trivy
|
||||
/tmp/trivy version
|
||||
|
||||
- name: Escanear manifiestos Kubernetes (Trivy IaC)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
EXIT_CODE=0
|
||||
if [ "${{ inputs.fail_on_findings }}" = "true" ]; then
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
/tmp/trivy config \
|
||||
--severity CRITICAL,HIGH,MEDIUM \
|
||||
--exit-code "${EXIT_CODE}" \
|
||||
"${{ inputs.target }}"
|
||||
@@ -0,0 +1,65 @@
|
||||
name: "Trivy imagen — CVE de la imagen construida"
|
||||
description: >-
|
||||
Escanea la imagen Docker ya construida y cargada localmente (load:
|
||||
true en el build, no hace falta push todavía). CRITICAL bloquea el
|
||||
pipeline; HIGH es informativo. --timeout 15m0s siempre — el default
|
||||
de Trivy (5m) no alcanza bajo carga real del host (visto en vivo en
|
||||
commerce-backend, "context deadline exceeded" a los 4m52s).
|
||||
|
||||
inputs:
|
||||
image_ref:
|
||||
description: "Referencia completa imagen:tag a escanear (ej. \\${{ env.IMAGE_NAME }}:\\${{ steps.vars.outputs.VERSION }})."
|
||||
required: true
|
||||
trivy_version:
|
||||
description: "Versión de Trivy a instalar."
|
||||
required: false
|
||||
default: "0.74.0"
|
||||
ignorefile:
|
||||
description: >-
|
||||
Ruta a un .trivyignore con excepciones documentadas (opcional).
|
||||
Cada excepción debe tener justificación y fecha en el propio
|
||||
archivo — no es un mecanismo para silenciar sin revisar. Ver
|
||||
devsecops-snippets/trivy-image-step/README.md.
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Instalar Trivy
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sSfL \
|
||||
"https://github.com/aquasecurity/trivy/releases/download/v${{ inputs.trivy_version }}/trivy_${{ inputs.trivy_version }}_Linux-64bit.tar.gz" \
|
||||
-o /tmp/trivy.tar.gz
|
||||
tar -xzf /tmp/trivy.tar.gz -C /tmp trivy
|
||||
chmod +x /tmp/trivy
|
||||
/tmp/trivy version
|
||||
|
||||
- name: Escanear imagen (Trivy) — CRITICAL bloquea
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
IGNOREFILE_ARGS=()
|
||||
if [ -n "${{ inputs.ignorefile }}" ]; then
|
||||
IGNOREFILE_ARGS=(--ignorefile "${{ inputs.ignorefile }}")
|
||||
fi
|
||||
/tmp/trivy image \
|
||||
--severity CRITICAL \
|
||||
--exit-code 1 \
|
||||
--ignore-unfixed \
|
||||
--timeout 15m0s \
|
||||
"${IGNOREFILE_ARGS[@]}" \
|
||||
"${{ inputs.image_ref }}"
|
||||
|
||||
- name: Escanear imagen (Trivy) — HIGH informativo
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
/tmp/trivy image \
|
||||
--severity HIGH \
|
||||
--exit-code 0 \
|
||||
--ignore-unfixed \
|
||||
--timeout 15m0s \
|
||||
"${{ inputs.image_ref }}"
|
||||
Reference in new issue
Block a user