Compare commits

...

7 Commits

Author SHA1 Message Date
Daniel Barranquero
206feeb5a8 fix: accept legacy metadata from db 2026-03-20 09:35:23 +01:00
César Arroba
cece2cb87e chore: pin Prowler version to lastest master commit on push (#10384) 2026-03-19 14:32:38 +01:00
Adrián Peña
ab266080d0 perf(api): add trigram indexes for finding groups (#10378) 2026-03-19 13:54:50 +01:00
Prowler Bot
4638b39ed4 chore(api): Bump version to v1.23.0 (#10393)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 13:42:46 +01:00
Prowler Bot
997f9bf64a docs: Update version to v5.21.0 (#10391)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 13:40:33 +01:00
Prowler Bot
aecc234f78 chore(release): Bump version to v5.22.0 (#10389)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 13:40:22 +01:00
Pepe Fagoaga
8317eff67b chore(changelog): prepare for v5.21.0 (#10380) 2026-03-19 11:09:51 +01:00
16 changed files with 382 additions and 54 deletions

View File

@@ -99,6 +99,12 @@ jobs:
with:
persist-credentials: false
- name: Pin prowler SDK to latest master commit
if: github.event_name == 'push'
run: |
LATEST_SHA=$(git ls-remote https://github.com/prowler-cloud/prowler.git refs/heads/master | cut -f1)
sed -i "s|prowler-cloud/prowler.git@master|prowler-cloud/prowler.git@${LATEST_SHA}|" api/pyproject.toml
- name: Login to DockerHub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:

View File

@@ -2,16 +2,16 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.22.0] (Prowler UNRELEASED)
## [1.22.0] (Prowler v5.21.0)
### 🚀 Added
- `CORS_ALLOWED_ORIGINS` configurable via environment variable [(#10355)](https://github.com/prowler-cloud/prowler/pull/10355)
- Attack Paths: Tenant and provider related labels to the nodes so they can be easily filtered on custom queries [(#10308)](https://github.com/prowler-cloud/prowler/pull/10308)
### 🔄 Changed
- Attack Paths: Complete migration to private graph labels and properties, removing deprecated dual-write support [(#10268)](https://github.com/prowler-cloud/prowler/pull/10268)
- Attack Paths: Added tenant and provider related labels to the nodes so they can be easily filtered on custom queries [(#10308)](https://github.com/prowler-cloud/prowler/pull/10308)
- Attack Paths: Reduce sync and findings memory usage with smaller batches, cursor iteration, and sequential sessions [(#10359)](https://github.com/prowler-cloud/prowler/pull/10359)
### 🐞 Fixed

View File

@@ -49,7 +49,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.22.0"
version = "1.23.0"
[project.scripts]
celery = "src.backend.config.settings.celery"

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.1.15 on 2026-03-18
from django.contrib.postgres.indexes import GinIndex, OpClass
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations
from django.db.models.functions import Upper
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0084_googleworkspace_provider"),
]
operations = [
AddIndexConcurrently(
model_name="findinggroupdailysummary",
index=GinIndex(
OpClass(Upper("check_id"), name="gin_trgm_ops"),
name="fgds_check_id_trgm_idx",
),
),
AddIndexConcurrently(
model_name="findinggroupdailysummary",
index=GinIndex(
OpClass(Upper("check_title"), name="gin_trgm_ops"),
name="fgds_check_title_trgm_idx",
),
),
]

View File

@@ -1783,6 +1783,15 @@ class FindingGroupDailySummary(RowLevelSecurityProtectedModel):
fields=["tenant_id", "provider", "inserted_at"],
name="fgds_tenant_prov_ins_idx",
),
# Trigram indexes for case-insensitive search
GinIndex(
OpClass(Upper("check_id"), name="gin_trgm_ops"),
name="fgds_check_id_trgm_idx",
),
GinIndex(
OpClass(Upper("check_title"), name="gin_trgm_ops"),
name="fgds_check_title_trgm_idx",
),
]
class JSONAPIMeta:

View File

@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.22.0
version: 1.23.0
description: |-
Prowler API specification.

View File

@@ -408,7 +408,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.22.0"
spectacular_settings.VERSION = "1.23.0"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)

View File

@@ -334,6 +334,172 @@ class TestGenerateOutputs:
output_location="s3://bucket/zipped.zip"
)
@patch("tasks.tasks._upload_to_s3")
@patch("tasks.tasks._compress_output_files")
@patch("tasks.tasks.get_compliance_frameworks")
@patch("tasks.tasks.Compliance.get_bulk")
@patch("tasks.tasks.initialize_prowler_provider")
@patch("tasks.tasks.Provider.objects.get")
@patch("tasks.tasks.ScanSummary.objects.filter")
@patch("tasks.tasks.Finding.all_objects.filter")
def test_generate_outputs_accepts_legacy_persisted_check_metadata(
self,
mock_finding_filter,
mock_scan_summary_filter,
mock_provider_get,
mock_initialize_provider,
mock_compliance_get_bulk,
mock_get_available_frameworks,
mock_compress,
mock_upload,
):
mock_scan_summary_filter.return_value.exists.return_value = True
mock_provider = MagicMock()
mock_provider.uid = "azure-subscription-123"
mock_provider.provider = "azure"
mock_provider_get.return_value = mock_provider
prowler_provider = MagicMock()
prowler_provider.type = "azure"
prowler_provider.identity.identity_type = "mock_identity_type"
prowler_provider.identity.identity_id = "mock_identity_id"
prowler_provider.identity.subscriptions = {
"legacy-subscription": "legacy-sub-id"
}
prowler_provider.identity.tenant_ids = ["test-ing-432a-a828-d9c965196f87"]
prowler_provider.identity.tenant_domain = "mock_tenant_domain"
prowler_provider.region_config.name = "AzureCloud"
mock_initialize_provider.return_value = prowler_provider
mock_compliance_get_bulk.return_value = {}
mock_get_available_frameworks.return_value = []
resource = MagicMock()
resource.uid = (
"/subscriptions/legacy-sub-id/providers/Microsoft.Authorization/"
"policyAssignments/legacy"
)
resource.name = "legacy-policy"
resource.region = "global"
resource.metadata = "{}"
resource.details = ""
resource.tags.all.return_value = [MagicMock(key="env", value="prod")]
dummy_finding = MagicMock()
dummy_finding.uid = "finding-uid-legacy"
dummy_finding.status = "FAIL"
dummy_finding.status_extended = "Legacy metadata finding"
dummy_finding.muted = False
dummy_finding.compliance = {}
dummy_finding.raw_result = {}
dummy_finding.check_id = (
"entra_conditional_access_policy_require_mfa_for_management_api"
)
dummy_finding.check_metadata = {
"provider": "azure",
"checkid": "entra_conditional_access_policy_require_mfa_for_management_api",
"checktitle": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API",
"checktype": [],
"servicename": "entra",
"subservicename": "",
"severity": "medium",
"resourcetype": "#microsoft.graph.conditionalAccess",
"resourcegroup": "IAM",
"description": "Legacy description",
"risk": "Legacy risk",
"relatedurl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management",
"additionalurls": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
],
"remediation": {
"code": {
"cli": "",
"other": "",
"nativeiac": "",
"terraform": "",
},
"recommendation": {
"text": "Legacy remediation",
"url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps",
},
},
"resourceidtemplate": "",
"categories": [],
"dependson": [],
"relatedto": [],
"notes": "Legacy notes",
}
dummy_finding.resources.first.return_value = resource
mock_finding_filter.return_value.order_by.return_value.iterator.return_value = [
dummy_finding
]
writer_instances = []
def writer_factory(*args, **kwargs):
writer = MagicMock()
writer._data = []
writer.transform = MagicMock()
writer.batch_write_data_to_file = MagicMock()
writer.findings = kwargs["findings"]
writer_instances.append(writer)
return writer
with (
patch(
"tasks.tasks.FindingOutput._transform_findings_stats",
return_value={"some": "stats"},
),
patch(
"tasks.tasks.OUTPUT_FORMATS_MAPPING",
{
"json": {
"class": writer_factory,
"suffix": ".json",
"kwargs": {},
}
},
),
patch(
"tasks.tasks._generate_output_directory",
return_value=(
"/tmp/test/out-dir",
"/tmp/test/comp-dir",
),
),
patch("tasks.tasks.Scan.all_objects.filter") as mock_scan_update,
patch("tasks.tasks.rmtree"),
):
mock_compress.return_value = "/tmp/zipped.zip"
mock_upload.return_value = "s3://bucket/zipped.zip"
result = generate_outputs_task(
scan_id=self.scan_id,
provider_id=self.provider_id,
tenant_id=self.tenant_id,
)
assert result == {"upload": True}
assert len(writer_instances) == 1
transformed_finding = writer_instances[0].findings[0]
assert transformed_finding.metadata.CheckTitle.startswith("Ensure")
assert (
transformed_finding.metadata.RelatedUrl
== "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management"
)
assert (
transformed_finding.metadata.Remediation.Recommendation.Url
== "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
)
assert transformed_finding.metadata.Severity.value == "medium"
mock_scan_update.return_value.update.assert_called_once_with(
output_location="s3://bucket/zipped.zip"
)
def test_generate_outputs_fails_upload(self):
with (
patch("tasks.tasks.ScanSummary.objects.filter") as mock_filter,

View File

@@ -121,8 +121,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.20.0"
PROWLER_API_VERSION="5.20.0"
PROWLER_UI_VERSION="5.21.0"
PROWLER_API_VERSION="5.21.0"
```
<Note>

View File

@@ -2,7 +2,7 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.5.0] (Prowler v5.20.0)
## [0.5.0] (Prowler v5.21.0)
### 🚀 Added

View File

@@ -2,7 +2,7 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.21.0] (Prowler UNRELEASED)
## [5.21.0] (Prowler v5.21.0)
### 🚀 Added
@@ -10,7 +10,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `entra_conditional_access_policy_device_code_flow_blocked` check for M365 provider [(#10218)](https://github.com/prowler-cloud/prowler/pull/10218)
- RBI compliance for the Azure provider [(#10339)](https://github.com/prowler-cloud/prowler/pull/10339)
-`entra_conditional_access_policy_require_mfa_for_admin_portals` check for Azure provider and update CIS compliance [(#10330)](https://github.com/prowler-cloud/prowler/pull/10330)
- CheckMetadata Pydantic validators [(#8584)](https://github.com/prowler-cloud/prowler/pull/8583)
- CheckMetadata Pydantic validators [(#8583)](https://github.com/prowler-cloud/prowler/pull/8583)
- `organization_repository_deletion_limited` check for GitHub provider [(#10185)](https://github.com/prowler-cloud/prowler/pull/10185)
- SecNumCloud 3.2 for the GCP provider [(#10364)](https://github.com/prowler-cloud/prowler/pull/10364)
- SecNumCloud 3.2 for the Azure provider [(#10358)](https://github.com/prowler-cloud/prowler/pull/10358)
@@ -27,16 +27,16 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update ResourceType and Categories for Azure Entra ID service metadata [(#10334)](https://github.com/prowler-cloud/prowler/pull/10334)
- Update OCI Regions to include US DoD regions [(#10375)](https://github.com/prowler-cloud/prowler/pull/10376)
### 🔐 Security
- Bump `multipart` to 1.3.1 to fix [GHSA-p2m9-wcp5-6qw3](https://github.com/defnull/multipart/security/advisories/GHSA-p2m9-wcp5-6qw3) [(#10331)](https://github.com/prowler-cloud/prowler/pull/10331)
### 🐞 Fixed
- Route53 dangling IP check false positive when using `--region` flag [(#9952)](https://github.com/prowler-cloud/prowler/pull/9952)
- RBI compliance framework support on Prowler Dashboard for the Azure provider [(#10360)](https://github.com/prowler-cloud/prowler/pull/10360)
- CheckMetadata strict validators rejecting valid external tool provider data (image, iac, llm) [(#10363)](https://github.com/prowler-cloud/prowler/pull/10363)
### 🔐 Security
- Bump `multipart` to 1.3.1 to fix [GHSA-p2m9-wcp5-6qw3](https://github.com/defnull/multipart/security/advisories/GHSA-p2m9-wcp5-6qw3) [(#10331)](https://github.com/prowler-cloud/prowler/pull/10331)
---
## [5.20.0] (Prowler v5.20.0)

View File

@@ -38,7 +38,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.21.0"
prowler_version = "5.22.0"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"

View File

@@ -12,6 +12,7 @@ from prowler.lib.check.models import (
Code,
Recommendation,
Remediation,
Severity,
)
from prowler.lib.logger import logger
from prowler.lib.outputs.common import Status, fill_common_finding_data
@@ -536,48 +537,74 @@ class Finding(BaseModel):
finding.zone_name = getattr(resource, "zone_name", resource.name)
finding.account_id = getattr(finding, "account_id", "")
finding.check_metadata = CheckMetadata(
Provider=finding.check_metadata["provider"],
CheckID=finding.check_metadata["checkid"],
CheckTitle=finding.check_metadata["checktitle"],
CheckType=finding.check_metadata["checktype"],
ServiceName=finding.check_metadata["servicename"],
SubServiceName=finding.check_metadata["subservicename"],
Severity=finding.check_metadata["severity"],
ResourceType=finding.check_metadata["resourcetype"],
Description=finding.check_metadata["description"],
Risk=finding.check_metadata["risk"],
RelatedUrl=finding.check_metadata["relatedurl"],
Remediation=Remediation(
Recommendation=Recommendation(
Text=finding.check_metadata["remediation"]["recommendation"][
"text"
],
Url=finding.check_metadata["remediation"]["recommendation"]["url"],
),
Code=Code(
NativeIaC=finding.check_metadata["remediation"]["code"][
"nativeiac"
],
Terraform=finding.check_metadata["remediation"]["code"][
"terraform"
],
CLI=finding.check_metadata["remediation"]["code"]["cli"],
Other=finding.check_metadata["remediation"]["code"]["other"],
),
),
ResourceIdTemplate=finding.check_metadata["resourceidtemplate"],
Categories=finding.check_metadata["categories"],
DependsOn=finding.check_metadata["dependson"],
RelatedTo=finding.check_metadata["relatedto"],
Notes=finding.check_metadata["notes"],
)
metadata_kwargs = cls._get_api_check_metadata_kwargs(finding.check_metadata)
try:
finding.check_metadata = CheckMetadata(**metadata_kwargs)
except ValidationError as validation_error:
check_id = metadata_kwargs.get("CheckID", getattr(finding, "check_id", ""))
logger.warning(
"Legacy persisted check metadata failed validation during API finding transformation "
f"for {check_id}. Falling back to compatibility mode. Errors: {validation_error.errors()}"
)
finding.check_metadata = cls._construct_legacy_check_metadata(
metadata_kwargs
)
finding.resource_tags = unroll_tags(
[{"key": tag.key, "value": tag.value} for tag in resource.tags.all()]
)
return cls.generate_output(provider, finding, SimpleNamespace())
@staticmethod
def _get_api_check_metadata_kwargs(check_metadata: dict) -> dict:
remediation = check_metadata["remediation"]
remediation_code = remediation["code"]
remediation_recommendation = remediation["recommendation"]
return {
"Provider": check_metadata["provider"],
"CheckID": check_metadata["checkid"],
"CheckTitle": check_metadata["checktitle"],
"CheckType": check_metadata["checktype"],
"CheckAliases": check_metadata.get("checkaliases", []),
"ServiceName": check_metadata["servicename"],
"SubServiceName": check_metadata["subservicename"],
"Severity": check_metadata["severity"],
"ResourceType": check_metadata["resourcetype"],
"ResourceGroup": check_metadata.get("resourcegroup", ""),
"Description": check_metadata["description"],
"Risk": check_metadata["risk"],
"RelatedUrl": check_metadata["relatedurl"],
"Remediation": Remediation(
Recommendation=Recommendation(
Text=remediation_recommendation["text"],
Url=remediation_recommendation["url"],
),
Code=Code(
NativeIaC=remediation_code["nativeiac"],
Terraform=remediation_code["terraform"],
CLI=remediation_code["cli"],
Other=remediation_code["other"],
),
),
"ResourceIdTemplate": check_metadata["resourceidtemplate"],
"AdditionalURLs": check_metadata.get("additionalurls", []),
"Categories": check_metadata["categories"],
"DependsOn": check_metadata["dependson"],
"RelatedTo": check_metadata["relatedto"],
"Notes": check_metadata["notes"],
"Compliance": check_metadata.get("compliance", []),
}
@staticmethod
def _construct_legacy_check_metadata(metadata_kwargs: dict) -> CheckMetadata:
severity = metadata_kwargs["Severity"]
if not isinstance(severity, Severity):
severity = Severity(severity)
legacy_metadata_kwargs = {**metadata_kwargs, "Severity": severity}
return CheckMetadata.construct(**legacy_metadata_kwargs)
def _transform_findings_stats(scan_summaries: list[dict]) -> dict:
"""
Aggregate and transform scan summary data into findings statistics.

View File

@@ -94,7 +94,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
name = "prowler"
readme = "README.md"
requires-python = ">3.9.1,<3.13"
version = "5.21.0"
version = "5.22.0"
[project.scripts]
prowler = "prowler.__main__:prowler"

View File

@@ -1126,6 +1126,95 @@ class TestFinding:
for segment in expected_segments:
assert segment in finding_obj.uid
@patch(
"prowler.lib.outputs.finding.get_check_compliance",
new=mock_get_check_compliance,
)
def test_transform_api_finding_azure_accepts_legacy_persisted_metadata(self):
provider = MagicMock()
provider.type = "azure"
provider.identity.identity_type = "mock_identity_type"
provider.identity.identity_id = "mock_identity_id"
provider.identity.subscriptions = {"legacy-subscription": "legacy-sub-id"}
provider.identity.tenant_ids = ["test-ing-432a-a828-d9c965196f87"]
provider.identity.tenant_domain = "mock_tenant_domain"
provider.region_config.name = "AzureCloud"
api_finding = DummyAPIFinding()
api_finding.uid = "finding-uid-legacy"
api_finding.status = "FAIL"
api_finding.status_extended = "Legacy metadata finding"
api_finding.check_id = (
"entra_conditional_access_policy_require_mfa_for_management_api"
)
api_finding.check_metadata = {
"provider": "azure",
"checkid": "entra_conditional_access_policy_require_mfa_for_management_api",
"checktitle": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API",
"checktype": [],
"servicename": "entra",
"subservicename": "",
"severity": "medium",
"resourcetype": "#microsoft.graph.conditionalAccess",
"resourcegroup": "IAM",
"description": "Legacy description",
"risk": "Legacy risk",
"relatedurl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management",
"additionalurls": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
],
"remediation": {
"code": {
"cli": "",
"other": "",
"nativeiac": "",
"terraform": "",
},
"recommendation": {
"text": "Legacy remediation",
"url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps",
},
},
"resourceidtemplate": "",
"categories": [],
"dependson": [],
"relatedto": [],
"notes": "Legacy notes",
}
api_finding.muted = False
api_resource = DummyResource(
uid="/subscriptions/legacy-sub-id/providers/Microsoft.Authorization/policyAssignments/legacy",
name="legacy-policy",
resource_arn="arn",
region="global",
tags=[],
)
api_finding.resources = DummyResources(api_resource)
finding_obj = Finding.transform_api_finding(api_finding, provider)
assert finding_obj.account_uid == "legacy-sub-id"
assert finding_obj.resource_uid == api_resource.uid
assert finding_obj.resource_name == api_resource.name
meta = finding_obj.metadata
assert (
meta.CheckTitle
== "Ensure Multifactor Authentication is Required for Windows Azure Service Management API"
)
assert (
meta.RelatedUrl
== "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management"
)
assert (
meta.Remediation.Recommendation.Url
== "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
)
assert meta.ResourceGroup == "IAM"
assert meta.AdditionalURLs == [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
]
@patch(
"prowler.lib.outputs.finding.get_check_compliance",
new=mock_get_check_compliance,

View File

@@ -2,7 +2,7 @@
All notable changes to the **Prowler UI** are documented in this file.
## [1.21.0] (Prowler v5.21.0 UNRELEASED)
## [1.21.0] (Prowler v5.21.0)
### 🚀 Added
@@ -13,8 +13,8 @@ All notable changes to the **Prowler UI** are documented in this file.
- Google Workspace provider support [(#10333)](https://github.com/prowler-cloud/prowler/pull/10333)
- Image (Container Registry) provider support in UI: badge icon, credentials form, and provider-type filtering [(#10167)](https://github.com/prowler-cloud/prowler/pull/10167)
- Organization and organizational unit row actions (Edit Name, Update Credentials, Test Connections, Delete) in providers table dropdown [(#10317)](https://github.com/prowler-cloud/prowler/pull/10317)
- Events tab in Findings and Resource detail cards showing an AWS CloudTrail timeline with expandable event rows, actor info, request/response JSON payloads, and error details [(#10320)](https://github.com/prowler-cloud/prowler/pull/10320)
- AWS Organization and organizational unit row actions (Edit Name, Update Credentials, Test Connections, Delete) in providers table dropdown [(#10317)](https://github.com/prowler-cloud/prowler/pull/10317)
---