diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 69b95a65fe..cb6e74ad2f 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -22,10 +22,9 @@ All notable changes to the **Prowler SDK** are documented in this file. - CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057) - OCI regions updater script and CI workflow [(#10020)](https://github.com/prowler-cloud/prowler/pull/10020) - `image` provider for container image scanning with Trivy integration [(#9984)](https://github.com/prowler-cloud/prowler/pull/9984) -- OpenStack compute 7 new checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944) - CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061) - ECS Exec (ECS-006) privilege escalation detection via `ecs:ExecuteCommand` + `ecs:DescribeTasks` [(#10066)](https://github.com/prowler-cloud/prowler/pull/10066) -- OpenStack network service with 6 security checks [(#9970)](https://github.com/prowler-cloud/prowler/pull/9970) +- OpenStack networking service with 7 security checks [(#9970)](https://github.com/prowler-cloud/prowler/pull/9970) - `--export-ocsf` CLI flag to upload OCSF scan results to Prowler Cloud [(#10095)](https://github.com/prowler-cloud/prowler/pull/10095) - `scan_id` field in OCSF `unmapped` output for ingestion correlation [(#10095)](https://github.com/prowler-cloud/prowler/pull/10095) - `defenderxdr_endpoint_privileged_user_exposed_credentials` check for M365 provider [(#10084)](https://github.com/prowler-cloud/prowler/pull/10084) @@ -33,11 +32,13 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_seamless_sso_disabled` check for m365 provider [(#10086)](https://github.com/prowler-cloud/prowler/pull/10086) - Registry scan mode for `image` provider: enumerate and scan all images from OCI standard, Docker Hub, and ECR [(#9985)](https://github.com/prowler-cloud/prowler/pull/9985) - Add file descriptor limits (`ulimits`) to Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107) -- Openstack block storage 7 new checks [(#10120)](https://github.com/prowler-cloud/prowler/pull/10120) - SecNumCloud compliance framework for the AWS provider [(#10117)](https://github.com/prowler-cloud/prowler/pull/10117) - CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127) - `entra_require_mfa_for_management_api` check for m365 provider [(#10150)](https://github.com/prowler-cloud/prowler/pull/10150) - OpenStack provider multiple regions support [(#10135)](https://github.com/prowler-cloud/prowler/pull/10135) +- Openstack block storage 7 new checks [(#10120)](https://github.com/prowler-cloud/prowler/pull/10120) +- OpenStack compute 7 new checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944) +- OpenStack image 6 new checks [(#10096)](https://github.com/prowler-cloud/prowler/pull/10096) ### 🔄 Changed diff --git a/prowler/providers/openstack/services/image/__init__.py b/prowler/providers/openstack/services/image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/image/image_client.py b/prowler/providers/openstack/services/image/image_client.py new file mode 100644 index 0000000000..1d7c3a3f0c --- /dev/null +++ b/prowler/providers/openstack/services/image/image_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.openstack.services.image.image_service import Image + +image_client = Image(Provider.get_global_provider()) diff --git a/prowler/providers/openstack/services/image/image_hw_mem_encryption_enabled/__init__.py b/prowler/providers/openstack/services/image/image_hw_mem_encryption_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled.metadata.json b/prowler/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled.metadata.json new file mode 100644 index 0000000000..2c9ab2bdcf --- /dev/null +++ b/prowler/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "image_hw_mem_encryption_enabled", + "CheckTitle": "Images have hardware memory encryption enabled", + "CheckType": [], + "ServiceName": "image", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OS::Glance::WebImage", + "ResourceGroup": "storage", + "Description": "**OpenStack images** are evaluated to verify that the **hw_mem_encryption property is set to True**. Hardware memory encryption (AMD SEV) protects guest memory from host-level attacks by encrypting it with a per-VM key managed by the CPU. Best practices recommend enabling memory encryption for workloads processing sensitive data in shared infrastructure.", + "Risk": "Images without memory encryption enabled may expose guest memory to host-level attacks, hypervisor vulnerabilities, or malicious co-tenants. In shared infrastructure, an attacker with hypervisor access could read sensitive data from unencrypted guest memory, including cryptographic keys, credentials, and application data.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/security-guide/image-storage/checklist.html", + "https://docs.openstack.org/nova/latest/admin/sev.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack image set --property hw_mem_encryption=True ", + "NativeIaC": "", + "Other": "**Enable memory encryption:**\n1. Verify compute hosts support AMD SEV\n2. Set image property: `openstack image set --property hw_mem_encryption=True `\n3. Ensure a flavor with SEV support is available\n4. Boot instances using the encrypted image with an SEV-enabled flavor", + "Terraform": "```hcl\nresource \"openstack_images_image_v2\" \"image\" {\n name = \"secure-image\"\n container_format = \"bare\"\n disk_format = \"qcow2\"\n visibility = \"private\"\n\n properties = {\n hw_mem_encryption = \"true\" # GOOD: Memory encryption enabled\n }\n}\n```" + }, + "Recommendation": { + "Text": "Enable hardware memory encryption on images processing sensitive data. Ensure compute hosts support AMD SEV or equivalent technology. Create dedicated SEV-enabled flavors and host aggregates. Test workload compatibility with memory encryption before production deployment.", + "Url": "https://hub.prowler.com/check/image_hw_mem_encryption_enabled" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check requires AMD SEV-capable hardware on compute nodes. Not all workloads are compatible with memory encryption. Some performance overhead is expected. The hw_mem_encryption property is a request - actual encryption depends on flavor and host capabilities." +} diff --git a/prowler/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled.py b/prowler/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled.py new file mode 100644 index 0000000000..8d39c87e5e --- /dev/null +++ b/prowler/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled.py @@ -0,0 +1,35 @@ +"""OpenStack Image Memory Encryption Check.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.image.image_client import image_client + + +class image_hw_mem_encryption_enabled(Check): + """Ensure images have hardware memory encryption enabled.""" + + def execute(self) -> List[CheckReportOpenStack]: + """Execute image_hw_mem_encryption_enabled check. + + Iterates over all images and verifies that the hw_mem_encryption + property is set to True, enabling AMD SEV guest memory encryption. + + Returns: + list[CheckReportOpenStack]: List of findings for each image. + """ + findings: List[CheckReportOpenStack] = [] + + for image in image_client.images: + report = CheckReportOpenStack(metadata=self.metadata(), resource=image) + + if image.hw_mem_encryption is True: + report.status = "PASS" + report.status_extended = f"Image {image.name} ({image.id}) has hardware memory encryption enabled." + else: + report.status = "FAIL" + report.status_extended = f"Image {image.name} ({image.id}) does not have hardware memory encryption enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/image/image_not_publicly_visible/__init__.py b/prowler/providers/openstack/services/image/image_not_publicly_visible/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible.metadata.json b/prowler/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible.metadata.json new file mode 100644 index 0000000000..73cdcf8bbe --- /dev/null +++ b/prowler/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "image_not_publicly_visible", + "CheckTitle": "Images are not publicly visible to all tenants", + "CheckType": [], + "ServiceName": "image", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "OS::Glance::WebImage", + "ResourceGroup": "storage", + "Description": "**OpenStack images** are evaluated to verify that **visibility is not set to 'public'**. Public images are accessible to all tenants in the cloud, potentially exposing operating system configurations, embedded credentials, proprietary software, and security hardening details. Best practices recommend keeping images private or shared only with specific trusted projects.", + "Risk": "Public images expose operating system configurations, embedded credentials, and proprietary software to all tenants. Attackers can analyze public images to discover vulnerabilities, extract secrets, or clone them for malicious purposes. Public images may also violate compliance requirements for data isolation between tenants.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/security-guide/image-storage/checklist.html", + "https://docs.openstack.org/glance/latest/admin/manage-images.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack image set --private ", + "NativeIaC": "", + "Other": "**Change visibility via Horizon:**\n1. Navigate to **Project > Compute > Images**\n2. Select the public image\n3. Click **Edit Image**\n4. Change Visibility to **Private**\n5. Click **Update Image**", + "Terraform": "```hcl\nresource \"openstack_images_image_v2\" \"image\" {\n name = \"app-image\"\n container_format = \"bare\"\n disk_format = \"qcow2\"\n visibility = \"private\" # GOOD: Not publicly visible\n}\n```" + }, + "Recommendation": { + "Text": "Set image visibility to 'private' or 'shared' with specific trusted projects. Regularly audit image visibility settings. Implement policies to prevent creation of public images in production environments.", + "Url": "https://hub.prowler.com/check/image_not_publicly_visible" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check flags images where visibility is set to 'public'. Images with visibility 'private', 'shared', or 'community' will pass. Community images are visible to all but require explicit opt-in and are considered acceptable in most environments." +} diff --git a/prowler/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible.py b/prowler/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible.py new file mode 100644 index 0000000000..5451e332fe --- /dev/null +++ b/prowler/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible.py @@ -0,0 +1,35 @@ +"""OpenStack Image Public Visibility Check.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.image.image_client import image_client + + +class image_not_publicly_visible(Check): + """Ensure images are not publicly visible to all tenants.""" + + def execute(self) -> List[CheckReportOpenStack]: + """Execute image_not_publicly_visible check. + + Iterates over all images and verifies that visibility is not set to + 'public', which would expose the image to all tenants. + + Returns: + list[CheckReportOpenStack]: List of findings for each image. + """ + findings: List[CheckReportOpenStack] = [] + + for image in image_client.images: + report = CheckReportOpenStack(metadata=self.metadata(), resource=image) + + if image.visibility == "public": + report.status = "FAIL" + report.status_extended = f"Image {image.name} ({image.id}) is publicly visible to all tenants." + else: + report.status = "PASS" + report.status_extended = f"Image {image.name} ({image.id}) is not publicly visible (visibility={image.visibility})." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/image/image_not_shared_with_multiple_projects/__init__.py b/prowler/providers/openstack/services/image/image_not_shared_with_multiple_projects/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects.metadata.json b/prowler/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects.metadata.json new file mode 100644 index 0000000000..ec7f7923e6 --- /dev/null +++ b/prowler/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "image_not_shared_with_multiple_projects", + "CheckTitle": "Images are not shared with an excessive number of projects", + "CheckType": [], + "ServiceName": "image", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OS::Glance::WebImage", + "ResourceGroup": "storage", + "Description": "**OpenStack images** are evaluated to verify that **shared images do not exceed a configurable member threshold** (default: 5 accepted members). Images shared with many projects amplify the blast radius of any vulnerability found in the image, as all consuming projects would be affected. Best practices recommend limiting image sharing to the minimum set of projects required.", + "Risk": "Images shared with many projects amplify the blast radius of image vulnerabilities, backdoors, or misconfigurations. If a widely shared image is compromised, all projects using it are affected. Oversharing also increases the risk of unauthorized access to proprietary software or sensitive OS configurations embedded in images.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/security-guide/image-storage/checklist.html", + "https://docs.openstack.org/glance/latest/admin/manage-images.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack image remove project ", + "NativeIaC": "", + "Other": "**Reduce image sharing:**\n1. List shared members: `openstack image member list `\n2. Review each member project for continued need\n3. Remove unnecessary members: `openstack image remove project `\n4. Consider publishing separate images per team instead of oversharing", + "Terraform": "" + }, + "Recommendation": { + "Text": "Review shared image membership regularly and remove projects that no longer need access. Consider creating separate images per team or environment instead of sharing a single image widely. The threshold is configurable via audit_config 'image_sharing_threshold' (default: 5).", + "Url": "https://hub.prowler.com/check/image_not_shared_with_multiple_projects" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check only evaluates images with visibility='shared'. Private, public, and community images are automatically passed. Only members with status='accepted' count toward the threshold. The default threshold of 5 can be customized via audit_config 'image_sharing_threshold'." +} diff --git a/prowler/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects.py b/prowler/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects.py new file mode 100644 index 0000000000..ee18e0d096 --- /dev/null +++ b/prowler/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects.py @@ -0,0 +1,51 @@ +"""OpenStack Image Sharing Scope Check.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.image.image_client import image_client + + +class image_not_shared_with_multiple_projects(Check): + """Ensure images are not shared with an excessive number of projects.""" + + def execute(self) -> List[CheckReportOpenStack]: + """Execute image_not_shared_with_multiple_projects check. + + Iterates over all images and verifies that shared images do not + exceed the accepted member threshold (default 5, configurable via + audit_config 'image_sharing_threshold'). + + Returns: + list[CheckReportOpenStack]: List of findings for each image. + """ + findings: List[CheckReportOpenStack] = [] + threshold = image_client.audit_config.get("image_sharing_threshold", 5) + + for image in image_client.images: + report = CheckReportOpenStack(metadata=self.metadata(), resource=image) + + if image.visibility != "shared": + report.status = "PASS" + report.status_extended = f"Image {image.name} ({image.id}) is not shared (visibility={image.visibility})." + else: + accepted_count = sum(1 for m in image.members if m.status == "accepted") + + if accepted_count > threshold: + report.status = "FAIL" + report.status_extended = ( + f"Image {image.name} ({image.id}) is shared with " + f"{accepted_count} accepted projects, exceeding the " + f"threshold of {threshold}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Image {image.name} ({image.id}) is shared with " + f"{accepted_count} accepted projects, within the " + f"threshold of {threshold}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/image/image_protected_status_enabled/__init__.py b/prowler/providers/openstack/services/image/image_protected_status_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled.metadata.json b/prowler/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled.metadata.json new file mode 100644 index 0000000000..bc5b0c0ab0 --- /dev/null +++ b/prowler/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "image_protected_status_enabled", + "CheckTitle": "Images have deletion protection enabled", + "CheckType": [], + "ServiceName": "image", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OS::Glance::WebImage", + "ResourceGroup": "storage", + "Description": "**OpenStack images** are evaluated to verify that the **protected flag is set to True**. Protected images cannot be deleted, preventing accidental or malicious removal of base images that dependent workloads rely on. Best practices recommend enabling deletion protection on all production images.", + "Risk": "Unprotected images can be accidentally or maliciously deleted, breaking all dependent instances and workloads. Image deletion is irreversible and can cause prolonged outages while replacement images are built and tested. In multi-tenant environments, unauthorized deletion of shared images can impact multiple projects.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/security-guide/image-storage/checklist.html", + "https://docs.openstack.org/glance/latest/admin/manage-images.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack image set --protected ", + "NativeIaC": "", + "Other": "**Enable protection via Horizon:**\n1. Navigate to **Project > Compute > Images**\n2. Select the unprotected image\n3. Click **Edit Image**\n4. Check the **Protected** checkbox\n5. Click **Update Image**", + "Terraform": "```hcl\nresource \"openstack_images_image_v2\" \"image\" {\n name = \"app-image\"\n container_format = \"bare\"\n disk_format = \"qcow2\"\n visibility = \"private\"\n protected = true # GOOD: Deletion protection enabled\n}\n```" + }, + "Recommendation": { + "Text": "Enable deletion protection on all production images to prevent accidental or malicious removal. Implement image lifecycle management procedures that require explicit unprotection before decommissioning images.", + "Url": "https://hub.prowler.com/check/image_protected_status_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check flags images where the protected flag is False. Some images may intentionally be unprotected during testing or development. Verify that production images used by running workloads are protected." +} diff --git a/prowler/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled.py b/prowler/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled.py new file mode 100644 index 0000000000..b1b5df8d49 --- /dev/null +++ b/prowler/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled.py @@ -0,0 +1,37 @@ +"""OpenStack Image Protected Status Check.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.image.image_client import image_client + + +class image_protected_status_enabled(Check): + """Ensure images have deletion protection enabled.""" + + def execute(self) -> List[CheckReportOpenStack]: + """Execute image_protected_status_enabled check. + + Iterates over all images and verifies that the protected flag is + set to True, preventing accidental or malicious deletion. + + Returns: + list[CheckReportOpenStack]: List of findings for each image. + """ + findings: List[CheckReportOpenStack] = [] + + for image in image_client.images: + report = CheckReportOpenStack(metadata=self.metadata(), resource=image) + + if image.protected: + report.status = "PASS" + report.status_extended = ( + f"Image {image.name} ({image.id}) has deletion protection enabled." + ) + else: + report.status = "FAIL" + report.status_extended = f"Image {image.name} ({image.id}) does not have deletion protection enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/image/image_secure_boot_enabled/__init__.py b/prowler/providers/openstack/services/image/image_secure_boot_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled.metadata.json b/prowler/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled.metadata.json new file mode 100644 index 0000000000..9aa05e40ee --- /dev/null +++ b/prowler/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "openstack", + "CheckID": "image_secure_boot_enabled", + "CheckTitle": "Images have Secure Boot set to required", + "CheckType": [], + "ServiceName": "image", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OS::Glance::WebImage", + "ResourceGroup": "storage", + "Description": "**OpenStack images** are evaluated to verify that the **os_secure_boot property is set to 'required'**. Secure Boot ensures that only signed and trusted bootloaders and firmware execute during the boot process, protecting against bootkits, rootkits, and firmware-level attacks. Best practices recommend requiring Secure Boot for all production workloads.", + "Risk": "Images without Secure Boot allow unauthorized bootloader and firmware modifications. Attackers can install bootkits or rootkits that persist across reboots and are invisible to operating system security tools. Without Secure Boot, compromised firmware can intercept all system operations including encryption key management.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/security-guide/image-storage/checklist.html", + "https://docs.openstack.org/nova/latest/admin/secure-boot.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack image set --property os_secure_boot=required ", + "NativeIaC": "", + "Other": "**Enable Secure Boot:**\n1. Verify the image supports UEFI boot mode\n2. Set the Secure Boot property: `openstack image set --property os_secure_boot=required `\n3. Ensure the image also has `hw_firmware_type=uefi` set\n4. Use a compatible flavor and verify boot succeeds", + "Terraform": "```hcl\nresource \"openstack_images_image_v2\" \"image\" {\n name = \"secure-image\"\n container_format = \"bare\"\n disk_format = \"qcow2\"\n visibility = \"private\"\n\n properties = {\n os_secure_boot = \"required\" # GOOD: Secure Boot required\n hw_firmware_type = \"uefi\" # Required for Secure Boot\n }\n}\n```" + }, + "Recommendation": { + "Text": "Set os_secure_boot to 'required' on all production images. Ensure images use UEFI firmware type (hw_firmware_type=uefi). Test Secure Boot compatibility before enabling in production. Use signed bootloaders and kernel images.", + "Url": "https://hub.prowler.com/check/image_secure_boot_enabled" + } + }, + "Categories": [ + "encryption", + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check requires UEFI-capable compute nodes. Only the value 'required' passes; 'optional', 'disabled', and unset values all fail. Some legacy operating systems may not support Secure Boot. The image must also have hw_firmware_type=uefi for Secure Boot to function." +} diff --git a/prowler/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled.py b/prowler/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled.py new file mode 100644 index 0000000000..874d9e08b3 --- /dev/null +++ b/prowler/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled.py @@ -0,0 +1,41 @@ +"""OpenStack Image Secure Boot Check.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.image.image_client import image_client + + +class image_secure_boot_enabled(Check): + """Ensure images have Secure Boot set to required.""" + + def execute(self) -> List[CheckReportOpenStack]: + """Execute image_secure_boot_enabled check. + + Iterates over all images and verifies that the os_secure_boot + property is set to 'required', ensuring only signed bootloaders + and firmware can execute. + + Returns: + list[CheckReportOpenStack]: List of findings for each image. + """ + findings: List[CheckReportOpenStack] = [] + + for image in image_client.images: + report = CheckReportOpenStack(metadata=self.metadata(), resource=image) + + if image.os_secure_boot == "required": + report.status = "PASS" + report.status_extended = ( + f"Image {image.name} ({image.id}) has Secure Boot set to required." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Image {image.name} ({image.id}) does not have Secure Boot " + f"set to required (os_secure_boot={image.os_secure_boot})." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/image/image_service.py b/prowler/providers/openstack/services/image/image_service.py new file mode 100644 index 0000000000..0c01c5aad5 --- /dev/null +++ b/prowler/providers/openstack/services/image/image_service.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Optional + +from openstack import exceptions as openstack_exceptions + +from prowler.lib.logger import logger +from prowler.providers.openstack.lib.service.service import OpenStackService + + +class Image(OpenStackService): + """Service wrapper using openstacksdk image (Glance) APIs.""" + + def __init__(self, provider) -> None: + super().__init__(__class__.__name__, provider) + self.images: List[ImageResource] = [] + self._list_images() + + def _list_images(self) -> None: + """List all images with their properties across all audited regions.""" + logger.info("Image - Listing images...") + for region, conn in self.regional_connections.items(): + try: + for img in conn.image.images(): + # Skip images not owned by the current project (e.g. provider public images) + owner = getattr(img, "owner_id", getattr(img, "owner", "")) + if owner != self.project_id: + continue + + # Signature properties may be direct attributes or inside a properties dict + properties = getattr(img, "properties", {}) or {} + + visibility = getattr(img, "visibility", "private") + + members = [] + if visibility == "shared": + members = self._list_image_members(conn, getattr(img, "id", "")) + + self.images.append( + ImageResource( + id=getattr(img, "id", ""), + name=getattr(img, "name", ""), + status=getattr(img, "status", ""), + visibility=visibility, + protected=getattr(img, "is_protected", False), + owner=getattr(img, "owner_id", getattr(img, "owner", "")), + img_signature=self._resolve_property( + img, "img_signature", properties + ), + img_signature_hash_method=self._resolve_property( + img, "img_signature_hash_method", properties + ), + img_signature_key_type=self._resolve_property( + img, "img_signature_key_type", properties + ), + img_signature_certificate_uuid=self._resolve_property( + img, "img_signature_certificate_uuid", properties + ), + hw_mem_encryption=self._parse_bool( + self._resolve_property( + img, "hw_mem_encryption", properties + ) + ), + os_secure_boot=self._resolve_property( + img, + "needs_secure_boot", + properties, + fallback_attr="os_secure_boot", + ), + members=members, + tags=getattr(img, "tags", []), + project_id=getattr( + img, "project_id", getattr(img, "owner", "") + ), + region=region, + ) + ) + except openstack_exceptions.SDKException as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- " + f"Failed to list images in region {region}: {error}" + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- " + f"Unexpected error listing images in region {region}: {error}" + ) + + @staticmethod + def _resolve_property( + img, + attr_name: str, + properties: dict, + fallback_attr: str = None, + ): + """Get an image attribute, falling back to properties dict only when None. + + Uses ``is not None`` instead of ``or`` so that falsy values like + ``False`` or ``""`` on the image object are preserved. + + Args: + img: The SDK image object. + attr_name: Primary SDK attribute name to check. + properties: The image properties dict for final fallback. + fallback_attr: Optional secondary attribute name to try before + falling back to properties (e.g. when the SDK exposes a + property under a different name like ``needs_secure_boot`` + vs ``os_secure_boot``). + """ + value = getattr(img, attr_name, None) + if value is not None: + return value + if fallback_attr is not None: + value = getattr(img, fallback_attr, None) + if value is not None: + return value + return properties.get(fallback_attr or attr_name) + + @staticmethod + def _parse_bool(value) -> Optional[bool]: + """Parse a boolean value that may be a string from the Glance API. + + Args: + value: A bool, string ("True"/"False"), or None. + + Returns: + True, False, or None. + """ + if value is None: + return None + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.lower() == "true" + return None + + def _list_image_members(self, conn, image_id: str) -> List[ImageMember]: + """List members (shared projects) for a specific image. + + Args: + conn: The regional OpenStack connection to use. + image_id: The image UUID to list members for. + + Returns: + List of ImageMember dataclasses. + """ + members = [] + try: + for member in conn.image.members(image_id): + members.append( + ImageMember( + member_id=getattr( + member, "member_id", getattr(member, "id", "") + ), + status=getattr(member, "status", "pending"), + ) + ) + except openstack_exceptions.SDKException as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- " + f"Failed to list members for image {image_id}: {error}" + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- " + f"Unexpected error listing members for image {image_id}: {error}" + ) + return members + + +@dataclass +class ImageMember: + """Represents a project that an image is shared with.""" + + member_id: str + status: str + + +@dataclass +class ImageResource: + """Represents an OpenStack image.""" + + id: str + name: str + status: str + visibility: str + protected: bool + owner: str + img_signature: Optional[str] + img_signature_hash_method: Optional[str] + img_signature_key_type: Optional[str] + img_signature_certificate_uuid: Optional[str] + hw_mem_encryption: Optional[bool] + os_secure_boot: Optional[str] + members: List[ImageMember] = field(default_factory=list) + tags: List[str] = field(default_factory=list) + project_id: str = "" + region: str = "" diff --git a/prowler/providers/openstack/services/image/image_signature_verification_enabled/__init__.py b/prowler/providers/openstack/services/image/image_signature_verification_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.metadata.json b/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.metadata.json new file mode 100644 index 0000000000..c9630e75aa --- /dev/null +++ b/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "openstack", + "CheckID": "image_signature_verification_enabled", + "CheckTitle": "Images have cryptographic signature verification enabled", + "CheckType": [], + "ServiceName": "image", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "OS::Glance::WebImage", + "ResourceGroup": "storage", + "Description": "**OpenStack images** are evaluated to verify that all **four signature properties** are configured: `img_signature`, `img_signature_hash_method`, `img_signature_key_type`, and `img_signature_certificate_uuid`. Signed images allow Nova to verify image integrity before booting, detecting tampering or corruption. Best practices recommend signing all production images using Barbican-managed certificates.", + "Risk": "Unsigned images can be tampered with to inject backdoors, malware, or rootkits without detection. Without signature verification, compromised storage backends or man-in-the-middle attacks can modify images between upload and boot. Nova cannot verify the integrity of unsigned images, allowing corrupted or malicious images to be launched.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/security-guide/image-storage/checklist.html", + "https://docs.openstack.org/glance/latest/user/signature.html" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "**Sign images using Barbican:**\n1. Create a signing key and certificate in Barbican\n2. Sign the image data with the private key\n3. Upload the image with signature properties:\n `openstack image create --property img_signature= --property img_signature_hash_method=SHA-256 --property img_signature_key_type=RSA-PSS --property img_signature_certificate_uuid= `\n4. Configure Nova to verify signatures: set `verify_glance_signatures = True` in nova.conf", + "Terraform": "" + }, + "Recommendation": { + "Text": "Sign all production images using Barbican-managed certificates. Enable signature verification in Nova (verify_glance_signatures=True). Implement an image signing pipeline in CI/CD. Regularly rotate signing certificates and audit image signatures.", + "Url": "https://hub.prowler.com/check/image_signature_verification_enabled" + } + }, + "Categories": [ + "encryption", + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check requires all four signature properties to be set. Partial configuration (e.g., only img_signature without the hash method or certificate UUID) is considered incomplete and will fail. Image signing requires Barbican (OpenStack Key Manager) to be deployed and configured." +} diff --git a/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.py b/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.py new file mode 100644 index 0000000000..bcdd290d63 --- /dev/null +++ b/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.py @@ -0,0 +1,45 @@ +"""OpenStack Image Signature Verification Check.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.image.image_client import image_client + + +class image_signature_verification_enabled(Check): + """Ensure images have cryptographic signature verification properties set.""" + + def execute(self) -> List[CheckReportOpenStack]: + """Execute image_signature_verification_enabled check. + + Iterates over all images and verifies that all four signature + properties are set: img_signature, img_signature_hash_method, + img_signature_key_type, and img_signature_certificate_uuid. + + Returns: + list[CheckReportOpenStack]: List of findings for each image. + """ + findings: List[CheckReportOpenStack] = [] + + for image in image_client.images: + report = CheckReportOpenStack(metadata=self.metadata(), resource=image) + + has_all_signatures = all( + [ + image.img_signature, + image.img_signature_hash_method, + image.img_signature_key_type, + image.img_signature_certificate_uuid, + ] + ) + + if has_all_signatures: + report.status = "PASS" + report.status_extended = f"Image {image.name} ({image.id}) has all signature verification properties configured." + else: + report.status = "FAIL" + report.status_extended = f"Image {image.name} ({image.id}) does not have all signature verification properties configured." + + findings.append(report) + + return findings diff --git a/tests/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled_test.py b/tests/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled_test.py new file mode 100644 index 0000000000..4fd3477e99 --- /dev/null +++ b/tests/providers/openstack/services/image/image_hw_mem_encryption_enabled/image_hw_mem_encryption_enabled_test.py @@ -0,0 +1,231 @@ +"""Tests for image_hw_mem_encryption_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.image.image_service import ImageResource +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_image_hw_mem_encryption_enabled: + def test_no_images(self): + """Test when no images exist.""" + image_client = mock.MagicMock() + image_client.images = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled import ( + image_hw_mem_encryption_enabled, + ) + + check = image_hw_mem_encryption_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_image_hw_mem_encryption_enabled(self): + """Test PASS when hw_mem_encryption is True.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-1", + name="encrypted-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=True, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled import ( + image_hw_mem_encryption_enabled, + ) + + check = image_hw_mem_encryption_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image encrypted-image (img-1) has hardware memory encryption enabled." + ) + assert result[0].resource_id == "img-1" + assert result[0].resource_name == "encrypted-image" + assert result[0].region == OPENSTACK_REGION + + def test_image_encryption_not_set(self): + """Test FAIL when hw_mem_encryption is None.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-2", + name="unencrypted-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled import ( + image_hw_mem_encryption_enabled, + ) + + check = image_hw_mem_encryption_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Image unencrypted-image (img-2) does not have hardware memory encryption enabled." + ) + + def test_image_encryption_false(self): + """Test FAIL when hw_mem_encryption is False.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-3", + name="no-encrypt-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=False, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled import ( + image_hw_mem_encryption_enabled, + ) + + check = image_hw_mem_encryption_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_multiple_images_mixed(self): + """Test mixed results with encrypted and unencrypted images.""" + image_client = mock.MagicMock() + base = dict( + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + image_client.images = [ + ImageResource( + id="img-enc", name="encrypted", hw_mem_encryption=True, **base + ), + ImageResource( + id="img-noenc", name="unencrypted", hw_mem_encryption=None, **base + ), + ImageResource( + id="img-false", name="false-enc", hw_mem_encryption=False, **base + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_hw_mem_encryption_enabled.image_hw_mem_encryption_enabled import ( + image_hw_mem_encryption_enabled, + ) + + check = image_hw_mem_encryption_enabled() + result = check.execute() + + assert len(result) == 3 + assert result[0].status == "PASS" + assert result[1].status == "FAIL" + assert result[2].status == "FAIL" diff --git a/tests/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible_test.py b/tests/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible_test.py new file mode 100644 index 0000000000..608ab67ecd --- /dev/null +++ b/tests/providers/openstack/services/image/image_not_publicly_visible/image_not_publicly_visible_test.py @@ -0,0 +1,192 @@ +"""Tests for image_not_publicly_visible check.""" + +from unittest import mock + +from prowler.providers.openstack.services.image.image_service import ImageResource +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_image_not_publicly_visible: + def test_no_images(self): + """Test when no images exist.""" + image_client = mock.MagicMock() + image_client.images = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_publicly_visible.image_not_publicly_visible.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_publicly_visible.image_not_publicly_visible import ( + image_not_publicly_visible, + ) + + check = image_not_publicly_visible() + result = check.execute() + + assert len(result) == 0 + + def test_image_private(self): + """Test PASS when image is private.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-1", + name="private-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_publicly_visible.image_not_publicly_visible.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_publicly_visible.image_not_publicly_visible import ( + image_not_publicly_visible, + ) + + check = image_not_publicly_visible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image private-image (img-1) is not publicly visible (visibility=private)." + ) + assert result[0].resource_id == "img-1" + assert result[0].resource_name == "private-image" + assert result[0].region == OPENSTACK_REGION + + def test_image_public(self): + """Test FAIL when image is public.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-2", + name="public-image", + status="active", + visibility="public", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_publicly_visible.image_not_publicly_visible.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_publicly_visible.image_not_publicly_visible import ( + image_not_publicly_visible, + ) + + check = image_not_publicly_visible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Image public-image (img-2) is publicly visible to all tenants." + ) + assert result[0].resource_id == "img-2" + assert result[0].resource_name == "public-image" + assert result[0].region == OPENSTACK_REGION + + def test_multiple_images_mixed(self): + """Test mixed results with public, private, shared, and community images.""" + image_client = mock.MagicMock() + base = dict( + status="active", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + image_client.images = [ + ImageResource(id="img-pub", name="public-img", visibility="public", **base), + ImageResource( + id="img-priv", name="private-img", visibility="private", **base + ), + ImageResource( + id="img-shared", name="shared-img", visibility="shared", **base + ), + ImageResource( + id="img-comm", name="community-img", visibility="community", **base + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_publicly_visible.image_not_publicly_visible.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_publicly_visible.image_not_publicly_visible import ( + image_not_publicly_visible, + ) + + check = image_not_publicly_visible() + result = check.execute() + + assert len(result) == 4 + assert result[0].status == "FAIL" # public + assert result[1].status == "PASS" # private + assert result[2].status == "PASS" # shared + assert result[3].status == "PASS" # community diff --git a/tests/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects_test.py b/tests/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects_test.py new file mode 100644 index 0000000000..0a46eb2280 --- /dev/null +++ b/tests/providers/openstack/services/image/image_not_shared_with_multiple_projects/image_not_shared_with_multiple_projects_test.py @@ -0,0 +1,417 @@ +"""Tests for image_not_shared_with_multiple_projects check.""" + +from unittest import mock + +from prowler.providers.openstack.services.image.image_service import ( + ImageMember, + ImageResource, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_image_not_shared_with_multiple_projects: + def test_no_images(self): + """Test when no images exist.""" + image_client = mock.MagicMock() + image_client.images = [] + image_client.audit_config = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects import ( + image_not_shared_with_multiple_projects, + ) + + check = image_not_shared_with_multiple_projects() + result = check.execute() + + assert len(result) == 0 + + def test_image_not_shared(self): + """Test PASS when image is not shared.""" + image_client = mock.MagicMock() + image_client.audit_config = {} + image_client.images = [ + ImageResource( + id="img-1", + name="private-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects import ( + image_not_shared_with_multiple_projects, + ) + + check = image_not_shared_with_multiple_projects() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image private-image (img-1) is not shared (visibility=private)." + ) + assert result[0].resource_id == "img-1" + + def test_image_shared_within_threshold(self): + """Test PASS when shared image has accepted members within threshold.""" + image_client = mock.MagicMock() + image_client.audit_config = {} + members = [ + ImageMember(member_id=f"project-{i}", status="accepted") for i in range(3) + ] + image_client.images = [ + ImageResource( + id="img-2", + name="shared-image", + status="active", + visibility="shared", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=members, + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects import ( + image_not_shared_with_multiple_projects, + ) + + check = image_not_shared_with_multiple_projects() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image shared-image (img-2) is shared with 3 accepted projects, within the threshold of 5." + ) + + def test_image_shared_at_threshold(self): + """Test PASS when accepted members exactly equal threshold.""" + image_client = mock.MagicMock() + image_client.audit_config = {} + members = [ + ImageMember(member_id=f"project-{i}", status="accepted") for i in range(5) + ] + image_client.images = [ + ImageResource( + id="img-3", + name="threshold-image", + status="active", + visibility="shared", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=members, + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects import ( + image_not_shared_with_multiple_projects, + ) + + check = image_not_shared_with_multiple_projects() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image threshold-image (img-3) is shared with 5 accepted projects, within the threshold of 5." + ) + + def test_image_shared_above_threshold(self): + """Test FAIL when accepted members exceed threshold.""" + image_client = mock.MagicMock() + image_client.audit_config = {} + members = [ + ImageMember(member_id=f"project-{i}", status="accepted") for i in range(8) + ] + image_client.images = [ + ImageResource( + id="img-4", + name="overshared-image", + status="active", + visibility="shared", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=members, + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects import ( + image_not_shared_with_multiple_projects, + ) + + check = image_not_shared_with_multiple_projects() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Image overshared-image (img-4) is shared with 8 accepted projects, exceeding the threshold of 5." + ) + + def test_pending_members_not_counted(self): + """Test that pending and rejected members are not counted.""" + image_client = mock.MagicMock() + image_client.audit_config = {} + members = [ + ImageMember(member_id="project-1", status="accepted"), + ImageMember(member_id="project-2", status="pending"), + ImageMember(member_id="project-3", status="rejected"), + ImageMember(member_id="project-4", status="pending"), + ImageMember(member_id="project-5", status="accepted"), + ImageMember(member_id="project-6", status="pending"), + ImageMember(member_id="project-7", status="pending"), + ImageMember(member_id="project-8", status="pending"), + ImageMember(member_id="project-9", status="pending"), + ImageMember(member_id="project-10", status="pending"), + ] + image_client.images = [ + ImageResource( + id="img-5", + name="pending-members-image", + status="active", + visibility="shared", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=members, + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects import ( + image_not_shared_with_multiple_projects, + ) + + check = image_not_shared_with_multiple_projects() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image pending-members-image (img-5) is shared with 2 accepted projects, within the threshold of 5." + ) + + def test_custom_threshold_via_audit_config(self): + """Test custom threshold from audit_config.""" + image_client = mock.MagicMock() + image_client.audit_config = {"image_sharing_threshold": 2} + members = [ + ImageMember(member_id=f"project-{i}", status="accepted") for i in range(3) + ] + image_client.images = [ + ImageResource( + id="img-6", + name="custom-threshold-image", + status="active", + visibility="shared", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=members, + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects import ( + image_not_shared_with_multiple_projects, + ) + + check = image_not_shared_with_multiple_projects() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Image custom-threshold-image (img-6) is shared with 3 accepted projects, exceeding the threshold of 2." + ) + + def test_multiple_images_mixed(self): + """Test mixed results with shared and non-shared images.""" + image_client = mock.MagicMock() + image_client.audit_config = {} + base = dict( + status="active", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + image_client.images = [ + ImageResource( + id="img-priv", + name="private", + visibility="private", + members=[], + **base, + ), + ImageResource( + id="img-over", + name="overshared", + visibility="shared", + members=[ + ImageMember(member_id=f"p-{i}", status="accepted") for i in range(6) + ], + **base, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_not_shared_with_multiple_projects.image_not_shared_with_multiple_projects import ( + image_not_shared_with_multiple_projects, + ) + + check = image_not_shared_with_multiple_projects() + result = check.execute() + + assert len(result) == 2 + assert result[0].status == "PASS" # private + assert result[1].status == "FAIL" # overshared diff --git a/tests/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled_test.py b/tests/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled_test.py new file mode 100644 index 0000000000..19f14b5660 --- /dev/null +++ b/tests/providers/openstack/services/image/image_protected_status_enabled/image_protected_status_enabled_test.py @@ -0,0 +1,180 @@ +"""Tests for image_protected_status_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.image.image_service import ImageResource +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_image_protected_status_enabled: + def test_no_images(self): + """Test when no images exist.""" + image_client = mock.MagicMock() + image_client.images = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_protected_status_enabled.image_protected_status_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_protected_status_enabled.image_protected_status_enabled import ( + image_protected_status_enabled, + ) + + check = image_protected_status_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_image_protected(self): + """Test PASS when image is protected.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-1", + name="protected-image", + status="active", + visibility="private", + protected=True, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_protected_status_enabled.image_protected_status_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_protected_status_enabled.image_protected_status_enabled import ( + image_protected_status_enabled, + ) + + check = image_protected_status_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image protected-image (img-1) has deletion protection enabled." + ) + assert result[0].resource_id == "img-1" + assert result[0].resource_name == "protected-image" + assert result[0].region == OPENSTACK_REGION + + def test_image_not_protected(self): + """Test FAIL when image is not protected.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-2", + name="unprotected-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_protected_status_enabled.image_protected_status_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_protected_status_enabled.image_protected_status_enabled import ( + image_protected_status_enabled, + ) + + check = image_protected_status_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Image unprotected-image (img-2) does not have deletion protection enabled." + ) + assert result[0].resource_id == "img-2" + + def test_multiple_images_mixed(self): + """Test mixed results with protected and unprotected images.""" + image_client = mock.MagicMock() + base = dict( + status="active", + visibility="private", + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + image_client.images = [ + ImageResource(id="img-p", name="protected", protected=True, **base), + ImageResource(id="img-u", name="unprotected", protected=False, **base), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_protected_status_enabled.image_protected_status_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_protected_status_enabled.image_protected_status_enabled import ( + image_protected_status_enabled, + ) + + check = image_protected_status_enabled() + result = check.execute() + + assert len(result) == 2 + assert result[0].status == "PASS" + assert result[1].status == "FAIL" diff --git a/tests/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled_test.py b/tests/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled_test.py new file mode 100644 index 0000000000..fa7597cfc8 --- /dev/null +++ b/tests/providers/openstack/services/image/image_secure_boot_enabled/image_secure_boot_enabled_test.py @@ -0,0 +1,277 @@ +"""Tests for image_secure_boot_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.image.image_service import ImageResource +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_image_secure_boot_enabled: + def test_no_images(self): + """Test when no images exist.""" + image_client = mock.MagicMock() + image_client.images = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled import ( + image_secure_boot_enabled, + ) + + check = image_secure_boot_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_image_secure_boot_required(self): + """Test PASS when os_secure_boot is 'required'.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-1", + name="secure-boot-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot="required", + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled import ( + image_secure_boot_enabled, + ) + + check = image_secure_boot_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image secure-boot-image (img-1) has Secure Boot set to required." + ) + assert result[0].resource_id == "img-1" + assert result[0].resource_name == "secure-boot-image" + assert result[0].region == OPENSTACK_REGION + + def test_image_secure_boot_not_set(self): + """Test FAIL when os_secure_boot is None.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-2", + name="no-secureboot-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled import ( + image_secure_boot_enabled, + ) + + check = image_secure_boot_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Image no-secureboot-image (img-2) does not have Secure Boot set to required (os_secure_boot=None)." + ) + + def test_image_secure_boot_optional(self): + """Test FAIL when os_secure_boot is 'optional'.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-3", + name="optional-secureboot", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot="optional", + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled import ( + image_secure_boot_enabled, + ) + + check = image_secure_boot_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_image_secure_boot_disabled(self): + """Test FAIL when os_secure_boot is 'disabled'.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-4", + name="disabled-secureboot", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot="disabled", + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled import ( + image_secure_boot_enabled, + ) + + check = image_secure_boot_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_multiple_images_mixed(self): + """Test mixed results with various secure boot settings.""" + image_client = mock.MagicMock() + base = dict( + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + image_client.images = [ + ImageResource( + id="img-req", name="required", os_secure_boot="required", **base + ), + ImageResource( + id="img-opt", name="optional", os_secure_boot="optional", **base + ), + ImageResource( + id="img-dis", name="disabled", os_secure_boot="disabled", **base + ), + ImageResource(id="img-none", name="none-set", os_secure_boot=None, **base), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_secure_boot_enabled.image_secure_boot_enabled import ( + image_secure_boot_enabled, + ) + + check = image_secure_boot_enabled() + result = check.execute() + + assert len(result) == 4 + assert result[0].status == "PASS" # required + assert result[1].status == "FAIL" # optional + assert result[2].status == "FAIL" # disabled + assert result[3].status == "FAIL" # None diff --git a/tests/providers/openstack/services/image/image_service_test.py b/tests/providers/openstack/services/image/image_service_test.py new file mode 100644 index 0000000000..682350b1aa --- /dev/null +++ b/tests/providers/openstack/services/image/image_service_test.py @@ -0,0 +1,593 @@ +"""Tests for OpenStack Image service.""" + +from unittest.mock import MagicMock, patch + +from openstack import exceptions as openstack_exceptions + +from prowler.providers.openstack.services.image.image_service import ( + Image, + ImageMember, + ImageResource, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class TestImageService: + """Test suite for Image service.""" + + def test_image_service_initialization(self): + """Test Image service initializes correctly.""" + provider = set_mocked_openstack_provider() + + with patch.object(Image, "_list_images", return_value=[]): + image_service = Image(provider) + + assert image_service.service_name == "Image" + assert image_service.provider == provider + assert image_service.connection == provider.connection + assert image_service.regional_connections == provider.regional_connections + assert image_service.audited_regions == [OPENSTACK_REGION] + assert image_service.region == OPENSTACK_REGION + assert image_service.project_id == OPENSTACK_PROJECT_ID + assert image_service.images == [] + + def test_image_list_images_success(self): + """Test listing images successfully.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-1" + mock_img.name = "ubuntu-22.04" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = True + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = ["production"] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + provider.connection.image.images.return_value = [mock_img] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert isinstance(image_service.images[0], ImageResource) + assert image_service.images[0].id == "img-1" + assert image_service.images[0].name == "ubuntu-22.04" + assert image_service.images[0].status == "active" + assert image_service.images[0].visibility == "private" + assert image_service.images[0].protected is True + assert image_service.images[0].tags == ["production"] + assert image_service.images[0].members == [] + + def test_image_list_images_with_signature(self): + """Test listing images with signature properties.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-signed" + mock_img.name = "signed-image" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = "abc123sig" + mock_img.img_signature_hash_method = "SHA-256" + mock_img.img_signature_key_type = "RSA-PSS" + mock_img.img_signature_certificate_uuid = "cert-uuid-123" + mock_img.hw_mem_encryption = True + mock_img.needs_secure_boot = "required" + mock_img.os_secure_boot = "required" + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + provider.connection.image.images.return_value = [mock_img] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + img = image_service.images[0] + assert img.img_signature == "abc123sig" + assert img.img_signature_hash_method == "SHA-256" + assert img.img_signature_key_type == "RSA-PSS" + assert img.img_signature_certificate_uuid == "cert-uuid-123" + assert img.hw_mem_encryption is True + assert img.os_secure_boot == "required" + + def test_image_list_images_shared_with_members(self): + """Test listing shared images fetches members.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-shared" + mock_img.name = "shared-image" + mock_img.status = "active" + mock_img.visibility = "shared" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + mock_member = MagicMock() + mock_member.member_id = "project-2" + mock_member.id = "project-2" + mock_member.status = "accepted" + + provider.connection.image.images.return_value = [mock_img] + provider.connection.image.members.return_value = [mock_member] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert len(image_service.images[0].members) == 1 + assert isinstance(image_service.images[0].members[0], ImageMember) + assert image_service.images[0].members[0].member_id == "project-2" + assert image_service.images[0].members[0].status == "accepted" + provider.connection.image.members.assert_called_once_with("img-shared") + + def test_image_list_images_private_no_member_fetch(self): + """Test that private images do not trigger member listing.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-private" + mock_img.name = "private-image" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + provider.connection.image.images.return_value = [mock_img] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert image_service.images[0].members == [] + provider.connection.image.members.assert_not_called() + + def test_image_list_images_empty(self): + """Test listing images when none exist.""" + provider = set_mocked_openstack_provider() + provider.connection.image.images.return_value = [] + + image_service = Image(provider) + + assert image_service.images == [] + + def test_image_list_images_sdk_exception(self): + """Test handling SDKException when listing images.""" + provider = set_mocked_openstack_provider() + provider.connection.image.images.side_effect = ( + openstack_exceptions.SDKException("API error") + ) + + image_service = Image(provider) + + assert image_service.images == [] + + def test_image_list_images_generic_exception(self): + """Test handling generic Exception when listing images.""" + provider = set_mocked_openstack_provider() + provider.connection.image.images.side_effect = Exception("Unexpected error") + + image_service = Image(provider) + + assert image_service.images == [] + + def test_image_list_image_members_sdk_exception(self): + """Test handling SDKException when listing image members.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-shared-err" + mock_img.name = "shared-error-image" + mock_img.status = "active" + mock_img.visibility = "shared" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + provider.connection.image.images.return_value = [mock_img] + provider.connection.image.members.side_effect = ( + openstack_exceptions.SDKException("Members API error") + ) + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert image_service.images[0].members == [] + + def test_image_hw_mem_encryption_false_not_overridden_by_properties(self): + """Test that hw_mem_encryption=False is preserved, not overridden by properties dict.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-enc-false" + mock_img.name = "encryption-false-image" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = False + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {"hw_mem_encryption": "true"} + + provider.connection.image.images.return_value = [mock_img] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert image_service.images[0].hw_mem_encryption is False + + def test_image_os_secure_boot_disabled_not_overridden_by_properties(self): + """Test that os_secure_boot='disabled' is preserved, not overridden by properties dict.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-boot-disabled" + mock_img.name = "boot-disabled-image" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = "disabled" + mock_img.os_secure_boot = "disabled" + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {"os_secure_boot": "required"} + + provider.connection.image.images.return_value = [mock_img] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert image_service.images[0].os_secure_boot == "disabled" + + def test_image_signature_empty_string_not_overridden_by_properties(self): + """Test that empty string signature attrs are preserved, not overridden by properties.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-sig-empty" + mock_img.name = "sig-empty-image" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = "" + mock_img.img_signature_hash_method = "" + mock_img.img_signature_key_type = "" + mock_img.img_signature_certificate_uuid = "" + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = { + "img_signature": "should-not-override", + "img_signature_hash_method": "should-not-override", + "img_signature_key_type": "should-not-override", + "img_signature_certificate_uuid": "should-not-override", + } + + provider.connection.image.images.return_value = [mock_img] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + img = image_service.images[0] + assert img.img_signature == "" + assert img.img_signature_hash_method == "" + assert img.img_signature_key_type == "" + assert img.img_signature_certificate_uuid == "" + + def test_image_properties_fallback_when_attrs_are_none(self): + """Test that properties dict is used as fallback when image attrs are None.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-fallback" + mock_img.name = "fallback-image" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = { + "img_signature": "prop-sig", + "img_signature_hash_method": "SHA-256", + "img_signature_key_type": "RSA-PSS", + "img_signature_certificate_uuid": "cert-from-props", + "hw_mem_encryption": "true", + "os_secure_boot": "required", + } + + provider.connection.image.images.return_value = [mock_img] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + img = image_service.images[0] + assert img.img_signature == "prop-sig" + assert img.img_signature_hash_method == "SHA-256" + assert img.img_signature_key_type == "RSA-PSS" + assert img.img_signature_certificate_uuid == "cert-from-props" + assert img.hw_mem_encryption is True + assert img.os_secure_boot == "required" + + def test_image_needs_secure_boot_sdk_attr_resolved(self): + """Test that needs_secure_boot (SDK attr) is used when os_secure_boot is absent.""" + provider = set_mocked_openstack_provider() + + mock_img = MagicMock() + mock_img.id = "img-sdk-boot" + mock_img.name = "sdk-boot-image" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = "required" + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + provider.connection.image.images.return_value = [mock_img] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert image_service.images[0].os_secure_boot == "required" + + def test_image_list_images_multi_region(self): + """Test listing images across multiple regions.""" + provider = set_mocked_openstack_provider() + + mock_conn_uk1 = MagicMock() + mock_conn_de1 = MagicMock() + + provider.regional_connections = {"UK1": mock_conn_uk1, "DE1": mock_conn_de1} + + mock_img_uk = MagicMock() + mock_img_uk.id = "img-uk" + mock_img_uk.name = "ubuntu-uk" + mock_img_uk.status = "active" + mock_img_uk.visibility = "private" + mock_img_uk.is_protected = False + mock_img_uk.owner_id = OPENSTACK_PROJECT_ID + mock_img_uk.owner = OPENSTACK_PROJECT_ID + mock_img_uk.img_signature = None + mock_img_uk.img_signature_hash_method = None + mock_img_uk.img_signature_key_type = None + mock_img_uk.img_signature_certificate_uuid = None + mock_img_uk.hw_mem_encryption = None + mock_img_uk.needs_secure_boot = None + mock_img_uk.os_secure_boot = None + mock_img_uk.tags = [] + mock_img_uk.project_id = OPENSTACK_PROJECT_ID + mock_img_uk.properties = {} + + mock_img_de = MagicMock() + mock_img_de.id = "img-de" + mock_img_de.name = "ubuntu-de" + mock_img_de.status = "active" + mock_img_de.visibility = "private" + mock_img_de.is_protected = False + mock_img_de.owner_id = OPENSTACK_PROJECT_ID + mock_img_de.owner = OPENSTACK_PROJECT_ID + mock_img_de.img_signature = None + mock_img_de.img_signature_hash_method = None + mock_img_de.img_signature_key_type = None + mock_img_de.img_signature_certificate_uuid = None + mock_img_de.hw_mem_encryption = None + mock_img_de.needs_secure_boot = None + mock_img_de.os_secure_boot = None + mock_img_de.tags = [] + mock_img_de.project_id = OPENSTACK_PROJECT_ID + mock_img_de.properties = {} + + mock_conn_uk1.image.images.return_value = [mock_img_uk] + mock_conn_de1.image.images.return_value = [mock_img_de] + + image_service = Image(provider) + + assert len(image_service.images) == 2 + uk_img = next(i for i in image_service.images if i.id == "img-uk") + de_img = next(i for i in image_service.images if i.id == "img-de") + assert uk_img.region == "UK1" + assert de_img.region == "DE1" + + def test_image_list_images_multi_region_partial_failure(self): + """Test that a failing region doesn't prevent other regions from being listed.""" + provider = set_mocked_openstack_provider() + + mock_conn_ok = MagicMock() + mock_conn_fail = MagicMock() + + provider.regional_connections = {"UK1": mock_conn_ok, "DE1": mock_conn_fail} + + mock_img = MagicMock() + mock_img.id = "img-uk" + mock_img.name = "ubuntu-uk" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + mock_conn_ok.image.images.return_value = [mock_img] + mock_conn_fail.image.images.side_effect = openstack_exceptions.SDKException( + "API error in DE1" + ) + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert image_service.images[0].id == "img-uk" + assert image_service.images[0].region == "UK1" + + def test_image_list_images_multi_region_one_empty(self): + """Test multi-region where one region has images and the other is empty.""" + provider = set_mocked_openstack_provider() + + mock_conn_uk1 = MagicMock() + mock_conn_de1 = MagicMock() + + provider.regional_connections = {"UK1": mock_conn_uk1, "DE1": mock_conn_de1} + + mock_img = MagicMock() + mock_img.id = "img-uk" + mock_img.name = "ubuntu-uk" + mock_img.status = "active" + mock_img.visibility = "private" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + mock_conn_uk1.image.images.return_value = [mock_img] + mock_conn_de1.image.images.return_value = [] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert image_service.images[0].id == "img-uk" + assert image_service.images[0].region == "UK1" + + def test_image_list_images_multi_region_shared_with_members(self): + """Test listing shared images fetches members using the correct regional connection.""" + provider = set_mocked_openstack_provider() + + mock_conn_uk1 = MagicMock() + mock_conn_de1 = MagicMock() + + provider.regional_connections = {"UK1": mock_conn_uk1, "DE1": mock_conn_de1} + + mock_img = MagicMock() + mock_img.id = "img-shared-uk" + mock_img.name = "shared-uk" + mock_img.status = "active" + mock_img.visibility = "shared" + mock_img.is_protected = False + mock_img.owner_id = OPENSTACK_PROJECT_ID + mock_img.owner = OPENSTACK_PROJECT_ID + mock_img.img_signature = None + mock_img.img_signature_hash_method = None + mock_img.img_signature_key_type = None + mock_img.img_signature_certificate_uuid = None + mock_img.hw_mem_encryption = None + mock_img.needs_secure_boot = None + mock_img.os_secure_boot = None + mock_img.tags = [] + mock_img.project_id = OPENSTACK_PROJECT_ID + mock_img.properties = {} + + mock_member = MagicMock() + mock_member.member_id = "project-2" + mock_member.id = "project-2" + mock_member.status = "accepted" + + mock_conn_uk1.image.images.return_value = [mock_img] + mock_conn_uk1.image.members.return_value = [mock_member] + mock_conn_de1.image.images.return_value = [] + + image_service = Image(provider) + + assert len(image_service.images) == 1 + assert image_service.images[0].region == "UK1" + assert len(image_service.images[0].members) == 1 + assert image_service.images[0].members[0].member_id == "project-2" + mock_conn_uk1.image.members.assert_called_once_with("img-shared-uk") diff --git a/tests/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled_test.py b/tests/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled_test.py new file mode 100644 index 0000000000..1b828a7735 --- /dev/null +++ b/tests/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled_test.py @@ -0,0 +1,280 @@ +"""Tests for image_signature_verification_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.image.image_service import ImageResource +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_image_signature_verification_enabled: + def test_no_images(self): + """Test when no images exist.""" + image_client = mock.MagicMock() + image_client.images = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled import ( + image_signature_verification_enabled, + ) + + check = image_signature_verification_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_image_fully_signed(self): + """Test PASS when all four signature properties are set.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-1", + name="signed-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature="abc123sig", + img_signature_hash_method="SHA-256", + img_signature_key_type="RSA-PSS", + img_signature_certificate_uuid="cert-uuid-123", + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled import ( + image_signature_verification_enabled, + ) + + check = image_signature_verification_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Image signed-image (img-1) has all signature verification properties configured." + ) + assert result[0].resource_id == "img-1" + assert result[0].resource_name == "signed-image" + assert result[0].region == OPENSTACK_REGION + + def test_image_no_signatures(self): + """Test FAIL when no signature properties are set.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-2", + name="unsigned-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled import ( + image_signature_verification_enabled, + ) + + check = image_signature_verification_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Image unsigned-image (img-2) does not have all signature verification properties configured." + ) + + def test_image_partial_signatures(self): + """Test FAIL when only some signature properties are set.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-3", + name="partial-sig-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature="abc123sig", + img_signature_hash_method="SHA-256", + img_signature_key_type=None, + img_signature_certificate_uuid=None, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled import ( + image_signature_verification_enabled, + ) + + check = image_signature_verification_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_image_empty_string_signatures(self): + """Test FAIL when signature properties are empty strings.""" + image_client = mock.MagicMock() + image_client.images = [ + ImageResource( + id="img-4", + name="empty-sig-image", + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + img_signature="", + img_signature_hash_method="", + img_signature_key_type="", + img_signature_certificate_uuid="", + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled import ( + image_signature_verification_enabled, + ) + + check = image_signature_verification_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_multiple_images_mixed(self): + """Test mixed results with signed and unsigned images.""" + image_client = mock.MagicMock() + base = dict( + status="active", + visibility="private", + protected=False, + owner=OPENSTACK_PROJECT_ID, + hw_mem_encryption=None, + os_secure_boot=None, + members=[], + tags=[], + project_id=OPENSTACK_PROJECT_ID, + region=OPENSTACK_REGION, + ) + image_client.images = [ + ImageResource( + id="img-signed", + name="signed", + img_signature="sig", + img_signature_hash_method="SHA-256", + img_signature_key_type="RSA-PSS", + img_signature_certificate_uuid="cert-uuid", + **base, + ), + ImageResource( + id="img-unsigned", + name="unsigned", + img_signature=None, + img_signature_hash_method=None, + img_signature_key_type=None, + img_signature_certificate_uuid=None, + **base, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled.image_client", + new=image_client, + ), + ): + from prowler.providers.openstack.services.image.image_signature_verification_enabled.image_signature_verification_enabled import ( + image_signature_verification_enabled, + ) + + check = image_signature_verification_enabled() + result = check.execute() + + assert len(result) == 2 + assert result[0].status == "PASS" + assert result[1].status == "FAIL"