Skip to content

Add Linux Auditd Audit Rules Removed detection (T1562.001) - #4386

Open
vahidmalekk wants to merge 4 commits into
splunk:developfrom
vahidmalekk:detection/linux-auditd-rules-removed
Open

vahidmalekk wants to merge 4 commits into
splunk:developfrom
vahidmalekk:detection/linux-auditd-rules-removed

Conversation

@vahidmalekk

Copy link
Copy Markdown

Summary

Adds a new anomaly analytic, Linux Auditd Audit Rules Removed, plus a new Linux Auditd Config Change data source, detecting a runtime flush of the audit ruleset (T1562.001 / T1070) via the native CONFIG_CHANGE op=remove_rule events.

Coverage gap

ESCU has auditd daemon/service-stop detections, but an attacker who runs auditctl -D (or an AUDIT_DEL netlink) flushes the ruleset while the daemon keeps running - no DAEMON_END fires, so every existing auditd rule stays silent even though host auditing is now blind. This closes that gap. It complements the companion PR Linux Auditd Audit Configuration File Modification (on-disk config) - this one covers the runtime flush.

How it works / FP handling

The kernel emits a CONFIG_CHANGE record per rule add/removal (no custom audit rule needed). Legitimate reloads - augenrules --load, auditctl -R, service restart, boot - remove then re-add the rules in the same window, so the search fires only when rules_removed >= 5 and rules_added = 0 (a flush, not a reload). Validated live: a real reload removed 1856 / added 2056 in one minute and was correctly suppressed.

New data source

data_sources/linux_auditd_config_change.yml (separator type = CONFIG_CHANGE) - there was no existing CONFIG_CHANGE auditd data source. Happy to adjust its fields/naming to STRT conventions.

Testing

True-positive dataset captured on an authorized lab host: eight op=remove_rule events with no matching add_rule. Paired attack data: splunk/attack_data#1261; tests.attack_data points at it, so the unit test passes once #1261 merges.

MITRE ATT&CK

T1562.001 (Impair Defenses: Disable or Modify Tools), T1070 (Indicator Removal)

The rules_removed>=5 threshold (flush vs single-rule tuning) and the system-context exclusion are documented as tunable in how_to_implement / the filter macro.

Defines the CONFIG_CHANGE auditd data source (audit rule add/remove at
runtime) used by the Linux Auditd Audit Rules Removed detection.
Detects a runtime flush of audit rules (CONFIG_CHANGE op=remove_rule with
no matching add_rule in the window) - auditctl -D / AUDIT_DEL - which
blinds auditing while the daemon keeps running (no DAEMON_END). Legit
reloads re-add rules and are excluded by rules_added=0. T1562.001 / T1070.
@nasbench

nasbench commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T11:19:10.197316Z f351e10 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f351e10757

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

search: |-
`linux_auditd` type=CONFIG_CHANGE op IN ("remove_rule", "add_rule")
| bin _time span=1m
| stats count(eval(op=="remove_rule")) as rules_removed count(eval(op=="add_rule")) as rules_added values(key) as removed_keys min(_time) as firstTime max(_time) as lastTime BY _time host auid ses subj

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve removals when subj is absent

CONFIG_CHANGE records can omit subj on hosts without an active LSM security context, and the new data source does not require that field. Splunk's stats ... BY ... subj drops events with no value for that grouping field, so a qualifying auditctl -D on such a host produces no row and no detection; coalesce the optional context before stats or do not group on it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eb589f1. subj is no longer a grouping field - it's coalesced (coalesce(subj,"unknown")) and carried as a values() for context, so a remove_rule burst on a host without an active LSM context is no longer silently dropped by stats. Aggregation is now BY _time host only.

`linux_auditd` type=CONFIG_CHANGE op IN ("remove_rule", "add_rule")
| bin _time span=1m
| stats count(eval(op=="remove_rule")) as rules_removed count(eval(op=="add_rule")) as rules_added values(key) as removed_keys min(_time) as firstTime max(_time) as lastTime BY _time host auid ses subj
| where rules_removed>=5 AND rules_added=0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require re-additions to correspond to removed rules

If an attacker flushes the rules and then adds one arbitrary harmless rule in the same minute and session, rules_added becomes 1 and this predicate discards the group even if hundreds of removed rules remain absent. Because a legitimate reload is expected to restore the removed rules, treating any single add as a reload gives the attacker targeted by this detection a trivial bypass; compare the re-added rules or counts with the removals instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - rules_added=0 was a trivial bypass. Fixed in eb589f1 by comparing re-additions to removals as a ratio: where rules_removed>=5 AND rules_added < (rules_removed * 0.1). A legitimate reload re-adds the bulk of what it removed, so it is excluded; a flush that re-adds nothing, or an attacker who adds a token rule after flushing hundreds, stays well under the 10% ratio and still fires. The ratio is documented as tunable in how_to_implement.

- Linux Auditd Config Change
search: |-
`linux_auditd` type=CONFIG_CHANGE op IN ("remove_rule", "add_rule")
| bin _time span=1m

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correlate reloads across minute boundaries

When a legitimate reload starts near a wall-clock minute boundary, this fixed bin places its removal records in the preceding bucket and its add records in the following bucket. The first bucket can therefore satisfy rules_removed>=5 AND rules_added=0 and raise the high-severity result even though the rules were immediately restored, contradicting the documented reload exclusion; correlate over a sliding reload window rather than an aligned one-minute bucket.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call - and validating this against live data surfaced something worth noting for anyone correlating CONFIG_CHANGE events. On real hosts a reload's removals and re-additions are attributed to different login sessions (removals under the invoking user's ses/auid, re-additions under the unset/system context) and the burst does straddle bucket edges. So in eb589f1 I (a) switched aggregation from per-session to host-wide BY _time host, which keeps both halves of a reload in the same group regardless of session, (b) widened the bucket to 5m so a sub-minute reload burst doesn't split, and (c) made the exclusion a re-add ratio rather than an exact added=0 match, so a reload that lands partially across an edge is still recognised by the bulk of its re-adds. Validated across a multi-host fleet over 7 days: the one reload that previously false-positived (928 removed / 464 re-added, split across sessions and a minute edge) is now suppressed, with zero false positives, while the true-positive dataset still fires.

search: |-
`linux_auditd` type=CONFIG_CHANGE op IN ("remove_rule", "add_rule")
| bin _time span=1m
| stats count(eval(op=="remove_rule")) as rules_removed count(eval(op=="add_rule")) as rules_added values(key) as removed_keys min(_time) as firstTime max(_time) as lastTime BY _time host auid ses subj

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude failed rule-removal operations

CONFIG_CHANGE can emit op=remove_rule for rejected deletion requests with res=no/res=0, but this count ignores res. Five failed requests in one minute therefore meet the threshold and generate a risk event claiming rules were removed even though the ruleset is unchanged; count only successful removals, or report failed attempts separately.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eb589f1. The base search now excludes failed operations with NOT res IN ("no", "0", "failed"), so rejected deletion requests are not counted toward rules_removed. I used a negative match rather than requiring res=yes so that events where res is absent or rendered differently by the extraction are retained rather than dropped (same concern as the subj finding). how_to_implement updated to note it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants