von Hören

Technical research report

Continuous Exposure Management for the Mittelstand

Architecture and first results from production

Author
Welf von Hören
Publisher
von Hören Potential Ventures UG
Date
Version
1.0

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 (querybatch plus 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.

Architecture of the monitoring node Public sources and client systems feed a monitoring node. Modules match the data against the inventory, an AI triage judges hits, results land in SQLite, are routed into per-project queues, reported, and validated and fixed from a workstation. Public sources CISA KEV cisagov/kev-data OSV querybatch + /vulns/{id} Aikido malware lists npm + PyPI 12 RSS feeds CERT-Bund, oss-security, … GitHub API lockfiles, mirrors, workflows Shodan · crt.sh InternetDB, CT logs: external view Client systems agent/install.sh Trivy rootfs + containers ss -ltnup (listening ports) every 3 h · pull over SSH only forced command: tar reports Monitoring node (VPS) · timer every 5 min Ingest & matching against inventory vulnwatch.py KEV · OSV · Aikido · RSS · inventory hosts.py agent reports, packages, ports surface.py nmap, Shodan, CT logs, TLS leaks.py gitleaks over mirrors ci.py zizmor (GitHub Actions) xintel.py · bookmarks.py X signals first run = baseline, no alert Triage core.claude() headless, read-only context: STACK.md + inventory · feed text = untrusted verdict: applies · severity · why · action State core.py → SQLite hits · inventory · seen · kv lifecycle: new → triaged → resolved / dismissed Notify digest.py email from high push only critical Routing projects.py projects.json queue per project Backlog & reports backlog.py top 20 from baseline daily digest 07:30 · weekly report Mondays signed feed hub→node: designed, not built yet Validation & remediation Workstation bin/sicherheit-dispatch queue: read access only one session per project brief without free text Reproduce on client systems under written authorisation version state, configuration, rebuild in a test environment Fix pull request + tests firewall, bindings, updates log: docs/security/ production approval: client close: scanner or dismiss Tiers Monitor watch, report Respond deliver a tested fix Managed apply agreed fixes
Fig. 1 — Module layout of the system as of 25 October 2026. Boxes carry the real module names. Dashed: the validation path back to the client systems and the hand-over to the service tiers.

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.

Table 1 — Client A in the observation window 22–25 October 2026 (3 d 3 h). Source: the node's SQLite database and systemd journal.
MetricValueNote
Inventory covered2 servers · 7 repositories · 2 domains10,311 packages on the servers (Trivy), 771 packages in lockfiles
Items ingested, all projects7,556incl. initial baseline; of these 2,204 from RSS feeds, 1,756 KEV entries, 1,957 host packages
Hits against Client A's inventory941host packages 799 · secret scan 73 · dependencies 60 · CI 8 · external view 1
Scanner severitycritical 42 · high 860 · moderate 27 · low 12before triage; 785 of them baseline
Triaged44applies 7 · unclear 3 · does not apply 34
Relevant after triage22.7 %(7 + 3) / 44; all projects: 34.7 % (67 / 193)
Closed11584 automatically after rescan · 29 secret findings dead, dummy or false positive · 2 intended
Median detection → closure1.7 hn = 84, automatically closed findings; triaged findings n = 3 (0.1 h, 6.5 h, 21.8 h), too few for a median
Alerts sent1of which false positive 1: a new hostname in the CT log, intended by the operator
Time to triagen/a (not yet recorded)the database stores no triage timestamp
Open relevant findings7in 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Record triage timestamps and token cost per run in the database.
  2. Roll out a dedicated node for Client A; build the signed hub feed.
  3. Automate the baseline review: the backlog run escalates network-exposure findings immediately.
  4. Add EPSS to the production node's prioritisation.
  5. Measure verdict quality: a sample of triage verdicts against manual review, with an error rate per class.
  6. Report v1.1 after 30 days of operation with the same metrics.