mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-01 13:47:21 +00:00
Compare commits
2 Commits
dependabot
...
feat/prowl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f389ee3d1d | ||
|
|
2ceda9bac1 |
@@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- CloudTrail Timeline abstraction for querying resource modification history [(#9101)](https://github.com/prowler-cloud/prowler/pull/9101)
|
||||
- Cloudflare `--account-id` filter argument [(#9894)](https://github.com/prowler-cloud/prowler/pull/9894)
|
||||
- `rds_instance_extended_support` check for AWS provider [(#9865)](https://github.com/prowler-cloud/prowler/pull/9865)
|
||||
- `entra_conditional_access_policy_enforce_sign_in_frequency` check for M365 provider [(#9915)](https://github.com/prowler-cloud/prowler/pull/9915)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -307,6 +307,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
@@ -1102,10 +1103,11 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"app_minimum_tls_version_12",
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_app",
|
||||
"entra_non_privileged_user_has_mfa entra_privileged_user_has_mfa",
|
||||
"entra_user_with_vm_access_has_mfa",
|
||||
"app_minimum_tls_version_12",
|
||||
"sqlserver_tde_encryption_enabled",
|
||||
"storage_ensure_encryption_with_customer_managed_keys",
|
||||
"storage_infrastructure_encryption_is_enabled"
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"Description": "Adversaries may obtain and abuse credentials of existing accounts as a means of gaining Initial Access, Persistence, Privilege Escalation, or Defense Evasion. Compromised credentials may be used to bypass access controls placed on various resources on systems within the network and may even be used for persistent access to remote systems and externally available services, such as VPNs, Outlook Web Access, network devices, and remote desktop.[1] Compromised credentials may also grant an adversary increased privilege to specific systems or access to restricted areas of the network. Adversaries may choose not to use malware or tools in conjunction with the legitimate access those credentials provide to make it harder to detect their presence.",
|
||||
"TechniqueURL": "https://attack.mitre.org/techniques/T1078/",
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
@@ -804,6 +805,7 @@
|
||||
"Description": "Adversaries may modify authentication mechanisms and processes to access user credentials or enable otherwise unwarranted access to accounts. The authentication process is handled by mechanisms, such as the Local Security Authentication Server (LSASS) process and the Security Accounts Manager (SAM) on Windows, pluggable authentication modules (PAM) on Unix-based systems, and authorization plugins on MacOS systems, responsible for gathering, storing, and validating credentials. By modifying an authentication process, an adversary may be able to authenticate to a service or system without using Valid Accounts.",
|
||||
"TechniqueURL": "https://attack.mitre.org/techniques/T1556/",
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
@@ -995,6 +997,7 @@
|
||||
"Description": "Adversaries may use alternate authentication material, such as password hashes, Kerberos tickets, and application access tokens, in order to move laterally within an environment and bypass normal system access controls.",
|
||||
"TechniqueURL": "https://attack.mitre.org/techniques/T1550/",
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_policy_default_users_cannot_create_security_groups",
|
||||
@@ -1187,6 +1190,7 @@
|
||||
"Description": "Adversaries may forge credential materials that can be used to gain access to web applications or Internet services. Web applications and services (hosted in cloud SaaS environments or on-premise servers) often use session cookies, tokens, or other materials to authenticate and authorize user access.",
|
||||
"TechniqueURL": "https://attack.mitre.org/techniques/T1606/",
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_policy_default_users_cannot_create_security_groups",
|
||||
"entra_policy_ensure_default_user_cannot_create_apps",
|
||||
"entra_policy_ensure_default_user_cannot_create_tenants",
|
||||
|
||||
@@ -1603,6 +1603,7 @@
|
||||
"Id": "11.3.2.a",
|
||||
"Description": "establish strong identification, authentication such as multi-factor authentication, and authorisation procedures for privileged accounts and system administration accounts;",
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
@@ -1690,10 +1691,11 @@
|
||||
"Id": "11.4.2.c",
|
||||
"Description": "protect access to system administration systems through authentication and encryption.",
|
||||
"Checks": [
|
||||
"entra_trusted_named_locations_exists",
|
||||
"entra_user_with_vm_access_has_mfa",
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_privileged_user_has_mfa"
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_trusted_named_locations_exists",
|
||||
"entra_user_with_vm_access_has_mfa"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -1762,6 +1764,7 @@
|
||||
"Id": "11.6.2.a",
|
||||
"Description": "ensure the strength of authentication is appropriate to the classification of the asset to be accessed;",
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
@@ -1794,6 +1797,7 @@
|
||||
"Id": "11.7.2",
|
||||
"Description": "The relevant entities shall ensure that the strength of authentication is appropriate for the classification of the asset to be accessed.",
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"Id": "1.1.3",
|
||||
"Description": "Ensure Multi-factor Authentication is Required for Windows Azure Service Management API",
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api"
|
||||
],
|
||||
"Attributes": [
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
@@ -240,6 +241,7 @@
|
||||
"aks_clusters_public_access_disabled",
|
||||
"app_function_not_publicly_accessible",
|
||||
"containerregistry_not_publicly_accessible",
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"network_public_ip_shodan",
|
||||
"storage_blob_public_access_level_is_disabled"
|
||||
]
|
||||
@@ -280,6 +282,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_user_with_vm_access_has_mfa"
|
||||
@@ -298,14 +301,15 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"app_minimum_tls_version_12",
|
||||
"entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"mysql_flexible_server_minimum_tls_version_12",
|
||||
"mysql_flexible_server_ssl_connection_enabled",
|
||||
"network_http_internet_access_restricted",
|
||||
"network_rdp_internet_access_restricted",
|
||||
"network_ssh_internet_access_restricted",
|
||||
"network_udp_internet_access_restricted",
|
||||
"mysql_flexible_server_ssl_connection_enabled",
|
||||
"postgresql_flexible_server_enforce_ssl_enabled",
|
||||
"app_minimum_tls_version_12",
|
||||
"mysql_flexible_server_minimum_tls_version_12",
|
||||
"sqlserver_recommended_minimal_tls_version",
|
||||
"storage_ensure_minimum_tls_version_12"
|
||||
]
|
||||
|
||||
@@ -365,6 +365,43 @@ class Entra(AzureService):
|
||||
else:
|
||||
block_access_controls.append(str(access_control))
|
||||
|
||||
# Extract session controls (sign-in frequency)
|
||||
sign_in_frequency = None
|
||||
session_controls = getattr(policy, "session_controls", None)
|
||||
if session_controls:
|
||||
sif = getattr(session_controls, "sign_in_frequency", None)
|
||||
if sif:
|
||||
sign_in_frequency = SignInFrequencySessionControl(
|
||||
is_enabled=getattr(sif, "is_enabled", False),
|
||||
frequency_interval=(
|
||||
str(getattr(sif, "frequency_interval", None))
|
||||
if getattr(sif, "frequency_interval", None)
|
||||
else None
|
||||
),
|
||||
type=(
|
||||
str(getattr(sif, "type", None))
|
||||
if getattr(sif, "type", None)
|
||||
else None
|
||||
),
|
||||
value=getattr(sif, "value", None),
|
||||
)
|
||||
|
||||
# Extract device filter
|
||||
device_filter = None
|
||||
if conditions:
|
||||
devices = getattr(conditions, "devices", None)
|
||||
if devices:
|
||||
df = getattr(devices, "device_filter", None)
|
||||
if df:
|
||||
device_filter = DeviceFilter(
|
||||
mode=(
|
||||
str(getattr(df, "mode", None))
|
||||
if getattr(df, "mode", None)
|
||||
else None
|
||||
),
|
||||
rule=getattr(df, "rule", None),
|
||||
)
|
||||
|
||||
conditional_access_policy[tenant].update(
|
||||
{
|
||||
policy.id: ConditionalAccessPolicy(
|
||||
@@ -391,6 +428,8 @@ class Entra(AzureService):
|
||||
"grant": grant_access_controls,
|
||||
"block": block_access_controls,
|
||||
},
|
||||
sign_in_frequency=sign_in_frequency,
|
||||
device_filter=device_filter,
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -457,10 +496,30 @@ class DirectoryRole(BaseModel):
|
||||
members: List[User]
|
||||
|
||||
|
||||
class SignInFrequencySessionControl(BaseModel):
|
||||
"""Sign-in frequency session control settings."""
|
||||
|
||||
is_enabled: bool = False
|
||||
frequency_interval: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
value: Optional[int] = None
|
||||
|
||||
|
||||
class DeviceFilter(BaseModel):
|
||||
"""Device filter for conditional access policies."""
|
||||
|
||||
mode: Optional[str] = None
|
||||
rule: Optional[str] = None
|
||||
|
||||
|
||||
class ConditionalAccessPolicy(BaseModel):
|
||||
"""Conditional Access Policy model."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
state: str
|
||||
users: dict[str, List[str]]
|
||||
target_resources: dict[str, List[str]]
|
||||
access_controls: dict[str, List[str]]
|
||||
sign_in_frequency: Optional[SignInFrequencySessionControl] = None
|
||||
device_filter: Optional[DeviceFilter] = None
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_conditional_access_policy_enforce_sign_in_frequency",
|
||||
"CheckTitle": "Ensure that Conditional Access policy enforces sign-in frequency for non-corporate devices",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Conditional Access Policy",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "This check verifies that at least one Conditional Access policy enforces sign-in frequency controls for non-corporate devices. Sign-in frequency controls require users to reauthenticate at regular intervals when accessing resources from unmanaged devices, limiting the risk of persistent sessions on untrusted endpoints.",
|
||||
"Risk": "Without sign-in frequency controls for non-corporate devices:\n\n- **Session hijacking** - Long-lived sessions on unmanaged devices increase exposure to credential theft\n- **Unauthorized access** - Shared or compromised devices may retain access indefinitely\n- **Data leakage** - Sensitive resources remain accessible without periodic verification",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-session-lifetime"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to **Microsoft Entra admin center** > **Protection** > **Conditional Access**\n2. Click **+ New policy**\n3. Under **Users**, select **All users**\n4. Under **Target resources**, select **All cloud apps**\n5. Under **Conditions** > **Filter for devices**, configure to target non-corporate devices:\n - Mode: **Include filtered devices in policy**\n - Rule: `device.trustType -ne \"ServerAD\" -or device.isCompliant -ne True`\n6. Under **Session** > **Sign-in frequency**, enable and set to desired interval (e.g., 1 hour)\n7. Enable the policy",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Configure Conditional Access policies to enforce sign-in frequency for sessions originating from non-corporate devices. This limits persistent access from unmanaged endpoints and requires users to periodically reauthenticate, reducing the risk of unauthorized access through shared or compromised devices.",
|
||||
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_enforce_sign_in_frequency"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. The device filter targets devices that are not Hybrid Azure AD joined or not compliant with Intune policies."
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicyState,
|
||||
DeviceFilter,
|
||||
DeviceFilterMode,
|
||||
SignInFrequencyInterval,
|
||||
)
|
||||
|
||||
|
||||
class entra_conditional_access_policy_enforce_sign_in_frequency(Check):
|
||||
"""Check if at least one Conditional Access policy enforces sign-in frequency for non-corporate devices.
|
||||
|
||||
This check evaluates whether the tenant has a Conditional Access policy that:
|
||||
- Is enabled
|
||||
- Targets all users and all applications
|
||||
- Has sign-in frequency enabled with time-based interval
|
||||
- Targets non-corporate devices via device filter
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""Execute the check logic.
|
||||
|
||||
Returns:
|
||||
A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Conditional Access Policies",
|
||||
resource_id="conditionalAccessPolicies",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "No Conditional Access Policy enforces sign-in frequency for non-corporate devices."
|
||||
|
||||
for policy in entra_client.conditional_access_policies.values():
|
||||
if policy.state == ConditionalAccessPolicyState.DISABLED:
|
||||
continue
|
||||
|
||||
if not policy.session_controls.sign_in_frequency.is_enabled:
|
||||
continue
|
||||
|
||||
if (
|
||||
policy.session_controls.sign_in_frequency.interval
|
||||
!= SignInFrequencyInterval.TIME_BASED
|
||||
):
|
||||
continue
|
||||
|
||||
if "All" not in policy.conditions.user_conditions.included_users:
|
||||
continue
|
||||
|
||||
if (
|
||||
"All"
|
||||
not in policy.conditions.application_conditions.included_applications
|
||||
):
|
||||
continue
|
||||
|
||||
if not self._targets_non_corporate_devices(policy.conditions.device_filter):
|
||||
continue
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.display_name,
|
||||
resource_id=policy.id,
|
||||
)
|
||||
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is configured to enforce sign-in frequency for non-corporate devices but is in report-only mode."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' enforces sign-in frequency for non-corporate devices."
|
||||
break
|
||||
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
def _targets_non_corporate_devices(self, device_filter: DeviceFilter) -> bool:
|
||||
"""Check if the device filter targets non-corporate devices.
|
||||
|
||||
Non-corporate devices are identified by targeting devices that are:
|
||||
- Not Hybrid Azure AD joined (device.trustType != "ServerAD")
|
||||
- Not compliant (device.isCompliant != True)
|
||||
|
||||
Args:
|
||||
device_filter: The device filter from the conditional access policy.
|
||||
|
||||
Returns:
|
||||
True if the filter targets non-corporate devices, False otherwise.
|
||||
"""
|
||||
if not device_filter or not device_filter.rule:
|
||||
return False
|
||||
|
||||
rule = device_filter.rule.lower()
|
||||
mode = device_filter.mode
|
||||
|
||||
# Include mode: should target non-corporate devices directly
|
||||
if mode == DeviceFilterMode.INCLUDE:
|
||||
targets_non_compliant = (
|
||||
"device.iscompliant" in rule and "-ne" in rule and "true" in rule
|
||||
)
|
||||
targets_non_hybrid = (
|
||||
"device.trusttype" in rule and "-ne" in rule and "serverad" in rule
|
||||
)
|
||||
if targets_non_compliant or targets_non_hybrid:
|
||||
return True
|
||||
|
||||
# Exclude mode: should exclude corporate devices (equivalent to targeting non-corporate)
|
||||
elif mode == DeviceFilterMode.EXCLUDE:
|
||||
excludes_compliant = (
|
||||
"device.iscompliant" in rule and "-eq" in rule and "true" in rule
|
||||
)
|
||||
excludes_hybrid = (
|
||||
"device.trusttype" in rule and "-eq" in rule and "serverad" in rule
|
||||
)
|
||||
if excludes_compliant or excludes_hybrid:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -235,6 +235,28 @@ class Entra(M365Service):
|
||||
[],
|
||||
)
|
||||
],
|
||||
device_filter=(
|
||||
DeviceFilter(
|
||||
mode=(
|
||||
DeviceFilterMode(
|
||||
policy.conditions.devices.device_filter.mode.value.lower()
|
||||
)
|
||||
if policy.conditions.devices
|
||||
and policy.conditions.devices.device_filter
|
||||
and policy.conditions.devices.device_filter.mode
|
||||
else None
|
||||
),
|
||||
rule=(
|
||||
policy.conditions.devices.device_filter.rule
|
||||
if policy.conditions.devices
|
||||
and policy.conditions.devices.device_filter
|
||||
else None
|
||||
),
|
||||
)
|
||||
if policy.conditions.devices
|
||||
and policy.conditions.devices.device_filter
|
||||
else None
|
||||
),
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=(
|
||||
@@ -503,12 +525,23 @@ class ClientAppType(Enum):
|
||||
OTHER_CLIENTS = "other"
|
||||
|
||||
|
||||
class DeviceFilterMode(Enum):
|
||||
INCLUDE = "include"
|
||||
EXCLUDE = "exclude"
|
||||
|
||||
|
||||
class DeviceFilter(BaseModel):
|
||||
mode: Optional[DeviceFilterMode]
|
||||
rule: Optional[str]
|
||||
|
||||
|
||||
class Conditions(BaseModel):
|
||||
application_conditions: Optional[ApplicationsConditions]
|
||||
user_conditions: Optional[UsersConditions]
|
||||
client_app_types: Optional[List[ClientAppType]]
|
||||
user_risk_levels: List[RiskLevel] = []
|
||||
sign_in_risk_levels: List[RiskLevel] = []
|
||||
device_filter: Optional[DeviceFilter] = None
|
||||
|
||||
|
||||
class PersistentBrowser(BaseModel):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user