noid-privacy/Modules/SecurityBaseline/Private/Set-SecurityTemplate.ps1

239 lines
9.6 KiB
PowerShell

<#
.SYNOPSIS
Apply security template settings from parsed Security Baseline JSON
.DESCRIPTION
Converts JSON security template to INF format and applies via secedit.exe.
Handles:
- Password Policies
- Account Policies
- User Rights Assignments
- Security Options
- Event Log Settings
- Registry Values (security-related)
.PARAMETER SecurityTemplatePath
Path to SecurityTemplates.json
.PARAMETER DryRun
Preview changes without applying
.OUTPUTS
PSCustomObject with success status and errors
.NOTES
Requires Administrator privileges
Uses secedit.exe (built into Windows since Windows 2000)
#>
function Set-SecurityTemplate {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$SecurityTemplatePath,
[Parameter(Mandatory = $false)]
[switch]$DryRun
)
$result = [PSCustomObject]@{
Success = $false
SectionsApplied = 0
SettingsApplied = 0
Errors = @()
}
if (-not (Test-Path $SecurityTemplatePath)) {
$result.Errors += "Security template file not found: $SecurityTemplatePath"
return $result
}
try {
$templates = Get-Content -Path $SecurityTemplatePath -Raw | ConvertFrom-Json
# Merge all GPO templates into one master INF
$infContent = @()
$infContent += "[Unicode]"
$infContent += "Unicode=yes"
$infContent += ""
$infContent += "[Version]"
$infContent += "signature=`"`$CHICAGO$`""
$infContent += "Revision=1"
$infContent += ""
# Track which sections we have
$sectionsProcessed = @{}
# Process each GPO's security template
foreach ($gpoName in ($templates.PSObject.Properties.Name)) {
$gpoTemplate = $templates.$gpoName
foreach ($sectionName in ($gpoTemplate.PSObject.Properties.Name)) {
# Skip metadata sections
if ($sectionName -in @("Unicode", "Version")) {
continue
}
$section = $gpoTemplate.$sectionName
if (-not $sectionsProcessed.ContainsKey($sectionName)) {
$sectionsProcessed[$sectionName] = @()
}
# Add settings from this section
foreach ($key in ($section.PSObject.Properties.Name)) {
$value = $section.$key
# Service General Setting requires special format per MS-GPSB 2.2.8
# Format: "ServiceName",StartupMode,""
# StartupMode: 2=Automatic, 3=Manual, 4=Disabled
if ($sectionName -eq 'Service General Setting') {
# Parse StartupType from value (e.g., "StartupType=Disabled")
$startupMode = 4 # Default: Disabled
if ($value -match 'StartupType=(\w+)') {
$startupType = $matches[1]
switch ($startupType) {
'Disabled' { $startupMode = 4 }
'Manual' { $startupMode = 3 }
'Automatic' { $startupMode = 2 }
default { $startupMode = 4 }
}
}
# Microsoft INF format: "ServiceName",Mode,""
$settingLine = "`"$key`",$startupMode,`"`""
}
else {
# Standard format for other sections
# Values are used directly from JSON as they are already in correct MS INF format
# Examples:
# ScRemoveOption: 1,"1" (REG_SZ with string "1")
# RestrictRemoteSAM: 1,"O:BAG:BAD:(A;;RC;;;BA)" (REG_SZ with SDDL)
# EnableInstallerDetection: 4,1 (REG_DWORD with value 1)
# Format: Key = Value
$settingLine = "$key = $value"
}
# Avoid duplicates
if ($sectionsProcessed[$sectionName] -notcontains $settingLine) {
$sectionsProcessed[$sectionName] += $settingLine
}
}
}
}
# Write all sections in Microsoft INF required order
# Order matters! secedit expects sections in specific sequence
$sectionOrder = @(
'System Access',
'Event Audit',
'Registry Values',
'Privilege Rights',
'Service General Setting'
)
foreach ($sectionName in $sectionOrder) {
if ($sectionsProcessed.ContainsKey($sectionName)) {
$infContent += "[$sectionName]"
$infContent += $sectionsProcessed[$sectionName]
$infContent += ""
$result.SectionsApplied++
$result.SettingsApplied += $sectionsProcessed[$sectionName].Count
}
}
# Write any remaining sections not in standard order (safety net)
foreach ($sectionName in $sectionsProcessed.Keys) {
if ($sectionName -notin $sectionOrder) {
$infContent += "[$sectionName]"
$infContent += $sectionsProcessed[$sectionName]
$infContent += ""
$result.SectionsApplied++
$result.SettingsApplied += $sectionsProcessed[$sectionName].Count
}
}
Write-Log -Level DEBUG -Message "Generated security template: $($result.SectionsApplied) sections, $($result.SettingsApplied) settings" -Module "SecurityBaseline"
if ($DryRun) {
Write-Log -Level DEBUG -Message "[DRYRUN] Security template content:" -Module "SecurityBaseline"
$infContent | ForEach-Object { Write-Log -Level DEBUG -Message " $_" -Module "SecurityBaseline" }
$result.Success = $true
return $result
}
# Initialize temp file paths
$tempInf = $null
$dbFile = $null
$logFile = $null
try {
# Save to temporary INF file
$tempInf = Join-Path $env:TEMP "SecurityBaseline_$(Get-Date -Format 'yyyyMMddHHmmss').inf"
$infContent | Out-File -FilePath $tempInf -Encoding unicode -Force
Write-Log -Level DEBUG -Message "Applying security template via secedit.exe..." -Module "SecurityBaseline"
# Apply via secedit
$dbFile = Join-Path $env:TEMP "secedit_$(Get-Date -Format 'yyyyMMddHHmmss').sdb"
$logFile = Join-Path $env:TEMP "secedit_$(Get-Date -Format 'yyyyMMddHHmmss').log"
$seceditArgs = @(
"/configure",
"/db", "`"$dbFile`"",
"/cfg", "`"$tempInf`"",
"/log", "`"$logFile`"",
"/quiet"
)
$process = Start-Process -FilePath "secedit.exe" `
-ArgumentList $seceditArgs `
-Wait `
-NoNewWindow `
-PassThru
if ($process.ExitCode -eq 0) {
$result.Success = $true
Write-Log -Level DEBUG -Message "Security template applied successfully" -Module "SecurityBaseline"
}
if ($process.ExitCode -ne 0) {
$stderr = Get-Content $logFile -Raw -ErrorAction SilentlyContinue
$result.Errors += "secedit failed with exit code $($process.ExitCode): $stderr"
Write-Log -Level DEBUG -Message "secedit failed: $stderr" -Module "SecurityBaseline"
# On error: Save INF and log to Desktop for debugging
$debugInf = Join-Path ([Environment]::GetFolderPath("Desktop")) "SecurityBaseline_ERROR.inf"
$debugLog = Join-Path ([Environment]::GetFolderPath("Desktop")) "secedit_ERROR.log"
Copy-Item $tempInf $debugInf -Force -ErrorAction SilentlyContinue
Copy-Item $logFile $debugLog -Force -ErrorAction SilentlyContinue
Write-Log -Level DEBUG -Message "Error files saved to Desktop for debugging" -Module "SecurityBaseline"
}
}
catch {
$result.Errors += "Security template application failed: $($_.Exception.Message)"
Write-Log -Level DEBUG -Message "Security template application failed: $_" -Module "SecurityBaseline"
}
finally {
# ALWAYS cleanup temp files (even on error)
if ($tempInf -and (Test-Path $tempInf)) {
Remove-Item $tempInf -Force -ErrorAction SilentlyContinue
}
if ($dbFile -and (Test-Path $dbFile)) {
Remove-Item $dbFile -Force -ErrorAction SilentlyContinue
}
if ($logFile -and (Test-Path $logFile)) {
Remove-Item $logFile -Force -ErrorAction SilentlyContinue
}
}
}
catch {
# Outer catch for JSON parsing or INF generation errors
$result.Errors += "Failed to process security template: $($_.Exception.Message)"
Write-Log -Level DEBUG -Message "Security template processing error: $_" -Module "SecurityBaseline"
}
return $result
}