fix(agent99): triage security and noisy alert families

This commit is contained in:
ogt
2026-07-10 10:48:34 +08:00
parent 6da06e35d1
commit f3c5d5a9f9
4 changed files with 215 additions and 11 deletions

View File

@@ -423,6 +423,40 @@ Recovery is complete only when all of the following are captured:
- Send user-facing Telegram only for warnings, criticals, unresolved blockers, failed/escalated operator commands, and controlled-remediation actions.
- Keep successful automation in evidence, problem counters, KM, and RAG so repeated incidents trend down without flooding the SRE group.
## Agent99 Telegram Alert UX And Noise Freeze Work Items
2026-07-10 10:41 Asia/Taipei:
- Operator requirement:
- AwoooI SRE Telegram alerts must be human-readable, professionally formatted, and action-oriented.
- Important incident updates should include visual context when available: dashboard image, public-route screenshot, service card, or evidence summary image.
- Do not send raw machine fields, empty metrics, repeated success receipts, or text-only alert floods.
- Immediate runtime noise freeze applied on 99:
- Disabled scheduled task: `Wooo-Agent99-Performance-Guard`.
- Disabled scheduled task: `Wooo-Agent99-Backup-Health`.
- Disabled scheduled task: `Wooo-Agent99-Self-Health`.
- Ended running task instance: `Wooo-Agent99-Performance-Guard`.
- Kept alert intake active: `Wooo-Agent99-SRE-Alert-Relay`, `Wooo-Agent99-SRE-Alert-Inbox`, `Wooo-Agent99-Telegram-Inbox`, and `Wooo-Agent99-Control-Loop`.
- 2026-07-10 10:47 runtime readback:
- `Wooo-Agent99-Performance-Guard`: `Disabled`, last result `0`.
- `Wooo-Agent99-Backup-Health`: `Disabled`, last result `0`.
- `Wooo-Agent99-Self-Health`: `Disabled`, last result `0`.
- `Wooo-Agent99-SRE-Alert-Relay`: `Running`.
- `Wooo-Agent99-SRE-Alert-Inbox`: `Ready`, last result `0`.
- `Wooo-Agent99-Telegram-Inbox`: `Running`.
- `Wooo-Agent99-Control-Loop`: `Ready`, last result `0`.
- `SecurityTriage` smoke evidence: `C:\Wooo\Agent99\evidence\agent99-SecurityTriage-20260710-104456.json`.
- `SecurityTriage` smoke result: `security_triage_no_recent_alert_body_seen`, `hitCount=0`, `telegramCount=0`.
- P0 work items added:
- P0-0: keep noisy sensor schedules paused until their alerts have dedupe, clear reason text, and verifier-backed fields.
- P0-1: replace raw Agent99 text alerts with professional Telegram incident cards: title, severity, affected service, user impact, Agent99 action, verifier result, evidence path, repeated count, next action.
- P0-2: add visual attachment support for high-impact alerts: public URL screenshots, dashboard screenshots, and rendered evidence summary images.
- P0-3: suppress alerts with empty metric fields such as blank load/core, memory, disk, age, or maxAge unless a verifier explains why readback failed.
- P0-4: route application config alerts such as Bitan `E_AI_MISSING_KEY` to evidence-first `Status`; do not read or print secret values.
- P0-5: route IWOOOS/Wazuh/SIEM alerts to `SecurityTriage`; collect source log reference, affected host/service, blast radius, repeated count, and allowlisted next action without arbitrary shell or secret exposure.
- P0-6: route Gitea repository missing alerts to evidence-first `Status` until a dedicated Gitea recovery playbook is verified.
- P0-7: run live TG-to-Agent99 end-to-end tests for each alert family before re-enabling the paused sensor schedules.
## Agent99 Monitoring Alert Routing Self-Test Receipt
2026-07-10 10:17 Asia/Taipei:
@@ -437,6 +471,13 @@ Recovery is complete only when all of the following are captured:
- `caseCount=9`.
- `passedCount=9`.
- `failedCount=0`.
- Expanded self-test evidence:
- `C:\Wooo\Agent99\evidence\agent99-SreAlertRoutingSelfTest-20260710-104455.json`.
- `ok=True`.
- `caseCount=12`.
- `passedCount=12`.
- `failedCount=0`.
- Added routes: IWOOOS/Wazuh/SIEM security alert -> `SecurityTriage`; Gitea repository missing -> `Status`; Bitan AI support missing key / locale guard -> `Status`.
- Covered alert routes:
- Public route 502 -> `Recover`, `controlledApply=True`.
- Host down / reboot / VM not running -> `Recover`, `controlledApply=True`.

View File

@@ -1,5 +1,5 @@
param(
[ValidateSet("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck", "ProviderFreshness", "ControlTick")]
[ValidateSet("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck", "ProviderFreshness", "SecurityTriage", "ControlTick")]
[string]$Mode = "Status",
[switch]$ControlledApply,
[string]$ConfigPath = "C:\Wooo\Agent99\config\agent99.config.json",
@@ -221,6 +221,18 @@ function Format-AgentTelegramText {
$lines += "Event: AI service health"
$lines += "Service: $serviceName"
$lines += "Action: inspect health endpoint and service/container state before controlled repair."
} elseif ($EventType -eq "security_triage") {
$status = if ($Data -and $Data.PSObject.Properties["status"]) { $Data.status } else { "unknown" }
$hitCount = if ($Data -and $Data.PSObject.Properties["hitCount"]) { $Data.hitCount } else { 0 }
$evidencePath = if ($Data -and $Data.PSObject.Properties["evidencePath"]) { $Data.evidencePath } else { $null }
$lines += "Event: security triage"
$lines += "Status: $status"
$lines += "Evidence hits: $hitCount"
$lines += "Action: classify blast radius, affected host/service, source log reference, repeated count, and allowlisted next action."
$lines += "Guard: no arbitrary shell, no data deletion, no disabling controls, no secret exposure."
if ($evidencePath) {
$lines += "Evidence: $evidencePath"
}
} elseif ($EventType -eq "agent_selfcheck") {
$lines += "Event: Agent99 self health"
$lines += "Summary: $Message"
@@ -1889,7 +1901,7 @@ function Invoke-AgentQueuedCommands {
$failedDir = Join-Path $queueDir "failed"
New-Item -ItemType Directory -Force -Path $queueDir, $processedDir, $failedDir | Out-Null
$allowedModes = @("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck", "ProviderFreshness")
$allowedModes = @("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck", "ProviderFreshness", "SecurityTriage")
$commands = @(Get-ChildItem -Path $queueDir -Filter "*.json" -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notmatch "^(processed|failed|running)-" } |
Sort-Object LastWriteTime |
@@ -2009,6 +2021,88 @@ function Invoke-AgentControlTick {
$result
}
function Invoke-AgentSecurityTriage {
$lookbackMinutes = 120
$since = (Get-Date).AddMinutes(-1 * $lookbackMinutes)
$triageId = "security-triage-" + (Get-Date -Format "yyyyMMdd-HHmmss")
$triageDir = Join-Path $Config.agentRoot "state\security-triage"
New-Item -ItemType Directory -Force -Path $triageDir | Out-Null
$triagePath = Join-Path $triageDir "$triageId.json"
$patterns = @(
"wazuh",
"siem",
"security",
"auth_failed",
"bruteforce",
"brute force",
"intrusion",
"malware",
"privilege",
"vulnerability",
"cve",
"ioc",
"suspicious",
"unauthorized"
)
$scanDirs = @(
(Join-Path $Config.agentRoot "alerts\incoming"),
(Join-Path $Config.agentRoot "alerts\processed"),
(Join-Path $Config.agentRoot "queue"),
(Join-Path $Config.agentRoot "queue\processed"),
(Join-Path $Config.agentRoot "requests")
) | Where-Object { Test-Path $_ } | Select-Object -Unique
$hits = @()
foreach ($dir in $scanDirs) {
$files = @(Get-ChildItem -LiteralPath $dir -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -ge $since -and $_.Length -lt 2097152 })
foreach ($file in $files) {
$matchedPatterns = @()
foreach ($pattern in $patterns) {
if (Select-String -LiteralPath $file.FullName -Pattern $pattern -SimpleMatch -Quiet -ErrorAction SilentlyContinue) {
$matchedPatterns += $pattern
}
}
if (@($matchedPatterns).Count -gt 0) {
$excerpt = ""
$firstHit = Select-String -LiteralPath $file.FullName -Pattern $matchedPatterns[0] -SimpleMatch -ErrorAction SilentlyContinue | Select-Object -First 1
if ($firstHit) {
$excerpt = [string]$firstHit.Line
$excerpt = $excerpt -replace '("?(telegram|token|chat|authorization|password|secret|cookie)[^,}\r\n]*"?\s*:\s*)"[^"]+"', '$1"[redacted]"'
if ($excerpt.Length -gt 500) { $excerpt = $excerpt.Substring(0, 500) + "...(truncated)" }
}
$hits += [pscustomobject]@{
path = $file.FullName
lastWrite = $file.LastWriteTime.ToString("o")
patterns = @($matchedPatterns)
excerpt = $excerpt
}
}
}
}
$status = if (@($hits).Count -gt 0) { "security_triage_candidate_created" } else { "security_triage_no_recent_alert_body_seen" }
$triage = [pscustomobject]@{
timestamp = (Get-Date -Format o)
triageId = $triageId
status = $status
lookbackMinutes = $lookbackMinutes
since = $since.ToString("o")
hitCount = @($hits).Count
hits = @($hits | Sort-Object lastWrite -Descending | Select-Object -First 20)
requiredReadback = @("source_log_reference", "affected_host_or_service", "blast_radius", "repeated_count", "allowlisted_next_action")
allowedActions = @("classify", "read_existing_evidence", "write_km", "request_source_log_reference", "recommend_allowlisted_next_action")
prohibitedActions = @("arbitrary_shell", "data_delete", "disable_security_control", "secret_exposure")
evidencePath = $triagePath
}
$triage | ConvertTo-Json -Depth 10 | Set-Content -Path $triagePath -Encoding UTF8
Write-AgentLog "security_triage triage=$triageId hits=$(@($hits).Count) status=$status path=$triagePath"
$eventSeverity = if (@($hits).Count -gt 0) { "warning" } else { "info" }
Record-AgentEvent "security_triage" $eventSeverity "status=$status hits=$(@($hits).Count)" $triage -Alert
$triage
}
function Invoke-AgentProviderFreshnessDirectReadback {
param(
[int]$LookbackMinutes
@@ -2446,6 +2540,7 @@ $loadShedding = @()
$selfHealth = $null
$backupHealth = $null
$providerFreshness = $null
$securityTriage = $null
$controlTick = $null
if ($Mode -in @("Recover", "StartVMs")) {
@@ -2513,6 +2608,10 @@ if ($Mode -in @("ProviderFreshness")) {
$providerFreshness = Invoke-AgentProviderFreshnessTriage
}
if ($Mode -in @("SecurityTriage")) {
$securityTriage = Invoke-AgentSecurityTriage
}
if ($Mode -in @("ControlTick")) {
$controlTick = Invoke-AgentControlTick
}
@@ -2526,8 +2625,9 @@ $selfHealthWarning = if ($selfHealth -and $selfHealth.severity -eq "warning") {
$backupCritical = if ($backupHealth -and $backupHealth.severity -eq "critical") { 1 } else { 0 }
$backupWarning = if ($backupHealth -and $backupHealth.severity -eq "warning") { 1 } else { 0 }
$providerFreshnessWarning = if ($providerFreshness) { 1 } else { 0 }
$summarySeverity = if ($publicFailures -gt 0 -or $aiFailures -gt 0 -or $perfCritical -gt 0 -or $selfHealthCritical -gt 0 -or $backupCritical -gt 0) { "critical" } elseif ($perfWarning -gt 0 -or $selfHealthWarning -gt 0 -or $backupWarning -gt 0 -or $providerFreshnessWarning -gt 0) { "warning" } else { "info" }
Record-AgentEvent "agent_complete" $summarySeverity "mode=$Mode publicFailures=$publicFailures aiFailures=$aiFailures perfCritical=$perfCritical perfWarning=$perfWarning selfHealth=$($selfHealth.severity) backupHealth=$($backupHealth.severity) providerFreshness=$($providerFreshness.status) evidence=$jsonPath" $null -NoAlert
$securityTriageWarning = if ($securityTriage -and $securityTriage.hitCount -gt 0) { 1 } else { 0 }
$summarySeverity = if ($publicFailures -gt 0 -or $aiFailures -gt 0 -or $perfCritical -gt 0 -or $selfHealthCritical -gt 0 -or $backupCritical -gt 0) { "critical" } elseif ($perfWarning -gt 0 -or $selfHealthWarning -gt 0 -or $backupWarning -gt 0 -or $providerFreshnessWarning -gt 0 -or $securityTriageWarning -gt 0) { "warning" } else { "info" }
Record-AgentEvent "agent_complete" $summarySeverity "mode=$Mode publicFailures=$publicFailures aiFailures=$aiFailures perfCritical=$perfCritical perfWarning=$perfWarning selfHealth=$($selfHealth.severity) backupHealth=$($backupHealth.severity) providerFreshness=$($providerFreshness.status) securityTriage=$($securityTriage.status) evidence=$jsonPath" $null -NoAlert
$result = [pscustomobject]@{
timestamp = (Get-Date -Format o)
@@ -2544,6 +2644,7 @@ $result = [pscustomobject]@{
selfHealth = $selfHealth
backupHealth = $backupHealth
providerFreshness = $providerFreshness
securityTriage = $securityTriage
controlTick = $controlTick
events = $script:Events
telegram = $script:TelegramAttempts

View File

@@ -19,7 +19,7 @@ $RequestDir = Join-Path $AgentRoot "requests"
New-Item -ItemType Directory -Force -Path $EvidenceDir, $IncomingDir, $RunningDir, $ProcessedDir, $FailedDir, $IgnoredDir, $QueueDir, $RequestDir | Out-Null
$allowedModes = @("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck", "ProviderFreshness")
$allowedModes = @("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck", "ProviderFreshness", "SecurityTriage")
$remediationModes = @("Recover", "StartVMs", "HarborRepair", "AwoooRepair", "Perf", "LoadShed")
function Get-AgentField {
@@ -71,7 +71,7 @@ function Test-AgentAlertActionable {
if ($severity -in @("critical", "warning", "error", "degraded", "down", "failed")) {
return $true
}
if ($text -match "502|provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion|imagepullbackoff|errimagepull|crashloopbackoff|host_down|vm_not_running|backup_failed|backup_missing|disk_used|load_per_core|cpu|memory|harbor|registry|k3s") {
if ($text -match "502|provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion|imagepullbackoff|errimagepull|crashloopbackoff|host_down|vm_not_running|vm_stuck|backup_failed|backup_missing|disk_used|load_per_core|cpu|memory|harbor|registry|k3s|wazuh|siem|security|intrusion|malware|auth_failed|bruteforce|cve|ioc|gitea|repo_missing|repository_missing|missing_key|e_ai_missing_key|application_ai_config_error|locale guard|ai support") {
return $true
}
return $false
@@ -95,6 +95,9 @@ function Resolve-AgentAlertMode {
if ($text -match "backup|snapshot|restore drill|cron") {
return "BackupCheck"
}
if ($text -match "wazuh|siem|security|intrusion|malware|auth_failed|bruteforce|privilege|vulnerability|cve|ioc|suspicious|unauthorized") {
return "SecurityTriage"
}
if ($text -match "provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|last_seen|last seen|provider freshness|provider window|ingestion") {
return "ProviderFreshness"
}
@@ -104,6 +107,12 @@ function Resolve-AgentAlertMode {
if ($text -match "awoooi|k3s|imagepullbackoff|errimagepull|crashloopbackoff|pod|deployment") {
return "AwoooRepair"
}
if ($text -match "missing_key|e_ai_missing_key|application_ai_config_error|locale guard|ai support") {
return "Status"
}
if ($text -match "gitea|repo_missing|repository_missing|repo missing|repository missing") {
return "Status"
}
if ($suggestedMode -and $suggestedMode -in $allowedModes) {
return $suggestedMode
}
@@ -131,6 +140,9 @@ function New-AgentInstructionFromAlert {
if ($kind -eq "provider_freshness_signal" -or $message.ToLowerInvariant() -match "provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|provider freshness|provider window|last_seen|last seen|ingestion") {
return "Provider freshness candidate required; alertId=$AlertId; source=$source; severity=$severity; service=$service; mode=$ModeName; require providerWindow,lastSeen,directReference,verifierReadback; mark missing,expired,timeout,no_false_green; do not switch provider, call paid model, edit env, or reload."
}
if ($ModeName -eq "SecurityTriage" -or $kind -match "security|wazuh|siem") {
return "Security triage required; alertId=$AlertId; source=$source; severity=$severity; service=$service; mode=$ModeName; classify blast radius, affected host/service, source log reference, repeated count, and recommended allowlisted next action; do not run arbitrary shell, delete data, disable controls, or expose secrets."
}
$parts = @("SRE alert", "id=$AlertId", "source=$source", "severity=$severity", "service=$service", "kind=$kind", "mode=$ModeName")
if ($hostName) { $parts += "host=$hostName" }
@@ -255,6 +267,51 @@ function Invoke-AgentAlertRoutingSelfTest {
message = "provider_freshness_signal source_provider_freshness_triage raw_signal_normalized controlled_runtime_candidate"
}
},
[pscustomobject]@{
name = "security alert"
expectedActionable = $true
expectedMode = "SecurityTriage"
expectedControlledApply = $false
alert = [pscustomobject]@{
id = "selftest-security"
source = "agent99-routing-selftest"
severity = "critical"
kind = "wazuh_security_alert"
service = "iwooos-security"
host = "192.168.0.112"
message = "Wazuh SIEM security alert auth_failed brute force suspicious login"
}
},
[pscustomobject]@{
name = "gitea repository missing"
expectedActionable = $true
expectedMode = "Status"
expectedControlledApply = $false
alert = [pscustomobject]@{
id = "selftest-gitea-repo-missing"
source = "agent99-routing-selftest"
severity = "critical"
kind = "repository_missing"
service = "gitea"
host = "192.168.0.110"
message = "Gitea repo_missing repository missing for production project"
}
},
[pscustomobject]@{
name = "application ai config error"
expectedActionable = $true
expectedMode = "Status"
expectedControlledApply = $false
alert = [pscustomobject]@{
id = "selftest-app-ai-config"
source = "agent99-routing-selftest"
severity = "warning"
kind = "application_ai_config_error"
service = "bitan-pharmacy"
host = ""
message = "AI support locale guard failed E_AI_MISSING_KEY missing_key"
}
},
[pscustomobject]@{
name = "unknown warning"
expectedActionable = $true

View File

@@ -225,11 +225,11 @@ function Test-AgentMonitoringAlertText {
return $false
}
if ($lower -match "alertmanager|\bfiring\b|\bcritical\b|\bwarning\b|\bdegraded\b|\bdown\b|\b502\b|provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion|imagepullbackoff|errimagepull|crashloopbackoff|harbor|registry|k3s|backup|snapshot|cpu|load|memory|disk|hostdown|host_down|vm_not_running") {
if ($lower -match "alertmanager|\bfiring\b|\bcritical\b|\bwarning\b|\bdegraded\b|\bdown\b|\b502\b|provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion|imagepullbackoff|errimagepull|crashloopbackoff|harbor|registry|k3s|backup|snapshot|cpu|load|memory|disk|hostdown|host_down|vm_not_running|vm_stuck|vmware|wazuh|siem|security|intrusion|malware|auth_failed|bruteforce|cve|ioc|gitea|repo_missing|repository_missing|ai support|locale guard|missing_key|e_ai_missing_key") {
return $true
}
foreach ($code in @(0x544A, 0x8B66, 0x7570, 0x5E38, 0x56B4, 0x91CD, 0x8CA0, 0x8F09, 0x78C1, 0x789F, 0x5099, 0x4EFD)) {
foreach ($code in @(0x544A, 0x8B66, 0x7570, 0x5E38, 0x56B4, 0x91CD, 0x8CA0, 0x8F09, 0x78C1, 0x789F, 0x5099, 0x4EFD, 0x5BA2, 0x670D, 0x78A7, 0x6F6D)) {
if ($trimmed.IndexOf([string][char]$code) -ge 0) { return $true }
}
@@ -259,15 +259,18 @@ function Get-AgentTelegramAlertSeverity {
function Get-AgentTelegramAlertKind {
param([string]$Text)
$lower = $Text.ToLowerInvariant()
if ($lower -match "provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion") { return "provider_freshness_signal" }
if ($lower -match "backup|snapshot|cron" -or (Test-AgentTextContainsCode $Text @(0x5099, 0x4EFD))) { return "backup_health" }
if ($lower -match "wazuh|siem|security|intrusion|malware|auth_failed|bruteforce|cve|ioc") { return "wazuh_security_alert" }
if ($lower -match "gitea|repo_missing|repository_missing|repo missing|repository missing") { return "repository_missing" }
if ($lower -match "provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|last_seen|last seen|provider freshness|provider window|ingestion") { return "provider_freshness_signal" }
if ($lower -match "502|site_502|public_route|website|route") { return "public_route_502" }
if ($lower -match "missing_key|e_ai_missing_key|ai support|locale guard" -or (Test-AgentTextContainsCode $Text @(0x5BA2, 0x670D))) { return "application_ai_config_error" }
if ($lower -match "cpu|load" -or (Test-AgentTextContainsCode $Text @(0x8CA0, 0x8F09))) { return "performance_cpu" }
if ($lower -match "disk" -or (Test-AgentTextContainsCode $Text @(0x78C1, 0x789F))) { return "performance_disk" }
if ($lower -match "memory|mem_|ram" -or (Test-AgentTextContainsCode $Text @(0x8A18, 0x61B6, 0x9AD4))) { return "performance_memory" }
if ($lower -match "harbor|registry") { return "harbor_registry" }
if ($lower -match "awoooi|k3s|imagepullbackoff|errimagepull|crashloopbackoff|pod|deployment") { return "awoooi_k3s" }
if ($lower -match "backup|snapshot|cron" -or (Test-AgentTextContainsCode $Text @(0x5099, 0x4EFD))) { return "backup_health" }
if ($lower -match "hostdown|host_down|vm_not_running|reboot|restart|down" -or (Test-AgentTextContainsCode $Text @(0x91CD, 0x555F, 0x95DC, 0x6A5F))) { return "host_recovery" }
if ($lower -match "hostdown|host_down|vm_not_running|vm_stuck|vmware|reboot|restart|down" -or (Test-AgentTextContainsCode $Text @(0x91CD, 0x555F, 0x95DC, 0x6A5F))) { return "host_recovery" }
return "monitoring_alert"
}
@@ -276,8 +279,10 @@ function Get-AgentTelegramAlertService {
$lower = $Text.ToLowerInvariant()
if ($lower -match "stock") { return "stockplatform" }
if ($lower -match "2026fifa|fifa") { return "2026fifa" }
if ($lower -match "bitan" -or (Test-AgentTextContainsCode $Text @(0x78A7, 0x6F6D))) { return "bitan-pharmacy" }
if ($lower -match "gitea") { return "gitea" }
if ($lower -match "harbor") { return "harbor" }
if ($lower -match "wazuh|siem|security") { return "iwooos-security" }
if ($lower -match "agent99") { return "agent99" }
if ($lower -match "awoooi|k3s|502|site|route|website") { return "awoooi" }
return "unknown"