Skip to content

Port 3 BEC-relevant Sagan Cloud rules to legacy (skip BAV2ROPC) - #706

Merged
CGoggins56 merged 1 commit into
quadrantsec:mainfrom
bryant-smith:feature/o365/port-bec-cloud-rules-to-legacy
Aug 28, 2026
Merged

Port 3 BEC-relevant Sagan Cloud rules to legacy (skip BAV2ROPC)#706
CGoggins56 merged 1 commit into
quadrantsec:mainfrom
bryant-smith:feature/o365/port-bec-cloud-rules-to-legacy

Conversation

@bryant-smith

Copy link
Copy Markdown
Contributor

Summary

  • Ports 3 of 4 candidate sagan-cloud-rules BEC/O365 detections to legacy .rules, identified via a legacy-vs-cloud BEC coverage comparison and validated for noise against Grafana alert history (YTD 2026, all customers):
    • 5017957/5017958 (msapi-azuread.rules) — ungated illicit OAuth consent-grant / delegated-permission-grant detection, porting cloud 600548/600599. Legacy's existing consent rules (5004804/5004837) only fire from outside HOME_COUNTRY or a Bluedot-listed IP — a same-country, clean-IP grant was previously invisible.
    • 5017959/5017960 (new msapi-microsoftteams.rules) — Teams archive-file / multi-file exfil to foreign tenant users, porting cloud 6005331/6005332.
  • Deliberately excludes cloud 6005257 (Legacy Auth / BAV2ROPC) — Grafana showed 2,290 hits YTD across 17 tenants, by far the noisiest of the four candidates. Needs a suppression pass before it's portable, not a straight 1:1 port.

Known limitations (flagged inline in the rule comments)

  • ModifiedProperties (consent rules) and MessageFiles (Teams rules) are JSON arrays in the source UAL records. The legacy engine never expands JSON arrays via json_content/json_map, so the scope/file checks use plain content/pcre substring matching on the raw record instead of a structured field path — same technique already used elsewhere in msapi-azuread.rules for array-nested fields (e.g. the named-role rules).
  • .ParticipantInfo.HasForeignTenantUsers is 38 characters, over the engine's 31-char key-truncation limit (JSON_MAX_KEY_SIZE=32), so both Teams rules use the truncated key .ParticipantInfo.HasForeignTena, following the established fix pattern from sagan-rules#704.
  • Cloud's array-length count (MessageFiles[] ≥6) and ChatThreadId-based rate limiting can't be reproduced in legacy (no array counting, no arbitrary track_by field), so both Teams rules substitute a by_username rate as an approximation — flagged as such, not a literal port.

Test plan

  • python3 / repo lint check for duplicate SIDs and rule syntax (ran locally — no collisions against existing alert lines)
  • Replay against a real Entra/O365 consent-grant and Teams file-share log fixture before enabling in production (none of the 4 new rules have been fired against real telemetry yet)
  • Confirm ConsentAction.Permissions / DelegatedPermissionGrant.Scope / MessageFiles literal substrings actually appear as expected in a live UAL record for this ingest pipeline

🤖 Generated with Claude Code

https://claude.ai/code/session_01RHYEbkU9keVovW9UAACgsX

Adds ungated legacy analogues for three low-noise sagan-cloud-rules
BEC detections that legacy currently only covers geo/bluedot-gated
or not at all:

- sid:5017957/5017958 (msapi-azuread.rules) -- ungated illicit OAuth
  consent-grant / delegated-permission-grant detection, porting cloud
  600548/600599. Legacy's existing consent rules (5004804/5004837) only
  fire from outside HOME_COUNTRY or a Bluedot-listed IP, so a same-
  country, clean-IP grant was invisible. Uses plain content/pcre
  instead of json_content for the ModifiedProperties scope check,
  since ModifiedProperties is a JSON array and legacy never expands
  arrays via json_content/json_map.

- sid:5017959/5017960 (new msapi-microsoftteams.rules) -- Teams
  archive-file / multi-file exfil to foreign tenant users, porting
  cloud 6005331/6005332. json_content key truncated to the engine's
  31-char limit (.ParticipantInfo.HasForeignTena) per the documented
  JSON_MAX_KEY_SIZE fix pattern. Cloud's array-length/ChatThreadId
  rate logic can't be reproduced directly (no array counting, no
  arbitrary track_by field), so this substitutes a by_username rate
  as a proxy -- flagged inline as an approximation, not a literal port.

Deliberately excludes cloud 6005257 (Legacy Auth / BAV2ROPC): Grafana
alert volume showed 2,290 hits YTD 2026 across 17 tenants, the noisiest
of the four candidates evaluated -- needs a suppression pass before
porting, not a straight 1:1 port.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHYEbkU9keVovW9UAACgsX
@CGoggins56
CGoggins56 merged commit fb8eb32 into quadrantsec:main Aug 28, 2026
1 check passed
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