Files
resolutionflow/backend/scripts/seed_procedural_flows.py
Michael Chihlas 1b86f66954 feat: add --api-url flag to procedural flows seed script
Allows running against remote environments like Railway.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:48:55 -05:00

1005 lines
57 KiB
Python

#!/usr/bin/env python3
"""
Procedural Flow Seed Script for ResolutionFlow.
Creates sample procedural flows (step-by-step project templates) with intake forms
for common MSP project work.
Run from the backend directory with: python -m scripts.seed_procedural_flows
Requirements:
- Backend server must be running (uvicorn app.main:app)
"""
import asyncio
import argparse
import httpx
from typing import Any
# API Configuration
API_BASE_URL = "http://localhost:8000/api/v1"
ADMIN_EMAIL = None
ADMIN_PASSWORD = None
# =============================================================================
# PROCEDURAL FLOW DEFINITIONS
# =============================================================================
def get_domain_controller_flow() -> dict[str, Any]:
"""Domain Controller & Active Directory Setup — full build procedure."""
return {
"name": "Domain Controller & Active Directory Setup",
"description": "Complete procedure for building a new domain controller, installing Active Directory Domain Services, configuring DNS, and verifying replication. Includes intake form for server and domain details.",
"tree_type": "procedural",
"category": "Projects - Infrastructure",
"tags": ["active-directory", "dns", "domain-controller", "server-build"],
"intake_form": [
{
"variable_name": "server_name",
"label": "Server Name",
"field_type": "text",
"required": True,
"placeholder": "e.g. DC01, YOURCOMPANY-DC02",
"help_text": "NetBIOS name of the new domain controller (max 15 characters)",
"group_name": "Server Details",
"display_order": 1,
},
{
"variable_name": "server_ip",
"label": "Server IP Address",
"field_type": "ip_address",
"required": True,
"placeholder": "e.g. 10.0.1.10",
"help_text": "Static IP address for this server",
"group_name": "Server Details",
"display_order": 2,
},
{
"variable_name": "subnet_mask",
"label": "Subnet Mask",
"field_type": "text",
"required": True,
"placeholder": "e.g. 255.255.255.0",
"default_value": "255.255.255.0",
"group_name": "Server Details",
"display_order": 3,
},
{
"variable_name": "default_gateway",
"label": "Default Gateway",
"field_type": "ip_address",
"required": True,
"placeholder": "e.g. 10.0.1.1",
"group_name": "Server Details",
"display_order": 4,
},
{
"variable_name": "domain_name",
"label": "Domain Name (FQDN)",
"field_type": "text",
"required": True,
"placeholder": "e.g. corp.contoso.com",
"help_text": "Fully qualified domain name",
"group_name": "Domain Configuration",
"display_order": 5,
},
{
"variable_name": "netbios_name",
"label": "NetBIOS Domain Name",
"field_type": "text",
"required": True,
"placeholder": "e.g. CONTOSO",
"help_text": "Short domain name (auto-derived from FQDN if unsure)",
"group_name": "Domain Configuration",
"display_order": 6,
},
{
"variable_name": "is_first_dc",
"label": "Is this the first DC in the domain?",
"field_type": "select",
"required": True,
"options": ["Yes — new forest/domain", "No — additional DC in existing domain"],
"help_text": "Determines whether to create a new forest or join an existing one",
"group_name": "Domain Configuration",
"display_order": 7,
},
{
"variable_name": "existing_dc_ip",
"label": "Existing DC IP (if adding to existing domain)",
"field_type": "ip_address",
"required": False,
"placeholder": "e.g. 10.0.1.5",
"help_text": "IP of an existing domain controller for replication (leave blank for first DC)",
"group_name": "Domain Configuration",
"display_order": 8,
},
{
"variable_name": "dsrm_password",
"label": "DSRM Password",
"field_type": "password",
"required": True,
"help_text": "Directory Services Restore Mode password — store securely in password vault",
"group_name": "Security",
"display_order": 9,
},
{
"variable_name": "server_roles",
"label": "Additional Server Roles",
"field_type": "multi_select",
"required": False,
"options": ["DHCP Server", "Certificate Services (CA)", "DFS Namespaces", "Windows Server Backup", "WSUS"],
"help_text": "Optional roles to install alongside AD DS",
"group_name": "Additional Configuration",
"display_order": 10,
},
{
"variable_name": "client_name",
"label": "Client / Company Name",
"field_type": "text",
"required": True,
"placeholder": "e.g. Contoso Ltd",
"group_name": "Project Info",
"display_order": 11,
},
{
"variable_name": "ticket_number",
"label": "Ticket / Project Number",
"field_type": "text",
"required": False,
"placeholder": "e.g. PRJ-2024-0042",
"group_name": "Project Info",
"display_order": 12,
},
],
"tree_structure": {
"steps": [
{
"id": "step_1",
"type": "procedure_step",
"title": "Verify Prerequisites",
"content_type": "verification",
"description": "Before beginning, verify the following prerequisites are met for **[VAR:client_name]** project **[VAR:ticket_number]**:\n\n- Windows Server 2022 (or 2019) is installed and activated on **[VAR:server_name]**\n- Server has network connectivity\n- You have local administrator credentials\n- The IP address **[VAR:server_ip]** is not in use (ping test)\n- DNS is planned — if this is a new domain, the DC will be its own DNS server",
"verification": {
"type": "checkbox",
"prompt": "All prerequisites verified?"
},
},
{
"id": "step_2",
"type": "procedure_step",
"title": "Configure Static IP Address",
"content_type": "action",
"description": "Set the static IP address on the primary network adapter of **[VAR:server_name]**.",
"commands": [
{
"language": "powershell",
"code": "# Get the primary network adapter\n$adapter = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | Select-Object -First 1\n\n# Remove existing IP configuration\nRemove-NetIPAddress -InterfaceIndex $adapter.ifIndex -Confirm:$false -ErrorAction SilentlyContinue\nRemove-NetRoute -InterfaceIndex $adapter.ifIndex -Confirm:$false -ErrorAction SilentlyContinue\n\n# Set static IP\nNew-NetIPAddress -InterfaceIndex $adapter.ifIndex `\n -IPAddress \"[VAR:server_ip]\" `\n -PrefixLength 24 `\n -DefaultGateway \"[VAR:default_gateway]\"\n\n# Set DNS — point to self (will be DNS server after AD install)\nSet-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex `\n -ServerAddresses \"[VAR:server_ip]\",\"[VAR:default_gateway]\"",
"label": "Set Static IP via PowerShell"
}
],
"expected_outcome": "Server should have IP **[VAR:server_ip]** with gateway **[VAR:default_gateway]**. Verify with `ipconfig /all`.",
},
{
"id": "step_3",
"type": "procedure_step",
"title": "Rename Server",
"content_type": "action",
"description": "Rename the server to **[VAR:server_name]** and reboot.",
"commands": [
{
"language": "powershell",
"code": "# Rename the computer\nRename-Computer -NewName \"[VAR:server_name]\" -Force -Restart",
"label": "Rename and restart"
}
],
"expected_outcome": "Server reboots with hostname **[VAR:server_name]**. Wait for reboot to complete before proceeding.",
"verification": {
"type": "checkbox",
"prompt": "Server has rebooted and hostname is correct?"
},
},
{
"id": "step_4",
"type": "procedure_step",
"title": "Install AD DS Role",
"content_type": "action",
"description": "Install the Active Directory Domain Services role and management tools on **[VAR:server_name]**.",
"commands": [
{
"language": "powershell",
"code": "# Install AD DS role with management tools\nInstall-WindowsFeature AD-Domain-Services -IncludeManagementTools -Verbose",
"label": "Install AD DS"
}
],
"expected_outcome": "Feature installation completes successfully. No reboot required at this stage.",
},
{
"id": "step_5",
"type": "procedure_step",
"title": "Promote to Domain Controller",
"content_type": "action",
"description": "Promote **[VAR:server_name]** to a domain controller for **[VAR:domain_name]**.\n\n**Important:** The DSRM password should be stored securely in your password vault immediately after this step.",
"commands": [
{
"language": "powershell",
"code": "# For a NEW forest/domain:\nInstall-ADDSForest `\n -DomainName \"[VAR:domain_name]\" `\n -DomainNetBIOSName \"[VAR:netbios_name]\" `\n -ForestMode \"WinThreshold\" `\n -DomainMode \"WinThreshold\" `\n -InstallDns:$true `\n -DatabasePath \"C:\\Windows\\NTDS\" `\n -LogPath \"C:\\Windows\\NTDS\" `\n -SysvolPath \"C:\\Windows\\SYSVOL\" `\n -SafeModeAdministratorPassword (Read-Host -AsSecureString \"DSRM Password\") `\n -Force:$true",
"label": "Promote — New Forest"
},
{
"language": "powershell",
"code": "# For ADDITIONAL DC in existing domain:\nInstall-ADDSDomainController `\n -DomainName \"[VAR:domain_name]\" `\n -InstallDns:$true `\n -DatabasePath \"C:\\Windows\\NTDS\" `\n -LogPath \"C:\\Windows\\NTDS\" `\n -SysvolPath \"C:\\Windows\\SYSVOL\" `\n -SafeModeAdministratorPassword (Read-Host -AsSecureString \"DSRM Password\") `\n -Force:$true",
"label": "Promote — Additional DC"
}
],
"expected_outcome": "Server reboots automatically after promotion. This may take 5-10 minutes. After reboot, log in with **[VAR:netbios_name]\\Administrator**.",
},
{
"id": "step_6",
"type": "procedure_step",
"title": "Verify AD DS Installation",
"content_type": "verification",
"description": "After reboot, verify Active Directory and DNS are functioning correctly on **[VAR:server_name]**.",
"commands": [
{
"language": "powershell",
"code": "# Verify AD DS\nGet-ADDomainController -Filter *\nGet-ADDomain\nGet-ADForest\n\n# Verify DNS\nGet-DnsServerZone\nResolve-DnsName [VAR:domain_name]\n\n# Check SYSVOL and NETLOGON shares\nGet-SmbShare | Where-Object { $_.Name -in 'SYSVOL','NETLOGON' }\n\n# Check DCDIAG\ndcdiag /s:[VAR:server_name]",
"label": "AD & DNS verification"
}
],
"expected_outcome": "All DCDIAG tests should pass. SYSVOL and NETLOGON shares should be present. DNS zone for **[VAR:domain_name]** should exist.",
"verification": {
"type": "checkbox",
"prompt": "DCDIAG passed, SYSVOL/NETLOGON shares present, DNS zone exists?"
},
},
{
"id": "step_7",
"type": "procedure_step",
"title": "Configure DNS Forwarders",
"content_type": "action",
"description": "Configure DNS forwarders so **[VAR:server_name]** can resolve external domains.",
"commands": [
{
"language": "powershell",
"code": "# Set DNS forwarders (Cloudflare + Google as fallback)\nSet-DnsServerForwarder -IPAddress \"1.1.1.1\",\"8.8.8.8\" -PassThru\n\n# Verify\nGet-DnsServerForwarder\n\n# Test external resolution\nResolve-DnsName google.com",
"label": "Configure forwarders"
}
],
"expected_outcome": "External DNS resolution works. `Resolve-DnsName google.com` should return an IP address.",
},
{
"id": "step_8",
"type": "procedure_step",
"title": "Create OU Structure",
"content_type": "action",
"description": "Create the organizational unit structure for **[VAR:client_name]**.",
"commands": [
{
"language": "powershell",
"code": "$domain = (Get-ADDomain).DistinguishedName\n\n# Create top-level OUs\n$ous = @(\n \"OU=Company Users,$domain\",\n \"OU=Company Computers,$domain\",\n \"OU=Company Groups,$domain\",\n \"OU=Company Servers,$domain\",\n \"OU=Service Accounts,$domain\",\n \"OU=Disabled Accounts,$domain\"\n)\n\nforeach ($ou in $ous) {\n $name = ($ou -split ',')[0] -replace 'OU=',''\n try {\n New-ADOrganizationalUnit -Name $name -Path $domain -ProtectedFromAccidentalDeletion $true\n Write-Host \"Created: $name\" -ForegroundColor Green\n } catch {\n Write-Host \"Already exists: $name\" -ForegroundColor Yellow\n }\n}",
"label": "Create OUs"
}
],
"expected_outcome": "OU structure visible in Active Directory Users and Computers.",
},
{
"id": "step_9",
"type": "procedure_step",
"title": "Configure Group Policy Defaults",
"content_type": "action",
"description": "Configure essential Group Policy settings for the new domain.",
"commands": [
{
"language": "powershell",
"code": "# Set password policy\nSet-ADDefaultDomainPasswordPolicy -Identity \"[VAR:domain_name]\" `\n -LockoutThreshold 5 `\n -LockoutDuration \"00:30:00\" `\n -LockoutObservationWindow \"00:30:00\" `\n -MaxPasswordAge \"90.00:00:00\" `\n -MinPasswordAge \"1.00:00:00\" `\n -MinPasswordLength 12 `\n -PasswordHistoryCount 10 `\n -ComplexityEnabled $true\n\n# Verify\nGet-ADDefaultDomainPasswordPolicy",
"label": "Set password policy"
}
],
"expected_outcome": "Password policy configured: 12 char minimum, complexity enabled, 5 lockout threshold, 90-day max age.",
},
{
"id": "step_10",
"type": "procedure_step",
"title": "Document and Update Password Vault",
"content_type": "informational",
"description": "Document the following in the client's IT documentation and password vault:\n\n| Item | Value |\n|------|-------|\n| Server Name | **[VAR:server_name]** |\n| IP Address | **[VAR:server_ip]** |\n| Domain | **[VAR:domain_name]** |\n| NetBIOS | **[VAR:netbios_name]** |\n| DSRM Password | *(stored in vault)* |\n| DNS Forwarders | 1.1.1.1, 8.8.8.8 |\n\nUpdate the client's network diagram and runbook with the new DC information.",
"verification": {
"type": "checkbox",
"prompt": "Documentation and password vault updated?"
},
},
{
"id": "step_end",
"type": "procedure_end",
"title": "Domain Controller Setup Complete",
"description": "**[VAR:server_name]** has been configured as a domain controller for **[VAR:domain_name]** at **[VAR:client_name]**.\n\n**Next steps to consider:**\n- Configure DHCP if selected as an additional role\n- Set up Azure AD Connect for hybrid identity\n- Configure backup solution for System State\n- Schedule a replication health check in 24 hours",
},
]
},
}
def get_m365_user_onboarding_flow() -> dict[str, Any]:
"""Microsoft 365 User Onboarding — new hire setup procedure."""
return {
"name": "Microsoft 365 User Onboarding",
"description": "Step-by-step procedure for creating a new Microsoft 365 user account, assigning licenses, configuring email, adding to security groups, and setting up MFA. Includes intake form for new user details.",
"tree_type": "procedural",
"category": "Projects - Microsoft 365",
"tags": ["m365", "onboarding", "user-setup", "exchange-online"],
"intake_form": [
{
"variable_name": "first_name",
"label": "First Name",
"field_type": "text",
"required": True,
"placeholder": "e.g. Jane",
"group_name": "User Information",
"display_order": 1,
},
{
"variable_name": "last_name",
"label": "Last Name",
"field_type": "text",
"required": True,
"placeholder": "e.g. Smith",
"group_name": "User Information",
"display_order": 2,
},
{
"variable_name": "display_name",
"label": "Display Name",
"field_type": "text",
"required": True,
"placeholder": "e.g. Jane Smith",
"group_name": "User Information",
"display_order": 3,
},
{
"variable_name": "job_title",
"label": "Job Title",
"field_type": "text",
"required": True,
"placeholder": "e.g. Marketing Manager",
"group_name": "User Information",
"display_order": 4,
},
{
"variable_name": "department",
"label": "Department",
"field_type": "text",
"required": True,
"placeholder": "e.g. Marketing",
"group_name": "User Information",
"display_order": 5,
},
{
"variable_name": "manager_email",
"label": "Manager's Email",
"field_type": "email",
"required": False,
"placeholder": "e.g. john.doe@company.com",
"group_name": "User Information",
"display_order": 6,
},
{
"variable_name": "email_address",
"label": "Email Address",
"field_type": "email",
"required": True,
"placeholder": "e.g. jane.smith@company.com",
"group_name": "Account Setup",
"display_order": 7,
},
{
"variable_name": "license_type",
"label": "License Type",
"field_type": "select",
"required": True,
"options": [
"Microsoft 365 Business Basic",
"Microsoft 365 Business Standard",
"Microsoft 365 Business Premium",
"Microsoft 365 E3",
"Microsoft 365 E5",
"Exchange Online Plan 1",
"Exchange Online Plan 2"
],
"help_text": "Select the license to assign to this user",
"group_name": "Account Setup",
"display_order": 8,
},
{
"variable_name": "security_groups",
"label": "Security Groups",
"field_type": "textarea",
"required": False,
"placeholder": "One group per line, e.g.:\nMarketing Team\nAll Staff\nVPN Users",
"help_text": "List the security/distribution groups this user should be added to",
"group_name": "Access & Groups",
"display_order": 9,
},
{
"variable_name": "shared_mailboxes",
"label": "Shared Mailboxes (Full Access)",
"field_type": "textarea",
"required": False,
"placeholder": "One mailbox per line, e.g.:\ninfo@company.com\nsales@company.com",
"help_text": "Shared mailboxes this user needs access to",
"group_name": "Access & Groups",
"display_order": 10,
},
{
"variable_name": "start_date",
"label": "Start Date",
"field_type": "text",
"required": True,
"placeholder": "e.g. 2025-03-15",
"help_text": "When the user needs access (YYYY-MM-DD)",
"group_name": "Project Info",
"display_order": 11,
},
{
"variable_name": "client_name",
"label": "Client / Company Name",
"field_type": "text",
"required": True,
"placeholder": "e.g. Contoso Ltd",
"group_name": "Project Info",
"display_order": 12,
},
{
"variable_name": "ticket_number",
"label": "Ticket Number",
"field_type": "text",
"required": False,
"placeholder": "e.g. TKT-2024-1234",
"group_name": "Project Info",
"display_order": 13,
},
],
"tree_structure": {
"steps": [
{
"id": "step_1",
"type": "procedure_step",
"title": "Verify Prerequisites & Licensing",
"content_type": "verification",
"description": "Before creating the account for **[VAR:display_name]** at **[VAR:client_name]**:\n\n- Confirm **[VAR:license_type]** license is available in the tenant\n- Verify the domain for **[VAR:email_address]** is configured in M365\n- Confirm the email address is not already in use\n- Verify start date: **[VAR:start_date]**\n- Ticket reference: **[VAR:ticket_number]**",
"commands": [
{
"language": "powershell",
"code": "# Connect to Microsoft Graph (if not already connected)\nConnect-MgGraph -Scopes \"User.ReadWrite.All\",\"Group.ReadWrite.All\",\"Directory.ReadWrite.All\"\n\n# Check available licenses\nGet-MgSubscribedSku | Select-Object SkuPartNumber, ConsumedUnits, @{N='Available';E={$_.PrepaidUnits.Enabled - $_.ConsumedUnits}} | Format-Table\n\n# Check if email is already taken\nGet-MgUser -Filter \"mail eq '[VAR:email_address]'\" -ErrorAction SilentlyContinue",
"label": "Check licenses & email availability"
}
],
"verification": {
"type": "checkbox",
"prompt": "License available and email address is free?"
},
},
{
"id": "step_2",
"type": "procedure_step",
"title": "Create User Account",
"content_type": "action",
"description": "Create the Microsoft 365 user account for **[VAR:display_name]**.",
"commands": [
{
"language": "powershell",
"code": "# Generate a temporary password\n$tempPassword = -join ((65..90) + (97..122) + (48..57) + (33,35,36,37) | Get-Random -Count 16 | ForEach-Object { [char]$_ })\n\n# Create the user\n$params = @{\n AccountEnabled = $true\n DisplayName = \"[VAR:display_name]\"\n GivenName = \"[VAR:first_name]\"\n Surname = \"[VAR:last_name]\"\n MailNickname = \"[VAR:first_name].[VAR:last_name]\".ToLower()\n UserPrincipalName = \"[VAR:email_address]\"\n JobTitle = \"[VAR:job_title]\"\n Department = \"[VAR:department]\"\n PasswordProfile = @{\n ForceChangePasswordNextSignIn = $true\n Password = $tempPassword\n }\n UsageLocation = \"US\"\n}\n\n$newUser = New-MgUser @params\nWrite-Host \"User created: $($newUser.UserPrincipalName)\" -ForegroundColor Green\nWrite-Host \"Temp password: $tempPassword\" -ForegroundColor Yellow",
"label": "Create M365 user"
}
],
"expected_outcome": "User account created. **Save the temporary password securely** — you'll need it for the welcome email.",
},
{
"id": "step_3",
"type": "procedure_step",
"title": "Assign License",
"content_type": "action",
"description": "Assign the **[VAR:license_type]** license to **[VAR:display_name]**.",
"commands": [
{
"language": "powershell",
"code": "# Get the user\n$user = Get-MgUser -Filter \"userPrincipalName eq '[VAR:email_address]'\"\n\n# Get the SKU for the selected license\n# Common SKU mappings:\n# Business Basic = O365_BUSINESS_ESSENTIALS or SPB\n# Business Standard = O365_BUSINESS_PREMIUM\n# Business Premium = SPB\n# E3 = SPE_E3\n# E5 = SPE_E5\n$skus = Get-MgSubscribedSku | Where-Object { $_.PrepaidUnits.Enabled - $_.ConsumedUnits -gt 0 }\n$skus | Select-Object SkuPartNumber, SkuId | Format-Table\n\n# Assign license (replace SKU_ID with actual value from above)\n# Set-MgUserLicense -UserId $user.Id -AddLicenses @(@{SkuId = \"SKU_ID_HERE\"}) -RemoveLicenses @()",
"label": "Assign license via PowerShell"
}
],
"expected_outcome": "License assigned. Mailbox provisioning may take 5-15 minutes.",
"verification": {
"type": "checkbox",
"prompt": "License successfully assigned?"
},
},
{
"id": "step_4",
"type": "procedure_step",
"title": "Wait for Mailbox Provisioning",
"content_type": "informational",
"description": "Exchange Online mailbox provisioning typically takes 5-15 minutes after license assignment.\n\nWhile waiting, you can proceed to configure groups in the next step.\n\nTo check mailbox status:",
"commands": [
{
"language": "powershell",
"code": "# Check if mailbox exists yet\nConnect-ExchangeOnline\nGet-EXOMailbox -Identity \"[VAR:email_address]\" -ErrorAction SilentlyContinue | Select-Object DisplayName, PrimarySmtpAddress, RecipientTypeDetails",
"label": "Check mailbox provisioning"
}
],
"expected_outcome": "Mailbox appears with `RecipientTypeDetails` = `UserMailbox`.",
},
{
"id": "step_5",
"type": "procedure_step",
"title": "Add to Security Groups",
"content_type": "action",
"description": "Add **[VAR:display_name]** to the requested security and distribution groups.\n\n**Requested groups:**\n[VAR:security_groups]",
"commands": [
{
"language": "powershell",
"code": "# Get the user\n$user = Get-MgUser -Filter \"userPrincipalName eq '[VAR:email_address]'\"\n\n# List available groups (search by name)\n# Get-MgGroup -Filter \"displayName eq 'GROUP_NAME'\" | Select-Object DisplayName, Id\n\n# Add user to a group\n# New-MgGroupMember -GroupId \"GROUP_ID\" -DirectoryObjectId $user.Id",
"label": "Add to groups"
}
],
"expected_outcome": "User appears as a member of all requested groups.",
"verification": {
"type": "checkbox",
"prompt": "User added to all requested groups?"
},
},
{
"id": "step_6",
"type": "procedure_step",
"title": "Configure Shared Mailbox Access",
"content_type": "action",
"description": "Grant **[VAR:display_name]** Full Access and Send As permissions on requested shared mailboxes.\n\n**Requested shared mailboxes:**\n[VAR:shared_mailboxes]",
"commands": [
{
"language": "powershell",
"code": "# Grant Full Access to shared mailbox\n# Add-MailboxPermission -Identity \"SHARED_MAILBOX@company.com\" -User \"[VAR:email_address]\" -AccessRights FullAccess -AutoMapping $true\n\n# Grant Send As permission\n# Add-RecipientPermission -Identity \"SHARED_MAILBOX@company.com\" -Trustee \"[VAR:email_address]\" -AccessRights SendAs -Confirm:$false",
"label": "Configure shared mailbox access"
}
],
"expected_outcome": "Shared mailboxes auto-map in Outlook within 30-60 minutes.",
},
{
"id": "step_7",
"type": "procedure_step",
"title": "Set Manager (Optional)",
"content_type": "action",
"description": "Set the manager for **[VAR:display_name]** if provided.\n\nManager: **[VAR:manager_email]**",
"commands": [
{
"language": "powershell",
"code": "# Set manager\n$user = Get-MgUser -Filter \"userPrincipalName eq '[VAR:email_address]'\"\n$manager = Get-MgUser -Filter \"userPrincipalName eq '[VAR:manager_email]'\"\n\nif ($manager) {\n Set-MgUserManagerByRef -UserId $user.Id -BodyParameter @{ \"@odata.id\" = \"https://graph.microsoft.com/v1.0/users/$($manager.Id)\" }\n Write-Host \"Manager set successfully\" -ForegroundColor Green\n}",
"label": "Set manager"
}
],
},
{
"id": "step_8",
"type": "procedure_step",
"title": "Configure MFA",
"content_type": "action",
"description": "Ensure MFA is configured for **[VAR:display_name]**.\n\nIf the tenant uses **Security Defaults** or **Conditional Access**, MFA will be prompted on first login.\n\nIf using per-user MFA, enable it manually:\n1. Go to Microsoft Entra admin center > Users > Per-user MFA\n2. Find **[VAR:email_address]**\n3. Set MFA status to **Enabled**\n\nThe user will be prompted to set up the Microsoft Authenticator app on first login.",
"verification": {
"type": "checkbox",
"prompt": "MFA configured or will be prompted on first sign-in?"
},
},
{
"id": "step_9",
"type": "procedure_step",
"title": "Send Welcome Email & Credentials",
"content_type": "informational",
"description": "Send the new user their login credentials securely.\n\n**Account details to communicate:**\n- Email: **[VAR:email_address]**\n- Temporary password: *(from step 2)*\n- Sign-in URL: https://portal.office.com\n- They will be prompted to change their password on first login\n- They will be prompted to set up MFA (Microsoft Authenticator app)\n\n**Best practice:** Send the username and password via separate channels (e.g., email the username to the manager, text the password to the user's personal phone).",
"verification": {
"type": "checkbox",
"prompt": "Welcome email/credentials sent securely?"
},
},
{
"id": "step_10",
"type": "procedure_step",
"title": "Document & Update Ticket",
"content_type": "informational",
"description": "Update the documentation and close the ticket.\n\n**Ticket: [VAR:ticket_number]**\n\n| Item | Value |\n|------|-------|\n| User | **[VAR:display_name]** |\n| Email | **[VAR:email_address]** |\n| License | **[VAR:license_type]** |\n| Department | **[VAR:department]** |\n| Title | **[VAR:job_title]** |\n| Start Date | **[VAR:start_date]** |\n| Groups | [VAR:security_groups] |\n| Shared Mailboxes | [VAR:shared_mailboxes] |",
"verification": {
"type": "checkbox",
"prompt": "Ticket updated and documented?"
},
},
{
"id": "step_end",
"type": "procedure_end",
"title": "User Onboarding Complete",
"description": "**[VAR:display_name]** ([VAR:email_address]) has been onboarded to Microsoft 365 for **[VAR:client_name]**.\n\n**Summary:**\n- Account created with **[VAR:license_type]**\n- Added to security groups\n- Shared mailbox access configured\n- MFA will be prompted on first sign-in\n\n**Follow-up in 24 hours:**\n- Verify user has logged in successfully\n- Confirm MFA is registered\n- Check shared mailboxes auto-mapped in Outlook",
},
]
},
}
def get_vpn_gateway_flow() -> dict[str, Any]:
"""Site-to-Site VPN Configuration — firewall/network setup procedure."""
return {
"name": "Site-to-Site VPN Configuration",
"description": "Step-by-step procedure for configuring a site-to-site IPSec VPN tunnel between two locations. Covers network planning, firewall configuration, tunnel setup, and verification.",
"tree_type": "procedural",
"category": "Projects - Networking",
"tags": ["vpn", "ipsec", "networking", "firewall"],
"intake_form": [
{
"variable_name": "site_a_name",
"label": "Site A Name (Primary)",
"field_type": "text",
"required": True,
"placeholder": "e.g. HQ Office",
"group_name": "Site A — Primary",
"display_order": 1,
},
{
"variable_name": "site_a_public_ip",
"label": "Site A Public IP",
"field_type": "ip_address",
"required": True,
"placeholder": "e.g. 203.0.113.10",
"group_name": "Site A — Primary",
"display_order": 2,
},
{
"variable_name": "site_a_lan_subnet",
"label": "Site A LAN Subnet",
"field_type": "text",
"required": True,
"placeholder": "e.g. 10.0.1.0/24",
"help_text": "Local network behind Site A's firewall",
"group_name": "Site A — Primary",
"display_order": 3,
},
{
"variable_name": "site_a_firewall",
"label": "Site A Firewall Model",
"field_type": "select",
"required": True,
"options": ["SonicWall", "Fortinet FortiGate", "Meraki MX", "pfSense/OPNsense", "Ubiquiti USG/UDM", "Other"],
"group_name": "Site A — Primary",
"display_order": 4,
},
{
"variable_name": "site_b_name",
"label": "Site B Name (Remote)",
"field_type": "text",
"required": True,
"placeholder": "e.g. Branch Office",
"group_name": "Site B — Remote",
"display_order": 5,
},
{
"variable_name": "site_b_public_ip",
"label": "Site B Public IP",
"field_type": "ip_address",
"required": True,
"placeholder": "e.g. 198.51.100.20",
"group_name": "Site B — Remote",
"display_order": 6,
},
{
"variable_name": "site_b_lan_subnet",
"label": "Site B LAN Subnet",
"field_type": "text",
"required": True,
"placeholder": "e.g. 10.0.2.0/24",
"help_text": "Local network behind Site B's firewall",
"group_name": "Site B — Remote",
"display_order": 7,
},
{
"variable_name": "site_b_firewall",
"label": "Site B Firewall Model",
"field_type": "select",
"required": True,
"options": ["SonicWall", "Fortinet FortiGate", "Meraki MX", "pfSense/OPNsense", "Ubiquiti USG/UDM", "Other"],
"group_name": "Site B — Remote",
"display_order": 8,
},
{
"variable_name": "psk",
"label": "Pre-Shared Key",
"field_type": "password",
"required": True,
"help_text": "IKE pre-shared key — minimum 20 characters recommended. Store in password vault.",
"group_name": "Tunnel Configuration",
"display_order": 9,
},
{
"variable_name": "encryption",
"label": "Encryption Algorithm",
"field_type": "select",
"required": True,
"options": ["AES-256-GCM (recommended)", "AES-256-CBC", "AES-128-GCM", "AES-128-CBC"],
"default_value": "AES-256-GCM (recommended)",
"group_name": "Tunnel Configuration",
"display_order": 10,
},
{
"variable_name": "ike_version",
"label": "IKE Version",
"field_type": "select",
"required": True,
"options": ["IKEv2 (recommended)", "IKEv1"],
"default_value": "IKEv2 (recommended)",
"group_name": "Tunnel Configuration",
"display_order": 11,
},
{
"variable_name": "client_name",
"label": "Client Name",
"field_type": "text",
"required": True,
"placeholder": "e.g. Contoso Ltd",
"group_name": "Project Info",
"display_order": 12,
},
{
"variable_name": "ticket_number",
"label": "Ticket / Project Number",
"field_type": "text",
"required": False,
"placeholder": "e.g. PRJ-2024-0099",
"group_name": "Project Info",
"display_order": 13,
},
],
"tree_structure": {
"steps": [
{
"id": "step_1",
"type": "procedure_step",
"title": "Verify Network Prerequisites",
"content_type": "verification",
"description": "Verify the following before configuring the VPN for **[VAR:client_name]** (ticket: **[VAR:ticket_number]**):\n\n**Site A — [VAR:site_a_name]:**\n- Public IP: **[VAR:site_a_public_ip]** (static, not behind NAT if possible)\n- LAN Subnet: **[VAR:site_a_lan_subnet]**\n- Firewall: **[VAR:site_a_firewall]**\n\n**Site B — [VAR:site_b_name]:**\n- Public IP: **[VAR:site_b_public_ip]** (static, not behind NAT if possible)\n- LAN Subnet: **[VAR:site_b_lan_subnet]**\n- Firewall: **[VAR:site_b_firewall]**\n\n**Critical checks:**\n- LAN subnets must NOT overlap\n- Both firewalls support IPSec VPN\n- ISPs are not blocking UDP 500 and 4500\n- Both sites have admin access to their firewall",
"verification": {
"type": "checkbox",
"prompt": "All prerequisites verified — no subnet overlap, admin access confirmed?"
},
},
{
"id": "step_2",
"type": "procedure_step",
"title": "Document the Tunnel Configuration",
"content_type": "informational",
"description": "Record the agreed tunnel parameters before configuring either side:\n\n| Parameter | Value |\n|-----------|-------|\n| IKE Version | **[VAR:ike_version]** |\n| Encryption | **[VAR:encryption]** |\n| Hash | SHA-256 |\n| DH Group | 14 (2048-bit) |\n| SA Lifetime | Phase 1: 28800s / Phase 2: 3600s |\n| PFS | Enabled (DH Group 14) |\n| Site A Peer | **[VAR:site_a_public_ip]** |\n| Site B Peer | **[VAR:site_b_public_ip]** |\n| Site A Network | **[VAR:site_a_lan_subnet]** |\n| Site B Network | **[VAR:site_b_lan_subnet]** |\n\n**Both sides MUST match exactly.** Save this table — you'll reference it during configuration.",
},
{
"id": "step_3",
"type": "procedure_step",
"title": "Configure Site A Firewall",
"content_type": "action",
"description": "Configure the VPN tunnel on **[VAR:site_a_name]** (**[VAR:site_a_firewall]**).\n\nLog into the firewall at **[VAR:site_a_name]** and create a new site-to-site VPN policy:\n\n1. **Phase 1 (IKE SA):**\n - Remote gateway: **[VAR:site_b_public_ip]**\n - Authentication: Pre-shared key\n - IKE version: **[VAR:ike_version]**\n - Encryption: **[VAR:encryption]**\n - Hash: SHA-256\n - DH Group: 14\n - Lifetime: 28800 seconds\n\n2. **Phase 2 (IPSec SA):**\n - Local network: **[VAR:site_a_lan_subnet]**\n - Remote network: **[VAR:site_b_lan_subnet]**\n - Encryption: **[VAR:encryption]**\n - Hash: SHA-256\n - PFS: Group 14\n - Lifetime: 3600 seconds\n\n3. **Firewall rules:**\n - Allow traffic from **[VAR:site_a_lan_subnet]** to **[VAR:site_b_lan_subnet]** via VPN zone\n - Allow traffic from **[VAR:site_b_lan_subnet]** to **[VAR:site_a_lan_subnet]** via VPN zone",
"verification": {
"type": "checkbox",
"prompt": "Site A VPN policy and firewall rules configured?"
},
},
{
"id": "step_4",
"type": "procedure_step",
"title": "Configure Site B Firewall",
"content_type": "action",
"description": "Configure the matching VPN tunnel on **[VAR:site_b_name]** (**[VAR:site_b_firewall]**).\n\nRepeat the same configuration, but mirror the local/remote settings:\n\n1. **Phase 1 (IKE SA):**\n - Remote gateway: **[VAR:site_a_public_ip]**\n - All other settings IDENTICAL to Site A\n\n2. **Phase 2 (IPSec SA):**\n - Local network: **[VAR:site_b_lan_subnet]**\n - Remote network: **[VAR:site_a_lan_subnet]**\n - All other settings IDENTICAL to Site A\n\n3. **Firewall rules:**\n - Allow traffic from **[VAR:site_b_lan_subnet]** to **[VAR:site_a_lan_subnet]** via VPN zone\n - Allow traffic from **[VAR:site_a_lan_subnet]** to **[VAR:site_b_lan_subnet]** via VPN zone\n\n**Double check:** Encryption, hash, DH group, PFS, and lifetimes must be identical on both sides.",
"verification": {
"type": "checkbox",
"prompt": "Site B VPN policy and firewall rules configured (matching Site A)?"
},
},
{
"id": "step_5",
"type": "procedure_step",
"title": "Initiate Tunnel & Verify Phase 1",
"content_type": "verification",
"description": "Bring up the VPN tunnel and verify Phase 1 (IKE) negotiation succeeds.\n\nOn most firewalls, you can force the tunnel to initiate by:\n- Sending a ping from one LAN to the other\n- Using the firewall's \"Connect\" or \"Bring Up\" button in the VPN status page\n\n**Check the VPN status page on both firewalls:**\n- Phase 1 should show **Established** or **Connected**\n- If Phase 1 fails, check: pre-shared key match, IKE version match, encryption/hash match, public IPs correct\n\n**Common Phase 1 failures:**\n- Mismatched PSK (most common)\n- ISP blocking UDP 500/4500\n- NAT-T issues (enable NAT traversal if behind NAT)\n- DH group mismatch",
"verification": {
"type": "checkbox",
"prompt": "Phase 1 (IKE) established on both firewalls?"
},
},
{
"id": "step_6",
"type": "procedure_step",
"title": "Verify Phase 2 & Traffic Flow",
"content_type": "verification",
"description": "Verify Phase 2 (IPSec) is established and traffic flows between sites.\n\n**From a device at [VAR:site_a_name] ([VAR:site_a_lan_subnet]):**\n- Ping a device at [VAR:site_b_name] ([VAR:site_b_lan_subnet])\n- Try accessing a file share or RDP session across the tunnel\n\n**From a device at [VAR:site_b_name] ([VAR:site_b_lan_subnet]):**\n- Ping a device at [VAR:site_a_name] ([VAR:site_a_lan_subnet])\n\n**Common Phase 2 failures:**\n- Mismatched encryption/hash/PFS settings\n- Incorrect local/remote network definitions (proxy IDs)\n- Firewall rules not allowing VPN zone traffic\n- Asymmetric routing (traffic going out default route instead of tunnel)",
"verification": {
"type": "checkbox",
"prompt": "Bidirectional traffic confirmed between sites?"
},
},
{
"id": "step_7",
"type": "procedure_step",
"title": "Test Critical Services",
"content_type": "verification",
"description": "Test the specific services that need to work across the VPN:\n\n- **File shares:** Access shared folders from remote site\n- **RDP:** Remote desktop to servers across the tunnel\n- **Active Directory:** If domain-joined, verify domain auth works from remote site\n- **DNS:** Resolve internal hostnames across the tunnel\n- **Printing:** If printers need cross-site access\n\nTest from both directions (A→B and B→A).",
"verification": {
"type": "checkbox",
"prompt": "All required services working bidirectionally?"
},
},
{
"id": "step_8",
"type": "procedure_step",
"title": "Configure Monitoring & Alerts",
"content_type": "action",
"description": "Set up monitoring to detect VPN tunnel drops:\n\n1. **On the firewalls:** Enable VPN tunnel status alerting (email/SNMP)\n2. **In your RMM/monitoring tool:** Add a ping monitor from each site to a host on the other site\n3. **Recommended thresholds:**\n - Alert if tunnel is down for > 5 minutes\n - Alert if latency exceeds 100ms\n - Alert if packet loss exceeds 1%\n\n**DPD (Dead Peer Detection):**\nEnsure DPD is enabled on both firewalls with a 10-second interval. This allows automatic tunnel renegotiation after brief outages.",
"verification": {
"type": "checkbox",
"prompt": "Monitoring and DPD configured?"
},
},
{
"id": "step_9",
"type": "procedure_step",
"title": "Document & Update Password Vault",
"content_type": "informational",
"description": "Document the complete VPN configuration:\n\n| Parameter | Site A | Site B |\n|-----------|--------|--------|\n| Name | **[VAR:site_a_name]** | **[VAR:site_b_name]** |\n| Public IP | **[VAR:site_a_public_ip]** | **[VAR:site_b_public_ip]** |\n| LAN Subnet | **[VAR:site_a_lan_subnet]** | **[VAR:site_b_lan_subnet]** |\n| Firewall | **[VAR:site_a_firewall]** | **[VAR:site_b_firewall]** |\n| IKE Version | **[VAR:ike_version]** | **[VAR:ike_version]** |\n| Encryption | **[VAR:encryption]** | **[VAR:encryption]** |\n\n**Store in password vault:**\n- Pre-shared key\n- Firewall admin credentials for both sites\n\n**Update:**\n- Client network diagram with VPN tunnel\n- Firewall configuration backup on both sites",
"verification": {
"type": "checkbox",
"prompt": "Documentation complete and PSK stored in vault?"
},
},
{
"id": "step_end",
"type": "procedure_end",
"title": "VPN Tunnel Active",
"description": "Site-to-site VPN tunnel is established between **[VAR:site_a_name]** and **[VAR:site_b_name]** for **[VAR:client_name]**.\n\n**Summary:**\n- Tunnel: **[VAR:site_a_public_ip]** ↔ **[VAR:site_b_public_ip]**\n- Networks: **[VAR:site_a_lan_subnet]** ↔ **[VAR:site_b_lan_subnet]**\n- Encryption: **[VAR:encryption]** / **[VAR:ike_version]**\n\n**Follow-up:**\n- Verify tunnel stability over 24-48 hours\n- Confirm monitoring alerts fire correctly (test with a brief tunnel disconnect)\n- Schedule quarterly PSK rotation",
},
]
},
}
# =============================================================================
# SEEDING FUNCTIONS
# =============================================================================
async def get_admin_token(client: httpx.AsyncClient) -> str:
"""Authenticate and get admin token."""
response = await client.post(
f"{API_BASE_URL}/auth/login",
data={"username": ADMIN_EMAIL, "password": ADMIN_PASSWORD},
)
if response.status_code != 200:
raise Exception(f"Login failed ({response.status_code}): {response.text}")
return response.json()["access_token"]
async def create_procedural_flow(client: httpx.AsyncClient, token: str, flow_data: dict) -> dict | None:
"""Create a procedural flow via the API. Returns None if it already exists."""
headers = {"Authorization": f"Bearer {token}"}
# Mark as default/system flow (public and visible to all)
flow_data["is_default"] = True
flow_data["is_public"] = True
# Check if flow with same name exists
list_response = await client.get(f"{API_BASE_URL}/trees", headers=headers, params={"tree_type": "procedural"})
if list_response.status_code == 200:
existing = list_response.json()
for tree in existing:
if tree["name"] == flow_data["name"]:
if not tree.get("is_public") or not tree.get("is_default"):
patch_response = await client.put(
f"{API_BASE_URL}/trees/{tree['id']}",
json={"is_public": True, "is_default": True},
headers=headers,
)
if patch_response.status_code == 200:
print(f" [UPDATE] Flow '{flow_data['name']}' visibility updated (ID: {tree['id']})")
return None
print(f" [SKIP] Flow '{flow_data['name']}' already exists (ID: {tree['id']})")
return None
# Create the flow
response = await client.post(
f"{API_BASE_URL}/trees",
json=flow_data,
headers=headers,
)
if response.status_code not in (200, 201):
raise Exception(f"Failed to create flow '{flow_data['name']}': {response.text}")
tree = response.json()
print(f" [OK] Created flow '{flow_data['name']}' (ID: {tree['id']})")
return tree
async def seed_procedural_flows():
"""Main seeding function."""
print("\n" + "=" * 60)
print(" PATHERLY - Procedural Flow Templates Seeder")
print("=" * 60)
async with httpx.AsyncClient(timeout=60.0) as client:
# Health check
try:
health_check = await client.get(f"{API_BASE_URL.replace('/api/v1', '')}/health")
if health_check.status_code != 200:
print(f"\n[ERROR] API health check failed: {health_check.status_code}")
return False
except httpx.ConnectError:
print("\n[ERROR] Cannot connect to API server")
print(f" Make sure the server is running at {API_BASE_URL}")
return False
# Authenticate
print("\n[1/3] Authenticating...")
try:
token = await get_admin_token(client)
print(f" Logged in as {ADMIN_EMAIL}")
except Exception as e:
print(f" [ERROR] Failed to authenticate: {e}")
return False
# Get flow definitions
print("\n[2/3] Preparing procedural flows...")
flows_to_create = [
("Projects - Infrastructure", get_domain_controller_flow()),
("Projects - Microsoft 365", get_m365_user_onboarding_flow()),
("Projects - Networking", get_vpn_gateway_flow()),
]
print(f" Found {len(flows_to_create)} flows to seed\n")
# Create flows
print("[3/3] Creating procedural flows...")
created_count = 0
skipped_count = 0
current_category = None
for category, flow_data in flows_to_create:
if category != current_category:
print(f"\n {category}:")
current_category = category
try:
result = await create_procedural_flow(client, token, flow_data)
if result:
created_count += 1
else:
skipped_count += 1
except Exception as e:
print(f" [FAIL] Failed to create '{flow_data['name']}': {e}")
# Summary
print("\n" + "=" * 60)
print(" SEEDING COMPLETE")
print("=" * 60)
print(f" Flows created: {created_count}")
print(f" Flows skipped (already exist): {skipped_count}")
print()
return True
def main():
global ADMIN_EMAIL, ADMIN_PASSWORD, API_BASE_URL
parser = argparse.ArgumentParser(description="Seed procedural flow templates")
parser.add_argument("--email", required=True, help="Admin email for authentication")
parser.add_argument("--password", required=True, help="Admin password")
parser.add_argument("--api-url", default=API_BASE_URL, help="API base URL (default: http://localhost:8000/api/v1)")
args = parser.parse_args()
ADMIN_EMAIL = args.email
ADMIN_PASSWORD = args.password
API_BASE_URL = args.api_url
asyncio.run(seed_procedural_flows())
if __name__ == "__main__":
main()