diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f85576..5c3e1d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ OpenShield uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added +- Azure public-cloud Physical Layer Assurance API with complete OSI and IEEE PHY domain, sublayer, and provider-evidence coverage - Semgrep SAST integrated into GitHub Actions CI as an open-source, account-free complement to CodeQL - OpenSSF Best Practices Passing Badge achieved with 100% of applicable Passing-level criteria completed - Official live OpenSSF badge and verified project record added to project documentation diff --git a/api/app.py b/api/app.py index 217742f..e4993f3 100644 --- a/api/app.py +++ b/api/app.py @@ -195,6 +195,7 @@ def verify_jwt() -> None: # Blueprints # # ------------------------------------------------------------------ # from api.routes.ai import ai_bp + from api.routes.assurance import assurance_bp from api.routes.cbom import cbom_bp from api.routes.compliance import compliance_bp from api.routes.drift import drift_bp @@ -205,6 +206,7 @@ def verify_jwt() -> None: from api.routes.score import score_bp app.register_blueprint(ai_bp) + app.register_blueprint(assurance_bp) app.register_blueprint(cbom_bp) app.register_blueprint(compliance_bp) app.register_blueprint(drift_bp) diff --git a/api/routes/assurance.py b/api/routes/assurance.py new file mode 100644 index 0000000..285c7c1 --- /dev/null +++ b/api/routes/assurance.py @@ -0,0 +1,24 @@ +"""Provider-assurance routes for infrastructure that tenants cannot scan.""" + +import logging + +from flask import Blueprint, jsonify + +from api.services.physical_assurance import CatalogValidationError, get_physical_assurance_report + + +assurance_bp = Blueprint("assurance", __name__) +logger = logging.getLogger(__name__) + + +@assurance_bp.get("/api/assurance/physical-layer") +def get_physical_layer_assurance(): + """Return Azure public-cloud OSI Layer 1 responsibility and evidence coverage.""" + try: + return jsonify(get_physical_assurance_report()) + except CatalogValidationError as exc: + logger.error("Physical assurance catalog validation failed: %s", exc) + return jsonify({"error": "Physical assurance catalog is unavailable"}), 500 + except Exception as exc: + logger.error("Failed to build physical assurance report: %s", exc) + return jsonify({"error": "Physical assurance report generation failed"}), 500 diff --git a/api/services/physical_assurance.py b/api/services/physical_assurance.py new file mode 100644 index 0000000..4b6a680 --- /dev/null +++ b/api/services/physical_assurance.py @@ -0,0 +1,272 @@ +"""Load and validate Azure Physical-layer provider assurance evidence.""" + +from __future__ import annotations + +import copy +import json +from datetime import date +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + + +CATALOG_PATH = Path(__file__).resolve().parents[2] / "compliance" / "assurance" / "physical_layer.json" + +EXPECTED_MICROSOFT_CONTROLS = {f"PE-{number}" for number in range(1, 9)} +EXPECTED_ISO_CONTROLS = { + *(f"A.11.1.{number}" for number in range(1, 7)), + *(f"A.11.2.{number}" for number in range(1, 10)), +} +EXPECTED_DOMAIN_IDS = {f"PHY-{number:02d}" for number in range(1, 22)} +EXPECTED_SUBLAYER_IDS = { + "GENERIC-L1", + "IEEE-PLCP", + "IEEE-PCS", + "IEEE-FEC", + "IEEE-PMA", + "IEEE-PMD", + "IEEE-AN", + "IEEE-MDI", +} +ALLOWED_STATUSES = {"PROVIDER_ATTESTED", "REVIEW_DUE", "NOT_APPLICABLE", "UNKNOWN"} +ALLOWED_EVIDENCE_HOSTS = {"learn.microsoft.com"} + + +class CatalogValidationError(ValueError): + """Raised when the bundled assurance catalog is incomplete or unsafe.""" + + +def _require_string(item: dict[str, Any], field: str, context: str) -> str: + value = item.get(field) + if not isinstance(value, str) or not value.strip(): + raise CatalogValidationError(f"{context}: {field} must be a non-empty string") + return value + + +def _require_unique(items: list[dict[str, Any]], name: str) -> dict[str, dict[str, Any]]: + indexed: dict[str, dict[str, Any]] = {} + for item in items: + if not isinstance(item, dict): + raise CatalogValidationError(f"{name}: every entry must be an object") + item_id = _require_string(item, "id", name) + if item_id in indexed: + raise CatalogValidationError(f"{name}: duplicate id {item_id}") + indexed[item_id] = item + return indexed + + +def _require_reference_list(item: dict[str, Any], field: str, allowed_ids: set[str], context: str) -> list[str]: + values = item.get(field) + if not isinstance(values, list) or not values: + raise CatalogValidationError(f"{context}: {field} must be a non-empty list") + if any(not isinstance(value, str) for value in values): + raise CatalogValidationError(f"{context}: {field} must contain only strings") + if len(values) != len(set(values)): + raise CatalogValidationError(f"{context}: {field} contains duplicate references") + unknown = set(values) - allowed_ids + if unknown: + raise CatalogValidationError(f"{context}: {field} contains unknown ids {sorted(unknown)}") + return values + + +def _parse_date(value: Any, context: str) -> date: + if not isinstance(value, str): + raise CatalogValidationError(f"{context}: date must be an ISO-8601 string") + try: + return date.fromisoformat(value) + except ValueError as exc: + raise CatalogValidationError(f"{context}: invalid ISO-8601 date {value!r}") from exc + + +def load_catalog(path: Path = CATALOG_PATH) -> dict[str, Any]: + """Load and validate a physical assurance catalog from disk.""" + try: + with path.open(encoding="utf-8") as catalog_file: + catalog = json.load(catalog_file) + except (OSError, json.JSONDecodeError) as exc: + raise CatalogValidationError(f"Unable to load physical assurance catalog: {exc}") from exc + + validate_catalog(catalog) + return catalog + + +def validate_catalog(catalog: dict[str, Any]) -> None: + """Enforce the closed Layer 1 catalog and all cross-reference invariants.""" + if not isinstance(catalog, dict): + raise CatalogValidationError("Catalog root must be an object") + + layer = catalog.get("layer") + if not isinstance(layer, dict) or layer.get("number") != 1 or layer.get("name") != "Physical": + raise CatalogValidationError("Catalog must describe OSI Layer 1 Physical") + + scope = catalog.get("scope") + if not isinstance(scope, dict): + raise CatalogValidationError("scope must be an object") + if scope.get("environment") != "azure_public_cloud": + raise CatalogValidationError("scope.environment must be azure_public_cloud") + if scope.get("owner") != "Microsoft" or scope.get("runtime_hardware_observable") is not False: + raise CatalogValidationError("Azure physical infrastructure must be Microsoft-owned and unobservable") + + domains = catalog.get("domains") + sublayers = catalog.get("sublayers") + evidence_sources = catalog.get("evidence_sources") + controls = catalog.get("controls") + if not all(isinstance(items, list) for items in (domains, sublayers, evidence_sources, controls)): + raise CatalogValidationError("domains, sublayers, evidence_sources, and controls must be lists") + + domain_index = _require_unique(domains, "domains") + sublayer_index = _require_unique(sublayers, "sublayers") + evidence_index = _require_unique(evidence_sources, "evidence_sources") + control_index = _require_unique(controls, "controls") + + if set(domain_index) != EXPECTED_DOMAIN_IDS: + raise CatalogValidationError("domains must contain the complete PHY-01 through PHY-21 set") + if set(sublayer_index) != EXPECTED_SUBLAYER_IDS: + raise CatalogValidationError("sublayers must contain the complete generic and IEEE PHY set") + if len(control_index) != 23: + raise CatalogValidationError("controls must contain exactly 23 baseline controls") + + methodology = catalog.get("methodology") + if not isinstance(methodology, dict): + raise CatalogValidationError("methodology must be an object") + expected_counts = { + "baseline_control_count": len(control_index), + "domain_count": len(domain_index), + "sublayer_count": len(sublayer_index), + } + for field, expected in expected_counts.items(): + if methodology.get(field) != expected: + raise CatalogValidationError(f"methodology.{field} must equal {expected}") + + for domain_id, domain in domain_index.items(): + _require_string(domain, "name", domain_id) + _require_string(domain, "description", domain_id) + + domain_ids = set(domain_index) + sublayer_ids = set(sublayer_index) + evidence_ids = set(evidence_index) + + for sublayer_id, sublayer in sublayer_index.items(): + _require_string(sublayer, "name", sublayer_id) + _require_string(sublayer, "profile", sublayer_id) + _require_string(sublayer, "description", sublayer_id) + _require_reference_list(sublayer, "domain_ids", domain_ids, sublayer_id) + + for evidence_id, evidence in evidence_index.items(): + _require_string(evidence, "title", evidence_id) + _require_string(evidence, "evidence_type", evidence_id) + url = _require_string(evidence, "url", evidence_id) + parsed_url = urlparse(url) + if parsed_url.scheme != "https" or parsed_url.hostname not in ALLOWED_EVIDENCE_HOSTS: + raise CatalogValidationError(f"{evidence_id}: evidence URL must use HTTPS on an allowed host") + reviewed_at = _parse_date(evidence.get("reviewed_at"), f"{evidence_id}.reviewed_at") + review_due_at = _parse_date(evidence.get("review_due_at"), f"{evidence_id}.review_due_at") + if review_due_at <= reviewed_at: + raise CatalogValidationError(f"{evidence_id}: review_due_at must be after reviewed_at") + + microsoft_controls: set[str] = set() + iso_controls: set[str] = set() + referenced_domains: set[str] = set() + referenced_sublayers: set[str] = set() + referenced_evidence: set[str] = set() + + for control_id, control in control_index.items(): + framework = _require_string(control, "framework", control_id) + baseline_id = _require_string(control, "control_id", control_id) + _require_string(control, "title", control_id) + if control.get("responsibility") != "Microsoft": + raise CatalogValidationError(f"{control_id}: responsibility must be Microsoft") + if control.get("applicability") != "APPLICABLE": + raise CatalogValidationError(f"{control_id}: Azure baseline controls must be APPLICABLE") + if control.get("verification") != "PROVIDER_ASSURANCE": + raise CatalogValidationError(f"{control_id}: verification must be PROVIDER_ASSURANCE") + if control.get("status") not in ALLOWED_STATUSES: + raise CatalogValidationError(f"{control_id}: unsupported status {control.get('status')!r}") + + control_sublayers = _require_reference_list(control, "sublayer_ids", sublayer_ids, control_id) + control_domains = _require_reference_list(control, "domain_ids", domain_ids, control_id) + control_evidence = _require_reference_list(control, "evidence_source_ids", evidence_ids, control_id) + referenced_sublayers.update(control_sublayers) + referenced_domains.update(control_domains) + referenced_evidence.update(control_evidence) + + if framework == "Microsoft SOC": + microsoft_controls.add(baseline_id) + elif framework == "ISO/IEC 27001:2013": + iso_controls.add(baseline_id) + else: + raise CatalogValidationError(f"{control_id}: unsupported baseline framework {framework!r}") + + if microsoft_controls != EXPECTED_MICROSOFT_CONTROLS: + raise CatalogValidationError("Microsoft baseline must contain PE-1 through PE-8 exactly once") + if iso_controls != EXPECTED_ISO_CONTROLS: + raise CatalogValidationError("ISO baseline must contain all fifteen A.11 controls exactly once") + if referenced_domains != domain_ids: + raise CatalogValidationError(f"Uncovered physical domains: {sorted(domain_ids - referenced_domains)}") + if referenced_sublayers != sublayer_ids: + raise CatalogValidationError(f"Uncovered physical sublayers: {sorted(sublayer_ids - referenced_sublayers)}") + if referenced_evidence != evidence_ids: + raise CatalogValidationError(f"Unreferenced evidence sources: {sorted(evidence_ids - referenced_evidence)}") + + +def build_report(catalog: dict[str, Any], as_of: date | None = None) -> dict[str, Any]: + """Build the API report while keeping coverage and freshness independent.""" + validate_catalog(catalog) + report = copy.deepcopy(catalog) + as_of = as_of or date.today() + + evidence_by_id = {item["id"]: item for item in report["evidence_sources"]} + current_controls = 0 + status_counts = {status: 0 for status in ALLOWED_STATUSES} + + for control in report["controls"]: + expanded_evidence = [evidence_by_id[source_id] for source_id in control["evidence_source_ids"]] + evidence_current = all(date.fromisoformat(item["review_due_at"]) >= as_of for item in expanded_evidence) + if not evidence_current and control["status"] == "PROVIDER_ATTESTED": + control["status"] = "REVIEW_DUE" + control["evidence_current"] = evidence_current + control["evidence"] = expanded_evidence + current_controls += int(evidence_current) + status_counts[control["status"]] += 1 + + control_ids_by_domain = { + domain["id"]: [control["id"] for control in report["controls"] if domain["id"] in control["domain_ids"]] + for domain in report["domains"] + } + sublayer_ids_by_domain = { + domain["id"]: [sublayer["id"] for sublayer in report["sublayers"] if domain["id"] in sublayer["domain_ids"]] + for domain in report["domains"] + } + for domain in report["domains"]: + domain["control_ids"] = control_ids_by_domain[domain["id"]] + domain["sublayer_ids"] = sublayer_ids_by_domain[domain["id"]] + for sublayer in report["sublayers"]: + sublayer["control_ids"] = [ + control["id"] for control in report["controls"] if sublayer["id"] in control["sublayer_ids"] + ] + + total_controls = len(report["controls"]) + report["summary"] = { + "baseline_controls": total_controls, + "covered_controls": total_controls, + "coverage_percent": 100, + "domains": len(report["domains"]), + "covered_domains": len(report["domains"]), + "sublayers": len(report["sublayers"]), + "covered_sublayers": len(report["sublayers"]), + "evidence_current_controls": current_controls, + "evidence_current_percent": round((current_controls / total_controls) * 100) if total_controls else 0, + "status_counts": status_counts, + "assessed_as_of": as_of.isoformat(), + } + report["limitations"] = [ + "This is provider-assurance coverage, not a live scan of Microsoft datacenter hardware.", + "A 100 percent catalog score does not assert tenant compliance or certify physical infrastructure.", + "Expired evidence is reported as REVIEW_DUE and never converted into a technical security finding.", + ] + return report + + +def get_physical_assurance_report(as_of: date | None = None) -> dict[str, Any]: + """Load the bundled catalog and return its derived assurance report.""" + return build_report(load_catalog(), as_of=as_of) diff --git a/compliance/assurance/physical_layer.json b/compliance/assurance/physical_layer.json new file mode 100644 index 0000000..b28c42b --- /dev/null +++ b/compliance/assurance/physical_layer.json @@ -0,0 +1,269 @@ +{ + "schema_version": 1, + "catalog_version": "2026.08", + "layer": { + "number": 1, + "name": "Physical", + "model": "OSI", + "assessment_type": "provider_assurance" + }, + "scope": { + "environment": "azure_public_cloud", + "cloud_models": ["IaaS", "PaaS", "SaaS"], + "owner": "Microsoft", + "runtime_hardware_observable": false, + "statement": "Azure tenants consume software-defined networking and cannot inspect Microsoft datacenter cabling, signaling hardware, environmental systems, or physical access records. Coverage therefore measures responsibility and evidence completeness, not a tenant-side hardware scan." + }, + "methodology": { + "coverage_denominator": "baseline_controls", + "coverage_rule": "A baseline control is covered only when it has an owner, applicability decision, verification method, at least one physical domain, at least one relevant sublayer, and at least one evidence source.", + "evidence_freshness_rule": "Evidence is current through review_due_at. Expiry changes evidence status but does not erase catalog mapping coverage.", + "baseline_control_count": 23, + "domain_count": 21, + "sublayer_count": 8 + }, + "sublayers": [ + { + "id": "GENERIC-L1", + "name": "Generic OSI Layer 1 functions", + "profile": "osi", + "description": "Technology-neutral transmission of raw bits, physical interfaces, timing, rates, modes, topology, and physical link lifecycle.", + "domain_ids": ["PHY-01", "PHY-02", "PHY-03", "PHY-04", "PHY-10", "PHY-11", "PHY-12", "PHY-13", "PHY-14", "PHY-15", "PHY-17"] + }, + { + "id": "IEEE-PLCP", + "name": "Physical Layer Convergence Procedure", + "profile": "ieee_802_wireless", + "description": "Adapts MAC protocol data units to the physical medium, including physical framing, synchronization, rate signaling, and channel assessment functions.", + "domain_ids": ["PHY-08", "PHY-10", "PHY-11", "PHY-14", "PHY-16"] + }, + { + "id": "IEEE-PCS", + "name": "Physical Coding Sublayer", + "profile": "ieee_802_ethernet", + "description": "Encodes and decodes data blocks, aligns lanes, and provides physical coding and synchronization functions above the attachment layer.", + "domain_ids": ["PHY-04", "PHY-05", "PHY-10", "PHY-11", "PHY-12"] + }, + { + "id": "IEEE-FEC", + "name": "Forward Error Correction", + "profile": "ieee_802_ethernet", + "description": "Adds and checks redundant coding used to correct physical transmission errors on supported links.", + "domain_ids": ["PHY-06", "PHY-11", "PHY-15"] + }, + { + "id": "IEEE-PMA", + "name": "Physical Medium Attachment", + "profile": "ieee_802_ethernet", + "description": "Provides serialization, deserialization, clock recovery, lane distribution, and attachment between coding and medium-dependent functions.", + "domain_ids": ["PHY-03", "PHY-07", "PHY-10", "PHY-11", "PHY-12", "PHY-15"] + }, + { + "id": "IEEE-PMD", + "name": "Physical Medium Dependent", + "profile": "ieee_802", + "description": "Defines medium-specific transmission, reception, modulation, optical, electrical, or radio characteristics and signal measurements.", + "domain_ids": ["PHY-01", "PHY-02", "PHY-03", "PHY-04", "PHY-11", "PHY-15", "PHY-16"] + }, + { + "id": "IEEE-AN", + "name": "Auto-Negotiation and Link Training", + "profile": "ieee_802_ethernet", + "description": "Establishes compatible link capabilities, operating rates, duplex modes, lanes, and trained signal parameters where supported.", + "domain_ids": ["PHY-09", "PHY-11", "PHY-14", "PHY-15"] + }, + { + "id": "IEEE-MDI", + "name": "Medium Dependent Interface", + "profile": "ieee_802_ethernet", + "description": "Covers the physical connector, port, pin, fiber, or other attachment boundary between equipment and the transmission medium.", + "domain_ids": ["PHY-01", "PHY-02", "PHY-03", "PHY-17"] + } + ], + "domains": [ + {"id": "PHY-01", "name": "Transmission media and cabling", "description": "Copper, fiber, radio, and other media used to carry physical signals."}, + {"id": "PHY-02", "name": "Connectors and medium interfaces", "description": "Ports, connectors, pinouts, patching, and medium-dependent attachment boundaries."}, + {"id": "PHY-03", "name": "Transceivers and physical attachment", "description": "Optical, electrical, and radio transmit and receive hardware and its attachment functions."}, + {"id": "PHY-04", "name": "Signal representation and line coding", "description": "Conversion of bits into physical symbols, line codes, or modulated signals."}, + {"id": "PHY-05", "name": "Physical coding and lane alignment", "description": "Block coding, scrambling, alignment, and lane distribution functions."}, + {"id": "PHY-06", "name": "Forward error correction", "description": "Physical-layer redundancy used to detect or correct transmission errors."}, + {"id": "PHY-07", "name": "Serialization and clock recovery", "description": "Serialization, deserialization, clock generation, and recovered timing functions."}, + {"id": "PHY-08", "name": "Physical convergence and framing", "description": "Technology-specific convergence, physical preambles, headers, framing, and channel assessment."}, + {"id": "PHY-09", "name": "Auto-negotiation and link training", "description": "Capability exchange and adaptation used to establish a compatible physical link."}, + {"id": "PHY-10", "name": "Bit timing and synchronization", "description": "Symbol timing, bit synchronization, clocking, and alignment required for reliable transmission."}, + {"id": "PHY-11", "name": "Data rate, bandwidth, and transmission mode", "description": "Supported rates, bandwidth, simplex, half-duplex, full-duplex, and parallel or serial operation."}, + {"id": "PHY-12", "name": "Multiplexing and channelization", "description": "Physical aggregation, lanes, wavelengths, frequencies, and channel allocation."}, + {"id": "PHY-13", "name": "Physical topology", "description": "Physical arrangement of endpoints, devices, paths, tiers, and interconnects."}, + {"id": "PHY-14", "name": "Physical link lifecycle", "description": "Activation, deactivation, initialization, training, and loss-of-signal handling."}, + {"id": "PHY-15", "name": "Signal integrity and interference", "description": "Attenuation, noise, crosstalk, electromagnetic interference, optical budget, and physical error symptoms."}, + {"id": "PHY-16", "name": "Radio spectrum and antennas", "description": "Radio channels, frequencies, antennas, propagation, interference, and wireless physical boundaries."}, + {"id": "PHY-17", "name": "Physical devices, ports, and cross-connects", "description": "Switching and routing hardware, racks, ports, patching, and cross-connect protection."}, + {"id": "PHY-18", "name": "Path and failure-domain resilience", "description": "Redundant devices, diverse paths, power and cooling domains, and physical fault isolation."}, + {"id": "PHY-19", "name": "Facility access and surveillance", "description": "Perimeters, authorization, entry mechanisms, access reviews, guards, alarms, and monitoring."}, + {"id": "PHY-20", "name": "Power, cooling, fire, water, and environment", "description": "Supporting utilities and environmental controls protecting physical availability."}, + {"id": "PHY-21", "name": "Equipment lifecycle and incident handling", "description": "Siting, maintenance, movement, removal, reuse, disposal, and response to physical incidents."} + ], + "evidence_sources": [ + { + "id": "MS-SHARED-RESPONSIBILITY", + "title": "Shared responsibility in the cloud", + "url": "https://learn.microsoft.com/en-us/azure/security/fundamentals/shared-responsibility", + "evidence_type": "provider_documentation", + "reviewed_at": "2026-08-08", + "review_due_at": "2027-08-08" + }, + { + "id": "MS-DATACENTER-SECURITY", + "title": "Datacenter security overview", + "url": "https://learn.microsoft.com/en-us/compliance/assurance/assurance-datacenter-security", + "evidence_type": "provider_assurance", + "reviewed_at": "2026-08-08", + "review_due_at": "2027-08-08" + }, + { + "id": "MS-PHYSICAL-ACCESS", + "title": "Datacenter physical access security", + "url": "https://learn.microsoft.com/en-us/compliance/assurance/assurance-datacenter-physical-access-security", + "evidence_type": "provider_assurance", + "reviewed_at": "2026-08-08", + "review_due_at": "2027-08-08" + }, + { + "id": "MS-AZURE-NETWORK-ARCHITECTURE", + "title": "Azure network architecture", + "url": "https://learn.microsoft.com/en-us/azure/security/fundamentals/infrastructure-network", + "evidence_type": "provider_documentation", + "reviewed_at": "2026-08-08", + "review_due_at": "2027-08-08" + }, + { + "id": "MS-ISO-27001-POLICY", + "title": "Regulatory Compliance details for ISO 27001:2013", + "url": "https://learn.microsoft.com/en-us/azure/governance/policy/samples/iso-27001", + "evidence_type": "regulatory_mapping", + "reviewed_at": "2026-08-08", + "review_due_at": "2027-08-08" + } + ], + "controls": [ + { + "id": "MS-PE-1", "framework": "Microsoft SOC", "control_id": "PE-1", "title": "Datacenter physical access provisioning", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-19"], "evidence_source_ids": ["MS-SHARED-RESPONSIBILITY", "MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS"] + }, + { + "id": "MS-PE-2", "framework": "Microsoft SOC", "control_id": "PE-2", "title": "Datacenter security verification", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-19"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS"] + }, + { + "id": "MS-PE-3", "framework": "Microsoft SOC", "control_id": "PE-3", "title": "Datacenter user access review", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-19"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS"] + }, + { + "id": "MS-PE-4", "framework": "Microsoft SOC", "control_id": "PE-4", "title": "Datacenter physical access mechanisms", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-MDI"], "domain_ids": ["PHY-02", "PHY-17", "PHY-19"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS"] + }, + { + "id": "MS-PE-5", "framework": "Microsoft SOC", "control_id": "PE-5", "title": "Datacenter physical surveillance monitoring", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-19"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS"] + }, + { + "id": "MS-PE-6", "framework": "Microsoft SOC", "control_id": "PE-6", "title": "Datacenter critical environment maintenance", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-PMD"], "domain_ids": ["PHY-15", "PHY-20", "PHY-21"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-AZURE-NETWORK-ARCHITECTURE"] + }, + { + "id": "MS-PE-7", "framework": "Microsoft SOC", "control_id": "PE-7", "title": "Datacenter environmental controls", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-PMD"], "domain_ids": ["PHY-15", "PHY-18", "PHY-20"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-AZURE-NETWORK-ARCHITECTURE"] + }, + { + "id": "MS-PE-8", "framework": "Microsoft SOC", "control_id": "PE-8", "title": "Datacenter incident response", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-18", "PHY-19", "PHY-20", "PHY-21"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS"] + }, + { + "id": "ISO-A.11.1.1", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.1.1", "title": "Physical security perimeter", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-19"], "evidence_source_ids": ["MS-SHARED-RESPONSIBILITY", "MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.1.2", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.1.2", "title": "Physical entry controls", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-19"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.1.3", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.1.3", "title": "Securing offices, rooms, and facilities", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-17", "PHY-19"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.1.4", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.1.4", "title": "Protecting against external and environmental threats", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-PMD"], "domain_ids": ["PHY-15", "PHY-18", "PHY-20"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-AZURE-NETWORK-ARCHITECTURE", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.1.5", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.1.5", "title": "Working in secure areas", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-19", "PHY-21"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.1.6", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.1.6", "title": "Delivery and loading areas", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-17", "PHY-19", "PHY-21"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.1", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.1", "title": "Equipment siting and protection", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-MDI", "IEEE-PMD"], "domain_ids": ["PHY-02", "PHY-03", "PHY-15", "PHY-17", "PHY-18", "PHY-20"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-AZURE-NETWORK-ARCHITECTURE", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.2", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.2", "title": "Supporting utilities", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-PMD"], "domain_ids": ["PHY-15", "PHY-18", "PHY-20"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-AZURE-NETWORK-ARCHITECTURE", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.3", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.3", "title": "Cabling security", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-PLCP", "IEEE-PCS", "IEEE-FEC", "IEEE-PMA", "IEEE-PMD", "IEEE-AN", "IEEE-MDI"], + "domain_ids": ["PHY-01", "PHY-02", "PHY-03", "PHY-04", "PHY-05", "PHY-06", "PHY-07", "PHY-08", "PHY-09", "PHY-10", "PHY-11", "PHY-12", "PHY-13", "PHY-14", "PHY-15", "PHY-16", "PHY-17", "PHY-18"], + "evidence_source_ids": ["MS-SHARED-RESPONSIBILITY", "MS-DATACENTER-SECURITY", "MS-AZURE-NETWORK-ARCHITECTURE", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.4", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.4", "title": "Equipment maintenance", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-PLCP", "IEEE-PCS", "IEEE-FEC", "IEEE-PMA", "IEEE-PMD", "IEEE-AN", "IEEE-MDI"], + "domain_ids": ["PHY-01", "PHY-02", "PHY-03", "PHY-05", "PHY-06", "PHY-07", "PHY-08", "PHY-09", "PHY-10", "PHY-11", "PHY-12", "PHY-14", "PHY-15", "PHY-16", "PHY-17", "PHY-18", "PHY-20", "PHY-21"], + "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-AZURE-NETWORK-ARCHITECTURE", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.5", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.5", "title": "Removal of assets", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-MDI", "IEEE-PMD"], "domain_ids": ["PHY-01", "PHY-02", "PHY-03", "PHY-17", "PHY-21"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.6", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.6", "title": "Security of equipment and assets off-premises", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-PMD", "IEEE-MDI"], "domain_ids": ["PHY-01", "PHY-02", "PHY-03", "PHY-15", "PHY-16", "PHY-17", "PHY-21"], "evidence_source_ids": ["MS-SHARED-RESPONSIBILITY", "MS-DATACENTER-SECURITY", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.7", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.7", "title": "Secure disposal or reuse of equipment", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1", "IEEE-PMD", "IEEE-MDI"], "domain_ids": ["PHY-01", "PHY-02", "PHY-03", "PHY-17", "PHY-21"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.8", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.8", "title": "Unattended user equipment", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-17", "PHY-19", "PHY-21"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS", "MS-ISO-27001-POLICY"] + }, + { + "id": "ISO-A.11.2.9", "framework": "ISO/IEC 27001:2013", "control_id": "A.11.2.9", "title": "Clear desk and clear screen policy", + "responsibility": "Microsoft", "applicability": "APPLICABLE", "verification": "PROVIDER_ASSURANCE", "status": "PROVIDER_ATTESTED", + "sublayer_ids": ["GENERIC-L1"], "domain_ids": ["PHY-19", "PHY-21"], "evidence_source_ids": ["MS-DATACENTER-SECURITY", "MS-PHYSICAL-ACCESS", "MS-ISO-27001-POLICY"] + } + ] +} diff --git a/docs/physical-layer-assurance.md b/docs/physical-layer-assurance.md new file mode 100644 index 0000000..48ffecf --- /dev/null +++ b/docs/physical-layer-assurance.md @@ -0,0 +1,31 @@ +# Physical Layer Assurance + +OpenShield reports Azure public-cloud OSI Layer 1 coverage through provider assurance rather than tenant-side hardware scanning. Microsoft owns and operates the physical datacenter network for Azure IaaS, PaaS, and SaaS. Azure tenants cannot inspect its cables, optics, radios, racks, physical access records, power, or cooling systems. + +## Coverage definition + +The bundled catalog is closed and validated at runtime and in tests. It contains: + +- 21 physical domains covering generic OSI Layer 1 functions, Ethernet and wireless PHY functions, network resilience, facility protection, environmental systems, and equipment lifecycle controls. +- Eight generic and IEEE PHY profiles: generic Layer 1, PLCP, PCS, FEC, PMA, PMD, auto-negotiation and link training, and MDI. +- Microsoft SOC controls PE-1 through PE-8. +- All ISO/IEC 27001:2013 A.11 controls, A.11.1.1 through A.11.1.6 and A.11.2.1 through A.11.2.9. + +Catalog coverage and evidence freshness are separate measurements. A control remains mapped when evidence reaches its review date, but its status changes from `PROVIDER_ATTESTED` to `REVIEW_DUE`. OpenShield never converts provider evidence into a technical scan pass or failure and does not include it in the tenant security score. + +## API + +`GET /api/assurance/physical-layer` requires the same JWT authentication as other API routes. The response includes: + +- scope and shared-responsibility boundaries; +- catalog and evidence coverage summaries; +- all physical domains and their control and sublayer mappings; +- all generic and IEEE physical sublayers; +- all baseline controls with expanded Microsoft evidence; +- explicit limitations preventing the report from being interpreted as live hardware inspection or certification. + +The endpoint performs no external request and requires no paid service. Evidence metadata is stored in `compliance/assurance/physical_layer.json`, making assessments deterministic and reviewable in pull requests. + +## Maintaining evidence + +When Microsoft documentation changes, update the affected evidence entry's URL, `reviewed_at`, and `review_due_at` dates. Do not remove a domain, sublayer, PE control, or ISO A.11 control. The validator intentionally rejects incomplete catalogs, unknown references, insecure evidence URLs, non-Microsoft responsibility assignments, and claims of automated physical verification. diff --git a/tests/test_physical_assurance.py b/tests/test_physical_assurance.py new file mode 100644 index 0000000..bb9fe89 --- /dev/null +++ b/tests/test_physical_assurance.py @@ -0,0 +1,135 @@ +"""Completeness and API tests for Azure Physical-layer provider assurance.""" + +import copy +from datetime import date +from unittest.mock import patch + +import pytest + +from api.services.physical_assurance import ( + EXPECTED_DOMAIN_IDS, + EXPECTED_ISO_CONTROLS, + EXPECTED_MICROSOFT_CONTROLS, + EXPECTED_SUBLAYER_IDS, + CatalogValidationError, + build_report, + load_catalog, + validate_catalog, +) + + +def test_catalog_covers_every_baseline_control_domain_and_sublayer(): + catalog = load_catalog() + + microsoft_controls = { + control["control_id"] for control in catalog["controls"] if control["framework"] == "Microsoft SOC" + } + iso_controls = { + control["control_id"] for control in catalog["controls"] if control["framework"] == "ISO/IEC 27001:2013" + } + covered_domains = {domain_id for control in catalog["controls"] for domain_id in control["domain_ids"]} + covered_sublayers = {sublayer_id for control in catalog["controls"] for sublayer_id in control["sublayer_ids"]} + + assert microsoft_controls == EXPECTED_MICROSOFT_CONTROLS + assert iso_controls == EXPECTED_ISO_CONTROLS + assert covered_domains == EXPECTED_DOMAIN_IDS + assert covered_sublayers == EXPECTED_SUBLAYER_IDS + assert len(catalog["controls"]) == 23 + + +def test_report_expands_bidirectional_domain_and_sublayer_mappings(): + report = build_report(load_catalog(), as_of=date(2026, 8, 8)) + + assert report["summary"]["coverage_percent"] == 100 + assert report["summary"]["covered_controls"] == 23 + assert report["summary"]["covered_domains"] == 21 + assert report["summary"]["covered_sublayers"] == 8 + assert all(domain["control_ids"] for domain in report["domains"]) + assert all(sublayer["control_ids"] for sublayer in report["sublayers"]) + assert all(control["evidence"] for control in report["controls"]) + + +def test_expired_evidence_does_not_become_a_false_scan_failure(): + report = build_report(load_catalog(), as_of=date(2027, 8, 9)) + + assert report["summary"]["coverage_percent"] == 100 + assert report["summary"]["evidence_current_percent"] == 0 + assert report["summary"]["status_counts"]["REVIEW_DUE"] == 23 + assert report["summary"]["status_counts"]["UNKNOWN"] == 0 + assert {control["status"] for control in report["controls"]} == {"REVIEW_DUE"} + + +@pytest.mark.parametrize( + ("collection", "removed_id", "error_match"), + [ + ("domains", "PHY-21", "complete PHY-01 through PHY-21"), + ("sublayers", "IEEE-PMD", "complete generic and IEEE PHY set"), + ("controls", "MS-PE-8", "exactly 23 baseline controls"), + ], +) +def test_catalog_rejects_missing_required_entries(collection, removed_id, error_match): + catalog = copy.deepcopy(load_catalog()) + catalog[collection] = [item for item in catalog[collection] if item["id"] != removed_id] + + with pytest.raises(CatalogValidationError, match=error_match): + validate_catalog(catalog) + + +def test_catalog_rejects_unobservable_control_claimed_as_automated(): + catalog = copy.deepcopy(load_catalog()) + catalog["controls"][0]["verification"] = "AUTOMATED_SCAN" + + with pytest.raises(CatalogValidationError, match="verification must be PROVIDER_ASSURANCE"): + validate_catalog(catalog) + + +def test_catalog_rejects_untrusted_or_insecure_evidence_url(): + catalog = copy.deepcopy(load_catalog()) + catalog["evidence_sources"][0]["url"] = "http://example.com/untrusted" + + with pytest.raises(CatalogValidationError, match="HTTPS on an allowed host"): + validate_catalog(catalog) + + +def test_physical_assurance_endpoint_requires_authentication(client): + response = client.get("/api/assurance/physical-layer") + + assert response.status_code == 401 + + +def test_physical_assurance_endpoint_returns_complete_report(client, auth_headers): + response = client.get("/api/assurance/physical-layer", headers=auth_headers) + + assert response.status_code == 200 + payload = response.get_json() + assert payload["layer"] == { + "assessment_type": "provider_assurance", + "model": "OSI", + "name": "Physical", + "number": 1, + } + assert payload["scope"]["runtime_hardware_observable"] is False + assert payload["summary"]["coverage_percent"] == 100 + assert len(payload["controls"]) == 23 + assert len(payload["domains"]) == 21 + assert len(payload["sublayers"]) == 8 + + +def test_physical_assurance_endpoint_hides_catalog_error_details(client, auth_headers): + marker = "sensitive catalog path" + with patch( + "api.routes.assurance.get_physical_assurance_report", + side_effect=CatalogValidationError(marker), + ): + response = client.get("/api/assurance/physical-layer", headers=auth_headers) + + assert response.status_code == 500 + assert response.get_json() == {"error": "Physical assurance catalog is unavailable"} + assert marker not in response.get_data(as_text=True) + + +def test_physical_assurance_does_not_add_fake_scanner_rules(): + from pathlib import Path + + rules_dir = Path(__file__).parent.parent / "scanner" / "rules" + assert not list(rules_dir.glob("az_phy_*.py"))