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/3calls with interpolated input - XSS via unescaped
raw/1in templates orPhoenix.HTML.raw/1with user-controlled content - Directory traversal in file read/write operations using unvalidated user input
- Insecure configs — missing
force_ssl, weaksecret_key_basein 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
- Sign in at vigilmon.online
- Click New Monitor → Heartbeat
- Name it
Sobelow Security Scan — main branch - Set the expected interval to 25 hours (covers the nightly scan with a small buffer)
- Copy the URL and add it as
VIGILMON_SOBELOW_HEARTBEAT_URLin 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:
- Go to Notifications → New Channel → PagerDuty
- Set escalation to fire after 2 consecutive missed heartbeats
- 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.