I built this for my undergraduate thesis (Software Engineering) and wanted to share it here because I think the technical trade-offs are more interesting to this audience than the headline numbers.
The problem
An AppSec pipeline running SAST, SCA, and DAST on every commit generates more findings than any team can realistically triage. Most of them aren't real vulnerabilities — test code, an already-mitigated dependency, a scanner that flakes between runs. But each tool decides, in isolation, by severity, what becomes an alert. The result is alert fatigue: once the noise crosses a certain threshold, teams stop trusting the scanners, and a real finding gets lost among a thousand false ones.
I didn't want to build another scanner. I wanted to test whether correlating what scanners already produce, before deciding what becomes an alert, could help.
Why paraconsistent logic
Most triage mechanisms run on classical logic: a finding either is or isn't a vulnerability. That doesn't reflect the reality of a pipeline — it's common for SAST to flag missing authentication on an endpoint while DAST tries to exploit that same endpoint and gets blocked by a WAF. Both pieces of evidence are real and contradictory. A binary mechanism is forced to discard one of them.
Two-valued Paraconsistent Annotated Logic (LPA2v — da Costa, 1974; Abe, 2015) represents each piece of evidence with two degrees, favorable (μ) and unfavorable (λ), which can both be high at once:
GC = μ - λ certainty degree
GCT = μ + λ - 1 contradiction degree
When GCT is high, it's not a tie to be broken — it's genuine contradiction, and it should become an "inconsistent" state flagged for human review, not a forced "confirmed" or "dismissed."
Architecture
Five domain neurons (SAST, SCA, DAST, code context, operational context) each estimate (μ, λ) from that domain's raw signal. A master neuron aggregates them in two different ways — and this split was the part that took the most iterations to get right:
- Consensus: weighted mean of μ and λ across all domains, used to rank severity.
- Contradiction: the largest μ and largest λ among the primary detectors (SAST/SCA/DAST) — not a mean.
My first version used a single weighted mean for everything, and it didn't work: a real contradiction between two specific detectors got diluted by the mean of the neutral domains and never produced a GCT high enough to trigger the inconsistent state — the exact opposite of what paraconsistent logic is supposed to do. Splitting the two aggregations fixed it.
Before becoming an alert, the result still passes through a temporal persistence window (3 scan ticks), so an isolated spike doesn't trigger an escalation on its own.
Synthetic simulation
A TypeScript simulator with 206 synthetic assets across 12 scenarios (from a benign dependency bump to a confirmed RCE, including the WAF case above and a silent supply-chain compromise that only SCA sees). I compared plain threshold, rule-based (a small set of predefined IF-THEN exceptions), and the LPA2v cluster, across 3,005 events:
| Mechanism | Precision | Recall | False Positives |
|---|---|---|---|
| Threshold | 8.01% | 86.8% | 2,493 |
| Rule-Based | 10.41% | 86.8% | 1,868 |
| LPA2v-Cluster | 100% | 65.6% | 0 |
The number that matters here isn't the isolated 100% precision — it's that the cluster eliminated false positives without simply going quiet: it still catches 164 of the 250 simulated real positives. The recall drop is real, concentrated in three deliberately hard scenarios (single-domain evidence, a weak early signal in a progressive leak, and the persistence window's warm-up period). I reported the number as it came out, without recalibrating until it looked better.
Against real findings
Simulation validates the architecture, not production performance. So I ran Semgrep, Snyk, and OWASP ZAP for real against 5 applications (two SaaS platforms, freeCodeCamp, Plane, and my personal portfolio), collecting 559 findings labeled through AI-assisted human review — not the tools' own self-reported severity, which would make the study circular.
With the cluster unchanged — same weights, same thresholds as the simulation:
| Mechanism | Precision | Recall |
|---|---|---|
| Threshold | 31.66% | 100% |
| Rule-Based | 34.17% | 100% |
| LPA2v-Cluster | 86.84% | 74.58% |
The weakest point: 33.33% recall on one of the repositories, where 8 of 12 real vulnerabilities shared the same pattern (moderate-severity, single-domain evidence) and fell below the attention threshold — the same mean-dilution effect as before, just without contradiction to counteract it, because there was no contradiction there, only weak isolated evidence. I didn't recalibrate to hide it.
What's left unsolved
Weights and thresholds were hand-calibrated, not learned from labeled data. With 559 findings already labeled, adaptive calibration is the obvious next step, but it was out of scope for this thesis.
Code is fully open source, with seeded, deterministic simulation for anyone who wants to reproduce the exact numbers: Repo link
If anyone here has worked on alert correlation in a security pipeline, I'd really like critique on the decision to split consensus from contradiction — it's the design choice that cost me the most, and I'm still not sure it's the most elegant way to solve it.
Top comments (0)