Files
tomas.kracmar d3e0769799 release: v4.1.0 — restructure entry points, add CIS baselines, reporting tools and fzf hints
- Restructure launchers: Start-IntuneToolkit.ps1 moves to repo root;
  Start-HeadlessIntune.ps1 moves to Scripts/; TUI helper moves to Scripts/Private/
- Add AGENTS.md with project architecture, entry points, and security notes
- Add CIS M365 baseline assets (CISM365-v7, M365-CIS-Rapid) and reporting scripts
- Add Python reporting utilities (Export-SettingsReport, Export-AssignmentReport,
  Export-ObjectInventoryReport) and CA wizard helpers
- Update Deploy-IntuneBaseline.ps1 with Merge conflict resolution, ReportPath,
  and optimized group loading
- Update Initialize-IntuneAuth.ps1 with -RotateSecret and configurable secret expiry
- Update Extensions for Settings Catalog definition auto-export
- Update README with v4.1.0, new entry points and script catalog
- Bump VERSION to 4.1.0
- Harden .gitignore against .DS_Store, __pycache__, .venv-pdf/, local exports,
  Settings.json and IntuneManagement.log
2026-06-14 15:24:42 +02:00

1105 lines
40 KiB
Python
Executable File

#!/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:
<INDEX>-<TARGET>-<APP/RESOURCE>-<CONTROL>-<SCOPE>
"""
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><scope><seq2digit>-<TARGET>-<APP/RESOURCE>-<CONTROL>
# 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)