web(admin): Tenant Config Report renders from an exhaustive catalog over the generated wire types (D-215)

The report typed its own TenantConfig interface and drifted for five months: 31 stored
fields were missing (payingCustomer, the MSP overrides, three validation gates, the SLA
block, four analyzer flags, app-homing and trial bookkeeping) and three runtime values
were wrong (upload interval 30 instead of 10, diagnostics-enabled without the Hosted
destination, unrestricted mode without the entitlement).

tenantConfigReportCatalog.ts is Record<keyof TenantConfiguration> and
Record<keyof AgentConfigResponse | CollectorConfiguration | AnalyzerConfiguration>:
every key is rendered or excluded with a reason, so a new C# property fails tsc once the
wire type is regenerated. Runtime rows carry the GetAgentConfigFunction rule and a source
tag; entitlement values come from feature-flags, operator knobs from global/config.
A vitest pins the same parity against SHARED_MANIFEST.tenantConfiguration.fields and the
resolution rules (upload 10, Hosted destination, flags-driven unrestricted mode, admin
idle timeout, analyzer defaults). Empty strings count as unset for the custom highlight.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-06 21:08:03 +02:00
parent 864d1ed31e
commit e3991d55be
4 changed files with 832 additions and 457 deletions
@@ -4,6 +4,20 @@ import { useEffect, useState, useCallback } from 'react';
import { useAuth } from '../../../../contexts/AuthContext';
import { api } from '@/lib/api';
import { apiErrorText, fetchJson } from "@/lib/apiClient";
import type {
AdminConfiguration,
TenantConfiguration,
TenantFeatureFlagsResponse,
} from "@/utils/wire-types.generated";
import {
isNonDefault,
RUNTIME_REPORT_SECTIONS,
TENANT_REPORT_SECTIONS,
UNRESOLVED,
type RowKind,
type RuntimeContext,
type RuntimeSource,
} from './tenantConfigReportCatalog';
// ── Types ────────────────────────────────────────────────────────────────────
@@ -12,201 +26,6 @@ interface TenantInfo {
domainName: string;
}
/** Full tenant configuration as returned by GET /api/config/{tenantId}. */
interface TenantConfig {
tenantId: string;
domainName?: string;
lastUpdated?: string;
updatedBy?: string;
onboardedAt?: string;
contactEmail?: string | null;
companyName?: string | null;
// Tenant status
disabled?: boolean;
disabledReason?: string;
disabledUntil?: string;
mcpDisabled?: boolean;
mcpDisabledReason?: string;
// Security
customRateLimitRequestsPerMinute?: number | null;
customUserRateLimitRequestsPerMinute?: number | null;
manufacturerWhitelist?: string;
modelWhitelist?: string;
validateAutopilotDevice?: boolean;
validateCorporateIdentifier?: boolean;
allowInsecureAgentRequests?: boolean;
// Data management
dataRetentionDays?: number;
sessionTimeoutHours?: number;
maxNdjsonPayloadSizeMB?: number;
// Collectors
enablePerformanceCollector?: boolean;
performanceCollectorIntervalSeconds?: number;
helloWaitTimeoutSeconds?: number;
// Auth circuit breaker
maxAuthFailures?: number | null;
authFailureTimeoutMinutes?: number | null;
agentMaxLifetimeMinutes?: number | null;
// Agent behavior
selfDestructOnComplete?: boolean | null;
keepLogFile?: boolean | null;
rebootOnComplete?: boolean | null;
rebootDelaySeconds?: number | null;
enableGeoLocation?: boolean | null;
enableTimezoneAutoSet?: boolean | null;
enableDoGroupIdAutoSet?: boolean | null;
ntpServer?: string | null;
enableImeMatchLog?: boolean | null;
enableGatherRuleDebugLog?: boolean | null;
enableEspContinueAnywayObservation?: boolean | null;
logLevel?: string | null;
maxBatchSize?: number | null;
showEnrollmentSummary?: boolean | null;
enrollmentSummaryTimeoutSeconds?: number | null;
enrollmentSummaryBrandingImageUrl?: string | null;
enrollmentSummaryLaunchRetrySeconds?: number | null;
showScriptOutput?: boolean | null;
sendTraceEvents?: boolean;
// Analyzers
enableLocalAdminAnalyzer?: boolean | null;
enableSoftwareInventoryAnalyzer?: boolean | null;
localAdminAllowedAccountsJson?: string | null;
// Plan tier
planTier?: string;
trialExpiresUtc?: string | null;
trialConsumed?: boolean;
// Bootstrap & unrestricted
bootstrapTokenEnabled?: boolean;
unrestrictedModeEnabled?: boolean;
unrestrictedMode?: boolean;
// Diagnostics
diagnosticsBlobSasUrl?: string | null;
diagnosticsUploadMode?: string;
diagnosticsLogPathsJson?: string | null;
// Webhook notifications (new)
webhookProviderType?: number;
webhookUrl?: string | null;
webhookNotifyOnSuccess?: boolean;
webhookNotifyOnFailure?: boolean;
webhookNotifyOnStart?: boolean;
notificationChannelsJson?: string | null;
// Teams notifications (legacy)
teamsWebhookUrl?: string | null;
teamsNotifyOnSuccess?: boolean;
teamsNotifyOnFailure?: boolean;
teamsNotifyOnStart?: boolean;
}
// ── Defaults (mirrors C# TenantConfiguration defaults) ──────────────────────
const DEFAULTS: Record<string, unknown> = {
disabled: false,
mcpDisabled: false,
customRateLimitRequestsPerMinute: null,
customUserRateLimitRequestsPerMinute: null,
manufacturerWhitelist: 'Dell*,HP*,Lenovo*,Microsoft Corporation',
modelWhitelist: '*',
validateAutopilotDevice: false,
validateCorporateIdentifier: false,
allowInsecureAgentRequests: false,
dataRetentionDays: 90,
sessionTimeoutHours: 5,
maxNdjsonPayloadSizeMB: 5,
enablePerformanceCollector: true,
performanceCollectorIntervalSeconds: 30,
helloWaitTimeoutSeconds: 30,
maxAuthFailures: null,
authFailureTimeoutMinutes: null,
agentMaxLifetimeMinutes: null,
selfDestructOnComplete: true,
keepLogFile: false,
rebootOnComplete: null,
rebootDelaySeconds: null,
enableGeoLocation: null,
enableTimezoneAutoSet: null,
enableDoGroupIdAutoSet: null,
ntpServer: 'time.windows.com',
enableImeMatchLog: null,
enableGatherRuleDebugLog: null,
enableEspContinueAnywayObservation: null,
logLevel: null,
maxBatchSize: null,
showEnrollmentSummary: null,
enrollmentSummaryTimeoutSeconds: null,
enrollmentSummaryBrandingImageUrl: null,
enrollmentSummaryLaunchRetrySeconds: null,
showScriptOutput: true,
sendTraceEvents: true,
enableLocalAdminAnalyzer: null,
enableSoftwareInventoryAnalyzer: null,
localAdminAllowedAccountsJson: null,
planTier: 'free',
bootstrapTokenEnabled: false,
unrestrictedModeEnabled: false,
unrestrictedMode: false,
diagnosticsBlobSasUrl: null,
diagnosticsUploadMode: 'Off',
diagnosticsLogPathsJson: null,
webhookProviderType: 0,
webhookUrl: null,
webhookNotifyOnSuccess: true,
webhookNotifyOnFailure: true,
webhookNotifyOnStart: false,
teamsWebhookUrl: null,
teamsNotifyOnSuccess: true,
teamsNotifyOnFailure: true,
teamsNotifyOnStart: false,
};
// Runtime defaults (what the agent actually receives if tenant didn't set a value)
const RUNTIME_DEFAULTS: Record<string, unknown> = {
uploadIntervalSeconds: 30,
selfDestructOnComplete: true,
keepLogFile: false,
enableGeoLocation: true,
enableTimezoneAutoSet: false,
enableDoGroupIdAutoSet: false,
ntpServer: 'time.windows.com',
enableImeMatchLog: false,
enableGatherRuleDebugLog: false,
enableEspContinueAnywayObservation: false,
maxAuthFailures: 5,
authFailureTimeoutMinutes: 0,
logLevel: 'Info',
rebootOnComplete: false,
rebootDelaySeconds: 10,
showEnrollmentSummary: false,
enrollmentSummaryTimeoutSeconds: 60,
enrollmentSummaryLaunchRetrySeconds: 120,
maxBatchSize: 100,
diagnosticsUploadMode: 'Off',
sendTraceEvents: true,
unrestrictedMode: false,
// Collectors
enablePerformanceCollector: true,
performanceIntervalSeconds: 30,
collectorIdleTimeoutMinutes: 15,
enableAgentSelfMetrics: true,
agentSelfMetricsIntervalSeconds: 60,
helloWaitTimeoutSeconds: 30,
agentMaxLifetimeMinutes: 360,
// Analyzers
enableLocalAdminAnalyzer: true,
enableSoftwareInventoryAnalyzer: false,
};
// ── Helpers ──────────────────────────────────────────────────────────────────
const WEBHOOK_PROVIDERS: Record<number, string> = {
@@ -218,21 +37,22 @@ const WEBHOOK_PROVIDERS: Record<number, string> = {
30: 'Discord',
};
const SOURCE_LABELS: Record<RuntimeSource, string> = {
tenant: 'tenant',
admin: 'global',
flags: 'entitlement',
constant: 'fixed',
};
function formatValue(val: unknown): string {
if (val === null || val === undefined) return '—';
if (val === UNRESOLVED) return 'n/a (source not loaded)';
if (typeof val === 'boolean') return val ? 'Yes' : 'No';
if (typeof val === 'string') return val || '—';
if (Array.isArray(val)) return val.length ? val.join(', ') : '—';
return String(val);
}
function isNonDefault(key: string, val: unknown, defaults: Record<string, unknown>): boolean {
if (!(key in defaults)) return false;
const def = defaults[key];
// Both null/undefined → same
if ((val === null || val === undefined) && (def === null || def === undefined)) return false;
return val !== def;
}
function maskSasUrl(url: string | null | undefined): string {
if (!url) return '—';
try {
@@ -252,30 +72,37 @@ function formatDate(val: string | null | undefined): string {
}
}
// ── Section component ────────────────────────────────────────────────────────
function formatByKind(kind: RowKind | undefined, value: unknown): string {
switch (kind) {
case 'date':
return formatDate(value as string);
case 'masked':
return maskSasUrl(value as string);
case 'secret':
return value ? 'configured (hidden)' : '—';
default:
return formatValue(value);
}
}
// ── Row and section primitives ───────────────────────────────────────────────
interface ConfigRowProps {
label: string;
value: unknown;
configKey?: string;
defaults?: Record<string, unknown>;
masked?: boolean;
isDate?: boolean;
display: string;
highlight?: boolean;
sourceTag?: string;
}
function ConfigRow({ label, value, configKey, defaults, masked, isDate }: ConfigRowProps) {
const highlight = configKey && defaults ? isNonDefault(configKey, value, defaults) : false;
const displayVal = masked
? maskSasUrl(value as string)
: isDate
? formatDate(value as string)
: formatValue(value);
function ConfigRow({ label, display, highlight, sourceTag }: ConfigRowProps) {
return (
<tr className={highlight ? 'bg-purple-50 dark:bg-purple-900/20' : ''}>
<td className="py-1.5 px-3 text-sm text-gray-600 dark:text-gray-400 font-medium whitespace-nowrap">{label}</td>
<td className="py-1.5 px-3 text-sm text-gray-600 dark:text-gray-400 font-medium whitespace-nowrap">
{label}
{sourceTag && <span className="ml-2 text-xs text-gray-400 dark:text-gray-500 font-normal">{sourceTag}</span>}
</td>
<td className="py-1.5 px-3 text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
{displayVal}
{display}
{highlight && <span className="ml-2 text-xs text-purple-600 dark:text-purple-400 font-sans">(custom)</span>}
</td>
</tr>
@@ -300,46 +127,63 @@ function Section({ title, children }: SectionProps) {
);
}
// ── Compute runtime parameters ───────────────────────────────────────────────
// ── Notification channels (the one non-scalar block) ─────────────────────────
function computeRuntime(c: TenantConfig): Record<string, unknown> {
return {
uploadIntervalSeconds: 30,
selfDestructOnComplete: c.selfDestructOnComplete ?? true,
keepLogFile: c.keepLogFile ?? false,
enableGeoLocation: c.enableGeoLocation ?? true,
enableTimezoneAutoSet: c.enableTimezoneAutoSet ?? false,
enableDoGroupIdAutoSet: c.enableDoGroupIdAutoSet ?? false,
ntpServer: c.ntpServer ?? 'time.windows.com',
enableImeMatchLog: c.enableImeMatchLog ?? false,
enableGatherRuleDebugLog: c.enableGatherRuleDebugLog ?? false,
enableEspContinueAnywayObservation: c.enableEspContinueAnywayObservation ?? false,
maxAuthFailures: c.maxAuthFailures ?? 5,
authFailureTimeoutMinutes: c.authFailureTimeoutMinutes ?? 0,
logLevel: c.logLevel ?? 'Info',
rebootOnComplete: c.rebootOnComplete ?? false,
rebootDelaySeconds: c.rebootDelaySeconds ?? 10,
showEnrollmentSummary: c.showEnrollmentSummary ?? false,
enrollmentSummaryTimeoutSeconds: c.enrollmentSummaryTimeoutSeconds ?? 60,
enrollmentSummaryBrandingImageUrl: c.enrollmentSummaryBrandingImageUrl ?? null,
enrollmentSummaryLaunchRetrySeconds: c.enrollmentSummaryLaunchRetrySeconds ?? 120,
maxBatchSize: c.maxBatchSize ?? 100,
diagnosticsUploadEnabled: !!(c.diagnosticsBlobSasUrl && c.diagnosticsUploadMode && c.diagnosticsUploadMode !== 'Off'),
diagnosticsUploadMode: c.diagnosticsUploadMode ?? 'Off',
sendTraceEvents: c.sendTraceEvents ?? true,
unrestrictedMode: c.unrestrictedMode ?? false,
// Collectors
enablePerformanceCollector: c.enablePerformanceCollector ?? true,
performanceIntervalSeconds: c.performanceCollectorIntervalSeconds ?? 30,
collectorIdleTimeoutMinutes: 15,
enableAgentSelfMetrics: true,
agentSelfMetricsIntervalSeconds: 60,
helloWaitTimeoutSeconds: c.helloWaitTimeoutSeconds ?? 30,
agentMaxLifetimeMinutes: c.agentMaxLifetimeMinutes ?? 360,
// Analyzers
enableLocalAdminAnalyzer: c.enableLocalAdminAnalyzer ?? true,
enableSoftwareInventoryAnalyzer: c.enableSoftwareInventoryAnalyzer ?? false,
};
interface ChannelView {
id: string;
name?: string;
providerType?: number;
url?: string;
enabled?: boolean;
notifyOnStart?: boolean;
notifyOnSuccess?: boolean;
notifyOnFailure?: boolean;
notifyOnSlaEvents?: boolean;
}
/** Channel list when migrated, else the legacy single-webhook fields — mirrors TenantConfiguration.GetNotificationChannels. */
function resolveChannels(config: TenantConfiguration): ChannelView[] {
if (config.notificationChannelsJson) {
try {
const parsed = JSON.parse(config.notificationChannelsJson);
if (Array.isArray(parsed) && parsed.length > 0) return parsed as ChannelView[];
} catch { /* malformed → fall back to legacy display */ }
}
if (config.webhookUrl && config.webhookProviderType) {
return [{ id: 'legacy', name: 'Default (legacy)', providerType: config.webhookProviderType, url: config.webhookUrl, enabled: true, notifyOnStart: config.webhookNotifyOnStart, notifyOnSuccess: config.webhookNotifyOnSuccess, notifyOnFailure: config.webhookNotifyOnFailure, notifyOnSlaEvents: true }];
}
if (config.teamsWebhookUrl) {
return [{ id: 'legacy', name: 'Default (legacy)', providerType: 1, url: config.teamsWebhookUrl, enabled: true, notifyOnStart: config.teamsNotifyOnStart, notifyOnSuccess: config.teamsNotifyOnSuccess, notifyOnFailure: config.teamsNotifyOnFailure, notifyOnSlaEvents: true }];
}
return [];
}
function ChannelRows({ config }: { config: TenantConfiguration }) {
const channels = resolveChannels(config);
if (channels.length === 0) {
return <ConfigRow label="Provider" display={WEBHOOK_PROVIDERS[0]} />;
}
return (
<>
{channels.map((ch, i) => (
<tr key={ch.id ?? i} className={i > 0 ? 'border-t-2 border-gray-200 dark:border-gray-600' : ''}>
<td colSpan={2} className="p-0">
<table className="w-full">
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
<ConfigRow label="Channel" display={`${ch.name || ch.id}${ch.enabled === false ? ' (disabled)' : ''}`} />
<ConfigRow label="Provider" display={WEBHOOK_PROVIDERS[ch.providerType ?? 0] ?? 'Unknown'} />
<ConfigRow label="Webhook URL" display={maskSasUrl(ch.url)} />
<ConfigRow label="Notify On Start" display={formatValue(ch.notifyOnStart ?? false)} />
<ConfigRow label="Notify On Success" display={formatValue(ch.notifyOnSuccess ?? false)} />
<ConfigRow label="Notify On Failure" display={formatValue(ch.notifyOnFailure ?? false)} />
<ConfigRow label="Notify On SLA" display={formatValue(ch.notifyOnSlaEvents ?? false)} />
</tbody>
</table>
</td>
</tr>
))}
</>
);
}
// ── Main component ───────────────────────────────────────────────────────────
@@ -349,13 +193,13 @@ export function SectionTenantConfigReport() {
const [tenants, setTenants] = useState<TenantInfo[]>([]);
const [selectedTenantId, setSelectedTenantId] = useState('');
// Mount-time clock for trial-expiry checks — render must stay pure
// (react-hooks/purity); day-granularity expiry doesn't need a live clock.
const [nowMs] = useState(() => Date.now());
const [config, setConfig] = useState<TenantConfig | null>(null);
const [config, setConfig] = useState<TenantConfiguration | null>(null);
const [flags, setFlags] = useState<TenantFeatureFlagsResponse | null>(null);
const [adminConfig, setAdminConfig] = useState<AdminConfiguration | null>(null);
const [loading, setLoading] = useState(false);
const [loadingTenants, setLoadingTenants] = useState(true);
const [error, setError] = useState<string | null>(null);
const [sideError, setSideError] = useState<string | null>(null);
// Fetch tenant list
useEffect(() => {
@@ -379,20 +223,34 @@ export function SectionTenantConfigReport() {
fetchTenants();
}, [user?.isGlobalAdmin, getAccessToken]);
// Fetch config for selected tenant
// Fetch config plus its two runtime sources for the selected tenant. The full config is
// the report; feature-flags (entitlement) and global config (operator knobs) are fail-soft —
// their rows show "n/a" and a notice instead of blocking the whole report.
const fetchConfig = useCallback(async () => {
if (!selectedTenantId) return;
try {
setLoading(true);
setError(null);
const data = await fetchJson<TenantConfig>(
api.config.tenant(selectedTenantId),
getAccessToken,
);
setConfig(data);
setSideError(null);
const [configResult, flagsResult, adminResult] = await Promise.allSettled([
fetchJson<TenantConfiguration>(api.config.tenant(selectedTenantId), getAccessToken),
fetchJson<TenantFeatureFlagsResponse>(api.config.featureFlags(selectedTenantId), getAccessToken),
fetchJson<AdminConfiguration>(api.globalConfig.get(), getAccessToken),
]);
if (configResult.status === 'rejected') throw configResult.reason;
setConfig(configResult.value);
setFlags(flagsResult.status === 'fulfilled' ? flagsResult.value : null);
setAdminConfig(adminResult.status === 'fulfilled' ? adminResult.value : null);
const sideFailures = [
flagsResult.status === 'rejected' ? `feature flags: ${apiErrorText(flagsResult.reason)}` : null,
adminResult.status === 'rejected' ? `global config: ${apiErrorText(adminResult.reason)}` : null,
].filter((s): s is string => s !== null);
setSideError(sideFailures.length ? `Runtime sources unavailable — ${sideFailures.join('; ')}` : null);
} catch (err) {
setError(apiErrorText(err));
setConfig(null);
setFlags(null);
setAdminConfig(null);
} finally {
setLoading(false);
}
@@ -405,30 +263,7 @@ export function SectionTenantConfigReport() {
void run();
}, [fetchConfig]);
// Computed runtime
const runtime = config ? computeRuntime(config) : null;
// Webhook display helper: channel list when migrated, else legacy single-webhook fields
// (mirrors the backend's GetNotificationChannels fallback order).
const webhookChannels = config
? (() => {
if (config.notificationChannelsJson) {
try {
const parsed = JSON.parse(config.notificationChannelsJson);
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed as { id: string; name?: string; providerType?: number; url?: string; enabled?: boolean; notifyOnStart?: boolean; notifyOnSuccess?: boolean; notifyOnFailure?: boolean; notifyOnSlaEvents?: boolean }[];
}
} catch { /* malformed → fall back to legacy display */ }
}
if (config.webhookUrl && config.webhookProviderType) {
return [{ id: 'legacy', name: 'Default (legacy)', providerType: config.webhookProviderType, url: config.webhookUrl, enabled: true, notifyOnStart: config.webhookNotifyOnStart, notifyOnSuccess: config.webhookNotifyOnSuccess, notifyOnFailure: config.webhookNotifyOnFailure, notifyOnSlaEvents: true }];
}
if (config.teamsWebhookUrl) {
return [{ id: 'legacy', name: 'Default (legacy)', providerType: 1, url: config.teamsWebhookUrl, enabled: true, notifyOnStart: config.teamsNotifyOnStart, notifyOnSuccess: config.teamsNotifyOnSuccess, notifyOnFailure: config.teamsNotifyOnFailure, notifyOnSlaEvents: true }];
}
return [];
})()
: [];
const runtimeCtx: RuntimeContext | null = config ? { config, adminConfig, flags } : null;
return (
<div className="max-w-5xl mx-auto px-4 py-8">
@@ -487,7 +322,7 @@ export function SectionTenantConfigReport() {
)}
{/* Config report */}
{!loading && config && (
{!loading && config && runtimeCtx && (
<>
{/* Tenant header card */}
<div className="mb-6 p-4 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
@@ -509,6 +344,34 @@ export function SectionTenantConfigReport() {
<span className="font-medium text-gray-900 dark:text-gray-100">{config.updatedBy || '—'}</span>
</div>
</div>
{/* Effective entitlement — backend-resolved (feature-flags), not the stored plan tier */}
{flags && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm mt-3 pt-3 border-t border-purple-200 dark:border-purple-800">
<div>
<span className="text-gray-500 dark:text-gray-400 block">Effective Edition</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{flags.edition}
<span className="ml-1 text-xs text-gray-500 dark:text-gray-400">via {flags.editionSource}</span>
{flags.isTrial && flags.trialExpiresUtc ? ` (trial until ${formatDate(flags.trialExpiresUtc)})` : ''}
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 block">MCP Usage Plan</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{flags.entitlements.mcpUsagePlan || '—'}</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 block">Delegated Tenant Slots</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{flags.entitlements.maxDelegatedTenants}
{!flags.entitlements.delegatedAdminAllowed && <span className="ml-1 text-xs text-gray-500 dark:text-gray-400">(delegation not included)</span>}
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 block">Retention Cap (days)</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{flags.entitlements.retentionCapDays}</span>
</div>
</div>
)}
{config.disabled && (
<div className="mt-3 p-2 bg-red-100 dark:bg-red-900/40 rounded text-sm text-red-700 dark:text-red-300 font-medium">
TENANT DISABLED{config.disabledReason ? `: ${config.disabledReason}` : ''}
@@ -522,6 +385,12 @@ export function SectionTenantConfigReport() {
)}
</div>
{sideError && (
<div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg text-sm text-amber-800 dark:text-amber-300">
{sideError}
</div>
)}
{/* Legend */}
<div className="mb-4 text-xs text-gray-500 dark:text-gray-400 flex items-center gap-2">
<span className="inline-block w-3 h-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-700 rounded" />
@@ -533,177 +402,48 @@ export function SectionTenantConfigReport() {
<div>
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Tenant Configuration</h2>
<Section title="Tenant Status">
<ConfigRow label="Disabled" value={config.disabled} configKey="disabled" defaults={DEFAULTS} />
<ConfigRow label="Disabled Reason" value={config.disabledReason} />
<ConfigRow label="Disabled Until" value={config.disabledUntil} isDate />
<ConfigRow label="Onboarded At" value={config.onboardedAt} isDate />
<ConfigRow label="Contact Email" value={config.contactEmail} />
<ConfigRow label="Company" value={config.companyName} />
<ConfigRow
label="Plan Tier"
value={`${config.planTier || 'free'}${config.trialExpiresUtc && new Date(config.trialExpiresUtc).getTime() > nowMs ? ` (trial until ${formatDate(config.trialExpiresUtc)})` : ''}`}
configKey="planTier"
defaults={DEFAULTS}
/>
</Section>
<Section title="Security & Validation">
<ConfigRow label="Validate Autopilot Device" value={config.validateAutopilotDevice} configKey="validateAutopilotDevice" defaults={DEFAULTS} />
<ConfigRow label="Validate Corporate Identifier" value={config.validateCorporateIdentifier} configKey="validateCorporateIdentifier" defaults={DEFAULTS} />
<ConfigRow label="Allow Insecure Agent Requests" value={config.allowInsecureAgentRequests} configKey="allowInsecureAgentRequests" defaults={DEFAULTS} />
<ConfigRow label="Device API Rate Limit Override" value={config.customRateLimitRequestsPerMinute} configKey="customRateLimitRequestsPerMinute" defaults={DEFAULTS} />
<ConfigRow label="MCP & Integrations API Rate Limit Override" value={config.customUserRateLimitRequestsPerMinute} configKey="customUserRateLimitRequestsPerMinute" defaults={DEFAULTS} />
</Section>
<Section title="Hardware Whitelist">
<ConfigRow label="Manufacturers" value={config.manufacturerWhitelist} configKey="manufacturerWhitelist" defaults={DEFAULTS} />
<ConfigRow label="Models" value={config.modelWhitelist} configKey="modelWhitelist" defaults={DEFAULTS} />
</Section>
<Section title="Data Management">
<ConfigRow label="Data Retention (days)" value={config.dataRetentionDays} configKey="dataRetentionDays" defaults={DEFAULTS} />
<ConfigRow label="Session Timeout (hours)" value={config.sessionTimeoutHours} configKey="sessionTimeoutHours" defaults={DEFAULTS} />
<ConfigRow label="Max NDJSON Payload (MB)" value={config.maxNdjsonPayloadSizeMB} configKey="maxNdjsonPayloadSizeMB" defaults={DEFAULTS} />
</Section>
<Section title="Agent Collectors">
<ConfigRow label="Performance Collector" value={config.enablePerformanceCollector} configKey="enablePerformanceCollector" defaults={DEFAULTS} />
<ConfigRow label="Perf. Interval (sec)" value={config.performanceCollectorIntervalSeconds} configKey="performanceCollectorIntervalSeconds" defaults={DEFAULTS} />
<ConfigRow label="Hello Wait Timeout (sec)" value={config.helloWaitTimeoutSeconds} configKey="helloWaitTimeoutSeconds" defaults={DEFAULTS} />
</Section>
<Section title="Auth Circuit Breaker">
<ConfigRow label="Max Auth Failures" value={config.maxAuthFailures} configKey="maxAuthFailures" defaults={DEFAULTS} />
<ConfigRow label="Auth Failure Timeout (min)" value={config.authFailureTimeoutMinutes} configKey="authFailureTimeoutMinutes" defaults={DEFAULTS} />
<ConfigRow label="Agent Max Lifetime (min)" value={config.agentMaxLifetimeMinutes} configKey="agentMaxLifetimeMinutes" defaults={DEFAULTS} />
</Section>
<Section title="Agent Behavior">
<ConfigRow label="Self-Destruct On Complete" value={config.selfDestructOnComplete} configKey="selfDestructOnComplete" defaults={DEFAULTS} />
<ConfigRow label="Keep Log File" value={config.keepLogFile} configKey="keepLogFile" defaults={DEFAULTS} />
<ConfigRow label="Reboot On Complete" value={config.rebootOnComplete} configKey="rebootOnComplete" defaults={DEFAULTS} />
<ConfigRow label="Reboot Delay (sec)" value={config.rebootDelaySeconds} configKey="rebootDelaySeconds" defaults={DEFAULTS} />
<ConfigRow label="Geo-Location" value={config.enableGeoLocation} configKey="enableGeoLocation" defaults={DEFAULTS} />
<ConfigRow label="Timezone Auto-Set" value={config.enableTimezoneAutoSet} configKey="enableTimezoneAutoSet" defaults={DEFAULTS} />
<ConfigRow label="DO GroupId Auto-Set" value={config.enableDoGroupIdAutoSet} configKey="enableDoGroupIdAutoSet" defaults={DEFAULTS} />
<ConfigRow label="NTP Server" value={config.ntpServer} configKey="ntpServer" defaults={DEFAULTS} />
<ConfigRow label="IME Match Log" value={config.enableImeMatchLog} configKey="enableImeMatchLog" defaults={DEFAULTS} />
<ConfigRow label="Gather Rule Debug Log" value={config.enableGatherRuleDebugLog} configKey="enableGatherRuleDebugLog" defaults={DEFAULTS} />
<ConfigRow label="Continue-Anyway Observation" value={config.enableEspContinueAnywayObservation} configKey="enableEspContinueAnywayObservation" defaults={DEFAULTS} />
<ConfigRow label="Log Level" value={config.logLevel} configKey="logLevel" defaults={DEFAULTS} />
<ConfigRow label="Max Batch Size" value={config.maxBatchSize} configKey="maxBatchSize" defaults={DEFAULTS} />
<ConfigRow label="Show Script Output" value={config.showScriptOutput} configKey="showScriptOutput" defaults={DEFAULTS} />
<ConfigRow label="Send Trace Events" value={config.sendTraceEvents} configKey="sendTraceEvents" defaults={DEFAULTS} />
</Section>
<Section title="Enrollment Summary">
<ConfigRow label="Show Summary" value={config.showEnrollmentSummary} configKey="showEnrollmentSummary" defaults={DEFAULTS} />
<ConfigRow label="Timeout (sec)" value={config.enrollmentSummaryTimeoutSeconds} configKey="enrollmentSummaryTimeoutSeconds" defaults={DEFAULTS} />
<ConfigRow label="Branding Image URL" value={config.enrollmentSummaryBrandingImageUrl} configKey="enrollmentSummaryBrandingImageUrl" defaults={DEFAULTS} />
<ConfigRow label="Launch Retry (sec)" value={config.enrollmentSummaryLaunchRetrySeconds} configKey="enrollmentSummaryLaunchRetrySeconds" defaults={DEFAULTS} />
</Section>
<Section title="Analyzers">
<ConfigRow label="Local Admin Analyzer" value={config.enableLocalAdminAnalyzer} configKey="enableLocalAdminAnalyzer" defaults={DEFAULTS} />
<ConfigRow label="Software Inventory Analyzer" value={config.enableSoftwareInventoryAnalyzer} configKey="enableSoftwareInventoryAnalyzer" defaults={DEFAULTS} />
<ConfigRow label="Allowed Local Admin Accounts" value={config.localAdminAllowedAccountsJson} configKey="localAdminAllowedAccountsJson" defaults={DEFAULTS} />
</Section>
<Section title="Webhooks">
{webhookChannels.length === 0 && (
<ConfigRow label="Provider" value={WEBHOOK_PROVIDERS[0] ?? 'None'} />
)}
{webhookChannels.map((ch, i) => (
<div key={ch.id ?? i} className={i > 0 ? 'mt-2 pt-2 border-t border-gray-100 dark:border-gray-700' : ''}>
<ConfigRow label="Channel" value={`${ch.name || ch.id}${ch.enabled === false ? ' (disabled)' : ''}`} />
<ConfigRow label="Provider" value={WEBHOOK_PROVIDERS[ch.providerType ?? 0] ?? 'Unknown'} />
<ConfigRow label="Webhook URL" value={ch.url} masked />
<ConfigRow label="Notify On Start" value={ch.notifyOnStart ?? false} />
<ConfigRow label="Notify On Success" value={ch.notifyOnSuccess ?? false} />
<ConfigRow label="Notify On Failure" value={ch.notifyOnFailure ?? false} />
<ConfigRow label="Notify On SLA" value={ch.notifyOnSlaEvents ?? false} />
</div>
))}
</Section>
<Section title="Diagnostics">
<ConfigRow label="Upload Mode" value={config.diagnosticsUploadMode} configKey="diagnosticsUploadMode" defaults={DEFAULTS} />
<ConfigRow label="Blob SAS URL" value={config.diagnosticsBlobSasUrl} masked />
<ConfigRow label="Custom Log Paths" value={config.diagnosticsLogPathsJson} configKey="diagnosticsLogPathsJson" defaults={DEFAULTS} />
</Section>
<Section title="Feature Flags (Global)">
<ConfigRow label="Bootstrap Token Enabled" value={config.bootstrapTokenEnabled} configKey="bootstrapTokenEnabled" defaults={DEFAULTS} />
<ConfigRow label="Unrestricted Mode Enabled" value={config.unrestrictedModeEnabled} configKey="unrestrictedModeEnabled" defaults={DEFAULTS} />
<ConfigRow label="Unrestricted Mode Active" value={config.unrestrictedMode} configKey="unrestrictedMode" defaults={DEFAULTS} />
</Section>
{TENANT_REPORT_SECTIONS.map(({ section, rows }) => (
<Section key={section} title={section}>
{section === 'Webhooks' && <ChannelRows config={config} />}
{rows.map(({ key, row }) => {
const value = config[key as keyof TenantConfiguration];
return (
<ConfigRow
key={key}
label={row.label}
display={formatByKind(row.kind, value)}
highlight={'default' in row && isNonDefault(value, row.default)}
/>
);
})}
</Section>
))}
</div>
{/* ────── RIGHT: Runtime Parameters (Agent Config) ────── */}
<div>
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Runtime Parameters (Agent)</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
These are the effective values the agent receives at runtime, with defaults applied for unset fields.
The effective values the agent receives at runtime, with defaults applied for unset fields.
The tag says where a value is resolved from: tenant config, global config, the tenant&apos;s entitlement, or a fixed backend constant.
</p>
{runtime && (
<>
<Section title="Upload & Batching">
<ConfigRow label="Upload Interval (sec)" value={runtime.uploadIntervalSeconds} configKey="uploadIntervalSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Max Batch Size" value={runtime.maxBatchSize} configKey="maxBatchSize" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Agent Behavior">
<ConfigRow label="Self-Destruct" value={runtime.selfDestructOnComplete} configKey="selfDestructOnComplete" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Keep Log File" value={runtime.keepLogFile} configKey="keepLogFile" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Reboot On Complete" value={runtime.rebootOnComplete} configKey="rebootOnComplete" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Reboot Delay (sec)" value={runtime.rebootDelaySeconds} configKey="rebootDelaySeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Geo-Location" value={runtime.enableGeoLocation} configKey="enableGeoLocation" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Timezone Auto-Set" value={runtime.enableTimezoneAutoSet} configKey="enableTimezoneAutoSet" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="DO GroupId Auto-Set" value={runtime.enableDoGroupIdAutoSet} configKey="enableDoGroupIdAutoSet" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="NTP Server" value={runtime.ntpServer} configKey="ntpServer" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="IME Match Log" value={runtime.enableImeMatchLog} configKey="enableImeMatchLog" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Gather Rule Debug Log" value={runtime.enableGatherRuleDebugLog} configKey="enableGatherRuleDebugLog" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Continue-Anyway Observation" value={runtime.enableEspContinueAnywayObservation} configKey="enableEspContinueAnywayObservation" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Log Level" value={runtime.logLevel} configKey="logLevel" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Send Trace Events" value={runtime.sendTraceEvents} configKey="sendTraceEvents" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Unrestricted Mode" value={runtime.unrestrictedMode} configKey="unrestrictedMode" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Auth Circuit Breaker">
<ConfigRow label="Max Auth Failures" value={runtime.maxAuthFailures} configKey="maxAuthFailures" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Auth Failure Timeout (min)" value={runtime.authFailureTimeoutMinutes} configKey="authFailureTimeoutMinutes" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Enrollment Summary">
<ConfigRow label="Show Summary" value={runtime.showEnrollmentSummary} configKey="showEnrollmentSummary" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Timeout (sec)" value={runtime.enrollmentSummaryTimeoutSeconds} configKey="enrollmentSummaryTimeoutSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Branding Image URL" value={runtime.enrollmentSummaryBrandingImageUrl} />
<ConfigRow label="Launch Retry (sec)" value={runtime.enrollmentSummaryLaunchRetrySeconds} configKey="enrollmentSummaryLaunchRetrySeconds" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Collectors">
<ConfigRow label="Performance Collector" value={runtime.enablePerformanceCollector} configKey="enablePerformanceCollector" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Perf. Interval (sec)" value={runtime.performanceIntervalSeconds} configKey="performanceIntervalSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Idle Timeout (min)" value={runtime.collectorIdleTimeoutMinutes} configKey="collectorIdleTimeoutMinutes" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Agent Self-Metrics" value={runtime.enableAgentSelfMetrics} configKey="enableAgentSelfMetrics" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Self-Metrics Interval (sec)" value={runtime.agentSelfMetricsIntervalSeconds} configKey="agentSelfMetricsIntervalSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Hello Wait Timeout (sec)" value={runtime.helloWaitTimeoutSeconds} configKey="helloWaitTimeoutSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Agent Max Lifetime (min)" value={runtime.agentMaxLifetimeMinutes} configKey="agentMaxLifetimeMinutes" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Diagnostics">
<ConfigRow label="Upload Enabled" value={runtime.diagnosticsUploadEnabled} />
<ConfigRow label="Upload Mode" value={runtime.diagnosticsUploadMode} configKey="diagnosticsUploadMode" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Analyzers">
<ConfigRow label="Local Admin Analyzer" value={runtime.enableLocalAdminAnalyzer} configKey="enableLocalAdminAnalyzer" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Software Inventory Analyzer" value={runtime.enableSoftwareInventoryAnalyzer} configKey="enableSoftwareInventoryAnalyzer" defaults={RUNTIME_DEFAULTS} />
</Section>
</>
)}
{RUNTIME_REPORT_SECTIONS.map(({ section, rows }) => (
<Section key={section} title={section}>
{rows.map(({ key, row }) => {
const value = row.value(runtimeCtx);
return (
<ConfigRow
key={key}
label={row.label}
sourceTag={SOURCE_LABELS[row.source]}
display={formatValue(value)}
highlight={'default' in row && value !== UNRESOLVED && isNonDefault(value, row.default)}
/>
);
})}
</Section>
))}
</div>
</div>
</>
@@ -0,0 +1,215 @@
/**
* Parity pins for the Tenant Config Report catalog.
*
* The catalog is compile-time exhaustive over the generated wire types; this file adds the
* runtime half against SHARED_MANIFEST.tenantConfiguration.fields (the C#-reflected field
* list, independent of the TS type) and pins the runtime resolution rules that mirror
* GetAgentConfigFunction.cs with test-local fixtures.
*/
import { describe, expect, it } from "vitest";
import { SHARED_MANIFEST } from "@/utils/shared-manifests.generated";
import type { AdminConfiguration, TenantConfiguration, TenantFeatureFlagsResponse } from "@/utils/wire-types.generated";
import {
ANALYZER_FIELDS,
COLLECTOR_FIELDS,
DEFAULT_UPLOAD_INTERVAL_SECONDS,
isExcluded,
isNonDefault,
RUNTIME_FIELDS,
RUNTIME_REPORT_SECTIONS,
TENANT_FIELDS,
TENANT_REPORT_SECTIONS,
UNRESOLVED,
type RuntimeContext,
} from "../tenantConfigReportCatalog";
const manifestFields = [...SHARED_MANIFEST.tenantConfiguration.fields] as string[];
describe("TENANT_FIELDS parity with the C# field manifest", () => {
it("catalogues every wire field of TenantConfiguration (rendered or excluded with a reason)", () => {
const missing = manifestFields.filter((f) => !(f in TENANT_FIELDS));
expect(missing).toEqual([]);
});
it("has no key outside the manifest (a removed C# property must leave the catalog)", () => {
const stale = Object.keys(TENANT_FIELDS).filter((k) => !manifestFields.includes(k));
expect(stale).toEqual([]);
});
it("gives every exclusion a reason and every row a non-empty label", () => {
for (const [key, spec] of Object.entries(TENANT_FIELDS)) {
if (isExcluded(spec)) expect(spec.excluded, key).not.toBe("");
else expect(spec.label, key).not.toBe("");
}
});
it("renders the fields the operator asked for by name", () => {
const rendered = new Set(TENANT_REPORT_SECTIONS.flatMap((s) => s.rows.map((r) => r.key)));
for (const key of [
"payingCustomer", "maxDelegatedTenantsOverride", "mcpUsagePlanOverride", "managedByProTenantId",
"trialConsumed", "proDowngradedUtc", "validateDeviceAssociation", "validateCloudPcDevice",
"validateIntuneDeviceBinding", "sessionGraceHours", "absoluteMaxSessionHours",
"diagnosticsUploadDestination", "webhookNotifyOnHardwareRejection", "entraAppRolesEnabled",
"slaTargetSuccessRate", "slaConsecutiveFailureThreshold", "keepAwakeDuringUserEsp",
]) {
expect(rendered.has(key), key).toBe(true);
}
});
it("renders each field in exactly one section", () => {
const seen = new Map<string, number>();
for (const { rows } of TENANT_REPORT_SECTIONS) {
for (const { key } of rows) seen.set(key, (seen.get(key) ?? 0) + 1);
}
expect([...seen.entries()].filter(([, n]) => n !== 1)).toEqual([]);
});
});
// ── Runtime rules ────────────────────────────────────────────────────────────
/** Minimal stored config: everything nullable unset, non-nullables at their C# initializers. */
function storedConfig(overrides: Partial<TenantConfiguration> = {}): TenantConfiguration {
return {
tenantId: "00000000-0000-0000-0000-000000000001",
domainName: "contoso.example",
lastUpdated: "2026-09-06T00:00:00Z",
updatedBy: "ga@operator.example",
disabled: false,
mcpDisabled: false,
planTier: "free",
trialConsumed: false,
payingCustomer: false,
manufacturerWhitelist: "Dell*,HP*,Lenovo*,Microsoft Corporation",
modelWhitelist: "*",
validateAutopilotDevice: false,
validateCorporateIdentifier: false,
validateDeviceAssociation: false,
validateCloudPcDevice: false,
validateIntuneDeviceBinding: false,
allowInsecureAgentRequests: false,
dataRetentionDays: 90,
sessionTimeoutHours: 5,
sessionGraceHours: 0,
maxNdjsonPayloadSizeMB: 5,
enablePerformanceCollector: true,
performanceCollectorIntervalSeconds: 30,
helloWaitTimeoutSeconds: 30,
ntpServer: "time.windows.com",
logLevel: "",
enrollmentSummaryBrandingImageUrl: "",
localAdminAllowedAccountsJson: "",
bootstrapTokenEnabled: false,
unrestrictedModeEnabled: false,
unrestrictedMode: false,
entraAppRolesEnabled: false,
diagnosticsLogPathsJson: "",
diagnosticsBlobSasUrl: "",
diagnosticsUploadMode: "Off",
diagnosticsUploadDestination: "CustomerSas",
sendTraceEvents: true,
teamsWebhookUrl: "",
teamsNotifyOnSuccess: true,
teamsNotifyOnFailure: true,
teamsNotifyOnStart: false,
webhookProviderType: 0,
webhookUrl: "",
webhookNotifyOnSuccess: true,
webhookNotifyOnFailure: true,
webhookNotifyOnHardwareRejection: false,
webhookNotifyOnStart: false,
webhookCustomHeadersJson: "",
notificationChannelsJson: "",
slaNotifyOnSuccessRateBreach: false,
slaNotifyOnDurationBreach: false,
slaNotifyOnAppInstallBreach: false,
slaNotifyOnConsecutiveFailures: false,
slaConsecutiveFailureThreshold: 5,
...overrides,
};
}
const adminConfig = { collectorIdleTimeoutMinutes: 25, desktopDetectorNoCandidateTimeoutMinutes: 7, allowAgentDowngrade: true, modernDeploymentHarmlessEventIdsJson: "[100, 42]" } as AdminConfiguration;
const flags = (unrestrictedMode: boolean) => ({ unrestrictedMode }) as TenantFeatureFlagsResponse;
const ctx = (overrides: Partial<TenantConfiguration> = {}, extra: Partial<RuntimeContext> = {}): RuntimeContext => ({
config: storedConfig(overrides),
adminConfig: null,
flags: null,
...extra,
});
const runtimeValue = (record: typeof RUNTIME_FIELDS | typeof COLLECTOR_FIELDS | typeof ANALYZER_FIELDS, key: string, c: RuntimeContext) => {
const spec = (record as Record<string, (typeof RUNTIME_FIELDS)[keyof typeof RUNTIME_FIELDS]>)[key];
if (!spec || isExcluded(spec)) throw new Error(`${key} is not a rendered runtime row`);
return spec.value(c);
};
describe("runtime rows mirror GetAgentConfigFunction", () => {
it("upload interval is the backend constant (10), not the old 30", () => {
expect(DEFAULT_UPLOAD_INTERVAL_SECONDS).toBe(10);
expect(runtimeValue(RUNTIME_FIELDS, "uploadIntervalSeconds", ctx())).toBe(10);
});
it("diagnostics upload is enabled by a SAS URL or the hosted destination, regardless of mode", () => {
expect(runtimeValue(RUNTIME_FIELDS, "diagnosticsUploadEnabled", ctx())).toBe(false);
expect(runtimeValue(RUNTIME_FIELDS, "diagnosticsUploadEnabled", ctx({ diagnosticsUploadDestination: "Hosted" }))).toBe(true);
expect(runtimeValue(RUNTIME_FIELDS, "diagnosticsUploadEnabled", ctx({ diagnosticsBlobSasUrl: "https://x.blob.core.windows.net/c?sig=1", diagnosticsUploadMode: "Off" }))).toBe(true);
});
it("unrestricted mode comes from the entitlement flags, never from the stored toggle alone", () => {
expect(runtimeValue(RUNTIME_FIELDS, "unrestrictedMode", ctx({ unrestrictedModeEnabled: true, unrestrictedMode: true }))).toBe(UNRESOLVED);
expect(runtimeValue(RUNTIME_FIELDS, "unrestrictedMode", ctx({ unrestrictedModeEnabled: true, unrestrictedMode: true }, { flags: flags(false) }))).toBe(false);
expect(runtimeValue(RUNTIME_FIELDS, "unrestrictedMode", ctx({}, { flags: flags(true) }))).toBe(true);
});
it("operator knobs resolve from the global config and are unresolved without it", () => {
expect(runtimeValue(COLLECTOR_FIELDS, "collectorIdleTimeoutMinutes", ctx())).toBe(UNRESOLVED);
const c = ctx({}, { adminConfig });
expect(runtimeValue(COLLECTOR_FIELDS, "collectorIdleTimeoutMinutes", c)).toBe(25);
expect(runtimeValue(COLLECTOR_FIELDS, "desktopDetectorNoCandidateTimeoutMinutes", c)).toBe(7);
expect(runtimeValue(COLLECTOR_FIELDS, "modernDeploymentHarmlessEventIds", c)).toEqual([100, 42]);
expect(runtimeValue(RUNTIME_FIELDS, "allowAgentDowngrade", c)).toBe(true);
});
it("applies the analyzer defaults of the agent config builder", () => {
const c = ctx();
expect(runtimeValue(ANALYZER_FIELDS, "enableLocalAdminAnalyzer", c)).toBe(true);
expect(runtimeValue(ANALYZER_FIELDS, "enableIntegrityBypassAnalyzer", c)).toBe(true);
expect(runtimeValue(ANALYZER_FIELDS, "enableConsoleBypassDetection", c)).toBe(true);
expect(runtimeValue(ANALYZER_FIELDS, "enableSoftwareInventoryAnalyzer", c)).toBe(false);
expect(runtimeValue(ANALYZER_FIELDS, "enableRealmJoinWatcher", c)).toBe(false);
expect(runtimeValue(ANALYZER_FIELDS, "keepAwakeDuringUserEsp", c)).toBe(false);
expect(runtimeValue(ANALYZER_FIELDS, "localAdminAllowedAccounts", ctx({ localAdminAllowedAccountsJson: '["Admin","Helpdesk"]' }))).toEqual(["Admin", "Helpdesk"]);
expect(runtimeValue(ANALYZER_FIELDS, "localAdminAllowedAccounts", ctx({ localAdminAllowedAccountsJson: "not json" }))).toEqual([]);
});
it("nullable agent knobs fall back to the agent defaults", () => {
const c = ctx();
expect(runtimeValue(RUNTIME_FIELDS, "maxAuthFailures", c)).toBe(5);
expect(runtimeValue(RUNTIME_FIELDS, "authFailureTimeoutMinutes", c)).toBe(0);
expect(runtimeValue(RUNTIME_FIELDS, "logLevel", ctx({ logLevel: null as unknown as string }))).toBe("Info");
expect(runtimeValue(RUNTIME_FIELDS, "ntpServer", ctx({ ntpServer: "" }))).toBe("time.windows.com");
expect(runtimeValue(COLLECTOR_FIELDS, "agentMaxLifetimeMinutes", c)).toBe(360);
expect(runtimeValue(RUNTIME_FIELDS, "rebootDelaySeconds", ctx({ rebootDelaySeconds: 30 }))).toBe(30);
});
it("renders every runtime row in exactly one section", () => {
const keys = RUNTIME_REPORT_SECTIONS.flatMap((s) => s.rows.map((r) => r.key));
expect(new Set(keys).size).toBe(keys.length);
expect(keys).toContain("uploadIntervalSeconds");
expect(keys).toContain("collectorIdleTimeoutMinutes");
expect(keys).toContain("enableConsoleBypassDetection");
});
});
describe("isNonDefault", () => {
it("treats null and undefined as one value and compares arrays by content", () => {
expect(isNonDefault(undefined, null)).toBe(false);
expect(isNonDefault("", null)).toBe(false);
expect(isNonDefault("", "Off")).toBe(true);
expect(isNonDefault(null, false)).toBe(true);
expect(isNonDefault(false, false)).toBe(false);
expect(isNonDefault([100, 1005, 1010], [100, 1005, 1010])).toBe(false);
expect(isNonDefault([100, 42], [100, 1005, 1010])).toBe(true);
});
});
@@ -0,0 +1,420 @@
/**
* Row catalog for the Tenant Config Report (admin/tenants/config-report).
*
* Both halves of the report are exhaustive over the generated wire types: `TENANT_FIELDS`
* over every key of `TenantConfiguration` (GET config/{tenantId}) and `RUNTIME_FIELDS` /
* `COLLECTOR_FIELDS` / `ANALYZER_FIELDS` over every key of `AgentConfigResponse` and its two
* sub-objects (what GET agent/config serves). A key is either rendered (a row with section
* and label) or excluded with a reason — nothing can be silently forgotten: a new C# property
* forces the wire-type regeneration (SharedManifestParityTests) and `tsc` then fails here
* until the key is catalogued. The vitest next to this module pins the same thing at runtime
* against `SHARED_MANIFEST.tenantConfiguration.fields`.
*
* Runtime rows mirror the resolution rules of GetAgentConfigFunction.cs (backend); every
* rule carries the source it reads from so the operator sees which knob to turn.
*/
import type {
AdminConfiguration,
AgentConfigResponse,
AnalyzerConfiguration,
CollectorConfiguration,
TenantConfiguration,
TenantFeatureFlagsResponse,
} from "@/utils/wire-types.generated";
// ── Row model ────────────────────────────────────────────────────────────────
/** How a value is rendered. `secret` never shows the value, only whether one is set. */
export type RowKind = "value" | "date" | "masked" | "secret";
export interface TenantRow {
section: TenantSection;
label: string;
kind?: RowKind;
/**
* The C# initializer of TenantConfiguration.cs. A row WITH this key (even `null`) is
* compared against it and highlighted as "custom" when the stored value differs; a row
* without it is informational and never highlighted.
*/
default?: unknown;
}
export interface Excluded {
excluded: string;
}
export const isExcluded = <T extends object>(spec: T | Excluded): spec is Excluded =>
"excluded" in spec;
export const TENANT_SECTIONS = [
"Tenant Status",
"Plan & Billing",
"Identity & App Homing",
"Security & Validation",
"Hardware Whitelist",
"Data Management",
"Agent Collectors",
"Auth Circuit Breaker",
"Agent Behavior",
"Enrollment Summary",
"Analyzers",
"Webhooks",
"SLA Targets",
"Diagnostics",
"Feature Flags (Global)",
] as const;
export type TenantSection = (typeof TENANT_SECTIONS)[number];
const NTP_DEFAULT = "time.windows.com";
// ── Tenant configuration (left column) ───────────────────────────────────────
const HEADER = { excluded: "shown in the tenant header card" } as const;
const CHANNEL_BLOCK = { excluded: "rendered by the notification channel block (legacy fallback order)" } as const;
export const TENANT_FIELDS: Record<keyof TenantConfiguration, TenantRow | Excluded> = {
tenantId: HEADER,
domainName: HEADER,
lastUpdated: HEADER,
updatedBy: HEADER,
// Tenant Status
disabled: { section: "Tenant Status", label: "Disabled", default: false },
disabledReason: { section: "Tenant Status", label: "Disabled Reason" },
disabledUntil: { section: "Tenant Status", label: "Disabled Until", kind: "date" },
mcpDisabled: { section: "Tenant Status", label: "MCP Disabled", default: false },
mcpDisabledReason: { section: "Tenant Status", label: "MCP Disabled Reason" },
onboardedAt: { section: "Tenant Status", label: "Onboarded At", kind: "date" },
onboardedBy: { section: "Tenant Status", label: "Onboarded By" },
contactEmail: { section: "Tenant Status", label: "Contact Email" },
companyName: { section: "Tenant Status", label: "Company" },
// Plan & Billing (stored values; the effective edition comes from feature-flags, header card)
planTier: { section: "Plan & Billing", label: "Plan Tier (stored)", default: "free" },
trialExpiresUtc: { section: "Plan & Billing", label: "Trial Expires", kind: "date" },
trialStartedUtc: { section: "Plan & Billing", label: "Trial Started", kind: "date" },
trialGrantedBy: { section: "Plan & Billing", label: "Trial Granted By" },
trialConsumed: { section: "Plan & Billing", label: "Trial Consumed", default: false },
proDowngradedUtc: { section: "Plan & Billing", label: "Pro Downgraded At", kind: "date" },
payingCustomer: { section: "Plan & Billing", label: "Paying Customer", default: false },
maxDelegatedTenantsOverride: { section: "Plan & Billing", label: "Delegated Tenant Slots (override)", default: null },
mcpUsagePlanOverride: { section: "Plan & Billing", label: "MCP Usage Plan (override)", default: null },
managedByProTenantId: { section: "Plan & Billing", label: "Managed By Pro Tenant", default: null },
// Identity & App Homing
homedAppClientId: { section: "Identity & App Homing", label: "Homed App Client ID" },
lastAuthClientId: { section: "Identity & App Homing", label: "Last Auth Client ID" },
lastAuthClientIdSince: { section: "Identity & App Homing", label: "Last Auth Client ID Since", kind: "date" },
entraAppRolesEnabled: { section: "Identity & App Homing", label: "Entra App Roles Enabled", default: false },
// Security & Validation
validateAutopilotDevice: { section: "Security & Validation", label: "Validate Autopilot Device", default: false },
validateCorporateIdentifier: { section: "Security & Validation", label: "Validate Corporate Identifier", default: false },
validateDeviceAssociation: { section: "Security & Validation", label: "Validate Device Association", default: false },
validateCloudPcDevice: { section: "Security & Validation", label: "Validate Cloud PC Device", default: false },
validateIntuneDeviceBinding: { section: "Security & Validation", label: "Validate Intune Device Binding", default: false },
allowInsecureAgentRequests: { section: "Security & Validation", label: "Allow Insecure Agent Requests", default: false },
customRateLimitRequestsPerMinute: { section: "Security & Validation", label: "Device API Rate Limit Override", default: null },
customUserRateLimitRequestsPerMinute: { section: "Security & Validation", label: "MCP & Integrations API Rate Limit Override", default: null },
// Hardware Whitelist
manufacturerWhitelist: { section: "Hardware Whitelist", label: "Manufacturers", default: "Dell*,HP*,Lenovo*,Microsoft Corporation" },
modelWhitelist: { section: "Hardware Whitelist", label: "Models", default: "*" },
webhookNotifyOnHardwareRejection: { section: "Hardware Whitelist", label: "Notify On Hardware Rejection", default: false },
// Data Management
dataRetentionDays: { section: "Data Management", label: "Data Retention (days)", default: 90 },
sessionTimeoutHours: { section: "Data Management", label: "Session Timeout (hours)", default: 5 },
sessionGraceHours: { section: "Data Management", label: "Session Grace (hours)", default: 0 },
absoluteMaxSessionHours: { section: "Data Management", label: "Absolute Max Session (hours)", default: null },
maxNdjsonPayloadSizeMB: { section: "Data Management", label: "Max NDJSON Payload (MB)", default: 5 },
// Agent Collectors
enablePerformanceCollector: { section: "Agent Collectors", label: "Performance Collector", default: true },
performanceCollectorIntervalSeconds: { section: "Agent Collectors", label: "Perf. Interval (sec)", default: 30 },
helloWaitTimeoutSeconds: { section: "Agent Collectors", label: "Hello Wait Timeout (sec)", default: 30 },
// Auth Circuit Breaker
maxAuthFailures: { section: "Auth Circuit Breaker", label: "Max Auth Failures", default: null },
authFailureTimeoutMinutes: { section: "Auth Circuit Breaker", label: "Auth Failure Timeout (min)", default: null },
agentMaxLifetimeMinutes: { section: "Auth Circuit Breaker", label: "Agent Max Lifetime (min)", default: null },
// Agent Behavior
selfDestructOnComplete: { section: "Agent Behavior", label: "Self-Destruct On Complete", default: true },
keepLogFile: { section: "Agent Behavior", label: "Keep Log File", default: false },
rebootOnComplete: { section: "Agent Behavior", label: "Reboot On Complete", default: null },
rebootDelaySeconds: { section: "Agent Behavior", label: "Reboot Delay (sec)", default: null },
enableGeoLocation: { section: "Agent Behavior", label: "Geo-Location", default: null },
enableTimezoneAutoSet: { section: "Agent Behavior", label: "Timezone Auto-Set", default: null },
enableDoGroupIdAutoSet: { section: "Agent Behavior", label: "DO GroupId Auto-Set", default: null },
// TableConfigRepository maps an empty stored value to the default, so the wire never carries null here.
ntpServer: { section: "Agent Behavior", label: "NTP Server", default: NTP_DEFAULT },
keepAwakeDuringUserEsp: { section: "Agent Behavior", label: "Keep Awake During User ESP", default: null },
enableImeMatchLog: { section: "Agent Behavior", label: "IME Match Log", default: null },
enableGatherRuleDebugLog: { section: "Agent Behavior", label: "Gather Rule Debug Log", default: null },
enableEspContinueAnywayObservation: { section: "Agent Behavior", label: "Continue-Anyway Observation", default: null },
logLevel: { section: "Agent Behavior", label: "Log Level", default: null },
maxBatchSize: { section: "Agent Behavior", label: "Max Batch Size", default: null },
showScriptOutput: { section: "Agent Behavior", label: "Show Script Output", default: true },
sendTraceEvents: { section: "Agent Behavior", label: "Send Trace Events", default: true },
// Enrollment Summary
showEnrollmentSummary: { section: "Enrollment Summary", label: "Show Summary", default: null },
enrollmentSummaryTimeoutSeconds: { section: "Enrollment Summary", label: "Timeout (sec)", default: null },
enrollmentSummaryBrandingImageUrl: { section: "Enrollment Summary", label: "Branding Image URL", default: null },
enrollmentSummaryLaunchRetrySeconds: { section: "Enrollment Summary", label: "Launch Retry (sec)", default: null },
// Analyzers
enableLocalAdminAnalyzer: { section: "Analyzers", label: "Local Admin Analyzer", default: null },
localAdminAllowedAccountsJson: { section: "Analyzers", label: "Allowed Local Admin Accounts", default: null },
enableSoftwareInventoryAnalyzer: { section: "Analyzers", label: "Software Inventory Analyzer", default: null },
enableIntegrityBypassAnalyzer: { section: "Analyzers", label: "Integrity Bypass Analyzer", default: null },
enableConsoleBypassDetection: { section: "Analyzers", label: "Console Bypass Detection", default: null },
enableRealmJoinWatcher: { section: "Analyzers", label: "Realm Join Watcher", default: null },
// Webhooks — channel list plus the legacy single-webhook fields it falls back to
notificationChannelsJson: CHANNEL_BLOCK,
webhookProviderType: CHANNEL_BLOCK,
webhookUrl: CHANNEL_BLOCK,
webhookNotifyOnSuccess: CHANNEL_BLOCK,
webhookNotifyOnFailure: CHANNEL_BLOCK,
webhookNotifyOnStart: CHANNEL_BLOCK,
teamsWebhookUrl: CHANNEL_BLOCK,
teamsNotifyOnSuccess: CHANNEL_BLOCK,
teamsNotifyOnFailure: CHANNEL_BLOCK,
teamsNotifyOnStart: CHANNEL_BLOCK,
webhookCustomHeadersJson: { section: "Webhooks", label: "Custom Headers", kind: "secret" },
// SLA Targets
slaTargetSuccessRate: { section: "SLA Targets", label: "Target Success Rate (%)", default: null },
slaTargetMaxDurationMinutes: { section: "SLA Targets", label: "Target Max Duration (min)", default: null },
slaTargetAppInstallSuccessRate: { section: "SLA Targets", label: "Target App Install Success Rate (%)", default: null },
slaNotifyOnSuccessRateBreach: { section: "SLA Targets", label: "Notify On Success Rate Breach", default: false },
slaSuccessRateNotifyThreshold: { section: "SLA Targets", label: "Success Rate Notify Threshold (%)", default: null },
slaNotifyOnDurationBreach: { section: "SLA Targets", label: "Notify On Duration Breach", default: false },
slaNotifyOnAppInstallBreach: { section: "SLA Targets", label: "Notify On App Install Breach", default: false },
slaNotifyOnConsecutiveFailures: { section: "SLA Targets", label: "Notify On Consecutive Failures", default: false },
slaConsecutiveFailureThreshold: { section: "SLA Targets", label: "Consecutive Failure Threshold", default: 5 },
// Diagnostics
diagnosticsUploadMode: { section: "Diagnostics", label: "Upload Mode", default: "Off" },
diagnosticsUploadDestination: { section: "Diagnostics", label: "Upload Destination", default: "CustomerSas" },
diagnosticsBlobSasUrl: { section: "Diagnostics", label: "Blob SAS URL", kind: "masked" },
diagnosticsLogPathsJson: { section: "Diagnostics", label: "Custom Log Paths", default: null },
// Feature Flags (Global)
bootstrapTokenEnabled: { section: "Feature Flags (Global)", label: "Bootstrap Token Enabled", default: false },
unrestrictedModeEnabled: { section: "Feature Flags (Global)", label: "Unrestricted Mode Enabled (GA gate)", default: false },
unrestrictedMode: { section: "Feature Flags (Global)", label: "Unrestricted Mode (tenant toggle)", default: false },
};
// ── Runtime parameters (right column) ────────────────────────────────────────
export interface RuntimeContext {
config: TenantConfiguration;
/** GET global/config — null while loading or when the call failed. */
adminConfig: AdminConfiguration | null;
/** GET config/{tenantId}/feature-flags — backend-resolved entitlement surface. */
flags: TenantFeatureFlagsResponse | null;
}
/** Which knob a runtime value is resolved from. */
export type RuntimeSource = "tenant" | "admin" | "flags" | "constant";
export const RUNTIME_SECTIONS = [
"Upload & Batching",
"Agent Behavior",
"Auth Circuit Breaker",
"Enrollment Summary",
"Collectors",
"Diagnostics",
"Analyzers",
] as const;
export type RuntimeSection = (typeof RUNTIME_SECTIONS)[number];
/** Sentinel for a value that cannot be resolved because its source did not load. */
export const UNRESOLVED = Symbol("unresolved");
export interface RuntimeRow {
section: RuntimeSection;
label: string;
source: RuntimeSource;
/** Backend resolution rule (GetAgentConfigFunction.cs). */
value: (ctx: RuntimeContext) => unknown;
/** The agent-side default (AgentConfigResponse.cs / AdminConfiguration.cs initializer) for the "custom" highlight. */
default?: unknown;
}
/** Shared.Constants.DefaultUploadIntervalSeconds — the one runtime constant not on any config row. */
export const DEFAULT_UPLOAD_INTERVAL_SECONDS = 10;
/** GetAgentConfigFunction.ResolveDiagnosticsUploadEnabled: a SAS URL or the hosted destination, regardless of mode. */
export const resolveDiagnosticsUploadEnabled = (c: TenantConfiguration): boolean =>
!!c.diagnosticsBlobSasUrl || (c.diagnosticsUploadDestination ?? "").toLowerCase() === "hosted";
/** TenantConfiguration.GetLocalAdminAllowedAccounts: JSON string array, anything else → empty. */
export const parseLocalAdminAllowedAccounts = (json: string | null | undefined): string[] => {
if (!json) return [];
try {
const parsed: unknown = JSON.parse(json);
return Array.isArray(parsed) ? parsed.filter((x): x is string => typeof x === "string") : [];
} catch {
return [];
}
};
/** AdminConfiguration.GetModernDeploymentHarmlessEventIds: JSON int array, blank/invalid → defaults. */
export const parseModernDeploymentHarmlessEventIds = (json: string | null | undefined): number[] => {
const defaults = [100, 1005, 1010];
if (!json || !json.trim()) return defaults;
try {
const parsed: unknown = JSON.parse(json);
return Array.isArray(parsed) && parsed.every((x) => typeof x === "number") ? parsed : defaults;
} catch {
return defaults;
}
};
const admin = <T>(pick: (a: AdminConfiguration) => T) =>
(ctx: RuntimeContext): T | typeof UNRESOLVED => (ctx.adminConfig ? pick(ctx.adminConfig) : UNRESOLVED);
const NOT_CONFIG = { excluded: "per-request payload, not a configuration value" } as const;
const SUB_OBJECT = { excluded: "sub-object, catalogued separately" } as const;
const CLASS_DEFAULT = { excluded: "agent class default, neither tenant- nor operator-tunable" } as const;
export const RUNTIME_FIELDS: Record<keyof AgentConfigResponse, RuntimeRow | Excluded> = {
configVersion: { excluded: "compiled into the backend" },
uploadIntervalSeconds: { section: "Upload & Batching", label: "Upload Interval (sec)", source: "constant", value: () => DEFAULT_UPLOAD_INTERVAL_SECONDS, default: DEFAULT_UPLOAD_INTERVAL_SECONDS },
maxBatchSize: { section: "Upload & Batching", label: "Max Batch Size", source: "tenant", value: ({ config }) => config.maxBatchSize ?? 100, default: 100 },
selfDestructOnComplete: { section: "Agent Behavior", label: "Self-Destruct", source: "tenant", value: ({ config }) => config.selfDestructOnComplete ?? true, default: true },
keepLogFile: { section: "Agent Behavior", label: "Keep Log File", source: "tenant", value: ({ config }) => config.keepLogFile ?? false, default: false },
rebootOnComplete: { section: "Agent Behavior", label: "Reboot On Complete", source: "tenant", value: ({ config }) => config.rebootOnComplete ?? false, default: false },
rebootDelaySeconds: { section: "Agent Behavior", label: "Reboot Delay (sec)", source: "tenant", value: ({ config }) => config.rebootDelaySeconds ?? 10, default: 10 },
enableGeoLocation: { section: "Agent Behavior", label: "Geo-Location", source: "tenant", value: ({ config }) => config.enableGeoLocation ?? true, default: true },
enableTimezoneAutoSet: { section: "Agent Behavior", label: "Timezone Auto-Set", source: "tenant", value: ({ config }) => config.enableTimezoneAutoSet ?? false, default: false },
enableDoGroupIdAutoSet: { section: "Agent Behavior", label: "DO GroupId Auto-Set", source: "tenant", value: ({ config }) => config.enableDoGroupIdAutoSet ?? false, default: false },
ntpServer: { section: "Agent Behavior", label: "NTP Server", source: "tenant", value: ({ config }) => config.ntpServer || NTP_DEFAULT, default: NTP_DEFAULT },
enableImeMatchLog: { section: "Agent Behavior", label: "IME Match Log", source: "tenant", value: ({ config }) => config.enableImeMatchLog ?? false, default: false },
enableGatherRuleDebugLog: { section: "Agent Behavior", label: "Gather Rule Debug Log", source: "tenant", value: ({ config }) => config.enableGatherRuleDebugLog ?? false, default: false },
enableEspContinueAnywayObservation: { section: "Agent Behavior", label: "Continue-Anyway Observation", source: "tenant", value: ({ config }) => config.enableEspContinueAnywayObservation ?? false, default: false },
logLevel: { section: "Agent Behavior", label: "Log Level", source: "tenant", value: ({ config }) => config.logLevel ?? "Info", default: "Info" },
sendTraceEvents: { section: "Agent Behavior", label: "Send Trace Events", source: "tenant", value: ({ config }) => config.sendTraceEvents, default: true },
// TenantEntitlementService.IsUnrestrictedModeActive = edition entitlement && GA gate && tenant toggle;
// feature-flags carries the backend-resolved value, so the entitlement logic is not rebuilt here.
unrestrictedMode: { section: "Agent Behavior", label: "Unrestricted Mode (effective)", source: "flags", value: ({ flags }) => (flags ? flags.unrestrictedMode : UNRESOLVED), default: false },
allowAgentDowngrade: { section: "Agent Behavior", label: "Allow Agent Downgrade", source: "admin", value: admin((a) => a.allowAgentDowngrade), default: false },
maxAuthFailures: { section: "Auth Circuit Breaker", label: "Max Auth Failures", source: "tenant", value: ({ config }) => config.maxAuthFailures ?? 5, default: 5 },
authFailureTimeoutMinutes: { section: "Auth Circuit Breaker", label: "Auth Failure Timeout (min)", source: "tenant", value: ({ config }) => config.authFailureTimeoutMinutes ?? 0, default: 0 },
showEnrollmentSummary: { section: "Enrollment Summary", label: "Show Summary", source: "tenant", value: ({ config }) => config.showEnrollmentSummary ?? false, default: false },
enrollmentSummaryTimeoutSeconds: { section: "Enrollment Summary", label: "Timeout (sec)", source: "tenant", value: ({ config }) => config.enrollmentSummaryTimeoutSeconds ?? 60, default: 60 },
enrollmentSummaryBrandingImageUrl: { section: "Enrollment Summary", label: "Branding Image URL", source: "tenant", value: ({ config }) => config.enrollmentSummaryBrandingImageUrl ?? null },
enrollmentSummaryLaunchRetrySeconds: { section: "Enrollment Summary", label: "Launch Retry (sec)", source: "tenant", value: ({ config }) => config.enrollmentSummaryLaunchRetrySeconds ?? 120, default: 120 },
diagnosticsUploadEnabled: { section: "Diagnostics", label: "Upload Enabled", source: "tenant", value: ({ config }) => resolveDiagnosticsUploadEnabled(config), default: false },
diagnosticsUploadMode: { section: "Diagnostics", label: "Upload Mode", source: "tenant", value: ({ config }) => config.diagnosticsUploadMode ?? "Off", default: "Off" },
diagnosticsLogPaths: { excluded: "merged global + tenant path list (see Custom Log Paths and the global diagnostics settings)" },
collectors: SUB_OBJECT,
analyzers: SUB_OBJECT,
gatherRules: { excluded: "active gather-rule catalog for the tenant" },
imeLogPatterns: { excluded: "active IME log-pattern catalog for the tenant" },
whiteGloveSealingPatternIds: { excluded: "global pattern-id list (AdminConfiguration)" },
latestAgentSha256: { excluded: "per-major agent hash oracle selected from the caller's X-Agent-Version (AdminConfiguration)" },
latestAgentExeSha256: { excluded: "per-major agent hash oracle selected from the caller's X-Agent-Version (AdminConfiguration)" },
deviceBlocked: NOT_CONFIG,
deviceKillSignal: NOT_CONFIG,
unblockAt: NOT_CONFIG,
migrateToApiBaseUrl: { excluded: "endpoint-migration target, global or per-tenant override (AdminConfiguration)" },
};
export const COLLECTOR_FIELDS: Record<keyof CollectorConfiguration, RuntimeRow | Excluded> = {
enablePerformanceCollector: { section: "Collectors", label: "Performance Collector", source: "tenant", value: ({ config }) => config.enablePerformanceCollector, default: true },
performanceIntervalSeconds: { section: "Collectors", label: "Perf. Interval (sec)", source: "tenant", value: ({ config }) => config.performanceCollectorIntervalSeconds, default: 30 },
collectorIdleTimeoutMinutes: { section: "Collectors", label: "Idle Timeout (min)", source: "admin", value: admin((a) => a.collectorIdleTimeoutMinutes), default: 15 },
desktopDetectorNoCandidateTimeoutMinutes: { section: "Collectors", label: "Desktop Detector No-Candidate Timeout (min)", source: "admin", value: admin((a) => a.desktopDetectorNoCandidateTimeoutMinutes), default: 10 },
enableAgentSelfMetrics: { section: "Collectors", label: "Agent Self-Metrics", source: "constant", value: () => true, default: true },
agentSelfMetricsIntervalSeconds: { section: "Collectors", label: "Self-Metrics Interval (sec)", source: "constant", value: () => 60, default: 60 },
helloWaitTimeoutSeconds: { section: "Collectors", label: "Hello Wait Timeout (sec)", source: "tenant", value: ({ config }) => config.helloWaitTimeoutSeconds, default: 30 },
agentMaxLifetimeMinutes: { section: "Collectors", label: "Agent Max Lifetime (min)", source: "tenant", value: ({ config }) => config.agentMaxLifetimeMinutes ?? 360, default: 360 },
modernDeploymentHarmlessEventIds: { section: "Collectors", label: "Modern Deployment Harmless Event IDs", source: "admin", value: admin((a) => parseModernDeploymentHarmlessEventIds(a.modernDeploymentHarmlessEventIdsJson)), default: [100, 1005, 1010] },
enableDeliveryOptimizationCollector: CLASS_DEFAULT,
deliveryOptimizationIntervalSeconds: CLASS_DEFAULT,
enableOfficeInstallDetector: CLASS_DEFAULT,
officeInstallSettleSeconds: CLASS_DEFAULT,
enablePowerStateWatcher: CLASS_DEFAULT,
stallProbeEnabled: CLASS_DEFAULT,
stallProbeThresholdsMinutes: CLASS_DEFAULT,
stallProbeTraceIndices: CLASS_DEFAULT,
stallProbeSources: CLASS_DEFAULT,
sessionStalledAfterProbeIndex: CLASS_DEFAULT,
modernDeploymentWatcherEnabled: CLASS_DEFAULT,
modernDeploymentLogLevelMax: CLASS_DEFAULT,
modernDeploymentBackfillEnabled: CLASS_DEFAULT,
modernDeploymentBackfillLookbackMinutes: CLASS_DEFAULT,
windowsUpdateWatcherEnabled: CLASS_DEFAULT,
windowsUpdateTargetedEventIds: CLASS_DEFAULT,
windowsUpdateBackfillLookbackMinutes: CLASS_DEFAULT,
windowsUpdateChannelCensusEnabled: CLASS_DEFAULT,
mdmRebootPolicyWatcherEnabled: CLASS_DEFAULT,
mdmRebootPolicyBackfillLookbackMinutes: CLASS_DEFAULT,
enableSystemTimelineWatcher: CLASS_DEFAULT,
systemEventBackfillLookbackMinutes: CLASS_DEFAULT,
};
export const ANALYZER_FIELDS: Record<keyof AnalyzerConfiguration, RuntimeRow | Excluded> = {
enableLocalAdminAnalyzer: { section: "Analyzers", label: "Local Admin Analyzer", source: "tenant", value: ({ config }) => config.enableLocalAdminAnalyzer ?? true, default: true },
localAdminAllowedAccounts: { section: "Analyzers", label: "Allowed Local Admin Accounts", source: "tenant", value: ({ config }) => parseLocalAdminAllowedAccounts(config.localAdminAllowedAccountsJson), default: [] },
enableSoftwareInventoryAnalyzer: { section: "Analyzers", label: "Software Inventory Analyzer", source: "tenant", value: ({ config }) => config.enableSoftwareInventoryAnalyzer ?? false, default: false },
enableIntegrityBypassAnalyzer: { section: "Analyzers", label: "Integrity Bypass Analyzer", source: "tenant", value: ({ config }) => config.enableIntegrityBypassAnalyzer ?? true, default: true },
enableRealmJoinWatcher: { section: "Analyzers", label: "Realm Join Watcher", source: "tenant", value: ({ config }) => config.enableRealmJoinWatcher ?? false, default: false },
keepAwakeDuringUserEsp: { section: "Analyzers", label: "Keep Awake During User ESP", source: "tenant", value: ({ config }) => config.keepAwakeDuringUserEsp ?? false, default: false },
enableConsoleBypassDetection: { section: "Analyzers", label: "Console Bypass Detection", source: "tenant", value: ({ config }) => config.enableConsoleBypassDetection ?? true, default: true },
};
// ── Ordered views for rendering ──────────────────────────────────────────────
export interface CataloguedRow<TRow> {
key: string;
row: TRow;
}
function groupBySection<TSection extends string, TRow extends { section: TSection }>(
sections: readonly TSection[],
records: Array<Record<string, TRow | Excluded>>,
): Array<{ section: TSection; rows: CataloguedRow<TRow>[] }> {
const bySection = new Map<TSection, CataloguedRow<TRow>[]>(sections.map((s) => [s, []]));
for (const record of records) {
for (const [key, spec] of Object.entries(record)) {
if (isExcluded(spec)) continue;
bySection.get(spec.section)!.push({ key, row: spec });
}
}
return sections.map((section) => ({ section, rows: bySection.get(section)! }));
}
export const TENANT_REPORT_SECTIONS = groupBySection<TenantSection, TenantRow>(TENANT_SECTIONS, [TENANT_FIELDS]);
export const RUNTIME_REPORT_SECTIONS = groupBySection<RuntimeSection, RuntimeRow>(RUNTIME_SECTIONS, [
RUNTIME_FIELDS,
COLLECTOR_FIELDS,
ANALYZER_FIELDS,
]);
/**
* Value equality for the "custom" highlight: null, undefined and the empty string are one
* "unset" value (the table row stores a cleared string as "", the C# initializer is null),
* arrays compare by content.
*/
export function isNonDefault(value: unknown, def: unknown): boolean {
const isNil = (v: unknown) => v === null || v === undefined || v === "";
const valueNil = isNil(value);
const defNil = isNil(def);
if (valueNil || defNil) return valueNil !== defNil;
if (Array.isArray(value) && Array.isArray(def)) {
return value.length !== def.length || value.some((v, i) => v !== def[i]);
}
return value !== def;
}