#!/usr/bin/env python3 """ Conditional Access Policy Wizard ================================ Interactive TUI for generating a Conditional Access baseline YAML manifest. Run: python3 Scripts/ca-wizard.py Or: ./Scripts/Start-CAWizard.ps1 Uses the structured naming convention: ---- """ import sys import os from dataclasses import dataclass, field from typing import List, Optional, Dict, Any from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.text import Text from rich.prompt import Prompt, Confirm from rich import box try: import yaml except ImportError: yaml = None console = Console() # ===================================================================== # Named location country lists # ===================================================================== EU_EEA_COUNTRIES = [ "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO", ] CZ_SK_COUNTRIES = ["CZ", "SK"] # ===================================================================== # Data model # ===================================================================== @dataclass class ControlOption: label: str value: Any help_text: str = "" @dataclass class Control: area: str prefix: str name: str options: List[ControlOption] default_index: int = 0 selection: int = field(default=0, repr=False) # ===================================================================== # Control definitions # ===================================================================== CONTROLS: List[Control] = [ # --- Tenant policies (CA-0) --- Control( area="Tenant policies", prefix="CA-0", name="Legacy authentication", options=[ ControlOption("Allow", "allow", "No CA policy created"), ControlOption("Block", "block", "Block all legacy auth protocols"), ControlOption("Allow from trusted locations", "block_untrusted", "Block legacy auth except from trusted locations"), ], default_index=1, ), Control( area="Tenant policies", prefix="CA-0", name="Security info registration", options=[ ControlOption("Allow", "allow", "No restriction"), ControlOption("Allow from trusted locations", "trusted_only", "Require managed device or trusted location to register security info"), ], ), Control( area="Tenant policies", prefix="CA-0", name="Unsupported platforms", options=[ ControlOption("Allow", "allow", "No restriction"), ControlOption("Block", "block", "Block unknown/unsupported device platforms"), ], ), Control( area="Tenant policies", prefix="CA-0", name="Device code flow", options=[ ControlOption("Allow", "allow", "No restriction"), ControlOption("Block", "block", "Block device-code authentication flow"), ], default_index=1, ), # --- User policies (CA-1) --- Control( area="User policies", prefix="CA-1", name="Require MFA", options=[ ControlOption("Require", "require", "MFA required for all users"), ControlOption("Only from untrusted locations", "untrusted_only", "MFA required only when not on trusted locations"), ], default_index=0, ), Control( area="User policies", prefix="CA-1", name="BYOD", options=[ ControlOption("Allow", "allow", "No device restriction"), ControlOption("Block", "block", "Require compliant or hybrid-joined device"), ControlOption("Browser-only", "browser_only", "No persistent browser, no mobile/native app access without compliant device"), ], ), Control( area="User policies", prefix="CA-1", name="Trusted locations", options=[ ControlOption("Require", "require", "Block sign-ins from untrusted locations"), ControlOption("Don't require", "dont_require", "No location restriction"), ], ), Control( area="User policies", prefix="CA-1", name="Geofencing", options=[ ControlOption("None", "none", "No geofencing restriction"), ControlOption("EU", "eu", "Restrict to EU/EEA countries only"), ControlOption("EU + UK", "eu_uk", "Restrict to EU/EEA + UK"), ControlOption("Czech + Slovak", "cz_sk", "Restrict to Czech Republic and Slovakia"), ], ), Control( area="User policies", prefix="CA-1", name="Session timeout", options=[ ControlOption("Unlimited", "unlimited", "No session timeout policy"), ControlOption("Normal (168h managed / 8h unmanaged)", "normal", "Relaxed session timeout"), ControlOption("Strict (24h managed / 4h unmanaged)", "strict", "Aggressive session timeout"), ], ), Control( area="User policies", prefix="CA-1", name="Risky sign-ins (Entra ID P2)", options=[ ControlOption("Allow", "allow", "No action"), ControlOption("Require MFA", "require_mfa", "Challenge risky sign-ins with MFA"), ControlOption("Block", "block", "Block medium/high risk sign-ins"), ], ), Control( area="User policies", prefix="CA-1", name="High-risk users (Entra ID P2)", options=[ ControlOption("Allow", "allow", "No action"), ControlOption("Require password change", "pwd_change", "Force password reset for high-risk users"), ControlOption("Require MFA", "require_mfa", "Require MFA for high-risk users"), ControlOption("Block", "block", "Block high-risk users"), ], ), Control( area="User policies", prefix="CA-1", name="Insider risk (Purview)", options=[ ControlOption("Allow", "allow", "No action"), ControlOption("Block", "block", "Block insider-risk flagged sessions"), ], ), # --- Admin policies (CA-2) --- Control( area="Admin policies", prefix="CA-2", name="MFA", options=[ ControlOption("Any MFA", "any_mfa", "Standard MFA for admins"), ControlOption("Phishing-resistant MFA", "phishing_resistant", "FIDO2 / certificate-based MFA"), ], default_index=1, ), Control( area="Admin policies", prefix="CA-2", name="BYOD", options=[ ControlOption("Allow", "allow", "No device restriction for admins"), ControlOption("Block", "block", "Admins must use compliant or hybrid-joined devices"), ], default_index=1, ), Control( area="Admin policies", prefix="CA-2", name="Trusted locations", options=[ ControlOption("Require", "require", "Admins can only sign in from trusted locations"), ControlOption("Don't require", "dont_require", "No location restriction for admins"), ], default_index=0, ), Control( area="Admin policies", prefix="CA-2", name="Session timeout", options=[ ControlOption("Normal (24h managed / 4h unmanaged)", "normal", "Standard session timeout"), ControlOption("Strict (8h managed / 0h unmanaged)", "strict", "Aggressive session timeout"), ControlOption("No persistent session", "no_persistent", "No persistent browser + re-auth every 12h"), ], default_index=2, ), # --- Guest policies (CA-3) --- Control( area="Guest policies", prefix="CA-3", name="MFA", options=[ ControlOption("Require", "require", "Guests must use MFA"), ControlOption("Don't require", "dont_require", "No MFA for guests"), ], default_index=0, ), Control( area="Guest policies", prefix="CA-3", name="Terms of use", options=[ ControlOption("Require", "require", "Guests must accept terms of use"), ControlOption("Don't require", "dont_require", "No ToU for guests"), ], ), Control( area="Admin policies", prefix="CA-2", name="Geofencing", options=[ ControlOption("None", "none", "No geofencing restriction for admins"), ControlOption("EU", "eu", "Admins restricted to EU/EEA"), ControlOption("EU + UK", "eu_uk", "Admins restricted to EU/EEA + UK"), ControlOption("Czech + Slovak", "cz_sk", "Admins restricted to CZ + SK"), ], ), # --- Application policies (CA-4) --- Control( area="Application policies", prefix="CA-4", name="O365 Application restrictions", options=[ ControlOption("Use", "use", "Enforce app-level restrictions for O365"), ControlOption("Don't use", "dont_use", "No app restrictions"), ], ), Control( area="Application policies", prefix="CA-4", name="Azure management", options=[ ControlOption("Allow", "allow", "No extra restriction"), ControlOption("Require MFA", "require_mfa", "Require MFA for Azure portal"), ControlOption("Admin-only", "admin_only", "Only admin roles can access Azure portal"), ], ), Control( area="Application policies", prefix="CA-4", name="MS admin portals", options=[ ControlOption("Allow", "allow", "No extra restriction"), ControlOption("Require MFA", "require_mfa", "Require MFA for admin portals"), ControlOption("Admin-only", "admin_only", "Only admin roles can access admin portals"), ], ), ] # ===================================================================== # Pre-variables # ===================================================================== PRE_VARIABLES: Dict[str, Any] = { "scope": "Prod", "admin_mode": "Roles", "entra_license": "P2", "purview": False, "deploy_mode": "Report-only", "breakglass_group": "CIS-BreakGlass", } # ===================================================================== # Wizard engine # ===================================================================== def clear_screen(): console.clear() def print_header(title: str): console.print(Panel.fit( f"[bold cyan]{title}[/bold cyan]", border_style="cyan", padding=(1, 4), )) console.print() def ask_named_locations() -> Dict[str, Any]: clear_screen() print_header("Named Locations") console.print("[dim]Country-based named locations will be created in Entra ID if they do not exist.[/dim]\n") nl = {} if Confirm.ask("Create EU/EEA named location?", default=False): name = Prompt.ask(" Display name", default="EU") nl["eu"] = {"type": "country", "countries": EU_EEA_COUNTRIES, "display_name": name} if Confirm.ask("Create EU + UK named location?", default=False): name = Prompt.ask(" Display name", default="EU-UK") nl["eu_uk"] = {"type": "country", "countries": EU_EEA_COUNTRIES + ["GB"], "display_name": name} if Confirm.ask("Create Czech + Slovak named location?", default=False): name = Prompt.ask(" Display name", default="CZ-SK") nl["cz_sk"] = {"type": "country", "countries": CZ_SK_COUNTRIES, "display_name": name} if Confirm.ask("Create IP-based trusted location?", default=False): name = Prompt.ask(" Display name", default="Trusted-IP") cidr = Prompt.ask(" Enter CIDR range (e.g. 203.0.113.0/24)", default="10.0.0.0/8") nl["ip"] = {"type": "ip", "cidr": cidr, "display_name": name} return nl def ask_pre_variables() -> Dict[str, Any]: clear_screen() print_header("Pre-Variables") pv = PRE_VARIABLES.copy() console.print("[bold]Deployment settings[/bold]\n") pv["scope"] = Prompt.ask( "Scope", choices=["Test", "Pilot1", "Pilot2", "Pilot3", "Prod"], default=pv["scope"], ) pv["admin_mode"] = Prompt.ask( "Admin targeting mode", choices=["Roles", "Group"], default=pv["admin_mode"], ) if pv["admin_mode"] == "Group": pv["admin_group"] = Prompt.ask("Admin group display name", default="CIS-Admins") pv["entra_license"] = Prompt.ask( "Entra ID License", choices=["P1", "P2"], default=pv["entra_license"], ) pv["purview"] = Confirm.ask( "Enable Purview (Insider Risk) policies?", default=pv["purview"], ) pv["deploy_mode"] = Prompt.ask( "Deploy mode", choices=["Report-only", "Off"], default=pv["deploy_mode"], ) pv["breakglass_group"] = Prompt.ask( "Break-glass group name", default=pv["breakglass_group"], ) pv["prefix"] = Prompt.ask( "Optional policy name prefix", default="", ) console.print() return pv def ask_section(controls: List[Control], section_title: str) -> None: clear_screen() print_header(section_title) for ctrl in controls: table = Table(show_header=False, box=box.SIMPLE, padding=(0, 2)) table.add_column(style="dim") table.add_column() for i, opt in enumerate(ctrl.options, 1): marker = "[green]\u2713[/green]" if i - 1 == ctrl.default_index else " " table.add_row( f" {marker} [{i}]", f"{opt.label} [dim]{opt.help_text}[/dim]" ) console.print(f"[bold]{ctrl.prefix} {ctrl.name}[/bold]") console.print(table) default_val = str(ctrl.default_index + 1) choice = Prompt.ask( "Select option", choices=[str(i) for i in range(1, len(ctrl.options) + 1)], default=default_val, ) ctrl.selection = int(choice) - 1 console.print() def review_selections(controls: List[Control], pv: Dict[str, Any]) -> bool: clear_screen() print_header("Review & Generate") nl = pv.get("named_locations", {}) if nl: console.print("[bold]Named locations[/bold]") for nl_name in nl: console.print(f" [green]{nl_name}[/green]") console.print() table = Table(title="Selected Policies", box=box.ROUNDED) table.add_column("Area", style="cyan", no_wrap=True) table.add_column("Control", style="bold") table.add_column("Selection", style="green") for ctrl in controls: selected = ctrl.options[ctrl.selection] table.add_row(ctrl.area, ctrl.name, selected.label) console.print(table) console.print() console.print("[bold]Pre-variables[/bold]") for k, v in pv.items(): if k == "named_locations": continue if v: console.print(f" {k}: [green]{v}[/green]") console.print() return Confirm.ask("Generate baseline YAML?", default=True) # ===================================================================== # YAML generator # ===================================================================== def generate_yaml(controls: List[Control], pv: Dict[str, Any]) -> str: """Translate wizard selections into a YAML baseline manifest.""" # Naming: CA--- # Area: 0=Threat/Tenant, 1=User, 2=Admin, 3=Guest, 4=Application # Scope: 0=Test, 1=Pilot1, 2=Pilot2, 3=Pilot3, 9=Prod area_digit = {"User": "1", "Guest": "3", "Application": "4", "Admin": "2", "Threat": "0"} scope_digit = {"Test": "0", "Pilot1": "1", "Pilot2": "2", "Pilot3": "3", "Prod": "9"} next_seq = {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1} prefix = pv.get("prefix", "") scope = pv["scope"] report_only = pv["deploy_mode"] == "Report-only" breakglass = pv["breakglass_group"] has_p2 = pv["entra_license"] == "P2" named_locations = pv.get("named_locations", {}) # Helper: build structured name def struct_name(cat: str, target: str, appres: str, control: str) -> str: a = area_digit[cat] s = scope_digit[scope] seq = next_seq[a] next_seq[a] += 1 idx = f"{a}{s}{seq:02d}" name = f"CA{idx}-{target}-{appres}-{control}" if prefix: name = f"{prefix}{name}" return name state = "enabledForReportingButNotEnforced" if report_only else "enabled" # Admin role list admin_roles = [ "Global Administrator", "Privileged Role Administrator", "Security Administrator", "Exchange Administrator", "SharePoint Administrator", "Conditional Access Administrator", "Application Administrator", "Cloud Application Administrator", "User Administrator", "Helpdesk Administrator", "Billing Administrator", "Authentication Administrator", "Password Administrator", ] # Admin portal app IDs admin_portal_apps = [ "797f4846-ba00-4fd7-ba43-dac1f8f63013", "c44b4083-3bb0-49c1-b47d-974e53cbdf3c", "1b730954-1685-4b74-9bfd-dac224a7b894", "00000003-0000-0ff1-ce00-000000000000", "00000003-0000-0000-c000-000000000000", "de8bc8b5-d9f9-48b1-a8ad-b748da725064", "00000002-0000-0ff1-ce00-000000000000", "66a88757-258c-4c72-893c-3e8bed4d6899", ] policies = [] nl_yaml = [] # Build geo name map and named locations YAML geo_name_map = {} for nl_key, nl_def in named_locations.items(): display_name = nl_def.get("display_name", nl_key) geo_name_map[nl_key] = display_name if nl_def["type"] == "country": nl_yaml.append({ "displayName": display_name, "type": "country", "countriesAndRegions": nl_def["countries"], "includeUnknownCountriesAndRegions": False, }) elif nl_def["type"] == "ip": nl_yaml.append({ "displayName": display_name, "type": "ip", "isTrusted": True, "ipRanges": [nl_def["cidr"]], }) def add_policy(name: str, desc: str, conditions: dict, grant: dict, session: Optional[dict] = None): p = { "name": name, "description": desc, "state": state, "conditions": conditions, "grantControls": grant, } if session: p["sessionControls"] = session policies.append(p) # ---- Map selections to policies ---- # Lookup helper sel = {} for c in controls: sel[c.name] = c.options[c.selection].value # CA-0: Legacy authentication if sel.get("Legacy authentication") == "block": add_policy( struct_name("Threat", "AllUsers", "AllApps", "BlockLegacyAuth"), "Block all legacy authentication protocols", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "clientAppTypes": ["exchangeActiveSync", "other"], }, {"builtInControls": ["block"], "operator": "OR"}, ) elif sel.get("Legacy authentication") == "block_untrusted": add_policy( struct_name("Threat", "AllUsers", "AllApps", "BlockLegacyAuthUntrusted"), "Block legacy authentication from untrusted locations", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "clientAppTypes": ["exchangeActiveSync", "other"], "locations": {"includeLocations": ["All"], "excludeLocations": ["AllTrusted"]}, }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-0: Security info registration if sel.get("Security info registration") == "trusted_only": add_policy( struct_name("User", "AllUsers", "SecurityInfo", "RequireTrustedLocation"), "Require trusted location or managed device to register security info", { "applications": {"includeUserActions": ["urn:user:registersecurityinfo"]}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["compliantDevice", "domainJoinedDevice"], "operator": "OR"}, ) # CA-0: Unsupported platforms if sel.get("Unsupported platforms") == "block": add_policy( struct_name("Threat", "AllUsers", "AllApps", "BlockUnsupportedPlatforms"), "Block sign-ins from unknown or unsupported device platforms", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "platforms": { "includePlatforms": ["all"], "excludePlatforms": ["android", "iOS", "windows", "macOS"], }, }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-0: Device code flow if sel.get("Device code flow") == "block": add_policy( struct_name("Threat", "AllUsers", "AllApps", "BlockDeviceCodeFlow"), "Block device-code authentication flow", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "authenticationFlows": {"deviceCodeFlow": {"isEnabled": True}}, }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-1: Require MFA if sel.get("Require MFA") == "require": add_policy( struct_name("User", "AllUsers", "AllApps", "RequireMFA"), "Require multi-factor authentication for all users", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["mfa"], "operator": "OR"}, ) elif sel.get("Require MFA") == "untrusted_only": add_policy( struct_name("User", "AllUsers", "AllApps", "RequireMFAUntrusted"), "Require MFA only from untrusted locations", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "locations": {"includeLocations": ["All"], "excludeLocations": ["AllTrusted"]}, }, {"builtInControls": ["mfa"], "operator": "OR"}, ) # CA-1: BYOD if sel.get("BYOD") == "block": add_policy( struct_name("User", "AllUsers", "AllApps", "RequireCompliantDevice"), "Require compliant or hybrid-joined device for all users", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["compliantDevice", "domainJoinedDevice"], "operator": "OR"}, ) elif sel.get("BYOD") == "browser_only": add_policy( struct_name("User", "AllUsers", "AllApps", "NoPersistentBrowser"), "No persistent browser sessions; require compliant device for native apps", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["compliantDevice", "domainJoinedDevice"], "operator": "OR"}, {"persistentBrowser": {"mode": "never", "isEnabled": True}}, ) # CA-1: Trusted locations if sel.get("Trusted locations") == "require": add_policy( struct_name("User", "AllUsers", "AllApps", "BlockUntrustedLocations"), "Block sign-ins from untrusted locations", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "locations": {"includeLocations": ["All"], "excludeLocations": ["AllTrusted"]}, }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-1: Geofencing user_geo = sel.get("Geofencing", "none") loc_name = geo_name_map.get(user_geo, user_geo) if user_geo != "none" and user_geo in named_locations: add_policy( struct_name("User", "AllUsers", "AllApps", "Geofencing"), f"Restrict user sign-ins to {loc_name} only", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "locations": {"includeLocations": [loc_name]}, }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-1: Session timeout session_opt = sel.get("Session timeout", "unlimited") if session_opt == "normal": add_policy( struct_name("User", "AllUsers", "AllApps", "SessionNormal"), "Normal session timeout (168h managed / 8h unmanaged)", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["mfa"], "operator": "OR"}, {"signInFrequency": {"value": 8, "type": "hours", "isEnabled": True}}, ) elif session_opt == "strict": add_policy( struct_name("User", "AllUsers", "AllApps", "SessionStrict"), "Strict session timeout (24h managed / 4h unmanaged)", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["mfa"], "operator": "OR"}, {"signInFrequency": {"value": 4, "type": "hours", "isEnabled": True}}, ) # CA-1: Risky sign-ins (P2 only) risky = sel.get("Risky sign-ins (Entra ID P2)", "allow") if has_p2 and risky == "require_mfa": add_policy( struct_name("Threat", "AllUsers", "AllApps", "RequireMFAForRiskySignIns"), "Require MFA for medium/high risk sign-ins", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "signInRiskLevels": ["medium", "high"], }, {"builtInControls": ["mfa"], "operator": "OR"}, ) elif has_p2 and risky == "block": add_policy( struct_name("Threat", "AllUsers", "AllApps", "BlockRiskySignIns"), "Block medium/high risk sign-ins", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "signInRiskLevels": ["medium", "high"], }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-1: High-risk users (P2 only) hr_users = sel.get("High-risk users (Entra ID P2)", "allow") if has_p2 and hr_users == "pwd_change": add_policy( struct_name("Threat", "AllUsers", "AllApps", "ForcePasswordChangeHighRiskUsers"), "Force password change for high-risk users", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "userRiskLevels": ["high"], }, {"builtInControls": ["passwordChange"], "operator": "OR"}, ) elif has_p2 and hr_users == "require_mfa": add_policy( struct_name("Threat", "AllUsers", "AllApps", "RequireMFAHighRiskUsers"), "Require MFA for high-risk users", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "userRiskLevels": ["high"], }, {"builtInControls": ["mfa"], "operator": "OR"}, ) elif has_p2 and hr_users == "block": add_policy( struct_name("Threat", "AllUsers", "AllApps", "BlockHighRiskUsers"), "Block high-risk users", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "userRiskLevels": ["high"], }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-1: Insider risk if sel.get("Insider risk (Purview)") == "block": add_policy( struct_name("Threat", "AllUsers", "AllApps", "BlockInsiderRisk"), "Block sessions flagged by Purview Insider Risk", { "applications": {"includeApplications": ["All"]}, "users": {"includeUsers": ["All"]}, "insiderRiskLevels": ["elevated"], }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-2: Admin MFA admin_mfa = sel.get("MFA", "any_mfa") if admin_mfa == "any_mfa": add_policy( struct_name("Admin", "Admins", "AllApps", "RequireMFA"), "Require MFA for all administrative roles", { "applications": {"includeApplications": ["All"]}, "users": {"includeRoles": admin_roles}, }, {"builtInControls": ["mfa"], "operator": "OR"}, ) elif admin_mfa == "phishing_resistant": add_policy( struct_name("Admin", "Admins", "AllApps", "RequirePhishingResistantMFA"), "Require phishing-resistant MFA for administrative roles", { "applications": {"includeApplications": ["All"]}, "users": {"includeRoles": admin_roles}, }, { "builtInControls": ["authenticationStrength"], "authenticationStrength": {"id": "00000000-0000-0000-0000-000000000004"}, "operator": "OR", }, ) # CA-2: Admin BYOD if sel.get("BYOD") == "block": add_policy( struct_name("Admin", "Admins", "AllApps", "RequireCompliantDevice"), "Administrators must use compliant or hybrid-joined devices", { "applications": {"includeApplications": ["All"]}, "users": {"includeRoles": admin_roles}, }, {"builtInControls": ["compliantDevice", "domainJoinedDevice"], "operator": "OR"}, ) # CA-2: Admin trusted locations if sel.get("Trusted locations") == "require": add_policy( struct_name("Admin", "Admins", "AllApps", "BlockUntrustedLocations"), "Administrators can only sign in from trusted locations", { "applications": {"includeApplications": ["All"]}, "users": {"includeRoles": admin_roles}, "locations": {"includeLocations": ["All"], "excludeLocations": ["AllTrusted"]}, }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-2: Admin geofencing admin_geo = sel.get("Geofencing", "none") loc_name = geo_name_map.get(admin_geo, admin_geo) if admin_geo != "none" and admin_geo in named_locations: add_policy( struct_name("Admin", "Admins", "AllApps", "Geofencing"), f"Restrict admin sign-ins to {loc_name} only", { "applications": {"includeApplications": ["All"]}, "users": {"includeRoles": admin_roles}, "locations": {"includeLocations": [loc_name]}, }, {"builtInControls": ["block"], "operator": "OR"}, ) # CA-2: Admin session timeout admin_session = sel.get("Session timeout", "no_persistent") if admin_session == "normal": add_policy( struct_name("Admin", "Admins", "AllApps", "SessionNormal"), "Admin session timeout: 24h managed / 4h unmanaged", { "applications": {"includeApplications": ["All"]}, "users": {"includeRoles": admin_roles}, }, {"builtInControls": ["mfa"], "operator": "OR"}, {"signInFrequency": {"value": 4, "type": "hours", "isEnabled": True}}, ) elif admin_session == "strict": add_policy( struct_name("Admin", "Admins", "AllApps", "SessionStrict"), "Admin session timeout: 8h managed / 0h unmanaged", { "applications": {"includeApplications": ["All"]}, "users": {"includeRoles": admin_roles}, }, {"builtInControls": ["mfa"], "operator": "OR"}, {"signInFrequency": {"value": 8, "type": "hours", "isEnabled": True}}, ) elif admin_session == "no_persistent": add_policy( struct_name("Admin", "Admins", "AllApps", "NoPersistentSession"), "No persistent browser sessions for admins; re-auth every 12h", { "applications": {"includeApplications": ["All"]}, "users": {"includeRoles": admin_roles}, }, {"builtInControls": ["mfa"], "operator": "OR"}, { "signInFrequency": {"value": 12, "type": "hours", "isEnabled": True}, "persistentBrowser": {"mode": "never", "isEnabled": True}, }, ) # CA-3: Guest MFA if sel.get("MFA") == "require": add_policy( struct_name("Guest", "Guests", "AllApps", "RequireMFA"), "Require MFA for guest and external users", { "applications": {"includeApplications": ["All"]}, "users": { "includeGuestsOrExternalUsers": { "guestTypes": ["internalGuest", "b2bCollaborationGuest", "b2bCollaborationMember", "b2bDirectConnectUser"], "externalTenants": {"membershipKind": "all"}, } }, }, {"builtInControls": ["mfa"], "operator": "OR"}, ) # CA-3: Guest Terms of Use if sel.get("Terms of use") == "require": add_policy( struct_name("Guest", "Guests", "AllApps", "RequireTermsOfUse"), "Require guests to accept terms of use", { "applications": {"includeApplications": ["All"]}, "users": { "includeGuestsOrExternalUsers": { "guestTypes": ["internalGuest", "b2bCollaborationGuest", "b2bCollaborationMember", "b2bDirectConnectUser"], "externalTenants": {"membershipKind": "all"}, } }, }, {"builtInControls": ["termsOfUse"], "operator": "OR"}, ) # CA-4: O365 App restrictions if sel.get("O365 Application restrictions") == "use": add_policy( struct_name("Application", "AllUsers", "O365", "AppEnforcedRestrictions"), "Enforce application restrictions for Office 365", { "applications": {"includeApplications": ["Office365"]}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["mfa"], "operator": "OR"}, {"applicationEnforcedRestrictions": {"isEnabled": True}}, ) # CA-4: Azure management azure_mgmt = sel.get("Azure management", "allow") if azure_mgmt == "require_mfa": add_policy( struct_name("Application", "AllUsers", "AzureMgmt", "RequireMFA"), "Require MFA for Azure management portal", { "applications": {"includeApplications": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"]}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["mfa"], "operator": "OR"}, ) elif azure_mgmt == "admin_only": add_policy( struct_name("Application", "Admins", "AzureMgmt", "AdminOnly"), "Restrict Azure management portal to admin roles only", { "applications": {"includeApplications": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"]}, "users": {"includeRoles": admin_roles}, }, {"builtInControls": ["mfa"], "operator": "OR"}, ) # CA-4: MS admin portals ms_portals = sel.get("MS admin portals", "allow") if ms_portals == "require_mfa": add_policy( struct_name("Application", "AllUsers", "AdminPortals", "RequireMFA"), "Require MFA for Microsoft admin portals", { "applications": {"includeApplications": admin_portal_apps}, "users": {"includeUsers": ["All"]}, }, {"builtInControls": ["mfa"], "operator": "OR"}, ) elif ms_portals == "admin_only": add_policy( struct_name("Application", "Admins", "AdminPortals", "AdminOnly"), "Restrict Microsoft admin portals to admin roles only", { "applications": {"includeApplications": admin_portal_apps}, "users": {"includeRoles": admin_roles}, }, {"builtInControls": ["mfa"], "operator": "OR"}, ) # Build YAML root ca_section = { "reportOnly": report_only, "breakGlassGroup": breakglass, } if nl_yaml: ca_section["namedLocations"] = nl_yaml ca_section["policies"] = policies baseline = { "baseline": { "name": "Generated-ConditionalAccess-Baseline", "conflictResolution": "Skip", "whatIf": False, "tenantConfig": { "conditionalAccess": ca_section, }, } } # Use PyYAML if available, else manual fallback if yaml: return yaml.dump(baseline, default_flow_style=False, sort_keys=False, allow_unicode=True) # Minimal manual YAML fallback lines = ["baseline:"] lines.append(" name: Generated-ConditionalAccess-Baseline") lines.append(" conflictResolution: Skip") lines.append(" whatIf: false") lines.append(" tenantConfig:") lines.append(" conditionalAccess:") lines.append(f" reportOnly: {str(report_only).lower()}") lines.append(f" breakGlassGroup: {breakglass}") lines.append(" policies:") for p in policies: lines.append(" - name: " + p["name"]) lines.append(" description: " + p["description"]) lines.append(" state: " + p["state"]) # conditions, grantControls etc. would need full recursion here... return "\n".join(lines) # ===================================================================== # Main # ===================================================================== def main(): console.print(Panel.fit( "[bold blue]Conditional Access Policy Wizard[/bold blue]\n" "[dim]Generate a structured CA baseline from interactive choices[/dim]", border_style="blue", padding=(1, 4), )) console.print() # Pre-variables pv = ask_pre_variables() # Named locations nl = ask_named_locations() pv["named_locations"] = nl # Sections areas = {} for c in CONTROLS: areas.setdefault(c.area, []).append(c) for area_name, controls in areas.items(): ask_section(controls, area_name) # Review if not review_selections(CONTROLS, pv): console.print("[yellow]Generation cancelled.[/yellow]") sys.exit(0) # Generate yaml_text = generate_yaml(CONTROLS, pv) # Write file default_path = "./Baselines/CA-Wizard-Generated.yaml" out_path = Prompt.ask("Output file path", default=default_path) out_dir = os.path.dirname(out_path) if out_dir and not os.path.exists(out_dir): os.makedirs(out_dir, exist_ok=True) with open(out_path, "w", encoding="utf-8") as f: f.write(yaml_text) console.print() console.print(f"[bold green]Baseline written to:[/bold green] {os.path.abspath(out_path)}") console.print(f"[dim]Policies generated:[/dim] {len([p for p in yaml_text.split(chr(10)) if 'name:' in p and 'baseline' not in p])}") console.print() console.print("[cyan]Deploy with:[/cyan]") console.print(f" ./Scripts/Deploy-CISM365Baseline.ps1 -BaselinePath '{out_path}' -Mode Assess") console.print(f" ./Scripts/Deploy-CISM365Baseline.ps1 -BaselinePath '{out_path}' -Mode Deploy -Apply") if __name__ == "__main__": try: main() except KeyboardInterrupt: console.print("\n[yellow]Wizard interrupted. Exiting.[/yellow]") sys.exit(1)