Skip to content

Add E8-MAC-2.1: Win32 API call blocking ASR rule check#215

Open
SidharthSabu wants to merge 5 commits into
mainfrom
feature/e8-mac-2-1
Open

Add E8-MAC-2.1: Win32 API call blocking ASR rule check#215
SidharthSabu wants to merge 5 commits into
mainfrom
feature/e8-mac-2-1

Conversation

@SidharthSabu
Copy link
Copy Markdown
Collaborator

Summary

Implements the first ASD Essential Eight Maturity Model control: E8-MAC-2.1 - Win32 API calls blocked from Office macros. Adds the data collector, the Rego policy, and the benchmark metadata so a Microsoft 365 connection can run an "ASD Essential Eight" scan and get a real PASS/FAIL for this control.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor / code cleanup
  • Documentation
  • CI/CD / infrastructure
  • Security

Affected Components

  • /backend-api
  • /frontend
  • /engine (collectors / policies)
  • /security
  • /infrastructure
  • /.github/workflows
  • /docs

Motivation

First concrete ASD Essential Eight control delivered to AutoAudit. Unlocks the Macro Settings benchmark for the rest of the team to extend with their own controls. E8-MAC-2.1 is an ML2 control that requires Office macros to be blocked from making Win32 API calls — verifiable directly via Microsoft Graph against an Intune Endpoint Protection profile.

Dependency

Depends on #210 (Normalize framework name and control_id when building OPA package path). Without #210 merged first, the OPA lookup path will not match the Rego package and the control returns the generic OPA fallback instead of a real result.

Implementation

  • Collector(engine/collectors/entra/devices/asr_rules.py) — reads the Win32-API-from-macros ASR rule from a legacy Endpoint Protection device configuration. Uses the beta Graph endpoint for the per-config detail because the v1.0 schema windows10EndpointProtectionConfiguration omits the ASR rule properties. Returns defenderOfficeMacroCodeAllowWin32ImportsType normalised to block / audit / disabled.
  • Registry (engine/collectors/registry.py) — registers the new collector under entra.devices.asr_rules.
  • Rego (engine/policies/.../e8_mac_2_1_win32_api_block.rego) — pass condition: win32_api_rule_state == "block". Anything else (including audit) is non-compliant per ASD Essential Eight ML2.
  • Metadata(engine/policies/.../metadata.json) — defines all 12 controls of the macro settings benchmark with E8-MAC-2.1 set to automation_status: "ready" and the others marked not_started or manual so they are documented in the benchmark scope but not dispatched.

Testing Done

  • Unit tests pass locally

  • Tested manually — describe how:

  • Created a real Endpoint Protection profile in Intune (E8-MAC-2.1-Legacy-ASR) with the Win32-imports-from-macros rule set to "Block" , ran an E8 scan → control returns PASS with message "Win32 API calls from Office macros are blocked in Block mode".

  • Removed/changed the rule and re-ran → control returns FAIL with a meaningful message indicating the actual non-compliant state (audit, disabled, not_configured).

  • Confirmed CIS M365 scans behave identically — no regressions from the registry change.

  • No tests required — explain why:

Security Considerations

No new authentication surface area. The collector uses the existing Microsoft Graph client and the existing DeviceManagementConfiguration.Read.All application permission already required by other Intune collectors. No secrets, no new env vars, no new external endpoints.

Breaking Changes

  • No breaking changes
  • Yes — describe below:

Rollback Plan

  • Revert commit is sufficient
  • Requires additional steps — describe below:

Contributors

  • Sidharth — Collector (asr_rules.py) and registry wiring
  • Vrindha — Benchmark metadata (metadata.json)
  • Armeen — Rego policy (e8_mac_2_1_win32_api_block.rego)

Checklist

  • Code follows project conventions
  • No secrets, credentials, or tokens committed
  • Relevant documentation updated (if applicable)
  • CI/CD workflows pass on this branch
  • PR is focused on one thing

Screenshots

Test Setup performed in Intune

image
  1. Config Settings in Blocked state
image

Autoaudit Scan Result

image
  1. Config Settings in Audit state
image

Autoaudit Scan Result

image
  1. Config Settings is in not configured state
image

Autoaudit Scan Result
image

  1. Verified CIS related benchmarks are working as expected
image

Copy link
Copy Markdown

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

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: 12e0be420f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread engine/collectors/entra/devices/asr_rules.py Outdated
@du-dhartley
Copy link
Copy Markdown
Collaborator

@SidharthSabu I've sent you a teams message along with Emma so that a major path conflict can be resolved; that aside, I've also added a few changes here to address.

@du-dhartley
Copy link
Copy Markdown
Collaborator

three different style choices for "essential eight" are in play:

  • Directory: essential-eight/asd-essential-eight/ (kebab-case)
  • metadata.json framework: "essential-eight" (kebab)
  • metadata.json slug: "asd-essential-eight" (kebab)
  • Rego package: essential_eight.asd_essential_eight (snake)
  • Control IDs: E8-MAC-2.1 (uppercase + kebab + dot)
  • Normalized for OPA: essential_eight.asd_essential_eight.v2025.control_e8_mac_2_1

That works mechanically thanks to PR 210's normalizer, but it's a lot of stylistic surface. Two options:

  • Embrace the divergence as a deliberate "raw vs. normalized" split: metadata/paths use kebab-case (human-readable),
    Rego packages use snake_case (language requires it). Document that in engine/policies/README.md.
  • Or use kebab-case everywhere on the metadata side (control IDs included) and let the normalizer handle Rego.

from collectors.graph_client import GraphClient


def _normalize_state(raw: str) -> str:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

_normalize_state uses substring matching - this is a little brittle

  def _normalize_state(raw: str) -> str:
      raw = (raw or "").lower()
      if "block" in raw:
          return "block"
      if "audit" in raw:
          return "audit"
      if "warn" in raw:
          return "warn"
      if "disable" in raw or "userdefined" in raw or "off" in raw:
          return "disabled"
      return raw or "not_configured"

Substring matching on Microsoft enum values is fragile — Microsoft has historically used values like enable, enabled, enableBlockMode, userDefined, notConfigured, plus regional variations. A future Intune update that adds e.g. auditModeWithExceptions would match the "audit" branch but lose information.

Recommend an explicit enum map instead:

  INTUNE_ASR_STATE_MAP = {
      "block": "block",
      "audit": "audit",
      "warn": "warn",
      "disable": "disabled",
      "userdefined": "disabled",
      "notconfigured": "not_configured",
      "off": "disabled",
  }

  def _normalize_state(raw: str) -> str:
      return INTUNE_ASR_STATE_MAP.get((raw or "").lower(), "unknown")

Then "unknown" clearly surfaces as a non-Block state and the Rego fails correctly, without silently letting through a string that looks like noise. Pin the map to documented Microsoft Graph enum values and cite the source in a comment.

if not config_id:
continue
full_config = await client.get(
f"/deviceManagement/deviceConfigurations/{config_id}", beta=True,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not a Python style issue (PEP 8 allows trailing commas), just unusual to see one after a kwarg. Black/Ruff defaults often add it when arguments span multiple lines, so this may just be auto-formatting.

SidharthSabu added a commit that referenced this pull request May 12, 2026
…-normalization

Normalize framework name and control_id when building OPA package path. Lowercases the framework name and replaces hyphens with underscores so frameworks like essential-eight and control IDs like E8-MAC-2.1 resolve to the correct Rego package. Backwards-compatible on existing CIS inputs. Unblocks PR #215.
Defines all 12
  controls of the macro settings benchmark. Only E8-MAC-2.1 is wired up in this PR
  (automation_status: ready); the other ML1/ML3 controls are scaffolded with not_started or
  manual so the full benchmark scope is documented in metadata. The worker skips non-ready
  controls during scans, and the UI filters skipped controls out of the per-scan results list.
  Future PRs implement them by flipping the flag.

Author: Vrinda
Evaluates the win32_api_rule_state field returned by the ASR rules collector. Compliant only when the state is "block" — audit/warn/disabled all fail per ASD Essential Eight ML2, which explicitly requires Block mode (Audit is not accepted). Surfaces the rule state, source endpoint, and policy name in details for audit evidence.

Author: Armeen
…ot_started controls

test_control_schema_consistency requires that controls with automation_status="not_started" have null data_collector_id and policy_file. The original metadata referenced an unimplemented collector (entra.devices.configuration_policies) and rego files that don't exist yet. Setting both to null until the future PRs that implement those controls populate them.

Author: Vrinda
Previously the collector returned on the first profile match, ignoring the rest. For tenants with multiple Endpoint Protection profiles configuring the rule differently (e.g.one in block, one in audit), this caused the result to depend on Graph API ordering, that is the same tenant could pass or fail across runs. The collector now inspects every profile, collects all states, and returns the weakest. ASD ML2 requires block on every profile; any non-block profile makes the tenant non-compliant.

Addresses Codex review feedback (2nd Comment)
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