Technical research report
Continuous Exposure Management for the Mittelstand
Architecture and first results from production
This report describes the architecture of a continuous exposure management system we built for small and mid-sized companies, and evaluates its first three days of operation. In that window the monitoring node processed 7,556 items from public sources and its own scanners and matched them against the inventory of three projects. For the first production client (Client A, a SaaS platform for fitness studios) this produced 941 hits; after triage, 22.7 % of the reviewed hits remained relevant. A case study follows one finding from first record to verified fix. Every number comes from the system's database and logs. Where we do not measure something yet, we say so.
Problem
Small and mid-sized companies now run the same kind of infrastructure as large enterprises: cloud servers, containers, software with hundreds of dependencies, devices in the field. They rarely have a security team. The job of checking new vulnerabilities against their own inventory lands on the IT staff, or on a service provider who serves many clients at once.
The common instrument is the periodic penetration test. It delivers a snapshot. Twelve months often pass between two tests. In that time new vulnerabilities appear, the inventory changes, and nobody matches the two. During the three days covered by this report, twelve public feeds alone delivered 2,204 items. A vulnerability scanner shortens the interval but creates a second problem: long lists. Every hit must be checked by hand. Do we use this package? Is the service reachable? Is there a fix? That check costs time, so it waits.
Three things are missing: an inventory that stays current; a triage that judges every hit against that inventory; and a path that leads from judgment to verified fix. Our hypothesis: a small, continuously running node per client can provide all three if an AI triage does the matching and people only approve the fix.
Method and architecture
The system's internal name is vulnwatch. It is written in Python, runs as a systemd service on a Linux server and keeps its state in a SQLite database. A timer starts a run every five minutes. Each run passes through ingest, matching, triage and notification. Heavy sources run less often: the repository inventory hourly, host reports every three hours, external scan, secret scan and CI audit daily.
Ingest: what the system reads
- CISA KEV (Known Exploited Vulnerabilities, JSON catalogue): matched on vendor and product; every match counts as critical.
- OSV (
querybatchplus detail lookups): advisories for every package in the client's lockfiles. Ten lockfile formats are parsed (npm, bun, pnpm, yarn, requirements, poetry, uv, Cargo, go.sum). - Aikido malware lists for npm and PyPI: a malicious version in the inventory counts as critical.
- Twelve RSS feeds, including CERT-Bund WID, oss-security, Node.js security, vendor research and trade press. Keyword matching against 84 stack terms.
- Host agent on every client server: a Trivy scan of the filesystem and running containers, plus the list of listening ports (
ss -ltnup). The node pulls the reports over SSH; the agent opens no return channel. - External view: nmap over the 1,000 most common TCP ports (IPv4 and IPv6), Shodan InternetDB, certificate transparency logs (crt.sh), TLS validity and expiry.
- Secrets and CI: gitleaks over bare mirrors of all active repositories, zizmor over GitHub Actions workflows.
The first run of every source is a baseline. It raises no alert; it produces a backlog that an AI model condenses into a list of the twenty most important measures. Only changes against the baseline trigger notifications.
Triage: matching against the inventory
Every new hit goes to a triage instance in batches of at most 25. The instance is a Claude model running headless with restricted rights: read-only access, no access to configuration or keys, GitHub only through a GET-only wrapper. As context it receives a maintained description of the inventory (servers, services, network boundaries, dependencies) and the inventory tables. Feed text is declared untrusted; instructions inside it are ignored. Each hit receives a structured verdict: applies (yes, no, unclear), severity, a reason of at most three sentences, a recommended action. If triage fails, critical hits from KEV, OSV, the malware lists, the secret scan and the host scan still escalate.
Routing, notification, remediation
A registry assigns repositories, hosts, addresses and domains to projects. Hits are routed at query time through their assets; every project has its own queue. The queue carries validated, structured fields only. Free text from advisories never reaches an agent's brief unasked. Notifications go out by email from severity "high", by push only for "critical", plus a daily digest at 07:30 and a weekly report. A dispatcher on a workstation opens one session per project, in which an agent reproduces the finding on the client system, prepares a pull request with tests and records the result in a remediation log. Changes to production systems require the client's approval. A finding closes when the scanner no longer reports it, or when a person dismisses it with a reason.
The service tiers Monitor (watch and report), Respond (deliver a tested fix) and Managed (apply agreed fixes) differ only in how far that last step goes.
First results
The node has run since 22 October 2026, 08:43 UTC. This report evaluates the state as of 25 October 2026, 11:37 UTC: a window of three days and three hours, not yet a full week. In that time the timer started 591 runs without a single failure of the service unit. The first production environment is Client A, a SaaS platform for fitness studios with kiosk devices on site. The operator authorised monitoring and active testing in writing; von Hören Potential Ventures UG operates the system. The same node also watches our own infrastructure and a second client project.
| Metric | Value | Note |
|---|---|---|
| Inventory covered | 2 servers · 7 repositories · 2 domains | 10,311 packages on the servers (Trivy), 771 packages in lockfiles |
| Items ingested, all projects | 7,556 | incl. initial baseline; of these 2,204 from RSS feeds, 1,756 KEV entries, 1,957 host packages |
| Hits against Client A's inventory | 941 | host packages 799 · secret scan 73 · dependencies 60 · CI 8 · external view 1 |
| Scanner severity | critical 42 · high 860 · moderate 27 · low 12 | before triage; 785 of them baseline |
| Triaged | 44 | applies 7 · unclear 3 · does not apply 34 |
| Relevant after triage | 22.7 % | (7 + 3) / 44; all projects: 34.7 % (67 / 193) |
| Closed | 115 | 84 automatically after rescan · 29 secret findings dead, dummy or false positive · 2 intended |
| Median detection → closure | 1.7 h | n = 84, automatically closed findings; triaged findings n = 3 (0.1 h, 6.5 h, 21.8 h), too few for a median |
| Alerts sent | 1 | of which false positive 1: a new hostname in the CT log, intended by the operator |
| Time to triage | n/a (not yet recorded) | the database stores no triage timestamp |
| Open relevant findings | 7 | in progress; as of 25 October 2026, 11:37 UTC |
Three observations. First: the baseline dominates. 785 of the 941 hits come from the first scan and are old, known packages without a fix in the distribution repository. The system does not alert on them; it condenses them into a backlog. Second: triage removes most of the noise. Of 44 reviewed hits it judged 34 as not applicable, for example because the vulnerable code path is not reachable or the package only exists in a development environment. Third: the secret scanner is the loudest scanner. Across all projects we checked 280 findings from five private repositories against the running systems: zero live credentials, eight dead, ten test values, 262 false positives.
Across all three projects the node processed 3,010 hits, triaged 193, closed 307 automatically (median 4.8 h, n = 307) and sent 13 alerts and two daily digests.
Case study: database port on the staging server
Finding class. A database service was bound to all network interfaces. The cloud firewall allowed the port from outside for exactly one address, the service provider's operations server. At the same time the overlay network's policy allowed every device on the network to reach it, including the kiosk machines in the studios. The database thus hung on two rules that nobody checked. A change to either, or one compromised device, would have opened direct access.
- The host agent delivers its first report. The port list shows the database port on the public interface. 16 seconds later the external scan sees the port open from outside. Both are baseline: no alert, an entry in the inventory.
- During the review of the baseline and the overlay-network policy the finding is prioritised. A check of the firewall rules confirms the exception; a measurement from inside the overlay network confirms that kiosk devices reach the port. The rules are saved as evidence.
- FixFour changes, as a pull request with rollback copies: firewall rule deleted; the database listens on localhost and the overlay address only; the access list restricted to one user from one address with TLS and SCRAM; a sysctl entry so that the service can bind the overlay address after a reboot before the network daemon is up. The overlay-network policy follows the same day as policy-as-code with a test block.
- VerificationConnection attempts from two locations time out (IPv4 and IPv6). SSH keeps working. The authorised client connects over the overlay network with TLS. The application's health endpoint answers 200.
- The next host report records a new binding: the same port, now on the overlay address. The system raises a hit of severity "high"; triage judges it as applicable. Four minutes later it is closed with a reason as intended. The scanner has confirmed the fix independently.
The fix was in place about 30 hours after the first record. Two lessons: a silent baseline is right, so that day one does not drown in hundreds of alerts; but it needs a prompt human review. And the scanner that found the finding is also the best witness to its fix.
Limits and next steps
What this report does not show
- Short window. Three days are a sample, not statistics. The baseline shapes every number. A second evaluation after 30 days is planned.
- One node, three projects. The target architecture has one node per client and a central hub that distributes only signed public advisories. Today one node that we operate reads the sources directly and watches our infrastructure and two client projects together. The signed hub feed is designed, not built.
- Silent baseline. The case study shows the flip side: a finding from the first scan raises no alert. The baseline review is a manual step today.
- Missing timestamps. The database records when a hit is created and when it closes, but not when triage judged it. Time to triage and cost per triage are therefore not measurable.
- Hand-written inventory description. The triage context is a maintained text plus inventory tables, not a structured asset graph. If the text goes stale, verdict quality drops.
- Blind spots. No EPSS in the production node (prototype only). No inventory of workstations or the Windows kiosks. Hits from news feeds without an asset reference route only to the default project. The host scanner does not see an application's runtime when it is installed as a single binary.
- AI verdicts are fallible. Triage can misjudge a hit. The fail-safe escalates critical findings even without a verdict; a systematic measurement of verdict quality against a human second opinion is still outstanding.
- Fixes need approval. Several prepared fixes wait for the operator's approval for production. The time between "fix ready" and "fix applied" lies with the client and appears in no metric.
Next steps
- Record triage timestamps and token cost per run in the database.
- Roll out a dedicated node for Client A; build the signed hub feed.
- Automate the baseline review: the backlog run escalates network-exposure findings immediately.
- Add EPSS to the production node's prioritisation.
- Measure verdict quality: a sample of triage verdicts against manual review, with an error rate per class.
- Report v1.1 after 30 days of operation with the same metrics.