mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(azure/defender): add new check defender_attack_path_notifications_properly_configured (#8245)
This commit is contained in:
committed by
GitHub
parent
c4a9280ebb
commit
1211fe706e
@@ -78,6 +78,7 @@ The following list includes all the Azure checks with configurable variables tha
|
||||
| `app_ensure_python_version_is_latest` | `python_latest_version` | String |
|
||||
| `app_ensure_java_version_is_latest` | `java_latest_version` | String |
|
||||
| `sqlserver_recommended_minimal_tls_version` | `recommended_minimal_tls_versions` | List of Strings |
|
||||
| `defender_attack_path_notifications_properly_configured` | `defender_attack_path_minimal_risk_level` | String |
|
||||
|
||||
|
||||
## GCP
|
||||
|
||||
@@ -11,6 +11,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `vm_linux_enforce_ssh_authentication` check for Azure provider [(#8149)](https://github.com/prowler-cloud/prowler/pull/8149)
|
||||
- `vm_ensure_using_approved_images` check for Azure provider [(#8168)](https://github.com/prowler-cloud/prowler/pull/8168)
|
||||
- `vm_scaleset_associated_load_balancer` check for Azure provider [(#8181)](https://github.com/prowler-cloud/prowler/pull/8181)
|
||||
- `defender_attack_path_notifications_properly_configured` check for Azure provider [(#8245)](https://github.com/prowler-cloud/prowler/pull/8245)
|
||||
- `entra_intune_enrollment_sign_in_frequency_every_time` check for M365 provider [(#8223)](https://github.com/prowler-cloud/prowler/pull/8223)
|
||||
- Support for remote repository scanning in IaC provider [(#8193)](https://github.com/prowler-cloud/prowler/pull/8193)
|
||||
- Add `test_connection` method to GitHub provider [(#8248)](https://github.com/prowler-cloud/prowler/pull/8248)
|
||||
|
||||
@@ -430,6 +430,10 @@ azure:
|
||||
# TODO: create common config
|
||||
shodan_api_key: null
|
||||
|
||||
# Configurable minimal risk level for attack path notifications
|
||||
# azure.defender_attack_path_notifications_properly_configured
|
||||
defender_attack_path_minimal_risk_level: "High"
|
||||
|
||||
# Azure App Service
|
||||
# azure.app_ensure_php_version_is_latest
|
||||
php_latest_version: "8.2"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "defender_attack_path_notifications_properly_configured",
|
||||
"CheckTitle": "Ensure that email notifications for attack paths are enabled with minimal risk level",
|
||||
"CheckType": [],
|
||||
"ServiceName": "defender",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AzureEmailNotifications",
|
||||
"Description": "Ensure that Microsoft Defender for Cloud is configured to send email notifications for attack paths identified in the Azure subscription with an appropriate minimal risk level.",
|
||||
"Risk": "If attack path notifications are not enabled, security teams may not be promptly informed about exploitable attack sequences, increasing the risk of delayed mitigation and potential breaches.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable attack path email notifications in Microsoft Defender for Cloud to ensure that security teams are notified when potential attack paths are identified. Configure the minimal risk level as appropriate for your organization.",
|
||||
"Url": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.defender.defender_client import defender_client
|
||||
|
||||
|
||||
class defender_attack_path_notifications_properly_configured(Check):
|
||||
"""
|
||||
Ensure that email notifications for attack paths are enabled.
|
||||
|
||||
This check evaluates whether Microsoft Defender for Cloud is configured to send email notifications for attack paths in each Azure subscription.
|
||||
- PASS: Notifications are enabled for attack paths with a risk level set (not None) and equal or higher than the configured minimum.
|
||||
- FAIL: Notifications are not enabled for attack paths in the subscription or the risk level is too low.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[Check_Report_Azure]:
|
||||
findings = []
|
||||
|
||||
# Get the minimal risk level from config, default to 'High'
|
||||
risk_levels = ["Low", "Medium", "High", "Critical"]
|
||||
min_risk_level = defender_client.audit_config.get(
|
||||
"defender_attack_path_minimal_risk_level", "High"
|
||||
)
|
||||
if min_risk_level not in risk_levels:
|
||||
min_risk_level = "High"
|
||||
min_risk_index = risk_levels.index(min_risk_level)
|
||||
|
||||
for (
|
||||
subscription_name,
|
||||
security_contact_configurations,
|
||||
) in defender_client.security_contact_configurations.items():
|
||||
for contact_configuration in security_contact_configurations.values():
|
||||
report = Check_Report_Azure(
|
||||
metadata=self.metadata(), resource=contact_configuration
|
||||
)
|
||||
report.subscription = subscription_name
|
||||
actual_risk_level = getattr(
|
||||
contact_configuration, "attack_path_minimal_risk_level", None
|
||||
)
|
||||
if not actual_risk_level or actual_risk_level not in risk_levels:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Attack path notifications are not enabled in subscription {subscription_name} for security contact {contact_configuration.name}."
|
||||
else:
|
||||
actual_risk_index = risk_levels.index(actual_risk_level)
|
||||
if actual_risk_index <= min_risk_index:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -321,6 +321,7 @@ config_azure = {
|
||||
"python_latest_version": "3.12",
|
||||
"java_latest_version": "17",
|
||||
"recommended_minimal_tls_versions": ["1.2", "1.3"],
|
||||
"defender_attack_path_minimal_risk_level": "High",
|
||||
}
|
||||
|
||||
config_gcp = {"shodan_api_key": None, "max_unused_account_days": 30}
|
||||
|
||||
@@ -376,6 +376,8 @@ azure:
|
||||
# azure.network_public_ip_shodan
|
||||
# TODO: create common config
|
||||
shodan_api_key: null
|
||||
# Configurable minimal risk level for attack path notifications
|
||||
defender_attack_path_minimal_risk_level: "High"
|
||||
|
||||
# Azure App Service
|
||||
# azure.app_ensure_php_version_is_latest
|
||||
|
||||
@@ -85,6 +85,7 @@ class TestAzureProvider:
|
||||
"python_latest_version": "3.12",
|
||||
"java_latest_version": "17",
|
||||
"recommended_minimal_tls_versions": ["1.2", "1.3"],
|
||||
"defender_attack_path_minimal_risk_level": "High",
|
||||
}
|
||||
|
||||
def test_azure_provider_not_auth_methods(self):
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.azure.services.defender.defender_service import (
|
||||
NotificationsByRole,
|
||||
SecurityContactConfiguration,
|
||||
)
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_defender_attack_path_notifications_properly_configured:
|
||||
def test_no_subscriptions(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.security_contact_configurations = {}
|
||||
defender_client.audit_config = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
|
||||
defender_attack_path_notifications_properly_configured,
|
||||
)
|
||||
|
||||
check = defender_attack_path_notifications_properly_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_attack_path_notifications_none(self):
|
||||
resource_id = str(uuid4())
|
||||
contact_name = "default"
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.security_contact_configurations = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContactConfiguration(
|
||||
id=resource_id,
|
||||
name=contact_name,
|
||||
enabled=True,
|
||||
emails=[""],
|
||||
phone="",
|
||||
notifications_by_role=NotificationsByRole(
|
||||
state=True, roles=["Owner"]
|
||||
),
|
||||
alert_minimal_severity="High",
|
||||
attack_path_minimal_risk_level=None,
|
||||
)
|
||||
}
|
||||
}
|
||||
defender_client.audit_config = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
|
||||
defender_attack_path_notifications_properly_configured,
|
||||
)
|
||||
|
||||
check = defender_attack_path_notifications_properly_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == (
|
||||
f"Attack path notifications are not enabled in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == contact_name
|
||||
assert result[0].resource_id == resource_id
|
||||
|
||||
def test_attack_path_notifications_custom_config(self):
|
||||
# Configured minimal risk level is Medium
|
||||
resource_id = str(uuid4())
|
||||
contact_name = "default"
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.security_contact_configurations = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContactConfiguration(
|
||||
id=resource_id,
|
||||
name=contact_name,
|
||||
enabled=True,
|
||||
emails=[""],
|
||||
phone="",
|
||||
notifications_by_role=NotificationsByRole(
|
||||
state=True, roles=["Owner"]
|
||||
),
|
||||
alert_minimal_severity="High",
|
||||
attack_path_minimal_risk_level="Medium",
|
||||
)
|
||||
}
|
||||
}
|
||||
defender_client.audit_config = {
|
||||
"defender_attack_path_minimal_risk_level": "Medium"
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
|
||||
defender_attack_path_notifications_properly_configured,
|
||||
)
|
||||
|
||||
check = defender_attack_path_notifications_properly_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == contact_name
|
||||
assert result[0].resource_id == resource_id
|
||||
|
||||
def test_attack_path_notifications_invalid_config(self):
|
||||
# Configured minimal risk level is invalid, should default to High
|
||||
resource_id = str(uuid4())
|
||||
contact_name = "default"
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.security_contact_configurations = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContactConfiguration(
|
||||
id=resource_id,
|
||||
name=contact_name,
|
||||
enabled=True,
|
||||
emails=[""],
|
||||
phone="",
|
||||
notifications_by_role=NotificationsByRole(
|
||||
state=True, roles=["Owner"]
|
||||
),
|
||||
alert_minimal_severity="High",
|
||||
attack_path_minimal_risk_level="Medium",
|
||||
)
|
||||
}
|
||||
}
|
||||
defender_client.audit_config = {
|
||||
"defender_attack_path_minimal_risk_level": "INVALID"
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
|
||||
defender_attack_path_notifications_properly_configured,
|
||||
)
|
||||
|
||||
check = defender_attack_path_notifications_properly_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == contact_name
|
||||
assert result[0].resource_id == resource_id
|
||||
|
||||
def test_attack_path_notifications_low_default_high(self):
|
||||
# Low risk level, default config (High) -> PASS
|
||||
resource_id = str(uuid4())
|
||||
contact_name = "default"
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.security_contact_configurations = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContactConfiguration(
|
||||
id=resource_id,
|
||||
name=contact_name,
|
||||
enabled=True,
|
||||
emails=[""],
|
||||
phone="",
|
||||
notifications_by_role=NotificationsByRole(
|
||||
state=True, roles=["Owner"]
|
||||
),
|
||||
alert_minimal_severity="High",
|
||||
attack_path_minimal_risk_level="Low",
|
||||
)
|
||||
}
|
||||
}
|
||||
defender_client.audit_config = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
|
||||
defender_attack_path_notifications_properly_configured,
|
||||
)
|
||||
|
||||
check = defender_attack_path_notifications_properly_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"Attack path notifications are enabled with minimal risk level Low in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == contact_name
|
||||
assert result[0].resource_id == resource_id
|
||||
|
||||
def test_attack_path_notifications_medium_default_high(self):
|
||||
# Medium risk level, default config (High) -> PASS
|
||||
resource_id = str(uuid4())
|
||||
contact_name = "default"
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.security_contact_configurations = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContactConfiguration(
|
||||
id=resource_id,
|
||||
name=contact_name,
|
||||
enabled=True,
|
||||
emails=[""],
|
||||
phone="",
|
||||
notifications_by_role=NotificationsByRole(
|
||||
state=True, roles=["Owner"]
|
||||
),
|
||||
alert_minimal_severity="High",
|
||||
attack_path_minimal_risk_level="Medium",
|
||||
)
|
||||
}
|
||||
}
|
||||
defender_client.audit_config = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
|
||||
defender_attack_path_notifications_properly_configured,
|
||||
)
|
||||
|
||||
check = defender_attack_path_notifications_properly_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == contact_name
|
||||
assert result[0].resource_id == resource_id
|
||||
|
||||
def test_attack_path_notifications_high_default_high(self):
|
||||
# High risk level, default config (High) -> PASS
|
||||
resource_id = str(uuid4())
|
||||
contact_name = "default"
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.security_contact_configurations = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContactConfiguration(
|
||||
id=resource_id,
|
||||
name=contact_name,
|
||||
enabled=True,
|
||||
emails=[""],
|
||||
phone="",
|
||||
notifications_by_role=NotificationsByRole(
|
||||
state=True, roles=["Owner"]
|
||||
),
|
||||
alert_minimal_severity="High",
|
||||
attack_path_minimal_risk_level="High",
|
||||
)
|
||||
}
|
||||
}
|
||||
defender_client.audit_config = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
|
||||
defender_attack_path_notifications_properly_configured,
|
||||
)
|
||||
|
||||
check = defender_attack_path_notifications_properly_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"Attack path notifications are enabled with minimal risk level High in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == contact_name
|
||||
assert result[0].resource_id == resource_id
|
||||
|
||||
def test_attack_path_notifications_critical_default_high(self):
|
||||
# Critical risk level, default config (High) -> FAIL
|
||||
resource_id = str(uuid4())
|
||||
contact_name = "default"
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.security_contact_configurations = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContactConfiguration(
|
||||
id=resource_id,
|
||||
name=contact_name,
|
||||
enabled=True,
|
||||
emails=[""],
|
||||
phone="",
|
||||
notifications_by_role=NotificationsByRole(
|
||||
state=True, roles=["Owner"]
|
||||
),
|
||||
alert_minimal_severity="High",
|
||||
attack_path_minimal_risk_level="Critical",
|
||||
)
|
||||
}
|
||||
}
|
||||
defender_client.audit_config = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
|
||||
defender_attack_path_notifications_properly_configured,
|
||||
)
|
||||
|
||||
check = defender_attack_path_notifications_properly_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == (
|
||||
f"Attack path notifications are enabled with minimal risk level Critical in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == contact_name
|
||||
assert result[0].resource_id == resource_id
|
||||
Reference in New Issue
Block a user