iq2i / vigie-bundle
A structured, queryable activity log for Symfony, written as ECS NDJSON with zero database — and, opt-in, a detection/response loop with a SIEM
Requires
- php: >=8.3
- monolog/monolog: ^3.0
- psr/cache: ^2.0|^3.0
- psr/event-dispatcher: ^1.0
- psr/log: ^2.0|^3.0
- symfony/clock: ^6.4|^7.4|^8.0
- symfony/config: ^6.4|^7.4|^8.0
- symfony/dependency-injection: ^6.4|^7.4|^8.0
- symfony/event-dispatcher: ^6.4|^7.4|^8.0
- symfony/http-foundation: ^6.4|^7.4|^8.0
- symfony/http-kernel: ^6.4|^7.4|^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.52
- masterminds/html5: ^2.7.5
- phpstan/phpstan: ^2.2.2
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^10.5
- symfony/browser-kit: ^6.4|^7.4|^8.0
- symfony/cache: ^6.4|^7.4|^8.0
- symfony/console: ^6.4|^7.4|^8.0
- symfony/framework-bundle: ^6.4|^7.4|^8.0
- symfony/http-client: ^6.4|^7.4|^8.0
- symfony/lock: ^6.4|^7.4|^8.0
- symfony/phpunit-bridge: ^7.4|^8.0
- symfony/routing: ^6.4|^7.4|^8.0
- symfony/security-bundle: ^6.4|^7.4|^8.0
- symfony/twig-bundle: ^6.4|^7.4|^8.0
- symfony/uid: ^6.4|^7.4|^8.0
- symfony/web-profiler-bundle: ^6.4|^7.4|^8.0
- symfony/yaml: ^6.4|^7.4|^8.0
- twig/twig: ^3.4
Suggests
- phpunit/phpunit: To use the assertions shipped in IQ2i\VigieBundle\Test (see doc/testing.md)
- symfony/console: To use the vigie:* console commands
- symfony/http-client: To poll a SIEM's decisions with the built-in CrowdSec provider (see doc/threat.md)
- symfony/lock: To prevent overlapping vigie:threat:sync runs on the same host (see doc/threat.md)
- symfony/monolog-bundle: To configure a custom Monolog handler for iq2i_vigie.output.handlers (see doc/storage.md)
- symfony/routing: To redirect to a route from iq2i_vigie.threat.enforce.remediations, or to expose the threat ingest endpoint by importing @IQ2iVigieBundle/config/routes.php (see doc/threat.md)
- symfony/security-csrf: To record CSRF token failures outside the login form (see doc/recording.md)
- symfony/security-http: To track login, logout and switch user events
- symfony/uid: To generate RFC 4122 request ids for correlation instead of falling back to random bytes
- symfony/web-profiler-bundle: To show the Vigie panel in the debug toolbar
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-17 09:48:56 UTC
README
Know who did what, when and from where in your Symfony application: a structured, queryable activity log,
written as ECS (Elastic Common Schema) NDJSON, with zero database. Install it on its own to get an
activity log an admin backend or a support team can grep, jq, or ship to Elastic. The application never
queries it back itself.
And if you want to act on it: plug a SIEM behind the same stream and it becomes a detection/response loop, reading decisions back into the application. See Two ways to use Vigie.
- Records HTTP requests (opt-in), security events (login, logout, switch user) and your own business
events, as a stream of
Activityobjects, immediately written out. See doc/recording.md. - No database: activities are written through Monolog to a plain NDJSON file (the default), a stdout stream for containers, or syslog for a load-balanced fleet. See doc/multi-server.md.
- Anonymizes IPs (
record.ip_address: anonymize) and always HMACs session ids by default; user identifiers are recorded in the clear by default, andrecord.user_identifier: hashreplaces them with a stable, non-reversible pseudonym (user.hash) instead. Privacy-preserving defaults, not a compliance claim: Vigie does not make an application GDPR-compliant on its own. See doc/configuration.md. - Optionally reads back the decisions a SIEM makes about suspicious IPs, ranges, sessions, users,
countries and AS numbers, through
ThreatCheckerInterface, an opt-in enforcement listener, and a signed push endpoint for a SIEM that can't be polled. Nothing here is on unless you turn it on; see doc/threat.md. - Ships no HTML dashboard, no read API, and no entity-change auditing. The ECS output is the interface; see doc/siem.md for consuming it, or damienharper/auditor-bundle for entity diffs.
- Requires PHP 8.3+ and Symfony 6.4/7.4/8.x.
Quickstart
composer require iq2i/vigie-bundle
// config/bundles.php return [ // ... IQ2i\VigieBundle\IQ2iVigieBundle::class => ['all' => true], ];
Activities start flowing to %kernel.logs_dir%/vigie.jsonl as ECS documents. See
doc/storage.md to point it at stdout or a custom Monolog handler instead.
Security events (login, logout, switch user) are recorded automatically. Opt an HTTP controller in with
#[Track]:
use IQ2i\VigieBundle\Attribute as Vigie; #[Vigie\Track] // opts every action of this controller into recording class AdminDashboardController { // ... }
Record a business event in one line:
$this->recorder->custom('export.completed', ['rows' => 42]); // ActivityRecorderInterface
See doc/recording.md for processors, the Subject, and vetoing a recording.
Two ways to use Vigie
(a) Activity log only. Install the bundle, opt controllers in with #[Track], record your own
events with custom(), and read vigie.jsonl with jq, tail it, or ship it to Elastic/Wazuh — see
doc/siem.md. This requires nothing from threat.*: no SIEM, no LAPI, no enforcement
listener. A structured activity log is the whole deliverable.
(b) The full loop, with a SIEM. Point CrowdSec (or another SIEM) at the same vigie.jsonl, let a
scenario reason over it, and read decisions back into the application through
ThreatCheckerInterface — see doc/threat.md and crowdsec/. Opt-in,
layered entirely on top of (a): nothing about the activity log changes when this is off.
Documentation
- doc/recording.md: the
Activitymodel, what's recorded automatically, your own activities, processors, recording from a Messenger worker - doc/storage.md: the default Monolog/ECS storage, writing to stdout, syslog or a custom handler, writing your own storage
- doc/siem.md: the ECS field mapping, CrowdSec acquisition, Elastic/Wazuh ingestion
- crowdsec/: a CrowdSec parser and six ready-to-install scenarios reading Vigie's own output directly (login brute force, credential stuffing, access-denied probing, CSRF wave, impersonation abuse, IDOR probing)
- doc/multi-server.md: a load-balanced fleet, correlating a visitor across nodes, telling nodes apart, shipping straight to syslog
- doc/multi-tenant.md: one CrowdSec instance shared across several client
applications, one bouncer key per app, the cross-tenant leak on
username/sessionscopes - doc/threat.md: reading back a SIEM's decisions,
ThreatCheckerInterface, opt-in enforcement, the signed push endpoint,vigie:threat:sync/vigie:threat:list, the CrowdSec provider, writing your own - doc/remediation.md: recipes reacting to a decision, revoking a session, locking an account, disabling it, notifying
- doc/configuration.md: full
iq2i_vigie.*reference, optional dependencies - doc/testing.md: testing your own code against vigie, without a database
- doc/recipes.md: listeners for scheb/2fa, symfony/login-link, symfonycasts/reset-password
Why not the official CrowdSec bouncer?
crowdsecurity/bouncer-lib and its Symfony bundle already
poll or stream the LAPI, cache decisions, and remediate — the same job ThreatCheckerInterface and
ThreatEnforcementSubscriber do here. Three things they don't do, which is what Vigie is actually for:
- The other direction. A bouncer only ever reads decisions back. Vigie is first an emitter: the
ECS stream a scenario reasons over (
login_failurewith the identifier that failed,switch_user,access_denied) doesn't exist without it, whatever reads decisions back. session/usernamescopes. A network bouncer sits in front of the app and only ever sees an IP; it has no way to revoke a session or lock an account by identifier, the scopes CrowdSec itself supports but a reverse-proxy bouncer can't act on. See doc/remediation.md.- Multi-SIEM.
ThreatProviderInterfaceand the signed push endpoint (threat.ingest) mean CrowdSec is one provider among others (Wazuh, a Sentinel playbook, any SOAR), not the only thing this reads decisions from.
If the only need is banning IPs at the edge, the official bouncer or the reverse proxy is the better fit, and cheaper: no PHP request pays for a lookup that already happened upstream. Vigie's enforcement exists for the scopes that layer can't see, and for a deployment with no such layer at all.
Versioning and security
Classes marked @internal and final are implementation details and are not covered by
semver; everything else is.
Please do not open a public GitHub issue for a suspected security vulnerability. Instead, report it privately by emailing loic@sapone.fr with a description, steps to reproduce, and the affected commit. You should get an initial response within a few business days.