Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Redactor Laravel Package

kirschbaum-development/redactor

Redactor is a Laravel package for redacting sensitive data before it reaches logs, APIs, or users. Define rules to mask or remove emails, tokens, and other private values from models, arrays, and output with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its config:

composer require kirschbaum-development/redactor
php artisan vendor:publish --tag=redactor-config

The config file is the whole package. Open it first and look at three things, in this order:

  1. The profiles — default, strict, observability, file_scan. Each is a named bundle of detectors, rules and operator choices. You will almost always work by picking a profile per context rather than by writing new rules.
  2. known_secrets — this registers your own application credentials (including app.key) so they are redacted verbatim wherever they appear. Fill this in before anything else; it is the cheapest win and the one most people forget.
  3. The path rules and key policies — the place where you tell the redactor about your payload shape, not the world's.

Then verify the whole thing resolves:

php artisan redactor:validate

This resolves every profile and fails on the broken ones — a bad regex, a key listed as both safe and blocked, a rule whose samples no longer match. Run it in CI, not just once.

A sensible first use case is the one that costs nothing to try: scan the repository.

php artisan redactor:scan app/ config/ --output=table

That gives you a baseline of what the shipped rules think is sensitive in your codebase, and it is also the fastest way to see the profiles in action before wiring anything into the request lifecycle. Once the scan looks sane, move to redacting a real payload:

$result = Redactor::inspect($payload, 'default');   // a RedactionResult: findings, paths, operators applied
$safe   = Redactor::redactSafely($payload, 'default'); // never throws

redactSafely() is the method you want in production paths — telemetry, error reporters, log processors — where a redaction failure must not become an outage. Use redact() only where you genuinely want an exception on a bad profile.

Implementation Patterns

Choose the profile at the boundary, not in the middle. The package is designed so that detection is separate from what happens to the value. That means the same payload can be treated differently depending on where it is going:

Context Profile Why
General internal use default Balanced rules, entropy on, sensible operators
Anything leaving the trust boundary (exports, support dumps, third-party APIs) strict Maximum coverage, aggressive operators
Logs, traces, metrics — where you still want to join records observability Pseudonymises instead of destroying values
Repository and file content file_scan Provider credentials, PEM blocks, decoded layers

Prefer path rules over key guessing for structured payloads. If you control the shape of the data, naming the location is both cheaper and more precise than letting key matching and entropy run over everything:

'path_rules' => [
    'request.headers.authorization' => 'redact',
    'users[*].token'                => 'redact',
    'payload.**.password'           => 'hash',
],

Path rules are checked first and, when one matches, instead of everything else — no key matching, no pattern scanning, no walk below the node. * matches one level, ** any depth, and users[*].token handles lists. Specificity wins, so declaration order never matters and you can append without thinking.

Pick operators per entity, not per pattern. This is the key separation the package makes and it changes how you think about the config. A pattern says "this looks like an email"; the operator says "and here is what to do about it":

  • redact / remove — gone, unrecoverable. Default for anything you will never need again.
  • mask / partial — keeps a recognisable fragment (last four digits, domain only). Good for support workflows where an agent needs to confirm which card.
  • hash / surrogate — deterministic stand-ins. Same email in, same pseudonym out, so counts, joins and traces survive. Use surrogate when a downstream consumer needs a plausibly-shaped value (a valid email, a Luhn-valid card with its BIN intact); use hash when you only need equality.
  • nullify — replaces a value with null so a typed field keeps its type and its key. Inside a string, it deletes the span instead.
  • preserve — explicitly do nothing. Useful for carving an exception out of a broad profile.
  • tokenize — a reversible, model-friendly token (tok_email_k4m9rp2xzq) with the original encrypted in the cache for a TTL, exchanged back with Redactor::detokenize().

Wire it in as middleware for responses. The redact middleware redacts on the way out — JSON as data, text as text, files untouched — and importantly never writes _redacted markers into a payload, so the client sees a structurally valid document:

Route::middleware('redact:strict')->group(function () {
    Route::get('/export', ExportController::class);
});

It applies streaming redaction automatically to streamed responses, and it fails closed to a 500. That last property is the one to internalise: a broken profile in production turns every response through this middleware into a server error rather than a leak. Run redactor:validate in your deploy pipeline.

Use Redactor::fake() in tests, not in application code. The fake still redacts, but records every call, so you can assert the negative — that a secret never left — rather than hoping it didn't:

Redactor::fake();

$this->post('/support/dump', $payload)->assertOk();

Redactor::assertNeverEmitted('sk_live_...');
Redactor::assertProfileUsed('strict');
Redactor::assertFinding('email');

This is the single highest-leverage integration for a team, because redaction bugs are silent by construction. A test that proves the absence is worth a dozen that prove the presence.

Listen to RedactionPerformed for observability of the redactor itself. The event carries the profile, the keys touched and counts per rule and entity — never a value. A throwing listener cannot break redaction, so it is safe to attach metrics or alerting:

Event::listen(RedactionPerformed::class, fn ($e) => Metrics::increment('redactions', $e->counts));

Sudden spikes in a particular rule are how you find out that a new log line is dumping something it shouldn't.

Put the scanner in CI with a baseline and SARIF. The day-to-day value of redactor:scan is not the first run — it is the second one. Establish a baseline, then fail only on new findings:

php artisan redactor:scan --baseline=redactor-baseline.json --output=sarif > results.sarif

--update-baseline refreshes it. Use --staged, --diff=<ref> or --history[=<range>] to scan only the lines a change adds, on their real line numbers; in history mode a secret removed by a later commit is still found. When a fixture or a documented example is genuinely fine, redactor:allow on the line suppresses it in place — better than an unexplained entry in a JSON baseline, because the reason lives next to the code.

For AI and MCP surfaces, use the adapters rather than hand-rolling. Ai\RedactPrompt redacts a prompt before it reaches the provider and resolves tokens in the answer; Mcp\RedactsResponses covers tool results, structured content, resource reads, prompt messages, streamed output and errors over any transport. Both SDKs are suggested, not required, so you can install the adapter without pulling in a provider you don't use.

Extend at the seams, not by forking. Three contracts matter: register an operator with Redactor::registerOperator(), a detector via the Detector contract (anything that reports Detections against a string plugs into the same resolution and operator pipeline), a recogniser with Redactor::registerRecognizer(), and a runtime secret with Redactor::registerSecret(). A custom detector is the right answer for a domain-specific identifier — an internal account number format, a tenant-specific ID — that no shipped rule will ever know about.

Gotchas and Tips

Entropy is a weak detector, and the package's own history shows it. The release notes are candid that entropy misses a 20-character AWS key outright and a GitHub token by a tenth of a bit. That is why provider credential rules were pushed into every profile. The lesson for you: do not build a profile that relies on entropy alone for short, structured secrets. If you have a credential format, write a rule for it, and give it keywords and min_length while you're there.

keywords and min_length are not just optimisations. ['@'] keeps the email regex off almost every string in a payload, but a keyword can also be a precision requirement — the shipped ten-digit phone rule only fires next to a phone or tel label, precisely because a bare ten-digit run is a Unix timestamp or an order number far more often than a phone number. If you write a broad pattern, add a keyword and stop it from eating your logs. min_length is a one-integer compare that skips the rule entirely, so declare it everywhere it applies.

Tune with min_confidence, not by weakening patterns. Detections carry a score plus the signals behind it — a passing checksum, a nearby credential keyword. A pattern can therefore be filtered as noise when it fires alone and reported when corroborated. If you find yourself editing a regex to make it "less noisy", you are about to make it miss things. Adjust the profile's min_confidence, or use --min-confidence on a one-off scan to see what you'd get at a different threshold.

Surrogate and hash need a key, and they know it. Deterministic pseudonymisation is one-way (HMAC, not encryption). If no key is available, the operator falls back to plain redaction rather than emitting an unkeyed stand-in — because an unkeyed stand-in would look joinable and silently not be, which is far worse than an obvious gap. If your logs suddenly stop correlating, check the key before you check the rules. This is also why observability is a separate profile: it is configured to pseudonymise, and it needs that key to do its job.

Path rules are a short-circuit — that's the point and the trap. When one matches, nothing below the node is walked. If you add request.body => 'redact', you have disabled entropy scanning, key matching and every pattern rule for the entire body. Usually that is exactly what you want. But if you were relying on a nested detector to catch something the operator doesn't (say, a card number inside a redacted blob you actually wanted partial), it will quietly not happen. Be as specific as the payload allows; use ** deliberately.

Allow-lists run after detection, and a broken entry allows nothing. A profile allowlist of literals and regexes is applied after a finding is reported, so patterns stay as strong as written — good. But an entry that cannot be evaluated allows nothing, which means a typo'd regex fails in the safe direction but silently. redactor:validate and the self-testing samples / counter_samples on rules exist to catch exactly this; keep them up to date when you add rules, because a rule that stops detecting its sample is a rule that stopped working, and nothing else will tell you.

Baselines are tied to a ruleset fingerprint. The digest of the rules a scan ran is recorded in baselines and reported in JSON and SARIF. A baseline made under a different ruleset gets a warning, and reasonably so — you added rules, so old findings that were previously ignored may now be real. When you change the rules, expect to regenerate the baseline and review the delta rather than blindly --update-baseline.

--verify is off by default for good reasons. Credential verification asks providers whether a detected credential is live, which means network calls to third parties. It requires three things simultaneously: config enabling it, the run passing --verify, and the provider being on an explicit allowlist. The command names every host before contacting any, and it is never reachable from the redaction path at all. Keep it that way — verification belongs in a deliberate audit run, not in a request handler.

Streaming has a hold-back window. StreamRedactor keeps back enough of each chunk that a secret split across two chunks is still caught. That is the correct trade, but it means the first bytes are delayed and through() / wrap() / response() are not quite drop-in for a callback that expects to emit immediately. The redact middleware applies it to streamed responses for you; if you are wrapping manually, budget for the latency and don't assume chunk boundaries survive.

Entity recognition is inert until enabled, and degrades on its own. It asks a named entity recogniser (the built-in driver speaks Presidio's /analyze contract; a companion redactor-onnx package runs a token classification model in-process with no sidecar) about prose-shaped values within a length band, converts and verifies every character offset before replacement, and falls back to rules-only on failure. A circuit breaker stops a dead sidecar from being asked on every log line. Enable it for free-text fields — support tickets, notes — not for structured payloads, where path rules and patterns are cheaper and more certain.

Performance is profile-shaped, and the numbers tell you where to look. A 31-leaf request payload through default costs ~68µs; a Monolog record through the processor ~84µs; a 1 MB free-text subject ~24ms with ~1MB peak memory. The design choices that get you there are worth knowing because they constrain how you extend it: resolved profiles are cached and invalidated by comparing raw config, path rules compile once into a trie (so 200 path rules cost about what one does), and entropy analysis is skipped below min_length. Conversely, detection rules are linear in count — a profile that wants the old cost back removes the rules it does not need. Do not bolt twenty regexes onto default and then wonder why the log processor got slow. Put the expensive, narrow rules in a profile that runs where they matter.

tokenize is stateful — plan the TTL and the store. Tokens are stable and model-friendly, but the original is encrypted into the cache with a TTL, and detokenize() leaves unknown tokens alone (which is the right failure mode, but means an expired token silently stops resolving). If cache eviction is a real possibility in your environment, implement Tokenization\TokenStore against something durable rather than accepting the default.

In the scanner, decoding is one layer deep. Base64 tokens, percent-encoded runs and JSON-escaped lines are decoded and scanned, and the finding names the encoding with its excerpt taken from the decoded, redacted text. One layer — a double-encoded secret is not found. scan.decode switches it off entirely if the false-positive rate on your codebase isn't worth it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle