An NLP-powered triage engine that clusters raw application security alerts, maps them to OWASP Top 10 remediation playbooks, and exposes the workflow through a FastAPI REST API. Built to reduce manual review overhead in application security programs handling high alert volumes.
- Problem Statement
- Architecture
- Data Flow
- API Reference
- Project Structure
- Setup
- Configuration
- Usage
- Verified Pipeline Run
- Performance
- Technical Details
Application security tooling (SAST, DAST, SCA) generates hundreds of raw alerts per day. Most of these are duplicates, near-duplicates, or low-value noise. Security analysts spend significant time manually triaging, deduplicating, and routing these alerts to the right remediation workflows.
This tool addresses the problem by:
- Ingesting raw alert text from any scanner or pipeline.
- Generating semantic embeddings via sentence-transformers to capture alert meaning.
- Clustering similar alerts using agglomerative clustering with cosine distance.
- Classifying each alert and cluster against the OWASP Top 10 (2021) categories.
- Mapping clusters to structured remediation playbooks.
- Tracking severity distributions and false-positive rates over time.
The system ingests raw security alerts from SAST, DAST, and SCA tools through a FastAPI service, processes them through an NLP pipeline (embedding, clustering, OWASP classification), and persists results to PostgreSQL. Analysts query clusters and remediation playbooks through the same API.
The core data model maps many alerts to one cluster, with each cluster linked to an OWASP category and its corresponding remediation playbook.
The triage pipeline executes in a single pass per ingestion batch:
- Embed -- Encode raw alert text into 384-dimensional vectors using sentence-transformers (all-MiniLM-L6-v2).
- Cluster -- Group similar embeddings via agglomerative clustering with cosine distance. Threshold is configurable (default 0.35).
- Classify -- Assign severity (keyword matching + confidence scoring) and OWASP category (cosine similarity to category description embeddings).
- Map to Playbook -- Look up structured remediation steps for the dominant OWASP category in each cluster.
- Persist -- Write alerts, clusters, and run metadata to PostgreSQL.
- CI/CD pipeline or scanner sends a batch of raw alert strings to
POST /api/v1/alerts/ingest. - The triage service encodes all alerts into 384-dimensional vectors using the sentence-transformer model.
- Agglomerative clustering groups semantically similar alerts. The centroid (most representative alert) is identified for each cluster.
- Each alert is classified by severity (critical/high/medium/low/info) using keyword matching and by OWASP category using embedding similarity.
- Clusters are mapped to their dominant OWASP category and linked to the corresponding remediation playbook.
- All data is persisted to PostgreSQL. The API returns the triage result including cluster assignments and playbook references.
- The metrics endpoints aggregate severity distributions, false-positive rates, and cluster reduction ratios for dashboard consumption.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/alerts/ingest |
Bulk ingest raw alerts and trigger triage pipeline |
| GET | /api/v1/alerts |
List alerts with optional filters (severity, owasp_category, cluster_id) |
| PATCH | /api/v1/alerts/{id}/false-positive |
Toggle false-positive flag on an alert |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/clusters |
List all alert clusters |
| GET | /api/v1/clusters/{id} |
Get cluster detail with member alerts |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/playbooks |
List all OWASP Top 10 remediation playbooks |
| GET | /api/v1/playbooks/{category} |
Get playbook for a specific OWASP category |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/metrics/dashboard |
Aggregated dashboard: totals, reduction ratio, FP rate, severity breakdown |
| GET | /api/v1/metrics/severity-distribution |
Severity counts per OWASP category |
| GET | /api/v1/metrics/false-positive-rate |
Current false-positive rate |
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Liveness check |
Interactive API documentation is available at /docs (Swagger UI) when the service is running.
.
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI application with lifespan events
│ ├── config.py # Environment-driven settings (pydantic-settings)
│ ├── api/
│ │ ├── alerts.py # Alert ingestion and listing endpoints
│ │ ├── clusters.py # Cluster endpoints
│ │ ├── playbooks.py # OWASP playbook endpoints
│ │ └── metrics.py # Dashboard and metrics endpoints
│ ├── db/
│ │ ├── session.py # Async SQLAlchemy engine and session factory
│ │ └── init_db.py # Table creation and playbook seeding
│ ├── engine/
│ │ ├── embedder.py # Sentence-transformer encoding wrapper
│ │ ├── clustering.py # Agglomerative clustering with threshold tuning
│ │ ├── classifier.py # Severity and OWASP category classification
│ │ └── playbooks.py # OWASP Top 10 playbook definitions
│ ├── models/
│ │ ├── orm.py # SQLAlchemy ORM models
│ │ └── schemas.py # Pydantic request/response models
│ └── services/
│ ├── triage.py # Triage pipeline orchestration
│ └── metrics.py # Metrics computation
├── alembic/
│ ├── env.py
│ └── versions/
│ └── 001_initial.py # Initial database migration
├── scripts/
│ ├── seed_data.py # Synthetic alert generator (~800 alerts)
│ ├── smoke_test.py # Standalone NLP pipeline smoke test (no DB required)
│ └── retrain.py # Re-clustering with threshold tuning
├── alembic.ini
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── .env.example
├── .gitignore
└── README.md
- Docker and Docker Compose
- (Optional) Python 3.11+ for local development without Docker
# copy environment template
cp .env.example .env
# build and start services
docker-compose up --build -d
# check logs
docker-compose logs -f apiThe API will be available at http://localhost:8000. The Swagger UI is at http://localhost:8000/docs.
On first startup, the application automatically:
- Creates all database tables
- Seeds the 10 OWASP remediation playbooks
- Downloads the sentence-transformer model (~80MB, cached in a Docker volume)
# create virtual environment
python -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
# install dependencies
pip install -r requirements.txt
# start PostgreSQL (e.g., via Docker)
docker run -d --name triage-db \
-e POSTGRES_USER=triage \
-e POSTGRES_PASSWORD=triage_secret \
-e POSTGRES_DB=alert_triage \
-p 5432:5432 \
postgres:15-alpine
# configure environment
cp .env.example .env
# edit .env: change db host from "db" to "localhost"
# run the API
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000All configuration is driven by environment variables. See .env.example for defaults.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgresql+asyncpg://triage:triage_secret@db:5432/alert_triage |
Async database connection string |
SYNC_DATABASE_URL |
postgresql+psycopg2://triage:triage_secret@db:5432/alert_triage |
Sync database URL (used by Alembic and retrain script) |
EMBEDDING_MODEL |
all-MiniLM-L6-v2 |
Sentence-transformer model name |
EMBEDDING_BATCH_SIZE |
64 |
Batch size for encoding |
CLUSTER_DISTANCE_THRESHOLD |
0.35 |
Cosine distance threshold for agglomerative clustering |
MIN_CLUSTER_SIZE |
2 |
Minimum alerts to form a cluster |
LOG_LEVEL |
INFO |
Application log level |
API_HOST |
0.0.0.0 |
Server bind address |
API_PORT |
8000 |
Server port |
Generate synthetic alerts and send them to the API:
# generate 800 synthetic alerts
python scripts/seed_data.py 800 > alerts.json
# ingest via API
curl -X POST http://localhost:8000/api/v1/alerts/ingest \
-H "Content-Type: application/json" \
-d @alerts.jsonOr send alerts directly:
curl -X POST http://localhost:8000/api/v1/alerts/ingest \
-H "Content-Type: application/json" \
-d '{
"alerts": [
{"raw_text": "SQL injection in user-service via unsanitized user_id parameter", "source": "sonarqube"},
{"raw_text": "SQL injection vulnerability found in auth-gateway query builder", "source": "semgrep"},
{"raw_text": "XSS reflected in search-api through unescaped query parameter", "source": "zap"}
]
}'curl http://localhost:8000/api/v1/clusterscurl http://localhost:8000/api/v1/metrics/dashboard# manual threshold
python scripts/retrain.py --threshold 0.30
# automatic threshold tuning via silhouette score
python scripts/retrain.py --auto-tune# default: 10 hand-picked alerts + 200 synthetic alerts
python scripts/smoke_test.py
# larger synthetic batch and persist full JSON output
python scripts/smoke_test.py --count 800 --out smoke_output.jsonThe script exercises embedding, clustering, severity and OWASP classification, and playbook lookup against in-memory inputs. See Verified Pipeline Run for sample output.
curl -X PATCH http://localhost:8000/api/v1/alerts/{alert-uuid}/false-positiveThe core NLP pipeline (embed → cluster → classify → playbook) ships with a standalone smoke test that exercises the engine without PostgreSQL or the API. It runs against two inputs: a small hand-picked set with intentional near-duplicates, and a larger synthetic batch from scripts/seed_data.py.
python scripts/smoke_test.py --count 200Three SQL-injection variants describing the same finding, two SSRF variants describing the same finding, and five distinct singletons.
{
"alerts": [
{"raw_text": "SQL injection in user-service via unsanitized user_id parameter", "source": "sonarqube"},
{"raw_text": "SQL injection vulnerability found in user-service query builder for user_id", "source": "semgrep"},
{"raw_text": "Unsanitized user_id parameter leads to SQL injection in user-service", "source": "checkmarx"},
{"raw_text": "SSRF in webhook-handler: user-supplied URL fetched without validation", "source": "semgrep"},
{"raw_text": "Server-side request forgery in webhook-handler: callback URL not validated", "source": "zap"},
{"raw_text": "Hardcoded AWS secret key found in payment-api source code", "source": "trivy"},
{"raw_text": "MD5 used for password hashing in auth-gateway", "source": "snyk"},
{"raw_text": "Critical CVE-2021-44228 in log4j dependency (version 2.14.1)", "source": "dependabot"},
{"raw_text": "Verbose error messages in admin-panel expose stack traces to end users", "source": "burp-suite"},
{"raw_text": "Missing rate limiting on customer-portal password reset flow", "source": "fortify"}
]
}clusters formed: 7 reduction: 30.0%
severity distribution: {'high': 4, 'medium': 4, 'critical': 1, 'low': 1}
[cluster 2] sev=high owasp=A03:2021-Injection :: SQL injection in user-service via unsanitized user_id ...
[cluster 2] sev=high owasp=A03:2021-Injection :: SQL injection vulnerability found in user-service ...
[cluster 2] sev=high owasp=A03:2021-Injection :: Unsanitized user_id parameter leads to SQL injection ...
[cluster 0] sev=high owasp=A10:2021-SSRF :: SSRF in webhook-handler: user-supplied URL fetched ...
[cluster 0] sev=medium owasp=A10:2021-SSRF :: Server-side request forgery in webhook-handler: ...
[cluster 4] sev=critical owasp=A02:2021-Cryptographic Failures :: Hardcoded AWS secret key found in payment-api ...
[cluster 6] sev=medium owasp=A07:2021-Auth Failures :: MD5 used for password hashing in auth-gateway
[cluster 3] sev=medium owasp=A06:2021-Vulnerable Components :: Critical CVE-2021-44228 in log4j dependency ...
[cluster 5] sev=low owasp=A05:2021-Security Misconfiguration :: Verbose error messages in admin-panel ...
[cluster 1] sev=medium owasp=A07:2021-Auth Failures :: Missing rate limiting on customer-portal ...
playbook lookup (first cluster):
category: A03:2021-Injection
title: Injection Remediation
steps: 6 remediation steps
first step: Use parameterized queries or prepared statements for all database access.
clusters formed: 90 reduction: 55.0%
timings (ms): severity=1.1 embed=1268.5 cluster=13.6 owasp=1.1
severity distribution: {'medium': 138, 'high': 40, 'critical': 18, 'info': 2, 'low': 2}
top OWASP categories: {'A03:2021-Injection': 30, 'A07:2021-Auth Failures': 29,
'A10:2021-SSRF': 27, 'A09:2021-Logging Failures': 25,
'A08:2021-Data Integrity Failures': 23}
- Deduplication works end-to-end. Three rewordings of one SQL-injection finding and two rewordings of one SSRF finding collapsed into a single cluster each, while the five truly distinct alerts each stayed as their own cluster (10 → 7 clusters, 30% reduction on the small set; 200 → 90 clusters, 55% reduction on the synthetic batch).
- Semantic clustering, not keyword matching. "Unsanitized user_id parameter leads to SQL injection" clustered with "SQL injection in user-service" even though their word orders and structure differ — confirming the sentence-transformer embeddings are driving similarity, not literal token overlap.
- OWASP routing is consistent. Every SQL-injection variant routed to
A03:2021-Injection; both SSRF variants routed toA10:2021-SSRF; the CVE alert routed toA06:2021-Vulnerable Components; the verbose-error alert routed toA05:2021-Security Misconfiguration. - Severity classification picks up scanner cues. "Hardcoded secret key" →
critical; "SQL injection" / "SSRF" →high; "verbose error messages" →low. - Playbook mapping closes the loop. The dominant OWASP category for each cluster resolves to its remediation playbook (e.g., the SQL-injection cluster maps to the 6-step Injection playbook starting with "Use parameterized queries…").
- Performance is in the README's claimed range. The 200-alert batch completed embedding + clustering + classification in ~1.3 s on CPU after the model warm-up, matching the documented "~2.5 s for 800 alerts" budget.
This sanity check is what a reviewer should run first: it confirms the engine is functional before standing up Docker, PostgreSQL, and the API.
Benchmarks measured on a 4-core / 8GB instance with the default configuration:
| Metric | Value |
|---|---|
| Ingestion + clustering (800 alerts) | ~2.5 seconds |
| API response latency (GET endpoints) | <80ms average |
| Cluster reduction ratio | ~72% (800 raw -> ~220 clusters) |
| Embedding model | all-MiniLM-L6-v2 (384 dimensions) |
| Model size | ~80MB |
The distance threshold (default 0.35) directly controls the granularity of clustering. Lower values produce more clusters (finer grouping); higher values merge more aggressively. The retrain.py --auto-tune script searches for the threshold that maximizes the silhouette score on the current alert corpus.
The system uses all-MiniLM-L6-v2 from the sentence-transformers library. This model produces 384-dimensional normalized embeddings and is optimized for semantic similarity tasks. It runs on CPU and processes ~800 alert texts in under 2 seconds.
Agglomerative clustering with average linkage and cosine distance was chosen over HDBSCAN for two reasons:
- It handles uniformly dense alert data better than density-based methods.
- The distance threshold parameter provides a direct, interpretable knob for controlling cluster granularity.
The centroid of each cluster is identified as the alert whose embedding has the highest cosine similarity to the cluster mean vector.
Each alert is classified to an OWASP Top 10 (2021) category by computing the cosine similarity between its embedding and pre-computed embeddings of OWASP category descriptions. The category with the highest similarity score is assigned. This approach generalizes across different phrasings without maintaining exhaustive keyword lists.
Severity is assigned via keyword matching against curated keyword sets for each level (critical, high, medium, low, info). A confidence score is computed based on the number of matching keywords. When no keywords match, the default severity is "medium" with low confidence.
Four tables:
alerts-- individual security alerts with severity, OWASP category, cluster assignment, and false-positive flag.alert_clusters-- deduplicated alert groups with centroid text and aggregate severity.remediation_playbooks-- structured OWASP Top 10 remediation steps and references.triage_runs-- metadata for each ingestion run (counts, timing, reduction stats).
Analysts can flag individual alerts as false positives via the PATCH endpoint. The metrics service computes the false-positive rate as the ratio of flagged alerts to total alerts. Iterative tuning of the clustering threshold and severity keywords, guided by the FP rate metric, is the primary mechanism for reducing noise over time.

