Public/Import-AgentRegistration.ps1
|
function Import-AgentRegistration { <# .SYNOPSIS Processes JSON registration files dropped by self-registration agents. .DESCRIPTION Reads JSON files from a shared folder where non-Windows agents have deposited their registration data. For each JSON file, validates the token, then creates or updates an AD computer object with the full OS details, IP address, and hardware summary in the description. New registrations require confirmation unless -AutoApprove is specified. Updates to existing objects are always auto-approved. Processed files are moved to a "processed" subfolder with a timestamp. .PARAMETER RegistrationPath Path to the shared folder containing JSON registration files from agents. .PARAMETER Token Expected shared secret. If specified, JSON files with a mismatched token are rejected. If not specified, token validation is skipped. .PARAMETER OrganizationalUnit Target OU for new computer objects. Defaults to "OU=Non-Windows Servers" under the domain root. .PARAMETER AutoApprove Skip confirmation prompts for new registrations. Updates are always auto-approved regardless of this switch. .PARAMETER OutputPath If specified, generates an HTML report of the processing results. .EXAMPLE Import-AgentRegistration -RegistrationPath '\\fileserver\registrations$' -Token 'mySecret' -AutoApprove Processes all pending JSON registrations, auto-approving new entries. .EXAMPLE Import-AgentRegistration -RegistrationPath 'C:\Registrations' -Token 'mySecret' Processes registrations with manual confirmation for new systems. .EXAMPLE Import-AgentRegistration -RegistrationPath '\\fs01\reg$' -Token 's3cret' -OutputPath '.\report.html' Processes registrations and generates an HTML summary report. .NOTES Requires: ActiveDirectory module (RSAT). JSON files are expected to be produced by the AD-LinuxInventory agent (register-agent.sh). Each file is named {hostname}.json. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory, Position = 0)] [ValidateScript({ if (-not (Test-Path $_)) { throw "Registration path not found: $_" } $true })] [string]$RegistrationPath, [string]$Token, [string]$OrganizationalUnit, [switch]$AutoApprove, [string]$OutputPath ) begin { Import-Module ActiveDirectory -ErrorAction Stop if (-not $OrganizationalUnit) { $domainDN = (Get-ADDomain).DistinguishedName $OrganizationalUnit = "OU=Non-Windows Servers,$domainDN" } # Ensure the target OU exists Initialize-LinuxOU -OrganizationalUnit $OrganizationalUnit # Ensure the processed subfolder exists $processedDir = Join-Path -Path $RegistrationPath -ChildPath 'processed' if (-not (Test-Path $processedDir)) { New-Item -Path $processedDir -ItemType Directory -Force | Out-Null } $processedCount = 0 $newCount = 0 $updatedCount = 0 $rejectedCount = 0 $errorCount = 0 $results = [System.Collections.Generic.List[PSCustomObject]]::new() } process { $jsonFiles = Get-ChildItem -Path $RegistrationPath -Filter '*.json' -File -ErrorAction SilentlyContinue if (-not $jsonFiles -or @($jsonFiles).Count -eq 0) { Write-Verbose "No JSON registration files found in $RegistrationPath" return } Write-Verbose "Found $(@($jsonFiles).Count) JSON registration file(s) in $RegistrationPath" foreach ($jsonFile in $jsonFiles) { $processedCount++ Write-Verbose "Processing: $($jsonFile.Name)" # Parse JSON try { $regData = Get-Content -Path $jsonFile.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { $errorCount++ Write-Warning "Failed to parse $($jsonFile.Name): $_" $results.Add([PSCustomObject]@{ File = $jsonFile.Name Hostname = 'PARSE_ERROR' Action = 'Error' Detail = "JSON parse failed: $_" }) continue } # Validate required fields if ([string]::IsNullOrWhiteSpace($regData.hostname)) { $errorCount++ Write-Warning "File $($jsonFile.Name) is missing 'hostname' field. Skipping." $results.Add([PSCustomObject]@{ File = $jsonFile.Name Hostname = 'MISSING' Action = 'Error' Detail = 'Missing hostname field' }) continue } # Validate token if ($Token) { if ($regData.token -ne $Token) { $rejectedCount++ Write-Warning "Token mismatch for $($regData.hostname) ($($jsonFile.Name)). Rejecting." $results.Add([PSCustomObject]@{ File = $jsonFile.Name Hostname = $regData.hostname Action = 'Rejected' Detail = 'Token mismatch' }) continue } } $computerName = $regData.hostname.ToUpper() $dnsHostName = if ($regData.fqdn) { $regData.fqdn } else { $regData.hostname } # Get the first IP address $ipAddress = $null if ($regData.ip_addresses -and @($regData.ip_addresses).Count -gt 0) { $ipAddress = @($regData.ip_addresses)[0] } # Build the description $osFamily = if ($regData.os_family) { $regData.os_family } else { 'Unknown' } $cpuCores = if ($regData.cpu_cores) { $regData.cpu_cores } else { '?' } $memoryGb = if ($regData.memory_gb) { $regData.memory_gb } else { '?' } $registeredAt = if ($regData.registered_at) { $regData.registered_at } else { (Get-Date -Format 'yyyy-MM-dd HH:mm') } $description = "$osFamily | $cpuCores cores | ${memoryGb}GB RAM | Last seen: $registeredAt" $operatingSystem = if ($regData.operating_system) { $regData.operating_system } else { $osFamily } $operatingSystemVersion = if ($regData.operating_system_version) { $regData.operating_system_version } else { '' } # Check if the computer already exists $existing = $null try { $existing = Get-ADComputer -Identity $computerName -Properties OperatingSystem, OperatingSystemVersion, IPv4Address, Description -ErrorAction Stop } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { # Does not exist -- new registration } if ($existing) { # UPDATE existing object (auto-approved) if ($PSCmdlet.ShouldProcess($computerName, 'Update existing AD computer object from agent registration')) { try { $replaceHash = @{ 'OperatingSystem' = $operatingSystem 'Description' = $description } if ($operatingSystemVersion) { $replaceHash['OperatingSystemVersion'] = $operatingSystemVersion } if ($ipAddress) { $replaceHash['IPv4Address'] = $ipAddress } Set-ADComputer -Identity $computerName -Replace $replaceHash -ErrorAction Stop $updatedCount++ Write-Verbose "Updated existing entry: $computerName (OS: $operatingSystem)" $results.Add([PSCustomObject]@{ File = $jsonFile.Name Hostname = $computerName Action = 'Updated' Detail = "OS: $operatingSystem, IP: $ipAddress" }) } catch { $errorCount++ Write-Warning "Failed to update $computerName : $_" $results.Add([PSCustomObject]@{ File = $jsonFile.Name Hostname = $computerName Action = 'Error' Detail = "Update failed: $_" }) continue } } } else { # NEW registration $shouldCreate = $AutoApprove if (-not $AutoApprove) { $confirmMsg = "Register NEW system: $computerName ($operatingSystem, IP: $ipAddress)" if ($PSCmdlet.ShouldProcess($computerName, $confirmMsg)) { $shouldCreate = $true } } if ($shouldCreate) { if ($PSCmdlet.ShouldProcess($computerName, 'Create AD computer object from agent registration')) { try { # Create the computer object $newParams = @{ Name = $computerName SAMAccountName = "$computerName$" Path = $OrganizationalUnit DNSHostName = $dnsHostName Description = $description Enabled = $true ErrorAction = 'Stop' } New-ADComputer @newParams # Set extended properties $replaceHash = @{ 'OperatingSystem' = $operatingSystem } if ($operatingSystemVersion) { $replaceHash['OperatingSystemVersion'] = $operatingSystemVersion } if ($ipAddress) { $replaceHash['IPv4Address'] = $ipAddress } Set-ADComputer -Identity $computerName -Replace $replaceHash -ErrorAction Stop $newCount++ Write-Verbose "Registered new system: $computerName (OS: $operatingSystem)" $results.Add([PSCustomObject]@{ File = $jsonFile.Name Hostname = $computerName Action = 'Created' Detail = "OS: $operatingSystem, IP: $ipAddress" }) } catch { $errorCount++ Write-Warning "Failed to register $computerName : $_" $results.Add([PSCustomObject]@{ File = $jsonFile.Name Hostname = $computerName Action = 'Error' Detail = "Creation failed: $_" }) continue } } } else { $rejectedCount++ Write-Verbose "Registration declined for $computerName" $results.Add([PSCustomObject]@{ File = $jsonFile.Name Hostname = $computerName Action = 'Declined' Detail = 'User declined confirmation' }) continue } } # Archive the processed JSON file $archiveName = "{0}_{1}_{2}" -f (Get-Date -Format 'yyyyMMdd-HHmmss'), $computerName, $jsonFile.Name $archivePath = Join-Path -Path $processedDir -ChildPath $archiveName try { Move-Item -Path $jsonFile.FullName -Destination $archivePath -Force -ErrorAction Stop Write-Verbose "Archived $($jsonFile.Name) to processed/$archiveName" } catch { Write-Warning "Failed to archive $($jsonFile.Name): $_" } } } end { $summary = [PSCustomObject]@{ RegistrationPath = $RegistrationPath Processed = $processedCount NewRegistrations = $newCount Updated = $updatedCount Rejected = $rejectedCount Errors = $errorCount Details = $results } Write-Verbose "Import complete -- New: $newCount, Updated: $updatedCount, Rejected: $rejectedCount, Errors: $errorCount" # Generate HTML report if requested if ($OutputPath) { $reportDir = Split-Path -Path $OutputPath -Parent if ($reportDir -and -not (Test-Path $reportDir)) { New-Item -Path $reportDir -ItemType Directory -Force | Out-Null } $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $tableRows = ($results | ForEach-Object { $actionClass = switch ($_.Action) { 'Created' { 'finding-ok' } 'Updated' { 'finding-ok' } 'Rejected' { 'finding-bad' } 'Declined' { 'finding-bad' } 'Error' { 'finding-bad' } default { '' } } "<tr><td>$($_.File)</td><td>$($_.Hostname)</td><td class=`"$actionClass`">$($_.Action)</td><td>$($_.Detail)</td></tr>" }) -join "`n " $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Agent Registration Report</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; } .header { background: linear-gradient(135deg, #1a1f2e 0%, #2a1a0a 100%); padding: 2rem; border-radius: 8px; margin-bottom: 2rem; } .header h1 { color: #d29922; font-size: 1.8rem; margin-bottom: 0.5rem; } .header .meta { color: #8b949e; font-size: 0.9rem; } .summary-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 2rem; } .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; text-align: center; } .card .number { font-size: 2.5rem; font-weight: 700; } .card .label { color: #8b949e; font-size: 0.85rem; margin-top: 0.5rem; } .card.new .number { color: #3fb950; } .card.updated .number { color: #58a6ff; } .card.rejected .number { color: #f85149; } .card.errors .number { color: #f85149; } .card.total .number { color: #d29922; } .section { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; } .section h2 { color: #d29922; font-size: 1.3rem; margin-bottom: 1rem; } .table-wrapper { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } th { background: #21262d; color: #d29922; padding: 0.75rem; text-align: left; border-bottom: 2px solid #30363d; } td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #21262d; } tr:hover { background: #1c2128; } .finding-bad { color: #f85149; font-weight: 600; } .finding-ok { color: #3fb950; } .footer { text-align: center; color: #484f58; margin-top: 2rem; font-size: 0.8rem; } </style> </head> <body> <div class="header"> <h1>Agent Registration Report</h1> <div class="meta">Generated $timestamp | Source: $RegistrationPath | AD-LinuxInventory v2.0.0</div> </div> <div class="summary-cards"> <div class="card total"><div class="number">$processedCount</div><div class="label">Processed</div></div> <div class="card new"><div class="number">$newCount</div><div class="label">New</div></div> <div class="card updated"><div class="number">$updatedCount</div><div class="label">Updated</div></div> <div class="card rejected"><div class="number">$rejectedCount</div><div class="label">Rejected</div></div> <div class="card errors"><div class="number">$errorCount</div><div class="label">Errors</div></div> </div> <div class="section"> <h2>Processing Details</h2> <div class="table-wrapper"> <table> <thead><tr><th>File</th><th>Hostname</th><th>Action</th><th>Detail</th></tr></thead> <tbody> $tableRows </tbody> </table> </div> </div> <div class="footer">Generated by AD-LinuxInventory | github.com/larro1991/AD-LinuxInventory</div> </body> </html> "@ $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force Write-Verbose "HTML report saved: $OutputPath" } $summary } } |