Compare commits

..
Author SHA1 Message Date
devops 8b49ac2bc5 feat(devsecops): replicar pipeline de seguridad a commerce-backend
Agrega a build-medusa.yaml las mismas 6 etapas ya validadas en
build.yaml (run 134, frontend): Gitleaks, Trivy IaC, Semgrep (SAST),
Trivy imagen, Syft (SBOM) y Cosign (firma + verificación).

Ruleset de Semgrep adaptado: p/typescript + p/security-audit +
p/owasp-top-ten, sin p/react/p/nextjs -- commerce-backend es la API de
Medusa v2 (Node/TypeScript), no una app con SSR. Mismos umbrales que
el frontend: Trivy bloquea en CRITICAL, Semgrep en modo auditoría
(no bloquea todavía).

Reutiliza el mismo par de llaves Cosign del frontend (secrets ya
existentes a nivel de repo); se agrega workloads/commerce-backend/cosign.pub
con la misma llave pública para que la verificación quede local a
este workflow.
2026-08-15 12:46:45 -05:00
3 changed files with 271 additions and 266 deletions
+265 -8
View File
@@ -1,5 +1,9 @@
name: Build and Push Medusa
# Sin anchors/aliases de YAML (&x / *x) a propósito -- el parser de
# workflows de Gitea Actions no los resuelve en el bloque "on:" y
# descarta el archivo completo con "unknown on type". Las dos listas de
# paths quedan duplicadas literalmente (mismo criterio que build.yaml).
on:
push:
branches:
@@ -7,17 +11,76 @@ on:
paths:
- 'workloads/commerce-backend/**'
- '.gitea/workflows/build-medusa.yaml'
pull_request:
branches:
- main
paths:
- 'workloads/commerce-backend/**'
- '.gitea/workflows/build-medusa.yaml'
permissions:
contents: write
packages: write
jobs:
# Corre en push y en pull_request, siempre antes que build. Mismo
# criterio que el frontend: si hay un secreto commiteado, el job build
# nunca arranca (needs: gitleaks).
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Instalar Gitleaks
shell: bash
run: |
set -euo pipefail
GITLEAKS_VERSION="8.21.2"
curl -sSfL \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_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
- name: Escanear secretos en el árbol de archivos
shell: bash
run: |
set -euo pipefail
/tmp/gitleaks detect \
--source=workloads/commerce-backend \
--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
build:
name: Construir y publicar Medusa
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
APP_DIR: workloads/commerce-backend
MANIFEST_FILE: workloads/ecommerce/commerce/medusa.yaml
IMAGE_NAME: gitea.cruzcloud.net/devops/ecommerce-medusa
steps:
- name: Checkout del código
uses: actions/checkout@v3
@@ -48,6 +111,82 @@ jobs:
exit 1
}
- name: Instalar Trivy
shell: bash
run: |
set -euo pipefail
TRIVY_VERSION="0.74.0"
curl -sSfL \
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${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
# Escanea el manifiesto Kubernetes real de Medusa (no la imagen).
# Informativo por ahora -- mismo criterio que el frontend, se
# revisan los hallazgos en conjunto antes de decidir qué bloquea.
- name: Escanear manifiestos Kubernetes (Trivy IaC)
shell: bash
run: |
set -euo pipefail
/tmp/trivy config \
--severity CRITICAL,HIGH,MEDIUM \
--exit-code 0 \
"${MANIFEST_FILE}"
# Mismo motivo que en build.yaml: el runner ejecuta el job ya
# dentro de un contenedor propio que habla con el daemon Docker
# del host (sibling containers, no Docker-in-Docker) -- correr
# Semgrep como container aparte falla montando rutas que no
# existen en el host real. Se instala nativo en un venv.
- name: Instalar Semgrep
shell: bash
run: |
set -euo pipefail
python3 -m venv /tmp/semgrep-venv
/tmp/semgrep-venv/bin/pip install --quiet "semgrep==1.173.0"
/tmp/semgrep-venv/bin/semgrep --version
# Ruleset adaptado al stack real de este repo: commerce-backend es
# una API de Medusa v2 (Node.js/TypeScript sobre Express), no una
# app Next.js con SSR -- confirmado en package.json (sin
# dependencia de "next", react/react-dom solo llegan como
# dependencia transitiva del admin-sdk de Medusa, no hay código de
# UI propio en este directorio). Por eso se usa p/typescript en
# vez de p/typescript + p/react + p/nextjs del frontend.
# Modo auditoría, igual que el frontend: sin --error, primera
# vuelta para revisar hallazgos antes de decidir qué bloquea.
- name: Escaneo SAST (Semgrep) — modo auditoría, no bloquea
shell: bash
run: |
set -euo pipefail
/tmp/semgrep-venv/bin/semgrep scan \
--config=p/typescript \
--config=p/security-audit \
--config=p/owasp-top-ten \
--json \
--output=semgrep-report.json \
"${APP_DIR}"
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
- name: Login en Gitea Registry
uses: docker/login-action@v2
with:
@@ -56,15 +195,122 @@ jobs:
password: ${{ secrets.REGISTRY_PASSWORD }}
logout: true
- name: Construir y subir imagen
# push: false — igual que el frontend, la imagen se queda cargada
# en el daemon local para poder escanearla con Trivy antes de
# subirla al registry.
- name: Construir Imagen
uses: docker/build-push-action@v4
with:
context: workloads/commerce-backend/
file: workloads/commerce-backend/Dockerfile
push: true
context: ${{ env.APP_DIR }}/
file: ${{ env.APP_DIR }}/Dockerfile
push: false
load: true
tags: |
gitea.cruzcloud.net/devops/ecommerce-medusa:${{ steps.vars.outputs.VERSION }}
gitea.cruzcloud.net/devops/ecommerce-medusa:latest
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
# CRITICAL bloquea el pipeline: no se sube una imagen con una CVE
# crítica conocida y con fix disponible.
- name: Escanear imagen (Trivy) — CRITICAL bloquea
shell: bash
run: |
set -euo pipefail
/tmp/trivy image \
--severity CRITICAL \
--exit-code 1 \
--ignore-unfixed \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
# HIGH solo informa por ahora — mismo criterio que el frontend.
- name: Escanear imagen (Trivy) — HIGH informativo
shell: bash
run: |
set -euo pipefail
/tmp/trivy image \
--severity HIGH \
--exit-code 0 \
--ignore-unfixed \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
- name: Instalar Syft
shell: bash
run: |
set -euo pipefail
SYFT_VERSION="1.51.0"
curl -sSfL \
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${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 "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" \
-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-${{ steps.vars.outputs.VERSION }}
path: |
sbom.cdx.json
sbom.spdx.json
if-no-files-found: ignore
# Login ya se hizo arriba; recién acá se sube, después de que la
# imagen pasó el gate de CRITICAL.
- name: Subir Imagen al Registry
shell: bash
run: |
set -euo pipefail
docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
docker push "${IMAGE_NAME}:latest"
- name: Instalar Cosign
shell: bash
run: |
set -euo pipefail
COSIGN_VERSION="3.1.3"
curl -sSfL \
"https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" \
-o /tmp/cosign
chmod +x /tmp/cosign
/tmp/cosign version
# Mismo par de llaves que el frontend (COSIGN_PRIVATE_KEY /
# COSIGN_PASSWORD ya existen como secrets a nivel de repo, no hace
# falta un secret nuevo por app): una sola identidad de firma para
# todo el registry de este lab. La llave pública se commitea en
# cada directorio de app (workloads/commerce-backend/cosign.pub)
# para que la verificación quede local a cada workflow, igual que
# el frontend.
- name: Firmar Imagen (Cosign)
shell: bash
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
set -euo pipefail
/tmp/cosign sign \
--key env://COSIGN_PRIVATE_KEY \
--use-signing-config=false \
--tlog-upload=false \
--yes \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
- name: Verificar Firma (smoke test)
shell: bash
run: |
set -euo pipefail
/tmp/cosign verify \
--key "${APP_DIR}/cosign.pub" \
--insecure-ignore-tlog=true \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
- name: Verificar promoción segura
id: promotion
@@ -91,8 +337,8 @@ jobs:
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
MANIFEST="workloads/ecommerce/commerce/medusa.yaml"
IMAGE="gitea.cruzcloud.net/devops/ecommerce-medusa"
MANIFEST="${{ env.MANIFEST_FILE }}"
IMAGE="${{ env.IMAGE_NAME }}"
git config user.name "gitea-actions"
git config user.email "[email protected]"
@@ -115,3 +361,14 @@ jobs:
-m "chore(gitops): deploy Medusa ${VERSION} [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "ARI Shopping Commerce Backend (Medusa)"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"
+6 -258
View File
@@ -1,9 +1,5 @@
name: Build and Push Docs Portal
# Sin anchors/aliases de YAML (&x / *x) a propósito -- el parser de
# workflows de Gitea Actions no los resuelve en el bloque "on:" y
# descarta el archivo completo con "unknown on type". Las dos listas de
# paths quedan duplicadas literalmente (mismo criterio que build.yaml).
on:
push:
branches:
@@ -14,71 +10,14 @@ on:
- 'workloads/docs-portal/requirements.txt'
- 'workloads/docs-portal/Dockerfile'
- '.gitea/workflows/deploy-docs.yaml'
pull_request:
branches:
- main
paths:
- 'workloads/docs-portal/docs/**'
- 'workloads/docs-portal/mkdocs.yml'
- 'workloads/docs-portal/requirements.txt'
- 'workloads/docs-portal/Dockerfile'
- '.gitea/workflows/deploy-docs.yaml'
permissions:
contents: write
packages: write
jobs:
# Corre en push y en pull_request, siempre antes que build. Mismo
# criterio que el frontend: si hay un secreto commiteado, el job build
# nunca arranca (needs: gitleaks).
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Instalar Gitleaks
shell: bash
run: |
set -euo pipefail
GITLEAKS_VERSION="8.21.2"
curl -sSfL \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_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
- name: Escanear secretos en el árbol de archivos
shell: bash
run: |
set -euo pipefail
/tmp/gitleaks detect \
--source=workloads/docs-portal \
--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
build:
name: Construir y publicar Docs Portal
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 20
@@ -117,83 +56,6 @@ jobs:
exit 1
}
- name: Instalar Trivy
shell: bash
run: |
set -euo pipefail
TRIVY_VERSION="0.74.0"
curl -sSfL \
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${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
# Escanea todo el directorio de la app (Deployment, Service,
# Ingress y Dockerfile), no un único MANIFEST_FILE como el
# frontend -- docs-portal tiene varios manifiestos K8s separados
# (deployment.yaml, service.yaml, ingress.yaml) en vez de uno
# solo, así que un único archivo dejaría fuera la mayoría del
# directorio. Informativo por ahora, mismo criterio que el resto.
- name: Escanear manifiestos Kubernetes (Trivy IaC)
shell: bash
run: |
set -euo pipefail
/tmp/trivy config \
--severity CRITICAL,HIGH,MEDIUM \
--exit-code 0 \
"${APP_DIR}"
# Mismo motivo que en build.yaml: sibling containers, no
# Docker-in-Docker -- Semgrep corre nativo en un venv.
- name: Instalar Semgrep
shell: bash
run: |
set -euo pipefail
python3 -m venv /tmp/semgrep-venv
/tmp/semgrep-venv/bin/pip install --quiet "semgrep==1.173.0"
/tmp/semgrep-venv/bin/semgrep --version
# Ruleset adaptado: docs-portal no tiene código de aplicación
# propio (es contenido Markdown + configuración de MkDocs), así
# que ni p/typescript ni p/react/p/nextjs del frontend aplican
# acá. Se usa p/python -- el único código ejecutable real en este
# directorio sería un hook/plugin de Python de MkDocs, si algún
# día se agrega uno. Hoy no hay ningún archivo .py en
# workloads/docs-portal, así que 0 hallazgos es el resultado
# esperado, no un falso negativo -- se deja el stage para que
# detecte código nuevo el día que se agregue, sin tener que
# recordar volver a tocar el pipeline. Modo auditoría, igual que
# el resto: no bloquea.
- name: Escaneo SAST (Semgrep) — modo auditoría, no bloquea
shell: bash
run: |
set -euo pipefail
/tmp/semgrep-venv/bin/semgrep scan \
--config=p/python \
--config=p/security-audit \
--json \
--output=semgrep-report.json \
"${APP_DIR}"
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
- name: Login en Gitea Registry
uses: docker/login-action@v2
with:
@@ -202,121 +64,18 @@ jobs:
password: ${{ secrets.REGISTRY_PASSWORD }}
logout: true
# mkdocs build --strict corre dentro del propio Dockerfile (stage
# de build), así que un nav/link roto rompe este paso antes de
# publicar. push: false / load: true -- igual que el frontend, la
# imagen queda cargada localmente para escanearla con Trivy antes
# de subirla.
- name: Construir Imagen
# mkdocs build --strict corre dentro del propio Dockerfile (stage de
# build), así que un nav/link roto rompe este paso antes de publicar.
- name: Construir y subir imagen
uses: docker/build-push-action@v4
with:
context: ${{ env.APP_DIR }}/
file: ${{ env.APP_DIR }}/Dockerfile
push: false
load: true
context: workloads/docs-portal/
file: workloads/docs-portal/Dockerfile
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
# CRITICAL bloquea el pipeline: no se sube una imagen con una CVE
# crítica conocida y con fix disponible.
- name: Escanear imagen (Trivy) — CRITICAL bloquea
shell: bash
run: |
set -euo pipefail
/tmp/trivy image \
--severity CRITICAL \
--exit-code 1 \
--ignore-unfixed \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
# HIGH solo informa por ahora — mismo criterio que el resto.
- name: Escanear imagen (Trivy) — HIGH informativo
shell: bash
run: |
set -euo pipefail
/tmp/trivy image \
--severity HIGH \
--exit-code 0 \
--ignore-unfixed \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
- name: Instalar Syft
shell: bash
run: |
set -euo pipefail
SYFT_VERSION="1.51.0"
curl -sSfL \
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${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 "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" \
-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-${{ steps.vars.outputs.VERSION }}
path: |
sbom.cdx.json
sbom.spdx.json
if-no-files-found: ignore
# Login ya se hizo arriba; recién acá se sube, después de que la
# imagen pasó el gate de CRITICAL.
- name: Subir Imagen al Registry
shell: bash
run: |
set -euo pipefail
docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
docker push "${IMAGE_NAME}:latest"
- name: Instalar Cosign
shell: bash
run: |
set -euo pipefail
COSIGN_VERSION="3.1.3"
curl -sSfL \
"https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" \
-o /tmp/cosign
chmod +x /tmp/cosign
/tmp/cosign version
# Mismo par de llaves que el frontend y commerce-backend (secrets
# ya existentes a nivel de repo). Llave pública commiteada en
# workloads/docs-portal/cosign.pub.
- name: Firmar Imagen (Cosign)
shell: bash
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
set -euo pipefail
/tmp/cosign sign \
--key env://COSIGN_PRIVATE_KEY \
--use-signing-config=false \
--tlog-upload=false \
--yes \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
- name: Verificar Firma (smoke test)
shell: bash
run: |
set -euo pipefail
/tmp/cosign verify \
--key "${APP_DIR}/cosign.pub" \
--insecure-ignore-tlog=true \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
- name: Verificar promoción segura
id: promotion
shell: bash
@@ -364,14 +123,3 @@ jobs:
-m "chore(gitops): deploy Docs Portal ${VERSION} [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "CruzCloud Lab Docs Portal"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"