Download the PHP package hexis/audit-bundle without Composer
On this page you can find all versions of the php package hexis/audit-bundle. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download hexis/audit-bundle
More information about hexis/audit-bundle
Files in hexis/audit-bundle
Package audit-bundle
Short Description Symfony bundle that audits security events (login, logout, switch_user, failed-login) and Doctrine writes (insert/update/delete) with optional pre-image snapshots, storing to Elasticsearch or Doctrine with primary/fallback semantics.
License MIT
Homepage https://github.com/hexis-hr/audit-bundle
Informations about the package audit-bundle
hexis/audit-bundle
A Symfony bundle that captures security events (login, logout, failed-login, impersonation) and Doctrine writes (insert/update/delete) into pluggable storage — Elasticsearch primary with Doctrine fallback, or Doctrine-only. Supports optional pre-image snapshots per entity so versioning / time-travel can be built on top.
Built for when "who changed what, when" is a compliance question.
What it does
- Captures
LoginSuccessEvent,LoginFailureEvent,LogoutEvent,SwitchUserEvent(enter + exit). - Captures Doctrine writes on entities marked
#[Auditable].onFlushcollects,postFlushwrites — so audit failures never roll back host data. - Snapshot modes per-entity:
none(diff-only record),changed_fields(old+new values on changed fields),full(pre-image + post-image + diff, for versioning). - Storage: Elasticsearch primary + Doctrine fallback (or Doctrine-only).
ChainedAuditWriterauto-falls-back on primary exception;audit:drain-fallbackreplays pending rows. - Non-blocking: exceptions during capture or flush are swallowed and written to
error_log. Audit never poisons the request path. - Session-id workflow correlation: every event carries a SHA-256 hash of the session id — never the raw value — so sessions are correlatable for workflow visualization without creating a hijack vector.
- Legacy migration for apps with a hand-rolled audit table:
audit:migrate-legacywith a pluggable row mapper.
Requirements
- PHP 8.2+
- Symfony 6.4, 7.x or 8.x (tested on 6.4, 7.4 and 8.1)
- Doctrine ORM 3 / DBAL 3
- DoctrineMigrationsBundle 3 or 4
symfony/security-http
Optional:
elastic/elasticsearch: ^8.0— required when using Elasticsearch storagesymfony/messenger— enables async flush instead ofkernel.terminate
Install
1. Require the package
In a monorepo with a path repository:
2. Register the bundle
3. Configure the bundle
4. Run migrations
One table is created: hexis_audit_log (vendor-namespaced to avoid collisions with host-owned audit_log). actor_id and impersonator_id hold the user identifier as returned by getUserIdentifier(), up to 255 characters (64 before Version20261022000001). Name is configurable via audit.storage.doctrine.table (full override) or audit.storage.doctrine.table_prefix. Indexes cover (occurred_at), (actor_id, occurred_at), (target_class, target_id, occurred_at), (event_type, occurred_at), (session_id_hash, occurred_at), and (source_of_truth, pending_replay_at).
Usage
Capture a custom event from app code
Opt an entity into automatic capture
Modes:
| Mode | Written | Use case |
|---|---|---|
none |
Only the fact an entity was mutated + its class/id | Low-cost activity log |
changed_fields |
{field: {old, new}} diff over the changed columns, {field: {added, removed}} for many-to-many collections |
Change-tracking UI |
full |
pre_image + post_image + diff via Serializer group audit:snapshot |
Versioning / time-travel |
ignoreFields excludes named fields from both diff and snapshot — good for updatedAt columns that would otherwise show up in every row. It applies to collection fields too.
Many-to-many: a change to the owning side of a ManyToMany collection is an ENTITY_UPDATE of the owner, also when no column changed. In changed_fields and full mode its diff entry lists the elements added and removed, as {@ref, id} references:
Replacing the whole collection records only the net difference between the old and the new elements. When Doctrine no longer holds the old elements (after clear(), or when a collection that was never loaded is replaced), they are read from the join table with one query. In full mode the field's pre_image is its post_image list with the added elements taken out and the removed ones appended; the original order is not kept. Inverse sides and OneToMany collections are not recorded on their owner, because Doctrine persists those changes through the other entity.
References: a related entity, in a diff (to-one values and collection elements) or in an image, is recorded as {@ref, id} read from its Doctrine mapping, so it needs no getId() method. @ref is the mapped entity class (a lazy proxy is reported as the entity class). id is the value of a single identifier field, or {field: value, ...} for a composite identifier:
An identifier that is itself an entity (derived identity) is recorded as that entity's identifier, other identifier objects such as a Ulid as strings. A reference to an entity inserted in the same flush carries the id the database gives it: the listener fills it in after the insert, so two new elements stay distinguishable. Objects that are not mapped entities (embeddables, mapped superclasses, value objects) are recorded as {@ref, id} from their getId() when they have one, else as a bare {@ref}; a getId() that throws or returns something without a string form also gives a bare {@ref} instead of failing the flush.
For vendor entities you can't annotate, configure via YAML:
Soft deletes: when Gedmo SoftDeleteable turns a remove() into setting deletedAt, the row is an ENTITY_DELETE with action soft_delete (DoctrineAuditListener::ACTION_SOFT_DELETE), whatever order the two Doctrine listeners run in. In full mode its pre_image is the entity before the delete, deletedAt still empty. Removing an already soft-deleted entity again (Gedmo hardDelete) is a plain ENTITY_DELETE, and setting deletedAt back to null is an ENTITY_UPDATE. The bundle does not depend on Gedmo: without it the preSoftDelete listener simply never fires.
Elasticsearch storage
The bundle never hard-codes a specific ES client. You provide an adapter service implementing Hexis\AuditBundle\Storage\Elasticsearch\ElasticsearchClient with three methods: index(), bulk(), putIndexTemplate(). The host chooses how to wire it — typically a thin adapter over elastic/elasticsearch:^8.0.
Configure it:
Then install the index template once (safe to re-run):
The shipped template maps actor, target, request, session_id_hash, occurred_at, etc. with appropriate types; snapshot.pre_image / snapshot.post_image / context are object fields with enabled: false so arbitrary entity shapes don't explode ES's field count.
Fallback & drain
When fallback: doctrine is set, a primary write exception falls through to the Doctrine writer with source_of_truth = 'fallback' and pending_replay_at = occurred_at. Run periodically:
Replayed rows are marked source_of_truth = 'replayed' and pending_replay_at = NULL. Idempotent: the event_id (ULID) is the document id in ES, so replays upsert rather than duplicate.
Commands
| Command | Purpose |
|---|---|
audit:install-elasticsearch-template |
Install/update the shipped ES index template. Only registered when ES is configured. |
audit:drain-fallback [--limit=N] [--dry-run] |
Replay rows from the Doctrine fallback into the primary writer. |
audit:migrate-legacy [--source-table=…] [--source-connection=…] [--batch=N] [--limit=N] [--restart] [--dry-run] |
Copy rows from a host-owned legacy audit table into the bundle's storage. Resumable via a progress file. |
audit:prune [--older-than-days=N] [--dry-run] |
Delete Doctrine rows older than the retention window. Does not touch Elasticsearch — use ILM there. |
Multi-EM / cross-tenant setups
If the host's default connection is tenant-scoped but audit data should be cross-tenant, point the bundle at a global connection + EM:
Run the bundle's migration on that EM:
The Doctrine listener attaches per-EM via doctrine.event_listener tags. If you want only specific EMs audited, split the listener wiring via a compiler pass.
Safety properties
- No audit recursion. The bundle's
AuditLogentity is hard-coded inAuditableRegistry::BUNDLE_CLASSESas never-audited, andDoctrineAuditWriteruses direct DBAL (not ORM) so audit writes never re-enter the UoW the listener is attached to. - No host transaction coupling. Capture happens in
onFlush, actual writes happen inpostFlushafter the host transaction has committed. An audit write failure cannot roll back host data. - No credential leakage on failed login.
SecurityAuditSubscribernever reads the passport or raw token; it records only the attempted identifier (username/email) and the exception class. The password is never touched. - No lazy-load cascade during capture.
AuditSnapshotNormalizerstops at relations and emits{@ref, id}tuples instead of dereferencing, so full-mode snapshots don't trigger N+1 queries. - Session id is hashed. Raw session id never leaves
ContextCollector; only the SHA-256 hash is stored, preserving correlation without the hijack vector if the audit store is compromised.
Known limitations (v0.1)
- Soft deletes other than Gedmo's are not detected. Setting a
deletedAtfield by hand is an ordinaryENTITY_UPDATE. - No built-in UI. Query the
hexis_audit_logtable directly or via your ES tooling of choice. A search/read UI is on the follow-up roadmap. - No ILM. Elasticsearch retention must be handled on the cluster side.
Development
Tests cover the writers (Doctrine + ES mock), the BufferedAuditWriter, ChainedAuditWriter, SecurityAuditSubscriber (each event type + credential-leak assertion), DoctrineAuditListener (insert/update/delete + many-to-many diffs + Gedmo soft deletes in both listener orders + full-mode pre/post images + recursion guard + ignore_fields), the AuditLog mapping against the bundle migrations (MySQL, PostgreSQL, SQLite), AuditSnapshotNormalizer (cycle-safe, relation caps), DefaultLegacyRowMapper, and DrainFallbackCommand.
License
MIT.
All versions of audit-bundle with dependencies
doctrine/dbal Version ^3 || ^4
doctrine/doctrine-bundle Version ^2.8 || ^3.0
doctrine/doctrine-migrations-bundle Version ^3.0 || ^4.0
doctrine/orm Version ^2.14 || ^3.5 || ^4
symfony/config Version ^6.4 || ^7.0 || ^8.0
symfony/console Version ^6.4 || ^7.0 || ^8.0
symfony/dependency-injection Version ^6.4 || ^7.0 || ^8.0
symfony/event-dispatcher Version ^6.4 || ^7.0 || ^8.0
symfony/framework-bundle Version ^6.4 || ^7.0 || ^8.0
symfony/http-kernel Version ^6.4 || ^7.0 || ^8.0
symfony/property-access Version ^6.4 || ^7.0 || ^8.0
symfony/security-http Version ^6.4 || ^7.0 || ^8.0
symfony/serializer Version ^6.4 || ^7.0 || ^8.0
symfony/uid Version ^6.4 || ^7.0 || ^8.0