Skip to content

fix(m365): Defender policy checks crash with KeyError on preset security policies #12751

Description

@cesararroba

Issue search

  • I have searched the existing issues and this bug has not been reported yet

Which component is affected?

Prowler CLI/SDK

Cloud Provider (if applicable)

Microsoft 365

Steps to Reproduce

  1. Use a Microsoft 365 tenant with the built-in preset security policies enabled (Standard or Strict). This is the configuration Microsoft recommends, so most tenants have it.
  2. Run an M365 scan, for example prowler m365 -c defender_malware_policy_notifications_internal_users_malware_enabled.
  3. The check crashes with a KeyError on the name of the preset policy.

Expected behavior

A policy with no matching entry in the corresponding rules dictionary should be handled gracefully, either skipped or reported with a status that reflects that Microsoft manages it through a preset security policy rule. The check should still evaluate the remaining policies instead of aborting.

Actual Result with Screenshots or Logs

Something went wrong in defender_malware_policy_notifications_internal_users_malware_enabled, please use --log-level ERROR
[File: check.py:744] [Module: check] ERROR: defender_malware_policy_notifications_internal_users_malware_enabled -- KeyError[83]: 'Standard Preset Security Policy1716879803305'

The check aborts and returns no findings. The rest of the scan completes.

How did you install Prowler?

From pip package (pip install prowler)

Environment Resource

Workstation

OS used

Not reported

Prowler version

5.34.0, and still present on current master (5.40.0)

Python version

Not reported

Pip version

Not reported

Context

defender_service.py builds malware_rules from Get-MalwareFilterRule only, keyed by the MalwareFilterPolicy name. Preset security policies are returned by Get-MalwareFilterPolicy with IsDefault=False, but their rules come from Get-ATPProtectionPolicyRule and Get-EOPProtectionPolicyRule, so they never appear as keys in malware_rules.

The check treats every non-default policy as a custom policy and indexes the dictionary directly at line 83 of defender_malware_policy_notifications_internal_users_malware_enabled.py, which is the line the traceback points at. The helper in that same file already guards for the missing key at line 158 (policy.identity in defender_client.malware_rules), so the case was anticipated, but the four reporting branches index it without checking.

The same pattern is copied across seven Defender checks, so a tenant with preset security policies loses all of them, not just the one in the report:

  • defender_malware_policy_notifications_internal_users_malware_enabled (malware_rules, line 83)
  • defender_malware_policy_common_attachments_filter_enabled (malware_rules, line 83)
  • defender_malware_policy_comprehensive_attachments_filter_applied (malware_rules, line 153)
  • defender_antiphishing_policy_configured (antiphishing_rules, line 80)
  • defender_antispam_outbound_policy_configured (outbound_spam_rules, line 80)
  • defender_antispam_outbound_policy_forwarding_disabled (outbound_spam_rules, line 83)
  • defender_antispam_policy_inbound_no_allowed_domains (inbound_spam_rules, line 83)

The scan exits normally and the only trace is the log line, so the loss is silent. Seven email security controls return no findings on tenants running Microsoft's recommended configuration.

Proposed direction:

  • Resolve the rule once per policy with .get() and decide explicitly, rather than indexing. Skipping a policy with no rule avoids a false FAIL on policies that Microsoft configures by design.
  • Reporting those policies with an explicit status would be more transparent than skipping them, but it needs its own status_extended wording, so it is worth deciding separately.
  • Apply the same fix to the seven checks and add a regression test covering a policy list that includes a preset policy with no matching rule.
  • Longer term, the service already collects what is needed to evaluate preset policies properly. eop_protection_policy_rules and atp_protection_policy_rules exist in defender_service.py and are already used by defender_strict_preset_security_policy_enabled. Wiring the policy checks to them would let preset policies be assessed instead of ignored.

Origin of this issue

Reported by a user in the Prowler Community Slack: https://prowler-workspace.slack.com/archives/C045DAM2NE9/p1788682771936239

The reporter identified the cause correctly. The scope across the other six checks was found while confirming the report.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugprovider/m365Issues/PRs related with the M365 providerseverity/mediumResults in some unexpected or undesired behavior.status/needs-triageIssue pending triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions