Skip to content

Análisis de Vulnerabilidades usando Clair

Desarrollo del tema

K8S-Clair-Logo

Clair es un proyecto de open source que proporciona una herramienta para monitorear la seguridad de sus contenedores a través del análisis estático de vulnerabilidades en contenedores appc y docker. Clair es un motor de análisis basado en API que inspecciona los contenedores capa por capa en busca de fallas de seguridad conocidas. Con Clair, puede crear fácilmente servicios que proporcionen un monitoreo continuo de las vulnerabilidades de los contenedores.

Los datos de vulnerabilidad se importan continuamente desde un conjunto conocido de fuentes y se correlacionan con el contenido indexado de las imágenes del contenedor para producir listas de vulnerabilidades que amenazan un contenedor. Cuando los datos de vulnerabilidad cambian en sentido ascendente, el estado anterior y el nuevo estado de la vulnerabilidad junto con las imágenes a las que afectan se pueden enviar a través de un webhook a un punto final configurado. Todos los componentes principales se pueden personalizar mediante programación en tiempo de compilación sin bifurcar el proyecto. Clair apoya la seguridad de los contenedores mediante:

  • Actualizar datos de vulnerabilidad de un conjunto de fuentes que usted define y almacenar estos datos en su base de datos.

  • Permitir a los clientes consultar esta base de datos en busca de vulnerabilidades dentro de imágenes específicas mediante el uso de una API.

  • Indexación de imágenes de contenedores con una lista de características presentes en la imagen mediante el uso de una API

Laboratorio: Análisis usando Clair

Descripción

En la presente guía se creara una imagen de contenedor y se procederá con el respectivo análisis usando la herramienta Clair.

Objetivos

  • Instalar Clair y usarlo para analizar una imagen de contenedor.

Antes de comenzar

  • Contar con el acceso al ambiente del laboratorio.

Inicio de laboratorio

  1. Crearemos un archivo Docker Compose y Config siguiente manera:

    mkdir -p example-clair/docker-compose-data/clair-config; touch example-clair/docker-compose.yaml; touch example-clair/docker-compose-data/clair-config/config.yml
    
  2. Con el editor de texto añadiremos el contenido según corresponda:

    • Se agrega el contenido del archivo Docker Compose:
    vim example-clair/docker-compose.yaml
    
    services:
      postgres:
        image: postgres:14
        restart: unless-stopped
        volumes:
          - ./docker-compose-data/postgres-data/:/var/lib/postgresql/data:rw
        environment:
          - POSTGRES_PASSWORD=ChangeMe
          - POSTGRES_USER=clair
          - POSTGRES_DB=clair
    
      clair:
        image: quay.io/projectquay/clair:4.7.4
        restart: unless-stopped
        volumes:
          - ./docker-compose-data/clair-config/config.yaml:/etc/clair/config.yaml:ro
        environment:
          - CLAIR_CONF=/etc/clair/config.yaml
          - CLAIR_MODE=combo
        depends_on:
          postgres:
            condition: service_started
        ports:
          - "6060:6060"
          - "8089:8089"
    
    • Se agrega el contenido del archivo Config:
    vim example-clair/docker-compose-data/clair-config/config.yaml
    
    http_listen_addr: 0.0.0.0:6060
    introspection_addr: 0.0.0.0:8089
    log_level: debug
    
    indexer:
      connstring: host=postgres port=5432 dbname=clair user=clair password=ChangeMe sslmode=disable
      scanlock_retry: 10
      layer_scan_concurrency: 5
      migrations: true
    
    matcher:
      connstring: host=postgres port=5432 dbname=clair user=clair password=ChangeMe sslmode=disable
      max_conn_pool: 100
      migrations: true
    
    notifier:
      connstring: host=postgres port=5432 dbname=clair user=clair password=ChangeMe sslmode=disable
      migrations: true
    
    updaters:
      sets:
        - alpine
        - debian
        - ubuntu
        - aws
        - oracle
    
  3. Iniciamos el Docker Compose:

    cd example-clair; sudo docker compose up -d
    
  4. Cambiamos de manera temporal los permisos del docker.sock:

    sudo chmod 666 /var/run/docker.sock
    
  5. Descargamos una imagen de prueba:

    sudo docker pull infoslack/dvwa
    
  6. Le damos permisos a la siguiente carpeta

    sudo mkdir -p ./docker-compose-data/clairctl-reports
    sudo chmod 777 ./docker-compose-data/clairctl-reports
    

  7. Creamos el siguiente script para escanear la imagen.

    #!/bin/bash
    set -e
    
    IMAGE=$1
    CLAIR_URL="http://localhost:6060"
    WORK_DIR="/tmp/clair-scan-$$"
    HTTP_PORT=8088
    HOST_IP="172.17.0.1"
    
    if [ -z "$IMAGE" ]; then
      echo "Usage: $0 <image:tag>"
      exit 1
    fi
    
    echo "[*] Exporting image: $IMAGE"
    mkdir -p "$WORK_DIR"
    docker save "$IMAGE" -o "$WORK_DIR/image.tar"
    
    echo "[*] Extracting layers..."
    mkdir -p "$WORK_DIR/layers"
    tar -xf "$WORK_DIR/image.tar" -C "$WORK_DIR/layers"
    
    # Start HTTP server to serve layers
    echo "[*] Starting layer HTTP server on port $HTTP_PORT..."
    cd "$WORK_DIR/layers"
    python3 -m http.server $HTTP_PORT &
    HTTP_PID=$!
    cd - > /dev/null
    sleep 1
    
    # Parse manifest to get layers and image digest
    MANIFEST="$WORK_DIR/layers/manifest.json"
    IMAGE_DIGEST=$(sha256sum "$WORK_DIR/image.tar" | awk '{print $1}')
    
    echo "[*] Building Clair manifest payload..."
    
    # Extract layer paths from manifest.json
    LAYERS=$(python3 - <<EOF
    import json, sys
    
    with open("$MANIFEST") as f:
        manifest = json.load(f)
    
    layers = manifest[0].get("Layers", [])
    result = []
    for layer in layers:
        import hashlib
        with open("$WORK_DIR/layers/" + layer, "rb") as lf:
            digest = hashlib.sha256(lf.read()).hexdigest()
        result.append({
            "hash": "sha256:" + digest,
            "uri": "http://$HOST_IP:$HTTP_PORT/" + layer,
            "headers": {}
        })
    
    print(json.dumps(result))
    EOF
    )
    
    PAYLOAD=$(python3 -c "
    import json
    layers = $LAYERS
    payload = {
        'hash': 'sha256:$IMAGE_DIGEST',
        'layers': layers
    }
    print(json.dumps(payload, indent=2))
    ")
    
    echo "[*] Submitting manifest to Clair indexer..."
    curl -s -X POST "$CLAIR_URL/indexer/api/v1/index_report" \
      -H "Content-Type: application/json" \
      -d "$PAYLOAD" | python3 -m json.tool
    
    echo ""
    echo "[*] Waiting for indexing to complete..."
    for i in $(seq 1 30); do
      STATE=$(curl -s "$CLAIR_URL/indexer/api/v1/index_report/sha256:$IMAGE_DIGEST" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state',''))" 2>/dev/null)
      echo "    State: $STATE"
      if [ "$STATE" = "IndexFinished" ] || [ "$STATE" = "IndexError" ]; then
        break
      fi
      sleep 5
    done
    
    echo ""
    echo "[*] Fetching vulnerability report..."
    curl -s "$CLAIR_URL/matcher/api/v1/vulnerability_report/sha256:$IMAGE_DIGEST" | python3 -m json.tool > "./docker-compose-data/clairctl-reports/${IMAGE//\//_}_report.json"
    
    echo ""
    echo "[+] Report saved to: docker-compose-data/clairctl-reports/${IMAGE//\//_}_report.json"
    echo "[+] Vulnerability summary:"
    python3 -c "
    import json
    with open('./docker-compose-data/clairctl-reports/${IMAGE//\//_}_report.json') as f:
        report = json.load(f)
    vulns = report.get('vulnerabilities', {})
    print(f'  Total vulnerabilities found: {len(vulns)}')
    from collections import Counter
    severities = Counter(v.get('normalized_severity','Unknown') for v in vulns.values())
    for sev, count in sorted(severities.items()):
        print(f'  {sev}: {count}')
    "
    
    # Cleanup
    echo ""
    echo "[*] Cleaning up..."
    kill $HTTP_PID 2>/dev/null
    rm -rf "$WORK_DIR"
    echo "[+] Done."
    
  8. Observamos el reporte generado:

    cat docker-compose-data/clairctl-reports/infoslack_dvwa_report.json
    
  9. Limpiamos el ambiente:

    sudo docker compose down -v; cd ~
    sudo chmod 660 /var/run/docker.sock