From 7a3f3b186c4dd720cc36baa7c968f3cc824420bd Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 13 Mar 2026 00:26:03 -0400 Subject: [PATCH] feat(scripts): add migration 057 - script tables + AD User Management seed templates Co-Authored-By: Claude Sonnet 4.6 --- .../versions/057_add_script_templates.py | 690 ++++++++++++++++++ 1 file changed, 690 insertions(+) create mode 100644 backend/alembic/versions/057_add_script_templates.py diff --git a/backend/alembic/versions/057_add_script_templates.py b/backend/alembic/versions/057_add_script_templates.py new file mode 100644 index 00000000..91efd4c4 --- /dev/null +++ b/backend/alembic/versions/057_add_script_templates.py @@ -0,0 +1,690 @@ +"""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" +""", + ), + + ]