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
    }
}