Skip to content
Refalia Defani
Go back

Automating Security Pipelines & Real-time Notifications at Lyrid.io

Role Context

As a DevOps Engineer at Lyrid.io (Oct 2025 – Present), I architected automated security scanning pipelines integrated into GitLab CI/CD with real-time Discord notifications for the engineering team.

Pipeline Architecture

┌────────────────────────────────────────────────────────────┐
│                    GitLab CI/CD Pipeline                     │
│                                                            │
│  Stage 1              Stage 2              Stage 3          │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐ │
│  │ security-scan│    │dependency-scan│    │  build-scan  │ │
│  │              │    │              │    │              │ │
│  │ Scan base    │    │ Scan repo    │    │ Build image  │ │
│  │ Docker image │    │ dependencies │    │ + scan it    │ │
│  │ (golang:1.25)│    │ (npm, etc.)  │    │ (app:latest) │ │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘ │
│         │                   │                   │          │
│         └───────────────────┼───────────────────┘          │
│                             ▼                              │
│                   ┌──────────────────┐                     │
│                   │  Discord Webhook │                     │
│                   │  📊 Report + 📎  │                     │
│                   │  HTML/JSON files  │                     │
│                   └──────────────────┘                     │
└────────────────────────────────────────────────────────────┘

Full Pipeline Implementation (.gitlab-ci.yml)

Pipeline Stages

stages:
  - security-scan
  - dependency-scan
  - build-scan

Three dedicated stages ensure comprehensive coverage: base image vulnerabilities, dependency vulnerabilities, and built application vulnerabilities.

Discord Notification Template (YAML Anchor)

A reusable notification block that automatically sends scan reports to Discord after each job:

.discord_notify: &discord_notify
  after_script:
    - |
      # Install curl if not available
      if ! command -v curl &> /dev/null; then
        if command -v apk &> /dev/null; then apk add --no-cache curl;
        elif command -v apt-get &> /dev/null; then apt-get update && apt-get install -y curl;
        fi
      fi

      # Find latest report files
      HTML_FILE=$(ls -t osv-report-*.html 2>/dev/null | head -n 1)
      JSON_FILE=$(ls -t osv-report-*.json 2>/dev/null | head -n 1)

      if [ -z "$HTML_FILE" ]; then
        echo "❌ Report file not found."
        exit 0
      fi

      # Set icon & title based on job type
      if [[ "$CI_JOB_NAME" == *"dependencies"* ]]; then
        ICON="🔍"; TITLE="OSV Dependency Scan Report"
        TARGET_INFO=""
      else
        ICON="🐳"; TITLE="OSV Docker Image Scan Report"
        IMG_NAME=${TARGET_IMAGE:-$MY_IMAGE}
        TARGET_INFO="\n🏷️ **Image Tag:** \`$IMG_NAME\`"
      fi

      # Send Discord embed with pipeline context
      curl -X POST -H "Content-Type: application/json" \
        -d "{
          \"content\": \"$ICON **$TITLE**\",
          \"embeds\": [{
            \"color\": 3447003,
            \"description\": \"📦 **Repository:** \`$CI_PROJECT_PATH\`\n🌿 **Branch:** \`$CI_COMMIT_REF_NAME\`\n👤 **Triggered by:** \`$GITLAB_USER_NAME\`\n🔖 **Commit ID:** \`$CI_COMMIT_SHORT_SHA\`$TARGET_INFO\",
            \"footer\": { \"text\": \"📄 Please download the attached reports for details\" }
          }]
        }" $DISCORD_WEBHOOK

      # Attach report files (HTML + JSON)
      if [ -f "$JSON_FILE" ]; then
        curl -F "file1=@$HTML_FILE" -F "file2=@$JSON_FILE" $DISCORD_WEBHOOK
      else
        curl -F "file1=@$HTML_FILE" $DISCORD_WEBHOOK
      fi

Job 1: Base Image Security Scan

Scans the base Docker image (e.g., golang:1.25.5-bookworm) for known CVEs:

osv_scan_golang_image:
  stage: security-scan
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  variables:
    TARGET_IMAGE: "golang:1.25.5-bookworm"
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  script:
    - apk add --no-cache curl
    - curl -L https://github.com/google/osv-scanner/releases/download/v2.0.2/osv-scanner_linux_amd64 -o /usr/local/bin/osv-scanner
    - chmod +x /usr/local/bin/osv-scanner
    - |
      for i in $(seq 1 30); do
        docker info >/dev/null 2>&1 && break || sleep 2
      done
    - docker pull $TARGET_IMAGE
    - /usr/local/bin/osv-scanner scan image --format json $TARGET_IMAGE > osv-report-golang.json || true
    - /usr/local/bin/osv-scanner scan image --format html $TARGET_IMAGE > osv-report-golang.html || true
  artifacts:
    when: always
    paths:
      - osv-report-golang.json
      - osv-report-golang.html
  <<: *discord_notify

Job 2: Dependency Vulnerability Scan

Recursively scans all project dependencies (lockfiles, manifests):

osv_scan_repo_dependencies:
  stage: dependency-scan
  image: node:22-bookworm
  script:
    - apt-get update && apt-get install -y curl
    - npm install --package-lock-only --ignore-scripts
    - curl -L https://github.com/google/osv-scanner/releases/download/v2.0.2/osv-scanner_linux_amd64 -o /usr/local/bin/osv-scanner
    - chmod +x /usr/local/bin/osv-scanner
    - /usr/local/bin/osv-scanner scan source --recursive --format json . > osv-report-deps.json || true
    - /usr/local/bin/osv-scanner scan source --recursive --format html . > osv-report-deps.html || true
  artifacts:
    when: always
    paths:
      - osv-report-deps.json
      - osv-report-deps.html
  <<: *discord_notify

Job 3: Build & Scan Application Image

Builds the actual Docker image and scans it for vulnerabilities:

osv_build_and_scan_local:
  stage: build-scan
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
    MY_IMAGE: "juicy-app:latest"
  script:
    - apk add --no-cache curl
    - curl -L https://github.com/google/osv-scanner/releases/download/v2.0.2/osv-scanner_linux_amd64 -o /usr/local/bin/osv-scanner
    - chmod +x /usr/local/bin/osv-scanner
    - |
      for i in $(seq 1 30); do
        docker info >/dev/null 2>&1 && break || sleep 1
      done
    - docker build -t $MY_IMAGE .
    - /usr/local/bin/osv-scanner scan image --format json $MY_IMAGE > osv-report-local-build.json || true
    - /usr/local/bin/osv-scanner scan image --format html $MY_IMAGE > osv-report-local-build.html || true
  artifacts:
    when: always
    paths:
      - osv-report-local-build.json
      - osv-report-local-build.html
  <<: *discord_notify

Key Design Decisions

Why OSV-Scanner?

Why YAML Anchors (&discord_notify)?

Why Docker-in-Docker (DinD)?

Discord Notification Output

Each scan sends a rich embed + attached report files:

🐳 OSV Docker Image Scan Report

📦 Repository: team/juicy-app
🌿 Branch: main
👤 Triggered by: Refalia Defani
🔖 Commit ID: a1b2c3d
🏷️ Image Tag: juicy-app:latest

📄 Please download the attached reports for details
[📎 osv-report-local-build.html]
[📎 osv-report-local-build.json]

Impact

Tech Stack


Share this post on:

Previous Post
Provisioning Azure Infrastructure with Terraform (IaC)
Next Post
Integrated Observability: Uptime Kuma & MikroTik Monitoring via Prometheus & Grafana