One memory for security and engineering: linking attackers to our own code
One memory for security and engineering: linking attackers to our own code In most companies, security incidents and outages live in different worlds. The security team has its alerts, its ticket queue and its own post
One memory for security and engineering: linking attackers to our own code
In most companies, security incidents and outages live in different worlds. The security team has its alerts, its ticket queue and its own postmortems. The engineers have theirs. The same file can be at the center of a login outage on Monday and an attack on Tuesday, and nobody connects the two because the memories are kept in separate places.
When I built DejaVu, an incident-response agent with long-term memory, I put both kinds of incident into one memory bank on purpose. This article is about what that gave me, and where it still falls short.
Repo: https://github.com/cybersleuth25/Deja-Vu
One bank, two kinds of incident
DejaVu stores every resolved incident in a Hindsight memory bank: outages like "checkout 500s, DB pool exhausted" and security incidents like "credential stuffing against /api/v1/login". Each one carries the same fields: the service, the files involved, the attacker's network (ASN) if there was one, any recent code change that came before it, and every action the team took with a verdict on whether it worked.
Because they share a shape, the same code finds similar incidents for both. Here's the scoring that runs on every alert:
def score_incident(alert: dict, inc: dict, recalled: set[str], ...) -> tuple[float, list[str]]:
score, why = 0.0, []
if inc["type"] == alert["type"]:
score += 2; why.append(f"same type ({inc['type']})")
if inc["service"] == alert["service"]:
score += 2; why.append(f"same service ({inc['service']})")
shared = sorted(set(inc.get("files", [])) & set(alert.get("files", [])))
if shared:
score += 3 * len(shared); why.append("shared files: " + ", ".join(shared))
if alert.get("asn") and inc.get("asn") == alert["asn"]:
score += 3; why.append(f"same ASN ({inc['asn']})")
...
if inc["id"] in recalled:
score += 4; why.append(recall_label)
return score, why
Two things to notice. A shared file scores the same whether the old incident was an outage or an attack. And every point comes with a reason, so the engineer can see why an incident was matched ("shared files: auth/login.py", "same ASN (AS14061)", "recalled by Hindsight").
Before and after, for an attack
The security side gets the same "memory off vs memory on" difference as outages do.
Without memory, a credential-stuffing alert gets generic advice: rate-limit the offending IPs.
With memory:
This security incident on auth-api matches 4 past incidents: INC-0002, INC-0008, INC-0012, INC-0019. Block ASN at WAF worked 3 of 4 times; Per-IP rate limit failed 3 of 3.
The notes on the first of those incidents explain why per-IP limits fail here: "Attacker spread load across 63 IPs at ~4 req/min each." Rate-limiting each IP looks reasonable and does nothing against a botnet that keeps every IP under the limit. Blocking the whole network at the firewall worked. The memory remembers that, so nobody has to rediscover it at 2 AM.
[Screenshot 1: credential stuffing incident with memory on, showing "Block ASN at WAF 3 of 4" and "Per-IP rate limit" under "We tried this before and it failed"]
Where the two worlds meet: files
The interesting part is what happens when you look at a single file across both kinds of incident. DejaVu's pull-request check does exactly that: give it a list of changed files and it reports every incident each file was involved in, of either kind.
Here is its real output for middleware/jwt_verify.py:
middleware/jwt_verify.pywas involved in 2 outages and 1 security incident
- INC-0025 2026-10-27 SEV1: Login 500s: JWT verification failing after key rotation
- INC-0026 2026-10-27 SEV1: Forged JWTs with alg=none accepted on internal route
- INC-0028 2026-11-02 SEV1: API 5xx spike: auth middleware rejecting valid tokens
Fixes that did not work for these incidents: Restart service (INC-0025, INC-0028)
[Screenshot 2: the pull request check showing middleware/jwt_verify.py with 2 outages and 1 security incident]
Look at the dates. On the same day, that file broke logins for real users and accepted forged tokens from an attacker. Separate tools would file those under two teams. One memory puts them in one list, in front of whoever is about to change that file next.
Two agents, one memory
DejaVu also runs as two agents in Band: a SecurityAgent and a ReliabilityAgent. They read the same Hindsight bank, so a verdict recorded by one is recalled by the other.
Each agent only answers alerts in its own area and hands the rest to the other one:
mine = (alert["type"] == "security") == (role == "security")
if not mine:
if from_agent:
return {"reply": f"This is a {alert['type']} incident; already routed, not handing back.", "handoff": None}
other = "security" if role == "reliability" else "reliability"
return {"reply": f"This looks like a {alert['type']} incident ({alert['title']}). "
f"Handing off to the {other.capitalize()}Agent.", "handoff": other}
Mention the ReliabilityAgent with "credential stuffing on /api/v1/login from AS14061" and it replies "This looks like a security incident... Handing off to the SecurityAgent". The SecurityAgent then answers from the shared memory with the 3-of-4 and 3-of-3 track record. The from_agent check means an agent never hands a message back, so the two can't loop.
[Screenshot 3: the Band room showing the ReliabilityAgent handing off and the SecurityAgent answering]
Limitations
The SecurityAgent's code-risk handoff rarely fires.** It's designed to ping the ReliabilityAgent when the code under attack also has outage history. The built-in attack scenarios target auth/login.py and auth/sso.py, which only have security history, so in practice that cross-check doesn't trigger. The file overlap shows up in the pull-request check instead, as above.
Linking is by file path and attacker network only.** If a file is renamed or the attacker switches networks, the link is lost unless Hindsight's recall happens to connect the descriptions.
The traffic is simulated.** Detection runs on generated events, not real logs or a real firewall.
Takeaway
Attackers and outages don't respect team boundaries; they hit the same fragile code. Keeping one memory for both, with the same fields and the same scoring, is a small design choice that lets an agent say "this file was attacked and broke production on the same day". Two separate memories never could.
Hindsight docs: https://hindsight.vectorize.io/
Hindsight on GitHub: https://github.com/vectorize-io/hindsight
What is agent memory: https://vectorize.io/what-is-agent-memory
Tags: @Code.in
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.