b156b1647f
- 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
457 lines
18 KiB
PowerShell
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
|