d3e0769799
- 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
1105 lines
40 KiB
Python
Executable File
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)
|