From 5719c7a756a538e071033bddb0fcb713979a04b4 Mon Sep 17 00:00:00 2001
From: BitWuehler <39520752+BitWuehler@users.noreply.github.com>
Date: Wed, 10 Jun 2026 22:56:21 +0200
Subject: [PATCH 1/4] better report
---
app/web_routes.py | 9 ++++++++-
client/ScanOpClient.ps1 | 2 +-
docker-compose.yml | 4 ++++
templates/daily_report.html | 2 +-
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/app/web_routes.py b/app/web_routes.py
index dcf5a34..b5794ca 100644
--- a/app/web_routes.py
+++ b/app/web_routes.py
@@ -165,7 +165,12 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st
if historical_report.threats_found is True:
scan_result = "Fund!"
- threats_str = historical_report.threat_details if historical_report.threat_details else "Ja"
+ if historical_report.threat_details:
+ threats_str = historical_report.threat_details
+ elif historical_report.scan_result_message:
+ threats_str = historical_report.scan_result_message
+ else:
+ threats_str = "Ja"
else:
scan_result = historical_report.scan_result_message or "Keine Meldung"
if len(scan_result) > 50:
@@ -232,6 +237,8 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No
if laptop.last_scan_threats_found is True:
if getattr(laptop, "last_scan_threat_details", None):
status_text = laptop.last_scan_threat_details
+ elif laptop.last_scan_result_message:
+ status_text = laptop.last_scan_result_message
else:
status_text = "Bedrohung(en)!"
color_class = "status-red"
diff --git a/client/ScanOpClient.ps1 b/client/ScanOpClient.ps1
index 41459f1..8f38184 100644
--- a/client/ScanOpClient.ps1
+++ b/client/ScanOpClient.ps1
@@ -91,7 +91,7 @@ while ($true) {
$CommandUrl = "$ServerBaseUrl/api/v1/clientcommands/$($AliasName)?version=$($ClientVersion)"; $ReportUrl = "$ServerBaseUrl/api/v1/scanreports/"
# --- Hilfsfunktionen (unverändert) ---
- function Send-ScanReport { param( [Parameter(Mandatory = $true)][string]$ScanTime, [Parameter(Mandatory = $true)][string]$ScanType, [Parameter(Mandatory = $true)][string]$ScanResultMessage, [Parameter(Mandatory = $true)][bool]$ThreatsFound, [string]$ThreatDetails = $null ); Write-Log -Message "Bereite Scan-Bericht ($ScanType) für Versand vor."; if ([string]::IsNullOrWhiteSpace($ScanTime)) { $ScanTime = (Get-Date "1970-01-01").ToUniversalTime().ToString("o") } ; if ([string]::IsNullOrWhiteSpace($ScanType)) { $ScanType = "Unbekannt" } ; if ([string]::IsNullOrWhiteSpace($ScanResultMessage)) { $ScanResultMessage = "Keine Meldung" } ; $CleanResultMessage = $ScanResultMessage -replace '[\x00-\x1F\x7F]', '' ; $CleanThreatDetails = if ($ThreatDetails) { $ThreatDetails -replace '[\x00-\x1F\x7F]', '' } else { $null } ; $payloadContent = @{ laptop_identifier = $AliasName; client_scan_time = $ScanTime; scan_type = $ScanType; scan_result_message = $CleanResultMessage; threats_found = $ThreatsFound }; if ($null -ne $CleanThreatDetails -and (-not [string]::IsNullOrWhiteSpace($CleanThreatDetails))) { $payloadContent.threat_details = $CleanThreatDetails } else { $payloadContent.threat_details = $null } ; $payloadBodyJson = $payloadContent | ConvertTo-Json -Depth 5 -Compress; $utf8Encoding = [System.Text.Encoding]::UTF8; $payloadBytes = $utf8Encoding.GetBytes($payloadBodyJson); $requestHeaders = @{ "Content-Type" = "application/json; charset=utf-8"; "X-API-Key" = $ApiKey }; Write-Log -Message "Sende Bericht... (Länge: $($payloadBytes.Length) bytes)"; $ErrorActionPreferenceBackup = $ErrorActionPreference; $ErrorActionPreference = "Stop"; try { Invoke-RestMethod -Uri $ReportUrl -Method Post -Body $payloadBytes -Headers $requestHeaders -TimeoutSec 120; Write-Log -Message "Scan-Bericht erfolgreich an Server gesendet."; $Global:LastSuccessfulReportTimeUTC = (Get-Date).ToUniversalTime(); try { ($Global:LastSuccessfulReportTimeUTC.ToString("o") | ConvertTo-Json -Compress) | Set-Content -Path $LastReportTimeFilePath -Force -Encoding UTF8; Write-Log -Message "Letzte erfolgreiche Report-Zeit aktualisiert: $($Global:LastSuccessfulReportTimeUTC.ToLocalTime())" } catch { Write-Log -Level WARN -Message "Fehler beim Speichern von '$LastReportTimeFilePath': $($_.Exception.Message)" }; return $true } catch { $CaughtException = $_; Write-Log -Level ERROR -Message "FEHLER bei Send-ScanReport: $($CaughtException.ToString())"; if ($CaughtException.Exception -is [System.Net.WebException] -and $null -ne $CaughtException.Exception.Response) { $webEx = $CaughtException.Exception; $httpResponse = $webEx.Response; $actualHttpStatusCode = [int]$httpResponse.StatusCode; Write-Log -Level ERROR -Message " HTTP Status: $actualHttpStatusCode"; try { $responseStream = $httpResponse.GetResponseStream(); $streamReader = New-Object System.IO.StreamReader($responseStream, [System.Text.Encoding]::UTF8); $errorBodyContent = $streamReader.ReadToEnd(); $streamReader.Close(); $responseStream.Close(); Write-Log -Level ERROR -Message " Fehler-Body vom Server: $errorBodyContent" } catch { Write-Log -Level ERROR -Message " Zusätzlicher Fehler beim Lesen des Fehler-Bodys: $($_.Exception.Message)" } }; return $false } finally { $ErrorActionPreference = $ErrorActionPreferenceBackup } }; function ConvertFrom-DefenderEvent { param( [Parameter(Mandatory = $true)] $Event ); $eventTimeUTC = $Event.TimeCreated.ToUniversalTime().ToString("o"); $simplified = @{ Message = "Event $($Event.Id): " + (($Event.Message -replace '[\x00-\x1F\x7F]', '').Trim() -split '\r?\n')[0]; ThreatsFound = $false; ThreatDetails = $null }; if ($simplified.Message -match "Bedrohung gefunden") { $simplified.ThreatsFound = $true }; if ($Event.Id -in (1002, 1116, 1117, 1118)) { $simplified.ThreatsFound = $true }; if ($Event.Message -match "Name: (.*?)\s*Pfad: (.*?)\s*Aktion: (.*?)\s*") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim()), Aktion: $($Matches[3].Trim())" }; return [PSCustomObject]$simplified }
+ function Send-ScanReport { param( [Parameter(Mandatory = $true)][string]$ScanTime, [Parameter(Mandatory = $true)][string]$ScanType, [Parameter(Mandatory = $true)][string]$ScanResultMessage, [Parameter(Mandatory = $true)][bool]$ThreatsFound, [string]$ThreatDetails = $null ); Write-Log -Message "Bereite Scan-Bericht ($ScanType) für Versand vor."; if ([string]::IsNullOrWhiteSpace($ScanTime)) { $ScanTime = (Get-Date "1970-01-01").ToUniversalTime().ToString("o") } ; if ([string]::IsNullOrWhiteSpace($ScanType)) { $ScanType = "Unbekannt" } ; if ([string]::IsNullOrWhiteSpace($ScanResultMessage)) { $ScanResultMessage = "Keine Meldung" } ; $CleanResultMessage = $ScanResultMessage -replace '[\x00-\x1F\x7F]', '' ; $CleanThreatDetails = if ($ThreatDetails) { $ThreatDetails -replace '[\x00-\x1F\x7F]', '' } else { $null } ; $payloadContent = @{ laptop_identifier = $AliasName; client_scan_time = $ScanTime; scan_type = $ScanType; scan_result_message = $CleanResultMessage; threats_found = $ThreatsFound }; if ($null -ne $CleanThreatDetails -and (-not [string]::IsNullOrWhiteSpace($CleanThreatDetails))) { $payloadContent.threat_details = $CleanThreatDetails } else { $payloadContent.threat_details = $null } ; $payloadBodyJson = $payloadContent | ConvertTo-Json -Depth 5 -Compress; $utf8Encoding = [System.Text.Encoding]::UTF8; $payloadBytes = $utf8Encoding.GetBytes($payloadBodyJson); $requestHeaders = @{ "Content-Type" = "application/json; charset=utf-8"; "X-API-Key" = $ApiKey }; Write-Log -Message "Sende Bericht... (Länge: $($payloadBytes.Length) bytes)"; $ErrorActionPreferenceBackup = $ErrorActionPreference; $ErrorActionPreference = "Stop"; try { Invoke-RestMethod -Uri $ReportUrl -Method Post -Body $payloadBytes -Headers $requestHeaders -TimeoutSec 120; Write-Log -Message "Scan-Bericht erfolgreich an Server gesendet."; $Global:LastSuccessfulReportTimeUTC = (Get-Date).ToUniversalTime(); try { ($Global:LastSuccessfulReportTimeUTC.ToString("o") | ConvertTo-Json -Compress) | Set-Content -Path $LastReportTimeFilePath -Force -Encoding UTF8; Write-Log -Message "Letzte erfolgreiche Report-Zeit aktualisiert: $($Global:LastSuccessfulReportTimeUTC.ToLocalTime())" } catch { Write-Log -Level WARN -Message "Fehler beim Speichern von '$LastReportTimeFilePath': $($_.Exception.Message)" }; return $true } catch { $CaughtException = $_; Write-Log -Level ERROR -Message "FEHLER bei Send-ScanReport: $($CaughtException.ToString())"; if ($CaughtException.Exception -is [System.Net.WebException] -and $null -ne $CaughtException.Exception.Response) { $webEx = $CaughtException.Exception; $httpResponse = $webEx.Response; $actualHttpStatusCode = [int]$httpResponse.StatusCode; Write-Log -Level ERROR -Message " HTTP Status: $actualHttpStatusCode"; try { $responseStream = $httpResponse.GetResponseStream(); $streamReader = New-Object System.IO.StreamReader($responseStream, [System.Text.Encoding]::UTF8); $errorBodyContent = $streamReader.ReadToEnd(); $streamReader.Close(); $responseStream.Close(); Write-Log -Level ERROR -Message " Fehler-Body vom Server: $errorBodyContent" } catch { Write-Log -Level ERROR -Message " Zusätzlicher Fehler beim Lesen des Fehler-Bodys: $($_.Exception.Message)" } }; return $false } finally { $ErrorActionPreference = $ErrorActionPreferenceBackup } }; function ConvertFrom-DefenderEvent { param( [Parameter(Mandatory = $true)] $Event ); $eventTimeUTC = $Event.TimeCreated.ToUniversalTime().ToString("o"); $simplified = @{ Message = "Event $($Event.Id): " + (($Event.Message -replace '[\x00-\x1F\x7F]', '').Trim() -split '\r?\n')[0]; ThreatsFound = $false; ThreatDetails = $null }; if ($simplified.Message -match "Bedrohung gefunden" -or $simplified.Message -match "Malware found") { $simplified.ThreatsFound = $true }; if ($Event.Id -in (1002, 1116, 1117, 1118)) { $simplified.ThreatsFound = $true }; if ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)\s*(?:Aktion|Action):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim()), Aktion: $($Matches[3].Trim())" } elseif ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim())" }; return [PSCustomObject]$simplified }
# --- HAUPT-POLLING-SCHLEIFE ---
$currentRetryDelay = $InitialRetryDelaySeconds
diff --git a/docker-compose.yml b/docker-compose.yml
index b3da17a..1d778c6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,6 +12,10 @@ services:
# Verbindet den Ordner "data" auf dem Host mit dem Ordner "/app/data" im Container.
# Dadurch wird die Datenbankdatei persistent gespeichert und überlebt Container-Neustarts.
- ./data:/app/data
+ # Binde den lokalen Code ein, damit Änderungen sofort sichtbar werden:
+ - ./app:/app/app
+ - ./templates:/app/templates
+ - ./client:/app/client
environment:
# Setzt die Umgebungsvariablen für die Anwendung.
# Pydantic-Settings priorisiert diese über eine eventuelle .env-Datei.
diff --git a/templates/daily_report.html b/templates/daily_report.html
index 5128920..84f35dc 100644
--- a/templates/daily_report.html
+++ b/templates/daily_report.html
@@ -141,7 +141,7 @@
+ |
{{ item.status_text }}
|
From 24a36ae39c63aa090c6d6b5f4f56058bb443d0af Mon Sep 17 00:00:00 2001
From: BitWuehler <39520752+BitWuehler@users.noreply.github.com>
Date: Wed, 10 Jun 2026 23:06:28 +0200
Subject: [PATCH 2/4] Fix Defender Event handling, text formatting, and
retroactively classify Event 1002 as error instead of threat
---
app/web_routes.py | 26 ++++++++++++++++++++++++--
client/ScanOpClient.ps1 | 16 +++++++++-------
2 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/app/web_routes.py b/app/web_routes.py
index b5794ca..ffb04a6 100644
--- a/app/web_routes.py
+++ b/app/web_routes.py
@@ -163,7 +163,14 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st
scan_time_berlin = historical_report.client_scan_time.replace(tzinfo=timezone.utc).astimezone(berlin_tz)
scan_time_str = scan_time_berlin.strftime('%d.%m.%Y %H:%M:%S')
- if historical_report.threats_found is True:
+ is_real_threat = historical_report.threats_found
+ is_error = False
+ if historical_report.scan_result_message and ("Event 1002" in historical_report.scan_result_message or "FEHLER:" in historical_report.scan_result_message or "stopped" in historical_report.scan_result_message or "Fehler" in historical_report.scan_result_message):
+ if "Event 1002" in historical_report.scan_result_message:
+ is_real_threat = False
+ is_error = True
+
+ if is_real_threat is True:
scan_result = "Fund!"
if historical_report.threat_details:
threats_str = historical_report.threat_details
@@ -171,6 +178,9 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st
threats_str = historical_report.scan_result_message
else:
threats_str = "Ja"
+ elif is_error:
+ scan_result = "Fehler"
+ threats_str = historical_report.scan_result_message or "Fehler aufgetreten"
else:
scan_result = historical_report.scan_result_message or "Keine Meldung"
if len(scan_result) > 50:
@@ -234,7 +244,16 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No
status_text, color_class = "N/A", "status-white"
if laptop.last_scan_time is not None:
last_scan_time_aware = laptop.last_scan_time.replace(tzinfo=timezone.utc)
- if laptop.last_scan_threats_found is True:
+
+ # Retroactively fix old DB entries where Event 1002 was marked as a threat
+ is_real_threat = laptop.last_scan_threats_found
+ is_error = False
+ if laptop.last_scan_result_message and ("Event 1002" in laptop.last_scan_result_message or "FEHLER:" in laptop.last_scan_result_message or "stopped" in laptop.last_scan_result_message or "Fehler" in laptop.last_scan_result_message):
+ if "Event 1002" in laptop.last_scan_result_message:
+ is_real_threat = False # 1002 is just a cancelled scan, not a threat
+ is_error = True
+
+ if is_real_threat is True:
if getattr(laptop, "last_scan_threat_details", None):
status_text = laptop.last_scan_threat_details
elif laptop.last_scan_result_message:
@@ -242,6 +261,9 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No
else:
status_text = "Bedrohung(en)!"
color_class = "status-red"
+ elif is_error:
+ status_text = laptop.last_scan_result_message
+ color_class = "status-yellow"
elif (now_utc.date() == last_scan_time_aware.date()) and (now_utc - last_scan_time_aware) <= timedelta(days=1):
status_text, color_class = "OK (Scan heute)", "status-green"
elif (now_utc - last_scan_time_aware) <= timedelta(days=1):
diff --git a/client/ScanOpClient.ps1 b/client/ScanOpClient.ps1
index 8f38184..5ca664d 100644
--- a/client/ScanOpClient.ps1
+++ b/client/ScanOpClient.ps1
@@ -91,7 +91,11 @@ while ($true) {
$CommandUrl = "$ServerBaseUrl/api/v1/clientcommands/$($AliasName)?version=$($ClientVersion)"; $ReportUrl = "$ServerBaseUrl/api/v1/scanreports/"
# --- Hilfsfunktionen (unverändert) ---
- function Send-ScanReport { param( [Parameter(Mandatory = $true)][string]$ScanTime, [Parameter(Mandatory = $true)][string]$ScanType, [Parameter(Mandatory = $true)][string]$ScanResultMessage, [Parameter(Mandatory = $true)][bool]$ThreatsFound, [string]$ThreatDetails = $null ); Write-Log -Message "Bereite Scan-Bericht ($ScanType) für Versand vor."; if ([string]::IsNullOrWhiteSpace($ScanTime)) { $ScanTime = (Get-Date "1970-01-01").ToUniversalTime().ToString("o") } ; if ([string]::IsNullOrWhiteSpace($ScanType)) { $ScanType = "Unbekannt" } ; if ([string]::IsNullOrWhiteSpace($ScanResultMessage)) { $ScanResultMessage = "Keine Meldung" } ; $CleanResultMessage = $ScanResultMessage -replace '[\x00-\x1F\x7F]', '' ; $CleanThreatDetails = if ($ThreatDetails) { $ThreatDetails -replace '[\x00-\x1F\x7F]', '' } else { $null } ; $payloadContent = @{ laptop_identifier = $AliasName; client_scan_time = $ScanTime; scan_type = $ScanType; scan_result_message = $CleanResultMessage; threats_found = $ThreatsFound }; if ($null -ne $CleanThreatDetails -and (-not [string]::IsNullOrWhiteSpace($CleanThreatDetails))) { $payloadContent.threat_details = $CleanThreatDetails } else { $payloadContent.threat_details = $null } ; $payloadBodyJson = $payloadContent | ConvertTo-Json -Depth 5 -Compress; $utf8Encoding = [System.Text.Encoding]::UTF8; $payloadBytes = $utf8Encoding.GetBytes($payloadBodyJson); $requestHeaders = @{ "Content-Type" = "application/json; charset=utf-8"; "X-API-Key" = $ApiKey }; Write-Log -Message "Sende Bericht... (Länge: $($payloadBytes.Length) bytes)"; $ErrorActionPreferenceBackup = $ErrorActionPreference; $ErrorActionPreference = "Stop"; try { Invoke-RestMethod -Uri $ReportUrl -Method Post -Body $payloadBytes -Headers $requestHeaders -TimeoutSec 120; Write-Log -Message "Scan-Bericht erfolgreich an Server gesendet."; $Global:LastSuccessfulReportTimeUTC = (Get-Date).ToUniversalTime(); try { ($Global:LastSuccessfulReportTimeUTC.ToString("o") | ConvertTo-Json -Compress) | Set-Content -Path $LastReportTimeFilePath -Force -Encoding UTF8; Write-Log -Message "Letzte erfolgreiche Report-Zeit aktualisiert: $($Global:LastSuccessfulReportTimeUTC.ToLocalTime())" } catch { Write-Log -Level WARN -Message "Fehler beim Speichern von '$LastReportTimeFilePath': $($_.Exception.Message)" }; return $true } catch { $CaughtException = $_; Write-Log -Level ERROR -Message "FEHLER bei Send-ScanReport: $($CaughtException.ToString())"; if ($CaughtException.Exception -is [System.Net.WebException] -and $null -ne $CaughtException.Exception.Response) { $webEx = $CaughtException.Exception; $httpResponse = $webEx.Response; $actualHttpStatusCode = [int]$httpResponse.StatusCode; Write-Log -Level ERROR -Message " HTTP Status: $actualHttpStatusCode"; try { $responseStream = $httpResponse.GetResponseStream(); $streamReader = New-Object System.IO.StreamReader($responseStream, [System.Text.Encoding]::UTF8); $errorBodyContent = $streamReader.ReadToEnd(); $streamReader.Close(); $responseStream.Close(); Write-Log -Level ERROR -Message " Fehler-Body vom Server: $errorBodyContent" } catch { Write-Log -Level ERROR -Message " Zusätzlicher Fehler beim Lesen des Fehler-Bodys: $($_.Exception.Message)" } }; return $false } finally { $ErrorActionPreference = $ErrorActionPreferenceBackup } }; function ConvertFrom-DefenderEvent { param( [Parameter(Mandatory = $true)] $Event ); $eventTimeUTC = $Event.TimeCreated.ToUniversalTime().ToString("o"); $simplified = @{ Message = "Event $($Event.Id): " + (($Event.Message -replace '[\x00-\x1F\x7F]', '').Trim() -split '\r?\n')[0]; ThreatsFound = $false; ThreatDetails = $null }; if ($simplified.Message -match "Bedrohung gefunden" -or $simplified.Message -match "Malware found") { $simplified.ThreatsFound = $true }; if ($Event.Id -in (1002, 1116, 1117, 1118)) { $simplified.ThreatsFound = $true }; if ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)\s*(?:Aktion|Action):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim()), Aktion: $($Matches[3].Trim())" } elseif ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim())" }; return [PSCustomObject]$simplified }
+ function Send-ScanReport { param( [Parameter(Mandatory = $true)][string]$ScanTime, [Parameter(Mandatory = $true)][string]$ScanType, [Parameter(Mandatory = $true)][string]$ScanResultMessage, [Parameter(Mandatory = $true)][bool]$ThreatsFound, [string]$ThreatDetails = $null ); Write-Log -Message "Bereite Scan-Bericht ($ScanType) für Versand vor."; if ([string]::IsNullOrWhiteSpace($ScanTime)) { $ScanTime = (Get-Date "1970-01-01").ToUniversalTime().ToString("o") } ; if ([string]::IsNullOrWhiteSpace($ScanType)) { $ScanType = "Unbekannt" } ; if ([string]::IsNullOrWhiteSpace($ScanResultMessage)) { $ScanResultMessage = "Keine Meldung" } ; $CleanResultMessage = $ScanResultMessage -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' ; $CleanThreatDetails = if ($ThreatDetails) { $ThreatDetails -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' } else { $null } ; $payloadContent = @{ laptop_identifier = $AliasName; client_scan_time = $ScanTime; scan_type = $ScanType; scan_result_message = $CleanResultMessage; threats_found = $ThreatsFound }; if ($null -ne $CleanThreatDetails -and (-not [string]::IsNullOrWhiteSpace($CleanThreatDetails))) { $payloadContent.threat_details = $CleanThreatDetails } else { $payloadContent.threat_details = $null } ; $payloadBodyJson = $payloadContent | ConvertTo-Json -Depth 5 -Compress; $utf8Encoding = [System.Text.Encoding]::UTF8; $payloadBytes = $utf8Encoding.GetBytes($payloadBodyJson); $requestHeaders = @{ "Content-Type" = "application/json; charset=utf-8"; "X-API-Key" = $ApiKey }; Write-Log -Message "Sende Bericht... (Länge: $($payloadBytes.Length) bytes)"; $ErrorActionPreferenceBackup = $ErrorActionPreference; $ErrorActionPreference = "Stop"; try { Invoke-RestMethod -Uri $ReportUrl -Method Post -Body $payloadBytes -Headers $requestHeaders -TimeoutSec 120; Write-Log -Message "Scan-Bericht erfolgreich an Server gesendet."; $Global:LastSuccessfulReportTimeUTC = (Get-Date).ToUniversalTime(); try { ($Global:LastSuccessfulReportTimeUTC.ToString("o") | ConvertTo-Json -Compress) | Set-Content -Path $LastReportTimeFilePath -Force -Encoding UTF8; Write-Log -Message "Letzte erfolgreiche Report-Zeit aktualisiert: $($Global:LastSuccessfulReportTimeUTC.ToLocalTime())" } catch { Write-Log -Level WARN -Message "Fehler beim Speichern von '$LastReportTimeFilePath': $($_.Exception.Message)" }; return $true } catch { $CaughtException = $_; Write-Log -Level ERROR -Message "FEHLER bei Send-ScanReport: $($CaughtException.ToString())"; if ($CaughtException.Exception -is [System.Net.WebException] -and $null -ne $CaughtException.Exception.Response) { $webEx = $CaughtException.Exception; $httpResponse = $webEx.Response; $actualHttpStatusCode = [int]$httpResponse.StatusCode; Write-Log -Level ERROR -Message " HTTP Status: $actualHttpStatusCode"; try { $responseStream = $httpResponse.GetResponseStream(); $streamReader = New-Object System.IO.StreamReader($responseStream, [System.Text.Encoding]::UTF8); $errorBodyContent = $streamReader.ReadToEnd(); $streamReader.Close(); $responseStream.Close(); Write-Log -Level ERROR -Message " Fehler-Body vom Server: $errorBodyContent" } catch { Write-Log -Level ERROR -Message " Zusätzlicher Fehler beim Lesen des Fehler-Bodys: $($_.Exception.Message)" } }; return $false } finally { $ErrorActionPreference = $ErrorActionPreferenceBackup } }
+
+ function ConvertFrom-DefenderEvent { param( [Parameter(Mandatory = $true)] $Event ); $eventTimeUTC = $Event.TimeCreated.ToUniversalTime().ToString("o");
+ $cleanMsg = $Event.Message -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' -replace '%n', "`n" -replace '%t', " " -replace '%b', ""
+ $simplified = @{ Message = "Event $($Event.Id): " + $cleanMsg.Trim(); ThreatsFound = $false; ThreatDetails = $null }; if ($simplified.Message -match "Bedrohung gefunden" -or $simplified.Message -match "Malware found") { $simplified.ThreatsFound = $true }; if ($Event.Id -in (1116, 1117, 1118)) { $simplified.ThreatsFound = $true }; if ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)\s*(?:Aktion|Action):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim()), Aktion: $($Matches[3].Trim())" } elseif ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim())" }; return [PSCustomObject]$simplified }
# --- HAUPT-POLLING-SCHLEIFE ---
$currentRetryDelay = $InitialRetryDelaySeconds
@@ -108,14 +112,12 @@ while ($true) {
if ($elapsedMinutes -gt $timeoutLimitMinutes) { $isTimedOut = $true }
if ($Script:ActiveScanJob.State -eq 'Running' -and (-not $isTimedOut)) {
- try {
- $completionEvent = Get-WinEvent -FilterHashtable @{ProviderName="Microsoft-Windows-Windows Defender"; ID=1001,1002,1005; StartTime=$Script:ScanInitiationTimeUTC.ToLocalTime()} -MaxEvents 1 -ErrorAction SilentlyContinue
- }
- catch { Write-Log -Level WARN -Message "Fehler bei proaktiver Event-Suche: $($_.Exception.Message)" }
+ # We NO LONGER check for completionEvent here to avoid preempting the job due to unrelated background scan events!
+ $completionEvent = $null
}
- if ($Script:ActiveScanJob.State -in @('Completed', 'Failed', 'Stopped') -or $completionEvent -or $isTimedOut) {
- Write-Log -Message "Scan-Abschluss erkannt. Grund: Job-Status='$($Script:ActiveScanJob.State)', Event-Gefunden='$($completionEvent -ne $null)', Timeout='$isTimedOut'."
+ if ($Script:ActiveScanJob.State -in @('Completed', 'Failed', 'Stopped') -or $isTimedOut) {
+ Write-Log -Message "Scan-Abschluss erkannt. Grund: Job-Status='$($Script:ActiveScanJob.State)', Timeout='$isTimedOut'."
if ($isTimedOut -and $Script:ActiveScanJob.State -notlike 'Stopped') {
Write-Log -Level WARN -Message "Scan hat das Zeitlimit von $timeoutLimitMinutes Minuten überschritten. Breche Job ab."
Stop-Job -Job $Script:ActiveScanJob -Force
From 5cfd0b6a1dc24bd569eeca5c9e505fc8a050d0a5 Mon Sep 17 00:00:00 2001
From: BitWuehler <39520752+BitWuehler@users.noreply.github.com>
Date: Wed, 10 Jun 2026 23:11:37 +0200
Subject: [PATCH 3/4] Fix dashboard overview to also show Fehler retroactively,
clean pseudo-localization tokens, and fix row height
---
app/web_routes.py | 39 +++++++++++++++++++++++++++++----
templates/daily_report.html | 2 ++
templates/laptops_overview.html | 2 ++
3 files changed, 39 insertions(+), 4 deletions(-)
diff --git a/app/web_routes.py b/app/web_routes.py
index ffb04a6..6dfd1c9 100644
--- a/app/web_routes.py
+++ b/app/web_routes.py
@@ -65,8 +65,21 @@ async def web_laptops_overview(request: Request, db: Session = Depends(get_db),
hours_since = time_since_last_scan.total_seconds() / 3600.0
hours_rounded = round(hours_since)
- if laptop_instance.last_scan_threats_found is True:
+ # Retroactively fix old DB entries where Event 1002 was marked as a threat
+ is_real_threat = laptop_instance.last_scan_threats_found
+ is_error = False
+ if laptop_instance.last_scan_result_message and ("Event 1002" in laptop_instance.last_scan_result_message or "FEHLER:" in laptop_instance.last_scan_result_message or "stopped" in laptop_instance.last_scan_result_message or "Fehler" in laptop_instance.last_scan_result_message):
+ if "Event 1002" in laptop_instance.last_scan_result_message:
+ is_real_threat = False # 1002 is just a cancelled scan, not a threat
+ is_error = True
+
+ laptop_instance.last_scan_threats_found = is_real_threat
+ laptop_instance.is_error = is_error
+
+ if is_real_threat is True:
status_info = {"text": "Bedrohung(en) gefunden!", "color_class": "status-red", "style": ""}
+ elif is_error:
+ status_info = {"text": "Fehler / Abbruch", "color_class": "status-yellow", "style": ""}
else:
if hours_since <= 5:
hue = 120 # Green
@@ -90,11 +103,14 @@ async def web_laptops_overview(request: Request, db: Session = Depends(get_db),
simplified_result_message = "N/A"
if laptop_instance.last_scan_result_message:
- msg = laptop_instance.last_scan_result_message
- if "erfolgreich abgeschlossen" in msg:
+ # Clean up old pseudo-localization tokens from database
+ clean_msg = laptop_instance.last_scan_result_message.replace('%n', '\n').replace('%t', ' ').replace('%b', '')
+ laptop_instance.last_scan_result_message = clean_msg
+
+ if "erfolgreich abgeschlossen" in clean_msg:
simplified_result_message = "OK"
else:
- simplified_result_message = msg[:30] + ("..." if len(msg) > 30 else "")
+ simplified_result_message = clean_msg[:30] + ("..." if len(clean_msg) > 30 else "")
has_error = False
@@ -186,6 +202,10 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st
if len(scan_result) > 50:
scan_result = scan_result[:50] + "..."
threats_str = "Nein"
+
+ # Clean up old pseudo-localization tokens from database
+ threats_str = threats_str.replace('%n', '\n').replace('%t', ' ').replace('%b', '')
+ scan_result = scan_result.replace('%n', '\n').replace('%t', ' ').replace('%b', '')
writer.writerow([laptop.alias_name, laptop.hostname, scan_time_str, scan_result, threats_str])
@@ -253,6 +273,9 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No
is_real_threat = False # 1002 is just a cancelled scan, not a threat
is_error = True
+ laptop.last_scan_threats_found = is_real_threat
+ laptop.is_error = is_error
+
if is_real_threat is True:
if getattr(laptop, "last_scan_threat_details", None):
status_text = laptop.last_scan_threat_details
@@ -270,6 +293,14 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No
status_text, color_class = "OK (Scan <24h)", "status-green"
else:
status_text, color_class = "OK (Scan älter)", "status-yellow"
+
+ # Clean up old pseudo-localization tokens from database
+ if status_text:
+ status_text = status_text.replace('%n', '\n').replace('%t', ' ').replace('%b', '')
+
+ # Truncate very long texts if they aren't threats or errors to save space
+ if not is_real_threat and not is_error and len(status_text) > 100:
+ status_text = status_text[:100] + "..."
else:
status_text, color_class = "Kein Scan bisher", "status-white"
diff --git a/templates/daily_report.html b/templates/daily_report.html
index 84f35dc..46ef33b 100644
--- a/templates/daily_report.html
+++ b/templates/daily_report.html
@@ -131,6 +131,8 @@
{% if laptop.last_scan_threats_found is true %}
Fund!
+ {% elif laptop.is_error %}
+ Fehler / Abbruch
{% elif laptop.last_scan_result_message %}
{% if 'erfolgreich abgeschlossen' in laptop.last_scan_result_message %}
OK
diff --git a/templates/laptops_overview.html b/templates/laptops_overview.html
index dc11296..5fc34ac 100644
--- a/templates/laptops_overview.html
+++ b/templates/laptops_overview.html
@@ -117,6 +117,8 @@
{% if laptop.last_scan_threats_found is true %}
Fund!
+ {% elif laptop.is_error %}
+ Fehler / Abbruch
{% else %}
{{ item.simplified_result_message }}
{% endif %}
From 6116e9acb02a246a5c9d7c54f0cb12b55412b3ad Mon Sep 17 00:00:00 2001
From: BitWuehler <39520752+BitWuehler@users.noreply.github.com>
Date: Wed, 10 Jun 2026 23:14:16 +0200
Subject: [PATCH 4/4] Fix pseudo-localization tokens cleanup regex to handle
Windows Insider mutated letters
---
app/web_routes.py | 19 +++++++++++++++----
client/ScanOpClient.ps1 | 2 +-
2 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/app/web_routes.py b/app/web_routes.py
index 6dfd1c9..093dc63 100644
--- a/app/web_routes.py
+++ b/app/web_routes.py
@@ -7,6 +7,7 @@
from pathlib import Path
import io
import csv
+import re
from typing import Union, Optional # KORREKTUR: Union und Optional importieren
from app.database import get_db
@@ -104,7 +105,10 @@ async def web_laptops_overview(request: Request, db: Session = Depends(get_db),
simplified_result_message = "N/A"
if laptop_instance.last_scan_result_message:
# Clean up old pseudo-localization tokens from database
- clean_msg = laptop_instance.last_scan_result_message.replace('%n', '\n').replace('%t', ' ').replace('%b', '')
+ clean_msg = laptop_instance.last_scan_result_message
+ clean_msg = re.sub(r'%[nиñńηйNИÑŃΗЙ]', '\n', clean_msg)
+ clean_msg = re.sub(r'%[tтŧťτTТŦŤΤ]', ' ', clean_msg)
+ clean_msg = re.sub(r'%[bьвβBЬВΒ]', '', clean_msg)
laptop_instance.last_scan_result_message = clean_msg
if "erfolgreich abgeschlossen" in clean_msg:
@@ -204,8 +208,13 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st
threats_str = "Nein"
# Clean up old pseudo-localization tokens from database
- threats_str = threats_str.replace('%n', '\n').replace('%t', ' ').replace('%b', '')
- scan_result = scan_result.replace('%n', '\n').replace('%t', ' ').replace('%b', '')
+ threats_str = re.sub(r'%[nиñńηйNИÑŃΗЙ]', '\n', threats_str)
+ threats_str = re.sub(r'%[tтŧťτTТŦŤΤ]', ' ', threats_str)
+ threats_str = re.sub(r'%[bьвβBЬВΒ]', '', threats_str)
+
+ scan_result = re.sub(r'%[nиñńηйNИÑŃΗЙ]', '\n', scan_result)
+ scan_result = re.sub(r'%[tтŧťτTТŦŤΤ]', ' ', scan_result)
+ scan_result = re.sub(r'%[bьвβBЬВΒ]', '', scan_result)
writer.writerow([laptop.alias_name, laptop.hostname, scan_time_str, scan_result, threats_str])
@@ -296,7 +305,9 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No
# Clean up old pseudo-localization tokens from database
if status_text:
- status_text = status_text.replace('%n', '\n').replace('%t', ' ').replace('%b', '')
+ status_text = re.sub(r'%[nиñńηйNИÑŃΗЙ]', '\n', status_text)
+ status_text = re.sub(r'%[tтŧťτTТŦŤΤ]', ' ', status_text)
+ status_text = re.sub(r'%[bьвβBЬВΒ]', '', status_text)
# Truncate very long texts if they aren't threats or errors to save space
if not is_real_threat and not is_error and len(status_text) > 100:
diff --git a/client/ScanOpClient.ps1 b/client/ScanOpClient.ps1
index 5ca664d..a71e0a2 100644
--- a/client/ScanOpClient.ps1
+++ b/client/ScanOpClient.ps1
@@ -94,7 +94,7 @@ while ($true) {
function Send-ScanReport { param( [Parameter(Mandatory = $true)][string]$ScanTime, [Parameter(Mandatory = $true)][string]$ScanType, [Parameter(Mandatory = $true)][string]$ScanResultMessage, [Parameter(Mandatory = $true)][bool]$ThreatsFound, [string]$ThreatDetails = $null ); Write-Log -Message "Bereite Scan-Bericht ($ScanType) für Versand vor."; if ([string]::IsNullOrWhiteSpace($ScanTime)) { $ScanTime = (Get-Date "1970-01-01").ToUniversalTime().ToString("o") } ; if ([string]::IsNullOrWhiteSpace($ScanType)) { $ScanType = "Unbekannt" } ; if ([string]::IsNullOrWhiteSpace($ScanResultMessage)) { $ScanResultMessage = "Keine Meldung" } ; $CleanResultMessage = $ScanResultMessage -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' ; $CleanThreatDetails = if ($ThreatDetails) { $ThreatDetails -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' } else { $null } ; $payloadContent = @{ laptop_identifier = $AliasName; client_scan_time = $ScanTime; scan_type = $ScanType; scan_result_message = $CleanResultMessage; threats_found = $ThreatsFound }; if ($null -ne $CleanThreatDetails -and (-not [string]::IsNullOrWhiteSpace($CleanThreatDetails))) { $payloadContent.threat_details = $CleanThreatDetails } else { $payloadContent.threat_details = $null } ; $payloadBodyJson = $payloadContent | ConvertTo-Json -Depth 5 -Compress; $utf8Encoding = [System.Text.Encoding]::UTF8; $payloadBytes = $utf8Encoding.GetBytes($payloadBodyJson); $requestHeaders = @{ "Content-Type" = "application/json; charset=utf-8"; "X-API-Key" = $ApiKey }; Write-Log -Message "Sende Bericht... (Länge: $($payloadBytes.Length) bytes)"; $ErrorActionPreferenceBackup = $ErrorActionPreference; $ErrorActionPreference = "Stop"; try { Invoke-RestMethod -Uri $ReportUrl -Method Post -Body $payloadBytes -Headers $requestHeaders -TimeoutSec 120; Write-Log -Message "Scan-Bericht erfolgreich an Server gesendet."; $Global:LastSuccessfulReportTimeUTC = (Get-Date).ToUniversalTime(); try { ($Global:LastSuccessfulReportTimeUTC.ToString("o") | ConvertTo-Json -Compress) | Set-Content -Path $LastReportTimeFilePath -Force -Encoding UTF8; Write-Log -Message "Letzte erfolgreiche Report-Zeit aktualisiert: $($Global:LastSuccessfulReportTimeUTC.ToLocalTime())" } catch { Write-Log -Level WARN -Message "Fehler beim Speichern von '$LastReportTimeFilePath': $($_.Exception.Message)" }; return $true } catch { $CaughtException = $_; Write-Log -Level ERROR -Message "FEHLER bei Send-ScanReport: $($CaughtException.ToString())"; if ($CaughtException.Exception -is [System.Net.WebException] -and $null -ne $CaughtException.Exception.Response) { $webEx = $CaughtException.Exception; $httpResponse = $webEx.Response; $actualHttpStatusCode = [int]$httpResponse.StatusCode; Write-Log -Level ERROR -Message " HTTP Status: $actualHttpStatusCode"; try { $responseStream = $httpResponse.GetResponseStream(); $streamReader = New-Object System.IO.StreamReader($responseStream, [System.Text.Encoding]::UTF8); $errorBodyContent = $streamReader.ReadToEnd(); $streamReader.Close(); $responseStream.Close(); Write-Log -Level ERROR -Message " Fehler-Body vom Server: $errorBodyContent" } catch { Write-Log -Level ERROR -Message " Zusätzlicher Fehler beim Lesen des Fehler-Bodys: $($_.Exception.Message)" } }; return $false } finally { $ErrorActionPreference = $ErrorActionPreferenceBackup } }
function ConvertFrom-DefenderEvent { param( [Parameter(Mandatory = $true)] $Event ); $eventTimeUTC = $Event.TimeCreated.ToUniversalTime().ToString("o");
- $cleanMsg = $Event.Message -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' -replace '%n', "`n" -replace '%t', " " -replace '%b', ""
+ $cleanMsg = $Event.Message -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' -replace '%[nиñńηйNИÑŃΗЙ]', "`n" -replace '%[tтŧťτTТŦŤΤ]', " " -replace '%[bьвβBЬВΒ]', ""
$simplified = @{ Message = "Event $($Event.Id): " + $cleanMsg.Trim(); ThreatsFound = $false; ThreatDetails = $null }; if ($simplified.Message -match "Bedrohung gefunden" -or $simplified.Message -match "Malware found") { $simplified.ThreatsFound = $true }; if ($Event.Id -in (1116, 1117, 1118)) { $simplified.ThreatsFound = $true }; if ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)\s*(?:Aktion|Action):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim()), Aktion: $($Matches[3].Trim())" } elseif ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim())" }; return [PSCustomObject]$simplified }
# --- HAUPT-POLLING-SCHLEIFE ---