[$line]]); return $f[0] ?? null; } $f = vv_ai_syslog_findings(0, [$line]); return $f[0] ?? null; } if ($only !== 'precision') { echo "── recall: fixtures ──────────────────────────────────────────────\n"; if (!is_readable($fixtures)) { echo " FAIL cannot read $fixtures\n"; exit(1); } foreach (file($fixtures, FILE_IGNORE_NEW_LINES) as $n => $raw) { $line = rtrim($raw); if ($line === '' || $line[0] === '#') continue; // A ! line must match nothing at all. ! source | line if ($line[0] === '!') { $rest = trim(substr($line, 1)); $bits = explode('|', $rest, 2); if (count($bits) !== 2) { ok(sprintf('L%d is malformed', $n + 1), false, $line); continue; } $src = trim($bits[0]); $sample = trim($bits[1]); $got = classify($sample, $src); ok(sprintf('L%-3d %s no match: %s', $n + 1, $src, mb_substr($sample, 0, 58)), $got === null, $got ? "matched as {$got['ref']} / {$got['subject']}" : ''); continue; } $parts = explode('|', $line, 4); if (count($parts) !== 4) { ok(sprintf('L%d is malformed', $n + 1), false, $line); continue; } [$src, $wantSubject, $wantLevel, $sample] = array_map('trim', $parts); $got = classify($sample, $src); if ($got === null) { ok(sprintf('L%-3d %s', $n + 1, mb_substr($sample, 0, 64)), false, 'no pattern matched'); continue; } ok(sprintf('L%-3d %-12s %-5s %s', $n + 1, $got['subject'], $got['sys_level'], mb_substr($got['ref'], 0, 30)), $got['subject'] === $wantSubject && $got['sys_level'] === $wantLevel, sprintf('wanted %s/%s, got %s/%s', $wantSubject, $wantLevel, $got['subject'], $got['sys_level'])); } } if ($only !== 'recall') { echo "\n── precision: this host's real syslog history ────────────────────\n"; $total = 0; $hits = []; foreach (glob('/var/log/syslog*') ?: [] as $file) { if (!is_readable($file)) continue; $fh = @fopen($file, 'r'); if (!$fh) continue; while (($l = fgets($fh)) !== false) { $total++; $got = classify(rtrim($l)); if ($got === null) continue; $k = $got['ref'] . ' | ' . $got['subject'] . ' | ' . $got['sys_level']; $hits[$k] = ($hits[$k] ?? 0) + 1; } fclose($fh); } printf(" scanned %s lines\n", number_format($total)); if ($hits) { arsort($hits); foreach ($hits as $k => $n) printf(" %-7s %s\n", number_format($n), $k); } else { echo " nothing matched\n"; } // Every running container, read far deeper than a sweep ever does. A pattern that is quiet // over this much real output is a pattern that will be quiet in service. echo "\n── precision: every running container's log ──────────────────────\n"; $cTotal = 0; $hits = []; $names = vv_ai_running_containers(); foreach ($names as $name) { $lines = vv_ai_container_log($name, 1, 2000); $cTotal += count($lines); $found = vv_ai_container_findings(0, [$name => $lines]); foreach ($found as $f) { $k = $f['ref'] . ' | ' . $f['subject'] . ' | ' . $f['sys_level']; $hits[$k] = ($hits[$k] ?? 0) + (int)filter_var($f['observed'], FILTER_SANITIZE_NUMBER_INT); } } printf(" scanned %s lines across %d container%s\n", number_format($cTotal), count($names), count($names) === 1 ? '' : 's'); if (!$hits) { echo " nothing matched — this host is not reporting any of these faults\n"; } else { arsort($hits); foreach ($hits as $k => $n) printf(" %-7s %s\n", number_format($n), $k); echo "\n Each line above is a fault the sweep would file. If any of them is normal\n" . " operation on this machine, the pattern is wrong — add it to the fixtures as a\n" . " ! line and tighten the pattern until it stops matching.\n"; } } printf("\n%d passed, %d failed\n", $pass, $fail); exit($fail ? 1 : 0);