<# .SYNOPSIS CLI d'export des journaux d'audit pour automates Distech Controls Eclypse. .DESCRIPTION AuditLogCLI recupere les journaux d'audit de chaque automate Distech Controls Eclypse liste dans un CSV d'entree, via leur API REST, et les enregistre au format CSV (un fichier par automate, ou un fichier consolide unique). Detection automatique de la version d'API par automate : - API v2 (package de diagnostic) essayee en premier. Le zip retourne est extrait et les evenements connexion/deconnexion (logger DConnection) des logs eclypse_*.log et karaf.log sont convertis en CSV. - En cas d'erreur HTTP sur l'appel v2, bascule automatique sur l'API v1 (audit-logs), qui retourne directement un CSV. .PARAMETER CsvInput Chemin vers le fichier CSV d'entree (separateur point-virgule). Colonnes obligatoires : Hostname, Current Ip, HttpPort, HttpsPort. Colonnes optionnelles : Username, Password (surchargent -Username/-Password). .PARAMETER Username Nom d'utilisateur pour l'authentification API (defaut: admin). Peut etre surcharge par la colonne Username du CSV. .PARAMETER Password Mot de passe pour l'authentification API (defaut: vide). Peut etre surcharge par la colonne Password du CSV. .PARAMETER StartDate Date de debut (format yyyy-MM-dd) pour filtrer les journaux d'audit. Optionnel. .PARAMETER EndDate Date de fin (format yyyy-MM-dd) pour filtrer les journaux d'audit. Optionnel. .PARAMETER OutputFormat Format de sortie : PerAutomate - Un fichier CSV brut par automate, dans un dossier horodate (defaut) Consolidated - Un seul fichier CSV avec toutes les lignes, colonnes Hostname/IP ajoutees .EXAMPLE .\AuditLogCLI.ps1 -CsvInput ".\automates.csv" Recupere les journaux d'audit de tous les automates listes dans le CSV. Genere un fichier CSV par automate dans un dossier Audit_yyyy-MM-dd_HH-mm. .EXAMPLE .\AuditLogCLI.ps1 -CsvInput ".\automates.csv" -OutputFormat Consolidated -StartDate 2026-01-01 -EndDate 2026-07-09 Recupere les journaux d'audit filtres sur la periode donnee et les consolide dans un seul fichier CSV. .NOTES Prerequis : PowerShell 5.1+ (inclus dans Windows 10/11) API : Distech Controls Eclypse REST API v1 (audit-logs) et v2 (package de diagnostic), detection automatique par automate Securite : TLS 1.0/1.1/1.2, certificats auto-signes acceptes Important : les noms de query params "startDate"/"endDate" (v1) sont une hypothese non validee contre la documentation officielle de l'API audit-logs. A verifier avant utilisation en production. #> param( [Parameter(Mandatory)] [string]$CsvInput, [string]$Username = "admin", [string]$Password = "", [string]$StartDate, [string]$EndDate, [ValidateSet("PerAutomate", "Consolidated")] [string]$OutputFormat = "PerAutomate" ) # Performance : desactiver la barre de progression Invoke-WebRequest $ProgressPreference = 'SilentlyContinue' # ===================================================================== # UTILITAIRES # ===================================================================== $script:LogFilePath = Join-Path (Get-Location) "AuditLogCLI_$(Get-Date -Format 'yyyy-MM-dd_HH-mm').log" function Write-Log { param( [string]$Message, [ValidateSet("INFO", "WARN", "ERROR", "OK")] [string]$Level = "INFO" ) $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $tag = switch ($Level) { "INFO" {"[INFO ]"} "WARN" {"[WARN ]"} "ERROR" {"[ERR ]"} "OK" {"[ OK ]"} } $color = switch ($Level) { "INFO" {"Cyan"} "WARN" {"Yellow"} "ERROR" {"Red"} "OK" {"Green"} } Write-Host "$ts $tag $Message" -ForegroundColor $color Add-Content -Path $script:LogFilePath -Value "$ts $tag $Message" -Encoding UTF8 } # ===================================================================== # FONCTION : Initialize-ApiClient # Force TLS 1.2 et accepte les certificats auto-signes (equivalent curl -k) # ===================================================================== $script:CurlAvailable = $false function Initialize-ApiClient { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) { Add-Type @" using System.Net; using System.Security.Cryptography.X509Certificates; public class TrustAllCertsPolicy : ICertificatePolicy { public bool CheckValidationResult( ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) { return true; } } "@ } [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy $script:CurlAvailable = [bool](Get-Command curl.exe -ErrorAction SilentlyContinue) } function Test-SslError { param([System.Exception]$Exception) $msg = $Exception.Message return ($msg -match "SSL" -or $msg -match "TLS" -or $msg -match "confiance" -or $msg -match "trust" -or $msg -match "certificate" -or $msg -match "envoi") } function Invoke-CurlGet { param( [string]$Url, [string]$Username, [AllowEmptyString()][string]$Password ) $output = & curl.exe -s -k -u "${Username}:${Password}" -H "Accept: text/csv, application/json, text/plain, */*" $Url 2>&1 if ($LASTEXITCODE -ne 0) { throw "curl GET erreur (exit code $LASTEXITCODE): $output" } return ($output | Out-String) } # ===================================================================== # FONCTION : Read-AutomateCsv # ===================================================================== function Read-AutomateCsv { param([string]$CsvPath) if (-not (Test-Path $CsvPath)) { throw "Fichier CSV introuvable : $CsvPath" } $rows = @(Import-Csv -Path $CsvPath -Delimiter ";" -Encoding UTF8) if ($rows.Count -eq 0) { throw "Fichier CSV vide : $CsvPath" } Write-Log -Message "CSV charge : $($rows.Count) ligne(s) depuis $CsvPath" -Level INFO return $rows } # ===================================================================== # FONCTION : Get-BaseUrl # HTTPS priorise sur HTTP, automate ignore si aucun port valide # ===================================================================== function Get-BaseUrl { param([PSCustomObject]$Automate) $ip = $Automate."Current Ip" $httpsPort = $Automate.HttpsPort $httpPort = $Automate.HttpPort if ($httpsPort -and $httpsPort -ne "" -and [int]$httpsPort -gt 0 -and [int]$httpsPort -ne -1) { if ([int]$httpsPort -eq 443) { return "https://$ip" } return "https://${ip}:$httpsPort" } if ($httpPort -and $httpPort -ne "" -and [int]$httpPort -gt 0 -and [int]$httpPort -ne -1) { if ([int]$httpPort -eq 80) { return "http://$ip" } return "http://${ip}:$httpPort" } return $null } # ===================================================================== # FONCTION : Get-Credentials # Identifiants CSV prioritaires sur les parametres globaux # ===================================================================== function Get-Credentials { param( [PSCustomObject]$Automate, [string]$DefaultUsername, [string]$DefaultPassword ) $username = $DefaultUsername $password = $DefaultPassword if ($Automate.Username -and $Automate.Username -ne "") { $username = $Automate.Username } if ($Automate.Password -and $Automate.Password -ne "") { $password = $Automate.Password } return @{ Username = $username; Password = $password } } function Get-AuthHeader { param( [string]$Username, [AllowEmptyString()][string]$Password ) $pair = "${Username}:${Password}" $bytes = [System.Text.Encoding]::ASCII.GetBytes($pair) $base64 = [System.Convert]::ToBase64String($bytes) return @{ "Authorization" = "Basic $base64" "Accept" = "text/csv, application/json, text/plain, */*" } } function Get-AuditLogUrl { param( [string]$BaseUrl, [string]$StartDate, [string]$EndDate ) $url = "$BaseUrl/api/rest/v1/system/audit-logs" $queryParams = @() if ($StartDate) { $queryParams += "startDate=$StartDate" } if ($EndDate) { $queryParams += "endDate=$EndDate" } if ($queryParams.Count -gt 0) { $url += "?" + ($queryParams -join "&") } return $url } function Invoke-AuditLogGet { param( [string]$Url, [string]$Username, [AllowEmptyString()][string]$Password ) $headers = Get-AuthHeader -Username $Username -Password $Password try { $response = Invoke-WebRequest -Uri $Url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 30 return $response.Content } catch { if ((Test-SslError -Exception $_.Exception) -and $script:CurlAvailable) { Write-Log -Message "Bascule sur curl.exe -k (echec TLS .NET)" -Level WARN return Invoke-CurlGet -Url $Url -Username $Username -Password $Password } throw } } # ===================================================================== # FONCTIONS : API v2 (package de diagnostic) # ===================================================================== function Get-DiagnosticPackageUrl { param([string]$BaseUrl) return "$BaseUrl/api/rest/v2/services/packages/diagnostic" } function Invoke-DiagnosticGet { param( [string]$Url, [string]$Username, [AllowEmptyString()][string]$Password ) $headers = Get-AuthHeader -Username $Username -Password $Password $zipPath = Join-Path $env:TEMP "AuditLogCLI_diag_$(Get-Random).zip" Invoke-WebRequest -Uri $Url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 120 -OutFile $zipPath return $zipPath } function Expand-DiagnosticPackage { param( [string]$ZipPath, [string]$DestinationDir ) Expand-Archive -Path $ZipPath -DestinationPath $DestinationDir -Force } # ===================================================================== # FONCTION : Get-ConnectionEvents # Extrait les evenements connexion/deconnexion (logger DConnection) des # logs eclypse_*.log et karaf.log d'un package de diagnostic V2 extrait # ===================================================================== function Get-ConnectionEvents { param( [string]$LogDir, [string]$StartDate, [string]$EndDate ) $logFiles = @(Get-ChildItem -Path $LogDir -Filter "eclypse_*.log" -ErrorAction SilentlyContinue) $karafLog = @(Get-ChildItem -Path $LogDir -Filter "karaf.log" -ErrorAction SilentlyContinue) $logFiles += $karafLog $startDt = if ($StartDate) { [DateTime]::ParseExact($StartDate, "yyyy-MM-dd", $null) } else { $null } $endDt = if ($EndDate) { [DateTime]::ParseExact($EndDate, "yyyy-MM-dd", $null).AddDays(1) } else { $null } $events = @() foreach ($logFile in $logFiles) { $hits = Select-String -Path $logFile.FullName -Pattern "DConnection.*'(.+?)' (connected|disconnected)" foreach ($hit in $hits) { $tsPart = $hit.Line.Split('|')[0].Trim() $ts = [DateTime]::MinValue if (-not [DateTime]::TryParseExact($tsPart, "yyyy-MM-ddTHH:mm:ss,fff", $null, [System.Globalization.DateTimeStyles]::None, [ref]$ts)) { continue } if ($startDt -and $ts -lt $startDt) { continue } if ($endDt -and $ts -ge $endDt) { continue } $events += [PSCustomObject]@{ Timestamp = $ts User = $hit.Matches[0].Groups[1].Value Action = $hit.Matches[0].Groups[2].Value SourceFile = $logFile.Name } } } return @($events | Sort-Object Timestamp) } function ConvertTo-DiagnosticCsv { param([array]$Events) if ($Events.Count -eq 0) { return "" } return ($Events | ConvertTo-Csv -NoTypeInformation | Out-String) } # ===================================================================== # VALIDATION DES PARAMETRES DE DATE # ===================================================================== foreach ($dateParam in @( @{ Name = "StartDate"; Value = $StartDate }, @{ Name = "EndDate"; Value = $EndDate } )) { if ($dateParam.Value) { $parsed = [DateTime]::MinValue if (-not [DateTime]::TryParseExact($dateParam.Value, "yyyy-MM-dd", $null, [System.Globalization.DateTimeStyles]::None, [ref]$parsed)) { throw "Parametre -$($dateParam.Name) invalide : '$($dateParam.Value)' (format attendu : yyyy-MM-dd)" } } } # ===================================================================== # INITIALISATION # ===================================================================== Initialize-ApiClient Write-Log -Message "=== AuditLogCLI demarre - OutputFormat: $OutputFormat ===" -Level INFO $automates = Read-AutomateCsv -CsvPath $CsvInput $statsTotal = 0 $statsError = 0 $consolidatedRows = @() $outputDir = $null if ($OutputFormat -eq "PerAutomate") { $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm" $outputDir = Join-Path (Get-Location) "Audit_$timestamp" New-Item -ItemType Directory -Path $outputDir -Force | Out-Null Write-Log -Message "Dossier de sortie : $outputDir" -Level INFO } # ===================================================================== # TRAITEMENT DE CHAQUE AUTOMATE # ===================================================================== foreach ($automate in $automates) { $hostname = $automate.Hostname $ip = $automate."Current Ip" $statsTotal++ try { $baseUrl = Get-BaseUrl -Automate $automate if (-not $baseUrl) { Write-Log -Message "[$hostname] Aucun port HTTP/HTTPS valide - ignore" -Level WARN $statsError++ continue } $creds = Get-Credentials -Automate $automate -DefaultUsername $Username -DefaultPassword $Password $content = $null $isV2 = $false $diagUrl = Get-DiagnosticPackageUrl -BaseUrl $baseUrl $zipPath = $null try { Write-Log -Message "[$hostname] GET $diagUrl (V2, user: $($creds.Username))" -Level INFO $zipPath = Invoke-DiagnosticGet -Url $diagUrl -Username $creds.Username -Password $creds.Password $isV2 = $true } catch { Write-Log -Message "[$hostname] V2 indisponible ($($_.Exception.Message)), bascule V1" -Level WARN } if ($isV2) { $tempDir = Join-Path $env:TEMP "AuditLogCLI_$($ip)_$(Get-Random)" try { Expand-DiagnosticPackage -ZipPath $zipPath -DestinationDir $tempDir $events = Get-ConnectionEvents -LogDir (Join-Path $tempDir "logs") -StartDate $StartDate -EndDate $EndDate $content = ConvertTo-DiagnosticCsv -Events $events Write-Log -Message "[$hostname] $($events.Count) evenement(s) connexion/deconnexion extrait(s) (V2)" -Level OK } finally { Remove-Item -Path $zipPath -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } else { $url = Get-AuditLogUrl -BaseUrl $baseUrl -StartDate $StartDate -EndDate $EndDate Write-Log -Message "[$hostname] GET $url (V1, user: $($creds.Username))" -Level INFO $content = Invoke-AuditLogGet -Url $url -Username $creds.Username -Password $creds.Password } if ($OutputFormat -eq "PerAutomate") { $filePath = Join-Path $outputDir "$ip.csv" Set-Content -Path $filePath -Value $content -Encoding UTF8 Write-Log -Message "[$hostname] Journal d'audit ecrit : $filePath" -Level OK } else { # Suppose une reponse API au format CSV (comme le .bat d'origine qui # ecrit directement la reponse brute dans un fichier .csv) $rows = @($content | ConvertFrom-Csv) foreach ($row in $rows) { $obj = [ordered]@{ Hostname = $hostname; IP = $ip } foreach ($prop in $row.PSObject.Properties) { $obj[$prop.Name] = $prop.Value } $consolidatedRows += [PSCustomObject]$obj } Write-Log -Message "[$hostname] $($rows.Count) entree(s) d'audit recuperee(s)" -Level OK } } catch { Write-Log -Message "[$hostname] ERREUR : $($_.Exception.Message)" -Level ERROR $statsError++ } } # ===================================================================== # ECRITURE DE LA SORTIE CONSOLIDEE # ===================================================================== if ($OutputFormat -eq "Consolidated") { $timestamp = Get-Date -Format "yyyy-MM-dd_HHhmm" $outputFile = Join-Path (Get-Location) "audit_$timestamp.csv" if ($consolidatedRows.Count -gt 0) { $consolidatedRows | Export-Csv -Path $outputFile -NoTypeInformation -Encoding UTF8 Write-Log -Message "CSV consolide ecrit : $outputFile ($($consolidatedRows.Count) ligne(s))" -Level OK } else { Write-Log -Message "Aucune donnee a ecrire dans le CSV consolide" -Level WARN } } # ===================================================================== # RESUME FINAL # ===================================================================== Write-Log -Message "========== RESUME ==========" -Level INFO Write-Log -Message "Automates traites : $statsTotal" -Level INFO Write-Log -Message "Automates en erreur : $statsError" -Level $(if ($statsError -gt 0) { "WARN" } else { "INFO" }) Write-Log -Message "============================" -Level INFO