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?
- Open-source by Google — free, no vendor lock-in
- Scans multiple ecosystems (npm, Go, Python, Docker images)
- Outputs both HTML (human-readable) and JSON (machine-parseable)
- Uses the comprehensive OSV.dev vulnerability database
Why YAML Anchors (&discord_notify)?
- DRY principle — notification logic written once, reused across all jobs
- Consistent reporting format across different scan types
- Easy to maintain — change once, applies everywhere
Why Docker-in-Docker (DinD)?
- Required for scanning Docker images inside GitLab CI runners
docker:24.0.5-dindservice provides isolated Docker daemon- Retry loop ensures DinD is ready before proceeding
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
- Proactive vulnerability detection across 3 attack surfaces (base image, dependencies, built app)
- Zero-friction reporting — team gets notified instantly on Discord with downloadable reports
- Full audit trail — JSON artifacts stored in GitLab for compliance
- Shift-left security — vulnerabilities caught before reaching production
Tech Stack
- Scanner: OSV-Scanner v2.0.2 (Google)
- CI/CD: GitLab CI/CD (.gitlab-ci.yml)
- Container: Docker-in-Docker (DinD)
- Notifications: Discord Webhooks (embeds + file attachments)
- Artifacts: HTML + JSON reports
- Images Scanned: golang:1.25.5-bookworm, node:22-bookworm, custom app builds