Compare commits

...

3 Commits

Author SHA1 Message Date
renovate[bot] f78e420938 chore(docker): update docker 2026-06-16 20:15:06 +00:00
Alejandro Bailo 262dfda0aa fix(ui): handle alert form errors (#11623) 2026-06-16 17:44:48 +02:00
s1ns3nz0 8bc42a5ded feat(azure): add cosmosdb_account_minimum_tls_version check (#11033)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-06-16 16:42:51 +02:00
11 changed files with 383 additions and 6 deletions
@@ -1,7 +1,7 @@
# Build command
# docker build --platform=linux/amd64 --no-cache -t prowler:latest .
ARG PROWLER_VERSION=latest@sha256:4b796c6df40a3350c7947747b59bdda230d0da6222287500e13b0a8e1574aad4
ARG PROWLER_VERSION=latest@sha256:f61e0dfb4b07eebdef884392955ba799b9c77d321ced1578bfd403814f4aec8d
FROM toniblyx/prowler:${PROWLER_VERSION}
+1
View File
@@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `identity_storage_service_level_admins_scoped` check for OCI provider CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions [(#11523)](https://github.com/prowler-cloud/prowler/pull/11523)
- `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031)
- `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032)
- `cosmosdb_account_minimum_tls_version` check for Azure provider, verifying Cosmos DB accounts enforce TLS 1.2 or higher for client connections [(#11033)](https://github.com/prowler-cloud/prowler/pull/11033)
- `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027)
- Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602)
- TLS certificate verification in the `codepipeline_project_repo_private` check, which previously used an unverified SSL context, leaving the repository-visibility probe open to MITM tampering [(#11603)](https://github.com/prowler-cloud/prowler/pull/11603)
@@ -1503,6 +1503,7 @@
],
"Checks": [
"app_minimum_tls_version_12",
"cosmosdb_account_minimum_tls_version",
"monitor_storage_account_with_activity_logs_cmk_encrypted",
"sqlserver_tde_encrypted_with_cmk",
"sqlserver_tde_encryption_enabled",
@@ -0,0 +1,38 @@
{
"Provider": "azure",
"CheckID": "cosmosdb_account_minimum_tls_version",
"CheckTitle": "Cosmos DB account enforces TLS 1.2 or higher",
"CheckType": [],
"ServiceName": "cosmosdb",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "microsoft.documentdb/databaseaccounts",
"ResourceGroup": "database",
"Description": "**Azure Cosmos DB accounts** are evaluated for **minimum TLS version**. TLS 1.0 and 1.1 are deprecated and contain known weaknesses. Enforcing **TLS 1.2** ensures all client connections negotiate modern, secure encryption protocols.",
"Risk": "Allowing **TLS 1.0/1.1** exposes client connections to **POODLE**, **BEAST**, and other protocol downgrade attacks that can compromise the **confidentiality** and **integrity** of data in transit, and may enable credential interception via weakened cipher suites.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/cosmos-db/security",
"https://learn.microsoft.com/en-us/cli/azure/cosmosdb",
"https://learn.microsoft.com/en-us/azure/templates/microsoft.documentdb/databaseaccounts"
],
"Remediation": {
"Code": {
"CLI": "az cosmosdb update --name <COSMOS_ACCOUNT_NAME> --resource-group <RESOURCE_GROUP> --minimal-tls-version Tls12",
"NativeIaC": "```bicep\n// Bicep: Enforce minimum TLS 1.2 on a Cosmos DB account\nresource account 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n kind: 'GlobalDocumentDB'\n properties: {\n databaseAccountOfferType: 'Standard'\n locations: [{ locationName: resourceGroup().location }]\n minimalTlsVersion: 'Tls12' // Critical: Rejects client connections negotiating TLS 1.0 or 1.1\n }\n}\n```",
"Other": "1. Sign in to the Azure portal and open your Cosmos DB account\n2. In the left menu, select Networking\n3. Locate the Minimum TLS version setting\n4. Select TLS 1.2\n5. Click Save\n6. Validate that all client applications support TLS 1.2 before enforcing the change",
"Terraform": "```hcl\n# Terraform: Enforce minimum TLS 1.2 on a Cosmos DB account\nresource \"azurerm_cosmosdb_account\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n offer_type = \"Standard\"\n kind = \"GlobalDocumentDB\"\n\n geo_location {\n location = \"<example_location>\"\n failover_priority = 0\n }\n\n minimal_tls_version = \"Tls12\" # Critical: Rejects client connections negotiating TLS 1.0 or 1.1\n}\n```"
},
"Recommendation": {
"Text": "Set the Cosmos DB account **minimum TLS version** to at least **1.2** to block legacy protocols (`TLS 1.0`/`1.1`) vulnerable to known downgrade and cipher attacks. Inventory and update **client SDKs** and **drivers** to support TLS 1.2 prior to enforcement, and pair this control with **private endpoints**, **AAD/RBAC authentication**, and **handshake failure monitoring** to identify outdated clients.",
"Url": "https://hub.prowler.com/check/cosmosdb_account_minimum_tls_version"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,30 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.cosmosdb.cosmosdb_client import cosmosdb_client
class cosmosdb_account_minimum_tls_version(Check):
"""Ensure that Cosmos DB accounts enforce TLS 1.2 or higher."""
def execute(self) -> Check_Report_Azure:
"""Execute the Cosmos DB minimum TLS version check.
Iterates over every Cosmos DB account fetched by the service and reports
PASS when `minimalTlsVersion` is `Tls12` or higher, FAIL otherwise
(including when the property is missing or set to a legacy value).
Returns:
A list of Check_Report_Azure with one report per Cosmos DB account.
"""
findings = []
for subscription, accounts in cosmosdb_client.accounts.items():
for account in accounts:
report = Check_Report_Azure(metadata=self.metadata(), resource=account)
report.subscription = subscription
report.status = "FAIL"
report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} does not enforce TLS 1.2 or higher."
if account.minimal_tls_version in {"Tls12", "Tls13"}:
report.status = "PASS"
report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} enforces TLS 1.2 or higher."
findings.append(report)
return findings
@@ -0,0 +1,209 @@
from unittest import mock
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
class Test_cosmosdb_account_minimum_tls_version:
def test_no_subscriptions(self):
cosmosdb_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version.cosmosdb_client",
new=cosmosdb_client,
),
):
from prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version import (
cosmosdb_account_minimum_tls_version,
)
cosmosdb_client.accounts = {}
check = cosmosdb_account_minimum_tls_version()
result = check.execute()
assert len(result) == 0
def test_pass_tls12(self):
cosmosdb_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version.cosmosdb_client",
new=cosmosdb_client,
),
):
from prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version import (
cosmosdb_account_minimum_tls_version,
)
from prowler.providers.azure.services.cosmosdb.cosmosdb_service import (
Account,
)
cosmosdb_client.accounts = {
AZURE_SUBSCRIPTION_ID: [
Account(
id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name="test-account",
kind="GlobalDocumentDB",
type="Microsoft.DocumentDB/databaseAccounts",
tags={},
is_virtual_network_filter_enabled=False,
location="eastus",
private_endpoint_connections=[],
disable_local_auth=False,
minimal_tls_version="Tls12",
)
]
}
check = cosmosdb_account_minimum_tls_version()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == (
f"CosmosDB account test-account from subscription "
f"{AZURE_SUBSCRIPTION_ID} enforces TLS 1.2 or higher."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
def test_pass_tls13(self):
cosmosdb_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version.cosmosdb_client",
new=cosmosdb_client,
),
):
from prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version import (
cosmosdb_account_minimum_tls_version,
)
from prowler.providers.azure.services.cosmosdb.cosmosdb_service import (
Account,
)
cosmosdb_client.accounts = {
AZURE_SUBSCRIPTION_ID: [
Account(
id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name="test-account",
kind="GlobalDocumentDB",
type="Microsoft.DocumentDB/databaseAccounts",
tags={},
is_virtual_network_filter_enabled=False,
location="eastus",
private_endpoint_connections=[],
disable_local_auth=False,
minimal_tls_version="Tls13",
)
]
}
check = cosmosdb_account_minimum_tls_version()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_fail_tls11(self):
cosmosdb_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version.cosmosdb_client",
new=cosmosdb_client,
),
):
from prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version import (
cosmosdb_account_minimum_tls_version,
)
from prowler.providers.azure.services.cosmosdb.cosmosdb_service import (
Account,
)
cosmosdb_client.accounts = {
AZURE_SUBSCRIPTION_ID: [
Account(
id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name="test-account",
kind="GlobalDocumentDB",
type="Microsoft.DocumentDB/databaseAccounts",
tags={},
is_virtual_network_filter_enabled=False,
location="eastus",
private_endpoint_connections=[],
disable_local_auth=False,
minimal_tls_version="Tls11",
)
]
}
check = cosmosdb_account_minimum_tls_version()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == (
f"CosmosDB account test-account from subscription "
f"{AZURE_SUBSCRIPTION_ID} does not enforce TLS 1.2 or higher."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
def test_fail_no_tls_version(self):
cosmosdb_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version.cosmosdb_client",
new=cosmosdb_client,
),
):
from prowler.providers.azure.services.cosmosdb.cosmosdb_account_minimum_tls_version.cosmosdb_account_minimum_tls_version import (
cosmosdb_account_minimum_tls_version,
)
from prowler.providers.azure.services.cosmosdb.cosmosdb_service import (
Account,
)
cosmosdb_client.accounts = {
AZURE_SUBSCRIPTION_ID: [
Account(
id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name="test-account",
kind="GlobalDocumentDB",
type="Microsoft.DocumentDB/databaseAccounts",
tags={},
is_virtual_network_filter_enabled=False,
location="eastus",
private_endpoint_connections=[],
disable_local_auth=False,
minimal_tls_version=None,
)
]
}
check = cosmosdb_account_minimum_tls_version()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
@@ -701,6 +701,31 @@ describe("AlertFormModal", () => {
expect(errorMessage).toHaveClass("text-text-error-primary");
});
it("should clear the preview error when save shows the form error", async () => {
// Given
const user = userEvent.setup();
alertsActionMocks.seedAlertRule.mockResolvedValue({
error: "No alert-compatible filters",
});
mockRecipientsList();
renderCreateModal({ editingAlert: createEditingAlert() });
// When
await user.click(screen.getByRole("button", { name: /^test$/i }));
expect(await screen.findByText("Test result")).toBeVisible();
await user.click(screen.getByRole("button", { name: /^save$/i }));
// Then
await waitFor(() =>
expect(screen.queryByText("Test result")).not.toBeInTheDocument(),
);
expect(
screen.getAllByText(
"Apply at least one alert-compatible Findings filter.",
),
).toHaveLength(1);
});
it("should hydrate advanced edit mode filters and normalize them on save", async () => {
// Given
const user = userEvent.setup();
@@ -1,6 +1,6 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { isValidElement, type ReactNode } from "react";
import { isValidElement, type ReactNode, useState } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
@@ -8,6 +8,10 @@ import {
ALERT_TRIGGER_KINDS,
type AlertRule,
} from "@/app/(prowler)/alerts/_types";
import type {
AlertFormSubmitResult,
AlertFormValues,
} from "@/app/(prowler)/alerts/_types/alert-form";
import { AlertsManager } from "../alerts-manager";
@@ -96,12 +100,32 @@ vi.mock("../alert-form-modal", () => ({
open,
editingAlert,
onOpenChange,
onSubmit,
}: {
open: boolean;
editingAlert?: AlertRule | null;
onOpenChange: (open: boolean) => void;
}) =>
open ? (
onSubmit: (values: AlertFormValues) => Promise<AlertFormSubmitResult>;
}) => {
const [error, setError] = useState<string | null>(null);
const submit = async () => {
const result = await onSubmit({
name: "Updated alert",
description: "",
method: "email",
frequency: ALERT_TRIGGER_KINDS.AFTER_SCAN,
condition: {
op: ALERT_AGGREGATE_OPS.ANY,
filter: { severity: ["critical"] },
},
recipientEmails: [],
enabled: true,
});
setError(result.ok ? null : (result.error ?? null));
};
return open ? (
<div
role="dialog"
aria-label={editingAlert ? "Edit Alert" : "Create Alert"}
@@ -109,9 +133,14 @@ vi.mock("../alert-form-modal", () => ({
<button type="button" onClick={() => onOpenChange(false)}>
Close modal
</button>
<button type="button" onClick={submit}>
Submit alert
</button>
{editingAlert?.attributes.name}
{error && <p>{error}</p>}
</div>
) : null,
) : null;
},
}));
vi.mock("../alerts-empty-state", () => ({
@@ -259,6 +288,42 @@ describe("AlertsManager", () => {
});
});
it("shows a manage alerts permission message for edit 403 errors", async () => {
// Given
const user = userEvent.setup();
const alert = makeAlert(true);
actionMocks.updateAlert.mockResolvedValue({
error: "You do not have permission to perform this action.",
status: 403,
});
render(
<AlertsManager
alerts={[alert]}
loadError={null}
providers={[]}
completedScanIds={[]}
scanDetails={[]}
uniqueRegions={[]}
uniqueServices={[]}
uniqueResourceTypes={[]}
uniqueCategories={[]}
uniqueGroups={[]}
initialEditingAlert={alert}
/>,
);
// When
await user.click(screen.getByRole("button", { name: /submit alert/i }));
// Then
expect(
await screen.findByText(
"You don't have permission to manage alerts. Ask an administrator to update your role.",
),
).toBeVisible();
expect(toastMock).not.toHaveBeenCalled();
});
it("shows a success toast after disabling an alert", async () => {
// Given
const user = userEvent.setup();
@@ -451,6 +451,7 @@ const AlertFormModalContent = ({
? await seedAlertRule(pendingFilters)
: null;
if (seedResult?.error) {
setPreview(null);
setErrors({ root: ALERT_SEED_ERROR });
return;
}
@@ -49,6 +49,8 @@ interface AlertsManagerProps {
const ALERTS_FINDINGS_HREF =
"/findings?filter[muted]=false&filter[status__in]=FAIL";
const ALERTS_PERMISSION_ERROR =
"You don't have permission to manage alerts. Ask an administrator to update your role.";
export const AlertsManager = ({
alerts,
@@ -108,7 +110,12 @@ export const AlertsManager = ({
}
const payload = toAlertPayload(values);
const result = await updateAlert(editingAlert.id, payload);
if (result?.error) return { ok: false, error: result.error };
if (result?.error) {
return {
ok: false,
error: result.status === 403 ? ALERTS_PERMISSION_ERROR : result.error,
};
}
toast({
title: "Alert updated",
description: result.data.attributes.name,