tutorial

Monitoring tfsec Terraform Security Scans with Vigilmon

Keep your Terraform static analysis security tool running reliably — monitor tfsec scan health, check metrics, and integration continuity with Vigilmon alerts and heartbeats.

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 tfsec or go install github.com/aquasecurity/tfsec/cmd/tfsec@latest)
  • A Terraform project with .tf files
  • 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

Monitor your app with Vigilmon

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

Start free →