taco-alert

Alerting service. Consumes enriched findings from RabbitMQ, evaluates rule-based alert conditions, and dispatches notifications to configured channels.

Alerting service. Consumes enriched findings from RabbitMQ, evaluates rule-based alert conditions, and dispatches notifications to configured channels.

Overview

PropertyValue
LanguageGo
FrameworkStandard library HTTP
Port8083 (health check + API)
ExposedInternal only (127.0.0.1:8083)
DependenciesMySQL, RabbitMQ
Repositorytacosec/taco-alert

Architecture

taco-enricher                    taco-alert
+------------------+            +---------------------------+
| enrichment       |            | consumer/                 |
| worker           |            |   RabbitMQ consumer       |
|                  |            |   (reconnect, DLX, QoS)   |
| publisher/       |            +----------+----------------+
|   PublishEnrich  |                       |
|   mentComplete() | --[RabbitMQ]--->      v
+------------------+  exchange:      +-----+-----+
                      taco.findings  | alerter/  |
                      routing key:   |           |
                      enrichment     | - ProcessEvent()
                      .complete      | - Rule evaluation
                                     | - Severity filter
                                     | - Dedup check
                                     | - Dispatch
                                     +-----+-----+
                                           |
                          +-------+--------+--------+--------+
                          |       |        |        |        |
                        Email   Slack   Discord  Webhook  PagerDuty
                       (Mailgun)

Data Flow

  1. taco-enricher enriches findings with EPSS, CVSS, CISA KEV, and risk scores.
  2. After enrichment, publisher.PublishEnrichmentComplete() groups findings by project and publishes one EnrichmentEvent per project to taco.findings exchange with routing key enrichment.complete.
  3. taco-alert consumes from taco.findings.alert queue (bound to finding.new and enrichment.complete).
  4. Each event goes through alerter.ProcessEvent(): rate limiting, config lookup, severity filtering, deduplication, rule evaluation, dispatch, and history recording.

RabbitMQ Topology

ComponentValue
Exchangetaco.findings (topic)
Queuetaco.findings.alert (durable)
Routing keys boundfinding.new, enrichment.complete
Dead-letter exchangetaco.findings.dlx (fanout)
Prefetch1 message
Max retries3 (then dead-lettered)

Key Packages

PackagePurpose
alerter/Core alert logic: rule evaluation, severity filtering, dedup, dispatch, rate limiting, history
alerter/rules.goAlert rule definitions and evaluation engine
alerter/config.goAlert configuration model and MySQL queries
alerter/history.goAlert history persistence and dedup checks
consumer/RabbitMQ consumer with reconnection, DLX, and retry logic
handler/HTTP API: alert config CRUD, history, stats, test alerts
notify/Channel implementations: Email, Slack, Discord, Webhook, PagerDuty
config/Environment variable loading
migrations/MySQL schema migrations

Rule Engine

Built-in Rules

Defined in alerter/rules.go. Each rule has an ID, name, severity, and Evaluate(EnrichedFinding) bool.

Rule IDNameSeverityCondition
kev_criticalKnown Exploited VulnerabilitycriticalKnownExploited == true
critical_risk_scoreCritical Risk ScorecriticalRiskScore >= 9.5
high_epssHigh Exploit ProbabilityhighEpssScore >= 0.20
no_fix_availableNo Fix AvailablemediumFixedVersion == "" AND severity >= high
sla_breachSLA BreachhighAge exceeds SLA (critical: 3d, high: 7d, medium: 30d)
regressionVulnerability RegressionhighIsRegression == true

Evaluation Flow

gotriggered := EvaluateRules(a.rules, finding)
if len(triggered) == 0 {
    continue // no rules matched, skip
}

EvaluateRules() runs every rule against the finding and returns matched rules. HighestRuleSeverity() determines overall alert severity.

How to Add a New Rule

  1. Add entry to DefaultRules() in alerter/rules.go.
  2. If needed, add fields to EnrichedFinding in alerter/alerter.go.
  3. Run go build ./... and go vet ./....
  4. Add test in alerter/alerter_test.go.

Notification Channels

ChannelFileTransportKey Details
Emailnotify/email.goMailgun APIHTML + plain-text, recipient list from JSON
Slacknotify/slack.goIncoming webhookBlock Kit, severity emoji, max 10 findings
Discordnotify/discord.goWebhookRich embeds, color-coded severity
Webhooknotify/webhook.goHTTP POSTHMAC-SHA256 signing via X-TACO-Signature
PagerDutynotify/pagerduty.goEvents API v2Dedup key: taco-{project_id}-{event_id}

How to Add a New Channel

  1. Create sender in notify/ (e.g., notify/teams.go).
  2. Add to Notifier struct in notify/notifier.go.
  3. Add interface method in alerter/alerter.go.
  4. Add config fields in alerter/config.go and handler/handler.go.
  5. Add migration for alert_configs columns.
  6. Add dispatch logic in alerter.dispatch().
  7. Update test handler in handler/handler.go.

Alert Processing Pipeline

  1. Unmarshal -- Deserialize RabbitMQ message into EnrichmentEvent.
  2. Rate limit -- Max one alert per project per 5 minutes (requeue if limited).
  3. Config lookup -- Query alert_configs for project, fall back to default (project_id IS NULL).
  4. Per-finding loop:
    • Skip accepted or suppressed findings.
    • Skip below cfg.MinSeverity (low=1, medium=2, high=3, critical=4).
    • Dedup via {project_id}:{cve_id}:{component_name}.
    • Check regression (was CVE previously resolved?).
    • Evaluate all 6 rules. Skip if none trigger.
  5. Dispatch -- Send to all enabled channels. Failures logged, not blocking.
  6. Record -- Insert per-finding rows into alert_history.
  7. Update rate limit -- Record alert time for project.

Database Schema

alert_configs

Per-project configuration. project_id IS NULL is the default fallback.

sqlCREATE TABLE alert_configs (
    id                    BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    project_id            BIGINT UNSIGNED NULL,
    min_severity          ENUM('low','medium','high','critical') NOT NULL DEFAULT 'high',
    email_enabled         BOOLEAN NOT NULL DEFAULT FALSE,
    email_to              JSON NULL,
    slack_enabled         BOOLEAN NOT NULL DEFAULT FALSE,
    slack_webhook         VARCHAR(512) NULL,
    webhook_enabled       BOOLEAN NOT NULL DEFAULT FALSE,
    webhook_url           VARCHAR(512) NULL,
    webhook_secret        VARCHAR(255) NULL,
    pagerduty_enabled     BOOLEAN NOT NULL DEFAULT FALSE,
    pagerduty_routing_key VARCHAR(255) NULL,
    discord_enabled       BOOLEAN NOT NULL DEFAULT FALSE,
    discord_webhook       VARCHAR(512) NULL,
    created_at            TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at            TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_project_id (project_id)
);

alert_history

Deduplication (via alert_key) and audit trail.

sqlCREATE TABLE alert_history (
    id               BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    alert_key        VARCHAR(255) NULL,
    event_id         VARCHAR(36) NOT NULL,
    project_id       BIGINT UNSIGNED NOT NULL,
    project_name     VARCHAR(255) NOT NULL,
    cve_id           VARCHAR(50) NULL,
    component_name   VARCHAR(255) NULL,
    severity         VARCHAR(20) NULL,
    rules_triggered  JSON NULL,
    finding_count    INT UNSIGNED NOT NULL,
    channels_used    JSON NOT NULL,
    sent_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_project_id (project_id),
    INDEX idx_event_id (event_id),
    INDEX idx_alert_key (alert_key),
    INDEX idx_cve_id (cve_id)
);

Migrations

#FileDescription
001001_create_alert_configs.sqlCreates alert_configs with email and Slack
002002_create_alert_history.sqlCreates alert_history with event-level tracking
003003_add_alert_channels.sqlAdds webhook, PagerDuty, Discord to alert_configs
004004_enhance_alert_history.sqlAdds alert_key, cve_id, component_name, severity, rules_triggered

HTTP API (taco-alert, port 8083)

MethodEndpointDescription
GET/api/v1/projects/{id}/alert-configGet config (?fallback=true for default)
PUT/api/v1/projects/{id}/alert-configUpsert config
DELETE/api/v1/projects/{id}/alert-configDelete (revert to default)
POST/api/v1/projects/{id}/alert-config/testTest all enabled channels
GET/api/v1/projects/{id}/alertsPaginated history (?page=&per_page=&severity=&cve_id=)
GET/api/v1/alerts/statsGlobal stats (by_severity, last_24h/7d/30d)
GET/healthzHealth check (MySQL + RabbitMQ)

taco-api Alert Endpoints

MethodEndpointDescription
GET/api/v1/projects/{id}/alerts/configGet config (frontend)
PUT/api/v1/projects/{id}/alerts/configUpdate config (frontend)
GET/api/v1/projects/{id}/alerts/webhookEffective Slack webhook (project > subscription > account)

Enrichment Event Format

json{
  "event_id": "uuid",
  "event_type": "enrichment.complete",
  "timestamp": "2025-01-15T10:00:00Z",
  "project_id": 123,
  "project_name": "",
  "findings": [{
    "cve_id": "CVE-2024-1234",
    "severity": "critical",
    "component_name": "lodash",
    "component_version": "4.17.20",
    "component_purl": "pkg:npm/lodash@4.17.20",
    "fixed_version": "4.17.21",
    "cvss_score": 9.8,
    "epss_score": 0.45,
    "risk_score": 9.8,
    "known_exploited": true,
    "first_detected_at": "2025-01-10T08:00:00Z",
    "resolved_at": null,
    "project_status": "open"
  }]
}

Note: project_name is empty because the enricher does not have project names.

Docker Compose Configuration

yamltaco-alert:
  image: ghcr.io/tacosec/taco-alert:latest
  ports: ["127.0.0.1:8083:8083"]
  environment:
    RABBITMQ_URL: amqp://${RABBITMQ_USER:-taco}:${RABBITMQ_PASS:-taco-dev-mq}@rabbitmq:5672/
    MYSQL_DSN: ${MYSQL_USER:-taco}:${MYSQL_PASSWORD:-taco}@tcp(mysql:3306)/taco
    MAILGUN_DOMAIN: ${MAILGUN_DOMAIN}
    MAILGUN_API_KEY: ${MAILGUN_API_KEY}
    MAILGUN_FROM: ${MAILGUN_FROM}
    HEALTH_PORT: "8083"

taco-enricher:
  image: ghcr.io/tacosec/taco-enricher:latest
  environment:
    MYSQL_DSN: "${MYSQL_USER:-taco}:${MYSQL_PASSWORD:-taco}@tcp(mysql:3306)/taco?parseTime=true"
    TACO_NVD_API_KEY: ${TACO_NVD_API_KEY:-}
    RABBITMQ_URL: amqp://${RABBITMQ_USER:-taco}:${RABBITMQ_PASS:-taco-dev-mq}@rabbitmq:5672/

Important: The enricher's RABBITMQ_URL must be set for alert integration. If missing, enrichment works but no events are published to taco-alert.

Environment Variables

VariableRequiredDefaultDescription
RABBITMQ_URLYes--RabbitMQ AMQP URL
MYSQL_DSNYes--MySQL connection string
MAILGUN_DOMAINNo--Mailgun sending domain
MAILGUN_API_KEYNo--Mailgun API key
MAILGUN_FROMNoalerts@taco.devMailgun sender address
HEALTH_PORTNo8083Health check and API HTTP port

Health Check

GET /healthz -- Returns 200 OK when MySQL and RabbitMQ are reachable. Returns 503 with detail if either is down.