ginkelsoft / laravel-data-breach-registry
A Laravel package that implements the GDPR art. 33/34 personal-data breach register with a hash-chained event log, 72-hour deadline helpers, and CLI for daily monitoring.
Package info
github.com/ginkelsoft-development/laravel-data-breach-registry
pkg:composer/ginkelsoft/laravel-data-breach-registry
Requires
- php: ^8.2
- ginkelsoft/laravel-compliance-core: dev-development
- illuminate/console: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/database: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0 || ^13.0
- nesbot/carbon: ^2.62 || ^3.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0 || ^11.0
- pestphp/pest: ^2.30 || ^3.0
- phpstan/phpstan: ^1.10 || ^2.0
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.0
This package is auto-updated.
Last update: 2026-05-28 17:16:22 UTC
README
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