diff --git a/playbooks/cli/fix_az_stor_006.sh b/playbooks/cli/fix_az_stor_006.sh new file mode 100644 index 0000000..a3ebfe3 --- /dev/null +++ b/playbooks/cli/fix_az_stor_006.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# OpenShield Remediation Playbook +# Rule: AZ-STOR-006 — Storage account HTTPS-only enforcement disabled +# Usage: ./fix_az_stor_006.sh +# Severity: HIGH + +set -euo pipefail + +RESOURCE_GROUP="${1:-}" +STORAGE_ACCOUNT="${2:-}" + +if [ -z "$RESOURCE_GROUP" ] || [ -z "$STORAGE_ACCOUNT" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Enabling HTTPS-only enforcement for storage account: ${STORAGE_ACCOUNT} (rg: ${RESOURCE_GROUP})" + +# Azure CLI command to enable HTTPS-only (secure transfer) +az storage account update \ + --name "${STORAGE_ACCOUNT}" \ + --resource-group "${RESOURCE_GROUP}" \ + --https-only true \ + --output json + +echo "Verification: current supportsHttpsTrafficOnly value:" +az storage account show \ + --name "${STORAGE_ACCOUNT}" \ + --resource-group "${RESOURCE_GROUP}" \ + --query "supportsHttpsTrafficOnly" \ + --output tsv + +echo "Remediation complete: HTTPS-only enforcement enabled for ${STORAGE_ACCOUNT}." diff --git a/scanner/rules/az_stor_006.py b/scanner/rules/az_stor_006.py new file mode 100644 index 0000000..f25ed53 --- /dev/null +++ b/scanner/rules/az_stor_006.py @@ -0,0 +1,109 @@ +""" +AZ-STOR-006: Storage account HTTPS-only enforcement disabled +""" + +import logging +from typing import Any, Dict, List, Optional + +logger = logging.getLogger(__name__) + +# ── Required module-level constants ───────────────────────────────────────── + +RULE_ID = "AZ-STOR-006" +RULE_NAME = "Storage Account HTTPS Only Enforcement Disabled" +SEVERITY = "HIGH" +CATEGORY = "Storage" +FRAMEWORKS = { + "CIS": "3.2", + "NIST": "SC-8", + "ISO27001": "A.10.1.1", + "SOC2": "CC6.1", +} +DESCRIPTION = ( + "The storage account does not enforce HTTPS-only traffic. When " + "`supportsHttpsTrafficOnly` is false, data can be transmitted over " + "unencrypted HTTP, exposing it to interception and downgrade attacks. " + "Enforce HTTPS-only to ensure secure transport for storage account traffic." +) +REMEDIATION = ( + "Enable HTTPS-only enforcement on the storage account by setting " + "`supportsHttpsTrafficOnly` to true. In the Azure portal: Storage Account " + "> Configuration > Secure transfer required > Enabled. Or use the Azure CLI: " + "`az storage account update --name --resource-group --https-only true`." +) +PLAYBOOK = "playbooks/cli/fix_az_stor_006.sh" + +# ── Required scan function ─────────────────────────────────────────────────── + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Detect storage accounts with HTTPS-only enforcement disabled. + + For each storage account returned by the azure_client, check the + `supportsHttpsTrafficOnly` property. If the property is False, emit a + finding. If the property cannot be determined (permissions or error), + skip and log a warning to avoid false positives. + + Returns: + A list of finding dicts for storage accounts where HTTPS-only is not enforced. + """ + findings: List[Dict[str, Any]] = [] + + for account in azure_client.get_storage_accounts(): + resource_id = getattr(account, "id", "") + account_name = getattr(account, "name", "") + location = getattr(account, "location", "") + + if not resource_id or not account_name: + continue + + parsed = azure_client.parse_resource_id(resource_id) + resource_group = parsed.get("resource_group", "") + if not resource_group: + continue + + # AzureClient implementations vary slightly across integrations: + # - some expose a dedicated get_storage_account_https_only() helper + # - others only surface the value on the resource's properties bag + if hasattr(azure_client, "get_storage_account_https_only"): + https_only: Optional[bool] = azure_client.get_storage_account_https_only( + resource_group, account_name + ) + else: + property_obj = getattr(account, "properties", None) + if property_obj is not None and hasattr(property_obj, "get"): + https_only = property_obj.get("supportsHttpsTrafficOnly") + else: + https_only = getattr(account, "supportsHttpsTrafficOnly", None) + + if https_only is None: + logger.warning( + "AZ-STOR-006: Could not determine HTTPS-only status for %s — skipping. " + "Ensure the service principal has Microsoft.Storage/storageAccounts/read permission.", + account_name, + ) + continue + + if https_only is False: + findings.append( + { + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": resource_id, + "resource_name": account_name, + "resource_type": "Microsoft.Storage/storageAccounts", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "resource_group": resource_group, + "location": location, + "supportsHttpsTrafficOnly": False, + }, + } + ) + + return findings diff --git a/tests/test_az_stor_006.py b/tests/test_az_stor_006.py new file mode 100644 index 0000000..3d05c8f --- /dev/null +++ b/tests/test_az_stor_006.py @@ -0,0 +1,60 @@ +"""Rule tests for AZ-STOR-006: HTTPS-only enforcement for storage accounts. + +These tests follow the same style as other storage rule tests in +[tests/test_rules_storage.py](/C:/Users/ACER/openshield/openshield/tests/test_rules_storage.py). +""" + +import scanner.rules.az_stor_006 as az_stor_006 +from tests.helpers.mock_azure import make_resource + +_SUB = "00000000-0000-0000-0000-000000000001" +_RG = "rg-test" + + +def _storage_id(name): + return f"/subscriptions/{_SUB}/resourceGroups/{_RG}/providers/Microsoft.Storage/storageAccounts/{name}" + + +def test_az_stor_006_compliant_returns_no_findings(mock_azure, subscription_id): + """A storage account with HTTPS-only enabled must produce no findings.""" + acct = make_resource( + id=_storage_id("https-only-storage"), + name="https-only-storage", + properties={"supportsHttpsTrafficOnly": True}, + ) + mock_azure.set_storage_accounts([acct]) + findings = az_stor_006.scan(mock_azure, subscription_id) + assert findings == [] + + +def test_az_stor_006_noncompliant_returns_one_finding(mock_azure, subscription_id): + """A storage account that allows HTTP traffic must produce exactly one finding.""" + acct = make_resource( + id=_storage_id("http-allowed-storage"), + name="http-allowed-storage", + properties={"supportsHttpsTrafficOnly": False}, + ) + mock_azure.set_storage_accounts([acct]) + findings = az_stor_006.scan(mock_azure, subscription_id) + assert len(findings) == 1 + finding = findings[0] + # basic schema checks similar to tests/test_rules_storage.py + assert finding["rule_id"] == "AZ-STOR-006" + assert finding["severity"] == "HIGH" + assert finding["category"] == "Storage" + assert finding["resource_name"] == "http-allowed-storage" + # message/description compatibility + text = finding.get("message") or finding.get("description", "") + assert "https" in text.lower() + + +def test_az_stor_006_unknown_status_skips(mock_azure, subscription_id): + """If the HTTPS status cannot be determined, the rule must not flag the resource.""" + acct = make_resource( + id=_storage_id("unknown-storage"), + name="unknown-storage", + properties={"supportsHttpsTrafficOnly": None}, + ) + mock_azure.set_storage_accounts([acct]) + findings = az_stor_006.scan(mock_azure, subscription_id) + assert findings == []