Service Responsibilities & Handovers

Each service's ownership boundaries, what data it owns, how handovers happen between services, and the contracts that connect them.

Ownership Principle

Each service owns exactly one responsibility. Services communicate through well-defined contracts โ€” RabbitMQ messages, HTTP APIs, or shared MySQL tables. A service never reaches into another service's internal logic; it only depends on the contract.

Service Responsibility Map

taco-inbound โ€” Ingestion Gateway

Owns: SBOM ingestion and authentication delegation.

Responsibility: Accept incoming SBOMs from external CI/CD pipelines, validate the project token, and hand off to the processing pipeline.

Does NOT own: Parsing, storage, scanning, or any analysis. It is a gateway only.

InputOutputContract
POST /api/v1/sbom with Bearer token + SBOM bodyRabbitMQ message on taco.sbom exchangeRouting key: sbom.ingest

Handover to taco-auth: Calls GET /api/v1/token/validate with the Bearer token. Expects 200 OK with { project_id, project_name } or 401.

Handover to RabbitMQ: Publishes the raw SBOM JSON + project metadata to taco.sbom exchange with routing key sbom.ingest. Message format:

json{
  "project_id": 5,
  "project_name": "webapp",
  "format": "cyclonedx",
  "raw_sbom": "{ ... full SBOM JSON ... }"
}

taco-auth โ€” Token Authentication

Owns: Project API token validation and caching.

Responsibility: Validate Bearer tokens against MySQL, cache results in Redis/Valkey.

Does NOT own: User authentication (JWT-based, handled by taco-api), OAuth, or session management.

InputOutputContract
GET /api/v1/token/validate with Bearer token200 { project_id, project_name } or 401HTTP JSON

DB tables owned: project_tokens (read-only; tokens are created by taco-api).

Cache: Valkey key token:{hash} with configurable TTL (default 5m).


taco-store โ€” SBOM Parser & Storage

Owns: SBOM parsing, component extraction, and storage.

Responsibility: Consume raw SBOMs from RabbitMQ, parse CycloneDX and SPDX formats, extract components, and persist everything to MySQL.

Does NOT own: Vulnerability matching, scanning, or any analysis of components.

InputOutputContract
RabbitMQ message from taco.sbom.store queueMySQL rows in sboms, components, sbom_componentsDirect DB writes

Handover from taco-inbound: Consumes from taco.sbom.store queue (bound to taco.sbom exchange, routing key sbom.ingest).

Handover to taco-scanner: Implicit โ€” taco-scanner reads the components table directly. No message is sent. The scanner discovers new components by comparing first_seen timestamps against its last scan time.

DB tables owned:

  • sboms โ€” SBOM metadata (project, format, version, timestamps)
  • components โ€” Extracted components (purl, name, version, type, layer)
  • sbom_components โ€” Junction table linking SBOMs to components

taco-scanner โ€” Vulnerability Scanner

Owns: Vulnerability matching, finding lifecycle (create/resolve), and scan state.

Responsibility: Periodically match components against the vulnerability database (TacoDB), create new findings, resolve old ones, and publish alerts for new discoveries.

Does NOT own: Component storage (taco-store), enrichment data (taco-enricher), or alert delivery (taco-alert).

InputOutputContract
MySQL components table + TacoDB fileMySQL findings table + RabbitMQ finding.new eventsDB writes + MQ publish

Handover from taco-store: Reads components table. No direct communication โ€” the scanner discovers new components by timestamp.

Handover to taco-alert: Publishes finding.new events to taco.findings exchange (topic). Message format:

json{
  "event_id": "uuid",
  "event_type": "finding.new",
  "project_id": 5,
  "project_name": "webapp",
  "findings": [
    {
      "cve_id": "CVE-2024-1234",
      "severity": "critical",
      "component_name": "openssl",
      "component_version": "3.0.10",
      "fixed_version": "3.0.11"
    }
  ]
}

Handover to taco-enricher: Implicit โ€” the enricher reads findings where enrichment_status = 'pending'. The scanner sets this status on insert.

DB tables owned:

  • findings โ€” Vulnerability findings (component_id, cve_id, severity, resolved_at, enrichment fields)
  • scan_state โ€” Scan cycle metadata (db_hash, components scanned, duration)
  • scan_requests โ€” Priority scan requests (SBOM-specific)
  • image_scan_requests โ€” Container image scan requests
  • finding_snapshots โ€” Daily trend data

Scan cycle flow:

  1. Acquire distributed lock (GET_LOCK('taco_scanner'))
  2. Hash TacoDB โ†’ decide full scan vs incremental
  3. Fetch components (all if DB changed, new + active-findings if not)
  4. Match against vulnerabilities
  5. Diff against existing active findings
  6. Insert new, resolve old
  7. Publish alerts for new findings
  8. Record scan state

taco-enricher โ€” CVE Enrichment

Owns: Vulnerability enrichment data (EPSS, CVSS vectors, risk scores, KEV status).

Responsibility: Periodically enrich findings with data from external APIs (NVD, FIRST EPSS, CISA KEV, OSV.dev), compute risk scores, and publish enrichment events for alerting.

Does NOT own: Finding creation/resolution (taco-scanner), alert delivery (taco-alert), or component data (taco-store).

InputOutputContract
MySQL findings where enrichment_status = 'pending'Updated findings + RabbitMQ enrichment.complete eventsDB updates + MQ publish

Handover from taco-scanner: Reads findings with enrichment_status = 'pending'. No direct message โ€” the scanner creates findings with this default status.

Handover to taco-alert: Publishes enrichment.complete events to taco.findings exchange. This is the preferred alert trigger โ€” alerts should fire after enrichment, not on raw scanner output. Message format:

json{
  "event_id": "enrich-5-1712750400000",
  "event_type": "enrichment.complete",
  "project_id": 5,
  "findings": [
    {
      "cve_id": "CVE-2024-1234",
      "severity": "critical",
      "cvss_score": 9.8,
      "cvss_vector": "CVSS:3.1/AV:N/AC:L/...",
      "epss_score": 0.65,
      "risk_score": 9.6,
      "known_exploited": true,
      "component_name": "openssl",
      "component_version": "3.0.10",
      "fixed_version": "3.0.11"
    }
  ]
}

External API contracts:

  • NVD: https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={id} (rate limited: 1/6s without key, 1/0.6s with key)
  • FIRST EPSS: https://api.first.org/data/v1/epss?cve={ids} (batch, up to 100)
  • CISA KEV: https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (cached, refreshed every 24h)
  • OSV.dev: https://api.osv.dev/v1/vulns/{id} (for non-CVE IDs like GHSA, ALAS)

Enrichment priority: NVD (primary for CVEs) โ†’ OSV (secondary, fills gaps) โ†’ EPSS (always) โ†’ KEV (always).

DB fields owned (on findings table):

  • epss_score, epss_percentile, cvss_vector, risk_score, known_exploited, enrichment_status

taco-alert โ€” Alert Dispatcher

Owns: Alert rules, notification delivery, alert history, and per-project alert configuration.

Responsibility: Consume enrichment events, evaluate alert rules, deduplicate per finding, and dispatch notifications through configured channels.

Does NOT own: Finding data (taco-scanner), enrichment data (taco-enricher), or project/user management (taco-api).

InputOutputContract
RabbitMQ enrichment.complete + finding.new eventsNotifications (email, Slack, Discord, webhook, PagerDuty) + MySQL alert_historyExternal API calls + DB writes

Handover from taco-enricher: Consumes from taco.findings.alert queue, bound to taco.findings exchange with routing keys finding.new and enrichment.complete.

Alert processing flow:

  1. Parse event โ†’ extract findings
  2. Rate limit check (5 min per project)
  3. Load alert config (project-specific, fallback to default)
  4. Per-finding dedup by alert_key = {project_id}:{cve_id}:{component_name}
  5. Filter: skip accepted/suppressed, filter by min_severity
  6. Evaluate 6 rules: KEV, critical risk, high EPSS, no fix, SLA breach, regression
  7. Dispatch via enabled channels
  8. Record in alert_history

DB tables owned:

  • alert_configs โ€” Per-project notification settings (channels, thresholds)
  • alert_history โ€” Audit log of sent alerts (alert_key, rules triggered, channels used)

HTTP API (port 8083):

  • GET /api/v1/projects/:id/alert-config โ€” Read config
  • PUT /api/v1/projects/:id/alert-config โ€” Create/update config
  • GET /api/v1/projects/:id/alerts โ€” Paginated alert history
  • GET /api/v1/alerts/stats โ€” Global stats

taco-api โ€” REST API

Owns: User management, authentication (JWT), subscriptions, projects, billing, and the frontend-facing API.

Responsibility: Serve all data to the frontend, manage user/project/subscription CRUD, handle OAuth, proxy alert config to taco-alert's DB.

Does NOT own: SBOM ingestion (taco-inbound), scanning (taco-scanner), enrichment (taco-enricher), or alert dispatch (taco-alert).

InputOutputContract
HTTP requests from taco-nextjsJSON responsesREST API

Key handover: taco-api reads from tables owned by other services (findings, components, sboms, alert_configs) but only writes to its own tables (users, subscriptions, projects, api_tokens, project_finding_statuses).

DB tables owned:

  • users, subscriptions, subscription_members, projects
  • api_tokens โ€” Project API tokens (used by taco-auth for validation)
  • project_finding_statuses โ€” User-driven finding status overrides (accepted, suppressed, fixed)
  • finding_audit_log, audit_log
  • project_risk_config โ€” Risk score multipliers

Cross-service reads:

  • findings (owned by scanner, enriched by enricher)
  • components, sboms, sbom_components (owned by store)
  • secrets (owned by taco-secrets)
  • alert_configs, alert_history (owned by taco-alert)

taco-secrets โ€” Secret Detection

Owns: Secret scanning rules and detection results.

Responsibility: Scan SBOM raw content for leaked credentials using regex-based rules.

InputOutputContract
MySQL sboms (raw content)MySQL secrets tableDB reads + writes

DB tables owned: secrets, secrets_scan_state


taco-fetcher โ€” Registry Fetcher

Owns: External registry connections and automatic SBOM fetching.

Responsibility: Poll configured artifact registries and submit discovered SBOMs to taco-inbound.

Handover to taco-inbound: POSTs SBOMs to POST /api/v1/sbom with a service token, starting the standard ingestion pipeline.


taco-nextjs โ€” Frontend

Owns: User interface and client-side experience.

Responsibility: Render the portal, call taco-api for all data, handle user interactions.

Does NOT own: Any data. All reads and writes go through taco-api.

InputOutputContract
User interactionsHTTP calls to taco-apiREST API

Handover Map

CI/CD Pipeline
     |
     | POST /api/v1/sbom (Bearer token)
     v
[taco-inbound] โ”€โ”€HTTPโ”€โ”€> [taco-auth] (token validation)
     |
     | RabbitMQ: taco.sbom / sbom.ingest
     v
[taco-store] โ”€โ”€writesโ”€โ”€> MySQL (sboms, components)
     |
     | (implicit: scanner reads components table)
     v
[taco-scanner] โ”€โ”€writesโ”€โ”€> MySQL (findings)
     |                |
     | RabbitMQ:      | (implicit: enricher reads pending findings)
     | finding.new    v
     |          [taco-enricher] โ”€โ”€writesโ”€โ”€> MySQL (enrichment fields)
     |                |
     |                | RabbitMQ: enrichment.complete
     |                v
     +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> [taco-alert] โ”€โ”€dispatchesโ”€โ”€> Email, Slack, Discord, Webhook, PagerDuty
                       |
                       | writes
                       v
                  MySQL (alert_history)

[taco-fetcher] โ”€โ”€HTTP POSTโ”€โ”€> [taco-inbound] (same pipeline)
[taco-secrets] โ”€โ”€reads/writesโ”€โ”€> MySQL (sboms โ†’ secrets)
[taco-nextjs] โ”€โ”€HTTPโ”€โ”€> [taco-api] โ”€โ”€readsโ”€โ”€> MySQL (all tables)

Handover Contracts Summary

FromToMechanismContract
External CI/CDtaco-inboundHTTP POSTBearer token + SBOM JSON body
taco-inboundtaco-authHTTP GETToken in header, returns project_id
taco-inboundtaco-storeRabbitMQtaco.sbom exchange, sbom.ingest key
taco-storetaco-scannerShared DBScanner reads components by timestamp
taco-scannertaco-enricherShared DBEnricher reads findings where enrichment_status = 'pending'
taco-scannertaco-alertRabbitMQtaco.findings exchange, finding.new key
taco-enrichertaco-alertRabbitMQtaco.findings exchange, enrichment.complete key
taco-fetchertaco-inboundHTTP POSTSame as external CI/CD
taco-nextjstaco-apiHTTPREST API with JWT auth
taco-apiall servicesShared DBRead-only access to other services' tables

Key Design Decisions

Why RabbitMQ for some handovers and shared DB for others?

  • RabbitMQ is used where the producer shouldn't wait for the consumer (async pipelines): SBOM ingestion, alert dispatch. The producer publishes and moves on.
  • Shared DB is used where the consumer needs to batch/poll on its own schedule: scanner reads all components at once, enricher processes findings in batches. A message-per-item approach would be wasteful here.

Why does taco-api read other services' tables?

The frontend needs to display data from multiple services in a single view (findings with enrichment data, components with SBOM context). Rather than building an API gateway that aggregates HTTP calls to 5+ services, taco-api reads directly from MySQL. This is a pragmatic trade-off: simpler architecture at the cost of tighter DB coupling. Each service still owns writes to its tables.

Why two alert triggers (finding.new and enrichment.complete)?

The enricher adds critical context (EPSS, KEV, risk score) that the alert rules need. Alerts should primarily fire on enrichment.complete โ€” this ensures the alert contains full risk data. The finding.new trigger exists as a fallback but currently doesn't fire rules (enrichment fields are zero).