Skip to content
33 changes: 33 additions & 0 deletions playbooks/cli/fix_az_stor_006.sh
Original file line number Diff line number Diff line change
@@ -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 <resource-group> <storage-account-name>
# Severity: HIGH

set -euo pipefail

RESOURCE_GROUP="${1:-}"
STORAGE_ACCOUNT="${2:-}"

if [ -z "$RESOURCE_GROUP" ] || [ -z "$STORAGE_ACCOUNT" ]; then
echo "Usage: $0 <resource-group> <storage-account-name>"
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}."
109 changes: 109 additions & 0 deletions scanner/rules/az_stor_006.py
Original file line number Diff line number Diff line change
@@ -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 <account> --resource-group <rg> --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
60 changes: 60 additions & 0 deletions tests/test_az_stor_006.py
Original file line number Diff line number Diff line change
@@ -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 == []
Loading