tutorial

How to Monitor Sobelow with Vigilmon

Integrate Sobelow security analysis into your Phoenix CI pipeline and use Vigilmon to alert your team when security scans stop running or discover new vulnerabilities.

How to Monitor Sobelow with Vigilmon

Sobelow is a static security analysis tool purpose-built for Phoenix applications. It scans your Elixir source code for vulnerabilities that commonly appear in web applications: SQL injection, cross-site scripting (XSS), directory traversal, unsafe file operations, insecure configurations, and more — all without running the application.

Like all static analysis tools, Sobelow only protects you when it runs. A disabled CI step, a suppressed finding that grows into a real vulnerability, or a skipped scan on a security-critical PR can expose your application to attacks that Sobelow would have caught. Vigilmon heartbeat monitoring ensures your security gate is always active — and alerts you immediately when it goes dark.


Why Monitor Sobelow?

Phoenix applications face the same web vulnerability classes as any other framework. Sobelow catches the most common ones at development time:

  • SQL injection via raw Ecto.Adapters.SQL.query/3 calls with interpolated input
  • XSS via unescaped raw/1 in templates or Phoenix.HTML.raw/1 with user-controlled content
  • Directory traversal in file read/write operations using unvalidated user input
  • Insecure configs — missing force_ssl, weak secret_key_base in dev seeping into prod, exposed debugging endpoints
  • CSRF vulnerabilities — routes that bypass the built-in Phoenix CSRF protection

Sobelow catches these before code review. But only if it runs.


Key Metrics to Track

| Metric | What it tells you | |--------|-------------------| | Sobelow CI pass rate | Whether security scans are running and passing | | Finding count by severity | High/medium/low vulnerability distribution over time | | New findings per PR | Security regression rate | | Scan coverage | Whether new modules are being included in scans | | Time since last scan | Whether the security gate has been silently disabled |


Step 1: Add Sobelow to Your Project

# mix.exs
defp deps do
  [
    {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false},
    # rest of your deps
  ]
end
mix deps.get
mix sobelow --help

Run a baseline scan to see your current security posture:

mix sobelow --config

This creates a .sobelow-conf file with your chosen settings. Review and commit it — it captures your team's explicit security policy decisions.


Step 2: Configure Sobelow for CI

Create a CI-specific configuration that fails on any high-severity finding:

# .sobelow-conf (committed to repo)
[
  verbose: false,
  private: false,
  skip: false,
  router: "lib/my_app_web/router.ex",
  exit: :low,     # fail on low-severity and above
  format: :json,
  out: "sobelow-report.json",
  ignore: [],
  ignore_files: []
]

For CI, run with --exit to ensure a non-zero exit code on findings:

mix sobelow --exit low --format json --out sobelow-report.json

Step 3: Emit a Heartbeat After Clean Scans

Only ping Vigilmon when Sobelow completes with no findings (or only below-threshold findings). A missed heartbeat means either a vulnerability was found or the scan didn't run at all.

#!/bin/bash
# scripts/ci_security.sh
set -e

echo "Running Sobelow security analysis..."
mix sobelow --exit low --format json --out sobelow-report.json

FINDING_COUNT=$(python3 -c "import json,sys; d=json.load(open('sobelow-report.json')); print(len(d.get('findings', [])))" 2>/dev/null || echo "0")

echo "Sobelow findings: $FINDING_COUNT"

if [ "$FINDING_COUNT" -eq "0" ]; then
  echo "No security findings. Pinging Vigilmon..."
  if [ -n "$VIGILMON_SOBELOW_HEARTBEAT_URL" ]; then
    curl -fsS "$VIGILMON_SOBELOW_HEARTBEAT_URL" > /dev/null
    echo "Heartbeat sent."
  fi
else
  echo "Security findings detected. Not sending heartbeat."
  exit 1
fi

GitHub Actions workflow:

# .github/workflows/security.yml
name: Security Analysis

on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: '0 2 * * *'  # Nightly scan

jobs:
  sobelow:
    name: Sobelow Security Scan
    runs-on: ubuntu-latest
    env:
      VIGILMON_SOBELOW_HEARTBEAT_URL: ${{ secrets.VIGILMON_SOBELOW_HEARTBEAT_URL }}
    steps:
      - uses: actions/checkout@v4

      - uses: erlef/setup-beam@v1
        with:
          elixir-version: '1.16'
          otp-version: '26'

      - run: mix deps.get

      - name: Run Sobelow
        run: mix sobelow --exit low --format json --out sobelow-report.json

      - name: Upload security report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: sobelow-report
          path: sobelow-report.json

      - name: Ping Vigilmon heartbeat
        if: success()
        run: curl -fsS "$VIGILMON_SOBELOW_HEARTBEAT_URL"

The scheduled nightly scan catches vulnerabilities introduced through dependency updates, even without new commits.


Step 4: Create a Heartbeat Monitor in Vigilmon

  1. Sign in at vigilmon.online
  2. Click New Monitor → Heartbeat
  3. Name it Sobelow Security Scan — main branch
  4. Set the expected interval to 25 hours (covers the nightly scan with a small buffer)
  5. Copy the URL and add it as VIGILMON_SOBELOW_HEARTBEAT_URL in your CI secrets

If the nightly scan finds a vulnerability, fails to run, or CI is broken, no heartbeat is sent and Vigilmon alerts your security team.


Step 5: Track Security Findings in Your Health Endpoint

For applications that run Sobelow as part of a pre-deploy check, expose the scan result in your health endpoint:

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = %{
      database: check_db(),
      security_scan: check_security_scan()
    }

    status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{
      status: if(status == 200, do: "ok", else: "degraded"),
      checks: checks
    }))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp check_db do
    case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
      {:ok, _} -> :ok
      _ -> :error
    end
  end

  defp check_security_scan do
    # Check whether last Sobelow scan is recent (e.g. within 26 hours)
    scan_age_hours = Application.get_env(:my_app, :last_sobelow_scan_age_hours, 0)
    if scan_age_hours < 26, do: :ok, else: :error
  end
end

This adds security scan freshness to your uptime signal — so a production health check also reflects whether security scanning is keeping pace.


Step 6: Alerting for Security Teams

In Vigilmon, go to Notifications → New Channel and configure escalating alerts:

Slack (immediate):

🔴 MISSED: Sobelow Security Scan — main branch
Last clean scan: 27 hours ago (expected every 25 hours)
Action: Check CI pipeline — possible security finding or broken scan step

PagerDuty (for high-severity findings):

Configure a separate alert channel that fires when the heartbeat misses twice — indicating a persistent finding blocking deploys:

  1. Go to Notifications → New Channel → PagerDuty
  2. Set escalation to fire after 2 consecutive missed heartbeats
  3. Route to your security on-call rotation

Step 7: Common Sobelow Findings and Remediation

Monitor which finding categories appear most often so you can address the root cause:

XSS via raw/1:

# Vulnerable
<%= raw(user.bio) %>

# Safe
<%= user.bio %>  # Phoenix auto-escapes by default

SQL injection via interpolation:

# Vulnerable
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM users WHERE id = #{id}", [])

# Safe
Ecto.Adapters.SQL.query(Repo, "SELECT * FROM users WHERE id = $1", [id])

Directory traversal:

# Vulnerable
File.read!("uploads/" <> params["filename"])

# Safe
safe_filename = Path.basename(params["filename"])
File.read!(Path.join("uploads", safe_filename))

Fixing these patterns upstream means fewer Sobelow findings and fewer missed heartbeats.


What You've Built

| What | How | |------|-----| | Sobelow CI integration | mix sobelow --exit low in dedicated CI job | | Nightly security scan | Scheduled GitHub Actions workflow | | Security gate monitoring | Vigilmon heartbeat — alerts when scan misses | | Immediate failure alerts | Slack notification channel | | On-call escalation | PagerDuty for persistent security findings | | Production health check | HTTP monitor including scan freshness signal | | Security report artifacts | JSON report uploaded as CI artifact |

Sobelow catches vulnerabilities at commit time. Vigilmon ensures it never silently stops catching them.


Next Steps

  • Add Sobelow to pre-commit hooks for local security feedback before CI
  • Track finding counts per category in a time-series dashboard to spot vulnerability trends
  • Configure separate heartbeat intervals for on-push scans (2 hours) vs. nightly dependency scans (25 hours)
  • Use Vigilmon's incident history to audit whether security scan gaps correlate with reported vulnerabilities

Get started free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →