diff --git a/app/web_routes.py b/app/web_routes.py index b5794ca..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 @@ -65,8 +66,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 +104,17 @@ 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 + 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: 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 @@ -163,7 +183,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,11 +198,23 @@ 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: scan_result = scan_result[:50] + "..." threats_str = "Nein" + + # Clean up old pseudo-localization tokens from database + 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]) @@ -234,7 +273,19 @@ 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 + + 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 elif laptop.last_scan_result_message: @@ -242,12 +293,25 @@ 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): 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 = 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: + status_text = status_text[:100] + "..." else: status_text, color_class = "Kein Scan bisher", "status-white" diff --git a/client/ScanOpClient.ps1 b/client/ScanOpClient.ps1 index 8f38184..a71e0a2 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ИÑŃΗЙ]', "`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 --- $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 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 %}