Terraform security starts before terraform apply. tfsec analyzes your Terraform code for hundreds of security misconfigurations — unencrypted S3 buckets, public RDS instances, security groups with wildcard ingress, and more — before a single cloud resource is created. A failed tfsec scan that nobody notices is infrastructure deployed without security review.
Teams that take tfsec seriously run it in CI on every pull request and on a scheduled baseline against their main branch. But the scan step itself needs monitoring: CI runners fail, credentials expire, configuration drift causes scans to skip files silently. The only way to know tfsec is actually running is to monitor it explicitly.
This tutorial shows how to integrate Vigilmon with tfsec to monitor scan continuity, check pass/fail trends, and alert when the security scan layer goes dark.
What You'll Build
- A heartbeat monitor confirming tfsec runs on schedule
- Webhook-based check metrics reporting to Vigilmon
- Alerting for scan failures, error spikes, and severity regressions
- A scheduled baseline scan with trend comparison
Prerequisites
- A Vigilmon account at vigilmon.online
- A Vigilmon API key (Settings → API Keys)
- tfsec installed (
brew install tfsecorgo install github.com/aquasecurity/tfsec/cmd/tfsec@latest) - A Terraform project with
.tffiles - GitHub Actions or equivalent CI system
Step 1: Get tfsec Producing JSON Output
tfsec supports multiple output formats. For Vigilmon integration, JSON gives you structured data you can parse and forward:
# Basic scan with JSON output
tfsec . \
--format json \
--out tfsec-results.json
echo "tfsec exit code: $?"
The output JSON has a results array where each item is a failing check. A key shape:
{
"results": [
{
"rule_id": "aws-s3-enable-bucket-encryption",
"description": "Bucket does not have encryption enabled",
"severity": "HIGH",
"location": {
"filename": "modules/storage/main.tf",
"start_line": 12
},
"passed": false
}
],
"statistics": {
"passed": 124,
"failed": 3,
"ignored": 0,
"critical": 0,
"high": 2,
"medium": 1,
"low": 0
}
}
Like Terrascan, tfsec returns a non-zero exit code when checks fail. Use || true to capture results without failing the CI step prematurely:
tfsec . --format json --out tfsec-results.json || true
Step 2: Create a Heartbeat Monitor
The heartbeat pattern answers: "Did tfsec actually run in the last 24 hours?"
In Vigilmon, go to Monitors → New Monitor → Heartbeat:
- Name: tfsec - main branch
- Expected interval: 24 hours
- Grace period: 2 hours
Add the heartbeat ping to your CI workflow:
# .github/workflows/tfsec.yml
name: tfsec Terraform Security Analysis
on:
pull_request:
push:
branches: [main]
paths: ['**.tf', '**/*.tfvars']
schedule:
- cron: '0 3 * * *' # Daily at 3am UTC
jobs:
tfsec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install tfsec
run: |
curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash
- name: Run tfsec scan
run: |
tfsec . \
--format json \
--out tfsec-results.json \
--no-color || true
- name: Ping Vigilmon heartbeat
if: always()
run: |
curl -s -X POST "${{ secrets.VIGILMON_TFSEC_HEARTBEAT_URL }}"
- name: Report metrics to Vigilmon
if: always()
env:
VIGILMON_WEBHOOK_URL: ${{ secrets.VIGILMON_WEBHOOK_URL }}
run: |
chmod +x ./scripts/report-tfsec-metrics.sh
./scripts/report-tfsec-metrics.sh
The heartbeat fires on if: always() — whether checks pass, fail, or the scan errors out. That way you know the job ran. The separate metrics reporting step gives you the detail.
Step 3: Parse and Report Check Metrics
#!/bin/bash
# scripts/report-tfsec-metrics.sh
RESULTS_FILE="tfsec-results.json"
VIGILMON_WEBHOOK="${VIGILMON_WEBHOOK_URL}"
if [ ! -f "$RESULTS_FILE" ]; then
curl -s -X POST "$VIGILMON_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{
"status": "down",
"message": "tfsec results file missing — scan likely crashed or skipped"
}'
exit 0
fi
# Extract statistics (tfsec 1.x format)
CRITICAL=$(jq '.statistics.critical // 0' "$RESULTS_FILE")
HIGH=$(jq '.statistics.high // 0' "$RESULTS_FILE")
MEDIUM=$(jq '.statistics.medium // 0' "$RESULTS_FILE")
LOW=$(jq '.statistics.low // 0' "$RESULTS_FILE")
PASSED=$(jq '.statistics.passed // 0' "$RESULTS_FILE")
FAILED=$(jq '.statistics.failed // 0' "$RESULTS_FILE")
TOTAL_FAILURES=$((CRITICAL + HIGH + MEDIUM + LOW))
# Map to Vigilmon status
STATUS="healthy"
if [ "$CRITICAL" -gt 0 ]; then
STATUS="down"
elif [ "$HIGH" -gt 0 ]; then
STATUS="degraded"
fi
MESSAGE="tfsec: ${TOTAL_FAILURES} failures (CRIT:${CRITICAL} HIGH:${HIGH} MED:${MEDIUM} LOW:${LOW}), ${PASSED} passed"
curl -s -X POST "$VIGILMON_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{
\"status\": \"$STATUS\",
\"message\": \"$MESSAGE\",
\"metadata\": {
\"critical\": $CRITICAL,
\"high\": $HIGH,
\"medium\": $MEDIUM,
\"low\": $LOW,
\"passed\": $PASSED,
\"failed\": $FAILED
}
}"
echo "Vigilmon updated: $STATUS — $MESSAGE"
This maps tfsec severity levels to Vigilmon status:
- Any CRITICAL failures →
down(infrastructure with critical security issues shouldn't deploy) - HIGH failures →
degraded(serious issues that warrant immediate attention) - MEDIUM/LOW only →
healthy(issues to address, but not blocking)
Adjust the thresholds based on your team's risk tolerance.
Step 4: Module-Level Scanning with Separate Monitors
Large Terraform codebases usually have multiple modules with different owners. Monitor each module separately so a violation spike in the networking module doesn't get buried under the storage module's clean bill of health.
#!/bin/bash
# scripts/scan-all-modules.sh
MODULES_DIR="./modules"
VIGILMON_API_KEY="${VIGILMON_API_KEY}"
for module_dir in "$MODULES_DIR"/*/; do
MODULE_NAME=$(basename "$module_dir")
RESULTS_FILE="tfsec-${MODULE_NAME}.json"
echo "Scanning module: $MODULE_NAME"
tfsec "$module_dir" --format json --out "$RESULTS_FILE" || true
HIGH=$(jq '.statistics.high // 0' "$RESULTS_FILE" 2>/dev/null || echo 0)
CRITICAL=$(jq '.statistics.critical // 0' "$RESULTS_FILE" 2>/dev/null || echo 0)
STATUS="healthy"
[ "$HIGH" -gt 0 ] && STATUS="degraded"
[ "$CRITICAL" -gt 0 ] && STATUS="down"
# Each module maps to its own Vigilmon webhook (set up in CI secrets)
WEBHOOK_VAR="VIGILMON_WEBHOOK_${MODULE_NAME^^}"
WEBHOOK_URL="${!WEBHOOK_VAR:-$VIGILMON_DEFAULT_WEBHOOK}"
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{
\"status\": \"$STATUS\",
\"message\": \"tfsec [$MODULE_NAME]: CRIT=${CRITICAL} HIGH=${HIGH}\",
\"metadata\": {\"module\": \"$MODULE_NAME\", \"critical\": $CRITICAL, \"high\": $HIGH}
}"
done
In Vigilmon, create a monitor per module: tfsec - networking, tfsec - storage, tfsec - compute. This gives individual module owners visibility into their own security scan status.
Step 5: Enforce a Severity Budget with PR Checks
Monitoring isn't just about alerting after the fact — it can gate pull requests. Set a maximum severity budget per scan and fail PRs that exceed it, while reporting the verdict to Vigilmon.
- name: Enforce severity budget
env:
MAX_HIGH: 5
MAX_CRITICAL: 0
VIGILMON_WEBHOOK_URL: ${{ secrets.VIGILMON_WEBHOOK_URL }}
run: |
CRITICAL=$(jq '.statistics.critical // 0' tfsec-results.json)
HIGH=$(jq '.statistics.high // 0' tfsec-results.json)
BUDGET_EXCEEDED=false
MESSAGE="tfsec budget check: CRIT=${CRITICAL}/${MAX_CRITICAL} HIGH=${HIGH}/${MAX_HIGH}"
if [ "$CRITICAL" -gt "$MAX_CRITICAL" ] || [ "$HIGH" -gt "$MAX_HIGH" ]; then
BUDGET_EXCEEDED=true
STATUS="degraded"
else
STATUS="healthy"
fi
curl -s -X POST "$VIGILMON_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"status\": \"$STATUS\", \"message\": \"$MESSAGE\"}"
if [ "$BUDGET_EXCEEDED" = "true" ]; then
echo "FAIL: Security severity budget exceeded"
exit 1
fi
echo "PASS: Within severity budget"
This creates an audit trail in Vigilmon: every PR's tfsec verdict is recorded as a webhook event, giving you a searchable history of when budgets were exceeded and by which PR.
Step 6: Alert on Terraform Configuration Drift
Set up a weekly scheduled scan that detects configuration drift — cases where tfsec results change on the main branch without an associated PR (which can indicate direct commits or automated infrastructure changes that bypassed review).
# .github/workflows/tfsec-drift.yml
name: tfsec Drift Detection
on:
schedule:
- cron: '0 8 * * 1' # Every Monday at 8am UTC
jobs:
drift-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install tfsec
run: curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash
- name: Run weekly baseline scan
run: tfsec . --format json --out tfsec-baseline.json || true
- name: Compare against cached baseline and alert on drift
env:
VIGILMON_WEBHOOK_URL: ${{ secrets.VIGILMON_WEBHOOK_URL }}
run: |
CURRENT_HIGH=$(jq '.statistics.high // 0' tfsec-baseline.json)
CURRENT_CRITICAL=$(jq '.statistics.critical // 0' tfsec-baseline.json)
# Store in GitHub Actions cache between runs for comparison
PREV_HIGH=$(cat .tfsec-baseline-high 2>/dev/null || echo 0)
PREV_CRITICAL=$(cat .tfsec-baseline-critical 2>/dev/null || echo 0)
echo "$CURRENT_HIGH" > .tfsec-baseline-high
echo "$CURRENT_CRITICAL" > .tfsec-baseline-critical
HIGH_DELTA=$((CURRENT_HIGH - PREV_HIGH))
CRIT_DELTA=$((CURRENT_CRITICAL - PREV_CRITICAL))
STATUS="healthy"
MESSAGE="tfsec weekly: HIGH=${CURRENT_HIGH} (delta:${HIGH_DELTA}) CRIT=${CURRENT_CRITICAL} (delta:${CRIT_DELTA})"
if [ "$HIGH_DELTA" -gt 0 ] || [ "$CRIT_DELTA" -gt 0 ]; then
STATUS="degraded"
fi
curl -s -X POST "$VIGILMON_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"status\": \"$STATUS\", \"message\": \"$MESSAGE\"}"
Monitoring Coverage Summary
| What to monitor | Monitor type | Interval | Alert on | |---|---|---|---| | tfsec running on main | Heartbeat | 24h | Missed ping | | PR scan heartbeat | Heartbeat | Per PR | Missed ping | | Critical check failures | Webhook | Per scan | Count > 0 | | High severity failures | Webhook | Per scan | Count > threshold | | Module-level scan health | Webhook per module | Per scan | Status ≠ healthy | | Weekly drift vs baseline | Script + webhook | Weekly | Delta > 0 |
Static analysis is a preventive control, not a detective one. When tfsec stops running, you lose that prevention layer entirely, and the next misconfigurations deploy silently. Vigilmon turns tfsec from a CI checkbox into an observable, alertable system — one where you know immediately when scans fail, when severity counts spike, or when a whole scan job went dark.
Start monitoring your Terraform security pipeline free at vigilmon.online
Tags: #tfsec #terraform #security #devsecops #monitoring