laravel-data-breach-registry maintained by ginkelsoft
Ginkelsoft Laravel Data Breach Registry
Overview
Implements the GDPR art. 33 (notification to supervisory authority within 72 hours) and art. 34 (notification to affected subjects when the risk is high) personal-data breach register for a Laravel application.
Two tables work together. breach_register holds the current state of
each breach — open, contained, resolved, reported to whom and when.
breach_event_log is the append-only, hash-chained audit trail of every
state transition, built on the shared HashChain from
ginkelsoft/laravel-compliance-core. The register answers "where do we
stand?"; the event log answers "how did we get here?", and is the part an
auditor will scrutinize.
This is the breach registry member of the GinkelSoft compliance family.
The family
| Package | GDPR Article(s) | Role |
|---|---|---|
laravel-compliance-core |
art. 5(2) | Shared primitives |
laravel-data-retention |
art. 5(1)(e) | Storage limitation |
laravel-data-right-to-be-forgotten |
art. 17 | Subject-driven erasure |
laravel-data-subject-access |
art. 15 + 20 | Subject access |
laravel-data-consent |
art. 6(1)(a) + 7 | Consent registry |
laravel-data-breach-registry |
art. 33 + 34 | Breach register — this package |
laravel-compliance-hub |
art. 5(2) | Umbrella |
How it works
Register a breach
use Ginkelsoft\DataBreachRegistry\Actions\BreachRegistry;
use Illuminate\Support\Carbon;
$registry = new BreachRegistry;
$breach = $registry->register(
reference: 'BREACH-2026-001',
discoveredAt: Carbon::parse('2026-05-27 09:15'),
description: 'Misdirected client export sent to wrong recipient.',
severity: 'high',
occurredAt: Carbon::parse('2026-05-27 08:50'),
dataCategories: ['name', 'email', 'order_history'],
subjectsAffected: 42,
cause: 'Operator selected the wrong recipient group.',
actor: 'ops@example.com',
);
The 72-hour deadline for notifying the supervisory authority (AP in NL)
runs from discoveredAt. The model exposes
authorityNotificationDeadline() and isAuthorityNotificationOverdue()
for direct use in dashboards.
Update, contain, resolve
$registry->update('BREACH-2026-001', [
'mitigation' => 'Recipient confirmed deletion. Tokens revoked.',
'severity' => 'medium',
], actor: 'ops@example.com');
$registry->reportToAuthority('BREACH-2026-001', notificationReference: 'AP-2026-9999');
$registry->reportToSubjects('BREACH-2026-001', channel: 'email');
$registry->contain('BREACH-2026-001');
$registry->resolve('BREACH-2026-001');
Each call atomically updates the register row and appends a hash-chained event. Updates with identical values are no-ops — no event is written when nothing actually changes.
Find the deadlines that matter
use Ginkelsoft\DataBreachRegistry\Support\BreachDeadlines;
$deadlines = new BreachDeadlines(warningWindowHours: 24);
$overdue = $deadlines->overdue(); // 72 hours passed, authority not notified
$approaching = $deadlines->approaching(); // deadline in the next 24 hours
CLI
php artisan retention:breach:register BREACH-2026-001 \
--description="Misdirected export" \
--severity=high \
--discovered="2026-05-27 09:15" \
--subjects=42 \
--categories="name,email"
php artisan retention:breach:list
php artisan retention:breach:list --status=open
php artisan retention:breach:show BREACH-2026-001
php artisan retention:breach:deadlines
php artisan retention:breach:deadlines --warning=48
retention:breach:deadlines exits with a non-zero code when there are
overdue breaches — perfect for a scheduled job that pages someone when a
72-hour clock is about to expire. The command names keep the
retention:breach: prefix for BC with the v1.x monolithic package.
Verify the event log
use Ginkelsoft\ComplianceCore\Config\LogSecret;
use Ginkelsoft\ComplianceCore\Support\HashChain;
use Illuminate\Support\Facades\DB;
$entries = DB::table('breach_event_log')->orderBy('id')->get()
->map(fn ($row) => (array) $row)->all();
$intact = HashChain::verify($entries, LogSecret::value());
Or run php artisan compliance:verify from the
hub to
verify every chain in the family in one shot.
What the log holds
The event log holds only metadata: action names, field diffs, optionally an actor. It never holds personal data — that data lives in the source systems the breach concerns, not in the register.
Compliance notes
- GDPR art. 33 — Notification of a personal-data breach to the supervisory authority within 72 hours of becoming aware of it.
- GDPR art. 34 — Communication of a personal-data breach to the data subject when the breach is likely to result in a high risk.
- GDPR art. 5(2) — Accountability. The event log is the evidence.
This package is not legal advice. Whether a breach requires subject notification (art. 34: "high risk") is your DPIA call, not the package's.
Installation
composer require ginkelsoft/laravel-data-breach-registry
php artisan vendor:publish --tag=compliance-config
php artisan vendor:publish --tag=breach-config
php artisan vendor:publish --tag=breach-migrations
php artisan migrate
Then add a secret to .env (shared with the rest of the family):
COMPLIANCE_LOG_SECRET="$(openssl rand -base64 32)"
Gotchas
- No notification is automatic. This module records that a breach
happened and that you notified — it does NOT actually send the email to
the AP or to subjects. The notification itself is a business process you
own. Use
reportToAuthority/reportToSubjectsto mark the moment you completed it. - Severity is your judgement. The package accepts
low,medium,high,criticalas enum-like values, but does not assess them for you. Whether a breach requires subject notification (art. 34: "high risk") is your DPIA call. - The register is the canonical record, the event log is the proof.
Direct Eloquent
update()onBreachRegisterEntryis allowed by Laravel but skips the event log; always go throughBreachRegistryso the audit trail stays complete.
Testing
composer install
vendor/bin/pest
vendor/bin/phpstan analyse --memory-limit=1G
vendor/bin/pint --test
Reporting bugs
Found a bug or unexpected behaviour? We want to hear about it.
Preferred — open a GitHub issue: https://github.com/ginkelsoft-development/laravel-data-breach-registry/issues/new
When opening an issue, please include:
- Versions — PHP, Laravel, and the package version
(
composer show ginkelsoft/laravel-data-breach-registry). - What you did — the artisan command, code snippet, or steps that triggered the bug.
- What you expected vs what actually happened — include full error output or a stack trace if there is one.
- A minimal reproduction if you can — a failing test or a small code sample beats a long description.
Security-sensitive findings (anything that could expose personal data, break a hash-chain, or bypass an audit log) — please do not open a public issue. E-mail info@ginkelsoft.com directly with "SECURITY" in the subject line and we will respond privately.
Not on GitHub? You can also e-mail info@ginkelsoft.com with the same information.
Contact
For commercial support, integration questions, or anything that doesn't fit a GitHub issue: info@ginkelsoft.com — https://ginkelsoft.com.
License
MIT License — see LICENSE. (c) 2026 Ginkelsoft