The Problem with Running Five Security Scanners
Self-hosted infrastructure has a security tooling problem that enterprise organisations solve with expensive SIEM platforms and dedicated security teams. Small to medium operations don’t have either, so they end up running multiple specialised tools — each with its own interface, notification system, and false positive rate.
Before OpsShield, the security posture review process looked like this: run ZAP for web vulnerabilities, Nuclei for CVE scanning, Trivy for container images, Semgrep for code-level issues, and Gitleaks for secrets detection. Then manually collate the results across five different output formats. Cross-reference with any existing CVEs. Try to work out which critical finding from three weeks ago is actually still present.
It’s not that the individual tools are bad — they’re excellent. The problem is the operational overhead of making them work together as a coherent system.
Architecture: The Aggregation Pattern
OpsShield is built on an aggregation pattern. Each scanner runs on a schedule or on-demand, writes its output to a designated directory, and OpsShield’s aggregation worker picks up the results and normalises them into a common schema.
This approach has one critical advantage over alternatives: each scanner continues to function independently. If OpsShield’s aggregation layer goes down, the scanners keep running. If a scanner’s API changes, only the normalisation adapter for that scanner needs updating.
The common vulnerability schema captures:
- Severity (critical, high, medium, low, info)
- Affected resource (IP:port, container image, file path, or code location)
- CVE reference (if applicable)
- First seen / last seen dates
- Current status (open, in-progress, false-positive, accepted-risk, fixed)
- Policy tags (critical-infrastructure, customer-facing, PCI-scope, etc.)
Why a common schema matters: the first thing you discover when normalising five different scanner outputs is that they use wildly different severity scales and naming conventions. ZAP has “High”, “Medium”, “Low”. Nuclei uses CVSS scores. Trivy has “CRITICAL” in block capitals with a different threshold than you’d expect. Semgrep’s findings are just code patterns with no severity until you define rules. Gitleaks has its own concept of “severity” that maps loosely to CVSS but not exactly.
Defining a common schema forces you to make explicit decisions about what “critical” means across all five tools. That’s valuable operational knowledge that most organisations never articulate.
The Normalisation Layer: Implementation Details
Each scanner has a normalisation adapter. These are lightweight classes that parse the scanner’s native output and emit events conforming to the common schema.
The ZAP adapter handles both the traditional HTML report and the newer JSON format. The tricky part with ZAP is that it re-reports findings on subsequent scans even if they haven’t changed. The adapter deduplicates by (target URL, finding type, parameter affected), only creating a “first seen” entry on the initial detection and updating “last seen” on subsequent detections.
The Trivy adapter has to handle the fact that Trivy scans Docker images, not running containers. The adapter is designed to work in a pipeline: a CI/CD job triggers a Trivy scan on a newly built image, pushes the JSON output to a results bucket, and OpsShield picks it up. Finding a vulnerability in a container image that was built three months ago and hasn’t been re-scanned since is a common failure mode that the adapter surfaces explicitly.
The Policy Engine
Raw vulnerability data isn’t actionable without context. A CRITICAL CVE on a test server that never touches production is lower priority than a MEDIUM CVE on an externally-facing customer portal.
The policy engine allows defining rules that set finding status automatically. Rules can be based on:
- Resource tags (e.g., “development” environment → auto-mark as false-positive for non-critical findings)
- CVE age (e.g., unpatched CVEs older than 30 days escalate severity)
- Resource exposure (internet-facing resources get automatic priority escalation)
The policy engine runs on every new finding and on a daily re-evaluation cycle. This means the dashboard doesn’t just show the raw scanner output — it shows the security team’s actual posture after applying the policies they’ve defined.
The Dashboard: Prioritisation as the Core UX Problem
The hardest design problem in a vulnerability management platform is prioritisation. Showing every finding sorted by CVSS score seems logical but produces dashboards that nobody reads — there are too many findings, and the critical ones get lost in the noise.
OpsShield’s dashboard prioritises by a composite score: CVSS base score multiplied by a recency factor, multiplied by an exposure factor (internet-facing services get a higher multiplier), multiplied by a policy-weighted severity.
This produces a prioritised list that surfaces the findings most likely to represent genuine, exploitable risk — not just the highest CVSS score. The exposure factor alone typically moves internet-facing findings to the top of the queue, which matches how real attacks work: adversaries scan for internet-facing services and exploit known CVEs against them, regardless of whether a “medium” CVE in an obscure internal tool scores higher on paper.
Deployment: Running Security Scanners Without Compromising the Network
The Docker networking configuration deserves mention because it’s non-obvious. Security scanners need to run in an isolated network segment that can reach internal services but is isolated from the scanner management interface. The setup uses Docker’s network driver with a dedicated scanning network that has access to the target subnet but not to the OpsShield API server itself.
This means even if a scanner is compromised (they’re running third-party tools, after all), the blast radius is limited to the scanning network. A compromised ZAP instance can’t reach OpsShield’s database or management API.
The trade-off is operational complexity — running scans across Docker network boundaries requires careful iptables configuration and regular validation that the isolation is actually working. It’s not a set-and-forget configuration.
Results and Observations
OpsShield has been running on internal infrastructure for approximately 8 months. The aggregated view has surfaced patterns that individual scanners would miss:
- Container images that pass Trivy scans individually but expose a critical vulnerability when deployed together — individual scanner scopes don’t cover cross-image dependency vulnerabilities
- API endpoints that ZAP doesn’t detect as having parameters (because they use POST bodies rather than URL parameters) — these require manual spidering that wasn’t being done consistently
- Secrets that Gitleaks catches in newly committed code but that live in configuration files that aren’t in version control — the solution here required adding a separate configuration scanner, not fixing Gitleaks
The false positive rate has been the ongoing challenge. Each scanner has its own baseline false positive rate, and normalising them into a common schema doesn’t reduce that rate. The policy engine helps by auto-closing known false positives, but new finding types require manual triage until a policy rule can be defined.
The dashboard shows approximately 340 findings per scan cycle across the full infrastructure. Of those, after policy application and false positive marking, roughly 12-15 require active remediation attention. That’s a much more actionable number than 340.