feat(guardduty): assess unified Runtime Monitoring and AI Protection (#12564)

This commit is contained in:
Jonathan Nguyen
2026-09-01 13:18:57 +02:00
committed by GitHub
parent fb7064401b
commit b6a8af3c54
15 changed files with 884 additions and 7 deletions
@@ -0,0 +1 @@
`guardduty_ai_protection_enabled` check for AWS provider, flagging GuardDuty detectors without AI Protection, which analyzes AWS CloudTrail data events from Amazon Bedrock, Amazon Bedrock AgentCore and Amazon SageMaker AI; a detector that does not report the feature is `MANUAL` rather than `FAIL`
@@ -0,0 +1 @@
`guardduty_runtime_monitoring_enabled` check for AWS provider, flagging GuardDuty detectors without unified Runtime Monitoring, the only feature that covers Amazon EC2 instances and Amazon ECS on AWS Fargate tasks in addition to Amazon EKS
@@ -0,0 +1 @@
`guardduty_eks_runtime_monitoring_enabled` no longer reports `FAIL` for detectors that use unified Runtime Monitoring; the GuardDuty service now reads the `RUNTIME_MONITORING` feature, which is mutually exclusive with `EKS_RUNTIME_MONITORING` and already covers Amazon EKS
@@ -878,9 +878,11 @@
"guardduty_s3_protection_enabled",
"guardduty_eks_audit_log_enabled",
"guardduty_eks_runtime_monitoring_enabled",
"guardduty_runtime_monitoring_enabled",
"guardduty_lambda_protection_enabled",
"guardduty_rds_protection_enabled",
"guardduty_ec2_malware_protection_enabled"
"guardduty_ec2_malware_protection_enabled",
"guardduty_ai_protection_enabled"
],
"ConfigRequirements": [
{
@@ -0,0 +1,41 @@
{
"Provider": "aws",
"CheckID": "guardduty_ai_protection_enabled",
"CheckTitle": "GuardDuty detector has AI Protection enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis",
"TTPs/Credential Access",
"Effects/Resource Consumption"
],
"ServiceName": "guardduty",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AwsGuardDutyDetector",
"ResourceGroup": "security",
"Description": "Active **Amazon GuardDuty detectors** are assessed for **AI Protection** being enabled, which analyzes AWS CloudTrail data events from Amazon Bedrock, Amazon Bedrock AgentCore and Amazon SageMaker AI to flag anomalous model invocations, cost harvesting and prompt injection. Detectors that do not report the feature return `MANUAL`, because absence means the Region does not offer it.",
"Risk": "Without **AI Protection**, model invocation activity is never baselined, so attackers using **stolen credentials** can invoke foundation models undetected.\n\nThat costs **confidentiality** of prompts and outputs, and **availability** too: expensive prompts harvest inference spend and exhaust quota.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/guardduty/latest/ug/ai-protection.html",
"https://docs.aws.amazon.com/guardduty/latest/ug/ai-protection-enable-standalone-account.html",
"https://docs.aws.amazon.com/guardduty/latest/ug/findings-ai-protection.html",
"https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_regions.html"
],
"Remediation": {
"Code": {
"CLI": "aws guardduty update-detector --detector-id <detector-id> --features Name=AI_PROTECTION,Status=ENABLED",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::GuardDuty::Detector\n Properties:\n Enable: true\n Features:\n - Name: AI_PROTECTION # Critical: selects the GuardDuty AI Protection plan\n Status: ENABLED # Critical: turns AI Protection on\n```",
"Other": "1. Open the AWS Console and go to Amazon GuardDuty\n2. In the Region selector, choose a Region that offers AI Protection\n3. In the navigation pane, choose Protection plans\n4. Choose Configure all enablements, then under AI Protection choose Enable\n5. Choose Save all, then Confirm and save\n6. In an organization, do this from the delegated GuardDuty administrator account and auto-enable it for new accounts",
"Terraform": "```hcl\nresource \"aws_guardduty_detector_feature\" \"<example_resource_name>\" {\n detector_id = aws_guardduty_detector.<example_detector_name>.id\n name = \"AI_PROTECTION\" # Critical: GuardDuty AI Protection plan\n status = \"ENABLED\" # Critical: enable the feature\n}\n```"
},
"Recommendation": {
"Text": "Enable **GuardDuty AI Protection** in every account and Region hosting AI workloads.\n- Enable it org-wide from the delegated GuardDuty administrator and auto-enable it for new accounts\n- Enforce **Amazon Bedrock Guardrails** for prompt attacks, which AI Protection requires to raise prompt injection findings\n- Route findings to AWS Security Hub and review them on a defined cadence",
"Url": "https://hub.prowler.com/check/guardduty_ai_protection_enabled"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,59 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.guardduty.guardduty_client import guardduty_client
class guardduty_ai_protection_enabled(Check):
"""Ensure GuardDuty AI Protection is enabled on every active detector.
AI Protection analyzes AWS CloudTrail data events from Amazon Bedrock, Amazon
Bedrock AgentCore and Amazon SageMaker AI, which makes it the detective control
for AI workloads.
The feature has three observable states rather than two:
1. Reported as ENABLED: PASS.
2. Reported as DISABLED: FAIL.
3. Not reported at all: MANUAL. GuardDuty omits features that the Region or the
GuardDuty version does not offer, and "could not tell" is not "not
compliant". The same account can carry a feature in some Regions and omit it
in others, so absence cannot be read as disablement.
A suspended detector, or one whose GetDetector call failed, is MANUAL as well:
the feature state is unknown either way, and guardduty_is_enabled owns the
detector-level finding. Regions with no detector at all are left to
guardduty_is_enabled entirely.
"""
def execute(self) -> list[Check_Report_AWS]:
"""Assess AI Protection on every GuardDuty detector in the account.
Returns:
list[Check_Report_AWS]: one report per detector that exists. PASS when AI
Protection is enabled, FAIL when GuardDuty reported the feature
disabled, and MANUAL when either the detector state or the feature
itself was not reported.
"""
findings = []
for detector in guardduty_client.detectors:
if not detector.enabled_in_account:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=detector)
if not detector.status:
report.status = "MANUAL"
report.status_extended = f"GuardDuty detector {detector.id} is not enabled or could not be read, so AI Protection coverage could not be determined."
elif detector.ai_protection is None:
report.status = "MANUAL"
report.status_extended = f"GuardDuty detector {detector.id} does not report the AI Protection feature, so verify manually whether AI Protection is available in region {detector.region}."
elif detector.ai_protection:
report.status = "PASS"
report.status_extended = (
f"GuardDuty detector {detector.id} has AI Protection enabled."
)
else:
report.status = "FAIL"
report.status_extended = f"GuardDuty detector {detector.id} does not have AI Protection enabled."
findings.append(report)
return findings
@@ -22,10 +22,10 @@
],
"Remediation": {
"Code": {
"CLI": "aws guardduty update-detector --detector-id <detector-id> --features name=EKS_RUNTIME_MONITORING,status=ENABLED",
"CLI": "aws guardduty update-detector --detector-id <detector-id> --features Name=EKS_RUNTIME_MONITORING,Status=ENABLED",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::GuardDuty::Detector\n Properties:\n Enable: true\n Features:\n - Name: EKS_RUNTIME_MONITORING # Critical: selects EKS Runtime Monitoring feature\n Status: ENABLED # Critical: enables the feature to pass the check\n```",
"Other": "1. Open the AWS Console and go to Amazon GuardDuty\n2. In the left pane, select Settings > Runtime monitoring\n3. Under EKS Runtime Monitoring, switch the status to Enabled\n4. Click Save changes",
"Terraform": "```hcl\nresource \"aws_guardduty_detector\" \"<example_resource_name>\" {\n enable = true\n\n features {\n name = \"EKS_RUNTIME_MONITORING\" # Critical: selects EKS Runtime Monitoring feature\n status = \"ENABLED\" # Critical: enables the feature to pass the check\n }\n}\n```"
"Terraform": "```hcl\nresource \"aws_guardduty_detector\" \"<example_resource_name>\" {\n enable = true\n}\n\nresource \"aws_guardduty_detector_feature\" \"<example_resource_name>\" {\n detector_id = aws_guardduty_detector.<example_resource_name>.id\n name = \"EKS_RUNTIME_MONITORING\" # Critical: selects EKS Runtime Monitoring\n status = \"ENABLED\" # Critical: enables the feature\n\n additional_configuration {\n name = \"EKS_ADDON_MANAGEMENT\"\n status = \"ENABLED\"\n }\n}\n```"
},
"Recommendation": {
"Text": "- Enable **EKS Runtime Monitoring** with automated agent management across all accounts and clusters\n- Enforce **least privilege** for agents and segment cluster access\n- Integrate findings with response workflows and periodically verify runtime coverage",
@@ -0,0 +1,40 @@
{
"Provider": "aws",
"CheckID": "guardduty_runtime_monitoring_enabled",
"CheckTitle": "GuardDuty detector has Runtime Monitoring enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "guardduty",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AwsGuardDutyDetector",
"ResourceGroup": "security",
"Description": "GuardDuty detectors are evaluated for unified **Runtime Monitoring** being enabled. The configuration is at the detector level and relates to visibility into *process execution, file access, and network connections* on Amazon EC2 instances, Amazon ECS on AWS Fargate tasks, and Amazon EKS nodes and containers. The legacy EKS-only feature covers Amazon EKS alone and does not satisfy this check.",
"Risk": "Without **Runtime Monitoring**, on-host behavior of EC2, Fargate and EKS workloads is blind to detection. Adversaries can run malware or cryptominers, break out of containers, harvest credentials from instance metadata, tamper with workloads, or pivot to other services, degrading confidentiality, corrupting integrity, and exhausting resources (availability).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/guardduty/latest/ug/runtime-monitoring.html",
"https://docs.aws.amazon.com/guardduty/latest/ug/runtime-monitoring-configuration.html",
"https://docs.aws.amazon.com/config/latest/developerguide/guardduty-runtime-monitoring-enabled.html",
"https://docs.aws.amazon.com/securityhub/latest/userguide/guardduty-controls.html#guardduty-11"
],
"Remediation": {
"Code": {
"CLI": "aws guardduty update-detector --detector-id <detector-id> --features Name=RUNTIME_MONITORING,Status=ENABLED",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::GuardDuty::Detector\n Properties:\n Enable: true\n Features:\n - Name: RUNTIME_MONITORING # Critical: selects unified Runtime Monitoring, which covers EC2, ECS-Fargate and EKS\n Status: ENABLED # Critical: enables the feature to pass the check\n AdditionalConfiguration:\n - Name: EC2_AGENT_MANAGEMENT\n Status: ENABLED\n - Name: ECS_FARGATE_AGENT_MANAGEMENT\n Status: ENABLED\n - Name: EKS_ADDON_MANAGEMENT\n Status: ENABLED\n```",
"Other": "1. Open the AWS Console and go to Amazon GuardDuty\n2. In the left pane, select Protection plans > Runtime Monitoring\n3. Switch Runtime Monitoring to Enabled\n4. Enable automated agent configuration for Amazon EC2, AWS Fargate (Amazon ECS only) and Amazon EKS\n5. Click Save changes\n6. If you were using EKS Runtime Monitoring, migrate to Runtime Monitoring; the two features are mutually exclusive",
"Terraform": "```hcl\nresource \"aws_guardduty_detector_feature\" \"<example_resource_name>\" {\n detector_id = aws_guardduty_detector.<example_detector_name>.id\n name = \"RUNTIME_MONITORING\" # Critical: unified feature covering EC2, ECS-Fargate and EKS\n status = \"ENABLED\" # Critical: enables the feature to pass the check\n\n additional_configuration {\n name = \"EC2_AGENT_MANAGEMENT\"\n status = \"ENABLED\"\n }\n\n additional_configuration {\n name = \"ECS_FARGATE_AGENT_MANAGEMENT\"\n status = \"ENABLED\"\n }\n\n additional_configuration {\n name = \"EKS_ADDON_MANAGEMENT\"\n status = \"ENABLED\"\n }\n}\n```"
},
"Recommendation": {
"Text": "- Enable unified **Runtime Monitoring** with automated agent management for Amazon EC2, AWS Fargate (Amazon ECS only) and Amazon EKS across all accounts\n- Migrate from EKS Runtime Monitoring, which covers Amazon EKS only and is mutually exclusive with Runtime Monitoring\n- Review runtime coverage statistics rather than treating enablement as coverage, and route findings to Security Hub or EventBridge for response",
"Url": "https://hub.prowler.com/check/guardduty_runtime_monitoring_enabled"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,75 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.guardduty.guardduty_client import guardduty_client
class guardduty_runtime_monitoring_enabled(Check):
"""Ensure GuardDuty unified Runtime Monitoring is enabled on every active detector.
Runtime Monitoring covers Amazon EC2 instances, Amazon ECS on AWS Fargate tasks
and Amazon EKS nodes and containers. Legacy EKS Runtime Monitoring covers Amazon
EKS alone, and its AdditionalConfiguration offers no EC2 or Fargate agent
management, so a detector running only the legacy feature has no runtime coverage
for EC2 or Fargate workloads and cannot PASS. The two features are mutually
exclusive at the API, so the FAIL message names the legacy case to point at
migration rather than at first-time enablement. That exclusivity is also why the
legacy verdict is reached before the unknown one: a legacy detector is precisely
the one whose GetDetector response carries no RUNTIME_MONITORING entry, so testing
for the unknown state first would report every legacy detector as undetermined.
A detector whose own state could not be read is MANUAL rather than absent from the
report. Detector.status is True only when GetDetector returned ENABLED and stays
None both for a suspended detector and for a GetDetector call that failed, so those
two cannot be told apart and neither is a definite absence of runtime coverage.
Leaving such a detector out of the findings would leave its Region with nothing to
read at all, and an unreported Region reads as a compliant one.
Regions with no detector at all are left to guardduty_is_enabled entirely, which is
also the check that owns the detector-level verdict.
guardduty_eks_runtime_monitoring_enabled remains the EKS-scoped check.
"""
def execute(self) -> list[Check_Report_AWS]:
"""Assess unified Runtime Monitoring on every GuardDuty detector in the account.
Returns:
list[Check_Report_AWS]: one report per detector that exists. PASS when
unified Runtime Monitoring is enabled, FAIL when GuardDuty reported the
feature disabled or reported only the legacy EKS one, and MANUAL when
either the detector state or the feature itself was not reported and no
legacy coverage was reported either.
"""
findings = []
for detector in guardduty_client.detectors:
if not detector.enabled_in_account:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=detector)
report.status = "FAIL"
report.status_extended = f"GuardDuty detector {detector.id} does not have Runtime Monitoring enabled."
if not detector.status:
report.status = "MANUAL"
report.status_extended = f"GuardDuty detector {detector.id} is not enabled or could not be read, so Runtime Monitoring coverage could not be determined."
elif detector.runtime_monitoring is True:
report.status = "PASS"
report.status_extended = (
f"GuardDuty detector {detector.id} has Runtime Monitoring enabled."
)
elif detector.eks_runtime_monitoring:
# Ordered ahead of the unknown branch below because a detector running the
# legacy feature is the shape that omits RUNTIME_MONITORING entirely: the
# two are mutually exclusive at the API. eks_runtime_monitoring is set by
# either feature, but the PASS branch above already took the unified case,
# so reaching here means EKS_RUNTIME_MONITORING is what enabled it. That is
# a known absence of EC2 and Fargate coverage, not an unknown one.
report.status_extended = f"GuardDuty detector {detector.id} only has the legacy EKS Runtime Monitoring enabled, leaving Amazon EC2 instances and Amazon ECS on Fargate tasks without runtime coverage."
elif detector.runtime_monitoring is None:
# GetDetector did not return RUNTIME_MONITORING at all, which is not the
# same as returning it DISABLED: a Region that does not offer the unified
# feature, or a features array that could not be read, would otherwise be
# reported as a definite FAIL. No legacy coverage was reported either, so
# nothing is known about this detector's runtime coverage.
report.status = "MANUAL"
report.status_extended = f"GuardDuty detector {detector.id} did not report the Runtime Monitoring feature, so runtime coverage could not be determined; verify manually."
findings.append(report)
return findings
@@ -63,6 +63,15 @@ class GuardDuty(AWSService):
)
def _get_detector(self, detector):
"""Read a detector's status, data sources and features.
A feature GuardDuty does not return is left as None rather than False, so a
Region that does not offer the feature stays distinguishable from one that
turned it off.
Args:
detector: Detector object to populate in place.
"""
logger.info("GuardDuty - getting detector info...")
try:
if detector.id and detector.enabled_in_account:
@@ -108,11 +117,33 @@ class GuardDuty(AWSService):
and feat.get("Status", "DISABLED") == "ENABLED"
):
detector.lambda_protection = True
elif (
feat.get("Name", "") == "EKS_RUNTIME_MONITORING"
and feat.get("Status", "DISABLED") == "ENABLED"
elif feat.get("Name", "") == "AI_PROTECTION":
# Recorded even when DISABLED, so a Region that offers AI
# Protection and turned it off stays distinguishable from one
# that never reports the feature.
detector.ai_protection = (
feat.get("Status", "DISABLED") == "ENABLED"
)
elif feat.get("Name", "") in (
"EKS_RUNTIME_MONITORING",
"RUNTIME_MONITORING",
):
detector.eks_runtime_monitoring = True
enabled = feat.get("Status", "DISABLED") == "ENABLED"
# Unified Runtime Monitoring (RUNTIME_MONITORING) already
# includes threat detection for Amazon EKS resources and is
# mutually exclusive with EKS_RUNTIME_MONITORING, so either
# feature means the detector has EKS runtime coverage.
if enabled:
detector.eks_runtime_monitoring = True
if feat.get("Name", "") == "RUNTIME_MONITORING":
# Only the unified feature covers Amazon EC2 and Amazon
# ECS on Fargate, so it is tracked separately. Recorded even
# when DISABLED, for the same reason AI_PROTECTION above is:
# a Region that offers the feature and turned it off must
# stay distinguishable from one that never reported it. A
# plain bool cannot express that, and the check would report
# a definite FAIL on a Region that has no unified feature.
detector.runtime_monitoring = enabled
except Exception as error:
logger.error(
@@ -345,8 +376,12 @@ class Detector(BaseModel):
rds_protection: bool = False
eks_audit_log_protection: bool = False
eks_runtime_monitoring: bool = False
# None when GuardDuty did not return the feature: unknown, not disabled.
runtime_monitoring: Optional[bool] = None
lambda_protection: bool = False
ec2_malware_protection: bool = False
# None when GuardDuty did not return the feature: unknown, not disabled.
ai_protection: Optional[bool] = None
# Organization configuration fields
organization_auto_enable_members: str = "NONE" # NEW, ALL, or NONE
organization_config_available: bool = False
@@ -0,0 +1,215 @@
from unittest import mock
import botocore
from boto3 import client
from moto import mock_aws
from prowler.providers.aws.services.guardduty.guardduty_service import GuardDuty
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
CHECK_CLIENT_PATH = "prowler.providers.aws.services.guardduty.guardduty_ai_protection_enabled.guardduty_ai_protection_enabled.guardduty_client"
make_api_call = botocore.client.BaseClient._make_api_call
def mock_get_detector_raises(self, operation_name, kwarg):
"""Deny GetDetector only, leaving every other GuardDuty operation intact."""
if operation_name == "GetDetector":
raise botocore.exceptions.ClientError(
{"Error": {"Code": "AccessDeniedException", "Message": "denied"}},
operation_name,
)
return make_api_call(self, operation_name, kwarg)
def _run_check(aws_provider):
"""Run the check against a GuardDuty service built from the mocked provider."""
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(CHECK_CLIENT_PATH, new=GuardDuty(aws_provider)),
):
from prowler.providers.aws.services.guardduty.guardduty_ai_protection_enabled.guardduty_ai_protection_enabled import (
guardduty_ai_protection_enabled,
)
return guardduty_ai_protection_enabled().execute()
class Test_guardduty_ai_protection_enabled:
@mock_aws
def test_no_detectors(self):
"""A Region with no detector has no resource to judge; guardduty_is_enabled owns it."""
client("guardduty", region_name=AWS_REGION_US_EAST_1)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 0
@mock_aws
def test_ai_protection_enabled(self):
"""An enabled feature PASSes and carries the detector's own resource fields."""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "AI_PROTECTION", "Status": "ENABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} has AI Protection enabled."
)
assert result[0].resource_id == response["DetectorId"]
assert result[0].region == AWS_REGION_US_EAST_1
assert (
result[0].resource_arn
== f"arn:aws:guardduty:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:detector/{response['DetectorId']}"
)
assert result[0].resource_tags == []
@mock_aws
def test_ai_protection_disabled(self):
"""A reported-disabled feature FAILs and names the detector."""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "AI_PROTECTION", "Status": "DISABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} does not have AI Protection enabled."
)
assert result[0].resource_id == response["DetectorId"]
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_ai_protection_feature_absent(self):
"""A feature GuardDuty does not report is not a feature GuardDuty turned off.
The Region or the GuardDuty version may not offer AI Protection at all.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[
{"Name": "S3_DATA_EVENTS", "Status": "ENABLED"},
{"Name": "LAMBDA_NETWORK_LOGS", "Status": "ENABLED"},
],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} does not report the AI Protection feature, so verify manually whether AI Protection is available in region {AWS_REGION_US_EAST_1}."
)
assert result[0].resource_id == response["DetectorId"]
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_no_features_reported(self):
"""An empty features array reads the same as an absent AI_PROTECTION entry."""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(Enable=True)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} does not report the AI Protection feature, so verify manually whether AI Protection is available in region {AWS_REGION_US_EAST_1}."
)
@mock_aws
def test_detector_not_enabled(self):
"""A suspended detector is MANUAL: its feature state is unknown, not absent."""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=False,
Features=[{"Name": "AI_PROTECTION", "Status": "ENABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} is not enabled or could not be read, so AI Protection coverage could not be determined."
)
@mock_aws
def test_get_detector_unreadable(self):
"""A denied GetDetector is unknown, not absent: MANUAL instead of FAIL."""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "AI_PROTECTION", "Status": "DISABLED"}],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_get_detector_raises
):
result = _run_check(aws_provider)
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} is not enabled or could not be read, so AI Protection coverage could not be determined."
)
@mock_aws
def test_real_get_detector_payload_ai_protection_disabled(self):
"""The real GetDetector payload carries AI_PROTECTION alongside AI_ANALYST.
AI_PROTECTION is absent from the pinned DetectorFeatureResult enum, so this
asserts the name still reaches the check alongside the two runtime feature names.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[
{"Name": "CLOUD_TRAIL", "Status": "ENABLED"},
{"Name": "DNS_LOGS", "Status": "ENABLED"},
{"Name": "FLOW_LOGS", "Status": "ENABLED"},
{"Name": "S3_DATA_EVENTS", "Status": "ENABLED"},
{"Name": "EKS_AUDIT_LOGS", "Status": "ENABLED"},
{"Name": "EBS_MALWARE_PROTECTION", "Status": "ENABLED"},
{"Name": "RDS_LOGIN_EVENTS", "Status": "ENABLED"},
{"Name": "AI_PROTECTION", "Status": "DISABLED"},
{"Name": "AI_ANALYST", "Status": "ENABLED"},
{"Name": "EKS_RUNTIME_MONITORING", "Status": "DISABLED"},
{"Name": "LAMBDA_NETWORK_LOGS", "Status": "ENABLED"},
{"Name": "RUNTIME_MONITORING", "Status": "DISABLED"},
],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} does not have AI Protection enabled."
)
@@ -0,0 +1,270 @@
from unittest import mock
import botocore
from boto3 import client
from moto import mock_aws
from prowler.providers.aws.services.guardduty.guardduty_service import GuardDuty
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
CHECK_CLIENT_PATH = "prowler.providers.aws.services.guardduty.guardduty_runtime_monitoring_enabled.guardduty_runtime_monitoring_enabled.guardduty_client"
make_api_call = botocore.client.BaseClient._make_api_call
def mock_get_detector_raises(self, operation_name, kwarg):
"""Deny GetDetector only, leaving every other GuardDuty operation intact."""
if operation_name == "GetDetector":
raise botocore.exceptions.ClientError(
{"Error": {"Code": "AccessDeniedException", "Message": "denied"}},
operation_name,
)
return make_api_call(self, operation_name, kwarg)
def _run_check(aws_provider):
"""Run the check against a GuardDuty service built from the mocked provider."""
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(CHECK_CLIENT_PATH, new=GuardDuty(aws_provider)),
):
from prowler.providers.aws.services.guardduty.guardduty_runtime_monitoring_enabled.guardduty_runtime_monitoring_enabled import (
guardduty_runtime_monitoring_enabled,
)
return guardduty_runtime_monitoring_enabled().execute()
class Test_guardduty_runtime_monitoring_enabled:
@mock_aws
def test_no_detectors(self):
"""A Region with no detector has no resource to judge; guardduty_is_enabled owns it."""
client("guardduty", region_name=AWS_REGION_US_EAST_1)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 0
@mock_aws
def test_detector_disabled(self):
"""A suspended detector is MANUAL, never dropped.
The detector exists, so omitting it would leave the Region unreported, which
reads as compliant -- and it is reported here with Runtime Monitoring ENABLED,
the case where the omission was hardest to notice.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=False,
Features=[{"Name": "RUNTIME_MONITORING", "Status": "ENABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} is not enabled or could not be read, so Runtime Monitoring coverage could not be determined."
)
assert result[0].resource_id == response["DetectorId"]
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_get_detector_unreadable(self):
"""A denied GetDetector is unknown, not absent: MANUAL instead of FAIL.
Detector.status cannot distinguish this from a suspended detector, which is why
both carry the same MANUAL wording rather than a definite verdict.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "RUNTIME_MONITORING", "Status": "DISABLED"}],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_get_detector_raises
):
result = _run_check(aws_provider)
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} is not enabled or could not be read, so Runtime Monitoring coverage could not be determined."
)
@mock_aws
def test_runtime_monitoring_enabled(self):
"""An enabled unified feature PASSes and carries the detector's own resource fields."""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "RUNTIME_MONITORING", "Status": "ENABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} has Runtime Monitoring enabled."
)
assert result[0].resource_id == response["DetectorId"]
assert result[0].region == AWS_REGION_US_EAST_1
assert (
result[0].resource_arn
== f"arn:aws:guardduty:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:detector/{response['DetectorId']}"
)
assert result[0].resource_tags == []
@mock_aws
def test_runtime_monitoring_disabled(self):
"""A reported-disabled unified feature FAILs and names the detector."""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "RUNTIME_MONITORING", "Status": "DISABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} does not have Runtime Monitoring enabled."
)
assert result[0].resource_id == response["DetectorId"]
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_unreported_runtime_feature_is_manual_not_fail(self):
"""A feature the detector never reports is not the same as one it reports DISABLED.
GuardDuty returns a disabled feature with Status DISABLED rather than omitting
it -- which is exactly why AI_PROTECTION is recorded even when off -- so an
omitted RUNTIME_MONITORING means the Region does not offer the unified feature,
or the features array could not be read. Neither is a definite absence of
runtime coverage.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "S3_DATA_EVENTS", "Status": "ENABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} did not report the Runtime Monitoring feature, so runtime coverage could not be determined; verify manually."
)
@mock_aws
def test_runtime_monitoring_reported_disabled_still_fails(self):
"""The complement of the unreported case: reported and DISABLED stays a FAIL.
Making the unreported case MANUAL must not soften a definite absence of coverage.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "RUNTIME_MONITORING", "Status": "DISABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} does not have Runtime Monitoring enabled."
)
@mock_aws
def test_legacy_eks_runtime_monitoring_only_fails(self):
"""The legacy EKS-only feature covers Amazon EKS and nothing else.
It must not PASS a check about Amazon EC2 and Amazon ECS on Fargate coverage.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[
{"Name": "EKS_RUNTIME_MONITORING", "Status": "ENABLED"},
{"Name": "RUNTIME_MONITORING", "Status": "DISABLED"},
],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} only has the legacy EKS Runtime Monitoring enabled, leaving Amazon EC2 instances and Amazon ECS on Fargate tasks without runtime coverage."
)
@mock_aws
def test_legacy_eks_only_without_any_unified_entry_fails(self):
"""The real legacy shape: the unified feature is absent, not reported DISABLED.
The two features are mutually exclusive at the API, so a detector on the legacy
one has no RUNTIME_MONITORING entry at all -- which is the same absence that makes
an unknown detector MANUAL. The test above supplies a DISABLED unified entry as
well, so it never reaches that ambiguity. Here the legacy verdict has to win on
ordering alone: EKS coverage is stated, so EC2 and Fargate are definitively
uncovered rather than undetermined.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "EKS_RUNTIME_MONITORING", "Status": "ENABLED"}],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} only has the legacy EKS Runtime Monitoring enabled, leaving Amazon EC2 instances and Amazon ECS on Fargate tasks without runtime coverage."
)
@mock_aws
def test_unified_enabled_with_legacy_disabled(self):
"""GetDetector returns an entry for both feature names on the same detector.
A DISABLED legacy feature must not mask the ENABLED unified one.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_US_EAST_1)
response = guardduty_client.create_detector(
Enable=True,
Features=[
{"Name": "EKS_RUNTIME_MONITORING", "Status": "DISABLED"},
{"Name": "RUNTIME_MONITORING", "Status": "ENABLED"},
],
)
result = _run_check(set_mocked_aws_provider([AWS_REGION_US_EAST_1]))
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"GuardDuty detector {response['DetectorId']} has Runtime Monitoring enabled."
)
@@ -2,6 +2,7 @@ from datetime import datetime
from unittest.mock import patch
import botocore
import pytest
from boto3 import client
from moto import mock_aws
@@ -143,6 +144,142 @@ class Test_GuardDuty_Service:
assert guardduty.detectors[0].region == AWS_REGION_EU_WEST_1
assert guardduty.detectors[0].tags == [{"test": "test"}]
@mock_aws
@pytest.mark.parametrize(
"feature_name", ["EKS_RUNTIME_MONITORING", "RUNTIME_MONITORING"]
)
def test_get_detector_eks_runtime_monitoring(self, feature_name):
"""Both feature names set eks_runtime_monitoring.
Unified Runtime Monitoring supersedes EKS Runtime Monitoring and covers EKS.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client.create_detector(
Enable=True,
Features=[{"Name": feature_name, "Status": "ENABLED"}],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
guardduty = GuardDuty(aws_provider)
assert len(guardduty.detectors) == 1
assert guardduty.detectors[0].eks_runtime_monitoring
@mock_aws
@pytest.mark.parametrize(
"feature_name", ["EKS_RUNTIME_MONITORING", "RUNTIME_MONITORING"]
)
def test_get_detector_eks_runtime_monitoring_disabled(self, feature_name):
"""Neither feature name sets eks_runtime_monitoring while it is DISABLED."""
guardduty_client = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client.create_detector(
Enable=True,
Features=[{"Name": feature_name, "Status": "DISABLED"}],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
guardduty = GuardDuty(aws_provider)
assert len(guardduty.detectors) == 1
assert not guardduty.detectors[0].eks_runtime_monitoring
@mock_aws
def test_get_detector_unified_runtime_monitoring_with_disabled_eks_feature(self):
"""GetDetector returns an entry for both feature names.
The DISABLED legacy feature must not mask the ENABLED unified one regardless of
the order they arrive in.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client.create_detector(
Enable=True,
Features=[
{"Name": "EKS_RUNTIME_MONITORING", "Status": "DISABLED"},
{"Name": "RUNTIME_MONITORING", "Status": "ENABLED"},
],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
guardduty = GuardDuty(aws_provider)
assert len(guardduty.detectors) == 1
assert guardduty.detectors[0].eks_runtime_monitoring
@mock_aws
def test_get_detector_runtime_monitoring_is_unified_only(self):
"""The legacy EKS feature must not set runtime_monitoring.
Only the unified feature covers Amazon EC2 and Amazon ECS on Fargate.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "EKS_RUNTIME_MONITORING", "Status": "ENABLED"}],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
guardduty = GuardDuty(aws_provider)
assert len(guardduty.detectors) == 1
assert guardduty.detectors[0].eks_runtime_monitoring
assert not guardduty.detectors[0].runtime_monitoring
@mock_aws
@pytest.mark.parametrize("status", ["ENABLED", "DISABLED"])
def test_get_detector_runtime_monitoring(self, status):
"""runtime_monitoring tracks the reported status of the unified feature."""
guardduty_client = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "RUNTIME_MONITORING", "Status": status}],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
guardduty = GuardDuty(aws_provider)
assert len(guardduty.detectors) == 1
assert guardduty.detectors[0].runtime_monitoring == (status == "ENABLED")
@mock_aws
@pytest.mark.parametrize(
"status, expected", [("ENABLED", True), ("DISABLED", False)]
)
def test_get_detector_ai_protection(self, status, expected):
"""ai_protection is recorded as a bool for both reported statuses."""
guardduty_client = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client.create_detector(
Enable=True,
Features=[{"Name": "AI_PROTECTION", "Status": status}],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
guardduty = GuardDuty(aws_provider)
assert len(guardduty.detectors) == 1
assert guardduty.detectors[0].ai_protection is expected
@mock_aws
def test_get_detector_ai_protection_absent_stays_none(self):
"""An AI_PROTECTION entry GuardDuty never returned must stay None.
That is what lets a check tell a Region without AI Protection apart from a
Region that offers the feature and disabled it.
"""
guardduty_client = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client.create_detector(
Enable=True,
Features=[
{"Name": "S3_DATA_EVENTS", "Status": "ENABLED"},
{"Name": "AI_ANALYST", "Status": "ENABLED"},
],
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
guardduty = GuardDuty(aws_provider)
assert len(guardduty.detectors) == 1
assert guardduty.detectors[0].ai_protection is None
@mock_aws
# Test GuardDuty session
def test_list_findings(self):