Skip to content

Implementa un pipeline CI/CD seguro (guía para equipos DevOps)

November 11, 2025

El Pipeline como Vector de Ataque Crítico

Un pipeline CI/CD es mucho más que automatización: es el corazón del software delivery moderno y un punto crítico de exposición si no se protege adecuadamente.

El Costo de la Inseguridad

  • SolarWinds (2020): Los atacantes comprometieron el pipeline de build, no la aplicación final
  • Gartner 2025: Proyecta que el 45% de organizaciones sufrirá ataques a la cadena de suministro
  • Realidad actual: Menos del 10% de las empresas monitorea la seguridad en su SDLC

Esta guía proporciona un enfoque práctico basado en frameworks reconocidos:

  • OWASP Top 10 CI/CD
  • NIST SP 800-204D
  • SLSA (Supply Chain Levels for Software Artifacts)

DevOps vs DevSecOps: El Cambio de Paradigma

DevOps Tradicional

Desarrollo → Testing → Deploy → [Seguridad al final]

Problema: Descubrir vulnerabilidades en producción es costoso y peligroso.

DevSecOps Moderno

[Seguridad] → Desarrollo → [Seguridad] → Testing → [Seguridad] → Deploy → [Seguridad]

Ventaja: Detección temprana mediante Shift-Left Security.


Arquitectura de un Pipeline CI/CD Seguro

┌─────────────────────────────────────────────────────────────┐
│ 1. DEVELOPER ENVIRONMENT                                     │
│    ✓ Pre-commit hooks (secretos, SAST)                      │
│    ✓ Signed commits (GPG/SSH)                               │
└──────────────────┬──────────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────────────┐
│ 2. SOURCE CODE MANAGEMENT (GitHub/GitLab)                    │
│    ✓ Branch protection                                       │
│    ✓ Mandatory code review                                   │
│    ✓ Secrets scanning (gitleaks)                            │
└──────────────────┬──────────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────────────┐
│ 3. CI PIPELINE: Build & Test                                 │
│    ✓ SAST (Static Analysis)                                 │
│    ✓ SCA (Dependency scanning)                              │
│    ✓ Container scanning                                     │
│    ✓ SBOM generation                                        │
└──────────────────┬──────────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────────────┐
│ 4. SECURITY GATES                                            │
│    ✓ DAST (Dynamic testing)                                 │
│    ✓ Policy enforcement (OPA)                               │
│    ✓ Image signing (Cosign)                                 │
└──────────────────┬──────────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────────────────────┐
│ 5. DEPLOYMENT & RUNTIME                                      │
│    ✓ Signature verification                                 │
│    ✓ Runtime monitoring                                     │
│    ✓ Continuous compliance                                  │
└─────────────────────────────────────────────────────────────┘

Fase 1: Protección del Código Fuente

1.1 Control de Acceso Granular (RBAC)

Principio: Least Privilege + Defense in Depth

# GitHub/GitLab Branch Protection
branch_protection:
  main:
    required_reviews: 2
    require_signed_commits: true
    require_status_checks: true
    restrict_push_access:
      - admins
      - ci-service-account
    dismiss_stale_reviews: true
    
  develop:
    required_reviews: 1
    require_signed_commits: true

Implementación crítica:

  • ✅ Nadie puede hacer merge sin revisión (ni siquiera admins)
  • ✅ Commits firmados criptográficamente (GPG)
  • ✅ Audit logs habilitados
  • ✅ MFA obligatorio para todos

1.2 Detección Automatizada de Secretos

Problema: API keys, tokens y contraseñas filtradas en Git.

Solución: Pre-commit hooks + CI scanning

# Instalación local (pre-commit)
brew install gitleaks
gitleaks detect --source . --verbose

# Pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
    echo "❌ SECRET DETECTED - Commit bloqueado"
    exit 1
fi
EOF
chmod +x .git/hooks/pre-commit

Pipeline CI/CD:

# GitLab CI
secret-detection:
  stage: validate
  image: zricethezav/gitleaks:latest
  script:
    - gitleaks detect --source . --verbose --exit-code 1
  allow_failure: false  # Bloquea el pipeline

Herramientas recomendadas:

  • gitleaks: Rápido, bajo falsos positivos
  • TruffleHog: Detección por entropía
  • GitGuardian: SaaS con integraciones

1.3 Gestión Centralizada de Secretos

❌ NUNCA hacer esto:

# INSEGURO
variables:
  DATABASE_PASSWORD: "my-secret-123"
  API_KEY: "ghp_xxxxxxxxxxxx"

✅ Solución: HashiCorp Vault

# 1. Configurar Vault
vault secrets enable -version=2 kv

# 2. Crear política de acceso
vault policy write ci-pipeline - <<EOF
path "kv/data/ci/*" {
  capabilities = ["read"]
}
EOF

# 3. Crear AppRole para CI
vault write auth/approle/role/ci-pipeline \
  token_ttl=1h \
  token_max_ttl=4h \
  policies="ci-pipeline"

# 4. Guardar secretos
vault kv put kv/ci/database password="secure-pass"
vault kv put kv/ci/github token="ghp_xxxx"

Integración en Pipeline:

# GitLab CI con Vault
build:
  stage: build
  before_script:
    # Autenticación con Vault
    - export VAULT_TOKEN=$(vault write -field=token 
        auth/approle/login 
        role_id=$ROLE_ID 
        secret_id=$SECRET_ID)
    
    # Obtener secretos
    - export DB_PASS=$(vault kv get -field=password kv/ci/database)
  script:
    - docker build --build-arg DB_PASS=$DB_PASS -t myapp .

Alternativa Kubernetes: External Secrets Operator

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
spec:
  secretStoreRef:
    name: vault-backend
  target:
    name: app-credentials
  data:
    - secretKey: database-password
      remoteRef:
        key: kv/ci/database
        property: password

Fase 2: Análisis Automatizado de Seguridad (CI)

2.1 SAST (Static Application Security Testing)

Detecta: Vulnerabilidades en código fuente sin ejecutarlo.

Herramientas por lenguaje:

  • Python: Bandit, Semgrep
  • JavaScript/Node: ESLint + security plugins, Semgrep
  • Java: SpotBugs, Checkmarx
  • Multi-lenguaje: SonarQube, Semgrep
# GitLab CI - SAST Multi-herramienta
sast:
  stage: test
  image: returntocorp/semgrep:latest
  script:
    # Semgrep con reglas OWASP
    - semgrep --config=p/owasp-top-ten src/ --json -o semgrep.json
    
    # SonarQube (opcional)
    - sonar-scanner \
        -Dsonar.projectKey=myapp \
        -Dsonar.sources=./src \
        -Dsonar.host.url=$SONAR_URL \
        -Dsonar.token=$SONAR_TOKEN
  artifacts:
    reports:
      sast: semgrep.json

Ejemplo de vulnerabilidad detectada:

# ❌ VULNERABLE: SQL Injection
query = f"SELECT * FROM users WHERE id = {user_id}"
db.execute(query)

# ✅ CORRECTO: Prepared statement
query = "SELECT * FROM users WHERE id = ?"
db.execute(query, (user_id,))

2.2 SCA (Software Composition Analysis)

Detecta: Vulnerabilidades conocidas en dependencias (CVEs).

# GitLab CI - SCA con Snyk
dependency-scan:
  stage: test
  image: snyk/snyk:python
  script:
    # Autenticación
    - snyk auth $SNYK_TOKEN
    
    # Escaneo con threshold
    - snyk test --severity-threshold=high --json > snyk-report.json
    
    # Generar SBOM (Software Bill of Materials)
    - snyk sbom --format=cyclonedx > sbom.json
  artifacts:
    reports:
      dependency_scanning: snyk-report.json
      cyclonedx: sbom.json
  allow_failure: false

Alternativas:

  • Dependabot (GitHub): Automatiza PRs de actualización
  • Trivy: Open-source, rápido
  • OWASP Dependency-Check: Gratuito, soporte amplio

2.3 Container Image Scanning

Detecta: Vulnerabilidades en imágenes Docker.

# GitLab CI - Trivy Container Scan
container-scan:
  stage: security
  image: aquasec/trivy:latest
  services:
    - docker:dind
  script:
    # Build
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    
    # Scan con múltiples severidades
    - trivy image --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    
    # Generar reporte JSON
    - trivy image --format json -o trivy-report.json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    
    # BLOQUEAR si hay críticos
    - trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  artifacts:
    reports:
      container_scanning: trivy-report.json

Best Practice – Imágenes Base Minimales:

# ❌ INSEGURO: Superficie de ataque grande
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 nodejs

# ✅ SEGURO: Distroless o Alpine
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage final: imagen minimal
FROM gcr.io/distroless/python3
COPY --from=builder /root/.local /app/.local
COPY . /app
USER nonroot:nonroot
CMD ["python", "app.py"]

Fase 3: Seguridad del Entorno de Build

3.1 Aislamiento de Runners

Riesgo: Runners comprometidos = pipeline comprometido.

Configuración segura GitLab Runner:

# /etc/gitlab-runner/config.toml
[[runners]]
  name = "secure-docker-runner"
  url = "https://gitlab.company.com"

[runners.docker]

image = “alpine:latest” privileged = false # ❌ NUNCA usar privileged disable_cache = false # Seguridad adicional cap_drop = [“ALL”] cap_add = [“NET_BIND_SERVICE”] security_opt = [“no-new-privileges:true”] # Volúmenes temporales volumes = [“/cache”, “/builds:/builds:rw”] tmpfs = [“/tmp:rw,noexec,nosuid”]

Kubernetes – Network Policies:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ci-runner-isolation
  namespace: gitlab-ci
spec:
  podSelector:
    matchLabels:
      app: gitlab-runner
  policyTypes:
    - Ingress
    - Egress
  egress:
    # Solo permitir conexiones a servicios necesarios
    - to:
        - podSelector:
            matchLabels:
              app: gitlab-server
      ports:
        - protocol: TCP
          port: 443
    - to:
        - podSelector:
            matchLabels:
              app: vault
      ports:
        - protocol: TCP
          port: 8200

3.2 Ephemeral Runners

Mejor práctica: Destruir runners después de cada ejecución.

# GitLab CI - Runners efímeros en Kubernetes
variables:
  KUBERNETES_CPU_REQUEST: "1"
  KUBERNETES_MEMORY_REQUEST: "512Mi"
  KUBERNETES_POD_ANNOTATIONS_1: "container.apparmor.security.beta.kubernetes.io/build=runtime/default"

build:
  tags:
    - kubernetes
    - ephemeral
  script:
    - docker build -t myapp .

Fase 4: DAST (Dynamic Application Security Testing)

Detecta: Vulnerabilidades en runtime (SQL injection, XSS, etc.)

# GitLab CI - OWASP ZAP
dast:
  stage: security
  image: owasp/zap2docker-stable:latest
  services:
    - name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      alias: test-app
  variables:
    DAST_WEBSITE: "http://test-app:8080"
  script:
    # Esperar a que la app inicie
    - until curl -s $DAST_WEBSITE/health; do sleep 5; done
    
    # Baseline scan (rápido)
    - zap-baseline.py -t $DAST_WEBSITE -J zap-report.json
    
    # Full scan (más profundo - solo en main)
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        zap-full-scan.py -t $DAST_WEBSITE -J zap-full.json
      fi
  artifacts:
    reports:
      dast: zap-report.json
  allow_failure: true  # DAST puede tener falsos positivos

Alternativas:

  • Burp Suite: Profesional, costoso
  • Nuclei: Rápido, basado en templates
  • Arachni: Open-source

Fase 5: Policy as Code (Infraestructura Segura)

Objetivo: Prevenir configuraciones inseguras automáticamente.

OPA (Open Policy Agent) para Terraform

# opa-policies/security.rego
package terraform.security

# Denegar bases de datos sin encriptación
deny[msg] {
    resource := input.resource.aws_db_instance[name]
    not resource.storage_encrypted
    msg := sprintf("❌ Database '%s' must enable encryption at rest", [name])
}

# Denegar buckets S3 públicos
deny[msg] {
    resource := input.resource.aws_s3_bucket[name]
    resource.acl == "public-read"
    msg := sprintf("❌ S3 bucket '%s' cannot be public", [name])
}

# Denegar security groups sin restricciones
deny[msg] {
    resource := input.resource.aws_security_group[name]
    rule := resource.ingress[_]
    rule.cidr_blocks[_] == "0.0.0.0/0"
    rule.from_port == 0
    msg := sprintf("❌ Security group '%s' allows unrestricted access", [name])
}

# Requerir tags obligatorios
required_tags := ["Environment", "Owner", "CostCenter"]

deny[msg] {
    resource := input.resource[type][name]
    type == "aws_instance"
    tag := required_tags[_]
    not resource.tags[tag]
    msg := sprintf("❌ EC2 instance '%s' missing required tag: %s", [name, tag])
}

Pipeline Integration:

# GitLab CI - Policy Enforcement
infrastructure-validation:
  stage: validate
  image: openpolicyagent/opa:latest
  script:
    - terraform init
    - terraform plan -out=tfplan
    - terraform show -json tfplan > tfplan.json
    
    # Validar con OPA
    - opa eval -d opa-policies/ -i tfplan.json 'data.terraform.security.deny' -f pretty
    
    # Fallar si hay violaciones
    - |
      VIOLATIONS=$(opa eval -d opa-policies/ -i tfplan.json 'data.terraform.security.deny | length' -f raw)
      if [ "$VIOLATIONS" != "0" ]; then
        echo "❌ Policy violations detected: $VIOLATIONS"
        exit 1
      fi
  allow_failure: false

Fase 6: Firma y Verificación de Artefactos

6.1 Firma de Imágenes con Cosign (Sigstore)

Objetivo: Garantizar integridad y procedencia.

# GitLab CI - Signing Pipeline
sign-and-push:
  stage: deploy
  image: gcr.io/projectsigstore/cosign:latest
  services:
    - docker:dind
  script:
    # Build
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    
    # Firmar imagen
    - cosign sign --key env://COSIGN_KEY $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    
    # Generar attestation (SBOM)
    - cosign attest --key env://COSIGN_KEY \
        --predicate sbom.json \
        $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

6.2 Verificación en Deployment

# Kubernetes Admission Controller (Kyverno)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: enforce
  background: false
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.company.com/*"
          attestors:
            - count: 1
              entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----

Pipeline Verification:

deploy:
  stage: deploy
  script:
    # Verificar firma antes de deploy
    - cosign verify --key $COSIGN_PUBLIC_KEY $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    
    # Verificar attestation
    - cosign verify-attestation \
        --key $COSIGN_PUBLIC_KEY \
        --type slsaprovenance \
        $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    
    # Deploy solo si verificación exitosa
    - kubectl set image deployment/myapp container=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Fase 7: Monitoreo y Auditoría Continua

7.1 Logging Centralizado (SIEM)

# GitLab CI - Log Shipping
.post-pipeline:
  stage: .post
  script:
    - |
      curl -X POST https://siem.company.com/api/logs \
        -H "Authorization: Bearer $SIEM_TOKEN" \
        -H "Content-Type: application/json" \
        -d @- << EOF
      {
        "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
        "pipeline_id": "$CI_PIPELINE_ID",
        "project": "$CI_PROJECT_PATH",
        "commit_sha": "$CI_COMMIT_SHA",
        "commit_author": "$CI_COMMIT_AUTHOR",
        "branch": "$CI_COMMIT_BRANCH",
        "status": "$CI_JOB_STATUS",
        "duration_seconds": "$CI_JOB_DURATION",
        "runner": "$CI_RUNNER_ID",
        "triggered_by": "$GITLAB_USER_LOGIN"
      }
      EOF
  when: always

7.2 Detección de Anomalías

anomaly-detection:
  stage: .post
  script:
    # Alertar en horarios inusuales
    - |
      HOUR=$(date +%H)
      if [ $HOUR -lt 6 ] || [ $HOUR -gt 22 ]; then
        echo "⚠️  Pipeline ejecutado fuera de horario laboral"
        curl -X POST $SLACK_WEBHOOK -d "{\"text\":\"Unusual pipeline at $HOUR:00\"}"
      fi
    
    # Detectar cambios sensibles
    - |
      git diff HEAD~1 --name-only | grep -E '(\.gitlab-ci\.yml|vault|secret)' && \
        echo "⚠️  Cambios detectados en configuración sensible"
    
    # Alertar en cambios de permisos
    - |
      if git diff HEAD~1 .gitlab-ci.yml | grep -E '(privileged|allow_failure.*false)'; then
        echo "⚠️  Cambios críticos en CI config"
      fi
  when: always

Fase 8: Compliance y Frameworks

8.1 OWASP Top 10 CI/CD Mapping

#RiesgoMitigación
1Insufficient Credential HygieneVault, secrets scanning, rotación automática
2Poisoned Pipeline ExecutionSigned commits, code review, RBAC estricto
3Insecure System ConfigurationHardening runners, least privilege
4Insecure DependenciesSCA continuo, SBOM, dependency pinning
5Insufficient LoggingSIEM, audit trails, alertas anomalías
6Insecure Artifact StorageRegistry privado, autenticación, firma
7Insecure Build ProcessContenedores aislados, policy-as-code
8Inadequate Access ControlRBAC, MFA, Just-In-Time access
9Unsafe Artifact ProvisioningCosign, SBOM, provenance verification
10Ungoverned 3rd Party ServicesInventory, approval workflows, SLA reviews

8.2 SLSA Framework (Level 3 Implementation)

# GitLab CI - SLSA Provenance
provenance:
  stage: deploy
  image: gcr.io/projectsigstore/cosign:latest
  script:
    # Generar provenance
    - |
      cat > provenance.json << EOF
      {
        "_type": "https://in-toto.io/Statement/v0.1",
        "subject": [{
          "name": "$CI_REGISTRY_IMAGE",
          "digest": {
            "sha256": "$(docker inspect --format='{{.Id}}' $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA | cut -d: -f2)"
          }
        }],
        "predicateType": "https://slsa.dev/provenance/v0.2",
        "predicate": {
          "builder": {
            "id": "https://gitlab.company.com/builder/$CI_RUNNER_ID"
          },
          "buildType": "https://gitlab.com/gitlab-ci/v1",
          "invocation": {
            "configSource": {
              "uri": "$CI_PROJECT_URL",
              "digest": {"sha1": "$CI_COMMIT_SHA"},
              "entryPoint": ".gitlab-ci.yml"
            }
          },
          "metadata": {
            "buildStartedOn": "$CI_PIPELINE_CREATED_AT",
            "buildFinishedOn": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
            "completeness": {
              "parameters": true,
              "environment": true,
              "materials": true
            },
            "reproducible": false
          },
          "materials": [{
            "uri": "$CI_PROJECT_URL",
            "digest": {"sha1": "$CI_COMMIT_SHA"}
          }]
        }
      }
      EOF
    
    # Firmar provenance
    - cosign attest --key env://COSIGN_KEY \
        --predicate provenance.json \
        $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Pipeline Completo: Ejemplo Productivo (Python)

# .gitlab-ci.yml - Pipeline Seguro Completo
stages:
  - validate
  - test
  - build
  - security
  - deploy
  - monitor

variables:
  REGISTRY: registry.company.com
  IMAGE: $REGISTRY/$CI_PROJECT_PATH
  TAG: $CI_COMMIT_SHORT_SHA
  DOCKER_TLS_CERTDIR: "/certs"

# ============ STAGE 1: VALIDATE ============
secrets-detection:
  stage: validate
  image: zricethezav/gitleaks:latest
  script:
    - gitleaks detect --source . --verbose --exit-code 1
  allow_failure: false

lint-code:
  stage: validate
  image: python:3.11
  script:
    - pip install black flake8 mypy
    - black --check src/
    - flake8 src/ --max-line-length=100
    - mypy src/ --strict
  allow_failure: false

# ============ STAGE 2: TEST ============
sast:
  stage: test
  image: python:3.11
  script:
    - pip install bandit semgrep
    - bandit -r src/ -f json -o bandit-report.json
    - semgrep --config=p/security-audit src/ --json -o semgrep-report.json
  artifacts:
    reports:
      sast: 
        - bandit-report.json
        - semgrep-report.json

dependency-scan:
  stage: test
  image: snyk/snyk:python
  script:
    - snyk auth $SNYK_TOKEN
    - snyk test --severity-threshold=high --json > snyk-report.json
    - snyk sbom --format=cyclonedx > sbom.json
  artifacts:
    reports:
      dependency_scanning: snyk-report.json
      cyclonedx: sbom.json
  allow_failure: false

unit-tests:
  stage: test
  image: python:3.11
  script:
    - pip install -r requirements-dev.txt
    - pytest tests/ -v --cov=src/ --cov-report=xml
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

# ============ STAGE 3: BUILD ============
build-image:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $REGISTRY
  script:
    # Multi-stage Dockerfile
    - |
      cat > Dockerfile << 'EOF'
      FROM python:3.11-slim AS builder
      WORKDIR /app
      COPY requirements.txt .
      RUN pip install --user --no-cache-dir -r requirements.txt
      
      FROM python:3.11-slim
      RUN useradd -m -u 1001 appuser && \
          apt-get update && \
          apt-get install -y --no-install-recommends curl && \
          apt-get clean && rm -rf /var/lib/apt/lists/*
      WORKDIR /app
      COPY --from=builder --chown=appuser:appuser /root/.local /home/appuser/.local
      COPY --chown=appuser:appuser . .
      USER appuser
      ENV PATH=/home/appuser/.local/bin:$PATH
      EXPOSE 8000
      HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8000/health || exit 1
      CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
      EOF
    
    - docker build -t $IMAGE:$TAG .
    - docker push $IMAGE:$TAG

# ============ STAGE 4: SECURITY ============
container-scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy image --severity HIGH,CRITICAL --exit-code 0 $IMAGE:$TAG
    - trivy image --format json -o trivy-report.json $IMAGE:$TAG
    - trivy image --exit-code 1 --severity CRITICAL $IMAGE:$TAG
  artifacts:
    reports:
      container_scanning: trivy-report.json

dast:
  stage: security
  image: owasp/zap2docker-stable:latest
  services:
    - name: $IMAGE:$TAG
      alias: test-app
  variables:
    TARGET_URL: "http://test-app:8000"
  script: