Skip to content

fix(m365): exclude users with future employeeHireDate from entra_users_mfa_capable #11486

@b-abderrahmane

Description

@b-abderrahmane

Steps to Reproduce

In an M365 tenant where Entra ID inbound provisioning (from any HR system) creates user accounts ahead of their start date and populates the standard Microsoft Graph employeeHireDate attribute, run:

prowler m365 --check entra_users_mfa_capable

Expected behavior

Users whose employeeHireDate is in the future should be skipped, in the same way the check already skips guests (user_type == "Guest") and disabled accounts (not account_enabled). Pre-provisioned future-start-date accounts have not been signed in to, MFA enrolment is part of day-1 onboarding, and a FAIL on these is operationally indistinguishable from a real "active user without MFA" finding in dashboards.

Actual Result with Screenshots or Logs

The check evaluates pre-provisioned future-start-date accounts and reports them as FAIL. The noise scales linearly with hiring cadence, and the only available mitigation today (per-user mute rule) has to be added pre-hire and removed post-hire, which is not sustainable in tenants with regular onboarding.

Suggested fix

employeeHireDate is a top-level documented property on the Microsoft Graph user resource (https://learn.microsoft.com/en-us/graph/api/resources/user), in the same group as employeeId, employeeType, employeeLeaveDateTime, and accountEnabled. Server-side filtering ($filter=employeeHireDate ne null) returns Request_UnsupportedQuery from Graph, so the check evaluates it client-side after _get_users enumeration. Two small additions:

1. prowler/providers/m365/services/entra/entra_service.py — surface the attribute

# in _get_users(), $select list:
select=[
    "id", "displayName", "userType",
    "accountEnabled", "onPremisesSyncEnabled",
    "employeeHireDate",                       # +
],
...
# in the User(...) constructor:
users[user.id] = User(
    ...
    employee_hire_date=getattr(user, "employee_hire_date", None),    # +
)

2. User model

employee_hire_date: Optional[datetime] = None

3. prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.py — skip future hires

from datetime import datetime, timezone
...
for user in entra_client.users.values():
    if user.user_type == "Guest" or not user.account_enabled:
        continue
    if user.employee_hire_date and user.employee_hire_date > datetime.now(timezone.utc):
        continue   # pre-provisioned, not yet onboarded — MFA enrolment happens on day-1
    ...

This matches the existing skip pattern for guests and disabled accounts (see PRs #10785, #11002, #8545 for precedent) and the check's docstring already documents the "scope to active members only" intent.

Happy to send a PR.

How did you install Prowler?

Docker (prowlercloud/prowler-api:5.27.0).

Prowler version

prowler 5.27.0 — also present on main (prowler/providers/m365/services/entra/entra_service.py _get_users + entra_users_mfa_capable/entra_users_mfa_capable.py:execute).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions