"""add script templates Revision ID: 057 Revises: 056 Create Date: 2026-03-12 """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import UUID, JSONB, ENUM as PG_ENUM import uuid import json from datetime import datetime, timezone revision = "057" down_revision = "056" branch_labels = None depends_on = None def upgrade() -> None: # ── Create enum ────────────────────────────────────────────────────── op.execute("CREATE TYPE script_complexity AS ENUM ('beginner', 'intermediate', 'advanced')") # ── script_categories ──────────────────────────────────────────────── op.create_table( "script_categories", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("name", sa.String(100), nullable=False), sa.Column("slug", sa.String(100), nullable=False, unique=True), sa.Column("description", sa.Text, nullable=True), sa.Column("icon", sa.String(50), nullable=True), sa.Column("sort_order", sa.Integer, nullable=False, server_default=sa.text("0")), sa.Column("is_active", sa.Boolean, nullable=False, server_default=sa.text("true")), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), ) op.create_index("ix_script_categories_slug", "script_categories", ["slug"]) # ── script_templates ───────────────────────────────────────────────── op.create_table( "script_templates", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("category_id", UUID(as_uuid=True), sa.ForeignKey("script_categories.id", ondelete="RESTRICT"), nullable=False), sa.Column("team_id", UUID(as_uuid=True), sa.ForeignKey("teams.id", ondelete="CASCADE"), nullable=True), sa.Column("created_by", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("name", sa.String(200), nullable=False), sa.Column("slug", sa.String(200), nullable=False), sa.Column("description", sa.Text, nullable=True), sa.Column("use_case", sa.Text, nullable=True), sa.Column("script_body", sa.Text, nullable=False), sa.Column("parameters_schema", JSONB, nullable=False, server_default=sa.text("'{}'::jsonb")), sa.Column("default_values", JSONB, nullable=False, server_default=sa.text("'{}'::jsonb")), sa.Column("validation_rules", JSONB, nullable=False, server_default=sa.text("'{}'::jsonb")), sa.Column("tags", JSONB, nullable=False, server_default=sa.text("'[]'::jsonb")), sa.Column("complexity", PG_ENUM("beginner", "intermediate", "advanced", name="script_complexity", create_type=False), nullable=False, server_default=sa.text("'beginner'")), sa.Column("estimated_runtime", sa.String(50), nullable=True), sa.Column("requires_elevation", sa.Boolean, nullable=False, server_default=sa.text("true")), sa.Column("requires_modules", JSONB, nullable=False, server_default=sa.text("'[]'::jsonb")), sa.Column("version", sa.Integer, nullable=False, server_default=sa.text("1")), sa.Column("is_verified", sa.Boolean, nullable=False, server_default=sa.text("false")), sa.Column("is_active", sa.Boolean, nullable=False, server_default=sa.text("true")), sa.Column("usage_count", sa.Integer, nullable=False, server_default=sa.text("0")), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), ) op.create_index("ix_script_templates_category_id", "script_templates", ["category_id"]) op.create_index("ix_script_templates_team_id", "script_templates", ["team_id"]) op.create_index("ix_script_templates_slug", "script_templates", ["slug"]) # ── script_generations ─────────────────────────────────────────────── op.create_table( "script_generations", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("template_id", UUID(as_uuid=True), sa.ForeignKey("script_templates.id", ondelete="RESTRICT"), nullable=False), sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), sa.Column("team_id", UUID(as_uuid=True), sa.ForeignKey("teams.id", ondelete="SET NULL"), nullable=True), sa.Column("session_id", UUID(as_uuid=True), sa.ForeignKey("sessions.id", ondelete="SET NULL"), nullable=True), sa.Column("parameters_used", JSONB, nullable=False, server_default=sa.text("'{}'::jsonb")), sa.Column("generated_script", sa.Text, nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), ) op.create_index("ix_script_generations_template_id", "script_generations", ["template_id"]) op.create_index("ix_script_generations_user_id", "script_generations", ["user_id"]) op.create_index("ix_script_generations_session_id", "script_generations", ["session_id"]) # ── Seed: Active Directory category ────────────────────────────────── now = datetime.now(timezone.utc) cat_id = uuid.UUID("00000000-0000-0000-0000-000000000001") conn = op.get_bind() conn.execute( sa.text(""" INSERT INTO script_categories (id, name, slug, description, icon, sort_order, is_active, created_at, updated_at) VALUES (:id, :name, :slug, :description, :icon, :sort_order, true, :now, :now) """), { "id": cat_id, "name": "Active Directory", "slug": "active-directory", "description": "User account and group management scripts for Active Directory environments", "icon": "shield-check", "sort_order": 1, "now": now, } ) templates = _get_seed_templates(cat_id, now) for tmpl in templates: conn.execute( sa.text(""" INSERT INTO script_templates ( id, category_id, name, slug, description, use_case, script_body, parameters_schema, default_values, validation_rules, tags, complexity, estimated_runtime, requires_elevation, requires_modules, version, is_verified, is_active, usage_count, created_at, updated_at ) VALUES ( :id, :category_id, :name, :slug, :description, :use_case, :script_body, CAST(:parameters_schema AS jsonb), CAST(:default_values AS jsonb), CAST(:validation_rules AS jsonb), CAST(:tags AS jsonb), :complexity, :estimated_runtime, :requires_elevation, CAST(:requires_modules AS jsonb), 1, true, true, 0, :now, :now ) """), tmpl, ) def downgrade() -> None: op.drop_table("script_generations") op.drop_table("script_templates") op.drop_table("script_categories") op.execute("DROP TYPE IF EXISTS script_complexity") def _get_seed_templates(cat_id: uuid.UUID, now: datetime) -> list[dict]: """Return seed data for the six AD User Management templates.""" def tmpl( id_suffix: int, name: str, slug: str, description: str, use_case: str, script_body: str, parameters_schema: dict, complexity: str, estimated_runtime: str, requires_elevation: bool, tags: list[str], ) -> dict: return { "id": uuid.UUID(f"00000000-0000-0000-0001-{id_suffix:012d}"), "category_id": cat_id, "name": name, "slug": slug, "description": description, "use_case": use_case, "script_body": script_body, "parameters_schema": json.dumps(parameters_schema), "default_values": json.dumps({}), "validation_rules": json.dumps({}), "tags": json.dumps(tags), "complexity": complexity, "estimated_runtime": estimated_runtime, "requires_elevation": requires_elevation, "requires_modules": json.dumps([]), "now": now, } return [ # ── 1: Create AD User ───────────────────────────────────────────── tmpl( id_suffix=1, name="Create AD User Account", slug="create-ad-user", description="Creates a new Active Directory user account with full property configuration.", use_case="Use when onboarding a new employee or creating a service account in Active Directory.", complexity="intermediate", estimated_runtime="< 5 seconds", requires_elevation=True, tags=["active-directory", "user-management", "onboarding"], parameters_schema={ "parameters": [ {"key": "first_name", "label": "First Name", "type": "text", "required": True, "group": "User Identity", "order": 1, "validation": {"pattern": "^[a-zA-Z\\-']+$", "maxLength": 64}}, {"key": "last_name", "label": "Last Name", "type": "text", "required": True, "group": "User Identity", "order": 2, "validation": {"pattern": "^[a-zA-Z\\-']+$", "maxLength": 64}}, {"key": "sam_account_name", "label": "SAM Account Name", "type": "text", "required": True, "group": "User Identity", "order": 3, "placeholder": "jsmith", "help_text": "The logon name (typically firstname.lastname or first initial + last name). Max 20 characters."}, {"key": "upn_suffix", "label": "UPN Suffix", "type": "text", "required": True, "group": "User Identity", "order": 4, "placeholder": "contoso.com"}, {"key": "ou_path", "label": "Organizational Unit", "type": "text", "required": True, "group": "AD Configuration", "order": 5, "placeholder": "OU=Users,DC=contoso,DC=com", "validation": {"pattern": "^(OU|CN)=.+"}}, {"key": "password", "label": "Initial Password", "type": "password", "required": True, "group": "Security", "order": 6, "sensitive": True}, {"key": "job_title", "label": "Job Title", "type": "text", "required": False, "group": "Profile", "order": 7}, {"key": "department", "label": "Department", "type": "text", "required": False, "group": "Profile", "order": 8}, {"key": "groups", "label": "Security Groups", "type": "multi_text", "required": False, "group": "Group Membership", "order": 9, "placeholder": "Domain Users"}, {"key": "force_password_change", "label": "Force Password Change at Logon", "type": "boolean", "required": False, "group": "Security", "order": 10, "default": True}, {"key": "account_enabled", "label": "Enable Account", "type": "boolean", "required": False, "group": "Security", "order": 11, "default": True}, ] }, script_body=r"""# ============================================================ # Create AD User Account # Generated by ResolutionFlow Script Generator # Template Version: 1 # ============================================================ #Requires -Modules ActiveDirectory #Requires -RunAsAdministrator $FirstName = '{{ first_name }}' $LastName = '{{ last_name }}' $SamAccountName = '{{ sam_account_name }}' $UPNSuffix = '{{ upn_suffix }}' $OUPath = '{{ ou_path }}' $Password = {{ password | as_secure_string }} $ForcePasswordChange = {{ force_password_change | as_bool }} $AccountEnabled = {{ account_enabled | as_bool }} {% if job_title %} $JobTitle = '{{ job_title }}' {% endif %} {% if department %} $Department = '{{ department }}' {% endif %} {% if groups %} $Groups = @({{ groups | as_array }}) {% endif %} $DisplayName = "$FirstName $LastName" $UPN = "$SamAccountName@$UPNSuffix" try { Import-Module ActiveDirectory -ErrorAction Stop Write-Host "[CHECK] ActiveDirectory module loaded" -ForegroundColor Green } catch { Write-Host "[ERROR] ActiveDirectory module not available: $_" -ForegroundColor Red exit 1 } try { $userParams = @{ Name = $DisplayName GivenName = $FirstName Surname = $LastName SamAccountName = $SamAccountName UserPrincipalName = $UPN Path = $OUPath AccountPassword = $Password Enabled = $AccountEnabled ChangePasswordAtLogon = $ForcePasswordChange } {% if job_title %} $userParams["Title"] = $JobTitle {% endif %} {% if department %} $userParams["Department"] = $Department {% endif %} New-ADUser @userParams -ErrorAction Stop Write-Host "[OK] User account created: $SamAccountName ($DisplayName)" -ForegroundColor Green } catch { Write-Host "[ERROR] Failed to create user: $_" -ForegroundColor Red exit 1 } {% if groups %} foreach ($group in $Groups) { try { Add-ADGroupMember -Identity $group -Members $SamAccountName -ErrorAction Stop Write-Host "[OK] Added to group: $group" -ForegroundColor Green } catch { Write-Host "[WARN] Could not add to group '$group': $_" -ForegroundColor Yellow } } {% endif %} Write-Host "" Write-Host "[DONE] User $SamAccountName created successfully." -ForegroundColor Cyan Write-Host " UPN: $UPN" -ForegroundColor Cyan """, ), # ── 2: Disable AD User ──────────────────────────────────────────── tmpl( id_suffix=2, name="Disable AD User Account", slug="disable-ad-user", description="Disables an Active Directory user account with optional group removal and OU relocation.", use_case="Use when offboarding an employee or temporarily suspending access.", complexity="beginner", estimated_runtime="< 5 seconds", requires_elevation=True, tags=["active-directory", "user-management", "offboarding"], parameters_schema={ "parameters": [ {"key": "sam_account_name", "label": "SAM Account Name", "type": "text", "required": True, "group": "Target Account", "order": 1, "placeholder": "jsmith"}, {"key": "disable_reason", "label": "Reason", "type": "text", "required": False, "group": "Audit", "order": 2, "placeholder": "Employee offboarding"}, {"key": "remove_groups", "label": "Remove from All Groups", "type": "boolean", "required": False, "group": "Options", "order": 3, "default": False}, {"key": "move_to_disabled_ou", "label": "Move to Disabled OU", "type": "boolean", "required": False, "group": "Options", "order": 4, "default": False}, {"key": "disabled_ou_path", "label": "Disabled OU Path", "type": "text", "required": False, "group": "Options", "order": 5, "placeholder": "OU=Disabled,DC=contoso,DC=com"}, ] }, script_body=r"""# ============================================================ # Disable AD User Account # Generated by ResolutionFlow Script Generator # ============================================================ #Requires -Modules ActiveDirectory #Requires -RunAsAdministrator $SamAccountName = '{{ sam_account_name }}' {% if disable_reason %} $DisableReason = '{{ disable_reason }}' {% endif %} $RemoveGroups = {{ remove_groups | as_bool }} $MoveToDisabled = {{ move_to_disabled_ou | as_bool }} {% if disabled_ou_path %} $DisabledOUPath = '{{ disabled_ou_path }}' {% endif %} try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Host "[ERROR] ActiveDirectory module not available: $_" -ForegroundColor Red; exit 1 } try { $user = Get-ADUser -Identity $SamAccountName -Properties MemberOf -ErrorAction Stop Write-Host "[CHECK] Found user: $($user.DistinguishedName)" -ForegroundColor Green } catch { Write-Host "[ERROR] User not found: $SamAccountName" -ForegroundColor Red; exit 1 } try { Disable-ADAccount -Identity $SamAccountName -ErrorAction Stop Write-Host "[OK] Account disabled: $SamAccountName" -ForegroundColor Green {% if disable_reason %} Set-ADUser -Identity $SamAccountName -Description $DisableReason Write-Host "[OK] Disable reason recorded in Description field" -ForegroundColor Green {% endif %} } catch { Write-Host "[ERROR] Failed to disable account: $_" -ForegroundColor Red; exit 1 } {% if remove_groups %} if ($RemoveGroups) { $groups = $user.MemberOf foreach ($group in $groups) { try { Remove-ADGroupMember -Identity $group -Members $SamAccountName -Confirm:$false -ErrorAction Stop Write-Host "[OK] Removed from group: $group" -ForegroundColor Green } catch { Write-Host "[WARN] Could not remove from group: $group" -ForegroundColor Yellow } } } {% endif %} {% if move_to_disabled_ou %} if ($MoveToDisabled) { try { Move-ADObject -Identity $user.DistinguishedName -TargetPath $DisabledOUPath -ErrorAction Stop Write-Host "[OK] Moved to disabled OU: $DisabledOUPath" -ForegroundColor Green } catch { Write-Host "[WARN] Could not move to disabled OU: $_" -ForegroundColor Yellow } } {% endif %} Write-Host "" Write-Host "[DONE] Account $SamAccountName has been disabled." -ForegroundColor Cyan """, ), # ── 3: Reset AD Password ────────────────────────────────────────── tmpl( id_suffix=3, name="Reset AD Password", slug="reset-ad-password", description="Resets an Active Directory user password with optional account unlock.", use_case="Use when a user has forgotten their password or their account is locked out.", complexity="beginner", estimated_runtime="< 5 seconds", requires_elevation=True, tags=["active-directory", "password", "account-management"], parameters_schema={ "parameters": [ {"key": "sam_account_name", "label": "SAM Account Name", "type": "text", "required": True, "group": "Target Account", "order": 1}, {"key": "new_password", "label": "New Password", "type": "password", "required": True, "group": "Security", "order": 2, "sensitive": True}, {"key": "force_change_at_logon", "label": "Force Change at Next Logon", "type": "boolean", "required": False, "group": "Security", "order": 3, "default": True}, {"key": "unlock_account", "label": "Unlock Account if Locked", "type": "boolean", "required": False, "group": "Options", "order": 4, "default": True}, ] }, script_body=r"""# ============================================================ # Reset AD Password # Generated by ResolutionFlow Script Generator # ============================================================ #Requires -Modules ActiveDirectory #Requires -RunAsAdministrator $SamAccountName = '{{ sam_account_name }}' $NewPassword = {{ new_password | as_secure_string }} $ForceChange = {{ force_change_at_logon | as_bool }} $UnlockAccount = {{ unlock_account | as_bool }} try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Host "[ERROR] $_ " -ForegroundColor Red; exit 1 } try { Set-ADAccountPassword -Identity $SamAccountName -NewPassword $NewPassword -Reset -ErrorAction Stop Write-Host "[OK] Password reset for: $SamAccountName" -ForegroundColor Green } catch { Write-Host "[ERROR] Failed to reset password: $_" -ForegroundColor Red; exit 1 } if ($ForceChange) { Set-ADUser -Identity $SamAccountName -ChangePasswordAtLogon $true Write-Host "[OK] User must change password at next logon" -ForegroundColor Green } if ($UnlockAccount) { try { Unlock-ADAccount -Identity $SamAccountName -ErrorAction Stop Write-Host "[OK] Account unlocked" -ForegroundColor Green } catch { Write-Host "[WARN] Could not unlock account (may not have been locked): $_" -ForegroundColor Yellow } } Write-Host "" Write-Host "[DONE] Password reset complete for $SamAccountName." -ForegroundColor Cyan """, ), # ── 4: Unlock AD Account ────────────────────────────────────────── tmpl( id_suffix=4, name="Unlock AD Account", slug="unlock-ad-account", description="Unlocks a locked-out Active Directory user account and optionally shows lockout information.", use_case="Use when a user is locked out after too many failed login attempts.", complexity="beginner", estimated_runtime="< 5 seconds", requires_elevation=True, tags=["active-directory", "account-management", "lockout"], parameters_schema={ "parameters": [ {"key": "sam_account_name", "label": "SAM Account Name", "type": "text", "required": True, "group": "Target Account", "order": 1}, {"key": "show_lockout_info", "label": "Show Lockout Source Info", "type": "boolean", "required": False, "group": "Options", "order": 2, "default": False, "help_text": "Queries domain controllers for lockout source (may be slow)"}, ] }, script_body=r"""# ============================================================ # Unlock AD Account # Generated by ResolutionFlow Script Generator # ============================================================ #Requires -Modules ActiveDirectory #Requires -RunAsAdministrator $SamAccountName = '{{ sam_account_name }}' $ShowLockoutInfo = {{ show_lockout_info | as_bool }} try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Host "[ERROR] $_ " -ForegroundColor Red; exit 1 } try { $user = Get-ADUser -Identity $SamAccountName -Properties LockedOut, BadLogonCount, LastBadPasswordAttempt -ErrorAction Stop Write-Host "[CHECK] User found: $($user.DistinguishedName)" -ForegroundColor Green Write-Host " Locked Out: $($user.LockedOut)" -ForegroundColor Cyan Write-Host " Bad Logon Count: $($user.BadLogonCount)" -ForegroundColor Cyan Write-Host " Last Bad Password Attempt: $($user.LastBadPasswordAttempt)" -ForegroundColor Cyan } catch { Write-Host "[ERROR] User not found: $SamAccountName" -ForegroundColor Red; exit 1 } if (-not $user.LockedOut) { Write-Host "[INFO] Account is not currently locked." -ForegroundColor Yellow } else { try { Unlock-ADAccount -Identity $SamAccountName -ErrorAction Stop Write-Host "[OK] Account unlocked: $SamAccountName" -ForegroundColor Green } catch { Write-Host "[ERROR] Failed to unlock account: $_" -ForegroundColor Red; exit 1 } } {% if show_lockout_info %} if ($ShowLockoutInfo) { Write-Host "" Write-Host "[INFO] Querying domain controllers for lockout source..." -ForegroundColor Cyan try { $DCs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName foreach ($DC in $DCs) { $events = Get-WinEvent -ComputerName $DC -FilterHashtable @{ LogName = "Security" Id = 4740 } -MaxEvents 10 -ErrorAction SilentlyContinue | Where-Object { $_.Properties[0].Value -eq $SamAccountName } foreach ($e in $events) { Write-Host " DC: $DC | Source: $($e.Properties[1].Value) | Time: $($e.TimeCreated)" -ForegroundColor Yellow } } } catch { Write-Host "[WARN] Could not query lockout source: $_" -ForegroundColor Yellow } } {% endif %} Write-Host "" Write-Host "[DONE] Unlock operation complete for $SamAccountName." -ForegroundColor Cyan """, ), # ── 5: Delete AD User ───────────────────────────────────────────── tmpl( id_suffix=5, name="Delete AD User Account", slug="delete-ad-user", description="Permanently deletes an Active Directory user account with optional CSV backup of user properties.", use_case="Use when permanently removing an account that is no longer needed. Prefer Disable for departing employees.", complexity="advanced", estimated_runtime="< 10 seconds", requires_elevation=True, tags=["active-directory", "user-management", "destructive"], parameters_schema={ "parameters": [ {"key": "sam_account_name", "label": "SAM Account Name", "type": "text", "required": True, "group": "Target Account", "order": 1}, {"key": "confirm_deletion", "label": "I confirm this account should be permanently deleted", "type": "boolean", "required": True, "group": "Confirmation", "order": 2, "default": False}, {"key": "backup_to_csv", "label": "Back Up User Properties to CSV First", "type": "boolean", "required": False, "group": "Options", "order": 3, "default": True}, {"key": "backup_path", "label": "CSV Backup Path", "type": "text", "required": False, "group": "Options", "order": 4, "placeholder": "C:\\Backups\\ad-user-backup.csv"}, ] }, script_body=r"""# ============================================================ # Delete AD User Account # Generated by ResolutionFlow Script Generator # DESTRUCTIVE OPERATION # ============================================================ #Requires -Modules ActiveDirectory #Requires -RunAsAdministrator $SamAccountName = '{{ sam_account_name }}' $ConfirmDeletion = {{ confirm_deletion | as_bool }} $BackupToCSV = {{ backup_to_csv | as_bool }} {% if backup_path %} $BackupPath = '{{ backup_path }}' {% else %} $BackupPath = "C:\Temp\ad-user-backup-$SamAccountName-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv" {% endif %} try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Host "[ERROR] $_ " -ForegroundColor Red; exit 1 } if (-not $ConfirmDeletion) { Write-Host "[ABORT] Deletion not confirmed. Set confirm_deletion to true to proceed." -ForegroundColor Red exit 1 } try { $user = Get-ADUser -Identity $SamAccountName -Properties * -ErrorAction Stop Write-Host "[CHECK] Found user: $($user.DistinguishedName)" -ForegroundColor Yellow Write-Host "[WARN] About to permanently delete this account." -ForegroundColor Yellow } catch { Write-Host "[ERROR] User not found: $SamAccountName" -ForegroundColor Red; exit 1 } if ($BackupToCSV) { try { $user | Select-Object Name, SamAccountName, UserPrincipalName, DistinguishedName, EmailAddress, Title, Department, Enabled, Created, Modified | Export-Csv -Path $BackupPath -NoTypeInformation -ErrorAction Stop Write-Host "[OK] User properties backed up to: $BackupPath" -ForegroundColor Green } catch { Write-Host "[WARN] Could not write backup: $_" -ForegroundColor Yellow } } try { Remove-ADUser -Identity $SamAccountName -Confirm:$false -ErrorAction Stop Write-Host "[OK] User account permanently deleted: $SamAccountName" -ForegroundColor Green } catch { Write-Host "[ERROR] Failed to delete user: $_" -ForegroundColor Red; exit 1 } Write-Host "" Write-Host "[DONE] Account $SamAccountName has been permanently deleted." -ForegroundColor Cyan """, ), # ── 6: Bulk User Import ─────────────────────────────────────────── tmpl( id_suffix=6, name="Bulk User Import from CSV", slug="bulk-user-import", description="Imports multiple Active Directory user accounts from a CSV file with dry-run support and detailed logging.", use_case="Use when onboarding multiple employees at once from an HR system export.", complexity="advanced", estimated_runtime="1-2 minutes", requires_elevation=True, tags=["active-directory", "bulk", "onboarding", "csv"], parameters_schema={ "parameters": [ {"key": "csv_path", "label": "CSV File Path", "type": "text", "required": True, "group": "Source", "order": 1, "placeholder": "C:\\Imports\\new-users.csv", "help_text": "CSV must have columns: FirstName, LastName, SamAccountName, UPN, Department, JobTitle"}, {"key": "ou_path", "label": "Target Organizational Unit", "type": "text", "required": True, "group": "AD Configuration", "order": 2, "placeholder": "OU=Users,DC=contoso,DC=com"}, {"key": "default_password", "label": "Default Password", "type": "password", "required": True, "group": "Security", "order": 3, "sensitive": True}, {"key": "groups", "label": "Add All Users to Groups", "type": "multi_text", "required": False, "group": "Group Membership", "order": 4}, {"key": "force_password_change", "label": "Force Password Change at Logon", "type": "boolean", "required": False, "group": "Security", "order": 5, "default": True}, {"key": "log_path", "label": "Log File Path", "type": "text", "required": False, "group": "Options", "order": 6, "placeholder": "C:\\Logs\\bulk-import.log"}, {"key": "dry_run", "label": "Dry Run (Preview Only - No Changes Made)", "type": "boolean", "required": False, "group": "Options", "order": 7, "default": False}, ] }, script_body=r"""# ============================================================ # Bulk User Import from CSV # Generated by ResolutionFlow Script Generator # ============================================================ # CSV Format: FirstName, LastName, SamAccountName, UPN, Department, JobTitle # ============================================================ #Requires -Modules ActiveDirectory #Requires -RunAsAdministrator $CSVPath = '{{ csv_path }}' $OUPath = '{{ ou_path }}' $DefaultPassword = {{ default_password | as_secure_string }} $ForceChange = {{ force_password_change | as_bool }} $DryRun = {{ dry_run | as_bool }} {% if log_path %} $LogPath = '{{ log_path }}' {% else %} $LogPath = "C:\Temp\bulk-import-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" {% endif %} {% if groups %} $DefaultGroups = @({{ groups | as_array }}) {% endif %} function Write-Log { param($Message, $Level = "INFO") $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message" Add-Content -Path $LogPath -Value $line -ErrorAction SilentlyContinue $color = switch ($Level) { "ERROR" { "Red" } "WARN" { "Yellow" } "OK" { "Green" } default { "White" } } Write-Host $line -ForegroundColor $color } try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Log "ActiveDirectory module not available: $_" "ERROR"; exit 1 } if (-not (Test-Path $CSVPath)) { Write-Log "CSV file not found: $CSVPath" "ERROR"; exit 1 } $users = Import-Csv -Path $CSVPath Write-Log "Loaded $($users.Count) users from CSV" if ($DryRun) { Write-Log "DRY RUN MODE - no changes will be made" "WARN" } $successCount = 0 $errorCount = 0 foreach ($row in $users) { $sam = $row.SamAccountName try { $existing = Get-ADUser -Filter "SamAccountName -eq '$sam'" -ErrorAction SilentlyContinue if ($existing) { Write-Log "SKIP: $sam already exists" "WARN"; continue } if (-not $DryRun) { New-ADUser ` -Name "$($row.FirstName) $($row.LastName)" ` -GivenName $row.FirstName ` -Surname $row.LastName ` -SamAccountName $sam ` -UserPrincipalName $row.UPN ` -Path $OUPath ` -AccountPassword $DefaultPassword ` -Enabled $true ` -ChangePasswordAtLogon $ForceChange ` -Title $row.JobTitle ` -Department $row.Department ` -ErrorAction Stop {% if groups %} foreach ($group in $DefaultGroups) { try { Add-ADGroupMember -Identity $group -Members $sam -ErrorAction Stop } catch { Write-Log "WARN: $sam - could not add to group '$group'" "WARN" } } {% endif %} } Write-Log "$(if ($DryRun) { 'PREVIEW' } else { 'CREATED' }): $sam ($($row.FirstName) $($row.LastName))" "OK" $successCount++ } catch { Write-Log "FAILED: $sam - $_" "ERROR" $errorCount++ } } Write-Log "" Write-Log "[DONE] Import complete. Success: $successCount | Errors: $errorCount | Log: $LogPath" "INFO" """, ), ]