Files
cadjou b156b1647f Ajout du support API v2 (package de diagnostic) et du log fichier
- AuditLogCLI.ps1 : detection auto V1/V2 avec fallback sur erreur HTTP
- Téléchargement/extraction du zip de diagnostic
- Extraction des événements connexion/déconnexion (logs eclypse_*.log et karaf.log)
- Nettoyage automatique des fichiers temporaires
- Écriture d'un fichier .log horodate en complement de la console
- README.md : documentation FR/EN de la détection V1/V2
2026-07-15 10:00:09 +02:00

457 lines
18 KiB
PowerShell

<#
.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