Incident Response Document

What counts as a personal data breach, who declares one, the clocks that start, and who has to be told.

Incident Response Document — Heimdall API

1. Purpose

GDPR Art. 33 requires a personal data breach to be notified to the supervisory authority within 72 hours of the controller becoming aware of it, and Art. 34 requires the affected data subjects be told without undue delay where the risk to them is high. LGPD Art. 48 requires communication to the ANPD and the subjects, which ANPD Resolution CD/ANPD nº 15 of 24 April 2024 fixes at 3 working days. Art. 33(5) requires every breach be documented, whether or not it was notifiable.

None of that is possible without deciding, in advance and in writing, what counts and who decides. That is what this document is.

2. Who decides

RoleWhoWhat they do
Incident leadArtur Rios (controller)Declares an incident, decides whether it is a personal data breach, decides whether it is notifiable, and signs the register entry
EncarregadoArtur RiosNotifies the ANPD, and the data subjects where required
Processor contactArtur RiosNotifies tenant controllers whose scopes are affected

All three are the same person, which for a project this size is honest rather than a gap to paper over — see §2 of the Data Protection Document . The consequence worth naming: there is nobody to escalate to, so the deadlines in §4 are the only forcing function, and they are why they are written down.

3. What counts as a personal data breach

GDPR Art. 4(12): a breach of security leading to accidental or unlawful destruction, loss, alteration, unauthorised disclosure of, or access to personal data. Not only theft — losing data irrecoverably is a breach, and so is altering it without authority.

Concretely, for this system:

Is a breachIs not, by itself
The database or a backup copied by somebody unauthorisedA failed sign-in, however many
An identity acting outside its authorisation and reaching another tenant’s dataA refused authorisation attempt — that is the control working
Password hashes, TOTP secrets or tokens disclosedAn expired token being rejected
An erasure that did not happen, or was undone by a restore and not reconciledA scheduled pass failing once and succeeding on the next tick
Log files containing personal data leaving the host unencryptedLogs reaching the operator’s own collector as designed
A backup restored over current data, losing records irrecoverablyA restore that was reconciled per NFR-25

A signal from §6 is not a breach. It is a reason to look. Treating detection as declaration would put the 72-hour clock in the hands of a threshold.

4. The clocks, and when they start

Both run from awareness, not from the incident. Awareness is when the incident lead has enough information to conclude, on reasonable investigation, that personal data was probably affected — not the moment of certainty, and not the moment a log line was written that nobody read.

ObligationDeadlineFrom
Notify the ANPD (LGPD Art. 48, Res. 15/2024)3 working daysAwareness
Notify the supervisory authority (GDPR Art. 33)72 hoursAwareness
Notify affected data subjects (Art. 34, LGPD Art. 48)Without undue delay, where risk is highAwareness
Notify a tenant controller whose scope is affected24 hours — DPA §8Awareness

The 24 hours to tenants is deliberately the shortest. A tenant’s own 72-hour clock starts when they become aware, which our notification is what creates; a processor deadline equal to the controller’s would leave the controller no time at all.

GDPR applies. The determination was made on 10 September 2026 and is recorded in §7.2 of the Data Protection Document: the service is open to EEA data subjects. Both sets of deadlines are therefore live, and the shorter of the two governs — three working days to the ANPD, and, where a supervisory authority is also owed notice, seventy-two hours.

5. The procedure

  1. Contain. Stop the bleeding before investigating. Revoke the signing secret (HEIMDALL_AUTH_TOKEN_SECRET, rotating through _PREVIOUS), restrict or suspend the identities involved (UC-44), and if a scope is implicated, consider disabling its Google sign-in.
  2. Record the time of awareness. In the register, before anything else. Every deadline in §4 counts from this timestamp, and reconstructing it afterwards from memory is how a 72-hour obligation becomes a missed one.
  3. Establish scope. Which identities, which scopes, which categories of data, and over what period. The audit trail is the primary source; the application logs are the second.
  4. Decide: is it a personal data breach? Against §3. Record the reasoning either way.
  5. Decide: is it notifiable? A breach is notifiable unless it is unlikely to result in a risk to the rights and freedoms of the data subjects. Record the reasoning either way — this is the part regulators ask about, and a register full of entries reading “not notifiable” with no reasoning is worse than no register.
  6. Notify, per §4.
  7. Complete the register entry, per §7.
  8. Review. What made it possible, what made it detectable, what delayed either. Feed the answers into the Threat Model and the periodic review in the Development Workflow .

6. Detection

NFR-26. The system produced strong signals from the beginning and nothing consumed any of them — the audit trail recorded every refused write with its reason, the lockout counters tracked per-account brute force, and the hash gate shed under load. All written down; none read. A trail nobody reads does not make anyone aware of anything, and awareness is what starts every clock in §4.

SecurityMonitoringService now reads them on a schedule and writes what it finds to the log at warning level, each line carrying the marker SECURITY_SIGNAL and a stable Kind:

KindRaised whenWhy it matters
REPEATED_REFUSALSOne identity has more refused writes than the threshold, within the windowSustained refusals are what somebody probing for what they may do looks like
LOCKOUT_SPIKEMore accounts are locked out at once than the thresholdOne is ordinary; many together is credential stuffing against a list of addresses
CREDENTIAL_VERIFICATION_SHEDDINGThe hash gate refuses more often than the thresholdThe process is at its concurrent-derivation limit (TH-03) — a load condition, and also a lever

Every threshold is a rate, not a total. Twenty refusals over a year is somebody who forgets their password; twenty in a quarter of an hour is somebody trying things. A total alerts on the first and never notices the second.

Signals repeat while the condition persists. There is no suppression, deliberately: an attack that continues is more interesting on its tenth tick than its first, and a detector that goes quiet while the thing it detects is still happening is worse than none.

Where the alerts go is the operator’s choice. This API has no opinion about pagers or SIEMs, and building an integration for one would be guessing. What it does is make the signal unmistakable and machine-matchable where the operator already collects — the logs. Match on SECURITY_SIGNAL and the Kind, not the prose, which is written for whoever reads the line afterwards.

A signal is a warning-level line in the log, on stdout and in the log directory:

SECURITY_SIGNAL REPEATED_REFUSALS: One identity had 23 writes refused in the last
00:15:00. ... (count 23, actor 3f2a91c4-7d55-4e1a-9b2f-08c7d1e6a934)

What collects them

scripts/security_signals.py, on a schedule. It reads the log files, finds the signals it has not reported before, and mails them to one address through the Mailgun account the API already uses. Setup — the environment variables, the cron line, and what to do about the log directory living in a Docker volume — is in the operations guide, under Security signal detection. This section records what the job means for §4.

It is the thing that makes somebody aware, which is the moment every deadline in §4 starts. The alert says so in its own body, because the timestamp that goes in the register is when a person read the message — not when the signal was raised, and not when the job ran.

Three properties are worth knowing before an incident rather than during one:

  • Each signal is reported once. The job keeps a watermark. Signals repeat on every monitoring tick while a condition persists — deliberately, per above — and without a watermark every run would resend the history until the alerts were filtered into a folder, which is the same non-detection this closes, wearing a different hat.
  • A delivery failure is loud. The job exits non-zero and prints the signals it was holding to stderr, so a broken collector announces itself. Give cron a MAILTO, or the failure of the thing that catches failures is itself uncaught.
  • The watermark only advances after delivery succeeds. A Mailgun outage delays signals; it does not consume them.

What is in the alert. The marker, the kind, the count, the timestamp and the actor’s PublicId. A PublicId is a pseudonym — it names an account to somebody holding the database and to nobody else — so the alert can be read on a phone without carrying anybody’s address into a mailbox. NFR-22 keeps addresses out of the logs in the first place; the alert inherits that rather than re-deciding it.

The residual is the mailbox. Delivery is verifiable and being read is not. Nothing here can tell whether the message was opened, which is why R-08 in the DPIA is Low rather than closed. A signal that arrives and sits unread is still a signal nobody received.

Detection is off only if HEIMDALL_MONITORING_ENABLED=false, which logs a warning saying what has been given up.

7. The breach register

Art. 33(5) requires documentation of every breach, including those judged not notifiable. Keep it outside this repository — it will contain details of real incidents — and use this shape:

FieldNotes
ReferenceSequential
Time of awarenessThe timestamp from step 2. Every deadline counts from here
Time of the incidentWhere known; often it is not, and “unknown” is a legitimate entry
How it came to attentionA SECURITY_SIGNAL, a report, a routine review — this is what tells you whether detection works
NatureDestruction, loss, alteration, disclosure, or unauthorised access
Categories and approximate number of data subjectsApproximate is what Art. 33(3)(a) asks for
Categories and approximate number of records
Likely consequencesArt. 33(3)(c)
Measures takenContainment, remediation, mitigation of adverse effects
Notifiable? With reasoningBoth answers need reasoning. “No” needs it most
Authority notified, whenOr why not
Subjects notified, whenOr why not
Tenant controllers notified, whenPer DPA §8
Signed off byThe incident lead

A register entry is written for an incident judged not to be a breach at all, too, with that reasoning. The judgement is the evidence that the question was asked.

8. Reviewing this document

Reviewed after every incident, and on the cadence in the Development Workflow — whichever comes first. An untested incident procedure is a document, not a capability.