Feat/audit mdc routing - #6349
Conversation
PR Reviewer Guide 🔍(Review updated until commit 26cbb0f)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 26cbb0f Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit c9e6e8d
Suggestions up to commit be4a3b4
|
DarshitChanpura
left a comment
There was a problem hiding this comment.
Nice, focused feature — the finally-based cleanup, removing only the sink's own four keys (not clearMap()), and skipping work when the sink is disabled are all the right calls, and pre-compiling the Pattern is good.
Two test-quality issues below are worth fixing before merge — I pulled the branch and empirically confirmed both (details in the inline comments). There's also a security note on routing keys, plus some optional polish. Overall a useful addition.
| msg.addEffectiveUser("user-thread-1"); | ||
| sink.doStore(msg); | ||
| // Our MDC keys must be cleared on this thread after | ||
| assertThat(ThreadContext.get("audit_category"), is((String) null)); |
There was a problem hiding this comment.
Swallowed in-thread assertions (verified empirically). These assertThat(...) calls run on the spawned worker thread, so an AssertionError here never propagates to the JUnit thread and can't fail the test.
I confirmed this on the branch: inserting a guaranteed-false assertion into this worker-thread body still passes the test. So the in-thread cleanup checks (here and in thread2) are effectively dead.
The post-join() value assertions below do run, so value-bleed between events is covered — it's only the per-worker-thread cleanup that goes unverified. Suggestion: capture failures into a shared AtomicReference<Throwable> (or synchronized list) inside each thread and assert it's empty after join().
| msg.addEffectiveUser("exception-test-user"); | ||
| msg.addIndices(new String[] { "index-a", "index-b", "index-c" }); | ||
|
|
||
| sink.doStore(msg); |
There was a problem hiding this comment.
This test never actually throws (verified empirically). The name is testMdcCleanedUpEvenWhenLoggingThrows, but nothing on this path throws — it's a normal multi-index store. I confirmed on the branch by asserting that doStore throws: that assertion fails, so the finally exception path (the whole point of the test) is never exercised.
Suggestion: inject a failing appender/layout so auditLogger.log(...) actually throws and then assert the MDC keys are still removed, or rename to something like testMdcCleanedUpAfterMultiChunkStore to match what it verifies.
| // Values are sanitized for safe use in filenames (colons in action names etc.) | ||
| ThreadContext.put("audit_category", sanitizeForMdc(msg.getCategory() != null ? msg.getCategory().name() : "UNKNOWN")); | ||
| ThreadContext.put("audit_action", sanitizeForMdc(msg.getPrivilege())); | ||
| ThreadContext.put("audit_user", sanitizeForMdc(msg.getEffectiveUser())); |
There was a problem hiding this comment.
Security note — routing key safety. audit_user is unbounded and attacker-influenceable, so using it as a RoutingAppender key ($${ctx:audit_user}) lets a caller mint arbitrarily many distinct filenames -> inode/disk exhaustion. The example in the PR description safely routes on audit_category (a bounded enum); worth calling out explicitly in the docs/PR that audit_user should not be used as an unbounded routing key. (audit_action/privilege and audit_request_type are effectively bounded — action strings and request class names — so those are lower risk.)
| // Push audit attributes into Log4j MDC so operators can use | ||
| // RoutingAppender with $${ctx:audit_category} etc. to split logs. | ||
| // Values are sanitized for safe use in filenames (colons in action names etc.) | ||
| ThreadContext.put("audit_category", sanitizeForMdc(msg.getCategory() != null ? msg.getCategory().name() : "UNKNOWN")); |
There was a problem hiding this comment.
Fallback casing is inconsistent. audit_category falls back to "UNKNOWN" (uppercase) here, while the other three fall back to "unknown" (lowercase, via sanitizeForMdc). An operator keying a route on the "missing" value has to remember both spellings. Passing a null category name straight through sanitizeForMdc (which already returns "unknown") would unify it.
Minor, related: the four key names are string literals duplicated across the put here and the remove calls in the finally. Extracting private static final String constants removes any risk of put/remove drift and documents the owned key set in one place.
| @@ -44,8 +50,36 @@ public boolean isHandlingBackpressure() { | |||
|
|
|||
| public boolean doStore(final AuditMessage msg) { | |||
There was a problem hiding this comment.
Optional — unconditional per-event cost. This runs 4x put + 4x regex + 4x remove on every audit event regardless of whether any operator has configured a RoutingAppender. On high-volume clusters that's overhead nobody using the default config benefits from. Consider gating enrichment behind a setting (e.g. ...log4j.enable_mdc_routing, default off) so those clusters pay nothing.
| if (value == null || value.isEmpty()) { | ||
| return "unknown"; | ||
| } | ||
| return UNSAFE_FILENAME_CHARS.matcher(value).replaceAll("_"); |
There was a problem hiding this comment.
Optional — control chars not sanitized. The regex strips filename-dangerous chars but not control characters (\n, \r). Since audit_user can be attacker-influenced, a value containing newlines flows through unchanged; if an operator surfaces %X{audit_user} in a pattern layout this becomes a log-forging vector. Low severity for a routing key, but consider stripping control characters too.
be4a3b4 to
c9e6e8d
Compare
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit 26cbb0f. ⛔ Hard block: Issues at High severity or above will block this PR from merging.
The table above displays the top 10 most important findings. Pull Requests Author(s): Please update your Pull Request according to the report above. Repository Maintainer(s): You can Thanks. |
|
Persistent review updated to latest commit c9e6e8d |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6349 +/- ##
==========================================
- Coverage 75.45% 75.39% -0.06%
==========================================
Files 456 456
Lines 29924 29941 +17
Branches 4530 4533 +3
==========================================
- Hits 22578 22575 -3
- Misses 5251 5269 +18
- Partials 2095 2097 +2
🚀 New features to boost your workflow:
|
Add Log4j MDC enrichment to Log4JSink so operators can use RoutingAppender to split audit logs by category, user, action, or request type. - Gate MDC behind enable_mdc_routing setting (default off) to avoid per-event overhead on clusters not using RoutingAppender - Sanitize MDC values for filename safety and log-forging prevention (colons, slashes, brackets, control characters replaced with underscores) - Extract MDC key names as package-private constants for put/remove consistency - Use targeted ThreadContext.remove() instead of clearMap() to preserve upstream MDC entries - Unified fallback casing: null/empty values always become "unknown" Signed-off-by: Muzzamil Jolaade <muzzajol@amazon.com>
c9e6e8d to
26cbb0f
Compare
|
Persistent review updated to latest commit 26cbb0f |
Description
Enrich Log4j MDC (Mapped Diagnostic Context) with audit event attributes before each log call in
Log4JSink, enabling operators to use Log4j's nativeRoutingAppenderto split audit logs by category, user, action, or request type into separate files.Four MDC keys added:
audit_category: event type (e.g.,GRANTED_PRIVILEGES,FAILED_LOGIN,COMPLIANCE_DOC_WRITE)audit_action: the privilege/action being audited (e.g.,indices_data_write_index)audit_user: the effective user for the requestaudit_request_type: the transport request class (e.g.,IndexRequest,BulkShardRequest)Why
Operators running high-volume clusters need to route different audit event types to different destinations — security-critical events (
FAILED_LOGIN,MISSING_PRIVILEGES) to a SIEM, compliance events to long-term storage, and routineGRANTED_PRIVILEGESto a rotated local file. Today, all events go to the same log file, requiring post-processing to separate them.With MDC enrichment, a standard Log4j
RoutingAppenderconfig handles this natively:How it works
Log4JSink.doStore()sets 4 MDC keys before logging, removes them after (in afinallyblock)_)WARNING: audit_user should NOT be used as a RoutingAppender routing key for file paths — it is unbounded/attacker-influenceable and could cause inode/disk exhaustion. Use audit_category (bounded enum) instead.
Testing
Unit tests (
Log4JSinkMdcTest) — 12 tests covering:"unknown"doStore()completes (notclearMap())Check List
--signoffBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.