feat(masker): mask JSON held in string values via JSON keys - #4
Merged
Conversation
Some logs carry a serialized payload as a string (typically the raw body of a POST request). Until now such a value was an opaque leaf: secrets nested in the JSON leaked through. Declare those keys with MaskerBuilder::withJsonKeys(); the engine decodes the value, masks it recursively (same key + value rules), then re-encodes it. The feature is opt-in — without JSON keys nothing changes. A decodable JSON value takes precedence over a sensitive-key match; a non-decodable one falls back to the normal rules. Held to the project gates: 100% coverage, MSI 100%, property tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rflavien
force-pushed
the
feat/json-key-masking
branch
from
June 10, 2026 15:23
6f966da to
99d0410
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in feature to mask secrets inside JSON payloads that are logged as string values (e.g. raw HTTP request bodies), by declaring “JSON keys” via MaskerBuilder::withJsonKeys([...]). This extends the existing recursive masking engine so it can decode, mask, and re-encode JSON stored in strings while preserving the current default behavior when the feature is not enabled.
Changes:
- Add
MaskerBuilder::withJsonKeys()and wire a JSON-key matcher through toMasker. - Extend
Maskerto decode/mask/re-encode JSON for configured keys. - Add unit + property-based tests and README documentation for the new behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/Masker/Masker.php |
Adds JSON-string decoding/masking/re-encoding path gated by an optional JSON key matcher. |
src/MaskerBuilder.php |
Adds withJsonKeys() builder API and passes a KeyListMatcher for JSON keys into Masker. |
tests/Masker/MaskerTest.php |
Adds unit tests covering JSON-in-string masking behavior, precedence, maxDepth behavior, and idempotence. |
tests/MaskerBuilderTest.php |
Adds builder-level tests ensuring withJsonKeys() works, accumulates, and remains opt-in. |
tests/Property/JsonMaskerPropertyTest.php |
Adds property-based tests for JSON re-decodability, idempotence, and non-mutation. |
README.md |
Documents the new withJsonKeys() feature and its behavior/constraints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
76
to
+85
| foreach ($data as $key => $value) { | ||
| if (null !== $this->jsonKeyMatcher && \is_string($value) && $this->jsonKeyMatcher->matches($key)) { | ||
| $decoded = $this->decodeJsonArray($value); | ||
| if (null !== $decoded) { | ||
| $masked[$key] = $this->encodeJson($this->processArray($decoded, $depth + 1, $seen)); | ||
|
|
||
| continue; | ||
| } | ||
| // Not decodable JSON: fall through to the normal rules below. | ||
| } |
Comment on lines
+122
to
+123
| - Re-encoding normalises formatting (whitespace, escaping; an empty `{}` comes | ||
| back as `[]`) — the masked payload is semantically equal, not byte-identical. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contexte
Certains logs poussent dans
context/extraune clé dont la valeur est une string contenant du JSON — typiquement le body brut d'une requête POST. Jusqu'ici cette valeur était une feuille opaque : seul leValueMatcher(regex) s'appliquait sur la string entière, donc unpasswordenfoui dans le JSON fuyait dans les logs.Solution
Nouvelle notion de « clés JSON », opt-in, configurée via
MaskerBuilder::withJsonKeys([...]). Pour ces clés, si la valeur est une string décodable, le moteur la décode, applique le masquage récursif habituel (clés sensibles + valeurs sensibles), puis ré-encode :Comportement
withJsonKeys, comportement inchangé (la string reste une feuille opaque).{}vide ressort en[]) — payload sémantiquement équivalent, pas byte-identique.Détails d'implémentation
Masker: paramètre constructeur optionnel?KeyMatcherInterface $jsonKeyMatcher(réutiliseKeyListMatcher), branche en tête deprocess(), helpersdecodeJsonArray()/encodeJson(). La structure décodée est traitée àdepth+1→ la bornemaxDepthcontinue de s'appliquer à travers la frontière JSON.MaskerBuilder: méthode fluide accumulativewithJsonKeys().Tests
MaskerTest,MaskerBuilderTest) : objet/list JSON, valeur détectée par pattern, imbrication + truncationmaxDepth, fallback (JSON invalide / scalaire / valeur non-string), JSON gagne sur sensible, ré-encodage slashes/unicode non échappés, idempotence, non-régression sans clés JSON.tests/Property/JsonMaskerPropertyTest.php(sortie re-décodable, aucun secret ne survit dans le payload, idempotence, non-mutation).Validation
make validate✅ — cs-check, phpstan (max), coverage 100% (110/110 lignes), infection MSI 100%, proofs (18 tests, 2738 assertions).🤖 Generated with Claude Code