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.
| Input | Output | Contract |
|---|---|---|
POST /api/v1/sbom with Bearer token + SBOM body | RabbitMQ message on taco.sbom exchange | Routing 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.
| Input | Output | Contract |
|---|---|---|
GET /api/v1/token/validate with Bearer token | 200 { project_id, project_name } or 401 | HTTP 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.
| Input | Output | Contract |
|---|---|---|
RabbitMQ message from taco.sbom.store queue | MySQL rows in sboms, components, sbom_components | Direct 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).
| Input | Output | Contract |
|---|---|---|
MySQL components table + TacoDB file | MySQL findings table + RabbitMQ finding.new events | DB 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 requestsfinding_snapshotsโ Daily trend data
Scan cycle flow:
- Acquire distributed lock (
GET_LOCK('taco_scanner')) - Hash TacoDB โ decide full scan vs incremental
- Fetch components (all if DB changed, new + active-findings if not)
- Match against vulnerabilities
- Diff against existing active findings
- Insert new, resolve old
- Publish alerts for new findings
- 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).
| Input | Output | Contract |
|---|---|---|
MySQL findings where enrichment_status = 'pending' | Updated findings + RabbitMQ enrichment.complete events | DB 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).
| Input | Output | Contract |
|---|---|---|
RabbitMQ enrichment.complete + finding.new events | Notifications (email, Slack, Discord, webhook, PagerDuty) + MySQL alert_history | External 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:
- Parse event โ extract findings
- Rate limit check (5 min per project)
- Load alert config (project-specific, fallback to default)
- Per-finding dedup by
alert_key={project_id}:{cve_id}:{component_name} - Filter: skip accepted/suppressed, filter by min_severity
- Evaluate 6 rules: KEV, critical risk, high EPSS, no fix, SLA breach, regression
- Dispatch via enabled channels
- 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 configPUT /api/v1/projects/:id/alert-configโ Create/update configGET /api/v1/projects/:id/alertsโ Paginated alert historyGET /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).
| Input | Output | Contract |
|---|---|---|
| HTTP requests from taco-nextjs | JSON responses | REST 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,projectsapi_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_logproject_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.
| Input | Output | Contract |
|---|---|---|
MySQL sboms (raw content) | MySQL secrets table | DB 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.
| Input | Output | Contract |
|---|---|---|
| User interactions | HTTP calls to taco-api | REST 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
| From | To | Mechanism | Contract |
|---|---|---|---|
| External CI/CD | taco-inbound | HTTP POST | Bearer token + SBOM JSON body |
| taco-inbound | taco-auth | HTTP GET | Token in header, returns project_id |
| taco-inbound | taco-store | RabbitMQ | taco.sbom exchange, sbom.ingest key |
| taco-store | taco-scanner | Shared DB | Scanner reads components by timestamp |
| taco-scanner | taco-enricher | Shared DB | Enricher reads findings where enrichment_status = 'pending' |
| taco-scanner | taco-alert | RabbitMQ | taco.findings exchange, finding.new key |
| taco-enricher | taco-alert | RabbitMQ | taco.findings exchange, enrichment.complete key |
| taco-fetcher | taco-inbound | HTTP POST | Same as external CI/CD |
| taco-nextjs | taco-api | HTTP | REST API with JWT auth |
| taco-api | all services | Shared DB | Read-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).