|
| 1 | +from datetime import datetime, timezone |
| 2 | + |
| 3 | +from prowler.lib.check.models import Check, Check_Report_Azure |
| 4 | +from prowler.providers.azure.services.entra.entra_client import entra_client |
| 5 | + |
| 6 | +STALE_THRESHOLD_DAYS = 90 |
| 7 | + |
| 8 | + |
| 9 | +class entra_user_with_recent_sign_in(Check): |
| 10 | + """ |
| 11 | + Ensure enabled Entra ID users have signed in within the last 90 days. |
| 12 | +
|
| 13 | + This check evaluates each enabled user's last interactive sign-in to detect stale or dormant accounts that should be reviewed or deprovisioned. Sign-in activity requires Entra ID P1/P2 licensing. |
| 14 | +
|
| 15 | + - PASS: The enabled user signed in within the last 90 days. |
| 16 | + - FAIL: The enabled user has not signed in for more than 90 days, or has never signed in. |
| 17 | + - FAIL (tenant-level): No sign-in activity data is available for any enabled user, indicating missing P1/P2 licensing or Graph permissions (reported once instead of flagging every user). |
| 18 | + """ |
| 19 | + |
| 20 | + def execute(self) -> Check_Report_Azure: |
| 21 | + findings = [] |
| 22 | + |
| 23 | + for tenant_domain, users in entra_client.users.items(): |
| 24 | + enabled_users = {k: v for k, v in users.items() if v.account_enabled} |
| 25 | + |
| 26 | + if not enabled_users: |
| 27 | + continue |
| 28 | + |
| 29 | + # If all enabled users are missing sign-in data, avoid claiming |
| 30 | + # they never signed in. This usually indicates missing telemetry, |
| 31 | + # often due to licensing or Graph permission limitations. |
| 32 | + all_null = all(u.last_sign_in is None for u in enabled_users.values()) |
| 33 | + if all_null: |
| 34 | + first_user = next(iter(enabled_users.values())) |
| 35 | + report = Check_Report_Azure( |
| 36 | + metadata=self.metadata(), resource=first_user |
| 37 | + ) |
| 38 | + report.subscription = f"Tenant: {tenant_domain}" |
| 39 | + report.resource_name = "Sign-in Activity Data" |
| 40 | + count = len(enabled_users) |
| 41 | + noun = "user" if count == 1 else "users" |
| 42 | + report.status = "FAIL" |
| 43 | + report.status_extended = ( |
| 44 | + f"No sign-in activity data available for any of the " |
| 45 | + f"{count} enabled {noun}. This likely means the tenant " |
| 46 | + f"is missing Entra ID P1/P2 licensing or the required " |
| 47 | + f"Graph permissions to read sign-in activity." |
| 48 | + ) |
| 49 | + findings.append(report) |
| 50 | + continue |
| 51 | + |
| 52 | + for user_domain_name, user in enabled_users.items(): |
| 53 | + report = Check_Report_Azure(metadata=self.metadata(), resource=user) |
| 54 | + report.subscription = f"Tenant: {tenant_domain}" |
| 55 | + |
| 56 | + if user.last_sign_in is None: |
| 57 | + report.status = "FAIL" |
| 58 | + report.status_extended = f"User {user.name} has never signed in." |
| 59 | + else: |
| 60 | + last = user.last_sign_in |
| 61 | + if last.tzinfo is None: |
| 62 | + last = last.replace(tzinfo=timezone.utc) |
| 63 | + days_since = (datetime.now(timezone.utc) - last).days |
| 64 | + if days_since > STALE_THRESHOLD_DAYS: |
| 65 | + report.status = "FAIL" |
| 66 | + report.status_extended = ( |
| 67 | + f"User {user.name} has not signed in for {days_since} days." |
| 68 | + ) |
| 69 | + else: |
| 70 | + report.status = "PASS" |
| 71 | + report.status_extended = ( |
| 72 | + f"User {user.name} signed in {days_since} days ago." |
| 73 | + ) |
| 74 | + |
| 75 | + findings.append(report) |
| 76 | + |
| 77 | + return findings |
0 commit comments