#!/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()