This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Rapid Adaptive Deterministic Protection Against AI-Generated Threats
Wednesday, 28 October 2026
Behavioural inference, bait endpoints, and some rather sneaky 404s.
Someone requests a PHP file from your site. It doesn't exist. You return a 404.
A little later, someone else requests the same file. Different address, different user agent. Same small collection of parameters.
Then another caller tries a slightly different filename, with a body that looks remarkably similar. A few values change. A response comes back. Another variation follows.
Each caller might make only a couple of requests. There may be nothing particularly interesting about their individual rate. Taken together, though, something is happening.
Your site has become the subject of an experiment.
The interesting question is whether we can recognise that experiment, learn what its requests have in common, and respond before we know the name of the vulnerability someone is looking for.
This is where I want to take a more firewall-like function in Stylo.Bot: rapid, adaptive protection whose enforcement remains deterministic, even when the activity aimed at it is being generated and revised by an AI agent.
It is another application of my behavioural inference approach. Add small, focused detector components, called atoms, that characterise the missing evidence, let observations accumulate into useful representations, and use bounded policies to dampen or contain the activity. An LLM can help describe what emerges; it does not have to make the blocking decision.
Where this stands: This article introduces a proposed extension. Stylo.Bot already has behavioural inference, request-pattern detectors, response history, clustering and honeypots. Learned payload families and the evidence-driven containment loop described here are additions to build and evaluate. The examples are illustrative, not results from a deployed exploit detector.
What AI Changes About the Problem
An automated attacker can produce variations, try them, read the response and try again. An AI agent can make that loop more flexible: it can revise its approach when a response suggests a different target or interpretation. Cloudflare's recent full-transaction detection announcement specifically discusses persistent agents adapting their requests using feedback.
We can characterise the resulting activity without reliably attributing it to an AI. A human-written scanner might behave similarly. The protection should work against the behaviour either way.
The speed matters. If every new variation needs a human to write a signature, or a remote model to inspect and approve a request, the defender has introduced a bottleneck into its own response.
The alternative I want to explore is a cheap deterministic decision over changing learned state: recognise a request family, gather stronger evidence with a controlled response, then publish a narrow protection update. That is an architectural goal here, not a latency result we've already measured.
What These Attacks Teach Us to Look For
The recent PHP2Shell and Plex2Shell reports prompted this, but the useful lesson extends beyond their particular payloads.
PHP's advisories describe failures involving memory handling and state surviving between requests. Plex's notice describes a chain through server-side requests, internal functionality, credentials and file writes. They are different vulnerabilities, with different conditions and fixes. PHP advisories, related stale-state advisory, Plex security notice.
From outside the application, we cannot directly observe a corrupted pointer or know everything happening inside a worker process. We can observe some of the activity around an attempt:
- Callers converge on particular targets.
- Requests carry related structures or fragments.
- Inputs change while other parts of the request stay stable.
- Follow-up requests depend on information returned earlier.
- Response patterns change as the caller explores.
Some attacks exhibit several of these. Some need only one request and provide almost no warning. The proposal is to make better use of the evidence we can observe.
That is a familiar problem if you've read my Behavioural Inference articles: characterise observations, accumulate evidence, and make a bounded decision from it.
The First Clue Might Be a Very Boring 404
A normal access log encourages us to read one line at a time:
POST /service/report.php -> 404
Fine. A missing file.
But consider three different views of that same event:
| View | Question |
|---|---|
| Caller | Has this client been searching around the site? |
| Target | Why are many clients suddenly asking for this particular path? |
| Request family | Are those clients sending variations of the same thing? |
Stylo.Bot already does a lot with the first view. The second and third are what I want to develop here.
A coordinated scan does not have to look aggressive from any one caller. Its commonality may live in the target, rather than the source address.
We can learn how often a path is requested, what kinds of callers normally reach it, what methods they use and which responses it normally produces. Then compare a recent window with that slower background.
A quiet path attracting unusual attention becomes worth examining. A previously unseen path attracting several similar POST bodies becomes more interesting still.
Notice that we haven't added a rule saying “block /service/report.php”. The path became interesting because demand changed.
A 404 is an observation, not an accusation. A broken link in a popular newsletter, an old mobile client or a bad deployment can produce a sudden burst too. We need enough context to distinguish those from probing.
It also matters who produced the 404. An origin response, a CDN response and a rejection generated by Stylo.Bot are different evidence. If our own protection returns a 404 and we then count it as proof that the origin is under attack, we've built ourselves a very convincing feedback loop.
Different Paths Can Be the Same Search
Exact path matching gives us a useful starting point. Attackers do not have to keep using the exact path.
These hypothetical requests might be related:
/service/report.php
/service/report_old.php
/legacy/report.php
Or they might have nothing to do with each other. We need a representation that lets us compare them without deciding the answer in advance.
I would retain several views:
- The exact path, scoped to the site and HTTP method.
- Its segments, suffix and other structural properties.
- An inferred path family, where observations support grouping.
- Its relationship to known application routes and navigation.
For example, repeated numeric IDs under /orders/ are quite different from callers exploring similarly named PHP files on a site whose operator has declared that it serves no PHP.
Stylo.Bot already has a PathNormalizer for Markov analysis. It groups things such as IDs, hashes and slugs. That is useful existing machinery.
For security analysis, I would keep the exact identity alongside it. The current normaliser deliberately groups static assets and lowercases paths. Those are useful simplifications for some navigation questions, but can erase distinctions a particular origin considers important.
Commonality is an additional view, not permission to discard the original observation.
That same principle becomes even more important when we look at the payload.
A PHP Path With a Certain Payload
Suppose the requests share more than a filename. They use the same method, a similar collection of fields, and a body with a repeated structure.
Now we have something more useful than “PHP looks suspicious”.
On a WordPress site, requests to PHP endpoints are ordinary business. On a site with no PHP routes, they may be probes. In either case, the structure and evolution of the input can tell us more than the extension.
Currently, HaxxorAtom checks path and query metadata against attack-pattern categories. That is useful, but it does not give us a learned characterisation of arbitrary request bodies.
We need to represent those bodies in a form we can compare.
This is another version of the question from SHOW ME WHAT YOU GOT: what information should the vector preserve for the decision we're making?
Describe the Structure
Consider an ordinary JSON search request:
{
"query": "walking boots",
"limit": 20
}
A second request searches for jackets. The words differ, but the structure is the same: an object with a string query and a numeric limit.
We can characterise that without understanding English:
| Property | What it helps distinguish |
|---|---|
| Actual body size | Typical input versus an unusual volume of input |
| Nesting depth | A flat form versus deeply nested containers |
| Field and part counts | Normal form structure versus a large or changing collection |
| Value types | A field changing from a string into an object or array |
| Duplicate keys | Repeated input that different parsers might interpret differently |
| Encoding and repetition | Long repeated runs, unusual encoding or structural changes |
These measurements become coordinates in a small vector. Over time, we learn their normal ranges for an endpoint and content type.
An upload endpoint and a login endpoint should not share one definition of normal. Neither should a JSON API and a free-text editor.
The extractor is ordinary code. The learned part is the distribution of what this application actually receives.
Preserve Some Local Detail Too
Structure alone loses information. Two bodies can have identical field types while one contains a dangerous value.
We therefore need another view that preserves short sequences of bytes or tokens.
An n-gram is just a sliding window. For a three-character window over report, the fragments are:
rep epo por ort
The same idea works over bytes or structural tokens. We can compare the fragments a request contains, learn which are common in a context, and detect families sharing unusual fragments even when other values change.
This has a long research history. PAYL models payload byte distributions; Anagram examines higher-order n-grams to preserve more local ordering. These are useful precedents for statistical payload analysis without a neural model. Their historical benchmark results do not tell us how well a new implementation would work on our traffic. PAYL paper, Anagram paper.
For a compact implementation, feature hashing can map many tokens into a fixed-size representation. That gives us bounded storage at the cost of collisions. It doesn't give us a semantic embedding, and it doesn't make retained features automatically anonymous.
I would initially keep the structural vector and token representation separate. Each answers a different question. We can combine their evidence without allowing a large token vector to overwhelm every other behavioural dimension.
Representation is where we choose what we can learn. Replace every value with
<string>and we may erase the dangerous part. Keep every raw value and we collect secrets, personal data and enormous amounts of irrelevant variation. We need useful summaries, explicit limits and a reason for every retained feature.
The Movement Between Requests Is Part of the Payload's Story
Now consider a sequence:
Similar target, similar body
Similar target, one changed field
Same body structure, different encoding
Response changes
Follow-up uses something from that response
This does not prove exploitation. Developers debugging a client and legitimate security testers also experiment with inputs.
But it gives us a behaviour to characterise: controlled variation followed by response-dependent action.
We could measure how much the target changes, how much the payload changes, which features stay stable, and whether response differences precede the next change.
Across callers, we could ask whether the same structure and sequence keeps appearing. A collection of addresses may be participating in the same campaign even though their individual request rates are unremarkable.
This fits the Markov and clustering ideas already used in Stylo.Bot. Transitions describe what follows what; similarity connects observations; community discovery can expose groups worth investigating.
A graph community is not a malicious label. It is a group of related observations. The evidence explaining why that relationship matters must remain available.
Nor do we need to wait for a full clustering pass on every request. Cheap comparisons against a bounded set of current families can happen inline; wider discovery can run in the background.
Deterministic Does Not Mean Frozen
A deterministic decision uses defined inputs and defined state. Given the same request representation, model version, policy version and recorded time/assignment, it produces the same result.
Learning changes that state. It does not require the decision itself to become an unconstrained judgement.
We can learn which targets are normal for this application, which payload structures belong to an endpoint, and which request families keep producing particular follow-up behaviour. New evidence updates the statistics. An admitted snapshot becomes the reference for subsequent decisions.
That gives us two different activities:
| Activity | How it operates |
|---|---|
| Handle the current request | Bounded feature extraction, prototype comparison and an allowed policy action |
| Adapt the protection | Accumulate evidence, update candidate representations, evaluate and publish a versioned snapshot |
The second does not have to wait for a nightly training job. Small online updates can make a newly observed family useful quickly. Wider graph discovery and optional LLM enrichment can happen in the background.
Give the Probe Family Its Own Centroid
A centroid is a statistical centre of a collection of vectors. For our probe family, those vectors describe the payload, target and observed behaviour. We also retain variation, support and recent movement: the centre alone does not describe the whole family.
When we spoof an endpoint, its response becomes part of the context. We can observe how the caller reacts to a fixed error, a simulated status response or an inert token. Across related interactions, that gives us a behavioural characterisation of the probe family.
It needs its own representation. We should not overwrite the existing caller fingerprint with an incompatible payload vector. Nor should we forget order: the centroid summarises what we observed; transition counts preserve useful parts of what followed what.
This is the same behavioural inference pattern applied to another entity. Previously we characterised a caller. Here we also characterise an emerging class of activity.
What Is an Atom?
An atom is a small component with a defined detection responsibility. It reads the current request, existing signals or retained observations, then publishes evidence for other components to use. One might recognise a honeypot path; another might compare the caller's current behaviour with its history.
A signal is a named observation or derived value. A contribution adds an interpretation, such as evidence supporting a bot classification. The distinction matters: “this request contains a previously issued canary” is an observation; “this caller is malicious” is a conclusion that needs more context.
The Stylo.Bot orchestrator runs the detection work and exposes aggregated evidence to the rest of the system. Atoms can declare the signals they require; priority and dependency rules determine when they run. The signal-contract documentation explains that arrangement in detail. An atom isn't an autonomous LLM agent, and adding an atom doesn't require adding a model.
For a small example, the existing HoneypotLinkAtom publishes the path evidence it has established:
sink.Raise($"{SignalMatchedPattern}:{matchedPattern}", sessionId);
sink.Raise($"{SignalNormalizedPath}:{normalizedPath}", sessionId);
sink.Raise($"{SignalTier}:{(int)tier}", sessionId);
These are actual lines from its BuildContribution method. The signal keys name the matched pattern, normalised path and honeypot tier; sessionId associates them with this detection evaluation. Publishing those facts makes them available to the wider inference pipeline. It does not itself send a blocking HTTP response.
Context Can Dampen a Particular Suspicion
The existing HaxxorAtom provides a useful example of keeping evidence specific:
var pathProbeExempt = _exemptStore is not null
&& _exemptStore.IsExempt(Honeypot.HoneypotPathDefinitions.NormalizePath(path), context);
if (!pathProbeExempt && MatchesAnyPathPattern(pathSpan, pathPatterns.PathProbes)) matchedFlags |= CategoryFlag.PathProbes;
if (MatchesAnyPathPattern(pathSpan, pathPatterns.Webshell)) matchedFlags |= CategoryFlag.Webshell;
On a site whose profile declares the corresponding framework paths legitimate, their existence should not count as probing for an absent framework. The exemption affects that particular inference. Other checks, such as the webshell check immediately below it, still run.
That is the kind of damping we want: context can weaken one explanation without granting the whole request a free pass.
A Canary Match Supplies Linkage, Not a Verdict
The optional ApiHolodeck BeaconAtom looks up candidate markers in its store. Once it finds one, it emits signals including:
sink.Raise("beacon.matched:true", sessionId);
sink.Raise($"beacon.original_fingerprint:{record.Fingerprint}", sessionId);
Its contribution is deliberately informational. Those two lines connect the current request to an earlier issuance; they do not prove hostile intent. The action taken with the marker, the route context and other observations supply the rest of the interpretation.
That separation gives our proposed bait learner somewhere to attach: reuse the linkage, then accumulate evidence about the behaviour around it.
Extend the Same Pattern
The proposed target-demand atom would publish unusual population attention. A payload atom would publish structure and commonality. A mutation atom would compare related attempts. A bait-evidence atom would combine issued-marker linkage with the observed follow-up action.
There are already examples of broader behavioural analysis in ContentSequenceAtom and ClusterAtom. The new observations would extend that evidence system rather than establish another independent scoring and blocking pipeline.
These new atom responsibilities are proposals. The linked classes and excerpts are existing source; none of them implements the complete adaptive bait loop described here. The action policy remains responsible for deciding how combined evidence affects traffic.
Fixed code, learned parameters. The atom knows how to measure structure. The system learns what structures belong together and which observations justify containment. We still choose the actions it is allowed to take.
There is a trap here: sending enough requests must not let an attacker redefine normal. Keep a stable reference beside an adapting candidate, cap each source's influence, and quarantine suspicious samples from normal-profile updates. A 200 response is not a clean training label.
Our 404s for Bots Can Be Sneaky
So far, we've watched the experiment. Honeypots let us introduce a controlled observation of our own.
Suppose the site operator has declared a namespace unused, or explicitly enabled a decoy route. We have a suspicious request family targeting it.
We can terminate those requests locally, before they reach the real application, and return a bounded fake response.
The endpoint does not have to exist at the origin. For selected bot traffic, it can exist only inside our simulation.
It might return an ordinary-looking 404 with a synthetic reference, or a deliberately configured fake service response. If a subsequent request uses an inert token issued by that simulation in a sensitive operation, we have an additional relationship to examine.
That is the sneaky part of the 404: the real application stays uninvolved while the response gives us an opportunity to observe the caller's next move. Whether a particular response actually attracts useful follow-up is something to test, not assume.
We didn't have to know which exploit string was fashionable this morning. We observed what the caller did with information we deliberately supplied.
flowchart TD
A["Requests across callers"] --> B["Target and payload families"]
B --> C["Compare with learned local behaviour"]
C --> D["Suspicious family with supporting evidence"]
D --> E["Check whether local diversion is allowed"]
E --> F["Bounded decoy with an issued canary"]
F --> G["Observe follow-up behaviour"]
G --> H["Update evidence for this family"]
H --> I["Evaluate a temporary containment policy"]
Stylo.Bot already has useful pieces for this. Its honeypot terminator can stop selected requests before the origin. Simulation packs can serve static templates. Optional canary generation, storage and beacon lookup provide a starting point for linking follow-ups.
The addition is an experiment record connecting the request family, the response variant, the issued canary and the later observation. It feeds the probe-family centroid and sequence evidence. The current beacon lookup provides linkage information; making that into learned enforcement needs further work.
We can do the first version entirely with static responses. The interesting intelligence is in the relationship between observations.
Following a link is weak evidence. Crawlers, previews and security scanners follow things too. Attempting an operation using a fake credential may be stronger, but it still needs context. The canary shows a relationship; the action taken with it contributes to the interpretation.
The marker must be an issued, expiring value with enough strength to avoid accidental matches. It must never grant access to the real application. Store its issuance before serving the response, and account for replay or a response being copied to another caller.
A 404 by itself is not permission to hijack a route. There must be an explicit reason diversion is safe for that application. And there is no requirement to offer an interactive decoy everywhere: an ordinary local rejection remains a useful policy.
Configure a Named Bait Endpoint
We need to say what we are simulating. The configuration should give each bait endpoint a name, the paths and methods it owns, who can receive it, what it returns and what we want to observe.
A small part of the proposed configuration, rather than a feature available today, could look like this:
{
"legacy-report": {
"Path": "/retired/report.php",
"Methods": ["GET", "POST"],
"Eligibility": {
"RequireBotClassification": true,
"Ownership": "ReservedDecoy"
},
"Response": {
"StatusCode": 404,
"Data": {
"error": "not_found",
"detail": "The report service is unavailable.",
"reference": { "$slot": "canary" }
},
"Generation": {
"Provider": "llmock",
"Mode": "PrepareAndFreeze",
"MutableFields": ["detail"],
"Instructions": "Prepare synthetic legacy report error text. Keep the JSON keys and types. Change only detail. No scripts, external URLs or real credentials."
}
}
}
}
The name is stable even if we later prepare several response variants. The path is operator-owned bait. The canary slot is rendered by trusted code, not generated as a credential by the model.
The full specification also needs expiry, response-size limits, interaction budgets and the learning policy. A future template could instantiate a bait endpoint at runtime within an explicitly allowed namespace. An arbitrary caller's choice of URL must not create a real route or displace a legitimate endpoint.
Where LLMApi Fits
My LLMApi project already generates shaped mock responses and maintains context across mock API calls. It is a useful starting point for making the fake endpoint coherent.
It needs a constrained adapter for this use. Its normal request-driven shape controls, tool calls and outbound actions are not things we should expose to the traffic we are investigating.
The model could prepare response data from the operator's instructions. We validate it against the configured fields, freeze it into a versioned snapshot, and serve that snapshot with controlled substitutions. A model timeout leaves us with static or previously validated content. It does not force a request through to the origin.
That preserves deterministic serving. Even with temperature zero, model generation itself is not a reproducibility contract; the admitted artifact is.
For a simulated multi-step API, a small declarative state machine could define allowed transitions: issue an inert token, return a fake status, record its use. We are pretending an endpoint has state, not executing submitted commands or installing vulnerable software to see what happens.
From Evidence to a Firewall Decision
What might the system learn to block?
Something narrower than “all PHP”, and more useful than one exact payload copied into a permanent rule.
Conceptually, a containment decision could say:
For this site and target family:
this well-linked caller has interacted with a local decoy;
these requests belong to the associated probe family;
divert matching requests locally for a limited period.
Those are explanatory statements, not a proposed configuration language or output from a working detector.
The runtime policy would carry its scope, supporting observations, representation version, action and expiry. Before extending it to related callers, we would compare the proposed match against retained benign examples and require stronger evidence.
This is where the firewall function starts to emerge. The system has learned a relationship between a target, an input family and observed behaviour, then used it to control what reaches the application.
We still choose the allowed actions: observe, divert, rate-limit or temporarily deny. We also choose maximum scope and lifetime. Learning supplies evidence and matching parameters inside those boundaries.
It does not get to invent executable rules, fetch a URL from a payload, or permanently ban an entire cluster because one member touched a trap.
And the block needs to expire. Applications change, clients change, and evidence gets old. Temporary policies can be renewed with fresh support, revoked by the operator or retired when they stop explaining the traffic.
Let an LLM Describe What We Found
An optional LLM has another useful job: describing an emerging family.
We can supply the dimension definitions, centroid and variation, representative redacted structures, and the transitions we observed. It can propose a short name and description explaining what those measurements suggest. That makes a dashboard much more readable than a family ID and a bag of coordinates.
Later, when our trusted CVE list updates, we can compare a retained family with the new advisory. A model might propose that the observed targeting and payload behaviour are consistent with that issue. Keep the advisory revision, supporting evidence and the association's provisional status.
A description is an interpretation. A CVE association is a hypothesis. Neither silently becomes a blocking rule or proof that an exploit succeeded.
The same boundary applies if a model supplies additional characterisation features. Admit a bounded, versioned result, evaluate what it adds, and let the deterministic policy consume the approved representation. It is optional enrichment of the behavioural system.
The Response Matters, Including Our Own
The same input followed by a normal rejection tells a different story from an input followed by an unexpected disclosure.
Cloudflare's full-transaction detection announcement on 21 October describes this explicitly: combine request and response evidence, then use response rules to prevent a matching response reaching the caller. Its initial managed protections address PHP2Shell.
Cloudflare also has Application Profiles, which model expected request structures. This is established territory for modern firewalls. The interesting work for Stylo.Bot is how these observations fit into its existing behavioural history, communities and local response policies.
We need to be precise about our current boundary. Stylo.Bot already records upstream status and response history. Its current middleware does not generally capture and learn response bodies just because a body-summary type exists in the source.
Observing a response after delivery can help protect a later request. It cannot stop the current response leaking data.
If we want that second capability, we need a real response gate: inspect before committing headers or bytes, with deliberate handling for streaming, compression, size limits and timeouts. The earlier StyloWall response-layer work provides useful background, but an optimisation transform is not automatically a security boundary.
Even a response gate cannot undo code already executed at the origin. That matters particularly for the kinds of runtime and chained failures which motivated this article.
Our honeypot experiment also changes what the caller sees. Synthetic responses must stay labelled as synthetic. They must not train the model of normal origin behaviour, and our own rejection codes must not become independent evidence confirming our earlier suspicion.
Build the Smallest Useful Version First
The first demonstration should start with the boring case: many callers requesting the same missing path, then variations that retain common structure.
I would build it in this order:
- Track target demand across callers, retaining exact paths and related families.
- Add atoms for bounded payload structure, commonality and mutation evidence.
- Configure named bait endpoints and build behavioural centroids from the resulting interactions.
- Publish temporary containment from validated evidence and versioned state.
- Add optional LLMApi-prepared responses, family descriptions and provisional CVE associations.
That gives us a useful progression without pretending that response-body enforcement is already solved.
The visualisation almost suggests itself: a target starts attracting callers; related payloads form a family; one branch enters a decoy; a follow-up adds a stronger evidence link; a narrow containment scope appears.
Beside it, show the broken newsletter link producing a burst of ordinary 404s. If the system cannot explain why those two cases differ, the demo hasn't taught us enough.
This is also a good fit for the behavioural testing work. Generate harmless traffic against a mock origin, include normal uploads, actual PHP application routes, new releases, legitimate scanners and rotating clients, then hold out whole campaigns and later time windows for evaluation.
Measure false positives, detection delay, requests reaching the origin before containment and resource use. Test random-path floods and excessive bodies too: a detector that learns an unbounded collection of attacker-chosen features is another denial-of-service target.
A size cap should mean “partially inspected”, not “clean”. Unknown and unsupported inputs need visible states and an explicit policy.
Learning What a Caller Is Trying to Do
Stylo.Bot began by asking what behaviour tells us about a caller. This extension adds another question: what do related callers, their chosen targets and their inputs tell us about the activity taking place?
A repeated PHP request becomes interesting because of where it sits in that activity. A payload becomes interesting because of its structure, its relationship to other payloads and what happens next. A honeypot gives us a controlled way to collect another piece of evidence.
We may never infer the precise vulnerability being attempted. We can still learn enough to recognise and contain a family of probing behaviour.
That is the firewall direction I want to explore: rapid adaptation through behavioural inference, deterministic enforcement from inspectable evidence, and controlled bait that helps us learn what to protect against next.