The Great Unification: Mapping Eight Threat Classes to a Working System Flagship article in the Cybersecurity Defense Lab series

Threat taxonomies are abundant; architectural guidance derived from them is less so. This article reports on a mapping exercise: eight historical malware classes — viruses, worms, trojans, ransomware, botnets, supply chain compromise, social engineering, and prompt injection — translated into eight concrete architectural controls in a working personal AI agent, with each control verified by an automated test suite. The contribution is not the mapping itself, which is largely derivable from the literature. It is what the verification step revealed. Three distinct failure modes appeared in controls that were believed to be in place: a control implemented but never invoked, a control invoked but semantically incorrect, and a control working correctly but unprotected by any test. A fourth observation, arriving unplanned, demonstrated the inverse: a deliberate security restriction survived an external "improvement" specifically because it had been encoded as a test. The practical conclusion is narrower than a general claim about security engineering, but sharper: a security control that is not exercised by a test is a claim, not a control — and the interval between writing it and discovering this can be measured in months.

8/27/202612 min read

1. The Gap Between Knowing and Defending

The preceding three articles in this series analyzed eight threat classes along three axes: how much autonomy malicious code possesses, how sophisticated a lie aimed at a human must become, and whether a system verifies what an artifact isor only what it claims to be.

Each of those articles ends with an implication for design. None of them tests one.

This is the normal state of security writing, and it is not a criticism — analysis and implementation are separate crafts. But it produces a recurring gap. An organization reads that least privilege matters, agrees, and believes it has least privilege. The belief is rarely wrong in intent. It is frequently wrong in fact, and the mechanism by which it becomes wrong is almost never dramatic.

This article documents an attempt to close that gap on a small scale: a single personal AI agent, eight controls, and a test suite that attempts to prove each control does what it is claimed to do.

1.1 The system

The system under discussion is a personal AI agent operated through a chat interface, running local language models with an optional external research capability. It reads and writes files in a bounded workspace, retrieves web content, and executes a small set of predefined tools on the operator's behalf. It runs in a hardened container.

Its architecture is deliberately conventional. Nothing in this article depends on the system being novel; the point is that the controls examined here are ones any comparable system would plausibly claim to have.

1.2 Why an AI agent is a useful subject

An agent of this kind occupies an unusual position relative to the threat classes analyzed in this series. It runs continuously, acts without a discrete human decision per action, ingests content from sources the operator does not control, and maintains outbound communication. Structurally, those are the properties that made botnets difficult (Article 1) and that constitute the "lethal trifecta" in prompt injection research (Article 3).

This is not a flaw in agent design. It is a description of what makes agents useful. But it means an agent is a subject where all eight threat classes have plausible application simultaneously — which makes it a reasonable place to test whether a mapping from threat to control can be made concrete.

2. The Mapping

Each threat class was translated into a single architectural principle, stated in system-independent terms, and then into a specific implementation.

#Threat classArchitectural principleImplementation1VirusesNo persistent action without explicit, time-bounded human approvalApproval gate with TTL expiry2WormsEvery component runs isolated, with minimum privilegeRead-only container, all capabilities dropped, non-root3TrojansEnumerate what is permitted, not what is forbiddenTool allowlist4RansomwareMinimize reachable surface; constrain irreversible operations furtherPath confinement, extension allowlist, deletion limits5BotnetsVerify every outbound contact; leave a durable record of every actionSSRF guard, redirect refusal, audit trail6Supply chainPin dependencies by cryptographic fingerprint, not by nameImage digest pinning, dependency hash locking7Social engineeringAuthenticate on multiple independent dimensionsFour-factor request validation8Prompt injectionWhere private data, untrusted content, and external communication intersect, default to denialData classification policy, fail-closed

The remainder of this section describes each implementation and, where relevant, the design decision that is not obvious from the principle alone.

2.1 Approval gate (viruses)

Every operation with persistent effect — writing, appending, deleting — creates a pending action requiring explicit confirmation through the chat interface. Pending actions carry a creation timestamp and expire after a configured interval.

The non-obvious decision is in the expiry check. When the pending record is missing, malformed, or carries an unparseable timestamp, the function returns expired. Three separate paths converge on the same conservative outcome. An approval whose provenance cannot be established is treated as invalid rather than as valid-by-default.

2.2 Container isolation (worms)

The agent's container runs with a read-only root filesystem, no-new-privileges set, all Linux capabilities dropped, and a non-root user. Writable state is confined to an explicit tmpfs mount and a single bind-mounted workspace directory.

The relevant property is not any single flag but their conjunction: a compromised process cannot escalate, cannot persist changes to its own filesystem, and cannot acquire capabilities it was not started with.

2.3 Tool allowlist (trojans)

The model can request tool invocations. The set of tools it may actually invoke is defined as an explicit allowlist of two entries.

The design decision worth noting: when the model requests a tool outside the allowlist, the request is refused, but a refusal message is returned to the model rather than being silently dropped. Silent refusal produces a system that cannot distinguish "the model never tried" from "the model tried and was blocked." The refusal is also logged. This turns an access control into an observable event — which, per Article 1, is the property that distinguishes a Level 2 defense from a Level 1 one.

2.4 Surface minimization (ransomware)

File access is confined in three layers. Paths are resolved before validation, then checked for containment within the workspace root — the ordering matters, since validating before resolution permits traversal via symbolic links and relative segments. Writable file types are restricted to a small set of data and text formats, excluding anything executable. Deletion is restricted further: directories cannot be deleted, and neither can dotfiles.

The third layer reflects an asymmetry worth making explicit. Write operations are recoverable if versioned; recursive deletion is not. Controls should be graduated by reversibility, not applied uniformly.

2.5 Verified contact and durable record (botnets)

Outbound requests resolve the target hostname and reject any that resolve to a non-global address, closing the server-side request forgery path that would otherwise allow the agent to be used to probe its own internal network. HTTP redirects are refused rather than followed, since a permitted destination that redirects to a forbidden one would otherwise defeat the check performed before the request.

Every command, model invocation, and security-relevant event is recorded. Identifiers in those records are truncated rather than stored in full — enough to correlate events, not enough to make the log itself a disclosure risk.

2.6 Fingerprint pinning (supply chain)

The container base image is pinned by digest rather than by tag. Python dependencies are locked to specific versions with cryptographic hashes, and installation runs with hash verification required.

The --require-hashes flag does more than verify the current file set. It changes the failure mode of a future mistake: if a dependency is later added without a hash, installation fails rather than proceeding. Article 3 argued that identity checks are frequently mistaken for integrity checks; this is the corresponding implementation, where the integrity check is mandatory rather than best-effort.

2.7 Multi-dimensional authentication (social engineering)

Incoming requests are validated on four independent dimensions: sender identity, chat context (direct conversation only), message provenance (forwarded messages rejected), and message age (requests older than sixty seconds rejected).

The fourth check is the one that does not appear in most comparable systems, and it addresses a threat the other three do not. Identity, context, and provenance all establish who and where. Only the age check addresses when, and thereby a replay: a legitimate, correctly-signed instruction captured and re-submitted later. Article 2 concluded that the durable defense against deception is verification through an independent dimension. Time is such a dimension, and it is cheap.

2.8 Fail-closed classification (prompt injection)

Files are classified into four categories, with a policy file defining what each category may do. Unclassified files default to the most restrictive category.

The design decision here is the one most likely to appear as an error to an outside reader, and it is not: files classified PUBLIC are nonetheless denied external transfer. The classification governs several capabilities independently, and the external-transfer capability is disabled across all four categories. The reasoning follows Article 3 directly — the trifecta requires all three legs, so severing one leg unconditionally is stronger than governing it conditionally. Public content is not sensitive, but automatic outbound transfer of workspace content is the capability an injected instruction would need, regardless of what it targets.

The retrieval function's default return value, when a classification is unrecognized or the policy file is unreadable, is denial.

3. Verification as the Actual Contribution

The mapping above is defensible but not novel. Any competent engineer given the same eight threat classes would produce something similar.

What is worth reporting is what happened when each claim in that table was subjected to a test.

The test suite comprises 46 tests covering path confinement, URL validation, approval expiry, rate limiting, scope isolation, data classification, tool allowlist enforcement, and write-extension restriction. They run in the same container as the application.

Three of the eight controls above turned out to have gaps. The gaps were not sophisticated, and none were exploited. That is precisely what makes them worth reporting: each is the kind of failure that is invisible without a test and undramatic when found.

3.1 Failure mode one: implemented but never invoked

The data classification module (§2.8) was written, complete and correct, and a policy file was authored defining four categories and their permissions.

A search of the codebase for calls to the classification functions returned a single site. That call passed a search query string to a function expecting a file path. Two things were wrong with it. First, the variable referenced in the call did not exist in that scope — the function would have raised a NameError on invocation. Second, and more instructively: had the variable been correct, the logic would still have been wrong, because a search topic cannot match a file glob pattern. Every query would have fallen through to the restrictive default and been denied. The control would have blocked everything, permanently, and this would have been read as the control working.

A second function in the same module — one producing a human-readable classification report — was correctly implemented and referenced nowhere at all.

The policy file had existed for months. The report the system generates about its own security posture listed data classification among its active controls, accurately describing what the policy file contained. Nothing in that report was false. The control was simply not connected to anything.

3.2 Failure mode two: working but untested

The write-extension restriction (§2.4) works. It has always worked. A review of the test suite found no test exercising it.

This is a weaker finding than the first, and it is included because the distinction matters. An untested control is not broken. It is unprotected — nothing stands between it and a future refactoring that changes its behavior without anyone noticing. Four tests were subsequently added covering permitted extensions, executable and script extensions, extensionless files, and case sensitivity.

The system's own security report had listed this control as active for as long as it had existed. The report was correct. It was also unfalsifiable.

3.3 Failure mode three: stale self-description

The security report includes a manually maintained list of known gaps and planned improvements. Two of its five entries called for adding unit tests to specific functions — functions that, by the time the list was read carefully, were covered by the test suite.

This is the least severe finding and the most ordinary. It is included because it illustrates a boundary that matters in any self-reporting system: part of that report is generated from code constants and cannot drift, and part is hand-written prose that can and did. The report itself did not distinguish between the two. A line was subsequently added noting which section is manually maintained.

4. The Inverse Case: A Restriction That Held

The three findings above concern controls that failed silently. A fourth event, which occurred during this work rather than being discovered by it, demonstrates the opposite.

During an AI-assisted refactoring session, an external suggestion was made to correct a genuine gap in the classification policy: a glob pattern that failed to match dotfiles in the repository root, meaning a root-level .env file was classified as PRIVATE rather than SECRET. The correction was valid.

The revised policy file that accompanied the suggestion, however, also changed the PUBLIC category's external-transfer permission from denied to permitted. This was not adversarial. It was a reasonable-looking normalization — PUBLICcontent being externally shareable is what the label implies. It was also a reversal of the deliberate decision described in §2.8.

The test suite rejected the change. Two tests failed: one asserting that a PUBLIC-classified file is nonetheless denied external transfer, and one asserting the resulting report text. The change was never deployed.

The observation this supports is narrow but, I think, non-obvious. The reason the intent survived is not that it was documented — it was documented, in the policy file's own comments and in the project's security notes, and the suggestion was made anyway by a party that had read them. It survived because it had been encoded as an executable assertion, and executable assertions are checked mechanically rather than read voluntarily.

This has a specific relevance to AI-assisted development, which is why it is reported here rather than as an aside. An assistant proposing changes to security-relevant code operates on general priors about what such code usually looks like. Deliberate deviations from the usual pattern — the kind that constitute a considered security posture — are precisely the cases where general priors are wrong. A test is the mechanism by which a local decision outranks a general prior.

5. Discussion

5.1 What the three failure modes share

All three findings are gaps between what a system reports about itself and what it does. In no case was the report dishonest. The report described the policy file, and the policy file was real. It listed the extension restriction, and the restriction functioned. It listed planned improvements that had, since the list was written, been completed.

The gap in each case is between a control existing and a control being exercised. This suggests a working definition sharper than "test your code": a security control is a claim about system behavior, and an untested claim about behavior is indistinguishable from a comment.

The classification failure is the clearest illustration. The control existed as source code, was described accurately in documentation, and was reported as active. It had no effect on the system's behavior for months.

5.2 The compounding property

The three failure modes are not independent; they form a progression. A control with no test can silently stop working. A control that silently stops working still appears in self-reporting. Self-reporting that contains unverified claims trains its reader to trust the verified ones less carefully.

The intervention that breaks this progression is the same at every stage, and it is not more documentation.

5.3 What generalizes

The eight-control mapping is specific to a personal AI agent. The three failure modes are not:

  • A policy artifact whose enforcement path is never invoked

  • An enforcement path that works but is unprotected against regression

  • A self-description that mixes generated facts with hand-maintained assertions

None of these require a novel system to occur, and none are detectable by reading the code with the assumption that it works.

6. Limitations

This is an experience report on a single system, and its evidentiary weight should be assessed accordingly.

Sample size is one. No claim is made that the three failure modes occur at any particular rate in comparable systems, only that they occurred here and that the mechanism by which they occurred is not system-specific.

The evaluator is the author. The code examined was written by the person examining it. This biases toward finding the failures that were findable and away from those requiring an outside perspective — an important caveat given that the first finding, an unconnected control, had gone unnoticed for months precisely by that same author.

Tests verify implementation, not sufficiency. Each of the 46 tests establishes that a control does what it was written to do. None establishes that the control is adequate against a determined adversary, that the threat model is complete, or that the eight-class taxonomy is the right decomposition. A control can be correctly implemented, fully tested, and insufficient.

The threat model is a single-operator personal system. Multi-tenancy, insider threat, and organizational access control are outside scope and would substantially change several of the eight controls, particularly §2.7.

No adversarial evaluation was performed. The prompt injection control in §2.8 was verified for correct behavior on classification inputs. It was not subjected to red-teaming. Published evaluations cited in Article 3 suggest that injection success rates rise substantially with repeated attempts; nothing here measures how this implementation behaves under that pressure.

7. Conclusion

Eight threat classes were translated into eight architectural controls and subjected to automated verification. The mapping was straightforward. The verification was not.

Three of eight controls exhibited gaps between claim and behavior — one entirely unconnected, one untested, one stale in self-description. All three were invisible to code review, documentation review, and the system's own security report, each of which described the intended state accurately.

A fourth observation, from the inverse direction, showed a deliberate security restriction surviving a plausible and well-intentioned change specifically because it had been written as a test rather than as a comment.

The unifying observation across all four is a distinction that is easy to state and apparently easy to lose:

Writing a security control and demonstrating that it functions are separate acts. Only the second one produces evidence. The first produces a claim — and a claim is what a compromised system will also be able to make.

The threat analysis in the preceding three articles identifies what a system should defend against. This article's finding is that identifying the defense and possessing it are separated by a step that is easy to skip, invisible when skipped, and cheap to perform.

References

The threat analysis underlying the eight-class mapping is documented in the preceding three articles of this series, which contain the primary source citations:

Video Companion

  • The Great Unification: Eight Threats, Eight Defenseshttps://youtu.be/kO9Olq4Mslw

    The implementation discussed in this article is documented, step by step, in the accompanying build series.

© 2026. All rights reserved.