diff --git a/README.md b/README.md index e8002922..f7d40c24 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,10 @@ Findings map to NIST FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA | Feature | Description | |---|---| -| **Misconfiguration Scanner** | Runs 144 Azure security rules across storage, network, identity, database, compute, Key Vault, AKS, Kubernetes workloads, post-quantum cryptography, backup, serverless, private endpoint, and supply chain posture | +| **Misconfiguration Scanner** | Runs 154 Azure security rules across storage, network, identity, database, compute, Key Vault, AKS, Kubernetes workloads, post-quantum cryptography, backup, serverless, private endpoint, supply chain, and governance posture | | **Compliance Mapper** | Maps findings to CIS Benchmarks, NIST CSF, ISO 27001, and SOC 2 framework JSON files | | **Scan History API** | Stores scans and findings in PostgreSQL and exposes findings, score, scan history, compliance posture, drift, and resource inventory over REST | -| **Remediation Playbooks** | Every documented rule ships with a matching review-gated remediation script (144 playbooks) | +| **Remediation Playbooks** | Every documented rule ships with a matching review-gated remediation script (154 playbooks) | | **Security Dashboard** | Full React dashboard deployed on Vercel - live monitoring, findings, compliance, drift, prioritization, and AI-layer views | | **Project Website** | Documentation and reference site at [owasp.github.io/openshield](https://owasp.github.io/openshield/) - blog, rules gallery, architecture, evidence guides, roadmap, and releases | | **Sentinel Integration** | Normalises findings and pushes them into Microsoft Sentinel via a Log Analytics custom table and KQL analytics rules | @@ -120,11 +120,11 @@ Project policies and assurance evidence: flowchart TD A["React Dashboard\nVercel · Live"] B["Flask REST API\nJWT · CORS · Blueprints"] - C["Scanner Engine\n144 Python rules"] + C["Scanner Engine\n154 Python rules"] D["Azure Subscription\nScanned via Azure SDK + Graph"] E["Compliance Framework JSON\nCIS · NIST · ISO 27001 · SOC 2"] F["PostgreSQL Database\nFindings · Scans"] - G["Azure CLI Playbooks\n144 remediation scripts"] + G["Azure CLI Playbooks\n154 remediation scripts"] H["sentinel/ingest.py\nNormalise + HMAC upload"] I["Microsoft Sentinel\nOpenShieldFindings_CL · KQL rules"] diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index f3181a4e..d6e9b14b 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -1734,6 +1734,126 @@ "owner": null, "review_status": "pending_review", "review_date": null + }, + "AZ-GOV-001": { + "control_id": "N/A-GOV-001", + "control_name": "Subscription Outside Approved Management Group Hierarchy", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-001. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-002": { + "control_id": "N/A-GOV-002", + "control_name": "Required Security Policy Initiative Missing", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-002. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-003": { + "control_id": "N/A-GOV-003", + "control_name": "Preventive Policy Uses Non-Enforcing Effect", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-003. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-004": { + "control_id": "N/A-GOV-004", + "control_name": "Policy Exemption Missing Governance Metadata", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-004. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-005": { + "control_id": "N/A-GOV-005", + "control_name": "Critical Production Resource Missing Deletion Lock", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-005. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-006": { + "control_id": "N/A-GOV-006", + "control_name": "Excessive Subscription Owner Assignments", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-006. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-007": { + "control_id": "N/A-GOV-007", + "control_name": "Privileged Access Assigned at Broad Scope", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-007. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-008": { + "control_id": "N/A-GOV-008", + "control_name": "Undocumented Resource Provider Registered", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-008. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-009": { + "control_id": "N/A-GOV-009", + "control_name": "Production Resource Missing Ownership Metadata", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-009. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-010": { + "control_id": "N/A-GOV-010", + "control_name": "Security Configuration Drift Exceeds SLA", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "not_applicable", + "evidence_type": "no_direct_mapping", + "primary_source": "OpenShield compliance mapping pack, authored against CIS Microsoft Azure Foundations Benchmark v2.0.0. Technical-evidence mapping only.", + "rationale": "No numbered control in this mapping pack directly maps to AZ-GOV-010. This rule is excluded from this framework score.", + "owner": null, + "review_status": "pending_review", + "review_date": null } } } diff --git a/compliance/frameworks/iso27001.json b/compliance/frameworks/iso27001.json index 5ccaf205..f297f8db 100644 --- a/compliance/frameworks/iso27001.json +++ b/compliance/frameworks/iso27001.json @@ -1734,6 +1734,126 @@ "owner": null, "review_status": "pending_review", "review_date": null + }, + "AZ-GOV-001": { + "control_id": "A.5.15", + "control_name": "Access control", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.5.15", + "rationale": "AZ-GOV-001 supplies Azure configuration evidence supporting A.5.15. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-002": { + "control_id": "A.5.1", + "control_name": "Policies for information security", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.5.1", + "rationale": "AZ-GOV-002 supplies Azure configuration evidence supporting A.5.1. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-003": { + "control_id": "A.5.1", + "control_name": "Policies for information security", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.5.1", + "rationale": "AZ-GOV-003 supplies Azure configuration evidence supporting A.5.1. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-004": { + "control_id": "A.5.1", + "control_name": "Policies for information security", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.5.1", + "rationale": "AZ-GOV-004 supplies Azure configuration evidence supporting A.5.1. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-005": { + "control_id": "A.8.9", + "control_name": "Configuration management", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.8.9", + "rationale": "AZ-GOV-005 supplies Azure configuration evidence supporting A.8.9. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-006": { + "control_id": "A.5.18", + "control_name": "Access rights", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.5.18", + "rationale": "AZ-GOV-006 supplies Azure configuration evidence supporting A.5.18. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-007": { + "control_id": "A.5.18", + "control_name": "Access rights", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.5.18", + "rationale": "AZ-GOV-007 supplies Azure configuration evidence supporting A.5.18. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-008": { + "control_id": "A.5.9", + "control_name": "Inventory of information and other associated assets", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.5.9", + "rationale": "AZ-GOV-008 supplies Azure configuration evidence supporting A.5.9. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-009": { + "control_id": "A.5.9", + "control_name": "Inventory of information and other associated assets", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.5.9", + "rationale": "AZ-GOV-009 supplies Azure configuration evidence supporting A.5.9. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-010": { + "control_id": "A.8.8", + "control_name": "Management of technical vulnerabilities", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "ISO/IEC 27001:2022, Annex A control A.8.8", + "rationale": "AZ-GOV-010 supplies Azure configuration evidence supporting A.8.8. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null } } } diff --git a/compliance/frameworks/nist_csf.json b/compliance/frameworks/nist_csf.json index 742110fd..180b44cd 100644 --- a/compliance/frameworks/nist_csf.json +++ b/compliance/frameworks/nist_csf.json @@ -1734,6 +1734,126 @@ "owner": null, "review_status": "pending_review", "review_date": null + }, + "AZ-GOV-001": { + "control_id": "PR.AC-4", + "control_name": "Subscription Outside Approved Management Group Hierarchy", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-001 supplies Azure configuration evidence supporting PR.AC-4. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-002": { + "control_id": "PR.IP-1", + "control_name": "Required Security Policy Initiative Missing", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-002 supplies Azure configuration evidence supporting PR.IP-1. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-003": { + "control_id": "PR.IP-1", + "control_name": "Preventive Policy Uses Non-Enforcing Effect", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-003 supplies Azure configuration evidence supporting PR.IP-1. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-004": { + "control_id": "PR.IP-1", + "control_name": "Policy Exemption Missing Governance Metadata", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-004 supplies Azure configuration evidence supporting PR.IP-1. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-005": { + "control_id": "PR.IP-3", + "control_name": "Critical Production Resource Missing Deletion Lock", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-005 supplies Azure configuration evidence supporting PR.IP-3. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-006": { + "control_id": "PR.AC-4", + "control_name": "Excessive Subscription Owner Assignments", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-006 supplies Azure configuration evidence supporting PR.AC-4. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-007": { + "control_id": "PR.AC-4", + "control_name": "Privileged Access Assigned at Broad Scope", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-007 supplies Azure configuration evidence supporting PR.AC-4. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-008": { + "control_id": "PR.IP-1", + "control_name": "Undocumented Resource Provider Registered", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-008 supplies Azure configuration evidence supporting PR.IP-1. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-009": { + "control_id": "ID.AM-1", + "control_name": "Production Resource Missing Ownership Metadata", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-009 supplies Azure configuration evidence supporting ID.AM-1. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-010": { + "control_id": "DE.CM-8", + "control_name": "Security Configuration Drift Exceeds SLA", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against NIST Cybersecurity Framework 1.1. Technical-evidence mapping only.", + "rationale": "AZ-GOV-010 supplies Azure configuration evidence supporting DE.CM-8. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null } } } diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index cdfdb858..d7d7d1cb 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -1734,6 +1734,126 @@ "owner": null, "review_status": "pending_review", "review_date": null + }, + "AZ-GOV-001": { + "control_id": "CC6.3", + "control_name": "Subscription Outside Approved Management Group Hierarchy", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-001 supplies Azure configuration evidence supporting CC6.3. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-002": { + "control_id": "CC5.2", + "control_name": "Required Security Policy Initiative Missing", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-002 supplies Azure configuration evidence supporting CC5.2. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-003": { + "control_id": "CC5.2", + "control_name": "Preventive Policy Uses Non-Enforcing Effect", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-003 supplies Azure configuration evidence supporting CC5.2. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-004": { + "control_id": "CC5.3", + "control_name": "Policy Exemption Missing Governance Metadata", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-004 supplies Azure configuration evidence supporting CC5.3. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-005": { + "control_id": "CC6.5", + "control_name": "Critical Production Resource Missing Deletion Lock", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-005 supplies Azure configuration evidence supporting CC6.5. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-006": { + "control_id": "CC6.3", + "control_name": "Excessive Subscription Owner Assignments", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-006 supplies Azure configuration evidence supporting CC6.3. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-007": { + "control_id": "CC6.3", + "control_name": "Privileged Access Assigned at Broad Scope", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-007 supplies Azure configuration evidence supporting CC6.3. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-008": { + "control_id": "CC6.6", + "control_name": "Undocumented Resource Provider Registered", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-008 supplies Azure configuration evidence supporting CC6.6. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-009": { + "control_id": "CC2.2", + "control_name": "Production Resource Missing Ownership Metadata", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-009 supplies Azure configuration evidence supporting CC2.2. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null + }, + "AZ-GOV-010": { + "control_id": "CC7.2", + "control_name": "Security Configuration Drift Exceeds SLA", + "description": "OpenShield evaluates this governance control from read-only Azure control-plane evidence and organisation-approved policy inputs.", + "mapping_type": "supporting", + "evidence_type": "automated_configuration_scan", + "primary_source": "OpenShield compliance mapping pack, authored against the AICPA SOC 2 2017 Trust Services Criteria. Technical-evidence mapping only.", + "rationale": "AZ-GOV-010 supplies Azure configuration evidence supporting CC7.2. It does not establish full organizational compliance; policy and process evidence require separate review.", + "owner": null, + "review_status": "pending_review", + "review_date": null } } } diff --git a/config/governance-policy.example.json b/config/governance-policy.example.json new file mode 100644 index 00000000..84d8f622 --- /dev/null +++ b/config/governance-policy.example.json @@ -0,0 +1,42 @@ +{ + "approved_management_group_ids": [ + "/providers/Microsoft.Management/managementGroups/production" + ], + "required_policy_initiatives": [ + { + "definition_id": "/providers/Microsoft.Authorization/policySetDefinitions/security-baseline", + "scope": "/subscriptions/00000000-0000-0000-0000-000000000000" + } + ], + "preventive_policy_definition_ids": [ + "/providers/Microsoft.Authorization/policyDefinitions/deny-public-resources" + ], + "allowed_preventive_effects": ["deny", "deployIfNotExists", "modify"], + "production_resource_types": [ + "Microsoft.KeyVault/vaults", + "Microsoft.Sql/servers", + "Microsoft.Storage/storageAccounts" + ], + "production_tag": "environment", + "production_tag_values": ["production", "prod"], + "maximum_subscription_owners": 3, + "privileged_role_definition_ids": [ + "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "approved_privileged_scopes": [ + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/platform-security" + ], + "approved_provider_namespaces": [ + "Microsoft.Authorization", + "Microsoft.Compute", + "Microsoft.KeyVault", + "Microsoft.Network", + "Microsoft.PolicyInsights", + "Microsoft.Resources", + "Microsoft.Storage" + ], + "ownership_tags": ["owner", "service-owner"], + "drift_sla_days": 30, + "excluded_resource_ids": [] +} diff --git a/docs/architecture.md b/docs/architecture.md index e9748971..9bbc5a4b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,7 +2,7 @@ ## Overview -OpenShield is a modular, open source Cloud Security Posture Management (CSPM) platform for Azure. It scans Azure and connected Kubernetes evidence against 95 documented security rules, maps findings to compliance frameworks (CIS, NIST CSF, ISO 27001, SOC 2), stores results in PostgreSQL, and exposes posture data through a Flask REST API consumed by a live React dashboard. +OpenShield is a modular, open source Cloud Security Posture Management (CSPM) platform for Azure. It scans Azure and connected Kubernetes evidence against documented security rules, maps findings to compliance frameworks (CIS, NIST CSF, ISO 27001, SOC 2), stores results in PostgreSQL, and exposes posture data through a Flask REST API consumed by a live React dashboard. --- @@ -43,7 +43,7 @@ OpenShield is a modular, open source Cloud Security Posture Management (CSPM) pl ┌───────────▼──────────────────────────────────────────────────────┐ │ Rule Modules (scanner/rules/) │ │ │ -│ 95 documented rules across Storage, Network, Identity, Database, │ +│ Rule files across Storage, Network, Identity, Database, │ │ Compute, Key Vault, AKS, post-quantum cryptography, Backup, │ │ Serverless, Private Endpoint posture, and Supply Chain │ └───────────┬───────────────────────────────────────────────────────┘ @@ -111,24 +111,27 @@ result = engine.run_scan() ### 4. Current Rule Modules -There are 95 documented controls in `docs/rules-reference.md`, including 21 AKS and Kubernetes controls. +See `docs/rules-reference.md` for the full table of documented controls. | Category | Count | Rules | |---|---|---| -| Storage | 9 | AZ-STOR-001 to 009 | -| Network | 15 | AZ-NET-001 to 015 | +| Storage | 10 | AZ-STOR-001 to 010 | +| Network | 27 | AZ-NET-001 to 027 | | Identity | 25 | AZ-IDN-001 to 025 | | Database | 7 | AZ-DB-001 to 007 | | Cosmos DB | 2 | AZ-COSMOS-001 to 002 | | Managed Cache | 1 | AZ-CACHE-001 | | Compute | 4 | AZ-CMP-001 to 004 | -| Key Vault | 5 | AZ-KV-001 to 005 | -| Kubernetes | 21 | AZ-AKS-001 to 021 | +| Key Vault | 6 | AZ-KV-001 to 006 | +| Kubernetes | 27 | AZ-AKS-001 to 027 | | Post-quantum | 3 | AZ-PQC-001 to 003 | +| Governance | 10 | AZ-GOV-001 to 010 | | Backup | 4 | AZ-BAK-001, 002, 004, 006 | | Serverless | 5 | AZ-FUNC-001 to 005 | | Private Endpoint | 6 | AZ-PE-001 to 006 | | Supply Chain | 8 | AZ-SC-001 to 008 | +| Security Operations | 10 | AZ-SECOPS-001 to 010 | +| Data Link | 2 | AZ-DL-001 to 002 | Every rule has a matching Azure CLI playbook in `playbooks/cli/`. diff --git a/docs/governance-rules.md b/docs/governance-rules.md new file mode 100644 index 00000000..61cd550e --- /dev/null +++ b/docs/governance-rules.md @@ -0,0 +1,27 @@ +# Enterprise Governance Rules + +`AZ-GOV-001` through `AZ-GOV-010` evaluate Azure management hierarchy, policy assignments and exemptions, RBAC, resource locks, provider registration, ownership metadata, and unresolved policy drift. + +## Configuration + +Copy `config/governance-policy.example.json` to an organisation-controlled location, replace every example value, and set: + +```bash +export OPENSHIELD_GOVERNANCE_POLICY=/secure/path/governance-policy.json +``` + +The scanner does not invent governance defaults. If the policy is missing or invalid, the rules log an `UNKNOWN` result and do not create a finding. Evidence collections are independently nullable, so a permission failure in one Azure API does not become a false pass or false failure in another control. + +## Evidence and permissions + +The collector uses read-only Azure Resource Manager and Resource Graph requests. Grant only the read permissions listed in each finding. Policy state collection also needs `Microsoft.PolicyInsights/policyStates/queryResults/action`. + +Resource scope is determined by the configured production resource types and production tag. Valid exclusions are exact, case-insensitive resource ID matches. Policy and RBAC assignment IDs are compared at their effective Azure scopes. + +## Remediation safety + +Every governance playbook requires the literal `--apply` flag. Review inherited access, policy impact, active exceptions, and workload dependencies before running it. The exemption playbook intentionally stops with operator guidance because exemption metadata schemas are organisation-specific. + +## Current scanner contract + +The scanner persists findings and per-resource rule evaluations. Governance rules currently emit findings through the legacy scan interface, so the engine records UNKNOWN coverage with LEGACY_RULE_NOT_MIGRATED rather than inferring PASS from an empty finding list. Dedicated governance coverage evaluators are still required before these controls can contribute affirmative compliance or resolve lifecycle findings. diff --git a/docs/rules-reference.md b/docs/rules-reference.md index bdef2bb0..e46b3442 100644 --- a/docs/rules-reference.md +++ b/docs/rules-reference.md @@ -1,6 +1,6 @@ -# Rules Reference +# Rules Reference -OpenShield currently ships 95 Azure scan rules. This table is generated from the module-level constants in `scanner/rules/`. +OpenShield currently ships 154 Azure scan rules. This table is generated from the module-level constants in `scanner/rules/`. | Rule ID | Name | Severity | Category | CIS | NIST | ISO 27001 | |---|---|---|---|---|---|---| @@ -131,6 +131,17 @@ OpenShield currently ships 95 Azure scan rules. This table is generated from the | AZ-SC-007 | Pipeline Service Connection Scoped to Subscription | HIGH | Supply Chain | N/A-SC-007 | PR.AC-4 | A.8.2 | | AZ-SC-008 | Pipeline Service Connection Uses Password Instead of Federated Credential | MEDIUM | Supply Chain | N/A-SC-008 | PR.AC-1 | A.5.17 | +| AZ-GOV-001 | Subscription Outside Approved Management Group Hierarchy | HIGH | Governance | N/A-GOV-001 | PR.AC-4 | A.5.15 | +| AZ-GOV-002 | Required Security Policy Initiative Missing | HIGH | Governance | N/A-GOV-002 | PR.IP-1 | A.5.1 | +| AZ-GOV-003 | Preventive Policy Uses Non-Enforcing Effect | HIGH | Governance | N/A-GOV-003 | PR.IP-1 | A.5.1 | +| AZ-GOV-004 | Policy Exemption Missing Governance Metadata | MEDIUM | Governance | N/A-GOV-004 | PR.IP-1 | A.5.1 | +| AZ-GOV-005 | Critical Production Resource Missing Deletion Lock | HIGH | Governance | N/A-GOV-005 | PR.IP-3 | A.8.9 | +| AZ-GOV-006 | Excessive Subscription Owner Assignments | HIGH | Governance | N/A-GOV-006 | PR.AC-4 | A.5.18 | +| AZ-GOV-007 | Privileged Access Assigned at Broad Scope | HIGH | Governance | N/A-GOV-007 | PR.AC-4 | A.5.18 | +| AZ-GOV-008 | Undocumented Resource Provider Registered | MEDIUM | Governance | N/A-GOV-008 | PR.IP-1 | A.5.9 | +| AZ-GOV-009 | Production Resource Missing Ownership Metadata | MEDIUM | Governance | N/A-GOV-009 | ID.AM-1 | A.5.9 | +| AZ-GOV-010 | Security Configuration Drift Exceeds SLA | HIGH | Governance | N/A-GOV-010 | DE.CM-8 | A.8.8 | + SOC 2 mappings are maintained in `compliance/frameworks/soc2.json`. Every rule has a matching remediation playbook in `playbooks/cli/fix_.sh`. diff --git a/playbooks/cli/fix_az_gov_001.sh b/playbooks/cli/fix_az_gov_001.sh new file mode 100755 index 00000000..83bbba60 --- /dev/null +++ b/playbooks/cli/fix_az_gov_001.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-001" "$@" diff --git a/playbooks/cli/fix_az_gov_002.sh b/playbooks/cli/fix_az_gov_002.sh new file mode 100755 index 00000000..5c7404a8 --- /dev/null +++ b/playbooks/cli/fix_az_gov_002.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-002" "$@" diff --git a/playbooks/cli/fix_az_gov_003.sh b/playbooks/cli/fix_az_gov_003.sh new file mode 100755 index 00000000..82aee67c --- /dev/null +++ b/playbooks/cli/fix_az_gov_003.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-003" "$@" diff --git a/playbooks/cli/fix_az_gov_004.sh b/playbooks/cli/fix_az_gov_004.sh new file mode 100755 index 00000000..1a092900 --- /dev/null +++ b/playbooks/cli/fix_az_gov_004.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-004" "$@" diff --git a/playbooks/cli/fix_az_gov_005.sh b/playbooks/cli/fix_az_gov_005.sh new file mode 100755 index 00000000..4f9a7557 --- /dev/null +++ b/playbooks/cli/fix_az_gov_005.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-005" "$@" diff --git a/playbooks/cli/fix_az_gov_006.sh b/playbooks/cli/fix_az_gov_006.sh new file mode 100755 index 00000000..c34a9c5b --- /dev/null +++ b/playbooks/cli/fix_az_gov_006.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-006" "$@" diff --git a/playbooks/cli/fix_az_gov_007.sh b/playbooks/cli/fix_az_gov_007.sh new file mode 100755 index 00000000..e3f5648a --- /dev/null +++ b/playbooks/cli/fix_az_gov_007.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-007" "$@" diff --git a/playbooks/cli/fix_az_gov_008.sh b/playbooks/cli/fix_az_gov_008.sh new file mode 100755 index 00000000..7dc1e72d --- /dev/null +++ b/playbooks/cli/fix_az_gov_008.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-008" "$@" diff --git a/playbooks/cli/fix_az_gov_009.sh b/playbooks/cli/fix_az_gov_009.sh new file mode 100755 index 00000000..c59b903c --- /dev/null +++ b/playbooks/cli/fix_az_gov_009.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-009" "$@" diff --git a/playbooks/cli/fix_az_gov_010.sh b/playbooks/cli/fix_az_gov_010.sh new file mode 100755 index 00000000..9b7c9675 --- /dev/null +++ b/playbooks/cli/fix_az_gov_010.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/fix_az_governance_control.sh" "AZ-GOV-010" "$@" diff --git a/playbooks/cli/fix_az_governance_control.sh b/playbooks/cli/fix_az_governance_control.sh new file mode 100755 index 00000000..6e67bd99 --- /dev/null +++ b/playbooks/cli/fix_az_governance_control.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -euo pipefail + +RULE_ID="${1:-}" +MODE="${2:-}" +shift 2 || true + +if [ "$MODE" != "--apply" ]; then + echo "Usage: fix_az_gov_NNN.sh --apply " + echo "This playbook changes Azure governance state. Review the finding evidence and approved policy first." + exit 1 +fi + +az account show --output none + +case "$RULE_ID" in + AZ-GOV-001) + [ "$#" -eq 2 ] || { echo "Arguments: "; exit 1; } + az account management-group subscription add --subscription "$1" --name "$2" + ;; + AZ-GOV-002) + [ "$#" -eq 3 ] || { echo "Arguments: "; exit 1; } + az policy assignment create --name "$1" --policy-set-definition "$2" --scope "$3" + ;; + AZ-GOV-003) + [ "$#" -eq 3 ] || { echo "Arguments: "; exit 1; } + az policy assignment update --name "$1" --scope "$2" --params "{\"effect\":{\"value\":\"$3\"}}" + ;; + AZ-GOV-004) + echo "Update the exemption owner, justification, and expiration through the approved governance workflow." + echo "Automatic exemption metadata changes are intentionally disabled because metadata schemas are organisation-specific." + exit 2 + ;; + AZ-GOV-005) + [ "$#" -eq 2 ] || { echo "Arguments: "; exit 1; } + az lock create --name "$2" --lock-type CanNotDelete --resource "$1" + ;; + AZ-GOV-006|AZ-GOV-007) + [ "$#" -eq 1 ] || { echo "Arguments: "; exit 1; } + az role assignment delete --ids "$1" + ;; + AZ-GOV-008) + [ "$#" -eq 1 ] || { echo "Arguments: "; exit 1; } + az provider unregister --namespace "$1" + ;; + AZ-GOV-009) + [ "$#" -eq 3 ] || { echo "Arguments: "; exit 1; } + az tag update --resource-id "$1" --operation Merge --tags "$2=$3" + ;; + AZ-GOV-010) + [ "$#" -eq 1 ] || { echo "Arguments: "; exit 1; } + az policy state trigger-scan --subscription "$1" + ;; + *) + echo "Unsupported governance rule: $RULE_ID" + exit 1 + ;; +esac diff --git a/requirements-dev.txt b/requirements-dev.txt index 32cdba87..9498dfda 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,125 +2,167 @@ aiohappyeyeballs==2.7.1 \ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 -aiohttp==3.14.3 \ - --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ - --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ - --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ - --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ - --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ - --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ - --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ - --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ - --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ - --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ - --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ - --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ - --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ - --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ - --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ - --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ - --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ - --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ - --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ - --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ - --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ - --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ - --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ - --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ - --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ - --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ - --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ - --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ - --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ - --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ - --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ - --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ - --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ - --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ - --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ - --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ - --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ - --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ - --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ - --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ - --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ - --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ - --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ - --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ - --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ - --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ - --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ - --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ - --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ - --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ - --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ - --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ - --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ - --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ - --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ - --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ - --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ - --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ - --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ - --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ - --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ - --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ - --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ - --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ - --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ - --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ - --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ - --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ - --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ - --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ - --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ - --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ - --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ - --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ - --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ - --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ - --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ - --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ - --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ - --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ - --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ - --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ - --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ - --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ - --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ - --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ - --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ - --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ - --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ - --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ - --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ - --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ - --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ - --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ - --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ - --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ - --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ - --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ - --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ - --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ - --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ - --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ - --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ - --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ - --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ - --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ - --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ - --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ - --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ - --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ - --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ - --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ - --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ - --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ - --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ - --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ - --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ - --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 +aiohttp==3.14.5 \ + --hash=sha256:0133c3c3b54a0bf1e71fa5c1ad95c93f07fd54e24ef1fe182f5122e1573d2bf1 \ + --hash=sha256:038c2c7e8caa26b6c8423779b5eaf1893904048a512c19b32fe841ffa5592b50 \ + --hash=sha256:0684952aeae1f5dbfe02d46039338513b94009baecd15d8e4098a357c4c4a2a6 \ + --hash=sha256:0790ec66fa4013e83c53b9025a45d454723da1a2fce28b3208c9b32d08af162f \ + --hash=sha256:083673c7a94c3ea035caaa5ca04288bdb44887abfe1f5ba23294e6a4b03efd2d \ + --hash=sha256:09e0eb18c7e0c8777e2f9149de63799195b9b3ca1b5c81ba6f32f2c6b8628210 \ + --hash=sha256:09ec102b4b8c9a920275733bbc11fdbb615efe6f9231a06007c0218d336fb77a \ + --hash=sha256:0a8ea271867e360ac985ae607f4a23ad9a38414b9aca1d49ec98839ae660e49f \ + --hash=sha256:0e6f16f5e49c4b8267988c05ab07760d7064cea57d077c3d068d04b0fbb992cb \ + --hash=sha256:1061b364556e8172e8d46b0b183adeeb73e8c42d30ebc745591e1bd89acad52e \ + --hash=sha256:1220353657ad49493551f089ce02f1a348fd57ffd585bfec77f2f3c4fe3a7346 \ + --hash=sha256:137351bf20bbed9a65e839f4a4452ac377389bdb2f2857d2acffef38f5e9f2d1 \ + --hash=sha256:149fb56caf7acb67073126f675d0958d9c4b3125fcd3f6d4877df98aa8a97ce9 \ + --hash=sha256:14f04769cfefe4734016a856a83af36133cd17779cef9ae817f812b8ba9d6d51 \ + --hash=sha256:15a310d3c71398e3d7bfc93a1a73fbe664315cd9e9016b8efc1cff85eeab7155 \ + --hash=sha256:1612fa5857b37bf32e5c1eaeefb96e3b01e9c70679eec81f0934e8a600080863 \ + --hash=sha256:16c8abd5bca220a47efe667d26f8460124c81810787e79ee87b242677563d9dd \ + --hash=sha256:19e2ba471507c34f8252402ab50f5ab512398b9ea8c8f1cb26beb3f75793ba30 \ + --hash=sha256:1aead151c3abbac6b32942e452020cb66d7efc099d253cc6c20f748e926c858b \ + --hash=sha256:1b438b73c38111818d0c9d6a5c2bfed8584c8e503a49ef085d70e874ec846738 \ + --hash=sha256:1b5416552740edf07234cc9437d0706f2acb67b93c198670b1a68e1b2b587dec \ + --hash=sha256:1d2d981b53dd09a319e3570ef8cc3bbc3ef86f5a7abef0f6b2bff3867db3a9e7 \ + --hash=sha256:1eb8167961ec4dfcc8cb9dd50bd0ee72519f7ef496be95203e49e27b01618382 \ + --hash=sha256:1ffa3a523a36d8628f98c06492ae16a31a23d14c0b4ec721757b477319f656d6 \ + --hash=sha256:20064a177a070d789ee64a50b01a9161d3468e989baacfc6c714aa685c4b332f \ + --hash=sha256:20726f9782d5c2744c1c66255842d1d163bb3edcf768b8de25216bf47f7b6ccf \ + --hash=sha256:20f085697d7e911f1f73c43ed03fafbed1e7121797e2eb5428efa80398060584 \ + --hash=sha256:225c579c23b68b343cccea27a7e06e3bd8ec23a09c30b427eb3f1e4ca6239b20 \ + --hash=sha256:24409db442e2fb6e766bc7f3943851a8381dec3098140e43bb2e843b79e31b12 \ + --hash=sha256:248d779ad720b49d4fb355720e60c9e5f444f95887bc16974fea48fc56c41789 \ + --hash=sha256:2528cb4c6b92008c76ac9ac6298624069bb2db91ff4929905512d1d84485f658 \ + --hash=sha256:276a4fc00b1d9ae492b802763a789c5b86328b989c5ea169f2faa447d6a11c7c \ + --hash=sha256:27c2322e03f66101acb09869ce1cf1efc04994ee95e1735b69827bf8c8b9d781 \ + --hash=sha256:293d3ae7c6a0ed176a42e59a1b5fde825ead65c835360f734148e96729f928d2 \ + --hash=sha256:2cc38a4f2b516bef1714e690df87a0e043faf1a7693c82d860091684453d5111 \ + --hash=sha256:3093b72c215bda16ce961a6d073f6e71d46e022962a9d5d457c5d4d421c78b57 \ + --hash=sha256:32e8fa6644e541fcd7e02430588c7fc93b602c1778ea0bc345505db76b61cfb4 \ + --hash=sha256:330900acd0dc4cb8b27f9c127fbaad770964845338493e7906ae3822e82dbf8d \ + --hash=sha256:33f706574e32c6e694f352a856e05caf18f7f2c871b3e87b41c55ea452b409ab \ + --hash=sha256:3ae800a20947e2c2e53088047d021e6bf7d51560cc49f6a0737a1f79d2e3a13c \ + --hash=sha256:3be7dd397d64ca3e1869626fa9318aaebb54b7bf93bc72d7a205448d83e4f748 \ + --hash=sha256:3e0eb43bed3c6801a6cee315195377789e90b2a72c2277a475b578535312488d \ + --hash=sha256:3e51a27980c3788e6e6b3325d694fdd4898087fa8a86b2763af77b39353da41e \ + --hash=sha256:3edbece0379b8b4aaa67619b8aa2399bb66fce372cd5911098a434ea77220aa0 \ + --hash=sha256:3f2dcc00191fd563e9075181a14ec31d7dd63223ced7582cc70a15a499de0c79 \ + --hash=sha256:42b5e616946dbaf505e2bff18c9af2cd4ef9e7ef300ee58a6e951a5b7cf147ae \ + --hash=sha256:42f320d4a5b00b9af0bddcfec5407dc6f2d9816f006b2f79ebbaa31f16895df3 \ + --hash=sha256:43351bdb5e4c3cb7d1772368e988534e869a74db7778079a83782c11c69535c7 \ + --hash=sha256:43e1b7994a8b038125f722bff07492ef501110722c2727c408995d9fb864c421 \ + --hash=sha256:4887d130a7bbfed3a85493bb5a25e5b5b558d40c1d986dd16970d2bb26d63793 \ + --hash=sha256:4f07fe3ac408d8b3f768be471dc3f56d43843c47d97c66120534467a15ead197 \ + --hash=sha256:50343c1757b4b6f6708eeaf24534b32f19dfb99fb1b762c00420867a62fc81e0 \ + --hash=sha256:50983e3be33d8c0942ab88cec3905b10602f64c469b20153c48c5d4e558dd016 \ + --hash=sha256:50a195903119008fe9cc68710535eb37f556ffffd6a7759afe70a2c145587045 \ + --hash=sha256:56572c42e3ecd636de8d2c3dd54cf5fc939cb5c32eb56297f176a0d366fac622 \ + --hash=sha256:56d9828f204331a5ca8850fcfe2bcce95a149f1f223f60cc7216e5524978e480 \ + --hash=sha256:579f97d5120f2971876d2ddca2968135f6944d00c44c3a6590ad7d86ca9b403f \ + --hash=sha256:5af42135fdfebdadbc2bcd9c0842a48ccf0d62794c36a260b21dc4b94d1e0119 \ + --hash=sha256:5ba14a839fbe87cf7c12a6b5661c05f324a296eb8363141edb3944ba63d4c9d3 \ + --hash=sha256:5c76f1802bab718a68ac3cce447160605c734551f95c67ae90fa1132b215cb29 \ + --hash=sha256:5e8f97c0488ffda3082766ac0f2c8150a9a58c4d05788330e479cfd449b37939 \ + --hash=sha256:5f3e96071686755d9cd3600c3880183eb94b012178e92746d68101800f0ed8a3 \ + --hash=sha256:5fb6a6e919bfb703227bc1ce6579281b84b1a2ba57deb9794dfdbec7dcd1e40c \ + --hash=sha256:602c1e9b718a3275c580149f947e7fac65044c0a20e599553fb12e9700da9eca \ + --hash=sha256:657291433bf4dd3142f3abac495764cd47d0c7c92087751e6666c6447e65fcef \ + --hash=sha256:6774814fd5c338e72ee0da5cbb9432816df450e69c019f72b5d29bdec2a1792d \ + --hash=sha256:6da32b5ff3fd78d244e37300463434c7145162bfd2b6e9e915ab164da37f7343 \ + --hash=sha256:6e1d8637cf73eebc92eba2e11d4cfff98a3b562f2505bd75bba766d908926e8d \ + --hash=sha256:6e4251c0ba4624a68a2c11471a1ac54c3306876c21f0ae86de085cc9241c8905 \ + --hash=sha256:6f275c11d1aa6d4c458e05a68be084efe3c55a113d99e3f46a318098e52948fc \ + --hash=sha256:6f967dde489ca6a8c02d093ab245d2cbf50ccb5c36adf0188b17b0ca39d24b67 \ + --hash=sha256:6fdcd6af7e2e51d1ba1b4bea16e97b074bcb7b5dd0246a9d8201341bb28085a0 \ + --hash=sha256:70cb4008ac2ed1e0ca9e824deb4b53d3aa0d939109698ebf1e723a84337bd794 \ + --hash=sha256:7457580535e019e1247ea35d6a02bf081ad30c26d0cbc210c93f6c3ab67a0835 \ + --hash=sha256:74b0a9c8270f9b0a11410e124ff8d4f18bfc1f1837440ec84da5ae7b50927b5d \ + --hash=sha256:74efb69332b85675b1eabd760a8cfc2e2cf42c60607c66f88014c1bdfb40942d \ + --hash=sha256:755933b107ea7a6a9ac916f635a70595a5b1a32fac10a8ff0b9f2ab88555550c \ + --hash=sha256:7779cd97e61ebe583ec2f1c5616cdd038aa08a4453b1848c67842176d054948e \ + --hash=sha256:788ecaa9c10533b786ce5ba70c4f2df78ad41819fd00a6c99d92b66f9a32e1da \ + --hash=sha256:7bd8ac754ebd6733a3e2a0dd1674c4d8ab086196803fd8dcd776f07b4e2607d9 \ + --hash=sha256:81c2b3dfd56c62bee6108e4852d5970b4cf9086390b6983f52b666e878c1f115 \ + --hash=sha256:823c910f046f23f4c713b8d99a2242dc65f591cb45ee86418fa11762a3c2963c \ + --hash=sha256:82c7583cd3dfdc7dcc927835b4f6c7faae7ecc1ba3ca5879321621ae2e6f8e84 \ + --hash=sha256:8966ecac808dd5f473c9c4cefd10cd3ffda71c18a4d3493b7c7d2ae1803bf2cc \ + --hash=sha256:8df7d481654ac96fe1ba9a02a9f67770fdd367823e0d5ef01b922725c4bd2cfa \ + --hash=sha256:8e317e0fb6b16212c881d2205a7d87414c29acd69320b3aa6dce9d9c7b86fe4f \ + --hash=sha256:932ce7e694bbc29b2bf6f64f2343c27d148d4997c771d01bdade4639b6749ff4 \ + --hash=sha256:939042d5cda21d41a6f512e7cc8b8e33a2aebff863352251da495fbd91b673b5 \ + --hash=sha256:94684b879ac1d71e4238850c99b62dc1b28d9086b156a2555f082010b85a865c \ + --hash=sha256:96a2e584f0b9ed8f1fa33211397dcf67bb7069866402cb405d191c2f0defb9a3 \ + --hash=sha256:9ad7e6aa38c20da1be697874349c4c273c8a03b7887169665081706398d0439a \ + --hash=sha256:9b42db919715e91eb76acf3bc492a9a7ccd8bd9adc6745c1412b689735269f14 \ + --hash=sha256:9bab2045550c4fe0f7baf89574db1b455c195750702ba96fef1f16972b146617 \ + --hash=sha256:9bf1d5dcc15204d9ec8b8ea4c18fd66e6b80e5de1f4ecbafb3a2f2740f8039d4 \ + --hash=sha256:9c061aa954daaf57d2a4b8374f9fca621ef0e1b603584431c220c22458c59b6d \ + --hash=sha256:9c428eb2bd8817588d16a0ab898aa4eb5d141f896aa2b394cc79a4cf61d9a8e2 \ + --hash=sha256:9cc882cf8619109583c906b4d4a85d6a111a98afa34b7a450d1e08118d016820 \ + --hash=sha256:9ce66feae6ac65327379460380549bf1b8df8e17c4e25df2a2bcf168272e3bed \ + --hash=sha256:a23fe35d776bc03cb495938b9594450d047e3bc08c5255315a82323e9cb7d2dd \ + --hash=sha256:a2c473a355f9239efcb72c92d5abfd8fcdb0cc78c8e9af607e72ca12dbb36593 \ + --hash=sha256:a63afd1f757de949028387e65a7127b61ad0f775432dbb0e62816ae619fe69ac \ + --hash=sha256:a6d02b4c38de03d9c7617813433e6a0fb6b522797974177d69d9dad431900833 \ + --hash=sha256:a7d470cf7b206e6359fc77b1b860632fde400d5a2ed59cd0181b93a686bc81ee \ + --hash=sha256:a95529a92a446db351675f4aab518feaf5e99842f63f5dd17160c2b74f382db3 \ + --hash=sha256:a9918e58faf62ba2c7147927d06057aec78f42475aff5048047ec47e7265a600 \ + --hash=sha256:a9d3983bd6ab7aa1cfd573544ae98df9b6cb6912a5185a198263e024a636861d \ + --hash=sha256:ab52d8f1fc1b64821c1fbad64a647ed6203627004059a6d1ed4f0858a1499703 \ + --hash=sha256:abfda5cb094a829f7bc25216a32f7db2e85cc65bd59910f8e7b40b3d9b224764 \ + --hash=sha256:adbeee7d6fd4cf5fe0aece2fb3edc4243615d3180430ba8149d01a90670cac99 \ + --hash=sha256:ae53924aa853a7a2ca20ed4142c7c6b56338e4d4cd999e2980075b9efc2e257a \ + --hash=sha256:b032a0023eb41d768ce77d83210ab2a3c389bc0b09313273c7e1eca48c10a755 \ + --hash=sha256:b1b8ece1e71132d2afba4dbc0c3d62c766e25165990b25db1196c04969eb3d84 \ + --hash=sha256:b2966998927d7bed9db12c0a4647b0c7b179755878fc9c357fe1ffd3e3b0c1a5 \ + --hash=sha256:b3cc509327c7b27f6f4727a8830f4004f6df7766e179f2f4b8e54e65c0bec5d3 \ + --hash=sha256:b7806e804889231b0e06469fd4a5c06313d1c0a3377322b6d9237fa5e0fe4167 \ + --hash=sha256:bea559ad70218d230663e4210875735076a9bfea5994cef34a55a25faeaf2544 \ + --hash=sha256:bf163cc701f3d4ac43ba7d97771bf5fd955220ef5500ef3ee847bc0ecfbf4ec1 \ + --hash=sha256:c147451b4a58e7050f7f7394e6c467867c84161560001f9ad4fb2d1446743946 \ + --hash=sha256:c172db893e516e1358e65a95ee20b7ce7173963eefe318b6ab2a2220688b999e \ + --hash=sha256:c1d60eafd9c7e8e74abd03a5b00df44e7febfe6d9b89b559c0a6551eef0699d4 \ + --hash=sha256:c2c30484dd1417ef98b51021ffa2cc0d7f3c78918adaaaab7e70817335ab3e02 \ + --hash=sha256:c32e26310cc10e547f53cd13d39a369034f69dcb7d749d5cb0e5f67bc196b6ba \ + --hash=sha256:c5ed596aedb9c42afd3fe0aae3117725378ac73d2cc5ddc735056fbdb96c5d02 \ + --hash=sha256:c8859a013ae0de1074660992139a1a440df3e6b219b86cf0d3f11c2692bb4fe3 \ + --hash=sha256:c8c4478bef6d57fcfda15dae461ea3c9f06aa7b257c58df3f2300174ccbb185a \ + --hash=sha256:cb11a971a3aea10f9b8373be628f1df932964fc6c6b174516d318a48c3ac4412 \ + --hash=sha256:cb131d775a1573c1aee66656bd78b023577bbdb6cb8349a07773bd4f73e68a6e \ + --hash=sha256:cd88b01f3d37b7a2a34f91d98f14720206f1ea3d540843fab2d649dd5fb91fec \ + --hash=sha256:d05e94cdfe0d15d0206f970722d2554780ce562787b21b218b275447f8751319 \ + --hash=sha256:d079c0a0135c36e7beb6f1c88087c8f108dc5891cdd0b5eafa778421bda70ed2 \ + --hash=sha256:d3112585250b199296c26ca6e0131640b6a8d01bab8b232d2eb3763ed469de11 \ + --hash=sha256:d418ce2af40c6bb685b3f663e9e8de27cb0a22431d8e88a167348d7f01878073 \ + --hash=sha256:d51db97c96384fbfcaf8f4c65922183a68b94f891c3c10c862ef5f6df2adbb1f \ + --hash=sha256:d94e44be379e569758fee8a9a58431cfc3c2598c708b92b1cfe96c66b4c94aef \ + --hash=sha256:dab9ac5a67c8d1f070c00fa8fccb7cbd1b8dcc1a8d6b42f37540df9b3d4cc603 \ + --hash=sha256:dbf53ae2601b7fd5a93c3944deea3a78d40f495226d582c35ef7a433425ce2b2 \ + --hash=sha256:dca3fa8d8a0a26679862eccb0b1a9151b2b9f1cd2c212e7a6335778faaff5833 \ + --hash=sha256:df37b620684e19b5e25724412518ccafc3b1a49cdac706fdbd2f983fad943450 \ + --hash=sha256:e1cc2bfaee8c214f06080a7c7d5772419b8a1108e8e5349236189811823fb02a \ + --hash=sha256:e29347c142cf6e99e0dff5e2995ead1d50fa3b51bf37a7c726a7ccfe5419745a \ + --hash=sha256:e4f5cf4dc72a71c4cfa9751b4950be22f733626670230d46e7d606592aa22d59 \ + --hash=sha256:e724a7b6091f0b1ac064f9d1b15ff9ec52e6033a86cdae649e5f086e32a3c0db \ + --hash=sha256:e87046c8ff77a8decdb6a41d8ab25824b47531b2da933aeab0c1e21c7acff329 \ + --hash=sha256:e95c8def4b81c5d68d5cf1f54c07acd7c0d2577af244e5b6da802120825737c6 \ + --hash=sha256:eb324e2009fb54db30a071dad7caf6998ee2879c4704007efb244514dad1fec1 \ + --hash=sha256:ef60869969180ec2464f1349aff07138ae35ca2200f0946cb3552e49e8f301a8 \ + --hash=sha256:ef692a24087a699c0a4a26af45e746e0c1eae2116f6d8a5ff91d8aae2b867b45 \ + --hash=sha256:efc21a454892828368b11c2c780de0ff8bc991f73f6b99c6b66e56205470929b \ + --hash=sha256:f001b571ead90ca1770f1e616db255351a1703317f20374c361ef22f12c06d09 \ + --hash=sha256:f2a7966bda23dd85051f1661ce0ace38d6890e05ec6c357ecae9d2479cba377e \ + --hash=sha256:f2ebb54b3f932210503072f09974b4fb574d823e497a944adfdcd140a6a00255 \ + --hash=sha256:f2ed8b64dc0c651c0f5a9c926777719770021251b8f336d97c4b80b660836ce1 \ + --hash=sha256:f375db73a39f5cf83696d500e21a67f418dc9a988955756f254be8f03b7b3651 \ + --hash=sha256:f59c7673465908cbe506117176156c127f29f917677afceada34957179221d91 \ + --hash=sha256:f8d40ce41991e9d56fab4f5dc4a51fe59bc3b5c77c27f4b148963064d00232e8 \ + --hash=sha256:f9033b43f511f27547c557dcaba0177649e10a3725336ccd2cce0fdc1dc4850d \ + --hash=sha256:fba47bc2c3d7303c3d027c6cf4d07626c37b1314ac81f5820c31032e0ca1f677 \ + --hash=sha256:fbdc5ec49f9ca3cd24955cf3520b10a4d4c901ba2572094c84274e9e7eb30534 \ + --hash=sha256:fce9523df31cea6284f3e2c479876750d7687cf671d7b25d32b19effc0e86441 \ + --hash=sha256:ff75a7537413a86e7cafe98e0e1d6e3dc4b15c6349896e7d5c6b881bfdb6d550 aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e alembic==1.18.5 \ @@ -187,8 +229,8 @@ azure-monitor-ingestion==1.0.3 \ --hash=sha256:73f4f1067d58df6cb9f23023c0e8f15d9bb7feb84684061203daf277ea99c717 blinker==1.9.0 \ --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc -certifi==2026.6.17 \ - --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db +certifi==2026.10.19 \ + --hash=sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b cffi==2.1.1 \ --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ @@ -956,177 +998,177 @@ msal-extensions==1.3.1 \ --hash=sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca msrest==0.7.1 \ --hash=sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32 -multidict==6.8.0 \ - --hash=sha256:003a3bddb32915c3f67096ea41d24e53edf710edb65a1f5d0c70ab40b0e4d20b \ - --hash=sha256:00be37bde741bf60871082cd347a093218c44886e99231b7516671c70f2c280d \ - --hash=sha256:029897732a9c798737457e382bf84e8c64237eff224a90aea2639f4413c45e4e \ - --hash=sha256:05c2e90c5289c5f7436ba2c25812a5fbdaa1c1bc11c8d8d3bbf64f5cd7c633dd \ - --hash=sha256:071da134651b04a8507dfb331ac0988f376337c2aea59486bf20989fb5b5a64e \ - --hash=sha256:088b04a66b3c1fce6fe4d771ec184a0426262d0b86709c908477b4ac7965df40 \ - --hash=sha256:093167d22a8c95af30f597b8a5686f20a14512989942d4be804d119899caca20 \ - --hash=sha256:0935971bffd0b479fc90c4811ca787703e93fcb6afea939a375dfc80285ab368 \ - --hash=sha256:095f62ea4e7a3be2f6c567ab695ce10e950f2adb905c1bec82281593e0b2d2ad \ - --hash=sha256:0b143d53590e89f43153d81d505a8448d4d57354354385aef8a51d67ffefa27e \ - --hash=sha256:0c1c4debad7337627b86837abdf0237ca3cb3d7e17de7eab0177c263878546d4 \ - --hash=sha256:0eca15d627e942ce186a935061f1568cc46c02e97c419c8da802df2be9f917d8 \ - --hash=sha256:0ef606c15cac6c90279acf34120784b6f36662cbf382defd3955cd8f1115336b \ - --hash=sha256:10456943903744ae1249728161c96bd9d2f7eb5ee17fcc2ffda2dc32e1bb36c7 \ - --hash=sha256:11d71490bf4bbff1141b14b93af419ad68c56b60bea9277fcb3f94dcca4796eb \ - --hash=sha256:122adc7c46ac1e31ecfc7f81b2530533dccafdba70f5d741649f87e336c63384 \ - --hash=sha256:13967dca8b2f33230a1427b52438326bb1c9101a1df22a3309ed3fcbbb3c96f0 \ - --hash=sha256:13e26f59f0eecfc5f67c663ad550ffdaf62c0f657547cde387f6c86af1c9449e \ - --hash=sha256:15db8e6cab5f4cc9241bc56e69fdf3452cf49c10ee3c7977c742e68a275b3786 \ - --hash=sha256:18f0e06360c3e451a3ab800355773c8d125a758238d780c800b0ee5e90ee903c \ - --hash=sha256:1969971900b0871530f9b62280dcc2d75688e74d2a69262bc01faf2b96c78f04 \ - --hash=sha256:1b8986d4313dcee7c932837d16a535f1840b827bac1ea7c5c4c80751d0423794 \ - --hash=sha256:1bdb9b8fba5a9aef673ec90db3f55b1ce743f2fbdea4d37dc04d14ccdfc153ff \ - --hash=sha256:1f57c414be82490bc0e0305fdb834186229b2d9b6a35fa0afd1eb1a772d125ab \ - --hash=sha256:1f66fe6a021173d0d47968491791966b9f3e6d61115f2491744aa0c07a6e67af \ - --hash=sha256:202436df907c15adbb94360296c425ea53cf8968a5d2cff9b5b9790ae1972b33 \ - --hash=sha256:2196ba6df392c3574acadd14ef87550f3611349c8618564de324b806a7a31cee \ - --hash=sha256:22a310ad37672a261e55a8b5e28d0ae08cfb68abb1f46418ccd19835c3b8e836 \ - --hash=sha256:23c9ee89967b6a9b4048acb3b93b660ed714ce9c8bf3bbe652959bc120dc02dc \ - --hash=sha256:2622fe114c0bd66ca5c461859357587f5a5e35ee5ff49fc5643d1bc78dbb41c6 \ - --hash=sha256:26a7aafc992e78872e2c8c1f7248c0e01139cf9020a7781b0c064fa566832712 \ - --hash=sha256:27747162712e85c84598d364425dbf1714ff335bdb6ba3171c4e5081196e8916 \ - --hash=sha256:29631224698de1e42abc8fa7658d830e0aed0029785144b5832b695da5adef2f \ - --hash=sha256:29b6e7bc4442a56cf8e0dc1cabf3fdc77cd533568d6829fc76a1effd2ce332ec \ - --hash=sha256:29be9fd289e9ab8f480996ea2f686e1654b80242033843cb11691688329423f1 \ - --hash=sha256:2ba9933e8f35fe4a70f540b837254c4055da82dc3a9e500a8f95e61498083a15 \ - --hash=sha256:2cc66abb85e2108c9ff8a1c0d20fa260bf690bbb33caef4ff3ecb2c2cbdfff5d \ - --hash=sha256:2cd560498ae8e1bcc955643c1d78eb8e338226d07a983c656ea8c4443d3eec0f \ - --hash=sha256:2f79cc3e8039a8cf5c77e0811b0807953fd52d0863b9b76970b20d696dc64a78 \ - --hash=sha256:2f8a4b0b4d639d525928c7f30de527bfdf9ead6e44a5e8cb9c50aced5e4590cb \ - --hash=sha256:307c1acd812fe897e7fbe10c6758822e8c04be4e7c60a9f54901cdf8b5ab8bc3 \ - --hash=sha256:3126f2a96704505aa4e92a72d6e8a5d7f29d40a987ced8bf69e29d71dfc71fbc \ - --hash=sha256:31e8901637e20ccb3cf8f8848b5d0f7a00462bf5b34f7cf3dcbb2753b18e8b39 \ - --hash=sha256:346ac52e56bcda320c0dcdfdd081947ed7cada33afea4e2284bef7b0733bff9b \ - --hash=sha256:348bb85e2038b40c007383616d73f734869063772372519549ebd7da1723d1a4 \ - --hash=sha256:3533a03e4e789baf6a286e7b0b1b6da3f3d7c3eab569686ee29ee1d8b52e2cb4 \ - --hash=sha256:35977263d9bf506dbc65349f63b3b8c91606d4abc110990945e3b94bc671319c \ - --hash=sha256:397599503b718f0137f26d3f6532d6955069cd2e5917c47ef581495bc2529ff8 \ - --hash=sha256:3bafff8598f0528017ddc74194e5451d5c22d046c98935f8f86247b0f286e4f8 \ - --hash=sha256:3d1f48582686a0a3b81e9b43234766cc96697df72081af3f48107bd3f34d34e5 \ - --hash=sha256:4261863fc8b5ab1b815ede94e592e94c6af5b04616014929057e61859e7382a9 \ - --hash=sha256:43a4b56555bbcf8af161e7c7682bd93eec10f068c95844511864c018c8e5e13b \ - --hash=sha256:45cc39ba50fb0754a4359b90f8229ae08598fe2266abe3521b4e5a9ba916534a \ - --hash=sha256:46029e6e27a3ec0dc55b53f58df82d10f04c5e111f78248279b530bedad2c30a \ - --hash=sha256:48ea524a25a1cd5972cf293bc95713918cba0bcd6fa9b992d906c857c546abe2 \ - --hash=sha256:4ee953a5ebaeed38dc21cc032ed17a9d9782802e00042200497ab4b01b0bf7c0 \ - --hash=sha256:54af1266710cb0f305127ae0b970aff8d208057f8a29cd6e1db99b0114947035 \ - --hash=sha256:560b211fc3bd4a1e1c6de44f6d38113bf5b410dfc89a4c0d2a3c0edbf1a0dfb8 \ - --hash=sha256:563661919f603374c40cf45ffcd25535c12b8954203569a2ab1cee5265871cf4 \ - --hash=sha256:563d6500ca80dac7bba6f48a78e0ffd87e21a7d4d24642c6503a2ddccd70c110 \ - --hash=sha256:59e539c4eb4d3a53b0e630a6ba2b2f2824732b5e73f90e30a280f12fde157b15 \ - --hash=sha256:5bbbb696c8024475b1877d14ce20d5f1cc05b8f6d786cea0fe3aa7fedc02e891 \ - --hash=sha256:5caf684986a2490628f059a99dd107b566a2d34cf947f8eb8387e0500a1f90c5 \ - --hash=sha256:610c7637bc36b90f39e6c66f710f93d57018f83d53e1e187caaa218c6892b95f \ - --hash=sha256:628ff11e6720f90acd0c305dfa3339f04a783a20de8cda6ac333ba46447261e8 \ - --hash=sha256:62b8e291a4f7edbf7cde7a43d831d893ba443a1b627498b53581943b0e348feb \ - --hash=sha256:6300d5176647145ba1e22991c924fb29743e54b4d7b8bc85a0d3ec0e55e189cb \ - --hash=sha256:64eaeda36ee8d88f9e8616a587a8c66a663283cf6e0dcf013c1ddd8c758e4aef \ - --hash=sha256:658f5a1895b804423d97b22d06fc0d0b171c7c01dcc3aa9c8faf0c0e26a249a5 \ - --hash=sha256:65c85c79f5a2c04fbbc18f006c014674dc5fdf270cb978d8862c82c6f694e60c \ - --hash=sha256:68186a2d4051c8ffd17be33553bea2ec9bbc8ef860fe2980a221d96126296f31 \ - --hash=sha256:68d40b2bace413f3231f5729d3fcfb1837fd31c4907e241b5d43211bfd76f3c2 \ - --hash=sha256:69708fecaa88bcb2341397b49fc95057a835b02a3670c551b37f95dd79e64e3a \ - --hash=sha256:69b3e519a132bb943b0daae15fc8c2168706b17f826481d32a32a5e784b129e3 \ - --hash=sha256:6b62b7e0025aa48dec11e125e655d1157985a5fdcec04b1ad500101ad072b891 \ - --hash=sha256:714597cb5d5e15a8a449d2ae23c45b486a9e8fa33c462c7a33d7f35b65d92943 \ - --hash=sha256:758233648ac47b07c575224c4eadd73c8929c3b4c31e2afcfea935fde1cda735 \ - --hash=sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e \ - --hash=sha256:77745725125d01fd613b6db043362aa7c6bfbfdb23d45dbfc3d92bf58160af62 \ - --hash=sha256:7941ef106ca1f2c62314a13c7ed913bcf49641f3efdc12864d588e17870920ac \ - --hash=sha256:7a2573d0fd34f361a4a14e54d8cda3a91ac4e55fbf0d719698024f3b09c5b147 \ - --hash=sha256:7a62e302fc8cd6aa8972207e7e951d1fdee7c1dda18568305041d19f0e2c00f5 \ - --hash=sha256:7bb0dad75068fee80fcb60f88569722c199d8656a16706702dc6e3b786819c90 \ - --hash=sha256:7bc7003991ebd368a20d05228137a37b3d3066751f3ea1e4f7b8efe8e752f2f5 \ - --hash=sha256:7d26dc8f070c0ec5579e987fa615ffd6883086106eefdff9e10d160fc5630630 \ - --hash=sha256:8125e60f3c70e323ac07dd8b3635f7b3bbc5c3a9ac04ae5988f668ff7ae28a18 \ - --hash=sha256:8180b635290a75af8478f1b3e9810135381ae24833293fe77b85c1c21ff842ab \ - --hash=sha256:82780eb8bf59e8fb25dd081fde6e058805045d6374a7f2f877effc826ca4434b \ - --hash=sha256:835d5a90b11d1f5f8200ff3cc8316bded76eebebc92436398947a27657e645e7 \ - --hash=sha256:83ff054b04915be5c15680da6c6012474a2cc2bf534129a0e8c6a99f17ba7238 \ - --hash=sha256:8457aff3c12a89a8e1c4674de5c777857fbc429f40fe117a3d29538547cbc364 \ - --hash=sha256:847d6082ae694dc95e548acb201bc100e1cfa96513bc71fdcb86f709dad6c435 \ - --hash=sha256:883284137e25318ed9735b742ae46341a864888fae28e8b6314c4f84da080f08 \ - --hash=sha256:887f9a975996032c686719eb7b3e1e7942fab5079c2b778bbd9afe9a9d78244f \ - --hash=sha256:8890c89d662560e51c55ac1304d6f919b23942abe9ae1127cb1de9aa6132fa52 \ - --hash=sha256:88a6df88567680504ae28bfa7a1f2f64243d91e79a40b2c92ef42efc531e23da \ - --hash=sha256:8d1046b5427dcafe6e8a0e07527dd74f1ee694006160162f53f3a17f15aad3b4 \ - --hash=sha256:8daafaa0b2eb43f76898ced78b1e0fb91b38c4fa50da516c18067f2a2d578c20 \ - --hash=sha256:8dc2d9c3a924ed14166e63650b2cf9f59e7821743bdd50b23802bd97ca09bde5 \ - --hash=sha256:90c10b22860dbd09982d0b8993b66231a861bea2993d4a817ff35273f6ea285a \ - --hash=sha256:91fa75d0a693832106d98f66c849f034f21c828d14437f1fb97d3784aab89e84 \ - --hash=sha256:930c6058047410e3edff445f5a6e4457f2e089042dede00e2d18ce06f3ceae2e \ - --hash=sha256:9442b14eec262a1f74369bbd07e75bc5155105164649a4b9fbc1ebc7b8fb0b14 \ - --hash=sha256:95c27b4f3f04320fc44e338573f40c5c956b504a7fcf081a157fd0b02579311c \ - --hash=sha256:9606f583e7acaf61e7b3f56074e14037b9af7cb194590edfc0114b3ae5931ff7 \ - --hash=sha256:962f18c59a000f30b084ea2e6b8001521bb315efd4e5f10acf9fb36f366b7882 \ - --hash=sha256:9caef53b20a105c0d66518a34be2f71b2783de8d091767575ef86f6ea422236d \ - --hash=sha256:9e37024b41d7a7e7e9cce14b248d54707c21c2a2ea30a47b71bdcefcafec00f2 \ - --hash=sha256:a5a7ee1217949ddd43c6b7bcf70d5c22193bb50e8c695386de5905325e93ce9f \ - --hash=sha256:a5e1583c14775580da05641240ce0d93f36ce3ddef3d5083a827468b0bcfe874 \ - --hash=sha256:a9e246f67ac038568b854ed7c5578e4c6af1f742359901a8fcc3603ff1358df6 \ - --hash=sha256:ab83fdd8cf307353edba9c427c17a3a021c2522d690f5633dd9f72d28b48ccca \ - --hash=sha256:ac746cb365bac1c462da9e3e6ab8904a8efe2217a56b0b2e3d9480f41d2b2602 \ - --hash=sha256:ad474c11d851b6fc97cb625e4822bc0cbd567fc07dc2602e28faec5a36b42bbb \ - --hash=sha256:b03ca066b47b18b205cc080dca6f76cbd159f8cdd33a02a0700164c13b37e463 \ - --hash=sha256:b1cd4d66ce894a45482e1ac2837c31d0bd447df35065e542b60055aa2d00404b \ - --hash=sha256:b25426f9f6ed402835617c8f23609a47045f91ecff365eb6734817e039a8ed25 \ - --hash=sha256:b367c342327717d644db4c0ddb37ceb655c84822215ea0773a3a36911b74b71d \ - --hash=sha256:b7e62b8fc7bd6cad007b9f2e0ad9c8d4854c06350d5f51e1a439dd18b510ecac \ - --hash=sha256:b8b7aa75146266fd3e2a2437cf69ae188688c04ab8665b163d4257b46c1e0c83 \ - --hash=sha256:bb36381e1f9f9d06eba2f10bdd438e5d20c07d5b55e1a3eee30b9f44cbf52316 \ - --hash=sha256:bb8c7da8c861391f7ae48e3593762be2dabe405109e01aec520fbe1a6d15d14b \ - --hash=sha256:bb9a60b7faa5d37c426fa91cf4d6738182a1f2755b9fab7c9c64cd466c4ce51e \ - --hash=sha256:be007d1aee2cbd530347dcafedb400891a3b5f1bd7135f95cf5d5b330b5219ee \ - --hash=sha256:be569fff1d85cd29391c431c5641c8772acb75bbdc61e60a8e82fceb9023d385 \ - --hash=sha256:bea7df027015856ba5d0a88e3b4777ff8cb5c66b58fc108050fe79d4dd9d4d2d \ - --hash=sha256:c0fe437a6d2f36aac2b49517057776575b5bf359df314cca20d230a6e139c089 \ - --hash=sha256:c2b2a96cf1dd99fe7867be4c013314225f4d5786e6685906e29932d42aca6f11 \ - --hash=sha256:c2c5fd0fd39574ccd58e1a52565b341aff522c5c836f1b3eb7605c371e61f52c \ - --hash=sha256:c46a08bf070d6849fed483e9d9833f9d06aecb8382ed985be0b38508b3ae958e \ - --hash=sha256:c5f3a2af441670d80ce5fdf13b6c1b421fc1fc7fc5182d58ac7486738bb2b742 \ - --hash=sha256:c60e50bc5b07faac92fd3a20fa21cc8cf3e3f7204d2867b206c73293ebc19101 \ - --hash=sha256:c68e0c0649d17c2d0339e3674e86a4aeba4a7e6b21c1e394cf947a95433b31d0 \ - --hash=sha256:c9c98d2f0126ba84cb45601eed97ff67ff767e19ae6eb3c31b02827b54d700e5 \ - --hash=sha256:ca52b9ec80851366197577154c862c4c4c7036ca76ae94cef5cb59c5cfeab944 \ - --hash=sha256:cbd86f9787c5e2f5fd27d8b21458222f107347c6731c4e93dde68f554b466a2d \ - --hash=sha256:d0264f8d5cb0a803f650a6a8572dfa0cd1e099a2234c588dc8fb220b415b865f \ - --hash=sha256:d0be2b832435001bc623ca7f1499ca1a853d4f082fb61221a80ce71132f50b26 \ - --hash=sha256:d244cf6b52b5ba1c34c3832f4652a668ebb36d95949b96eed9a1c54d916a90dd \ - --hash=sha256:d2d236b8a44ae91536a12ebcb996bdb31cf27425f36b4d05c87f2ba2716050ba \ - --hash=sha256:d3da668e903c934ed0b587ecacfed6901f6ae6384a6e975887592b61845e78bc \ - --hash=sha256:d6dc7804c50fabd28644d4d18a4b20aad3681b3e64f3acd3182b330ca73f7a32 \ - --hash=sha256:d7e5ba0a0153e35fbce9c51df530c8b4cb0c3012b46a04ff9a048441a269c2ed \ - --hash=sha256:d8a5ac357ac283490a8d1899b0383355fd1f8634b14ba0d59e4c0dd97db85556 \ - --hash=sha256:da1c112c5784ccd9d32cd90be6739fee32644e874eff6ae8f0497cba3e352e58 \ - --hash=sha256:dc911ae6152e455b16a2a1a626aa6cd612fa01efb9d0a4ab3f5cf328b911483d \ - --hash=sha256:e0db3a4d1e264e225037a6023888972c25206a96e016021a5bea41c9a939f2a9 \ - --hash=sha256:e192018b732f7b168e6604cbdf40fa8e05c996693b9eb445a0d8a73f4b77c5d3 \ - --hash=sha256:e37b744849fb631bb52e3dadde35ffeee365a6c41cf71257b5b7acc9cd83fd38 \ - --hash=sha256:e41226ecf607f062fe34a2f4cf64ad3a89e3a0180dc800b463b6b14c06dd10dc \ - --hash=sha256:e418ec99574ca24365ca96546af285c2b021a1a072478a79f0e3cc3b08837154 \ - --hash=sha256:e6ec7d37841609a691b96a10b4fde386c7cd93ebbb939f59c9f23325ee788395 \ - --hash=sha256:e886ef8c9879105fe4fc99417447b3a5f35d1131412ce839470bd2089fe2043f \ - --hash=sha256:e8e1e895e23818d343e4ae7dd95a0a556fdeaf8b471acf1c0a39b93c6f54d478 \ - --hash=sha256:e9dc7b4ff6ef184504b49ef9a4113d49a646653b2ce89f5f48c1f57cdf6ba081 \ - --hash=sha256:ea880d441be7c510106bc56064be39266d948aef94ad4955e8784690019a5d9f \ - --hash=sha256:eabb03dc3e4ed6333ecd1cc9826ec80e7a98b5506deeb832d7260c8e44166d23 \ - --hash=sha256:ec0a4d066356054d569a66e0a94691a2058b680be5e710298f61db11a3c4609f \ - --hash=sha256:edda19aff836ec515caafc09ea53d2ab144a041f09ee9a7cefcbd3ae4e976256 \ - --hash=sha256:f1f4a220db6ed7c8fd16b6d644ffd1f082651693204daf3275e049fadc849e39 \ - --hash=sha256:f25b61a708bd276e8cbb6afcbbf1b8e793a3be70ba0a842d0b8692020f83b706 \ - --hash=sha256:f2fa3d3b1c933d4bcb8fd2018700d5e7235c52f2ab8c88d22286965c5c0f00f8 \ - --hash=sha256:f3071e6515cc63714d014da8f738ae9fa3997c476203f3cd46de380c2376ed7b \ - --hash=sha256:f3a0a31189acf6703307397c6139ddabd734c20c5ef92649fc93e473df6615a3 \ - --hash=sha256:f7eefd0233a7c33ca980a5cfef26f1e9b5e2137839e752a99963696729f12d91 \ - --hash=sha256:f8b09b25e0f4dc2ea9e2adbb1cc3ba11a94d6fa3dd978ae659c8743052e1afbc \ - --hash=sha256:f8d7b66c9e09c0bb0add2b5895e646b62a0849e71155066f215523de6b95cbe6 \ - --hash=sha256:fa6c2880709c84457de104385b704fc28860f27e442ad13966fc4af8e714fe9c \ - --hash=sha256:fc5460940f50dff00731b4132366840ba9685286ea88ea104b661899084f3fea \ - --hash=sha256:fd789a294d8e098528be29b2669b83005ce569339f8cef167fc0274c3115c34c +multidict==6.9.1 \ + --hash=sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c \ + --hash=sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9 \ + --hash=sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa \ + --hash=sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188 \ + --hash=sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201 \ + --hash=sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382 \ + --hash=sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f \ + --hash=sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919 \ + --hash=sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418 \ + --hash=sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771 \ + --hash=sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c \ + --hash=sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76 \ + --hash=sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345 \ + --hash=sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580 \ + --hash=sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9 \ + --hash=sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1 \ + --hash=sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1 \ + --hash=sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64 \ + --hash=sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5 \ + --hash=sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006 \ + --hash=sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8 \ + --hash=sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02 \ + --hash=sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132 \ + --hash=sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c \ + --hash=sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19 \ + --hash=sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b \ + --hash=sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba \ + --hash=sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b \ + --hash=sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08 \ + --hash=sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe \ + --hash=sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f \ + --hash=sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd \ + --hash=sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879 \ + --hash=sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f \ + --hash=sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9 \ + --hash=sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987 \ + --hash=sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd \ + --hash=sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea \ + --hash=sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc \ + --hash=sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c \ + --hash=sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b \ + --hash=sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709 \ + --hash=sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4 \ + --hash=sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce \ + --hash=sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94 \ + --hash=sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a \ + --hash=sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e \ + --hash=sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b \ + --hash=sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96 \ + --hash=sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1 \ + --hash=sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e \ + --hash=sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455 \ + --hash=sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3 \ + --hash=sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471 \ + --hash=sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d \ + --hash=sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b \ + --hash=sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d \ + --hash=sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f \ + --hash=sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004 \ + --hash=sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4 \ + --hash=sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e \ + --hash=sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5 \ + --hash=sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60 \ + --hash=sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14 \ + --hash=sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f \ + --hash=sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9 \ + --hash=sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3 \ + --hash=sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194 \ + --hash=sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a \ + --hash=sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3 \ + --hash=sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42 \ + --hash=sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c \ + --hash=sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828 \ + --hash=sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290 \ + --hash=sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485 \ + --hash=sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e \ + --hash=sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8 \ + --hash=sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c \ + --hash=sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b \ + --hash=sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659 \ + --hash=sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b \ + --hash=sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9 \ + --hash=sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953 \ + --hash=sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c \ + --hash=sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1 \ + --hash=sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299 \ + --hash=sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94 \ + --hash=sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983 \ + --hash=sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d \ + --hash=sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac \ + --hash=sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a \ + --hash=sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7 \ + --hash=sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e \ + --hash=sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45 \ + --hash=sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410 \ + --hash=sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40 \ + --hash=sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4 \ + --hash=sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805 \ + --hash=sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d \ + --hash=sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa \ + --hash=sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d \ + --hash=sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701 \ + --hash=sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80 \ + --hash=sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6 \ + --hash=sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40 \ + --hash=sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20 \ + --hash=sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694 \ + --hash=sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a \ + --hash=sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203 \ + --hash=sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519 \ + --hash=sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125 \ + --hash=sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0 \ + --hash=sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d \ + --hash=sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a \ + --hash=sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea \ + --hash=sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2 \ + --hash=sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c \ + --hash=sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3 \ + --hash=sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d \ + --hash=sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3 \ + --hash=sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a \ + --hash=sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68 \ + --hash=sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8 \ + --hash=sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca \ + --hash=sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8 \ + --hash=sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840 \ + --hash=sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82 \ + --hash=sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610 \ + --hash=sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca \ + --hash=sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a \ + --hash=sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1 \ + --hash=sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5 \ + --hash=sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a \ + --hash=sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13 \ + --hash=sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07 \ + --hash=sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05 \ + --hash=sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1 \ + --hash=sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4 \ + --hash=sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2 \ + --hash=sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82 \ + --hash=sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6 \ + --hash=sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d \ + --hash=sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88 \ + --hash=sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f \ + --hash=sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126 \ + --hash=sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24 \ + --hash=sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab \ + --hash=sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67 \ + --hash=sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3 \ + --hash=sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1 \ + --hash=sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800 \ + --hash=sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3 \ + --hash=sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8 \ + --hash=sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5 \ + --hash=sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24 \ + --hash=sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c \ + --hash=sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a \ + --hash=sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841 \ + --hash=sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387 \ + --hash=sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec \ + --hash=sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721 \ + --hash=sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d \ + --hash=sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390 \ + --hash=sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1 \ + --hash=sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d \ + --hash=sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e \ + --hash=sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df \ + --hash=sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb \ + --hash=sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5 \ + --hash=sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9 oauthlib==4.0.0 \ --hash=sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd packaging==26.3 \ @@ -1483,8 +1525,8 @@ urllib3==2.8.0 \ --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 websocket-client==1.9.2 \ --hash=sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce -werkzeug==3.1.8 \ - --hash=sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50 +werkzeug==3.1.9 \ + --hash=sha256:6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab yarl==1.25.1 \ --hash=sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3 \ --hash=sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de \ diff --git a/requirements.in b/requirements.in index 1c8abe85..28a48cf5 100644 --- a/requirements.in +++ b/requirements.in @@ -1,5 +1,9 @@ # Direct runtime dependencies. Regenerate locks with scripts/lock_dependencies.py. flask==3.1.3 +# Security floors for transitive runtime dependencies. +# GHSA-g6x2-hccm-hh4m and GHSA-54p9-h82j-f925. +werkzeug>=3.1.9 +multidict>=6.9.1 flask-cors==6.0.5 alembic==1.18.5 azure-identity==1.25.3 @@ -44,4 +48,5 @@ azure-devops==7.1.0b4 prometheus-client>=0.19.0 python-json-logger>=2.0.7 sentry-sdk>=1.40.0 -certifi==2026.6.17 +certifi==2026.10.19 +aiohttp>=3.14.4 diff --git a/requirements.txt b/requirements.txt index 45eb9a8a..c0f93ec3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,125 +2,167 @@ aiohappyeyeballs==2.7.1 \ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 -aiohttp==3.14.3 \ - --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ - --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ - --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ - --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ - --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ - --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ - --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ - --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ - --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ - --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ - --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ - --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ - --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ - --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ - --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ - --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ - --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ - --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ - --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ - --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ - --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ - --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ - --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ - --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ - --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ - --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ - --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ - --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ - --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ - --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ - --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ - --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ - --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ - --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ - --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ - --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ - --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ - --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ - --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ - --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ - --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ - --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ - --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ - --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ - --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ - --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ - --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ - --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ - --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ - --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ - --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ - --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ - --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ - --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ - --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ - --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ - --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ - --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ - --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ - --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ - --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ - --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ - --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ - --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ - --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ - --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ - --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ - --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ - --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ - --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ - --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ - --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ - --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ - --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ - --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ - --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ - --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ - --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ - --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ - --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ - --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ - --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ - --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ - --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ - --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ - --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ - --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ - --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ - --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ - --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ - --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ - --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ - --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ - --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ - --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ - --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ - --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ - --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ - --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ - --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ - --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ - --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ - --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ - --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ - --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ - --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ - --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ - --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ - --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ - --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ - --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ - --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ - --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ - --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ - --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ - --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ - --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ - --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 +aiohttp==3.14.5 \ + --hash=sha256:0133c3c3b54a0bf1e71fa5c1ad95c93f07fd54e24ef1fe182f5122e1573d2bf1 \ + --hash=sha256:038c2c7e8caa26b6c8423779b5eaf1893904048a512c19b32fe841ffa5592b50 \ + --hash=sha256:0684952aeae1f5dbfe02d46039338513b94009baecd15d8e4098a357c4c4a2a6 \ + --hash=sha256:0790ec66fa4013e83c53b9025a45d454723da1a2fce28b3208c9b32d08af162f \ + --hash=sha256:083673c7a94c3ea035caaa5ca04288bdb44887abfe1f5ba23294e6a4b03efd2d \ + --hash=sha256:09e0eb18c7e0c8777e2f9149de63799195b9b3ca1b5c81ba6f32f2c6b8628210 \ + --hash=sha256:09ec102b4b8c9a920275733bbc11fdbb615efe6f9231a06007c0218d336fb77a \ + --hash=sha256:0a8ea271867e360ac985ae607f4a23ad9a38414b9aca1d49ec98839ae660e49f \ + --hash=sha256:0e6f16f5e49c4b8267988c05ab07760d7064cea57d077c3d068d04b0fbb992cb \ + --hash=sha256:1061b364556e8172e8d46b0b183adeeb73e8c42d30ebc745591e1bd89acad52e \ + --hash=sha256:1220353657ad49493551f089ce02f1a348fd57ffd585bfec77f2f3c4fe3a7346 \ + --hash=sha256:137351bf20bbed9a65e839f4a4452ac377389bdb2f2857d2acffef38f5e9f2d1 \ + --hash=sha256:149fb56caf7acb67073126f675d0958d9c4b3125fcd3f6d4877df98aa8a97ce9 \ + --hash=sha256:14f04769cfefe4734016a856a83af36133cd17779cef9ae817f812b8ba9d6d51 \ + --hash=sha256:15a310d3c71398e3d7bfc93a1a73fbe664315cd9e9016b8efc1cff85eeab7155 \ + --hash=sha256:1612fa5857b37bf32e5c1eaeefb96e3b01e9c70679eec81f0934e8a600080863 \ + --hash=sha256:16c8abd5bca220a47efe667d26f8460124c81810787e79ee87b242677563d9dd \ + --hash=sha256:19e2ba471507c34f8252402ab50f5ab512398b9ea8c8f1cb26beb3f75793ba30 \ + --hash=sha256:1aead151c3abbac6b32942e452020cb66d7efc099d253cc6c20f748e926c858b \ + --hash=sha256:1b438b73c38111818d0c9d6a5c2bfed8584c8e503a49ef085d70e874ec846738 \ + --hash=sha256:1b5416552740edf07234cc9437d0706f2acb67b93c198670b1a68e1b2b587dec \ + --hash=sha256:1d2d981b53dd09a319e3570ef8cc3bbc3ef86f5a7abef0f6b2bff3867db3a9e7 \ + --hash=sha256:1eb8167961ec4dfcc8cb9dd50bd0ee72519f7ef496be95203e49e27b01618382 \ + --hash=sha256:1ffa3a523a36d8628f98c06492ae16a31a23d14c0b4ec721757b477319f656d6 \ + --hash=sha256:20064a177a070d789ee64a50b01a9161d3468e989baacfc6c714aa685c4b332f \ + --hash=sha256:20726f9782d5c2744c1c66255842d1d163bb3edcf768b8de25216bf47f7b6ccf \ + --hash=sha256:20f085697d7e911f1f73c43ed03fafbed1e7121797e2eb5428efa80398060584 \ + --hash=sha256:225c579c23b68b343cccea27a7e06e3bd8ec23a09c30b427eb3f1e4ca6239b20 \ + --hash=sha256:24409db442e2fb6e766bc7f3943851a8381dec3098140e43bb2e843b79e31b12 \ + --hash=sha256:248d779ad720b49d4fb355720e60c9e5f444f95887bc16974fea48fc56c41789 \ + --hash=sha256:2528cb4c6b92008c76ac9ac6298624069bb2db91ff4929905512d1d84485f658 \ + --hash=sha256:276a4fc00b1d9ae492b802763a789c5b86328b989c5ea169f2faa447d6a11c7c \ + --hash=sha256:27c2322e03f66101acb09869ce1cf1efc04994ee95e1735b69827bf8c8b9d781 \ + --hash=sha256:293d3ae7c6a0ed176a42e59a1b5fde825ead65c835360f734148e96729f928d2 \ + --hash=sha256:2cc38a4f2b516bef1714e690df87a0e043faf1a7693c82d860091684453d5111 \ + --hash=sha256:3093b72c215bda16ce961a6d073f6e71d46e022962a9d5d457c5d4d421c78b57 \ + --hash=sha256:32e8fa6644e541fcd7e02430588c7fc93b602c1778ea0bc345505db76b61cfb4 \ + --hash=sha256:330900acd0dc4cb8b27f9c127fbaad770964845338493e7906ae3822e82dbf8d \ + --hash=sha256:33f706574e32c6e694f352a856e05caf18f7f2c871b3e87b41c55ea452b409ab \ + --hash=sha256:3ae800a20947e2c2e53088047d021e6bf7d51560cc49f6a0737a1f79d2e3a13c \ + --hash=sha256:3be7dd397d64ca3e1869626fa9318aaebb54b7bf93bc72d7a205448d83e4f748 \ + --hash=sha256:3e0eb43bed3c6801a6cee315195377789e90b2a72c2277a475b578535312488d \ + --hash=sha256:3e51a27980c3788e6e6b3325d694fdd4898087fa8a86b2763af77b39353da41e \ + --hash=sha256:3edbece0379b8b4aaa67619b8aa2399bb66fce372cd5911098a434ea77220aa0 \ + --hash=sha256:3f2dcc00191fd563e9075181a14ec31d7dd63223ced7582cc70a15a499de0c79 \ + --hash=sha256:42b5e616946dbaf505e2bff18c9af2cd4ef9e7ef300ee58a6e951a5b7cf147ae \ + --hash=sha256:42f320d4a5b00b9af0bddcfec5407dc6f2d9816f006b2f79ebbaa31f16895df3 \ + --hash=sha256:43351bdb5e4c3cb7d1772368e988534e869a74db7778079a83782c11c69535c7 \ + --hash=sha256:43e1b7994a8b038125f722bff07492ef501110722c2727c408995d9fb864c421 \ + --hash=sha256:4887d130a7bbfed3a85493bb5a25e5b5b558d40c1d986dd16970d2bb26d63793 \ + --hash=sha256:4f07fe3ac408d8b3f768be471dc3f56d43843c47d97c66120534467a15ead197 \ + --hash=sha256:50343c1757b4b6f6708eeaf24534b32f19dfb99fb1b762c00420867a62fc81e0 \ + --hash=sha256:50983e3be33d8c0942ab88cec3905b10602f64c469b20153c48c5d4e558dd016 \ + --hash=sha256:50a195903119008fe9cc68710535eb37f556ffffd6a7759afe70a2c145587045 \ + --hash=sha256:56572c42e3ecd636de8d2c3dd54cf5fc939cb5c32eb56297f176a0d366fac622 \ + --hash=sha256:56d9828f204331a5ca8850fcfe2bcce95a149f1f223f60cc7216e5524978e480 \ + --hash=sha256:579f97d5120f2971876d2ddca2968135f6944d00c44c3a6590ad7d86ca9b403f \ + --hash=sha256:5af42135fdfebdadbc2bcd9c0842a48ccf0d62794c36a260b21dc4b94d1e0119 \ + --hash=sha256:5ba14a839fbe87cf7c12a6b5661c05f324a296eb8363141edb3944ba63d4c9d3 \ + --hash=sha256:5c76f1802bab718a68ac3cce447160605c734551f95c67ae90fa1132b215cb29 \ + --hash=sha256:5e8f97c0488ffda3082766ac0f2c8150a9a58c4d05788330e479cfd449b37939 \ + --hash=sha256:5f3e96071686755d9cd3600c3880183eb94b012178e92746d68101800f0ed8a3 \ + --hash=sha256:5fb6a6e919bfb703227bc1ce6579281b84b1a2ba57deb9794dfdbec7dcd1e40c \ + --hash=sha256:602c1e9b718a3275c580149f947e7fac65044c0a20e599553fb12e9700da9eca \ + --hash=sha256:657291433bf4dd3142f3abac495764cd47d0c7c92087751e6666c6447e65fcef \ + --hash=sha256:6774814fd5c338e72ee0da5cbb9432816df450e69c019f72b5d29bdec2a1792d \ + --hash=sha256:6da32b5ff3fd78d244e37300463434c7145162bfd2b6e9e915ab164da37f7343 \ + --hash=sha256:6e1d8637cf73eebc92eba2e11d4cfff98a3b562f2505bd75bba766d908926e8d \ + --hash=sha256:6e4251c0ba4624a68a2c11471a1ac54c3306876c21f0ae86de085cc9241c8905 \ + --hash=sha256:6f275c11d1aa6d4c458e05a68be084efe3c55a113d99e3f46a318098e52948fc \ + --hash=sha256:6f967dde489ca6a8c02d093ab245d2cbf50ccb5c36adf0188b17b0ca39d24b67 \ + --hash=sha256:6fdcd6af7e2e51d1ba1b4bea16e97b074bcb7b5dd0246a9d8201341bb28085a0 \ + --hash=sha256:70cb4008ac2ed1e0ca9e824deb4b53d3aa0d939109698ebf1e723a84337bd794 \ + --hash=sha256:7457580535e019e1247ea35d6a02bf081ad30c26d0cbc210c93f6c3ab67a0835 \ + --hash=sha256:74b0a9c8270f9b0a11410e124ff8d4f18bfc1f1837440ec84da5ae7b50927b5d \ + --hash=sha256:74efb69332b85675b1eabd760a8cfc2e2cf42c60607c66f88014c1bdfb40942d \ + --hash=sha256:755933b107ea7a6a9ac916f635a70595a5b1a32fac10a8ff0b9f2ab88555550c \ + --hash=sha256:7779cd97e61ebe583ec2f1c5616cdd038aa08a4453b1848c67842176d054948e \ + --hash=sha256:788ecaa9c10533b786ce5ba70c4f2df78ad41819fd00a6c99d92b66f9a32e1da \ + --hash=sha256:7bd8ac754ebd6733a3e2a0dd1674c4d8ab086196803fd8dcd776f07b4e2607d9 \ + --hash=sha256:81c2b3dfd56c62bee6108e4852d5970b4cf9086390b6983f52b666e878c1f115 \ + --hash=sha256:823c910f046f23f4c713b8d99a2242dc65f591cb45ee86418fa11762a3c2963c \ + --hash=sha256:82c7583cd3dfdc7dcc927835b4f6c7faae7ecc1ba3ca5879321621ae2e6f8e84 \ + --hash=sha256:8966ecac808dd5f473c9c4cefd10cd3ffda71c18a4d3493b7c7d2ae1803bf2cc \ + --hash=sha256:8df7d481654ac96fe1ba9a02a9f67770fdd367823e0d5ef01b922725c4bd2cfa \ + --hash=sha256:8e317e0fb6b16212c881d2205a7d87414c29acd69320b3aa6dce9d9c7b86fe4f \ + --hash=sha256:932ce7e694bbc29b2bf6f64f2343c27d148d4997c771d01bdade4639b6749ff4 \ + --hash=sha256:939042d5cda21d41a6f512e7cc8b8e33a2aebff863352251da495fbd91b673b5 \ + --hash=sha256:94684b879ac1d71e4238850c99b62dc1b28d9086b156a2555f082010b85a865c \ + --hash=sha256:96a2e584f0b9ed8f1fa33211397dcf67bb7069866402cb405d191c2f0defb9a3 \ + --hash=sha256:9ad7e6aa38c20da1be697874349c4c273c8a03b7887169665081706398d0439a \ + --hash=sha256:9b42db919715e91eb76acf3bc492a9a7ccd8bd9adc6745c1412b689735269f14 \ + --hash=sha256:9bab2045550c4fe0f7baf89574db1b455c195750702ba96fef1f16972b146617 \ + --hash=sha256:9bf1d5dcc15204d9ec8b8ea4c18fd66e6b80e5de1f4ecbafb3a2f2740f8039d4 \ + --hash=sha256:9c061aa954daaf57d2a4b8374f9fca621ef0e1b603584431c220c22458c59b6d \ + --hash=sha256:9c428eb2bd8817588d16a0ab898aa4eb5d141f896aa2b394cc79a4cf61d9a8e2 \ + --hash=sha256:9cc882cf8619109583c906b4d4a85d6a111a98afa34b7a450d1e08118d016820 \ + --hash=sha256:9ce66feae6ac65327379460380549bf1b8df8e17c4e25df2a2bcf168272e3bed \ + --hash=sha256:a23fe35d776bc03cb495938b9594450d047e3bc08c5255315a82323e9cb7d2dd \ + --hash=sha256:a2c473a355f9239efcb72c92d5abfd8fcdb0cc78c8e9af607e72ca12dbb36593 \ + --hash=sha256:a63afd1f757de949028387e65a7127b61ad0f775432dbb0e62816ae619fe69ac \ + --hash=sha256:a6d02b4c38de03d9c7617813433e6a0fb6b522797974177d69d9dad431900833 \ + --hash=sha256:a7d470cf7b206e6359fc77b1b860632fde400d5a2ed59cd0181b93a686bc81ee \ + --hash=sha256:a95529a92a446db351675f4aab518feaf5e99842f63f5dd17160c2b74f382db3 \ + --hash=sha256:a9918e58faf62ba2c7147927d06057aec78f42475aff5048047ec47e7265a600 \ + --hash=sha256:a9d3983bd6ab7aa1cfd573544ae98df9b6cb6912a5185a198263e024a636861d \ + --hash=sha256:ab52d8f1fc1b64821c1fbad64a647ed6203627004059a6d1ed4f0858a1499703 \ + --hash=sha256:abfda5cb094a829f7bc25216a32f7db2e85cc65bd59910f8e7b40b3d9b224764 \ + --hash=sha256:adbeee7d6fd4cf5fe0aece2fb3edc4243615d3180430ba8149d01a90670cac99 \ + --hash=sha256:ae53924aa853a7a2ca20ed4142c7c6b56338e4d4cd999e2980075b9efc2e257a \ + --hash=sha256:b032a0023eb41d768ce77d83210ab2a3c389bc0b09313273c7e1eca48c10a755 \ + --hash=sha256:b1b8ece1e71132d2afba4dbc0c3d62c766e25165990b25db1196c04969eb3d84 \ + --hash=sha256:b2966998927d7bed9db12c0a4647b0c7b179755878fc9c357fe1ffd3e3b0c1a5 \ + --hash=sha256:b3cc509327c7b27f6f4727a8830f4004f6df7766e179f2f4b8e54e65c0bec5d3 \ + --hash=sha256:b7806e804889231b0e06469fd4a5c06313d1c0a3377322b6d9237fa5e0fe4167 \ + --hash=sha256:bea559ad70218d230663e4210875735076a9bfea5994cef34a55a25faeaf2544 \ + --hash=sha256:bf163cc701f3d4ac43ba7d97771bf5fd955220ef5500ef3ee847bc0ecfbf4ec1 \ + --hash=sha256:c147451b4a58e7050f7f7394e6c467867c84161560001f9ad4fb2d1446743946 \ + --hash=sha256:c172db893e516e1358e65a95ee20b7ce7173963eefe318b6ab2a2220688b999e \ + --hash=sha256:c1d60eafd9c7e8e74abd03a5b00df44e7febfe6d9b89b559c0a6551eef0699d4 \ + --hash=sha256:c2c30484dd1417ef98b51021ffa2cc0d7f3c78918adaaaab7e70817335ab3e02 \ + --hash=sha256:c32e26310cc10e547f53cd13d39a369034f69dcb7d749d5cb0e5f67bc196b6ba \ + --hash=sha256:c5ed596aedb9c42afd3fe0aae3117725378ac73d2cc5ddc735056fbdb96c5d02 \ + --hash=sha256:c8859a013ae0de1074660992139a1a440df3e6b219b86cf0d3f11c2692bb4fe3 \ + --hash=sha256:c8c4478bef6d57fcfda15dae461ea3c9f06aa7b257c58df3f2300174ccbb185a \ + --hash=sha256:cb11a971a3aea10f9b8373be628f1df932964fc6c6b174516d318a48c3ac4412 \ + --hash=sha256:cb131d775a1573c1aee66656bd78b023577bbdb6cb8349a07773bd4f73e68a6e \ + --hash=sha256:cd88b01f3d37b7a2a34f91d98f14720206f1ea3d540843fab2d649dd5fb91fec \ + --hash=sha256:d05e94cdfe0d15d0206f970722d2554780ce562787b21b218b275447f8751319 \ + --hash=sha256:d079c0a0135c36e7beb6f1c88087c8f108dc5891cdd0b5eafa778421bda70ed2 \ + --hash=sha256:d3112585250b199296c26ca6e0131640b6a8d01bab8b232d2eb3763ed469de11 \ + --hash=sha256:d418ce2af40c6bb685b3f663e9e8de27cb0a22431d8e88a167348d7f01878073 \ + --hash=sha256:d51db97c96384fbfcaf8f4c65922183a68b94f891c3c10c862ef5f6df2adbb1f \ + --hash=sha256:d94e44be379e569758fee8a9a58431cfc3c2598c708b92b1cfe96c66b4c94aef \ + --hash=sha256:dab9ac5a67c8d1f070c00fa8fccb7cbd1b8dcc1a8d6b42f37540df9b3d4cc603 \ + --hash=sha256:dbf53ae2601b7fd5a93c3944deea3a78d40f495226d582c35ef7a433425ce2b2 \ + --hash=sha256:dca3fa8d8a0a26679862eccb0b1a9151b2b9f1cd2c212e7a6335778faaff5833 \ + --hash=sha256:df37b620684e19b5e25724412518ccafc3b1a49cdac706fdbd2f983fad943450 \ + --hash=sha256:e1cc2bfaee8c214f06080a7c7d5772419b8a1108e8e5349236189811823fb02a \ + --hash=sha256:e29347c142cf6e99e0dff5e2995ead1d50fa3b51bf37a7c726a7ccfe5419745a \ + --hash=sha256:e4f5cf4dc72a71c4cfa9751b4950be22f733626670230d46e7d606592aa22d59 \ + --hash=sha256:e724a7b6091f0b1ac064f9d1b15ff9ec52e6033a86cdae649e5f086e32a3c0db \ + --hash=sha256:e87046c8ff77a8decdb6a41d8ab25824b47531b2da933aeab0c1e21c7acff329 \ + --hash=sha256:e95c8def4b81c5d68d5cf1f54c07acd7c0d2577af244e5b6da802120825737c6 \ + --hash=sha256:eb324e2009fb54db30a071dad7caf6998ee2879c4704007efb244514dad1fec1 \ + --hash=sha256:ef60869969180ec2464f1349aff07138ae35ca2200f0946cb3552e49e8f301a8 \ + --hash=sha256:ef692a24087a699c0a4a26af45e746e0c1eae2116f6d8a5ff91d8aae2b867b45 \ + --hash=sha256:efc21a454892828368b11c2c780de0ff8bc991f73f6b99c6b66e56205470929b \ + --hash=sha256:f001b571ead90ca1770f1e616db255351a1703317f20374c361ef22f12c06d09 \ + --hash=sha256:f2a7966bda23dd85051f1661ce0ace38d6890e05ec6c357ecae9d2479cba377e \ + --hash=sha256:f2ebb54b3f932210503072f09974b4fb574d823e497a944adfdcd140a6a00255 \ + --hash=sha256:f2ed8b64dc0c651c0f5a9c926777719770021251b8f336d97c4b80b660836ce1 \ + --hash=sha256:f375db73a39f5cf83696d500e21a67f418dc9a988955756f254be8f03b7b3651 \ + --hash=sha256:f59c7673465908cbe506117176156c127f29f917677afceada34957179221d91 \ + --hash=sha256:f8d40ce41991e9d56fab4f5dc4a51fe59bc3b5c77c27f4b148963064d00232e8 \ + --hash=sha256:f9033b43f511f27547c557dcaba0177649e10a3725336ccd2cce0fdc1dc4850d \ + --hash=sha256:fba47bc2c3d7303c3d027c6cf4d07626c37b1314ac81f5820c31032e0ca1f677 \ + --hash=sha256:fbdc5ec49f9ca3cd24955cf3520b10a4d4c901ba2572094c84274e9e7eb30534 \ + --hash=sha256:fce9523df31cea6284f3e2c479876750d7687cf671d7b25d32b19effc0e86441 \ + --hash=sha256:ff75a7537413a86e7cafe98e0e1d6e3dc4b15c6349896e7d5c6b881bfdb6d550 aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e alembic==1.18.5 \ @@ -187,8 +229,8 @@ azure-monitor-ingestion==1.0.3 \ --hash=sha256:73f4f1067d58df6cb9f23023c0e8f15d9bb7feb84684061203daf277ea99c717 blinker==1.9.0 \ --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc -certifi==2026.6.17 \ - --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db +certifi==2026.10.19 \ + --hash=sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b cffi==2.1.1 \ --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ @@ -833,177 +875,177 @@ msal-extensions==1.3.1 \ --hash=sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca msrest==0.7.1 \ --hash=sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32 -multidict==6.8.0 \ - --hash=sha256:003a3bddb32915c3f67096ea41d24e53edf710edb65a1f5d0c70ab40b0e4d20b \ - --hash=sha256:00be37bde741bf60871082cd347a093218c44886e99231b7516671c70f2c280d \ - --hash=sha256:029897732a9c798737457e382bf84e8c64237eff224a90aea2639f4413c45e4e \ - --hash=sha256:05c2e90c5289c5f7436ba2c25812a5fbdaa1c1bc11c8d8d3bbf64f5cd7c633dd \ - --hash=sha256:071da134651b04a8507dfb331ac0988f376337c2aea59486bf20989fb5b5a64e \ - --hash=sha256:088b04a66b3c1fce6fe4d771ec184a0426262d0b86709c908477b4ac7965df40 \ - --hash=sha256:093167d22a8c95af30f597b8a5686f20a14512989942d4be804d119899caca20 \ - --hash=sha256:0935971bffd0b479fc90c4811ca787703e93fcb6afea939a375dfc80285ab368 \ - --hash=sha256:095f62ea4e7a3be2f6c567ab695ce10e950f2adb905c1bec82281593e0b2d2ad \ - --hash=sha256:0b143d53590e89f43153d81d505a8448d4d57354354385aef8a51d67ffefa27e \ - --hash=sha256:0c1c4debad7337627b86837abdf0237ca3cb3d7e17de7eab0177c263878546d4 \ - --hash=sha256:0eca15d627e942ce186a935061f1568cc46c02e97c419c8da802df2be9f917d8 \ - --hash=sha256:0ef606c15cac6c90279acf34120784b6f36662cbf382defd3955cd8f1115336b \ - --hash=sha256:10456943903744ae1249728161c96bd9d2f7eb5ee17fcc2ffda2dc32e1bb36c7 \ - --hash=sha256:11d71490bf4bbff1141b14b93af419ad68c56b60bea9277fcb3f94dcca4796eb \ - --hash=sha256:122adc7c46ac1e31ecfc7f81b2530533dccafdba70f5d741649f87e336c63384 \ - --hash=sha256:13967dca8b2f33230a1427b52438326bb1c9101a1df22a3309ed3fcbbb3c96f0 \ - --hash=sha256:13e26f59f0eecfc5f67c663ad550ffdaf62c0f657547cde387f6c86af1c9449e \ - --hash=sha256:15db8e6cab5f4cc9241bc56e69fdf3452cf49c10ee3c7977c742e68a275b3786 \ - --hash=sha256:18f0e06360c3e451a3ab800355773c8d125a758238d780c800b0ee5e90ee903c \ - --hash=sha256:1969971900b0871530f9b62280dcc2d75688e74d2a69262bc01faf2b96c78f04 \ - --hash=sha256:1b8986d4313dcee7c932837d16a535f1840b827bac1ea7c5c4c80751d0423794 \ - --hash=sha256:1bdb9b8fba5a9aef673ec90db3f55b1ce743f2fbdea4d37dc04d14ccdfc153ff \ - --hash=sha256:1f57c414be82490bc0e0305fdb834186229b2d9b6a35fa0afd1eb1a772d125ab \ - --hash=sha256:1f66fe6a021173d0d47968491791966b9f3e6d61115f2491744aa0c07a6e67af \ - --hash=sha256:202436df907c15adbb94360296c425ea53cf8968a5d2cff9b5b9790ae1972b33 \ - --hash=sha256:2196ba6df392c3574acadd14ef87550f3611349c8618564de324b806a7a31cee \ - --hash=sha256:22a310ad37672a261e55a8b5e28d0ae08cfb68abb1f46418ccd19835c3b8e836 \ - --hash=sha256:23c9ee89967b6a9b4048acb3b93b660ed714ce9c8bf3bbe652959bc120dc02dc \ - --hash=sha256:2622fe114c0bd66ca5c461859357587f5a5e35ee5ff49fc5643d1bc78dbb41c6 \ - --hash=sha256:26a7aafc992e78872e2c8c1f7248c0e01139cf9020a7781b0c064fa566832712 \ - --hash=sha256:27747162712e85c84598d364425dbf1714ff335bdb6ba3171c4e5081196e8916 \ - --hash=sha256:29631224698de1e42abc8fa7658d830e0aed0029785144b5832b695da5adef2f \ - --hash=sha256:29b6e7bc4442a56cf8e0dc1cabf3fdc77cd533568d6829fc76a1effd2ce332ec \ - --hash=sha256:29be9fd289e9ab8f480996ea2f686e1654b80242033843cb11691688329423f1 \ - --hash=sha256:2ba9933e8f35fe4a70f540b837254c4055da82dc3a9e500a8f95e61498083a15 \ - --hash=sha256:2cc66abb85e2108c9ff8a1c0d20fa260bf690bbb33caef4ff3ecb2c2cbdfff5d \ - --hash=sha256:2cd560498ae8e1bcc955643c1d78eb8e338226d07a983c656ea8c4443d3eec0f \ - --hash=sha256:2f79cc3e8039a8cf5c77e0811b0807953fd52d0863b9b76970b20d696dc64a78 \ - --hash=sha256:2f8a4b0b4d639d525928c7f30de527bfdf9ead6e44a5e8cb9c50aced5e4590cb \ - --hash=sha256:307c1acd812fe897e7fbe10c6758822e8c04be4e7c60a9f54901cdf8b5ab8bc3 \ - --hash=sha256:3126f2a96704505aa4e92a72d6e8a5d7f29d40a987ced8bf69e29d71dfc71fbc \ - --hash=sha256:31e8901637e20ccb3cf8f8848b5d0f7a00462bf5b34f7cf3dcbb2753b18e8b39 \ - --hash=sha256:346ac52e56bcda320c0dcdfdd081947ed7cada33afea4e2284bef7b0733bff9b \ - --hash=sha256:348bb85e2038b40c007383616d73f734869063772372519549ebd7da1723d1a4 \ - --hash=sha256:3533a03e4e789baf6a286e7b0b1b6da3f3d7c3eab569686ee29ee1d8b52e2cb4 \ - --hash=sha256:35977263d9bf506dbc65349f63b3b8c91606d4abc110990945e3b94bc671319c \ - --hash=sha256:397599503b718f0137f26d3f6532d6955069cd2e5917c47ef581495bc2529ff8 \ - --hash=sha256:3bafff8598f0528017ddc74194e5451d5c22d046c98935f8f86247b0f286e4f8 \ - --hash=sha256:3d1f48582686a0a3b81e9b43234766cc96697df72081af3f48107bd3f34d34e5 \ - --hash=sha256:4261863fc8b5ab1b815ede94e592e94c6af5b04616014929057e61859e7382a9 \ - --hash=sha256:43a4b56555bbcf8af161e7c7682bd93eec10f068c95844511864c018c8e5e13b \ - --hash=sha256:45cc39ba50fb0754a4359b90f8229ae08598fe2266abe3521b4e5a9ba916534a \ - --hash=sha256:46029e6e27a3ec0dc55b53f58df82d10f04c5e111f78248279b530bedad2c30a \ - --hash=sha256:48ea524a25a1cd5972cf293bc95713918cba0bcd6fa9b992d906c857c546abe2 \ - --hash=sha256:4ee953a5ebaeed38dc21cc032ed17a9d9782802e00042200497ab4b01b0bf7c0 \ - --hash=sha256:54af1266710cb0f305127ae0b970aff8d208057f8a29cd6e1db99b0114947035 \ - --hash=sha256:560b211fc3bd4a1e1c6de44f6d38113bf5b410dfc89a4c0d2a3c0edbf1a0dfb8 \ - --hash=sha256:563661919f603374c40cf45ffcd25535c12b8954203569a2ab1cee5265871cf4 \ - --hash=sha256:563d6500ca80dac7bba6f48a78e0ffd87e21a7d4d24642c6503a2ddccd70c110 \ - --hash=sha256:59e539c4eb4d3a53b0e630a6ba2b2f2824732b5e73f90e30a280f12fde157b15 \ - --hash=sha256:5bbbb696c8024475b1877d14ce20d5f1cc05b8f6d786cea0fe3aa7fedc02e891 \ - --hash=sha256:5caf684986a2490628f059a99dd107b566a2d34cf947f8eb8387e0500a1f90c5 \ - --hash=sha256:610c7637bc36b90f39e6c66f710f93d57018f83d53e1e187caaa218c6892b95f \ - --hash=sha256:628ff11e6720f90acd0c305dfa3339f04a783a20de8cda6ac333ba46447261e8 \ - --hash=sha256:62b8e291a4f7edbf7cde7a43d831d893ba443a1b627498b53581943b0e348feb \ - --hash=sha256:6300d5176647145ba1e22991c924fb29743e54b4d7b8bc85a0d3ec0e55e189cb \ - --hash=sha256:64eaeda36ee8d88f9e8616a587a8c66a663283cf6e0dcf013c1ddd8c758e4aef \ - --hash=sha256:658f5a1895b804423d97b22d06fc0d0b171c7c01dcc3aa9c8faf0c0e26a249a5 \ - --hash=sha256:65c85c79f5a2c04fbbc18f006c014674dc5fdf270cb978d8862c82c6f694e60c \ - --hash=sha256:68186a2d4051c8ffd17be33553bea2ec9bbc8ef860fe2980a221d96126296f31 \ - --hash=sha256:68d40b2bace413f3231f5729d3fcfb1837fd31c4907e241b5d43211bfd76f3c2 \ - --hash=sha256:69708fecaa88bcb2341397b49fc95057a835b02a3670c551b37f95dd79e64e3a \ - --hash=sha256:69b3e519a132bb943b0daae15fc8c2168706b17f826481d32a32a5e784b129e3 \ - --hash=sha256:6b62b7e0025aa48dec11e125e655d1157985a5fdcec04b1ad500101ad072b891 \ - --hash=sha256:714597cb5d5e15a8a449d2ae23c45b486a9e8fa33c462c7a33d7f35b65d92943 \ - --hash=sha256:758233648ac47b07c575224c4eadd73c8929c3b4c31e2afcfea935fde1cda735 \ - --hash=sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e \ - --hash=sha256:77745725125d01fd613b6db043362aa7c6bfbfdb23d45dbfc3d92bf58160af62 \ - --hash=sha256:7941ef106ca1f2c62314a13c7ed913bcf49641f3efdc12864d588e17870920ac \ - --hash=sha256:7a2573d0fd34f361a4a14e54d8cda3a91ac4e55fbf0d719698024f3b09c5b147 \ - --hash=sha256:7a62e302fc8cd6aa8972207e7e951d1fdee7c1dda18568305041d19f0e2c00f5 \ - --hash=sha256:7bb0dad75068fee80fcb60f88569722c199d8656a16706702dc6e3b786819c90 \ - --hash=sha256:7bc7003991ebd368a20d05228137a37b3d3066751f3ea1e4f7b8efe8e752f2f5 \ - --hash=sha256:7d26dc8f070c0ec5579e987fa615ffd6883086106eefdff9e10d160fc5630630 \ - --hash=sha256:8125e60f3c70e323ac07dd8b3635f7b3bbc5c3a9ac04ae5988f668ff7ae28a18 \ - --hash=sha256:8180b635290a75af8478f1b3e9810135381ae24833293fe77b85c1c21ff842ab \ - --hash=sha256:82780eb8bf59e8fb25dd081fde6e058805045d6374a7f2f877effc826ca4434b \ - --hash=sha256:835d5a90b11d1f5f8200ff3cc8316bded76eebebc92436398947a27657e645e7 \ - --hash=sha256:83ff054b04915be5c15680da6c6012474a2cc2bf534129a0e8c6a99f17ba7238 \ - --hash=sha256:8457aff3c12a89a8e1c4674de5c777857fbc429f40fe117a3d29538547cbc364 \ - --hash=sha256:847d6082ae694dc95e548acb201bc100e1cfa96513bc71fdcb86f709dad6c435 \ - --hash=sha256:883284137e25318ed9735b742ae46341a864888fae28e8b6314c4f84da080f08 \ - --hash=sha256:887f9a975996032c686719eb7b3e1e7942fab5079c2b778bbd9afe9a9d78244f \ - --hash=sha256:8890c89d662560e51c55ac1304d6f919b23942abe9ae1127cb1de9aa6132fa52 \ - --hash=sha256:88a6df88567680504ae28bfa7a1f2f64243d91e79a40b2c92ef42efc531e23da \ - --hash=sha256:8d1046b5427dcafe6e8a0e07527dd74f1ee694006160162f53f3a17f15aad3b4 \ - --hash=sha256:8daafaa0b2eb43f76898ced78b1e0fb91b38c4fa50da516c18067f2a2d578c20 \ - --hash=sha256:8dc2d9c3a924ed14166e63650b2cf9f59e7821743bdd50b23802bd97ca09bde5 \ - --hash=sha256:90c10b22860dbd09982d0b8993b66231a861bea2993d4a817ff35273f6ea285a \ - --hash=sha256:91fa75d0a693832106d98f66c849f034f21c828d14437f1fb97d3784aab89e84 \ - --hash=sha256:930c6058047410e3edff445f5a6e4457f2e089042dede00e2d18ce06f3ceae2e \ - --hash=sha256:9442b14eec262a1f74369bbd07e75bc5155105164649a4b9fbc1ebc7b8fb0b14 \ - --hash=sha256:95c27b4f3f04320fc44e338573f40c5c956b504a7fcf081a157fd0b02579311c \ - --hash=sha256:9606f583e7acaf61e7b3f56074e14037b9af7cb194590edfc0114b3ae5931ff7 \ - --hash=sha256:962f18c59a000f30b084ea2e6b8001521bb315efd4e5f10acf9fb36f366b7882 \ - --hash=sha256:9caef53b20a105c0d66518a34be2f71b2783de8d091767575ef86f6ea422236d \ - --hash=sha256:9e37024b41d7a7e7e9cce14b248d54707c21c2a2ea30a47b71bdcefcafec00f2 \ - --hash=sha256:a5a7ee1217949ddd43c6b7bcf70d5c22193bb50e8c695386de5905325e93ce9f \ - --hash=sha256:a5e1583c14775580da05641240ce0d93f36ce3ddef3d5083a827468b0bcfe874 \ - --hash=sha256:a9e246f67ac038568b854ed7c5578e4c6af1f742359901a8fcc3603ff1358df6 \ - --hash=sha256:ab83fdd8cf307353edba9c427c17a3a021c2522d690f5633dd9f72d28b48ccca \ - --hash=sha256:ac746cb365bac1c462da9e3e6ab8904a8efe2217a56b0b2e3d9480f41d2b2602 \ - --hash=sha256:ad474c11d851b6fc97cb625e4822bc0cbd567fc07dc2602e28faec5a36b42bbb \ - --hash=sha256:b03ca066b47b18b205cc080dca6f76cbd159f8cdd33a02a0700164c13b37e463 \ - --hash=sha256:b1cd4d66ce894a45482e1ac2837c31d0bd447df35065e542b60055aa2d00404b \ - --hash=sha256:b25426f9f6ed402835617c8f23609a47045f91ecff365eb6734817e039a8ed25 \ - --hash=sha256:b367c342327717d644db4c0ddb37ceb655c84822215ea0773a3a36911b74b71d \ - --hash=sha256:b7e62b8fc7bd6cad007b9f2e0ad9c8d4854c06350d5f51e1a439dd18b510ecac \ - --hash=sha256:b8b7aa75146266fd3e2a2437cf69ae188688c04ab8665b163d4257b46c1e0c83 \ - --hash=sha256:bb36381e1f9f9d06eba2f10bdd438e5d20c07d5b55e1a3eee30b9f44cbf52316 \ - --hash=sha256:bb8c7da8c861391f7ae48e3593762be2dabe405109e01aec520fbe1a6d15d14b \ - --hash=sha256:bb9a60b7faa5d37c426fa91cf4d6738182a1f2755b9fab7c9c64cd466c4ce51e \ - --hash=sha256:be007d1aee2cbd530347dcafedb400891a3b5f1bd7135f95cf5d5b330b5219ee \ - --hash=sha256:be569fff1d85cd29391c431c5641c8772acb75bbdc61e60a8e82fceb9023d385 \ - --hash=sha256:bea7df027015856ba5d0a88e3b4777ff8cb5c66b58fc108050fe79d4dd9d4d2d \ - --hash=sha256:c0fe437a6d2f36aac2b49517057776575b5bf359df314cca20d230a6e139c089 \ - --hash=sha256:c2b2a96cf1dd99fe7867be4c013314225f4d5786e6685906e29932d42aca6f11 \ - --hash=sha256:c2c5fd0fd39574ccd58e1a52565b341aff522c5c836f1b3eb7605c371e61f52c \ - --hash=sha256:c46a08bf070d6849fed483e9d9833f9d06aecb8382ed985be0b38508b3ae958e \ - --hash=sha256:c5f3a2af441670d80ce5fdf13b6c1b421fc1fc7fc5182d58ac7486738bb2b742 \ - --hash=sha256:c60e50bc5b07faac92fd3a20fa21cc8cf3e3f7204d2867b206c73293ebc19101 \ - --hash=sha256:c68e0c0649d17c2d0339e3674e86a4aeba4a7e6b21c1e394cf947a95433b31d0 \ - --hash=sha256:c9c98d2f0126ba84cb45601eed97ff67ff767e19ae6eb3c31b02827b54d700e5 \ - --hash=sha256:ca52b9ec80851366197577154c862c4c4c7036ca76ae94cef5cb59c5cfeab944 \ - --hash=sha256:cbd86f9787c5e2f5fd27d8b21458222f107347c6731c4e93dde68f554b466a2d \ - --hash=sha256:d0264f8d5cb0a803f650a6a8572dfa0cd1e099a2234c588dc8fb220b415b865f \ - --hash=sha256:d0be2b832435001bc623ca7f1499ca1a853d4f082fb61221a80ce71132f50b26 \ - --hash=sha256:d244cf6b52b5ba1c34c3832f4652a668ebb36d95949b96eed9a1c54d916a90dd \ - --hash=sha256:d2d236b8a44ae91536a12ebcb996bdb31cf27425f36b4d05c87f2ba2716050ba \ - --hash=sha256:d3da668e903c934ed0b587ecacfed6901f6ae6384a6e975887592b61845e78bc \ - --hash=sha256:d6dc7804c50fabd28644d4d18a4b20aad3681b3e64f3acd3182b330ca73f7a32 \ - --hash=sha256:d7e5ba0a0153e35fbce9c51df530c8b4cb0c3012b46a04ff9a048441a269c2ed \ - --hash=sha256:d8a5ac357ac283490a8d1899b0383355fd1f8634b14ba0d59e4c0dd97db85556 \ - --hash=sha256:da1c112c5784ccd9d32cd90be6739fee32644e874eff6ae8f0497cba3e352e58 \ - --hash=sha256:dc911ae6152e455b16a2a1a626aa6cd612fa01efb9d0a4ab3f5cf328b911483d \ - --hash=sha256:e0db3a4d1e264e225037a6023888972c25206a96e016021a5bea41c9a939f2a9 \ - --hash=sha256:e192018b732f7b168e6604cbdf40fa8e05c996693b9eb445a0d8a73f4b77c5d3 \ - --hash=sha256:e37b744849fb631bb52e3dadde35ffeee365a6c41cf71257b5b7acc9cd83fd38 \ - --hash=sha256:e41226ecf607f062fe34a2f4cf64ad3a89e3a0180dc800b463b6b14c06dd10dc \ - --hash=sha256:e418ec99574ca24365ca96546af285c2b021a1a072478a79f0e3cc3b08837154 \ - --hash=sha256:e6ec7d37841609a691b96a10b4fde386c7cd93ebbb939f59c9f23325ee788395 \ - --hash=sha256:e886ef8c9879105fe4fc99417447b3a5f35d1131412ce839470bd2089fe2043f \ - --hash=sha256:e8e1e895e23818d343e4ae7dd95a0a556fdeaf8b471acf1c0a39b93c6f54d478 \ - --hash=sha256:e9dc7b4ff6ef184504b49ef9a4113d49a646653b2ce89f5f48c1f57cdf6ba081 \ - --hash=sha256:ea880d441be7c510106bc56064be39266d948aef94ad4955e8784690019a5d9f \ - --hash=sha256:eabb03dc3e4ed6333ecd1cc9826ec80e7a98b5506deeb832d7260c8e44166d23 \ - --hash=sha256:ec0a4d066356054d569a66e0a94691a2058b680be5e710298f61db11a3c4609f \ - --hash=sha256:edda19aff836ec515caafc09ea53d2ab144a041f09ee9a7cefcbd3ae4e976256 \ - --hash=sha256:f1f4a220db6ed7c8fd16b6d644ffd1f082651693204daf3275e049fadc849e39 \ - --hash=sha256:f25b61a708bd276e8cbb6afcbbf1b8e793a3be70ba0a842d0b8692020f83b706 \ - --hash=sha256:f2fa3d3b1c933d4bcb8fd2018700d5e7235c52f2ab8c88d22286965c5c0f00f8 \ - --hash=sha256:f3071e6515cc63714d014da8f738ae9fa3997c476203f3cd46de380c2376ed7b \ - --hash=sha256:f3a0a31189acf6703307397c6139ddabd734c20c5ef92649fc93e473df6615a3 \ - --hash=sha256:f7eefd0233a7c33ca980a5cfef26f1e9b5e2137839e752a99963696729f12d91 \ - --hash=sha256:f8b09b25e0f4dc2ea9e2adbb1cc3ba11a94d6fa3dd978ae659c8743052e1afbc \ - --hash=sha256:f8d7b66c9e09c0bb0add2b5895e646b62a0849e71155066f215523de6b95cbe6 \ - --hash=sha256:fa6c2880709c84457de104385b704fc28860f27e442ad13966fc4af8e714fe9c \ - --hash=sha256:fc5460940f50dff00731b4132366840ba9685286ea88ea104b661899084f3fea \ - --hash=sha256:fd789a294d8e098528be29b2669b83005ce569339f8cef167fc0274c3115c34c +multidict==6.9.1 \ + --hash=sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c \ + --hash=sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9 \ + --hash=sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa \ + --hash=sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188 \ + --hash=sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201 \ + --hash=sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382 \ + --hash=sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f \ + --hash=sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919 \ + --hash=sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418 \ + --hash=sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771 \ + --hash=sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c \ + --hash=sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76 \ + --hash=sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345 \ + --hash=sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580 \ + --hash=sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9 \ + --hash=sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1 \ + --hash=sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1 \ + --hash=sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64 \ + --hash=sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5 \ + --hash=sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006 \ + --hash=sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8 \ + --hash=sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02 \ + --hash=sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132 \ + --hash=sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c \ + --hash=sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19 \ + --hash=sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b \ + --hash=sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba \ + --hash=sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b \ + --hash=sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08 \ + --hash=sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe \ + --hash=sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f \ + --hash=sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd \ + --hash=sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879 \ + --hash=sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f \ + --hash=sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9 \ + --hash=sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987 \ + --hash=sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd \ + --hash=sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea \ + --hash=sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc \ + --hash=sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c \ + --hash=sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b \ + --hash=sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709 \ + --hash=sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4 \ + --hash=sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce \ + --hash=sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94 \ + --hash=sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a \ + --hash=sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e \ + --hash=sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b \ + --hash=sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96 \ + --hash=sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1 \ + --hash=sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e \ + --hash=sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455 \ + --hash=sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3 \ + --hash=sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471 \ + --hash=sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d \ + --hash=sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b \ + --hash=sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d \ + --hash=sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f \ + --hash=sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004 \ + --hash=sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4 \ + --hash=sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e \ + --hash=sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5 \ + --hash=sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60 \ + --hash=sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14 \ + --hash=sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f \ + --hash=sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9 \ + --hash=sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3 \ + --hash=sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194 \ + --hash=sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a \ + --hash=sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3 \ + --hash=sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42 \ + --hash=sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c \ + --hash=sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828 \ + --hash=sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290 \ + --hash=sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485 \ + --hash=sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e \ + --hash=sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8 \ + --hash=sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c \ + --hash=sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b \ + --hash=sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659 \ + --hash=sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b \ + --hash=sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9 \ + --hash=sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953 \ + --hash=sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c \ + --hash=sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1 \ + --hash=sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299 \ + --hash=sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94 \ + --hash=sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983 \ + --hash=sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d \ + --hash=sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac \ + --hash=sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a \ + --hash=sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7 \ + --hash=sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e \ + --hash=sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45 \ + --hash=sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410 \ + --hash=sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40 \ + --hash=sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4 \ + --hash=sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805 \ + --hash=sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d \ + --hash=sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa \ + --hash=sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d \ + --hash=sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701 \ + --hash=sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80 \ + --hash=sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6 \ + --hash=sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40 \ + --hash=sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20 \ + --hash=sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694 \ + --hash=sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a \ + --hash=sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203 \ + --hash=sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519 \ + --hash=sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125 \ + --hash=sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0 \ + --hash=sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d \ + --hash=sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a \ + --hash=sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea \ + --hash=sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2 \ + --hash=sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c \ + --hash=sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3 \ + --hash=sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d \ + --hash=sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3 \ + --hash=sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a \ + --hash=sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68 \ + --hash=sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8 \ + --hash=sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca \ + --hash=sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8 \ + --hash=sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840 \ + --hash=sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82 \ + --hash=sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610 \ + --hash=sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca \ + --hash=sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a \ + --hash=sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1 \ + --hash=sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5 \ + --hash=sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a \ + --hash=sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13 \ + --hash=sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07 \ + --hash=sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05 \ + --hash=sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1 \ + --hash=sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4 \ + --hash=sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2 \ + --hash=sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82 \ + --hash=sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6 \ + --hash=sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d \ + --hash=sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88 \ + --hash=sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f \ + --hash=sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126 \ + --hash=sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24 \ + --hash=sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab \ + --hash=sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67 \ + --hash=sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3 \ + --hash=sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1 \ + --hash=sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800 \ + --hash=sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3 \ + --hash=sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8 \ + --hash=sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5 \ + --hash=sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24 \ + --hash=sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c \ + --hash=sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a \ + --hash=sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841 \ + --hash=sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387 \ + --hash=sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec \ + --hash=sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721 \ + --hash=sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d \ + --hash=sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390 \ + --hash=sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1 \ + --hash=sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d \ + --hash=sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e \ + --hash=sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df \ + --hash=sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb \ + --hash=sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5 \ + --hash=sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9 oauthlib==4.0.0 \ --hash=sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd packaging==26.3 \ @@ -1324,8 +1366,8 @@ urllib3==2.8.0 \ --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 websocket-client==1.9.2 \ --hash=sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce -werkzeug==3.1.8 \ - --hash=sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50 +werkzeug==3.1.9 \ + --hash=sha256:6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab yarl==1.25.1 \ --hash=sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3 \ --hash=sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de \ diff --git a/scanner/governance.py b/scanner/governance.py new file mode 100644 index 00000000..d11fef65 --- /dev/null +++ b/scanner/governance.py @@ -0,0 +1,245 @@ +"""Read-only Azure governance evidence collection and policy loading.""" + +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Mapping + +import urllib.parse + +import requests +from azure.core.exceptions import AzureError + +logger = logging.getLogger(__name__) + +ARM_ENDPOINT = "https://management.azure.com" +_ARM_HOST = "management.azure.com" + + +def _is_valid_arm_continuation(url: str) -> bool: + """Return True only if url is a safe ARM pagination URL. + + Accepts exactly scheme=https, host=management.azure.com, no userinfo, + no non-standard port. Rejects lookalike hosts and redirect targets. + """ + try: + parsed = urllib.parse.urlparse(url) + except Exception: + return False + return parsed.scheme == "https" and parsed.netloc == _ARM_HOST and not parsed.username and not parsed.password + + +@dataclass(frozen=True) +class GovernancePolicy: + approved_management_group_ids: frozenset[str] + required_policy_initiatives: tuple[Mapping[str, str], ...] + preventive_policy_definition_ids: frozenset[str] + allowed_preventive_effects: frozenset[str] + production_resource_types: frozenset[str] + production_tag: str + production_tag_values: frozenset[str] + maximum_subscription_owners: int + privileged_role_definition_ids: frozenset[str] + approved_privileged_scopes: frozenset[str] + approved_provider_namespaces: frozenset[str] + ownership_tags: frozenset[str] + drift_sla_days: int + excluded_resource_ids: frozenset[str] + + +_FIELDS = { + "approved_management_group_ids", + "required_policy_initiatives", + "preventive_policy_definition_ids", + "allowed_preventive_effects", + "production_resource_types", + "production_tag", + "production_tag_values", + "maximum_subscription_owners", + "privileged_role_definition_ids", + "approved_privileged_scopes", + "approved_provider_namespaces", + "ownership_tags", + "drift_sla_days", + "excluded_resource_ids", +} + + +def _strings(raw: Mapping[str, Any], field: str) -> frozenset[str]: + values = raw[field] + if not isinstance(values, list) or any(not isinstance(item, str) or not item.strip() for item in values): + raise ValueError(f"{field} must be a list of non-empty strings") + return frozenset(item.strip().lower().rstrip("/") for item in values) + + +def load_governance_policy(path: str | Path) -> GovernancePolicy: + """Load strict organisation-owned governance expectations.""" + with Path(path).open(encoding="utf-8") as handle: + raw = json.load(handle) + if not isinstance(raw, dict) or set(raw) != _FIELDS: + raise ValueError("governance policy has missing or unsupported fields") + initiatives = raw["required_policy_initiatives"] + if not isinstance(initiatives, list): + raise ValueError("required_policy_initiatives must be a list") + normalised_initiatives = [] + for item in initiatives: + if not isinstance(item, dict) or set(item) != {"definition_id", "scope"}: + raise ValueError("each required policy initiative needs definition_id and scope") + if any(not isinstance(item[key], str) or not item[key].strip() for key in item): + raise ValueError("initiative definition_id and scope must be non-empty strings") + normalised_initiatives.append({key: value.strip().lower() for key, value in item.items()}) + for field in ("maximum_subscription_owners", "drift_sla_days"): + if not isinstance(raw[field], int) or isinstance(raw[field], bool) or raw[field] <= 0: + raise ValueError(f"{field} must be a positive integer") + if not isinstance(raw["production_tag"], str) or not raw["production_tag"].strip(): + raise ValueError("production_tag must be a non-empty string") + return GovernancePolicy( + approved_management_group_ids=_strings(raw, "approved_management_group_ids"), + required_policy_initiatives=tuple(normalised_initiatives), + preventive_policy_definition_ids=_strings(raw, "preventive_policy_definition_ids"), + allowed_preventive_effects=_strings(raw, "allowed_preventive_effects"), + production_resource_types=_strings(raw, "production_resource_types"), + production_tag=raw["production_tag"].strip().lower(), + production_tag_values=_strings(raw, "production_tag_values"), + maximum_subscription_owners=raw["maximum_subscription_owners"], + privileged_role_definition_ids=_strings(raw, "privileged_role_definition_ids"), + approved_privileged_scopes=_strings(raw, "approved_privileged_scopes"), + approved_provider_namespaces=_strings(raw, "approved_provider_namespaces"), + ownership_tags=_strings(raw, "ownership_tags"), + drift_sla_days=raw["drift_sla_days"], + excluded_resource_ids=_strings(raw, "excluded_resource_ids"), + ) + + +class GovernanceCollector: + """Collect governance control-plane metadata without changing Azure state.""" + + def __init__(self, credential: Any, subscription_id: str, *, session: Any = requests) -> None: + self.credential = credential + self.subscription_id = subscription_id + self.session = session + self.scope = f"/subscriptions/{subscription_id}" + + def _headers(self) -> dict[str, str]: + token = self.credential.get_token("https://management.azure.com/.default") + return {"Authorization": f"Bearer {token.token}", "Content-Type": "application/json"} + + def _get_all(self, path: str) -> list[dict[str, Any]] | None: + url = path if path.startswith("https://") else f"{ARM_ENDPOINT}{path}" + items: list[dict[str, Any]] = [] + try: + while url: + response = self.session.get(url, headers=self._headers(), timeout=30, allow_redirects=False) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + return None + values = payload.get("value", []) + if not isinstance(values, list): + return None + items.extend(item for item in values if isinstance(item, dict)) + next_link = payload.get("nextLink") + if next_link is None: + break + if not isinstance(next_link, str) or not _is_valid_arm_continuation(next_link): + logger.warning( + "ARM pagination aborted: nextLink failed origin validation (%r); " + "partial evidence is NOT treated as complete.", + next_link, + ) + return None + url = next_link + return items + except (requests.RequestException, AzureError, ValueError, TypeError): + logger.warning("_get_all collection failed", exc_info=True) + return None + + def _post_graph(self, query: str) -> list[dict[str, Any]] | None: + items: list[dict[str, Any]] = [] + options: dict[str, Any] = {"resultFormat": "objectArray"} + try: + while True: + response = self.session.post( + f"{ARM_ENDPOINT}/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01", + headers=self._headers(), + json={"subscriptions": [self.subscription_id], "query": query, "options": options}, + timeout=30, + allow_redirects=False, + ) + response.raise_for_status() + payload = response.json() + data = payload.get("data") + if not isinstance(data, list) or not all(isinstance(item, dict) for item in data): + return None + items.extend(data) + skip_token = payload.get("$skipToken") + if not isinstance(skip_token, str) or not skip_token: + return items + options = {"resultFormat": "objectArray", "$skipToken": skip_token} + except (requests.RequestException, AzureError, ValueError, TypeError, AttributeError): + logger.warning("_post_graph collection failed", exc_info=True) + return None + + def _post_values(self, path: str) -> list[dict[str, Any]] | None: + url = f"{ARM_ENDPOINT}{path}" + items: list[dict[str, Any]] = [] + try: + while url: + response = self.session.post(url, headers=self._headers(), json={}, timeout=30, allow_redirects=False) + response.raise_for_status() + payload = response.json() + values = payload.get("value") + if not isinstance(values, list) or not all(isinstance(item, dict) for item in values): + return None + items.extend(values) + next_link = payload.get("@odata.nextLink", payload.get("nextLink")) + if next_link is None: + break + if not isinstance(next_link, str) or not _is_valid_arm_continuation(next_link): + logger.warning( + "ARM pagination aborted: odata/nextLink failed origin validation (%r); " + "partial evidence is NOT treated as complete.", + next_link, + ) + return None + url = next_link + return items + except (requests.RequestException, AzureError, ValueError, TypeError, AttributeError): + logger.warning("_post_values collection failed", exc_info=True) + return None + + def collect(self) -> dict[str, Any]: + """Return independently nullable evidence sets so partial access stays UNKNOWN.""" + policy_api = "2023-04-01" + authorization_api = "2022-04-01" + resources = self._post_graph("Resources | project id, name, type, resourceGroup, tags, subscriptionId") + hierarchy_rows = self._post_graph( + "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' " + "| project properties.managementGroupAncestorsChain" + ) + return { + "scope": self.scope, + "hierarchy": hierarchy_rows, + "policy_assignments": self._get_all( + f"{self.scope}/providers/Microsoft.Authorization/policyAssignments?api-version={policy_api}&%24filter=atScope()" + ), + "policy_exemptions": self._get_all( + f"{self.scope}/providers/Microsoft.Authorization/policyExemptions" + "?api-version=2022-07-01-preview&%24filter=atScope()" + ), + "role_assignments": self._get_all( + f"{self.scope}/providers/Microsoft.Authorization/roleAssignments?api-version={authorization_api}&%24filter=atScope()" + ), + "policy_definitions": self._get_all( + "/providers/Microsoft.Authorization/policyDefinitions?api-version=2023-04-01" + ), + "locks": self._get_all(f"{self.scope}/providers/Microsoft.Authorization/locks?api-version=2016-09-01"), + "providers": self._get_all(f"{self.scope}/providers?api-version=2021-04-01"), + "resources": resources, + "policy_states": self._post_values( + f"{self.scope}/providers/Microsoft.PolicyInsights/policyStates/latest/queryResults?api-version=2019-10-01" + ), + } diff --git a/scanner/rules/_governance_common.py b/scanner/rules/_governance_common.py new file mode 100644 index 00000000..9c06be88 --- /dev/null +++ b/scanner/rules/_governance_common.py @@ -0,0 +1,417 @@ +"""Shared evaluation helpers for AZ-GOV-001 through AZ-GOV-010.""" + +from __future__ import annotations + +import logging +import os +import re +from datetime import datetime, timezone +from pathlib import PurePosixPath +from typing import Any, Mapping + +from scanner.governance import GovernanceCollector, GovernancePolicy, load_governance_policy + +logger = logging.getLogger(__name__) +POLICY_ENV = "OPENSHIELD_GOVERNANCE_POLICY" +OWNER_ROLE_ID = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" + + +def value(item: Any, field: str, default: Any = None) -> Any: + if isinstance(item, Mapping): + return item.get(field, default) + return getattr(item, field, default) + + +def normal(value_: Any) -> str: + return str(value_ or "").strip().lower().rstrip("/") + + +def properties(item: Any) -> Mapping[str, Any]: + candidate = value(item, "properties", {}) + return candidate if isinstance(candidate, Mapping) else {} + + +def load_context(azure_client: Any, subscription_id: str, rule_id: str) -> tuple[GovernancePolicy, dict] | None: + path = os.environ.get(POLICY_ENV) + if not path: + logger.warning("%s: %s is unset; result is UNKNOWN", rule_id, POLICY_ENV) + return None + try: + policy = load_governance_policy(path) + except (OSError, ValueError) as exc: + logger.warning("%s: governance policy is invalid or unavailable: %s", rule_id, exc) + return None + cache = getattr(azure_client, "_governance_snapshot_cache", None) + cache_sub = getattr(azure_client, "_governance_snapshot_cache_sub", None) + if cache is None or cache_sub != subscription_id: + cache = GovernanceCollector(azure_client.credential, subscription_id).collect() + setattr(azure_client, "_governance_snapshot_cache", cache) + setattr(azure_client, "_governance_snapshot_cache_sub", subscription_id) + return policy, cache + + +def _resource_name(resource_id: str) -> str: + return PurePosixPath(resource_id).name or "subscription" + + +def finding( + spec: Mapping[str, Any], subscription_id: str, resource_id: str, resource_type: str, evidence: dict +) -> dict: + return { + "rule_id": spec["id"], + "rule_name": spec["name"], + "severity": spec["severity"], + "category": "Governance", + "resource_id": resource_id, + "resource_name": _resource_name(resource_id), + "resource_type": resource_type, + "description": spec["description"], + "remediation": spec["remediation"], + "playbook": spec["playbook"], + "frameworks": spec["frameworks"], + "metadata": { + "scope": resource_id or f"/subscriptions/{subscription_id}", + "effective_scope": evidence.get("assignment_scope", resource_id), + "inherited_assignment": evidence.get("inherited_assignment", False), + "exception_state": evidence.get("exception_state", "not_exempt"), + "unknown_reason": None, + "evidence": evidence, + "confidence": "HIGH", + "permissions_required": spec["permissions"], + "evidence_collected_at": datetime.now(timezone.utc).isoformat(), + }, + } + + +def _unknown(rule_id: str, source: str) -> list[dict]: + logger.warning("%s: %s evidence is unavailable; result is UNKNOWN", rule_id, source) + return [] + + +def _production(resource: Mapping[str, Any], policy: GovernancePolicy) -> bool: + if normal(value(resource, "type")) not in policy.production_resource_types: + return False + tags = value(resource, "tags", {}) + if not isinstance(tags, Mapping): + return False + lowered = {normal(key): normal(item) for key, item in tags.items()} + return lowered.get(policy.production_tag) in policy.production_tag_values + + +def _assignment_scope(assignment: Mapping[str, Any]) -> str: + resource_id = normal(value(assignment, "id")) + marker = "/providers/microsoft.authorization/" + return resource_id.split(marker, 1)[0] if marker in resource_id else resource_id + + +def _effective_policy_effect(definition: Mapping[str, Any], assignment: Mapping[str, Any]) -> str: + policy_rule = definition.get("policyRule", {}) + then = policy_rule.get("then", {}) if isinstance(policy_rule, Mapping) else {} + raw_effect = value(then, "effect") + if not isinstance(raw_effect, str): + return "" + parameter_match = re.fullmatch(r"\[parameters\(['\"]([^'\"]+)['\"]\)\]", raw_effect.strip(), re.IGNORECASE) + if not parameter_match: + return normal(raw_effect) + parameter_name = normal(parameter_match.group(1)) + assigned = assignment.get("parameters", {}) + if isinstance(assigned, Mapping): + for key, parameter in assigned.items(): + if normal(key) == parameter_name: + return normal(value(parameter, "value", parameter)) + declared = definition.get("parameters", {}) + if isinstance(declared, Mapping): + for key, parameter in declared.items(): + if normal(key) == parameter_name: + return normal(value(parameter, "defaultValue")) + return "" + + +def evaluate(spec: Mapping[str, Any], azure_client: Any, subscription_id: str) -> list[dict]: + context = load_context(azure_client, subscription_id, spec["id"]) + if context is None: + return [] + policy, snapshot = context + rule_id = spec["id"] + subscription_scope = f"/subscriptions/{subscription_id}" + + if rule_id == "AZ-GOV-001": + rows = snapshot.get("hierarchy") + if rows is None: + return _unknown(rule_id, "management-group hierarchy") + if not rows: + return _unknown(rule_id, "subscription hierarchy row") + actual: set[str] = set() + for row in rows: + chain = value( + row, "managementGroupAncestorsChain", value(row, "properties_managementGroupAncestorsChain", []) + ) + if not chain: + chain = properties(row).get("managementGroupAncestorsChain", []) + for ancestor in chain if isinstance(chain, list) else []: + actual.add(normal(value(ancestor, "id", value(ancestor, "name")))) + if actual & policy.approved_management_group_ids: + return [] + return [ + finding( + spec, + subscription_id, + subscription_scope, + "Microsoft.Resources/subscriptions", + {"observed_management_groups": sorted(actual)}, + ) + ] + + if rule_id in {"AZ-GOV-002", "AZ-GOV-003"}: + assignments = snapshot.get("policy_assignments") + if assignments is None: + return _unknown(rule_id, "policy assignment") + if rule_id == "AZ-GOV-002": + found = { + (normal(properties(item).get("policyDefinitionId")), _assignment_scope(item)) for item in assignments + } + missing = [ + dict(item) + for item in policy.required_policy_initiatives + if (item["definition_id"], item["scope"]) not in found + ] + return ( + [] + if not missing + else [ + finding( + spec, + subscription_id, + subscription_scope, + "Microsoft.Authorization/policyAssignments", + {"missing_assignments": missing}, + ) + ] + ) + definitions = snapshot.get("policy_definitions") + if definitions is None: + return _unknown(rule_id, "policy definition") + definitions_by_id = {} + for definition in definitions: + props = properties(definition) + definitions_by_id[normal(value(definition, "id"))] = props + violations = [] + for item in assignments: + props = properties(item) + definition_id = normal(props.get("policyDefinitionId")) + if definition_id not in policy.preventive_policy_definition_ids: + continue + definition = definitions_by_id.get(definition_id) + if definition is None: + logger.warning("%s: policy definition is unavailable for %s; result is UNKNOWN", rule_id, definition_id) + continue + effect = _effective_policy_effect(definition, props) + enforcement_mode = normal(props.get("enforcementMode", "Default")) + if not effect: + logger.warning( + "%s: effective policy effect is unavailable for %s; result is UNKNOWN", rule_id, definition_id + ) + continue + if effect not in policy.allowed_preventive_effects or enforcement_mode == "donotenforce": + assignment_scope = _assignment_scope(item) + violations.append( + { + "assignment_id": value(item, "id"), + "assignment_scope": assignment_scope, + "inherited_assignment": assignment_scope != normal(subscription_scope), + "effect": effect, + "enforcement_mode": enforcement_mode, + } + ) + return [ + finding(spec, subscription_id, item["assignment_id"], "Microsoft.Authorization/policyAssignments", item) + for item in violations + ] + + if rule_id == "AZ-GOV-004": + exemptions = snapshot.get("policy_exemptions") + if exemptions is None: + return _unknown(rule_id, "policy exemption") + if not exemptions: + logger.info("%s: no policy exemptions exist; result is NOT_APPLICABLE", rule_id) + return [] + results = [] + now = datetime.now(timezone.utc) + for item in exemptions: + props = properties(item) + expires = props.get("expiresOn") + expired = False + if isinstance(expires, str): + try: + parsed_expiration = datetime.fromisoformat(expires.replace("Z", "+00:00")) + expired = parsed_expiration.tzinfo is None or parsed_expiration <= now + except (TypeError, ValueError): + expired = True + metadata = props.get("metadata", {}) if isinstance(props.get("metadata", {}), Mapping) else {} + missing = [] + if not normal(metadata.get("owner")): + missing.append("owner") + if not normal(metadata.get("justification", props.get("description"))): + missing.append("justification") + if not expires or expired: + missing.append("valid expiration") + if missing: + resource_id = str(value(item, "id", subscription_scope)) + results.append( + finding( + spec, + subscription_id, + resource_id, + "Microsoft.Authorization/policyExemptions", + {"missing_or_invalid": missing}, + ) + ) + return results + + if rule_id in {"AZ-GOV-005", "AZ-GOV-009"}: + resources = snapshot.get("resources") + if resources is None: + return _unknown(rule_id, "resource inventory") + production = [ + item + for item in resources + if _production(item, policy) and normal(value(item, "id")) not in policy.excluded_resource_ids + ] + if not production: + logger.info("%s: no in-scope production resources exist; result is NOT_APPLICABLE", rule_id) + return [] + if rule_id == "AZ-GOV-009": + results = [] + for item in production: + tags = value(item, "tags", {}) + tag_keys = {normal(key) for key in tags} if isinstance(tags, Mapping) else set() + if not tag_keys & policy.ownership_tags: + resource_id = str(value(item, "id")) + results.append( + finding( + spec, + subscription_id, + resource_id, + str(value(item, "type")), + {"required_ownership_tags": sorted(policy.ownership_tags)}, + ) + ) + return results + locks = snapshot.get("locks") + if locks is None: + return _unknown(rule_id, "resource lock") + protected_scopes = set() + for lock in locks: + if normal(properties(lock).get("level")) in {"cannotdelete", "readonly"}: + lock_id = normal(value(lock, "id")) + protected_scopes.add(lock_id.split("/providers/microsoft.authorization/locks/", 1)[0]) + results = [] + for item in production: + resource_id = str(value(item, "id")) + normal_id = normal(resource_id) + if not any(normal_id == scope or normal_id.startswith(f"{scope}/") for scope in protected_scopes): + results.append( + finding( + spec, subscription_id, resource_id, str(value(item, "type")), {"approved_lock_found": False} + ) + ) + return results + + if rule_id in {"AZ-GOV-006", "AZ-GOV-007"}: + assignments = snapshot.get("role_assignments") + if assignments is None: + return _unknown(rule_id, "role assignment") + if rule_id == "AZ-GOV-006": + # The collector uses atScope() which already returns both direct and + # inherited (management-group) assignments effective at this subscription. + # Count all of them; filtering to subscription scope would miss the most + # common enterprise pattern of an Owner granted at a parent MG. + owners = [ + item + for item in assignments + if normal(properties(item).get("roleDefinitionId", "")).rsplit("/", 1)[-1] == OWNER_ROLE_ID + ] + if len(owners) <= policy.maximum_subscription_owners: + return [] + return [ + finding( + spec, + subscription_id, + subscription_scope, + "Microsoft.Authorization/roleAssignments", + {"owner_count": len(owners), "maximum": policy.maximum_subscription_owners}, + ) + ] + results = [] + for item in assignments: + role_id = normal(properties(item).get("roleDefinitionId")) + scope = _assignment_scope(item) + if ( + any(role_id.rsplit("/", 1)[-1] == identifier for identifier in policy.privileged_role_definition_ids) + and scope not in policy.approved_privileged_scopes + ): + resource_id = str(value(item, "id", subscription_scope)) + results.append( + finding( + spec, + subscription_id, + resource_id, + "Microsoft.Authorization/roleAssignments", + { + "assignment_scope": scope, + "inherited_assignment": scope != normal(subscription_scope), + "role_definition_id": role_id, + }, + ) + ) + return results + + if rule_id == "AZ-GOV-008": + providers = snapshot.get("providers") + if providers is None: + return _unknown(rule_id, "resource provider") + return [ + finding( + spec, + subscription_id, + f"{subscription_scope}/providers/{value(item, 'namespace')}", + "Microsoft.Resources/providers", + {"registration_state": value(item, "registrationState")}, + ) + for item in providers + if normal(value(item, "registrationState")) == "registered" + and normal(value(item, "namespace")) not in policy.approved_provider_namespaces + ] + + states = snapshot.get("policy_states") + if states is None: + return _unknown(rule_id, "policy state") + results = [] + now = datetime.now(timezone.utc) + for item in states: + props = properties(item) or item + if normal(props.get("complianceState")) != "noncompliant": + continue + timestamp = props.get("timestamp") + try: + observed = datetime.fromisoformat(str(timestamp).replace("Z", "+00:00")) + except (TypeError, ValueError): + logger.warning("%s: malformed policy-state timestamp; result is UNKNOWN for one resource", rule_id) + continue + age_days = (now.date() - observed.date()).days + if age_days > policy.drift_sla_days: + resource_id = str(props.get("resourceId") or subscription_scope) + results.append( + finding( + spec, + subscription_id, + resource_id, + str(props.get("resourceType") or "Microsoft.PolicyInsights/policyStates"), + { + "age_days": age_days, + "sla_days": policy.drift_sla_days, + "policy_assignment_id": props.get("policyAssignmentId"), + }, + ) + ) + return results diff --git a/scanner/rules/az_gov_001.py b/scanner/rules/az_gov_001.py new file mode 100644 index 00000000..0dc66331 --- /dev/null +++ b/scanner/rules/az_gov_001.py @@ -0,0 +1,33 @@ +"""AZ-GOV-001: Subscription Outside Approved Management Group Hierarchy.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-001" +RULE_NAME = "Subscription Outside Approved Management Group Hierarchy" +SEVERITY = "HIGH" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-001", "NIST": "PR.AC-4", "ISO27001": "A.5.15", "SOC2": "CC6.3"} +DESCRIPTION = "An Azure subscription is outside the organisation's approved management-group hierarchy." +REMEDIATION = ( + "Move the subscription into an approved management group after validating inherited policy and access effects." +) +PLAYBOOK = "playbooks/cli/fix_az_gov_001.sh" +PERMISSIONS = "Microsoft.Management/managementGroups/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-001 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_002.py b/scanner/rules/az_gov_002.py new file mode 100644 index 00000000..b2bd2d4e --- /dev/null +++ b/scanner/rules/az_gov_002.py @@ -0,0 +1,31 @@ +"""AZ-GOV-002: Required Security Policy Initiative Missing.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-002" +RULE_NAME = "Required Security Policy Initiative Missing" +SEVERITY = "HIGH" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-002", "NIST": "PR.IP-1", "ISO27001": "A.5.1", "SOC2": "CC5.2"} +DESCRIPTION = "A required security policy initiative is not assigned at its approved scope." +REMEDIATION = "Assign the required initiative at the approved scope and validate its effective state." +PLAYBOOK = "playbooks/cli/fix_az_gov_002.sh" +PERMISSIONS = "Microsoft.Authorization/policyAssignments/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-002 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_003.py b/scanner/rules/az_gov_003.py new file mode 100644 index 00000000..e0ea000e --- /dev/null +++ b/scanner/rules/az_gov_003.py @@ -0,0 +1,31 @@ +"""AZ-GOV-003: Preventive Policy Uses Non-Enforcing Effect.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-003" +RULE_NAME = "Preventive Policy Uses Non-Enforcing Effect" +SEVERITY = "HIGH" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-003", "NIST": "PR.IP-1", "ISO27001": "A.5.1", "SOC2": "CC5.2"} +DESCRIPTION = "A mandatory preventive policy assignment uses an effect that does not enforce the approved control." +REMEDIATION = "Update the assignment effect to an organisation-approved preventive effect after testing impact." +PLAYBOOK = "playbooks/cli/fix_az_gov_003.sh" +PERMISSIONS = "Microsoft.Authorization/policyAssignments/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-003 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_004.py b/scanner/rules/az_gov_004.py new file mode 100644 index 00000000..9af8faad --- /dev/null +++ b/scanner/rules/az_gov_004.py @@ -0,0 +1,31 @@ +"""AZ-GOV-004: Policy Exemption Missing Governance Metadata.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-004" +RULE_NAME = "Policy Exemption Missing Governance Metadata" +SEVERITY = "MEDIUM" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-004", "NIST": "PR.IP-1", "ISO27001": "A.5.1", "SOC2": "CC5.3"} +DESCRIPTION = "A policy exemption lacks an accountable owner, justification, or valid expiration date." +REMEDIATION = "Add the required exemption metadata or remove the exemption if it is no longer approved." +PLAYBOOK = "playbooks/cli/fix_az_gov_004.sh" +PERMISSIONS = "Microsoft.Authorization/policyExemptions/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-004 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_005.py b/scanner/rules/az_gov_005.py new file mode 100644 index 00000000..090b6bc0 --- /dev/null +++ b/scanner/rules/az_gov_005.py @@ -0,0 +1,31 @@ +"""AZ-GOV-005: Critical Production Resource Missing Deletion Lock.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-005" +RULE_NAME = "Critical Production Resource Missing Deletion Lock" +SEVERITY = "HIGH" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-005", "NIST": "PR.IP-3", "ISO27001": "A.8.9", "SOC2": "CC6.5"} +DESCRIPTION = "A production resource in the protected scope has no effective deletion lock." +REMEDIATION = "Apply a CanNotDelete lock at the resource or an approved parent scope after validating operations." +PLAYBOOK = "playbooks/cli/fix_az_gov_005.sh" +PERMISSIONS = "Microsoft.Authorization/locks/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-005 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_006.py b/scanner/rules/az_gov_006.py new file mode 100644 index 00000000..3932f115 --- /dev/null +++ b/scanner/rules/az_gov_006.py @@ -0,0 +1,33 @@ +"""AZ-GOV-006: Excessive Subscription Owner Assignments.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-006" +RULE_NAME = "Excessive Subscription Owner Assignments" +SEVERITY = "HIGH" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-006", "NIST": "PR.AC-4", "ISO27001": "A.5.18", "SOC2": "CC6.3"} +DESCRIPTION = "The subscription has more Owner role assignments than the organisation permits." +REMEDIATION = ( + "Remove unnecessary Owner assignments and use least-privilege roles with eligible activation where appropriate." +) +PLAYBOOK = "playbooks/cli/fix_az_gov_006.sh" +PERMISSIONS = "Microsoft.Authorization/roleAssignments/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-006 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_007.py b/scanner/rules/az_gov_007.py new file mode 100644 index 00000000..a7c2653f --- /dev/null +++ b/scanner/rules/az_gov_007.py @@ -0,0 +1,31 @@ +"""AZ-GOV-007: Privileged Access Assigned at Broad Scope.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-007" +RULE_NAME = "Privileged Access Assigned at Broad Scope" +SEVERITY = "HIGH" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-007", "NIST": "PR.AC-4", "ISO27001": "A.5.18", "SOC2": "CC6.3"} +DESCRIPTION = "A privileged role is assigned outside the organisation's approved scopes." +REMEDIATION = "Reduce the assignment scope and use eligible time-bound access where supported." +PLAYBOOK = "playbooks/cli/fix_az_gov_007.sh" +PERMISSIONS = "Microsoft.Authorization/roleAssignments/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-007 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_008.py b/scanner/rules/az_gov_008.py new file mode 100644 index 00000000..3c5fa1fa --- /dev/null +++ b/scanner/rules/az_gov_008.py @@ -0,0 +1,33 @@ +"""AZ-GOV-008: Undocumented Resource Provider Registered.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-008" +RULE_NAME = "Undocumented Resource Provider Registered" +SEVERITY = "MEDIUM" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-008", "NIST": "PR.IP-1", "ISO27001": "A.5.9", "SOC2": "CC6.6"} +DESCRIPTION = "A resource provider is registered without an approved operational requirement." +REMEDIATION = ( + "Confirm the business requirement, record approval, or unregister the unused provider after impact review." +) +PLAYBOOK = "playbooks/cli/fix_az_gov_008.sh" +PERMISSIONS = "Microsoft.Resources/subscriptions/providers/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-008 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_009.py b/scanner/rules/az_gov_009.py new file mode 100644 index 00000000..855c645c --- /dev/null +++ b/scanner/rules/az_gov_009.py @@ -0,0 +1,31 @@ +"""AZ-GOV-009: Production Resource Missing Ownership Metadata.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-009" +RULE_NAME = "Production Resource Missing Ownership Metadata" +SEVERITY = "MEDIUM" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-009", "NIST": "ID.AM-1", "ISO27001": "A.5.9", "SOC2": "CC2.2"} +DESCRIPTION = "A production resource has no recognised accountable ownership tag." +REMEDIATION = "Add an approved ownership tag with a current team or service owner." +PLAYBOOK = "playbooks/cli/fix_az_gov_009.sh" +PERMISSIONS = "Microsoft.Resources/subscriptions/resources/read" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-009 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/scanner/rules/az_gov_010.py b/scanner/rules/az_gov_010.py new file mode 100644 index 00000000..3a322198 --- /dev/null +++ b/scanner/rules/az_gov_010.py @@ -0,0 +1,31 @@ +"""AZ-GOV-010: Security Configuration Drift Exceeds SLA.""" + +from typing import Any, Dict, List + +from scanner.rules._governance_common import evaluate as _evaluate + +RULE_ID = "AZ-GOV-010" +RULE_NAME = "Security Configuration Drift Exceeds SLA" +SEVERITY = "HIGH" +CATEGORY = "Governance" +FRAMEWORKS = {"CIS": "N/A-GOV-010", "NIST": "DE.CM-8", "ISO27001": "A.8.8", "SOC2": "CC7.2"} +DESCRIPTION = "Confirmed non-compliant policy state has remained unresolved beyond the approved remediation SLA." +REMEDIATION = "Investigate the policy state, remediate the drift, and confirm compliance with a new evaluation." +PLAYBOOK = "playbooks/cli/fix_az_gov_010.sh" +PERMISSIONS = "Microsoft.PolicyInsights/policyStates/queryResults/action" + +_SPEC = { + "id": RULE_ID, + "name": RULE_NAME, + "severity": SEVERITY, + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "permissions": PERMISSIONS, +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Evaluate AZ-GOV-010 from read-only governance evidence.""" + return _evaluate(_SPEC, azure_client, subscription_id) diff --git a/tests/test_governance_foundation.py b/tests/test_governance_foundation.py new file mode 100644 index 00000000..312dacd2 --- /dev/null +++ b/tests/test_governance_foundation.py @@ -0,0 +1,197 @@ +import json +from types import SimpleNamespace + +import pytest + +from scanner.governance import GovernanceCollector, load_governance_policy + + +def _valid_policy(): + return { + "approved_management_group_ids": ["/providers/Microsoft.Management/managementGroups/prod"], + "required_policy_initiatives": [{"definition_id": "/definitions/base", "scope": "/subscriptions/sub"}], + "preventive_policy_definition_ids": ["/definitions/deny-public"], + "allowed_preventive_effects": ["deny"], + "production_resource_types": ["Microsoft.Storage/storageAccounts"], + "production_tag": "environment", + "production_tag_values": ["prod"], + "maximum_subscription_owners": 2, + "privileged_role_definition_ids": ["owner-role"], + "approved_privileged_scopes": ["/subscriptions/sub/resourcegroups/security"], + "approved_provider_namespaces": ["Microsoft.Storage"], + "ownership_tags": ["owner"], + "drift_sla_days": 30, + "excluded_resource_ids": [], + } + + +def test_policy_loader_is_strict_and_normalises(tmp_path): + path = tmp_path / "policy.json" + path.write_text(json.dumps(_valid_policy()), encoding="utf-8") + policy = load_governance_policy(path) + assert policy.production_resource_types == frozenset({"microsoft.storage/storageaccounts"}) + assert policy.maximum_subscription_owners == 2 + + invalid = _valid_policy() + invalid["unexpected"] = True + path.write_text(json.dumps(invalid), encoding="utf-8") + with pytest.raises(ValueError, match="missing or unsupported"): + load_governance_policy(path) + + +class _Response: + def __init__(self, payload): + self.payload = payload + + def raise_for_status(self): + return None + + def json(self): + return self.payload + + +class _Session: + def __init__(self): + self.get_responses = [ + _Response({"value": [{"id": "one"}], "nextLink": "https://management.azure.com/next"}), + _Response({"value": [{"id": "two"}]}), + ] + + def get(self, *_args, **_kwargs): + return self.get_responses.pop(0) + + def post(self, _url, **kwargs): + if "query" in kwargs.get("json", {}): + return _Response({"data": []}) + return _Response({"value": []}) + + +def test_collector_follows_arm_next_link(): + credential = SimpleNamespace(get_token=lambda _scope: SimpleNamespace(token="secret")) + collector = GovernanceCollector(credential, "sub", session=_Session()) + assert collector._get_all("/first") == [{"id": "one"}, {"id": "two"}] + + +@pytest.mark.parametrize( + "bad_url", + [ + "https://management.azure.com.attacker.invalid/next", + "https://user@management.azure.com/next", + "https://management.azure.com:8443/next", + "http://management.azure.com/next", + "https://evil.com/next", + "", + ], +) +def test_get_all_rejects_unsafe_continuation_and_returns_none(bad_url): + class BadNextSession: + def get(self, *_args, **_kwargs): + return _Response({"value": [{"id": "one"}], "nextLink": bad_url}) + + credential = SimpleNamespace(get_token=lambda _scope: SimpleNamespace(token="secret")) + collector = GovernanceCollector(credential, "sub", session=BadNextSession()) + assert collector._get_all("/first") is None + + +def test_get_all_does_not_follow_redirects(): + called_with = {} + + class RedirectSession: + def get(self, url, **kwargs): + called_with["allow_redirects"] = kwargs.get("allow_redirects") + return _Response({"value": []}) + + credential = SimpleNamespace(get_token=lambda _scope: SimpleNamespace(token="secret")) + collector = GovernanceCollector(credential, "sub", session=RedirectSession()) + collector._get_all("/first") + assert called_with["allow_redirects"] is False + + +@pytest.mark.parametrize( + "bad_url", + [ + "https://management.azure.com.attacker.invalid/next", + "https://user@management.azure.com/next", + "https://management.azure.com:8080/next", + "http://management.azure.com/next", + ], +) +def test_post_values_rejects_unsafe_continuation_and_returns_none(bad_url): + class BadODataSession: + def post(self, *_args, **_kwargs): + return _Response({"value": [{"id": "one"}], "@odata.nextLink": bad_url}) + + credential = SimpleNamespace(get_token=lambda _scope: SimpleNamespace(token="secret")) + collector = GovernanceCollector(credential, "sub", session=BadODataSession()) + assert collector._post_values("/path") is None + + +def test_get_all_logs_warning_and_does_not_return_partial_evidence_on_bad_next_link(caplog): + """Partial results from page 1 must not be silently returned as complete when + the continuation URL fails origin validation.""" + import logging + + class BadNextSession: + def get(self, *_args, **_kwargs): + return _Response( + {"value": [{"id": "partial"}], "nextLink": "https://management.azure.com.attacker.invalid/page2"} + ) + + credential = SimpleNamespace(get_token=lambda _scope: SimpleNamespace(token="secret")) + collector = GovernanceCollector(credential, "sub", session=BadNextSession()) + with caplog.at_level(logging.WARNING, logger="scanner.governance"): + result = collector._get_all("/first") + assert result is None, "partial evidence must not be silently returned as complete" + assert any("pagination aborted" in record.message.lower() for record in caplog.records), ( + "a warning must be logged when a nextLink fails origin validation" + ) + + +def test_post_values_logs_warning_and_does_not_return_partial_evidence_on_bad_next_link(caplog): + """Partial results from page 1 must not be silently returned as complete when + the odata continuation URL fails origin validation.""" + import logging + + class BadODataSession: + def post(self, *_args, **_kwargs): + return _Response( + {"value": [{"id": "partial"}], "@odata.nextLink": "https://management.azure.com.attacker.invalid/page2"} + ) + + credential = SimpleNamespace(get_token=lambda _scope: SimpleNamespace(token="secret")) + collector = GovernanceCollector(credential, "sub", session=BadODataSession()) + with caplog.at_level(logging.WARNING, logger="scanner.governance"): + result = collector._post_values("/path") + assert result is None, "partial evidence must not be silently returned as complete" + assert any("pagination aborted" in record.message.lower() for record in caplog.records), ( + "a warning must be logged when an odata nextLink fails origin validation" + ) + + +def test_collector_returns_none_for_transport_failure(): + class BrokenSession: + @staticmethod + def get(*_args, **_kwargs): + raise TypeError("bad transport") + + credential = SimpleNamespace(get_token=lambda _scope: SimpleNamespace(token="secret")) + collector = GovernanceCollector(credential, "sub", session=BrokenSession()) + assert collector._get_all("/first") is None + + +def test_resource_graph_collector_follows_skip_token(): + class GraphSession: + def __init__(self): + self.requests = [] + + def post(self, _url, **kwargs): + self.requests.append(kwargs["json"]) + if len(self.requests) == 1: + return _Response({"data": [{"id": "one"}], "$skipToken": "next-page"}) + return _Response({"data": [{"id": "two"}]}) + + credential = SimpleNamespace(get_token=lambda _scope: SimpleNamespace(token="secret")) + session = GraphSession() + collector = GovernanceCollector(credential, "sub", session=session) + assert collector._post_graph("Resources | project id") == [{"id": "one"}, {"id": "two"}] + assert session.requests[1]["options"]["$skipToken"] == "next-page" diff --git a/tests/test_rules_governance.py b/tests/test_rules_governance.py new file mode 100644 index 00000000..ddc7f806 --- /dev/null +++ b/tests/test_rules_governance.py @@ -0,0 +1,313 @@ +from datetime import datetime, timedelta, timezone + +import pytest + +from scanner.governance import GovernancePolicy +from scanner.rules import ( + az_gov_001, + az_gov_002, + az_gov_003, + az_gov_004, + az_gov_005, + az_gov_006, + az_gov_007, + az_gov_008, + az_gov_009, + az_gov_010, +) +from scanner.rules import _governance_common as common + + +@pytest.mark.parametrize( + "module", + [ + az_gov_001, + az_gov_002, + az_gov_003, + az_gov_004, + az_gov_005, + az_gov_006, + az_gov_007, + az_gov_008, + az_gov_009, + az_gov_010, + ], +) +def test_engine_does_not_call_internal_findings_helper_as_coverage_evaluator(module, monkeypatch): + from scanner.engine import ScanEngine + + engine = ScanEngine.__new__(ScanEngine) + engine.subscription_id = "sub" + engine.client = object() + engine.rules = [module] + monkeypatch.delenv(common.POLICY_ENV, raising=False) + result = engine.run_scan() + assert result["findings"] == [] + assert result["failed_rule_ids"] == [] + assert result["evaluations"][0]["status"] == "UNKNOWN" + assert result["evaluations"][0]["reason_code"] == "LEGACY_RULE_NOT_MIGRATED" + + +SUB = "sub" +SCOPE = "/subscriptions/sub" +RESOURCE = f"{SCOPE}/resourceGroups/prod/providers/Microsoft.Storage/storageAccounts/data" +OWNER = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" + + +@pytest.fixture +def policy(): + return GovernancePolicy( + approved_management_group_ids=frozenset({"/providers/microsoft.management/managementgroups/prod"}), + required_policy_initiatives=({"definition_id": "/definitions/base", "scope": SCOPE},), + preventive_policy_definition_ids=frozenset({"/definitions/deny-public"}), + allowed_preventive_effects=frozenset({"deny"}), + production_resource_types=frozenset({"microsoft.storage/storageaccounts"}), + production_tag="environment", + production_tag_values=frozenset({"prod"}), + maximum_subscription_owners=1, + privileged_role_definition_ids=frozenset({OWNER}), + approved_privileged_scopes=frozenset({f"{SCOPE}/resourcegroups/security"}), + approved_provider_namespaces=frozenset({"microsoft.storage"}), + ownership_tags=frozenset({"owner"}), + drift_sla_days=30, + excluded_resource_ids=frozenset(), + ) + + +@pytest.fixture +def run(monkeypatch, policy): + def _run(module, snapshot): + monkeypatch.setattr(common, "load_context", lambda *_args: (policy, snapshot)) + return module.scan(object(), SUB) + + return _run + + +def test_gov_001_hierarchy_compliant_and_noncompliant(run): + good = { + "hierarchy": [ + {"managementGroupAncestorsChain": [{"id": "/providers/Microsoft.Management/managementGroups/prod"}]} + ] + } + assert run(az_gov_001, good) == [] + findings = run(az_gov_001, {"hierarchy": [{"managementGroupAncestorsChain": []}]}) + assert findings[0]["rule_id"] == "AZ-GOV-001" + + +def test_gov_002_required_initiative_scope(run): + good = { + "policy_assignments": [ + { + "id": f"{SCOPE}/providers/Microsoft.Authorization/policyAssignments/base", + "properties": {"policyDefinitionId": "/definitions/base"}, + } + ] + } + assert run(az_gov_002, good) == [] + assert run(az_gov_002, {"policy_assignments": []})[0]["metadata"]["evidence"]["missing_assignments"] + + +def test_gov_003_only_confirmed_bad_effect_fails(run): + definition = { + "id": "/definitions/deny-public", + "properties": { + "parameters": {"effect": {"defaultValue": "Audit"}}, + "policyRule": {"then": {"effect": "[parameters('effect')]"}}, + }, + } + assignment = { + "id": f"{SCOPE}/providers/Microsoft.Authorization/policyAssignments/deny", + "properties": {"policyDefinitionId": "/definitions/deny-public", "parameters": {}}, + } + snapshot = {"policy_assignments": [assignment], "policy_definitions": [definition]} + assert run(az_gov_003, snapshot)[0]["rule_id"] == "AZ-GOV-003" + assignment["properties"]["parameters"]["effect"] = {"value": "Deny"} + assert run(az_gov_003, snapshot) == [] + + +def test_gov_004_exemption_metadata_and_expiration(run): + future = (datetime.now(timezone.utc) + timedelta(days=20)).isoformat() + good = { + "policy_exemptions": [ + { + "id": f"{SCOPE}/providers/Microsoft.Authorization/policyExemptions/approved", + "properties": {"expiresOn": future, "metadata": {"owner": "team", "justification": "migration"}}, + } + ] + } + assert run(az_gov_004, good) == [] + bad = {"policy_exemptions": [{"id": "exemption", "properties": {"metadata": {}}}]} + assert set(run(az_gov_004, bad)[0]["metadata"]["evidence"]["missing_or_invalid"]) == { + "owner", + "justification", + "valid expiration", + } + + +def test_gov_005_honours_parent_lock(run): + resource = {"id": RESOURCE, "type": "Microsoft.Storage/storageAccounts", "tags": {"Environment": "Prod"}} + parent = f"{SCOPE}/resourceGroups/prod" + lock = {"id": f"{parent}/providers/Microsoft.Authorization/locks/protect", "properties": {"level": "CanNotDelete"}} + assert run(az_gov_005, {"resources": [resource], "locks": [lock]}) == [] + assert run(az_gov_005, {"resources": [resource], "locks": []})[0]["resource_id"] == RESOURCE + + +def test_gov_006_owner_threshold(run): + def owner(name): + return { + "id": f"{SCOPE}/providers/Microsoft.Authorization/roleAssignments/{name}", + "properties": {"roleDefinitionId": f"{SCOPE}/providers/Microsoft.Authorization/roleDefinitions/{OWNER}"}, + } + + def mg_owner(name): + mg_scope = "/providers/Microsoft.Management/managementGroups/root" + return { + "id": f"{mg_scope}/providers/Microsoft.Authorization/roleAssignments/{name}", + "properties": {"roleDefinitionId": f"{mg_scope}/providers/Microsoft.Authorization/roleDefinitions/{OWNER}"}, + } + + assert run(az_gov_006, {"role_assignments": [owner("one")]}) == [] + assert ( + run(az_gov_006, {"role_assignments": [owner("one"), owner("two")]})[0]["metadata"]["evidence"]["owner_count"] + == 2 + ) + # Inherited management-group Owner must count toward the threshold. + # The collector's atScope() returns both direct and MG-inherited assignments; + # filtering to subscription scope alone silently misses this common pattern. + assert ( + run(az_gov_006, {"role_assignments": [owner("direct"), mg_owner("inherited")]})[0]["metadata"]["evidence"][ + "owner_count" + ] + == 2 + ) + + +def test_gov_007_privileged_scope(run): + broad = { + "id": f"{SCOPE}/providers/Microsoft.Authorization/roleAssignments/one", + "properties": {"roleDefinitionId": f"/roleDefinitions/{OWNER}"}, + } + finding = run(az_gov_007, {"role_assignments": [broad]})[0] + assert finding["rule_id"] == "AZ-GOV-007" + assert finding["metadata"]["effective_scope"] == SCOPE + assert finding["metadata"]["permissions_required"] + assert finding["metadata"]["unknown_reason"] is None + + +def test_gov_008_registered_provider_allowlist(run): + providers = [ + {"namespace": "Microsoft.Storage", "registrationState": "Registered"}, + {"namespace": "Microsoft.Unapproved", "registrationState": "Registered"}, + ] + findings = run(az_gov_008, {"providers": providers}) + assert len(findings) == 1 + assert "Microsoft.Unapproved" in findings[0]["resource_id"] + + +def test_gov_009_ownership_tag(run): + resource = {"id": RESOURCE, "type": "Microsoft.Storage/storageAccounts", "tags": {"environment": "prod"}} + assert run(az_gov_009, {"resources": [resource]})[0]["rule_id"] == "AZ-GOV-009" + resource["tags"]["owner"] = "payments" + assert run(az_gov_009, {"resources": [resource]}) == [] + + +def test_gov_010_drift_sla_boundary(run): + old = (datetime.now(timezone.utc) - timedelta(days=31)).isoformat() + boundary = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat() + recent = (datetime.now(timezone.utc) - timedelta(days=29)).isoformat() + + def state(timestamp): + return {"complianceState": "NonCompliant", "timestamp": timestamp, "resourceId": RESOURCE} + + assert run(az_gov_010, {"policy_states": [state(old)]})[0]["rule_id"] == "AZ-GOV-010" + assert run(az_gov_010, {"policy_states": [state(boundary)]}) == [] + assert run(az_gov_010, {"policy_states": [state(recent)]}) == [] + + +def test_gov_003_missing_definitions_evidence_is_unknown(run): + assignment = { + "id": f"{SCOPE}/providers/Microsoft.Authorization/policyAssignments/deny", + "properties": {"policyDefinitionId": "/definitions/deny-public", "parameters": {}}, + } + assert run(az_gov_003, {"policy_assignments": [assignment], "policy_definitions": None}) == [] + + +def test_gov_005_missing_locks_evidence_is_unknown(run): + resource = {"id": RESOURCE, "type": "Microsoft.Storage/storageAccounts", "tags": {"Environment": "Prod"}} + assert run(az_gov_005, {"resources": [resource], "locks": None}) == [] + + +@pytest.mark.parametrize( + "module,source", + [ + (az_gov_001, "hierarchy"), + (az_gov_002, "policy_assignments"), + (az_gov_003, "policy_assignments"), + (az_gov_004, "policy_exemptions"), + (az_gov_005, "resources"), + (az_gov_006, "role_assignments"), + (az_gov_007, "role_assignments"), + (az_gov_008, "providers"), + (az_gov_009, "resources"), + (az_gov_010, "policy_states"), + ], +) +def test_missing_evidence_never_becomes_failure(run, module, source): + assert run(module, {source: None}) == [] + + +class TestLoadContextCaching: + """load_context must cache by subscription_id to prevent cross-subscription stale replays.""" + + def _make_client(self): + class _FakeCollector: + def __init__(self, credential, subscription_id): + self.subscription_id = subscription_id + + def collect(self): + return {"sub": self.subscription_id} + + class _FakeClient: + credential = object() + + return _FakeClient(), _FakeCollector + + def test_cache_is_populated_on_first_call(self, monkeypatch, tmp_path): + client, FakeCollector = self._make_client() + monkeypatch.setattr(common, "GovernanceCollector", FakeCollector) + policy_file = tmp_path / "policy.json" + policy_file.write_text('{"approved_management_group_ids": []}') + monkeypatch.setenv(common.POLICY_ENV, str(policy_file)) + monkeypatch.setattr(common, "load_governance_policy", lambda _path: object()) + common.load_context(client, "sub-A", "AZ-GOV-001") + assert getattr(client, "_governance_snapshot_cache_sub") == "sub-A" + assert getattr(client, "_governance_snapshot_cache") == {"sub": "sub-A"} + + def test_cache_is_reused_for_same_subscription(self, monkeypatch, tmp_path): + client, FakeCollector = self._make_client() + call_count = 0 + + class _CountingCollector(FakeCollector): + def __init__(self, credential, subscription_id): + nonlocal call_count + call_count += 1 + super().__init__(credential, subscription_id) + + monkeypatch.setattr(common, "GovernanceCollector", _CountingCollector) + monkeypatch.setenv(common.POLICY_ENV, str(tmp_path / "p.json")) + (tmp_path / "p.json").write_text("{}") + monkeypatch.setattr(common, "load_governance_policy", lambda _path: object()) + common.load_context(client, "sub-A", "AZ-GOV-001") + common.load_context(client, "sub-A", "AZ-GOV-002") + assert call_count == 1, "Second call with the same subscription must reuse the cache" + + def test_cache_is_invalidated_on_new_subscription(self, monkeypatch, tmp_path): + client, FakeCollector = self._make_client() + monkeypatch.setattr(common, "GovernanceCollector", FakeCollector) + monkeypatch.setenv(common.POLICY_ENV, str(tmp_path / "p.json")) + (tmp_path / "p.json").write_text("{}") + monkeypatch.setattr(common, "load_governance_policy", lambda _path: object()) + common.load_context(client, "sub-A", "AZ-GOV-001") + common.load_context(client, "sub-B", "AZ-GOV-001") + assert getattr(client, "_governance_snapshot_cache") == {"sub": "sub-B"} + assert getattr(client, "_governance_snapshot_cache_sub") == "sub-B"