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.

Maintainers

Package info

github.com/ginkelsoft-development/laravel-data-breach-registry

pkg:composer/ginkelsoft/laravel-data-breach-registry

Statistics

Installs: 0

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-05-28 17:13 UTC

This package is auto-updated.

Last update: 2026-05-28 17:16:22 UTC


README

Tests License Laravel PHP PHPStan

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 / reportToSubjects to mark the moment you completed it.
  • Severity is your judgement. The package accepts low, medium, high, critical as 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() on BreachRegisterEntry is allowed by Laravel but skips the event log; always go through BreachRegistry so 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:

  1. Versions — PHP, Laravel, and the package version (composer show ginkelsoft/laravel-data-breach-registry).
  2. What you did — the artisan command, code snippet, or steps that triggered the bug.
  3. What you expected vs what actually happened — include full error output or a stack trace if there is one.
  4. 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.comhttps://ginkelsoft.com.

License

MIT License — see LICENSE. (c) 2026 Ginkelsoft