Skip to content

feat(o365): normalize outcomes and group administrator changes - #2661

Merged
osmontero merged 6 commits into
utmstack:v11from
kryonsx:codex/data-engine-o365-20260923
Sep 25, 2026
Merged

osmontero merged 6 commits into
utmstack:v11from
kryonsx:codex/data-engine-o365-20260923

Conversation

@kryonsx

@kryonsx kryonsx commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem and resulting behavior

Microsoft 365 audit outcomes can be workload-specific. The existing v11 filter treated some non-final or failed operations as success, and Exchange inbox and mailbox administration had overlapping alerts and gaps. This draft derives canonical outcomes from the final workload result. It also adds low-severity awareness alerts for successful Exchange administration changes, and keeps the two forwarding detections at medium severity.

  • Awareness alerts (low severity, category Administrative Awareness): inbox rule commands, mailbox delegate grants, folder permission changes on someone else's mailbox, calendar delegation, mail connectors and mailbox client access.
  • Forwarding detections (medium severity, category Collection): an inbox rule created or changed with a forwarding setting (Suspicious Mail Forwarding Rule Creation), and Set-Mailbox setting a forwarding address (O365 Mailbox Auto-Forwarding (Set-Mailbox)). Both keep their v11 names, so the name-based rule sync updates the existing rules in place.

These alerts group by tenant, organization, data source, acting account and action. Later changes by the same account become child alerts, so the rule flood guard limit of 50 top-level alerts per data source a day is not reached. Routine records are excluded: Outlook desktop's repeated UpdateInboxRules saves, owners changing their own folders, Microsoft service accounts configuring mailboxes, permission removals, and Set-Mailbox calls that only clear forwarding. An alert does not by itself establish compromise or an external recipient.

The filter records exact Exchange parameter names before its existing string cast, after clearing any input-supplied markers. The mailbox forwarding flag needs a non-empty forwarding address. The forwarding and SendOnBehalf rules also accept the structured parameter array present in older deployed filters, but these rules need this filter to work with the current v11 cast, so the two should ship together. The overlapping generic forwarding rule is removed. The existing SendAs history escalation remains separate. The password-spraying rule groups by the attacking address.

Evidence and checks

  • Microsoft audit activities and the audit schema establish the operations and field shapes. Exchange Set-Mailbox and Set-InboxRule define the forwarding and delegation settings.
  • Read-only production records were checked privately for every rule family, with 30 days of matches measured on 15 servers. The eight rules match about 1,100 events there and would open about 300 top-level alerts, with at most 9 new top-level alerts for any rule on any server in one day. Instance names and document identifiers stay outside this pull request.
  • 122 fabricated raw cases cover positive and negative operations, cleared and set values, owners' own changes, Microsoft service accounts, missing optional identities and parameter spoofing. go test ./... in plugins/alerts passes on the newest v11 merge with go-sdk v1.1.36.
  • Local EventProcessor playground rebuilt from engine main 8a3ade7 (go-sdk v1.1.36): all 122 fabricated cases pass, and 328 real records parse with no errors, both before and after this draft and against v11 d2479c1a. The same checks also passed on the earlier 497bf53 build. This does not establish deployment, production grouping, alert indexing or notifications. History-based rules were not exercised end to end.

Threat-intelligence dependency: this draft writes failure where v11 wrote failed. The current feeds plugin skips only denied, blocked and failed, so failed sign-ins from listed addresses become eligible for "Known Malicious IP Detected". Merging EventProcessor #19 first avoids that. See the comments for measurements.

Remote-domain, Exchange sharing-policy and organization-relationship rules are deferred until relevant real events can be sampled. This remains a draft against v11 for review. No deployment or merge is part of this change.

🤖 Generated with Claude Code

@kryonsx kryonsx changed the title fix(o365): derive action results from final workload outcomes feat(o365): normalize outcomes and group administrator changes Sep 23, 2026
kryonsx and others added 2 commits September 24, 2026 10:06
Production counts showed the awareness rules would pass 50 top-level alerts
a day on busy tenants: they grouped per object, so every mailbox, folder
and target made a new top-level alert (up to 125 a day for folder
permissions on one data source), and the rule flood guard switches off a
rule after 50. Group the eight awareness rules by acting account and action
instead; later changes become child alerts, which stay visible but are not
counted. Folder permission changes a user makes to their own mailbox
(owner sharing a calendar) are routine and were most of the volume, so the
folder rule now skips them. Test fixture added for that case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A spray is one address trying many accounts. Grouping by address and user
made every targeted account a new top-level alert; yesterday's production
counts give up to about 1,200 such pairs a day on one tenant, far above the
rule flood guard limit of 50. Group by the attacking address so each
sprayer is one top-level alert and the per-account attempts are children.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kryonsx

kryonsx commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Changes pushed (be5533e2, 029bfa31): awareness and spraying alerts stay under the flood guard limit

What changed

  • The eight awareness rules (calendar delegation, mail connectors, client access, folder permissions, inbox rule, inbox forwarding, mailbox forwarding, delegate permission; now v1.1.1) group by tenant, organization, data source, acting account (adversary.user) and action. Before, they also grouped by ObjectId, target.user, MailboxOwnerUPN and folderId.
  • awareness_mailbox_folder_permissions.yml skips changes a user makes to their own mailbox: !exists("target.user") || safe("origin.user", "") != safe("target.user", "").
  • credential_access_microsoft_365_potential_password_spraying_attack.yml (v1.0.5) groups by the attacking address only.
  • o365_awareness_test.go now checks the new grouping, and o365_awareness.json has a new negative fixture for a user changing their own mailbox.

Why

  • The rule flood guard switches a rule off once it produces more than 50 top-level alerts for one data source in 24 hours. Child alerts do not count.
  • Grouping per object meant every mailbox, folder and target became its own top-level alert. Read-only production counts over the last 14 days, per data source per day:
    • folder permissions reached up to 125 new top-level alerts (over 50 on 5 of 14 days);
    • Set-Mailbox reached up to 112 (an upper bound, since only calls with forwarding parameters match);
    • inbox rules reached up to 59.
  • Grouping by account and action cuts these to at most 58, 5 and 32. The remaining folder-permission volume was users sharing their own calendars and folders: 427 events from up to 52 users a day. Changes to someone else's mailbox, the security-relevant case, came from at most 5 accounts a day. With both changes, the worst case is about 5 new top-level alerts a day for folder permissions.
  • Password spraying is one address trying many accounts. Grouping by address and user made each targeted account a new top-level alert: up to about 1,200 pairs a day on one tenant yesterday, among the 69 addresses that reached 5 failures in a minute. It is now one top-level alert per attacking address, with the per-account attempts as children.

Validation

  • Local EventProcessor playground (commit 497bf53, rule engine library v1.1.34), on 165 real Microsoft 365 records from 4 production instances, comparing this draft before and after the change.
  • The 4 folder-permission alerts in the sample were all users changing their own mailbox, and they no longer fire.
  • "Inbox Rule Changed" still raises the same 21 alerts, with 11 top-level instead of 13.
  • No other rule changed, and all records parse without errors.
  • go test ./... in plugins/alerts passes. The history thresholds themselves were not run end to end, because no local OpenSearch was available.

Note on threat intelligence: this draft changes failed to failure. The current feeds plugin skips only denied, blocked and failed, so failed sign-ins from listed addresses become eligible for "Known Malicious IP Detected". On two instances that is about 1,350 and 1,500 new public addresses a day, and only 2 of them appeared in those instances' feed alerts over the last 14 days. Merging EventProcessor #19 first avoids it.

@osmontero

Copy link
Copy Markdown
Member

@kryonsx heads-up: my unmerged branch fix/aws-filter-rules also carries an O365 filter v1.2.3 + 60 migrated rules (canonical action/origin.user/origin.ip, cast steps for array fields like log.Parameters/log.Members). We overlap on filters/office365/o365.yml and several rules/office365/* files in this PR. Same proposal as I left on #2660: you land these two (they're well-tested), then I rebase onto the new v11 head and port your changes into my v2.1.0/v1.2.3 filters before opening my PR — so we don't double-merge the same files. Let me know if you'd rather I review against your head branch directly.

…ng severity

Awareness alerts still recorded large amounts of routine activity as child
alerts, and each child is stored, passes tag and automation rules, and goes to
automatic alert analysis when that module is on.

- Inbox Rule Changed no longer includes UpdateInboxRules. Outlook desktop
  writes these records repeatedly for the same rules, usually with no rule
  change; they were almost all of this rule's volume.
- Folder Permission Changed also skips records with logon type Owner, which
  works with filters that do not map the mailbox owner, and changes by
  Microsoft service accounts (ExternalAccess true with an S-1-5 security
  identifier as the actor), for example group mailbox configuration.
- Mailbox Delegate Permission Changed alerts on grants only, not removals.
- The Set-Mailbox forwarding alert needs a non-empty ForwardingSmtpAddress or
  ForwardingAddress. Exchange records a cleared address as an empty value, and
  DeliverToMailboxAndForward alone sets no destination. The filter flag is
  renamed log.o365MailboxForwardingSet to match (filter 1.4.1).
- Both forwarding rules alert at medium severity in the Collection category
  and keep their v11 names, so the name-based rule sync updates the existing
  rules instead of deleting and recreating them.
- Standard technique codes for the connector, client access and inbox rule
  alerts; the calendar rule no longer lists one reference twice.

Fabricated regression cases cover each change and fail on the previous head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kryonsx

kryonsx commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Changes pushed (c2073208): remove routine noise from the awareness alerts, restore forwarding severity

What changed

  • Office 365 Inbox Rule Changed no longer includes UpdateInboxRules. Outlook desktop writes these records repeatedly for the same rules, mostly with no actual rule change.
  • Office 365 Mailbox Folder Permission Changed also skips records with logon type Owner, so owners' own changes are skipped even where the filter does not map the mailbox owner. It also skips changes by Microsoft service accounts: ExternalAccess true with an S-1-5-... security identifier as the actor, such as group mailbox configuration.
  • Office 365 Mailbox Delegate Permission Changed alerts on grants only. Removals no longer alert.
  • O365 Mailbox Auto-Forwarding (Set-Mailbox) needs a non-empty ForwardingSmtpAddress or ForwardingAddress. Exchange records a cleared address as an empty value, and DeliverToMailboxAndForward alone sets no destination. The filter flag is now log.o365MailboxForwardingSet (filter 1.4.1).
  • Both forwarding rules alert at medium severity (confidentiality 2) in the Collection category and keep their v11 names, Suspicious Mail Forwarding Rule Creation and O365 Mailbox Auto-Forwarding (Set-Mailbox). The name-based rule sync from rule/filter initial definition load #2687 then updates the existing rules instead of deleting and recreating them.
  • Standard technique codes for the connector (T1537), client access (T1114.002) and inbox rule (T1564.008) alerts. The calendar rule no longer lists one reference twice.

Why

  • Grouping by account and action (previous push) keeps top-level alerts under the flood guard limit. Child alerts are still stored, pass tag and automation rules, and go to automatic alert analysis when that module is on.
  • Read-only counts over the last 30 days on 15 production servers:
    • UpdateInboxRules made 237,273 of the inbox rule alert's 238,004 matches (99.7%), up to about 20,800 a day on one server. Single mailboxes wrote more than 3,000 in a day, almost all ModifyMailboxRule saves with no rule actions.
    • On the busiest server, 9,650 of 10,563 folder-permission matches were Microsoft service accounts configuring group mailboxes, and the other 913 were owners changing their own folders. Across all 15 servers, 5 matches remain with this push, all administrators running Set-MailboxFolderPermission: the case this alert is for.
    • 72 of 293 delegate matches were removals. 9 of 28 mailbox-forwarding matches only cleared forwarding.
  • With this push the eight rules match about 1,100 events in those 30 days (about 249,000 in the pack as first proposed) and would open about 300 top-level alerts across these servers. The worst day for any rule on any server is 9 new top-level alerts.

Validation

  • go test ./... in plugins/alerts passes. o365_awareness.json now has 122 fabricated cases (114 before). The new and changed cases fail on the previous head 029bfa31 and pass here.
  • Local EventProcessor playground (commit 497bf53; parser plugins on go-sdk v1.1.26, rule plugin on v1.1.34):
    • All 122 fabricated cases through the real parser and rule plugin: every expected field, alert, severity and non-alert matched, with no errors.
    • 328 real Microsoft 365 records (163 chosen from the changed event classes, 165 general), previous head against this commit, with every Office 365 filter and rule staged. All records parse without errors. Alerts change only for Outlook rule saves, permission removals, Microsoft service folder changes and forwarding turn-offs, plus the two restored names. No other rule changed, and the forwarding alerts come out at medium severity.
    • History-based rules were not exercised end to end, because no local OpenSearch was available.
  • The rule conditions were replayed with go-sdk v1.1.34 on 394 stored production documents. There were no evaluation errors, and matches changed only in the same classes.

Not covered: no rule alerts on inbox rules created from Outlook desktop, as in v11. Their UpdateInboxRules records carry the rule actions in OperationProperties, but the sampled records had no forwarding example to build a reliable condition on.

Brings the draft onto the newest v11, including the alerts module on
go-sdk v1.1.36, before revalidating on the newest engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kryonsx

kryonsx commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Updated to the newest v11 (007c722e) and revalidated on the newest engine and go-sdk

  • Merged official v11 (d2479c1a) into this branch. No file changed on both sides. plugins/alerts now builds on go-sdk v1.1.36, which is the current v11 pin and the newest tag.
  • go test ./... in plugins/alerts passes on v1.1.36, including the 122 Office 365 awareness cases.
  • Local EventProcessor playground rebuilt from engine main (8a3ade7). Every engine plugin links go-sdk v1.1.36. The geolocation plugin comes from v11 (v1.1.34).
    • All 122 fabricated cases pass through the real parser and rule plugin. Every expected field, alert, severity and non-alert matched, with no errors.
    • 328 real Microsoft 365 records, previous head 029bfa31 against this head: all records parse without errors. Alerts change only for Outlook rule saves, permission removals, Microsoft service folder changes and forwarding turn-offs, plus the two restored names. This is the same result as on the older engine build.
    • The same records, v11 d2479c1a against this head, with no errors on either side:
      • "Office 365 Mailbox Delegation Abuse" becomes "Office 365 Mailbox Delegate Permission Changed" on the same events.
      • The duplicate "Suspicious Email Forwarding Rule Created" no longer fires.
      • "O365 Mailbox Auto-Forwarding (Set-Mailbox)" also catches ForwardingAddress destinations, which the old smtp: text search missed.
      • The new awareness alerts fire for inbox rule commands, connectors, client access and calendar delegation.
      • Nothing fires on Outlook rule saves, Microsoft service folder changes or permission removals.
    • History-based rules were not exercised end to end, because no local OpenSearch was available.
  • The rule conditions, replayed with go-sdk v1.1.36 on 394 stored production documents, give the same matches as with v1.1.34, with no evaluation errors. v1.1.36 keeps regexMatch text-only, which is how the forwarding-address check uses it.

This stays a draft.

@kryonsx

kryonsx commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Revalidated against the newest published engine image, ghcr.io/utmstack/utmstack/eventprocessor:v11.2.14 (built 2026-09-24 19:13 UTC)

  • The image's programs record their build sources. The playground and every engine plugin come from EventProcessor 8a3ade7 with go-sdk v1.1.36 (Go 1.26.8). The alerts and geolocation plugins come from v11 d2479c1a (Go 1.25.5). These are the same sources as the previous validation. The base image ghcr.io/utmstack/eventprocessor/base:1.1.7 carries the identical playground program. 1.2.0-beta is older, from 2026-09-14.
  • I rebuilt locally from those exact revisions, with the same Go versions and module versions. Only the operating-system build target and one CPU-information package differ. The image's own geolocation data was loaded.
  • Results on head 007c722e:
    • All 122 fabricated cases pass: every expected field, alert, severity and non-alert, with no errors.
    • 328 real records parse with no errors on either side. The geolocation step added a location to 137 of the 322 records that carry an address.
    • Against the previous head 029bfa31, alerts change only for Outlook rule saves, permission removals, Microsoft service folder changes and forwarding turn-offs, plus the two restored names.
    • Against v11 d2479c1a, the differences are the same as reported in the previous comment.
  • History-based rules were not exercised end to end, because no local OpenSearch was available.

No code changed. This stays a draft.

@osmontero
osmontero marked this pull request as ready for review September 25, 2026 17:31
@osmontero
osmontero requested a review from a team September 25, 2026 17:31
@osmontero
osmontero merged commit 00bac09 into utmstack:v11 Sep 25, 2026
3 of 7 checks passed
osmontero added a commit that referenced this pull request Sep 25, 2026
FileMalwareDetected (RecordType 6) and AlertAdded (RecordType 78) are
system-generated detections that carry no ResultStatus in production.
KryonX's v1.4.1 filter (merged #2661) therefore never resolves
actionResult=success for them, which would silently dead the rules.
The event's presence is the signal, so match on action alone.

Verified against the real filter + production data shapes: all 9 O365
gap rules now fire (MailboxLogin RT2/Succeeded, Set-CASMailbox +
Add-MailboxFolderPermission + Remove-DlpPolicy + Set-MailboxAuditBypass
RT1/True, Disable/UpdateConditionalAccessPolicy RT8/Success,
MailItemsAccessed RT50/Succeeded, FileMalwareDetected RT6 + AlertAdded
RT78 no-ResultStatus).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants