diff --git a/ai/knowledge/skills/post-quantum-cryptography-azure/SKILL.md b/ai/knowledge/skills/post-quantum-cryptography-azure/SKILL.md new file mode 100644 index 0000000..9037fe8 --- /dev/null +++ b/ai/knowledge/skills/post-quantum-cryptography-azure/SKILL.md @@ -0,0 +1,89 @@ +--- +name: post-quantum-cryptography-azure +description: Identifies and remediates non-quantum-safe cryptographic configurations in Azure including classical TLS key exchange, RSA and ECC keys in Key Vault, and classical certificate algorithms. Maps findings to NIST PQC standards FIPS 203, FIPS 204, and FIPS 205. Use when assessing quantum readiness of Azure infrastructure or building a Cryptographic Bill of Materials. +domain: cybersecurity +subdomain: post-quantum-cryptography +tags: +- post-quantum +- pqc +- azure +- key-vault +- tls +- cryptography +- cbom +version: '1.0' +author: openshield +license: Apache-2.0 +nist_csf: +- PR.DS-2 +- PR.DS-1 +--- + +# Post-Quantum Cryptography Assessment for Azure + +## When to Use +- When assessing an Azure environment for quantum readiness +- When building a Cryptographic Bill of Materials for Azure resources +- When identifying classical cryptographic algorithms that need migration +- When planning post-quantum migration for Key Vault keys and certificates +- When evaluating TLS configurations for quantum vulnerability + +## Key Concepts + +| Term | Definition | +|------|------------| +| Harvest Now Decrypt Later | Attack where adversaries collect encrypted traffic today and decrypt it when quantum computers become available | +| Shor's Algorithm | Quantum algorithm that can break RSA and ECC by solving integer factorisation and discrete logarithm problems efficiently | +| ML-KEM | Module Lattice Key Encapsulation Mechanism, NIST FIPS 203, post-quantum safe key exchange | +| ML-DSA | Module Lattice Digital Signature Algorithm, NIST FIPS 204, post-quantum safe signing | +| SLH-DSA | Stateless Hash-Based Digital Signature Algorithm, NIST FIPS 205, post-quantum safe signing | +| CBOM | Cryptographic Bill of Materials, inventory of all cryptographic assets in a system | +| PQMA | Post-Quantum Migration Analyser, tool for validating PQC migration paths | + +## OpenShield PQC Rules + +| Rule | Description | Severity | +|------|-------------|----------| +| AZ-PQC-001 | TLS below 1.3 on App Service | HIGH | +| AZ-PQC-002 | Key Vault key using RSA or ECC algorithm | HIGH | +| AZ-PQC-003 | Key Vault certificate using non-quantum-safe signature algorithm | MEDIUM | + +## Assessment Workflow + +### Step 1: Identify Classical Keys in Key Vault +```bash +az keyvault list --output table +az keyvault key list --vault-name --output table +az keyvault certificate list --vault-name --output table +``` + +### Step 2: Check TLS Configuration on App Services +```bash +az webapp list --output table +az webapp config show --resource-group --name --query minTlsVersion +``` + +### Step 3: Build Cryptographic Bill of Materials +Document all findings with resource ID, algorithm type, key size, expiry date, and dependent services. + +### Step 4: Prioritise Migration +1. Keys and certificates exposed to internet traffic first +2. Long-lived keys with high blast radius second +3. Internal service-to-service encryption third + +## NIST PQC Standards Reference + +| Standard | Algorithm | Use Case | +|----------|-----------|----------| +| FIPS 203 | ML-KEM | Key encapsulation, replacing RSA and ECDH key exchange | +| FIPS 204 | ML-DSA | Digital signatures, replacing RSA-PSS and ECDSA | +| FIPS 205 | SLH-DSA | Digital signatures, hash-based alternative | + +## Compliance Mapping + +| Framework | Control | Requirement | +|-----------|---------|-------------| +| NIST CSF | PR.DS-2 | Data in transit is protected using quantum-safe algorithms | +| ISO 27001 | A.10.1.1 | Cryptographic controls policy must address quantum threats | +| CIS Azure | 8.1 | Key management must include post-quantum migration planning | +| SOC 2 | CC6.7 | Encryption protecting data in transit must be quantum-safe | diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index aedfde7..91661e8 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -172,6 +172,21 @@ "control_id": "6.4", "control_name": "Ensure that Azure Firewall is enabled on Virtual Networks", "description": "VNet peering connections with allowGatewayTransit or useRemoteGateways enabled allow traffic to route between network segments through shared gateways. This can break network segmentation and enable lateral movement between zones that should remain isolated. Peering connections should be reviewed and gateway transit disabled unless explicitly required and documented." + }, + "AZ-PQC-001": { + "control_id": "9.1", + "control_name": "Ensure TLS is enforced with quantum-safe configuration", + "description": "App Services configured with TLS versions below 1.3 use classical key exchange algorithms vulnerable to Harvest Now Decrypt Later attacks. CIS 9.1 requires that data in transit is protected using current encryption standards. Enforcing TLS 1.3 minimum reduces exposure to quantum-enabled decryption of captured traffic." + }, + "AZ-PQC-002": { + "control_id": "8.1", + "control_name": "Ensure Key Vault keys use quantum-safe algorithms", + "description": "Key Vault keys using RSA or ECC algorithms are vulnerable to Shor's algorithm on quantum computers. CIS 8.1 requires that cryptographic key management follows current standards. Keys should be inventoried in a Cryptographic Bill of Materials and migration to post-quantum safe algorithms planned." + }, + "AZ-PQC-003": { + "control_id": "8.5", + "control_name": "Ensure certificates use quantum-safe signature algorithms", + "description": "Key Vault certificates signed with RSA or ECDSA are vulnerable to quantum attacks. CIS 8.5 requires that certificate management includes monitoring of algorithm strength. Certificates should be migrated to post-quantum safe signature algorithms such as ML-DSA when CA support is available." } } } diff --git a/compliance/frameworks/iso27001.json b/compliance/frameworks/iso27001.json index f9e3f97..a777e39 100644 --- a/compliance/frameworks/iso27001.json +++ b/compliance/frameworks/iso27001.json @@ -172,6 +172,21 @@ "control_id": "A.13.1.1", "control_name": "Network controls", "description": "VNet peering connections with gateway transit enabled allow traffic to flow between network segments through shared gateways, potentially bypassing network controls. Networks should be managed and controlled to protect information in systems and applications. Gateway transit on peering connections should be disabled unless explicitly required." + }, + "AZ-PQC-001": { + "control_id": "A.10.1.1", + "control_name": "Policy on the use of cryptographic controls", + "description": "TLS configurations using classical key exchange algorithms do not align with a forward-looking cryptographic controls policy. A.10.1.1 requires that the organisation defines rules for effective use of cryptography. The policy must address post-quantum threats and mandate migration to quantum-safe cipher suites when supported." + }, + "AZ-PQC-002": { + "control_id": "A.10.1.1", + "control_name": "Policy on the use of cryptographic controls", + "description": "Key Vault keys using RSA or ECC do not meet the requirements of a cryptographic controls policy that accounts for quantum threats. A.10.1.1 requires that cryptographic controls are appropriate to the level of risk. Post-quantum safe algorithms must be adopted as part of the cryptographic policy when supported." + }, + "AZ-PQC-003": { + "control_id": "A.10.1.1", + "control_name": "Policy on the use of cryptographic controls", + "description": "Certificates using classical signature algorithms expose the organisation to quantum-enabled signature forgery. A.10.1.1 requires that the cryptographic controls policy covers all cryptographic assets including certificates. Migration planning to post-quantum safe signature algorithms is required." } } } diff --git a/compliance/frameworks/nist_csf.json b/compliance/frameworks/nist_csf.json index 30ae4b5..c592c21 100644 --- a/compliance/frameworks/nist_csf.json +++ b/compliance/frameworks/nist_csf.json @@ -172,6 +172,21 @@ "control_id": "PR.AC-5", "control_name": "Network integrity is protected", "description": "VNet peering with gateway transit enabled allows traffic to cross network boundaries through shared gateways, undermining network segmentation. PR.AC-5 requires that network integrity is protected. Disabling gateway transit on peering connections enforces boundary integrity between network zones." + }, + "AZ-PQC-001": { + "control_id": "PR.DS-2", + "control_name": "Data in transit is protected", + "description": "TLS configurations using classical key exchange algorithms expose data in transit to Harvest Now Decrypt Later attacks. PR.DS-2 requires that data in transit is protected. Migrating to TLS 1.3 and post-quantum safe cipher suites when supported helps data remain protected against quantum-enabled adversaries." + }, + "AZ-PQC-002": { + "control_id": "PR.DS-2", + "control_name": "Data in transit is protected", + "description": "Key Vault keys using RSA or ECC can be broken by Shor's algorithm on quantum computers, compromising protected data and signatures. PR.DS-2 requires that data protection mechanisms are maintained. Post-quantum safe key encapsulation algorithms such as ML-KEM should replace classical alternatives when supported." + }, + "AZ-PQC-003": { + "control_id": "PR.DS-2", + "control_name": "Data in transit is protected", + "description": "Certificates using classical signature algorithms are vulnerable to quantum attacks, undermining authentication and integrity guarantees. PR.DS-2 requires that data protection includes integrity mechanisms. Migration to ML-DSA or SLH-DSA signature algorithms should be planned." } } } diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index 3bf94d0..285fb41 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -163,10 +163,25 @@ "control_name": "Restricts Access from Outside the Network Boundary", "description": "A virtual network without an Azure Firewall relies on NSGs alone and lacks a centralized point to inspect, filter, and log traffic crossing the network boundary. CC6.6 requires that logical access from outside the network boundary is restricted and controlled. Deploying an Azure Firewall enforces inspected, logged perimeter access for the network." }, - "AZ-NET-014": { + "AZ-NET-014": { "control_id": "CC6.6", "control_name": "Restricts Access from Outside the Network Boundary", "description": "VNet peering with allowGatewayTransit or useRemoteGateways enabled allows traffic to cross network boundaries through shared gateways, weakening the logical separation between network zones. CC6.6 requires that logical access from outside the network boundary is restricted and controlled. Gateway transit on peering connections should be disabled to enforce boundary separation." + }, + "AZ-PQC-001": { + "control_id": "CC6.7", + "control_name": "Protects Data in Transit", + "description": "TLS configurations using classical key exchange algorithms expose data in transit to Harvest Now Decrypt Later attacks where adversaries collect traffic today and decrypt it with future quantum computers. CC6.7 requires that data transmitted over networks is protected using encryption. Enforcing TLS 1.3 minimum reduces this risk." + }, + "AZ-PQC-002": { + "control_id": "CC6.7", + "control_name": "Protects Data in Transit", + "description": "Key Vault keys using RSA or ECC will be vulnerable to Shor's algorithm, compromising data encrypted or signed with these keys. CC6.7 requires that data is protected using encryption. Post-quantum safe key encapsulation algorithms should replace classical alternatives when supported to maintain this protection." + }, + "AZ-PQC-003": { + "control_id": "CC6.7", + "control_name": "Protects Data in Transit", + "description": "Certificates using classical signature algorithms will be vulnerable to quantum-enabled forgery, undermining authentication and data integrity. CC6.7 requires that data integrity is maintained through encryption and signing. Migration to post-quantum safe certificate algorithms should be planned." } } } diff --git a/playbooks/cli/fix_az_pqc_001.sh b/playbooks/cli/fix_az_pqc_001.sh new file mode 100755 index 0000000..7cff288 --- /dev/null +++ b/playbooks/cli/fix_az_pqc_001.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +# Playbook: fix_az_pqc_001.sh +# Rule: AZ-PQC-001 - TLS using classical key exchange algorithm + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 " + exit 1 +fi + +RESOURCE_GROUP="$1" +APP_NAME="$2" + +echo "Enforcing TLS 1.3 minimum on App Service: $APP_NAME" +az webapp config set \ + --resource-group "$RESOURCE_GROUP" \ + --name "$APP_NAME" \ + --min-tls-version 1.3 \ + --output none + +echo "Done. Verify with:" +echo " az webapp config show --resource-group $RESOURCE_GROUP --name $APP_NAME --query minTlsVersion" diff --git a/playbooks/cli/fix_az_pqc_002.sh b/playbooks/cli/fix_az_pqc_002.sh new file mode 100755 index 0000000..1d2099f --- /dev/null +++ b/playbooks/cli/fix_az_pqc_002.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -euo pipefail + +# Playbook: fix_az_pqc_002.sh +# Rule: AZ-PQC-002 - Key Vault key using non-quantum-safe algorithm + +if [[ $# -lt 3 ]]; then + echo "Usage: $0 " + exit 1 +fi + +RESOURCE_GROUP="$1" +VAULT_NAME="$2" +KEY_NAME="$3" + +echo "Listing current key properties for: $KEY_NAME in vault: $VAULT_NAME" +az keyvault key show \ + --vault-name "$VAULT_NAME" \ + --name "$KEY_NAME" \ + --output table + +echo "" +echo "Next steps:" +echo " 1. Review all workloads using this key and plan migration." +echo " 2. Generate a new key using a post-quantum safe algorithm when supported." +echo " 3. Document this key in your Cryptographic Bill of Materials (CBOM)." +echo " 4. Update all dependent services to use the new key." +echo " 5. Disable and schedule deletion of the old key after migration." +echo "" +echo "Verify existing keys with:" +echo " az keyvault key list --vault-name $VAULT_NAME --output table" diff --git a/playbooks/cli/fix_az_pqc_003.sh b/playbooks/cli/fix_az_pqc_003.sh new file mode 100755 index 0000000..9c1dce6 --- /dev/null +++ b/playbooks/cli/fix_az_pqc_003.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -euo pipefail + +# Playbook: fix_az_pqc_003.sh +# Rule: AZ-PQC-003 - Key Vault certificate using non-quantum-safe algorithm + +if [[ $# -lt 3 ]]; then + echo "Usage: $0 " + exit 1 +fi + +RESOURCE_GROUP="$1" +VAULT_NAME="$2" +CERT_NAME="$3" + +echo "Listing current certificate properties for: $CERT_NAME in vault: $VAULT_NAME" +az keyvault certificate show \ + --vault-name "$VAULT_NAME" \ + --name "$CERT_NAME" \ + --output table + +echo "" +echo "Next steps:" +echo " 1. Identify the CA issuing this certificate." +echo " 2. Check if the CA supports post-quantum safe signature algorithms." +echo " 3. Document this certificate in your Cryptographic Bill of Materials." +echo " 4. Plan certificate renewal with a post-quantum safe algorithm." +echo " 5. Update all services using this certificate before expiry." +echo "" +echo "Verify existing certificates with:" +echo " az keyvault certificate list --vault-name $VAULT_NAME --output table" diff --git a/requirements.txt b/requirements.txt index 43d9ede..5326224 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ azure-mgmt-sql==3.0.1 azure-mgmt-keyvault==10.3.0 azure-mgmt-rdbms==10.1.0 azure-mgmt-authorization==4.0.0 +azure-mgmt-web==7.3.1 azure-monitor-ingestion==1.0.3 azure-mgmt-monitor==6.0.0 psycopg2-binary==2.9.9 @@ -21,5 +22,6 @@ cryptography==42.0.5 msrest==0.7.1 azure-mgmt-postgresqlflexibleservers==1.0.0b1 azure-keyvault-certificates==4.8.0 +azure-keyvault-keys==4.9.0 chromadb==0.4.24 sentence-transformers==2.7.0 diff --git a/scanner/azure_client.py b/scanner/azure_client.py index ef331ea..069c80b 100644 --- a/scanner/azure_client.py +++ b/scanner/azure_client.py @@ -298,6 +298,16 @@ def get_virtual_machines(self) -> List[Any]: logger.error("get_virtual_machines failed: %s", exc) return [] + def get_web_apps(self) -> List[Any]: + """List all App Services in the subscription.""" + try: + from azure.mgmt.web import WebSiteManagementClient + client = WebSiteManagementClient(self.credential, self.subscription_id) + return list(client.web_apps.list()) + except Exception as exc: + logger.error("get_web_apps failed: %s", exc) + return [] + def get_vm_extensions( self, resource_group: str, vm_name: str ) -> Optional[List[Any]]: @@ -387,6 +397,17 @@ def get_key_vault_certificates(self, vault_name: str) -> List[Any]: ) return [] + def get_key_vault_keys(self, vault_name: str) -> List[Any]: + """List all keys in a Key Vault using the Key Vault data plane API.""" + try: + from azure.keyvault.keys import KeyClient + vault_url = f"https://{vault_name}.vault.azure.net" + client = KeyClient(vault_url=vault_url, credential=self.credential) + return list(client.list_properties_of_keys()) + except Exception as exc: + logger.error("get_key_vault_keys(%s) failed: %s", vault_name, exc) + return [] + # ------------------------------------------------------------------ # # Monitoring # # ------------------------------------------------------------------ # @@ -533,4 +554,4 @@ def get_network_watcher_regions(self) -> List[str]: return list(regions) except Exception as exc: logger.error("get_network_watcher_regions failed: %s", exc) - return [] \ No newline at end of file + return [] diff --git a/scanner/rules/az_pqc_001.py b/scanner/rules/az_pqc_001.py new file mode 100644 index 0000000..7010ef0 --- /dev/null +++ b/scanner/rules/az_pqc_001.py @@ -0,0 +1,78 @@ +"""AZ-PQC-001: App Service TLS below 1.3.""" + +import logging +from typing import Any, Dict, List + +RULE_ID = "AZ-PQC-001" +RULE_NAME = "TLS Using Classical Key Exchange Algorithm" +SEVERITY = "HIGH" +CATEGORY = "PostQuantum" +FRAMEWORKS = { + "CIS": "9.1", + "NIST": "PR.DS-2", + "ISO27001": "A.10.1.1", + "SOC2": "CC6.7", +} +DESCRIPTION = ( + "The resource is configured with TLS using classical key exchange algorithms " + "such as RSA or ECDH. These algorithms are vulnerable to Harvest Now Decrypt " + "Later attacks where adversaries collect encrypted traffic today and decrypt " + "it once quantum computers are available. Post-quantum safe key exchange " + "algorithms should be used." +) +REMEDIATION = ( + "Migrate TLS configuration to use post-quantum safe key exchange. Update App " + "Service TLS policies to enforce TLS 1.3 and plan adoption of quantum-safe " + "cipher suites when supported. See playbooks/cli/fix_az_pqc_001.sh for " + "remediation steps." +) +PLAYBOOK = "playbooks/cli/fix_az_pqc_001.sh" + +logger = logging.getLogger(__name__) + + +def _tls_version_below_13(version: Any) -> bool: + if version is None: + return False + try: + major, minor = str(version).split(".", maxsplit=1) + return (int(major), int(minor)) < (1, 3) + except (TypeError, ValueError): + return str(version) < "1.3" + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Scan App Services for TLS versions below 1.3.""" + findings: List[Dict[str, Any]] = [] + + web_apps = azure_client.get_web_apps() + if web_apps is None: + logger.warning("AZ-PQC-001 skipped: unable to list web apps.") + return findings + + for app in web_apps: + app_id = getattr(app, "id", "") or "" + parsed = azure_client.parse_resource_id(app_id) + site_config = getattr(app, "site_config", None) + min_tls = getattr(site_config, "min_tls_version", None) if site_config else None + + if _tls_version_below_13(min_tls): + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": app_id, + "resource_name": getattr(app, "name", ""), + "resource_type": "Microsoft.Web/sites", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "resource_group": parsed.get("resource_group", ""), + "min_tls_version": str(min_tls), + }, + }) + + return findings diff --git a/scanner/rules/az_pqc_002.py b/scanner/rules/az_pqc_002.py new file mode 100644 index 0000000..4b19af5 --- /dev/null +++ b/scanner/rules/az_pqc_002.py @@ -0,0 +1,88 @@ +"""AZ-PQC-002: Key Vault keys using RSA or ECC algorithms.""" + +import logging +from typing import Any, Dict, List + +RULE_ID = "AZ-PQC-002" +RULE_NAME = "Key Vault Key Using Non-Quantum-Safe Algorithm" +SEVERITY = "HIGH" +CATEGORY = "PostQuantum" +FRAMEWORKS = { + "CIS": "8.1", + "NIST": "PR.DS-2", + "ISO27001": "A.10.1.1", + "SOC2": "CC6.7", +} +DESCRIPTION = ( + "The Key Vault contains keys using RSA or ECC algorithms which are vulnerable " + "to quantum attacks using Shor's algorithm. A sufficiently powerful quantum " + "computer can break RSA and ECC keys, compromising data encrypted or signed " + "with these keys. Keys should be migrated to post-quantum safe algorithms " + "such as those standardised by NIST in FIPS 203, FIPS 204, and FIPS 205." +) +REMEDIATION = ( + "Identify all RSA and ECC keys in Key Vault and plan migration to " + "post-quantum safe alternatives. For signing use ML-DSA (FIPS 204). For key " + "encapsulation use ML-KEM (FIPS 203). Document all keys requiring migration " + "in a Cryptographic Bill of Materials (CBOM). See " + "playbooks/cli/fix_az_pqc_002.sh for remediation steps." +) +PLAYBOOK = "playbooks/cli/fix_az_pqc_002.sh" + +logger = logging.getLogger(__name__) + +_CLASSICAL_KEY_TYPES = {"RSA", "EC", "EC-HSM", "RSA-HSM"} + + +def _key_type_value(key: Any) -> str: + key_type = getattr(key, "key_type", None) + return str(getattr(key_type, "value", None) or key_type or "") + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Scan Key Vault keys for non-quantum-safe algorithm usage.""" + findings: List[Dict[str, Any]] = [] + + vaults = azure_client.get_key_vaults() + if vaults is None: + logger.warning("AZ-PQC-002 skipped: unable to list Key Vaults.") + return findings + + for vault in vaults: + vault_id = getattr(vault, "id", "") or "" + vault_name = getattr(vault, "name", "") or azure_client.parse_resource_id( + vault_id + ).get("name", "") + parsed = azure_client.parse_resource_id(vault_id) + resource_group = parsed.get("resource_group", "") + + keys = azure_client.get_key_vault_keys(vault_name) + if keys is None: + logger.warning("AZ-PQC-002: unable to list keys for vault %s", vault_name) + continue + + for key in keys: + key_type = _key_type_value(key) + if key_type.upper() in _CLASSICAL_KEY_TYPES: + key_id = getattr(key, "id", "") or f"{vault_id}/keys/{getattr(key, 'name', '')}" + key_name = getattr(key, "name", "") or key_id.rstrip("/").split("/")[-1] + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": key_id, + "resource_name": key_name, + "resource_type": "Microsoft.KeyVault/vaults/keys", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "resource_group": resource_group, + "vault_name": vault_name, + "key_type": key_type, + }, + }) + + return findings diff --git a/scanner/rules/az_pqc_003.py b/scanner/rules/az_pqc_003.py new file mode 100644 index 0000000..3307d04 --- /dev/null +++ b/scanner/rules/az_pqc_003.py @@ -0,0 +1,91 @@ +"""AZ-PQC-003: Key Vault certificates using classical algorithms.""" + +import logging +from typing import Any, Dict, List + +RULE_ID = "AZ-PQC-003" +RULE_NAME = "Key Vault Certificate Using Non-Quantum-Safe Signature Algorithm" +SEVERITY = "MEDIUM" +CATEGORY = "PostQuantum" +FRAMEWORKS = { + "CIS": "8.5", + "NIST": "PR.DS-2", + "ISO27001": "A.10.1.1", + "SOC2": "CC6.7", +} +DESCRIPTION = ( + "The Key Vault contains certificates signed using RSA or ECDSA algorithms. " + "These classical signature schemes are vulnerable to Shor's algorithm on " + "quantum computers. Certificates used for authentication, TLS, and code " + "signing must be migrated to post-quantum safe signature algorithms such " + "as ML-DSA (FIPS 204) or SLH-DSA (FIPS 205)." +) +REMEDIATION = ( + "Audit all Key Vault certificates and identify those using RSA or ECDSA. " + "Plan migration to post-quantum safe certificate authorities and signature " + "algorithms. Include all certificates in your Cryptographic Bill of " + "Materials. See playbooks/cli/fix_az_pqc_003.sh for remediation steps." +) +PLAYBOOK = "playbooks/cli/fix_az_pqc_003.sh" + +logger = logging.getLogger(__name__) + +_CLASSICAL_KEY_TYPES = {"RSA", "EC", "EC-HSM", "RSA-HSM"} + + +def _certificate_key_type(cert: Any) -> str: + policy = getattr(cert, "policy", None) + key_props = getattr(policy, "key_properties", None) if policy else None + key_type = getattr(key_props, "key_type", None) if key_props else None + return str(getattr(key_type, "value", None) or key_type or "") + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Scan Key Vault certificates for non-quantum-safe algorithms.""" + findings: List[Dict[str, Any]] = [] + + vaults = azure_client.get_key_vaults() + if vaults is None: + logger.warning("AZ-PQC-003 skipped: unable to list Key Vaults.") + return findings + + for vault in vaults: + vault_id = getattr(vault, "id", "") or "" + vault_name = getattr(vault, "name", "") or azure_client.parse_resource_id( + vault_id + ).get("name", "") + parsed = azure_client.parse_resource_id(vault_id) + resource_group = parsed.get("resource_group", "") + + certs = azure_client.get_key_vault_certificates(vault_name) + if certs is None: + logger.warning( + "AZ-PQC-003: unable to list certificates for vault %s", vault_name + ) + continue + + for cert in certs: + key_type = _certificate_key_type(cert) + if key_type.upper() in _CLASSICAL_KEY_TYPES: + cert_id = getattr(cert, "id", "") or f"{vault_id}/certificates/{getattr(cert, 'name', '')}" + cert_name = getattr(cert, "name", "") or cert_id.rstrip("/").split("/")[-1] + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": cert_id, + "resource_name": cert_name, + "resource_type": "Microsoft.KeyVault/vaults/certificates", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "resource_group": resource_group, + "vault_name": vault_name, + "key_type": key_type, + }, + }) + + return findings diff --git a/tests/test_pqc_rules.py b/tests/test_pqc_rules.py new file mode 100644 index 0000000..4ffbcf3 --- /dev/null +++ b/tests/test_pqc_rules.py @@ -0,0 +1,129 @@ +"""Unit tests for post-quantum Azure rule modules.""" + +from types import SimpleNamespace + +from scanner.rules import az_pqc_001, az_pqc_002, az_pqc_003 + + +_VAULT_ID = ( + "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.KeyVault/vaults/vault1" +) + + +class FakeAzureClient: + def __init__(self, web_apps=None, vaults=None, keys=None, certificates=None): + self._web_apps = web_apps if web_apps is not None else [] + self._vaults = vaults if vaults is not None else [] + self._keys = keys if keys is not None else [] + self._certificates = certificates if certificates is not None else [] + + def get_web_apps(self): + return self._web_apps + + def get_key_vaults(self): + return self._vaults + + def get_key_vault_keys(self, vault_name): + return self._keys + + def get_key_vault_certificates(self, vault_name): + return self._certificates + + @staticmethod + def parse_resource_id(resource_id): + parts = resource_id.split("/") + result = {"name": parts[-1]} + if "resourceGroups" in parts: + result["resource_group"] = parts[parts.index("resourceGroups") + 1] + return result + + +def _enum_value(value): + return SimpleNamespace(value=value) + + +def test_pqc_001_flags_app_service_tls_below_13(): + app = SimpleNamespace( + id="/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/app1", + name="app1", + site_config=SimpleNamespace(min_tls_version="1.2"), + ) + client = FakeAzureClient(web_apps=[app]) + + findings = az_pqc_001.scan(client, "sub") + + assert len(findings) == 1 + assert findings[0]["rule_id"] == "AZ-PQC-001" + assert findings[0]["metadata"]["min_tls_version"] == "1.2" + + +def test_pqc_001_ignores_tls_13(): + app = SimpleNamespace( + id="/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/app1", + name="app1", + site_config=SimpleNamespace(min_tls_version="1.3"), + ) + client = FakeAzureClient(web_apps=[app]) + + assert az_pqc_001.scan(client, "sub") == [] + + +def test_pqc_002_flags_rsa_key_vault_key(): + vault = SimpleNamespace(id=_VAULT_ID, name="vault1") + key = SimpleNamespace( + id=f"{_VAULT_ID}/keys/key1", + name="key1", + key_type=_enum_value("RSA"), + ) + client = FakeAzureClient(vaults=[vault], keys=[key]) + + findings = az_pqc_002.scan(client, "sub") + + assert len(findings) == 1 + assert findings[0]["rule_id"] == "AZ-PQC-002" + assert findings[0]["metadata"]["key_type"] == "RSA" + assert findings[0]["metadata"]["vault_name"] == "vault1" + + +def test_pqc_002_ignores_non_classical_key_type(): + vault = SimpleNamespace(id=_VAULT_ID, name="vault1") + key = SimpleNamespace( + id=f"{_VAULT_ID}/keys/key1", + name="key1", + key_type=_enum_value("oct-HSM"), + ) + client = FakeAzureClient(vaults=[vault], keys=[key]) + + assert az_pqc_002.scan(client, "sub") == [] + + +def test_pqc_003_flags_classical_certificate_policy_key_type(): + vault = SimpleNamespace(id=_VAULT_ID, name="vault1") + cert = SimpleNamespace( + id=f"{_VAULT_ID}/certificates/cert1", + name="cert1", + policy=SimpleNamespace( + key_properties=SimpleNamespace(key_type=_enum_value("EC")) + ), + ) + client = FakeAzureClient(vaults=[vault], certificates=[cert]) + + findings = az_pqc_003.scan(client, "sub") + + assert len(findings) == 1 + assert findings[0]["rule_id"] == "AZ-PQC-003" + assert findings[0]["metadata"]["key_type"] == "EC" + + +def test_pqc_003_ignores_certificate_without_classical_policy_key_type(): + vault = SimpleNamespace(id=_VAULT_ID, name="vault1") + cert = SimpleNamespace( + id=f"{_VAULT_ID}/certificates/cert1", + name="cert1", + policy=SimpleNamespace( + key_properties=SimpleNamespace(key_type=_enum_value("ML-DSA")) + ), + ) + client = FakeAzureClient(vaults=[vault], certificates=[cert]) + + assert az_pqc_003.scan(client, "sub") == []