feat(scripts): add migration 057 - script tables + AD User Management seed templates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-13 00:26:03 -04:00
parent f1ed1fabbb
commit 7a3f3b186c

View File

@@ -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"
""",
),
]