hardened OOBE bootstrap script, restore of bootstrap and unrestricted mode in sidebar if enabled

This commit is contained in:
Oliver Kieselbach
2026-04-08 00:29:50 +02:00
parent 60d0127a52
commit 6b099329cf
10 changed files with 616 additions and 107 deletions
@@ -1 +1 @@
884
885
@@ -379,7 +379,9 @@ public class AuthFunction
isTenantAdmin,
role,
canManageBootstrapTokens,
hasMcpAccess = mcpCheck.IsAllowed
hasMcpAccess = mcpCheck.IsAllowed,
bootstrapTokenEnabled = tenantConfig.BootstrapTokenEnabled,
unrestrictedModeEnabled = tenantConfig.UnrestrictedModeEnabled
}, needsAutoAdmin);
}
@@ -1,20 +1,10 @@
import { validateBootstrapResponse } from "@/lib/bootstrapValidation";
export const runtime = "nodejs";
const BACKEND_API_URL =
process.env.BACKEND_API_URL ?? "https://autopilotmonitor-api.azurewebsites.net";
const AGENT_DOWNLOAD_URL =
"https://autopilotmonitor.blob.core.windows.net/agent/AutopilotMonitor-Agent.zip";
interface ValidateResponse {
success: boolean;
tenantId: string;
token: string;
agentDownloadUrl: string;
expiresAt: string;
message?: string;
}
export async function GET(
_req: Request,
{ params }: { params: Promise<{ code: string }> },
@@ -40,14 +30,24 @@ export async function GET(
return errorScript(msg);
}
const data = (await validateRes.json()) as ValidateResponse;
const data: unknown = await validateRes.json();
const result = validateBootstrapResponse(data);
if (!result.ok) {
// Log the failure category only — never the offending value, to keep
// attack payloads out of Application Insights dashboards/exports.
console.warn(
`[go/${code}] bootstrap response validation failed: ${result.reason}`,
);
return errorScript(
"Bootstrap response failed validation. Contact support.",
);
}
const script = generateOobeScript({
tenantId: data.tenantId,
bootstrapToken: data.token,
agentDownloadUrl: data.agentDownloadUrl || AGENT_DOWNLOAD_URL,
expiresAt: data.expiresAt,
generatedAt: new Date().toISOString(),
tenantId: result.value.tenantId,
bootstrapToken: result.value.token,
agentDownloadUrl: result.value.agentDownloadUrl,
expiresAt: result.value.expiresAt,
});
return new Response(script, {
@@ -78,18 +78,24 @@ function errorScript(message: string): Response {
});
}
// SECURITY INVARIANT: All template parameters MUST come from
// validateBootstrapResponse() in @/lib/bootstrapValidation. The single-quoted
// PowerShell string literals below are the second line of defense against
// injection (single-quoted PS strings do NOT expand $(...), $var, or
// backticks). Do NOT change to double-quoted without re-reviewing the
// validator's character classes end-to-end.
function generateOobeScript(params: {
tenantId: string;
bootstrapToken: string;
agentDownloadUrl: string;
expiresAt: string;
generatedAt: string;
}): string {
const generatedAt = new Date().toISOString();
return `#Requires -RunAsAdministrator
<#
.SYNOPSIS
Autopilot Monitor OOBE Bootstrap
Generated: ${params.generatedAt}
Generated: ${generatedAt}
Expires: ${params.expiresAt}
This script was generated by the Autopilot Monitor platform.
@@ -99,7 +105,7 @@ function generateOobeScript(params: {
$ErrorActionPreference = "Stop"
$AgentDownloadUrl = "${params.agentDownloadUrl}"
$AgentDownloadUrl = '${params.agentDownloadUrl}'
$AgentBasePath = "$env:ProgramData\\AutopilotMonitor"
$AgentBinPath = "$AgentBasePath\\Agent"
$AgentLogPath = "$AgentBasePath\\Logs"
@@ -120,12 +126,12 @@ function Write-Log {
try {
Write-Log "===== Autopilot Monitor OOBE Bootstrap ====="
Write-Log "Bootstrap token expires: ${params.expiresAt}"
Write-Log 'Bootstrap token expires: ${params.expiresAt}'
# Check if agent is already installed
$agentExePath = Join-Path $AgentBinPath "AutopilotMonitor.Agent.exe"
if (Test-Path $agentExePath) {
Write-Log "Agent already installed at $agentExePath — skipping download"
Write-Log "Agent already installed at $agentExePath -- skipping download"
}
else {
# Download agent ZIP
@@ -147,7 +153,7 @@ try {
if ($downloadAttempt -ge $maxDownloadAttempts) { throw }
$retryDelays = @(2, 4, 8)
$delay = $retryDelays[$downloadAttempt - 1]
Write-Log "Download failed (attempt $downloadAttempt): $($_.Exception.Message). Retrying in $\{delay}s..."
Write-Log "Download failed (attempt $downloadAttempt): $($_.Exception.Message). Retrying in \${delay}s..."
Start-Sleep -Seconds $delay
}
} while ($downloadAttempt -lt $maxDownloadAttempts)
@@ -164,7 +170,7 @@ try {
# Install agent with bootstrap token
Write-Log "Installing agent with bootstrap token..."
& $agentExePath --install --no-auth --bootstrap-token "${params.bootstrapToken}" --tenant-id "${params.tenantId}"
& $agentExePath --install --no-auth --bootstrap-token '${params.bootstrapToken}' --tenant-id '${params.tenantId}'
$installExitCode = $LASTEXITCODE
if ($installExitCode -ne 0) {
throw "Agent install failed with exit code $installExitCode"
@@ -1,74 +0,0 @@
"use client";
import { useMemo } from "react";
import { usePageSections } from "../../hooks/usePageSections";
import { PageSectionItem } from "../../contexts/SidebarContext";
import { useTenantConfig } from "./TenantConfigContext";
import { useAuth } from "../../contexts/AuthContext";
import {
BuildingOfficeIcon, GearIcon, WrenchScrewdriverIcon, ChartBarIcon,
} from "../../lib/sidebarIcons";
/**
* Registers all tenant settings sections in the global sidebar
* with group information for expandable rendering (GitHub-style).
*/
export function SettingsPageSections() {
const { config, user } = useTenantConfig();
const { user: authUser } = useAuth();
const items: PageSectionItem[] = useMemo(() => {
const sections: PageSectionItem[] = [];
const isAdmin = user?.isTenantAdmin || user?.isGlobalAdmin;
// 1. Tenant
if (isAdmin) {
sections.push(
{ id: "access-management", label: "Access Management", href: "/settings/tenant/access-management", group: "Tenant", groupIcon: <BuildingOfficeIcon /> },
{ id: "autopilot", label: "Autopilot Validation", href: "/settings/tenant/autopilot", group: "Tenant" },
{ id: "hardware-whitelist", label: "Hardware Whitelist", href: "/settings/tenant/hardware-whitelist", group: "Tenant" },
{ id: "notifications", label: "Notifications", href: "/settings/tenant/notifications", group: "Tenant" },
);
}
if (config?.bootstrapTokenEnabled && (isAdmin || user?.canManageBootstrapTokens)) {
sections.push(
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", href: "/settings/tenant/bootstrap-sessions", group: "Tenant", ...(!isAdmin ? { groupIcon: <BuildingOfficeIcon /> } : {}) },
);
}
// 2. Agent
if (isAdmin) {
sections.push(
{ id: "settings", label: "Agent Settings", href: "/settings/agent/settings", group: "Agent", groupIcon: <GearIcon /> },
{ id: "analyzers", label: "Agent Analyzers", href: "/settings/agent/analyzers", group: "Agent" },
{ id: "diagnostics", label: "Diagnostics Package", href: "/settings/agent/diagnostics", group: "Agent" },
);
if (config?.unrestrictedModeEnabled) {
sections.push(
{ id: "unrestricted-mode", label: "Unrestricted Mode", href: "/settings/agent/unrestricted-mode", group: "Agent" },
);
}
}
// 3. Maintenance
if (isAdmin) {
sections.push(
{ id: "data", label: "Data Management", href: "/settings/management/data", group: "Maintenance", groupIcon: <WrenchScrewdriverIcon /> },
{ id: "offboarding", label: "Offboarding", href: "/settings/management/offboarding", group: "Maintenance" },
);
}
// 4. Reporting (only visible if user has MCP access)
if (authUser?.hasMcpAccess) {
sections.push(
{ id: "mcp-usage", label: "MCP Usage", href: "/settings/reporting/mcp-usage", group: "Reporting", groupIcon: <ChartBarIcon /> },
);
}
return sections;
}, [user, config?.unrestrictedModeEnabled, config?.bootstrapTokenEnabled, authUser?.hasMcpAccess]);
usePageSections(items, "Configuration", "route");
return null;
}
@@ -189,19 +189,39 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
// Visible expandable groups (with item-level filtering for feature-gated items)
const hasMcpAccess = user?.hasMcpAccess ?? false;
const isAdminLike = isTenantAdmin || isGlobalAdmin;
const canManageBootstrapTokens = user?.canManageBootstrapTokens ?? false;
const bootstrapTokenEnabled = user?.bootstrapTokenEnabled ?? false;
const unrestrictedModeEnabled = user?.unrestrictedModeEnabled ?? false;
const visibleExpandableGroups = useMemo(() => {
return EXPANDABLE_NAV_GROUPS
.filter(isGroupVisible)
.map((group) => {
// Filter out MCP item if user doesn't have MCP access
const filteredItems = group.items.filter((item) => {
if (item.id === "cfg-reporting") return hasMcpAccess;
return true;
});
const filteredItems = group.items
.filter((item) => {
if (item.id === "cfg-reporting") return hasMcpAccess;
return true;
})
.map((item) => {
// Sub-item gating for feature-flagged entries (tenant feature flags)
const filteredSubs = item.items.filter((sub) => {
if (sub.id === "cfg-bootstrap-sessions") {
return bootstrapTokenEnabled && (isAdminLike || canManageBootstrapTokens);
}
if (sub.id === "cfg-agent-unrestricted") {
return isAdminLike && unrestrictedModeEnabled;
}
return true;
});
return { ...item, items: filteredSubs };
})
// Drop items whose sub-items have all been filtered out
.filter((item) => item.items.length > 0);
return { ...group, items: filteredItems };
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAdminOrOperator, isGlobalAdmin, globalAdminMode, hasMcpAccess]);
}, [isAdminOrOperator, isGlobalAdmin, globalAdminMode, hasMcpAccess, isAdminLike, canManageBootstrapTokens, bootstrapTokenEnabled, unrestrictedModeEnabled]);
// Auto-expand the group containing the current pathname
useEffect(() => {
@@ -97,6 +97,8 @@ interface UserInfo {
role: 'Admin' | 'Operator' | 'Viewer' | null;
canManageBootstrapTokens: boolean;
hasMcpAccess: boolean;
bootstrapTokenEnabled: boolean;
unrestrictedModeEnabled: boolean;
}
interface AuthContextType {
@@ -154,6 +156,8 @@ function AuthProviderInternal({ children }: { children: React.ReactNode }) {
role: (data.role as 'Admin' | 'Operator' | 'Viewer' | null) || null,
canManageBootstrapTokens: (data.canManageBootstrapTokens as boolean) || false,
hasMcpAccess: (data.hasMcpAccess as boolean) || false,
bootstrapTokenEnabled: (data.bootstrapTokenEnabled as boolean) || false,
unrestrictedModeEnabled: (data.unrestrictedModeEnabled as boolean) || false,
};
}
@@ -205,6 +209,8 @@ function AuthProviderInternal({ children }: { children: React.ReactNode }) {
role: null,
canManageBootstrapTokens: false,
hasMcpAccess: false,
bootstrapTokenEnabled: false,
unrestrictedModeEnabled: false,
};
}
}
@@ -223,6 +229,8 @@ function AuthProviderInternal({ children }: { children: React.ReactNode }) {
role: data.role || null,
canManageBootstrapTokens: data.canManageBootstrapTokens || false,
hasMcpAccess: data.hasMcpAccess || false,
bootstrapTokenEnabled: data.bootstrapTokenEnabled || false,
unrestrictedModeEnabled: data.unrestrictedModeEnabled || false,
};
} catch (error) {
// If the refresh token is expired or consent is required, redirect to
@@ -253,6 +261,8 @@ function AuthProviderInternal({ children }: { children: React.ReactNode }) {
role: null,
canManageBootstrapTokens: false,
hasMcpAccess: false,
bootstrapTokenEnabled: false,
unrestrictedModeEnabled: false,
};
}
}, [instance]);
@@ -0,0 +1,399 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { validateBootstrapResponse } from "../bootstrapValidation";
const VALID_TENANT_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
const VALID_TOKEN = "11111111-2222-3333-4444-555555555555";
const VALID_URL =
"https://autopilotmonitor.blob.core.windows.net/agent/AutopilotMonitor-Agent.zip";
// Fixed system time so expiresAt math is deterministic
const FIXED_NOW = new Date("2026-04-08T12:00:00.000Z").getTime();
function inFuture(ms: number): string {
return new Date(FIXED_NOW + ms).toISOString();
}
function validResponse(overrides: Record<string, unknown> = {}) {
return {
success: true,
tenantId: VALID_TENANT_ID,
token: VALID_TOKEN,
agentDownloadUrl: VALID_URL,
expiresAt: inFuture(60 * 60 * 1000), // 1 hour
...overrides,
};
}
describe("validateBootstrapResponse", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(FIXED_NOW);
});
afterEach(() => {
vi.useRealTimers();
});
// ---------- Happy paths ----------
describe("happy path", () => {
it("accepts a realistic backend response", () => {
const result = validateBootstrapResponse(validResponse());
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value.tenantId).toBe(VALID_TENANT_ID);
expect(result.value.token).toBe(VALID_TOKEN);
expect(result.value.agentDownloadUrl).toBe(VALID_URL);
}
});
it("accepts STJ 'O' format with 7 fractional digits", () => {
const result = validateBootstrapResponse(
validResponse({ expiresAt: "2026-04-08T13:34:56.7891234Z" }),
);
expect(result.ok).toBe(true);
});
it("accepts whole-second timestamp without fractional digits", () => {
const result = validateBootstrapResponse(
validResponse({ expiresAt: "2026-04-08T13:34:56Z" }),
);
expect(result.ok).toBe(true);
});
it("accepts numeric timezone offset", () => {
const result = validateBootstrapResponse(
validResponse({ expiresAt: "2026-04-08T15:34:56+02:00" }),
);
expect(result.ok).toBe(true);
});
});
// ---------- Injection payloads (the actual vuln) ----------
describe("injection payloads", () => {
it("rejects token with PS double-quote escape", () => {
const result = validateBootstrapResponse(
validResponse({ token: 'x"; Start-Process calc.exe #' }),
);
expect(result).toEqual({ ok: false, reason: "token" });
});
it("rejects tenantId with PS subshell expansion", () => {
const result = validateBootstrapResponse(
validResponse({ tenantId: "$(calc)" }),
);
expect(result).toEqual({ ok: false, reason: "tenantId" });
});
it("rejects agentDownloadUrl on a foreign host", () => {
const result = validateBootstrapResponse(
validResponse({ agentDownloadUrl: "https://evil.com/agent/x.zip" }),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with $ in pathname (PS subshell)", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://autopilotmonitor.blob.core.windows.net/agent/$(calc).zip",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with backtick in pathname", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://autopilotmonitor.blob.core.windows.net/agent/`whoami`.zip",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with query string (smuggling)", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://autopilotmonitor.blob.core.windows.net/agent/x.zip?sas=evil",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with hash fragment", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://autopilotmonitor.blob.core.windows.net/agent/x.zip#frag",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with trailing-dot hostname", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://autopilotmonitor.blob.core.windows.net./agent/x.zip",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with embedded credentials", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://user:pass@autopilotmonitor.blob.core.windows.net/agent/x.zip",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with non-standard port", () => {
// Note: :443 is normalized to "" by Node's URL parser (default port),
// so it can't be blocked and represents no attack surface. Non-default
// ports like :8443 do remain in url.port and are correctly rejected.
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://autopilotmonitor.blob.core.windows.net:8443/agent/x.zip",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl over http (not https)", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"http://autopilotmonitor.blob.core.windows.net/agent/x.zip",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with leading-dot filename", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://autopilotmonitor.blob.core.windows.net/agent/.hidden.zip",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects agentDownloadUrl with non-zip extension", () => {
const result = validateBootstrapResponse(
validResponse({
agentDownloadUrl:
"https://autopilotmonitor.blob.core.windows.net/agent/x.exe",
}),
);
expect(result).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
});
// ---------- Shape rejection ----------
describe("shape", () => {
it("rejects null", () => {
expect(validateBootstrapResponse(null)).toEqual({ ok: false, reason: "shape" });
});
it("rejects array", () => {
expect(validateBootstrapResponse([])).toEqual({ ok: false, reason: "shape" });
});
it("rejects string primitive", () => {
expect(validateBootstrapResponse("string")).toEqual({
ok: false,
reason: "shape",
});
});
it("rejects number primitive", () => {
expect(validateBootstrapResponse(42)).toEqual({ ok: false, reason: "shape" });
});
it("rejects empty object (success missing)", () => {
expect(validateBootstrapResponse({})).toEqual({
ok: false,
reason: "success",
});
});
it("rejects prototype-pollution probe (fields on __proto__ only)", () => {
const probe = Object.create({
success: true,
tenantId: VALID_TENANT_ID,
token: VALID_TOKEN,
agentDownloadUrl: VALID_URL,
expiresAt: inFuture(60 * 60 * 1000),
});
expect(validateBootstrapResponse(probe)).toEqual({
ok: false,
reason: "success",
});
});
});
// ---------- success coercion ----------
describe("success field", () => {
it("rejects success: false", () => {
expect(
validateBootstrapResponse(validResponse({ success: false })),
).toEqual({ ok: false, reason: "success" });
});
it("rejects success: 1 (truthy number)", () => {
expect(validateBootstrapResponse(validResponse({ success: 1 }))).toEqual({
ok: false,
reason: "success",
});
});
it('rejects success: "true" (string)', () => {
expect(
validateBootstrapResponse(validResponse({ success: "true" })),
).toEqual({ ok: false, reason: "success" });
});
it("rejects missing success", () => {
const { success: _omit, ...rest } = validResponse();
void _omit;
expect(validateBootstrapResponse(rest)).toEqual({
ok: false,
reason: "success",
});
});
});
// ---------- Control chars / non-ASCII ----------
describe("control chars and non-ASCII", () => {
it("rejects tenantId containing null byte", () => {
// Pad to length 36 so we hit ASCII filter, not length check
const padded = "\u0000" + VALID_TENANT_ID.slice(1);
expect(
validateBootstrapResponse(validResponse({ tenantId: padded })),
).toEqual({ ok: false, reason: "tenantId" });
});
it("rejects token containing newline", () => {
const padded = "\n" + VALID_TOKEN.slice(1);
expect(validateBootstrapResponse(validResponse({ token: padded }))).toEqual({
ok: false,
reason: "token",
});
});
it("rejects expiresAt with fullwidth digits (non-ASCII)", () => {
expect(
validateBootstrapResponse(
validResponse({ expiresAt: "2026-04-08T13:34:56\uFF10" }),
),
).toEqual({ ok: false, reason: "expiresAt" });
});
});
// ---------- Length and field-type bounds ----------
describe("length and type bounds", () => {
it("rejects 35-char tenantId", () => {
expect(
validateBootstrapResponse(
validResponse({ tenantId: VALID_TENANT_ID.slice(0, 35) }),
),
).toEqual({ ok: false, reason: "tenantId" });
});
it("rejects empty token", () => {
expect(validateBootstrapResponse(validResponse({ token: "" }))).toEqual({
ok: false,
reason: "token",
});
});
it("rejects agentDownloadUrl over 256 chars", () => {
const longPath = "x".repeat(260);
expect(
validateBootstrapResponse(
validResponse({
agentDownloadUrl: `https://autopilotmonitor.blob.core.windows.net/agent/${longPath}.zip`,
}),
),
).toEqual({ ok: false, reason: "agentDownloadUrl" });
});
it("rejects tenantId of wrong type (number)", () => {
expect(
validateBootstrapResponse(validResponse({ tenantId: 12345 })),
).toEqual({ ok: false, reason: "tenantId" });
});
it("rejects token of wrong type (null)", () => {
expect(validateBootstrapResponse(validResponse({ token: null }))).toEqual({
ok: false,
reason: "token",
});
});
});
// ---------- expiresAt time bounds ----------
describe("expiresAt time bounds", () => {
it("rejects expired (in the past)", () => {
expect(
validateBootstrapResponse(
validResponse({ expiresAt: inFuture(-60 * 1000) }),
),
).toEqual({ ok: false, reason: "expiresAt" });
});
it("rejects exactly now", () => {
expect(
validateBootstrapResponse(validResponse({ expiresAt: inFuture(0) })),
).toEqual({ ok: false, reason: "expiresAt" });
});
it("rejects more than 14 days in the future", () => {
expect(
validateBootstrapResponse(
validResponse({ expiresAt: inFuture(15 * 24 * 60 * 60 * 1000) }),
),
).toEqual({ ok: false, reason: "expiresAt" });
});
it("accepts 7 days in the future (within backend cap)", () => {
const result = validateBootstrapResponse(
validResponse({ expiresAt: inFuture(7 * 24 * 60 * 60 * 1000) }),
);
expect(result.ok).toBe(true);
});
it("rejects malformed string 'not-a-date'", () => {
expect(
validateBootstrapResponse(validResponse({ expiresAt: "not-a-date" })),
).toEqual({ ok: false, reason: "expiresAt" });
});
it("rejects date-only string (no time component)", () => {
expect(
validateBootstrapResponse(validResponse({ expiresAt: "2026-04-08" })),
).toEqual({ ok: false, reason: "expiresAt" });
});
it("rejects timestamp without timezone", () => {
expect(
validateBootstrapResponse(
validResponse({ expiresAt: "2026-04-08T13:34:56" }),
),
).toEqual({ ok: false, reason: "expiresAt" });
});
});
});
@@ -0,0 +1,144 @@
/**
* Bootstrap response validator.
*
* SECURITY-CRITICAL: All validated values are interpolated into a PowerShell
* script that runs as SYSTEM during Windows OOBE (see app/go/[code]/route.ts).
* Every character class below was chosen to block PS metacharacters
* ($, backticks, ", ', #>, newline) so that even if the second-line defense
* (single-quoted PS string literals) is accidentally loosened, the validator
* still prevents injection. Do NOT relax any rule without re-reviewing
* route.ts end-to-end.
*/
import { isGuid } from "./inputValidation";
export interface ValidatedBootstrapResponse {
tenantId: string;
token: string;
agentDownloadUrl: string;
expiresAt: string;
}
export type BootstrapValidationFailure =
| "shape"
| "success"
| "tenantId"
| "token"
| "agentDownloadUrl"
| "expiresAt";
export type BootstrapValidationResult =
| { ok: true; value: ValidatedBootstrapResponse }
| { ok: false; reason: BootstrapValidationFailure };
const PRINTABLE_ASCII_RE = /^[\x20-\x7E]+$/;
const ISO_8601_RE =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
const AGENT_PATH_RE = /^\/agent\/[A-Za-z0-9_-][A-Za-z0-9._-]{0,79}\.zip$/;
const AGENT_HOSTNAME = "autopilotmonitor.blob.core.windows.net";
const FOURTEEN_DAYS_MS = 14 * 24 * 60 * 60 * 1000;
function fail(reason: BootstrapValidationFailure): BootstrapValidationResult {
return { ok: false, reason };
}
function isPrintableAsciiString(value: unknown, maxLength: number): value is string {
return (
typeof value === "string" &&
value.length > 0 &&
value.length <= maxLength &&
PRINTABLE_ASCII_RE.test(value)
);
}
function readField(data: Record<string, unknown>, key: string): unknown {
return Object.prototype.hasOwnProperty.call(data, key) ? data[key] : undefined;
}
function isValidAgentDownloadUrl(value: string): boolean {
let url: URL;
try {
url = new URL(value);
} catch {
return false;
}
if (url.protocol !== "https:") return false;
if (url.hostname !== AGENT_HOSTNAME) return false;
if (url.username !== "" || url.password !== "") return false;
if (url.port !== "") return false;
if (url.search !== "" || url.hash !== "") return false;
if (!AGENT_PATH_RE.test(url.pathname)) return false;
return true;
}
function isValidExpiresAt(value: string): boolean {
if (!ISO_8601_RE.test(value)) return false;
const parsed = new Date(value).getTime();
if (!Number.isFinite(parsed)) return false;
const now = Date.now();
if (parsed <= now) return false;
if (parsed > now + FOURTEEN_DAYS_MS) return false;
return true;
}
export function validateBootstrapResponse(
data: unknown,
): BootstrapValidationResult {
// Shape: must be a plain object (not null, not array)
if (typeof data !== "object" || data === null || Array.isArray(data)) {
return fail("shape");
}
const obj = data as Record<string, unknown>;
// success === true (strict — no truthy coercion)
if (readField(obj, "success") !== true) {
return fail("success");
}
// Field presence + ASCII + length bounds
const tenantId = readField(obj, "tenantId");
if (!isPrintableAsciiString(tenantId, 36) || tenantId.length !== 36) {
return fail("tenantId");
}
if (!isGuid(tenantId)) {
return fail("tenantId");
}
const token = readField(obj, "token");
if (!isPrintableAsciiString(token, 36) || token.length !== 36) {
return fail("token");
}
if (!isGuid(token)) {
return fail("token");
}
const agentDownloadUrl = readField(obj, "agentDownloadUrl");
if (!isPrintableAsciiString(agentDownloadUrl, 256)) {
return fail("agentDownloadUrl");
}
if (!isValidAgentDownloadUrl(agentDownloadUrl)) {
return fail("agentDownloadUrl");
}
const expiresAt = readField(obj, "expiresAt");
if (!isPrintableAsciiString(expiresAt, 40)) {
return fail("expiresAt");
}
if (!isValidExpiresAt(expiresAt)) {
return fail("expiresAt");
}
return {
ok: true,
value: {
tenantId,
token,
agentDownloadUrl,
expiresAt,
},
};
}
@@ -180,6 +180,7 @@ export const EXPANDABLE_NAV_GROUPS: ExpandableNavGroup[] = [
{ id: "cfg-autopilot", label: "Autopilot Validation", href: "/settings/tenant/autopilot" },
{ id: "cfg-hardware", label: "Hardware Whitelist", href: "/settings/tenant/hardware-whitelist" },
{ id: "cfg-notifications", label: "Notifications", href: "/settings/tenant/notifications" },
{ id: "cfg-bootstrap-sessions", label: "Bootstrap Sessions", href: "/settings/tenant/bootstrap-sessions" },
],
},
{
@@ -188,6 +189,7 @@ export const EXPANDABLE_NAV_GROUPS: ExpandableNavGroup[] = [
{ id: "cfg-agent-settings", label: "Agent Settings", href: "/settings/agent/settings" },
{ id: "cfg-agent-analyzers", label: "Agent Analyzers", href: "/settings/agent/analyzers" },
{ id: "cfg-diagnostics", label: "Diagnostics Package", href: "/settings/agent/diagnostics" },
{ id: "cfg-agent-unrestricted", label: "Unrestricted Mode", href: "/settings/agent/unrestricted-mode" },
],
},
{
File diff suppressed because one or more lines are too long