fix(daemon): isolate audit evidence observers

This commit is contained in:
Brennan Benson
2026-07-30 15:45:55 -07:00
parent d5856ff181
commit ef5b075343
11 changed files with 326 additions and 177 deletions
+38 -24
View File
@@ -6,13 +6,12 @@ import {
type DaemonProcessEvidence,
type ExactDaemonIncarnation
} from './daemon-incarnation-evidence'
import type {
DaemonAuditFailureTrigger,
DaemonAuditGoneReason
} from '../../shared/daemon-audit-eligibility'
export type DaemonAuditTrigger =
| 'endpoint_identity_changed'
| 'inventory_answered'
| 'inventory_failed'
| 'token_missing_after_authenticated_disconnect'
| 'transport_closed'
export type { DaemonAuditTrigger } from '../../shared/daemon-audit-eligibility'
export type DaemonAuditContext = {
protocolGeneration: number
@@ -23,10 +22,6 @@ export type DaemonAuditContext = {
profileScope: string
}
type DaemonAuditGoneReason =
| Extract<DaemonProcessEvidence, { state: 'gone' }>['reason']
| 'windows_named_pipe_missing'
export type DaemonAuditObservation =
| {
state: 'present'
@@ -45,7 +40,7 @@ export type DaemonAuditObservation =
| {
state: 'gone'
reason: DaemonAuditGoneReason
trigger: Exclude<DaemonAuditTrigger, 'inventory_answered'>
trigger: DaemonAuditFailureTrigger
evidenceSources: DaemonEvidenceSources
context: DaemonAuditContext
exactIncarnation: ExactDaemonIncarnation
@@ -58,8 +53,8 @@ export type DaemonAuditObservation =
}
| {
state: 'unknown'
reason: Exclude<DaemonAuditTrigger, 'inventory_answered'>
trigger: Exclude<DaemonAuditTrigger, 'inventory_answered'>
reason: DaemonAuditFailureTrigger
trigger: DaemonAuditFailureTrigger
evidenceSources: DaemonEvidenceSources
context: DaemonAuditContext
exactIncarnation: ExactDaemonIncarnation | null
@@ -75,6 +70,17 @@ export type DaemonAuditObservation =
export type DaemonEndpointState = 'missing' | 'named-pipe' | 'non-socket' | 'socket' | 'unknown'
export type DaemonAuditClassifierDependencies = {
probeProcessIdentity?: typeof probeDaemonProcessIdentity
inspectEndpointState?: (context: DaemonAuditContext) => Promise<DaemonEndpointState>
}
export type DaemonAuditClassificationOptions = {
additionalEvidenceSources?: readonly DaemonEvidenceSource[]
endpointGoneProof?: 'windows_named_pipe_missing'
dependencies?: DaemonAuditClassifierDependencies
}
export function recordAuthenticatedInventory(
context: DaemonAuditContext,
exactIncarnation: ExactDaemonIncarnation | null
@@ -97,28 +103,34 @@ export function recordAuthenticatedInventory(
export async function classifyDaemonAuditFailure(
context: DaemonAuditContext,
trigger: Exclude<DaemonAuditTrigger, 'inventory_answered'>,
trigger: DaemonAuditFailureTrigger,
exactIncarnation: ExactDaemonIncarnation | null,
additionalEvidenceSources: readonly DaemonEvidenceSource[] = [],
endpointGoneProof?: 'windows_named_pipe_missing'
options: DaemonAuditClassificationOptions = {}
): Promise<DaemonAuditObservation> {
const probeProcessIdentity =
options.dependencies?.probeProcessIdentity ?? probeDaemonProcessIdentity
const inspectEndpoint = options.dependencies?.inspectEndpointState ?? inspectDaemonEndpointState
const [processEvidence, endpointState] = await Promise.all([
probeDaemonProcessIdentity(exactIncarnation, {
probeProcessIdentity(exactIncarnation, {
socketPath: context.endpoint,
tokenPath: context.tokenPath
}),
inspectEndpointState(context)
inspectEndpoint(context)
])
const reachability = reachabilityForTrigger(trigger)
const evidenceSources = combineEvidenceSources(
processEvidence.evidenceSources,
context.endpointKind === 'unix-socket' ? ['endpoint_stat'] : [],
additionalEvidenceSources
options.additionalEvidenceSources ?? []
)
if (endpointGoneProof && exactIncarnation) {
if (
options.endpointGoneProof &&
context.endpointKind === 'windows-named-pipe' &&
exactIncarnation
) {
return {
state: 'gone',
reason: endpointGoneProof,
reason: options.endpointGoneProof,
trigger,
evidenceSources,
context,
@@ -126,7 +138,7 @@ export async function classifyDaemonAuditFailure(
reachability,
inventoryAuthority: 'unavailable',
processLiveness: 'gone',
processReason: endpointGoneProof,
processReason: options.endpointGoneProof,
endpointState: 'missing',
observedAtMs: Date.now()
}
@@ -163,7 +175,9 @@ export async function classifyDaemonAuditFailure(
}
}
async function inspectEndpointState(context: DaemonAuditContext): Promise<DaemonEndpointState> {
async function inspectDaemonEndpointState(
context: DaemonAuditContext
): Promise<DaemonEndpointState> {
if (context.endpointKind === 'windows-named-pipe') {
return 'named-pipe'
}
@@ -176,7 +190,7 @@ async function inspectEndpointState(context: DaemonAuditContext): Promise<Daemon
}
function reachabilityForTrigger(
trigger: Exclude<DaemonAuditTrigger, 'inventory_answered'>
trigger: DaemonAuditFailureTrigger
): 'authenticated' | 'disconnected' | 'unknown' {
if (trigger === 'endpoint_identity_changed') {
return 'authenticated'
@@ -1,32 +1,30 @@
import type { DaemonEndpointIdentity } from './daemon-hello-protocol'
import type {
DaemonAuditGoneReason,
DaemonEvidenceSource,
DaemonProcessGoneReason,
DaemonProcessPresentReason,
DaemonProcessUnknownReason
} from '../../shared/daemon-audit-eligibility'
export const WINDOWS_CREATION_TIME_TOLERANCE_MS = 10_000
// Oracle contract validated 2026-07-29; omitted proofs remain unknown.
export const DAEMON_GONE_PROOFS = {
linux: [
'pid_missing',
'boot_identity_changed',
'raw_start_ticks_mismatch',
'matching_identity_zombie'
],
linux: ['pid_missing', 'linux_boot_changed', 'linux_start_ticks_mismatch', 'linux_zombie'],
darwin: ['pid_missing'],
win32: ['cim_process_missing', 'cim_creation_time_mismatch', 'named_pipe_missing']
} as const
win32: ['windows_process_missing', 'windows_creation_time_mismatch', 'windows_named_pipe_missing']
} as const satisfies Readonly<
Record<'linux' | 'darwin' | 'win32', readonly DaemonAuditGoneReason[]>
>
export type DaemonEvidenceSource =
| 'authenticated_inventory'
| 'boot_identity'
| 'endpoint_identity'
| 'endpoint_stat'
| 'linux_proc_stat'
| 'pid_record'
| 'process_command_line'
| 'process_signal'
| 'process_start_time'
| 'token_file'
| 'windows_cim'
| 'windows_named_pipe'
export type {
DaemonAuditGoneReason,
DaemonEvidenceSource,
DaemonProcessGoneReason,
DaemonProcessPresentReason,
DaemonProcessUnknownReason
} from '../../shared/daemon-audit-eligibility'
export type DaemonEvidenceSources = readonly [DaemonEvidenceSource, ...DaemonEvidenceSource[]]
@@ -39,34 +37,18 @@ export type ExactDaemonIncarnation = {
export type DaemonProcessEvidence =
| {
state: 'present'
reason: 'linux_identity_match' | 'macos_identity_match' | 'windows_identity_match'
reason: DaemonProcessPresentReason
evidenceSources: DaemonEvidenceSources
}
| {
state: 'gone'
reason:
| 'linux_boot_changed'
| 'linux_start_ticks_mismatch'
| 'linux_zombie'
| 'pid_missing'
| 'windows_creation_time_mismatch'
| 'windows_process_missing'
reason: DaemonProcessGoneReason
evidenceSources: DaemonEvidenceSources
exactIncarnation: ExactDaemonIncarnation
}
| {
state: 'unknown'
reason:
| 'command_line_mismatch'
| 'command_line_unavailable'
| 'exact_identity_unavailable'
| 'inspection_failed'
| 'linux_identity_incomplete'
| 'macos_start_time_mismatch'
| 'permission_denied'
| 'process_start_time_unavailable'
| 'windows_command_line_unavailable'
| 'windows_process_start_time_unavailable'
reason: DaemonProcessUnknownReason
evidenceSources: DaemonEvidenceSources
}
@@ -9,6 +9,7 @@ import {
import {
classifyDaemonAuditFailure,
recordAuthenticatedInventory,
type DaemonAuditClassifierDependencies,
type DaemonAuditContext
} from './daemon-audit-classifier'
@@ -18,6 +19,15 @@ const exactIncarnation: ExactDaemonIncarnation = {
linuxStartTicks: '4242',
bootId: 'boot-a'
}
const auditClassifierDependencies = {
probeProcessIdentity: async () => ({
state: 'unknown',
reason: 'inspection_failed',
evidenceSources: ['process_signal']
}),
inspectEndpointState: async (context) =>
context.endpointKind === 'windows-named-pipe' ? 'named-pipe' : 'missing'
} satisfies DaemonAuditClassifierDependencies
function linuxStat(state: string, startTicks: string): string {
return `42 (orca daemon with spaces) ${[state, ...Array(18).fill('0'), startTicks].join(' ')}`
@@ -40,14 +50,13 @@ function linuxDependencies(
describe('daemon process identity evidence', () => {
it('pins the dated conclusive-gone oracle contract', () => {
expect(DAEMON_GONE_PROOFS).toEqual({
linux: [
'pid_missing',
'boot_identity_changed',
'raw_start_ticks_mismatch',
'matching_identity_zombie'
],
linux: ['pid_missing', 'linux_boot_changed', 'linux_start_ticks_mismatch', 'linux_zombie'],
darwin: ['pid_missing'],
win32: ['cim_process_missing', 'cim_creation_time_mismatch', 'named_pipe_missing']
win32: [
'windows_process_missing',
'windows_creation_time_mismatch',
'windows_named_pipe_missing'
]
})
})
@@ -133,7 +142,7 @@ describe('daemon process identity evidence', () => {
).resolves.toMatchObject({ state: 'unknown', reason: 'exact_identity_unavailable' })
})
it('uses Windows CreationDate mismatch as gone while null fields stay unknown', async () => {
it('uses Windows CreationDate as the primary identity regardless of command line', async () => {
const base = {
platform: 'win32' as const,
signalProcess: () => 'occupied' as const
@@ -162,8 +171,8 @@ describe('daemon process identity evidence', () => {
})
})
).resolves.toMatchObject({
state: 'unknown',
reason: 'windows_command_line_unavailable'
state: 'present',
reason: 'windows_identity_match'
})
await expect(
probeDaemonProcessIdentity(exactIncarnation, endpoint, {
@@ -175,8 +184,8 @@ describe('daemon process identity evidence', () => {
})
})
).resolves.toMatchObject({
state: 'unknown',
reason: 'command_line_mismatch'
state: 'present',
reason: 'windows_identity_match'
})
await expect(
probeDaemonProcessIdentity(exactIncarnation, endpoint, {
@@ -214,8 +223,8 @@ describe('daemon process identity evidence', () => {
})
})
).resolves.toMatchObject({
state: 'unknown',
reason: 'windows_command_line_unavailable'
state: 'present',
reason: 'windows_identity_match'
})
})
@@ -261,7 +270,9 @@ describe('daemon audit availability evidence', () => {
})
it('represents failed legacy inventory as unknown, never an empty process list', async () => {
const observation = await classifyDaemonAuditFailure(context, 'inventory_failed', null)
const observation = await classifyDaemonAuditFailure(context, 'inventory_failed', null, {
dependencies: auditClassifierDependencies
})
expect(observation).toMatchObject({
state: 'unknown',
@@ -278,7 +289,10 @@ describe('daemon audit availability evidence', () => {
context,
'token_missing_after_authenticated_disconnect',
null,
['token_file']
{
additionalEvidenceSources: ['token_file'],
dependencies: auditClassifierDependencies
}
)
expect(observation.state).toBe('unknown')
@@ -295,8 +309,11 @@ describe('daemon audit availability evidence', () => {
windowsContext,
'inventory_failed',
exactIncarnation,
['windows_named_pipe'],
'windows_named_pipe_missing'
{
additionalEvidenceSources: ['windows_named_pipe'],
endpointGoneProof: 'windows_named_pipe_missing',
dependencies: auditClassifierDependencies
}
)
expect(observation).toMatchObject({
@@ -306,4 +323,23 @@ describe('daemon audit availability evidence', () => {
exactIncarnation
})
})
it('rejects named-pipe disappearance as a gone proof for Unix endpoints', async () => {
const observation = await classifyDaemonAuditFailure(
context,
'inventory_failed',
exactIncarnation,
{
additionalEvidenceSources: ['windows_named_pipe'],
endpointGoneProof: 'windows_named_pipe_missing',
dependencies: auditClassifierDependencies
}
)
expect(observation).toMatchObject({
state: 'unknown',
reason: 'inventory_failed',
processLiveness: 'unknown'
})
})
})
@@ -222,13 +222,13 @@ async function probeWindowsProcess(
exactIncarnation
)
}
if (!identity.commandLine) {
return unknown('windows_command_line_unavailable', ['windows_cim'])
}
if (!commandLineMatchesDaemon(identity.commandLine, endpoint.socketPath, endpoint.tokenPath)) {
return unknown('command_line_mismatch', ['windows_cim'])
}
return present('windows_identity_match', ['windows_cim', 'endpoint_identity'])
return present(
'windows_identity_match',
identity.commandLine &&
commandLineMatchesDaemon(identity.commandLine, endpoint.socketPath, endpoint.tokenPath)
? ['windows_cim', 'process_command_line', 'endpoint_identity']
: ['windows_cim', 'endpoint_identity']
)
}
export function parseLinuxProcessState(statLine: string): string | null {
@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from 'vitest'
import { queryWindowsProcess, readProcessCommandLine } from './daemon-process-inspection'
describe('daemon process inspection', () => {
it('falls back to ps when Linux procfs returns an empty command line', async () => {
const readTextFile = vi.fn(async () => '')
const runCommand = vi.fn(async () => 'node daemon-entry --socket daemon.sock')
await expect(readProcessCommandLine(42, 'linux', { readTextFile, runCommand })).resolves.toBe(
'node daemon-entry --socket daemon.sock'
)
expect(readTextFile).toHaveBeenCalledWith('/proc/42/cmdline')
expect(runCommand).toHaveBeenCalledWith('ps', ['-p', '42', '-o', 'command='], 2_000)
})
it('uses a non-empty Linux procfs command line without spawning ps', async () => {
const readTextFile = vi.fn(async () => 'node\0daemon-entry')
const runCommand = vi.fn()
await expect(readProcessCommandLine(42, 'linux', { readTextFile, runCommand })).resolves.toBe(
'node\0daemon-entry'
)
expect(runCommand).not.toHaveBeenCalled()
})
it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, Number.NaN])(
'rejects unsafe Windows pid %s before command interpolation',
async (pid) => {
const runCommand = vi.fn()
await expect(queryWindowsProcess(pid, { runCommand })).resolves.toEqual({
status: 'unavailable'
})
expect(runCommand).not.toHaveBeenCalled()
}
)
})
+34 -9
View File
@@ -9,6 +9,11 @@ import type {
const execFileAsync = promisify(execFile)
export type DaemonProcessInspectionDependencies = {
readTextFile?: (path: string) => Promise<string>
runCommand?: (file: string, args: string[], timeoutMs: number) => Promise<string>
}
export function inspectProcessSignal(pid: number): ProcessSignalEvidence {
try {
process.kill(pid, 0)
@@ -34,29 +39,40 @@ export async function readLinuxStat(pid: number): Promise<LinuxStatEvidence> {
export async function readProcessCommandLine(
pid: number,
platform: NodeJS.Platform
platform: NodeJS.Platform,
dependencies: DaemonProcessInspectionDependencies = {}
): Promise<string | undefined> {
const readTextFile =
dependencies.readTextFile ?? (async (path: string) => await readFile(path, 'utf8'))
const runCommand = dependencies.runCommand ?? runInspectionCommand
if (platform === 'linux') {
try {
return await readFile(`/proc/${pid}/cmdline`, 'utf8')
const procCommandLine = await readTextFile(`/proc/${pid}/cmdline`)
if (procCommandLine.length > 0) {
return procCommandLine
}
} catch {
// Fall through to ps for procfs privilege or mount restrictions.
}
}
try {
const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'command='], {
encoding: 'utf8',
timeout: 2_000
})
const stdout = await runCommand('ps', ['-p', String(pid), '-o', 'command='], 2_000)
return stdout.trim() || undefined
} catch {
return undefined
}
}
export async function queryWindowsProcess(pid: number): Promise<WindowsProcessEvidence> {
export async function queryWindowsProcess(
pid: number,
dependencies: DaemonProcessInspectionDependencies = {}
): Promise<WindowsProcessEvidence> {
if (!Number.isSafeInteger(pid) || pid <= 0) {
return { status: 'unavailable' }
}
const runCommand = dependencies.runCommand ?? runInspectionCommand
try {
const { stdout } = await execFileAsync(
const stdout = await runCommand(
'powershell.exe',
[
'-NoProfile',
@@ -68,7 +84,7 @@ export async function queryWindowsProcess(pid: number): Promise<WindowsProcessEv
`$start = [long]([DateTimeOffset]$p.CreationDate).ToUnixTimeMilliseconds() }; ` +
`@{ exists = $true; cmd = $p.CommandLine; start = $start } | ConvertTo-Json -Compress }`
],
{ encoding: 'utf8', timeout: 3_000 }
3_000
)
const parsed = JSON.parse(stdout.trim()) as {
exists?: unknown
@@ -92,6 +108,15 @@ export async function queryWindowsProcess(pid: number): Promise<WindowsProcessEv
}
}
async function runInspectionCommand(
file: string,
args: string[],
timeoutMs: number
): Promise<string> {
const { stdout } = await execFileAsync(file, args, { encoding: 'utf8', timeout: timeoutMs })
return stdout
}
function hasErrorCode(error: unknown, code: string): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === code
}
+26 -1
View File
@@ -1484,6 +1484,9 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => {
previous: NonNullable<typeof firstIdentity>
current: NonNullable<typeof firstIdentity>
}[] = []
adapter.onDaemonIdentityChanged(() => {
throw new Error('audit listener failed')
})
adapter.onDaemonIdentityChanged((event) => identityChanges.push(event))
await server.shutdown()
@@ -1505,7 +1508,7 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => {
})
await server.start()
await adapter.listProcesses()
await expect(adapter.listProcesses()).resolves.toEqual([])
expect(identityChanges).toEqual([
{
@@ -1520,6 +1523,28 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => {
expect(adapter.getLastAuthenticatedDaemonIdentity()).toEqual(identityChanges[0]?.current)
})
it('isolates audit observation listeners from inventory and later listeners', async () => {
const laterListener = vi.fn()
adapter.onAuditEligibilityObservation(() => {
throw new Error('audit listener failed')
})
adapter.onAuditEligibilityObservation(laterListener)
await expect(adapter.listProcesses()).resolves.toEqual([])
expect(laterListener).toHaveBeenCalledOnce()
expect(laterListener).toHaveBeenCalledWith(
expect.objectContaining({
state: 'present',
reason: 'authenticated_inventory'
})
)
expect(adapter.getLastAuditObservation()).toMatchObject({
state: 'present',
reason: 'authenticated_inventory'
})
})
it('audits token ENOENT only after an authenticated disconnect', async () => {
const observations: {
trigger: string
+14 -13
View File
@@ -1651,10 +1651,7 @@ export class DaemonPtyAdapter implements IPtyProvider {
return
}
const event = { previous: { ...previous }, current: { ...current } }
// oxlint-disable-next-line unicorn/no-useless-spread -- listeners may unsubscribe during dispatch
for (const listener of [...this.identityChangeListeners]) {
listener(event)
}
notifyAuditListeners(this.identityChangeListeners, event)
this.observeAuditFailure('endpoint_identity_changed', previousExactIncarnation, [
'endpoint_identity'
])
@@ -1682,13 +1679,10 @@ export class DaemonPtyAdapter implements IPtyProvider {
additionalEvidenceSources: readonly DaemonEvidenceSource[] = [],
endpointGoneProof?: 'windows_named_pipe_missing'
): void {
void classifyDaemonAuditFailure(
this.auditContext,
trigger,
exactIncarnation,
void classifyDaemonAuditFailure(this.auditContext, trigger, exactIncarnation, {
additionalEvidenceSources,
endpointGoneProof
)
})
.then((observation) => this.publishAuditObservation(observation))
.catch(() => {})
}
@@ -1696,10 +1690,7 @@ export class DaemonPtyAdapter implements IPtyProvider {
private publishAuditObservation(observation: DaemonAuditObservation): void {
this.lastAuditObservation = observation
trackDaemonAuditEligibility(observation)
// oxlint-disable-next-line unicorn/no-useless-spread -- listeners may unsubscribe during dispatch
for (const listener of [...this.auditObservationListeners]) {
listener(observation)
}
notifyAuditListeners(this.auditObservationListeners, observation)
}
private resyncBackgroundedSessions(): void {
@@ -2319,6 +2310,16 @@ function removeListener<T>(listeners: T[], listener: T): void {
}
}
function notifyAuditListeners<T>(listeners: readonly ((value: T) => void)[], value: T): void {
for (const listener of listeners.slice()) {
try {
listener(value)
} catch {
// Audit observers cannot affect daemon operations.
}
}
}
// Why: syscall='connect' distinguishes a dead-socket ENOENT/ECONNREFUSED from token-file ENOENT (no syscall);
// message strings incl. wedged-daemon "Hello response timed out" (#8689) also warrant a respawn.
function isDaemonGoneError(err: unknown): boolean {
@@ -633,6 +633,7 @@ describe('killAllProcessesForWorktree', () => {
// DaemonClient method so the shared connect+RPC budget path is exercised.
ensureConnectedWithin: async () => {},
isConnected: () => true,
getDaemonIdentity: () => null,
disconnect: () => {},
notify: () => {},
request: (
+81
View File
@@ -0,0 +1,81 @@
export const DAEMON_AUDIT_STATE_VALUES = ['present', 'gone', 'unknown'] as const
export const DAEMON_AUDIT_TRIGGER_VALUES = [
'endpoint_identity_changed',
'inventory_answered',
'inventory_failed',
'token_missing_after_authenticated_disconnect',
'transport_closed'
] as const
export const DAEMON_EVIDENCE_SOURCE_VALUES = [
'authenticated_inventory',
'boot_identity',
'endpoint_identity',
'endpoint_stat',
'linux_proc_stat',
'pid_record',
'process_command_line',
'process_signal',
'process_start_time',
'token_file',
'windows_cim',
'windows_named_pipe'
] as const
export const DAEMON_PROCESS_PRESENT_REASON_VALUES = [
'linux_identity_match',
'macos_identity_match',
'windows_identity_match'
] as const
export const DAEMON_PROCESS_GONE_REASON_VALUES = [
'linux_boot_changed',
'linux_start_ticks_mismatch',
'linux_zombie',
'pid_missing',
'windows_creation_time_mismatch',
'windows_process_missing'
] as const
export const DAEMON_PROCESS_UNKNOWN_REASON_VALUES = [
'command_line_mismatch',
'command_line_unavailable',
'exact_identity_unavailable',
'inspection_failed',
'linux_identity_incomplete',
'macos_start_time_mismatch',
'permission_denied',
'process_start_time_unavailable',
'windows_process_start_time_unavailable'
] as const
export const DAEMON_AUDIT_GONE_REASON_VALUES = [
...DAEMON_PROCESS_GONE_REASON_VALUES,
'windows_named_pipe_missing'
] as const
export const DAEMON_AUDIT_REASON_VALUES = [
'authenticated_inventory',
'endpoint_identity_changed',
'inventory_failed',
'token_missing_after_authenticated_disconnect',
'transport_closed',
...DAEMON_AUDIT_GONE_REASON_VALUES
] as const
export const DAEMON_AUDIT_PROCESS_REASON_VALUES = [
...DAEMON_PROCESS_PRESENT_REASON_VALUES,
...DAEMON_PROCESS_GONE_REASON_VALUES,
...DAEMON_PROCESS_UNKNOWN_REASON_VALUES,
'windows_named_pipe_missing'
] as const
export type DaemonAuditState = (typeof DAEMON_AUDIT_STATE_VALUES)[number]
export type DaemonAuditTrigger = (typeof DAEMON_AUDIT_TRIGGER_VALUES)[number]
export type DaemonAuditFailureTrigger = Exclude<DaemonAuditTrigger, 'inventory_answered'>
export type DaemonEvidenceSource = (typeof DAEMON_EVIDENCE_SOURCE_VALUES)[number]
export type DaemonProcessPresentReason = (typeof DAEMON_PROCESS_PRESENT_REASON_VALUES)[number]
export type DaemonProcessGoneReason = (typeof DAEMON_PROCESS_GONE_REASON_VALUES)[number]
export type DaemonProcessUnknownReason = (typeof DAEMON_PROCESS_UNKNOWN_REASON_VALUES)[number]
export type DaemonAuditGoneReason = (typeof DAEMON_AUDIT_GONE_REASON_VALUES)[number]
+12 -65
View File
@@ -27,6 +27,13 @@ import {
DAEMON_REPLACE_REASONS,
DAEMON_RETIRE_REASONS
} from './daemon-lifecycle-telemetry'
import {
DAEMON_AUDIT_PROCESS_REASON_VALUES,
DAEMON_AUDIT_REASON_VALUES,
DAEMON_AUDIT_STATE_VALUES,
DAEMON_AUDIT_TRIGGER_VALUES,
DAEMON_EVIDENCE_SOURCE_VALUES
} from './daemon-audit-eligibility'
import { SETUP_SCRIPT_IMPORT_PROVIDERS } from './setup-script-import-providers'
import { WORKSPACE_SOURCE_VALUES, type WorkspaceSource } from './workspace-source'
import { appStarSourceSchema } from './gh-star-source'
@@ -420,47 +427,10 @@ const daemonLifecycleSchema = z.discriminatedUnion('transition', [
const daemonAuditEligibilitySchema = z
.object({
state: z.enum(['present', 'gone', 'unknown']),
reason: z.enum([
'authenticated_inventory',
'endpoint_identity_changed',
'inventory_failed',
'linux_boot_changed',
'linux_start_ticks_mismatch',
'linux_zombie',
'pid_missing',
'token_missing_after_authenticated_disconnect',
'transport_closed',
'windows_creation_time_mismatch',
'windows_named_pipe_missing',
'windows_process_missing'
]),
trigger: z.enum([
'endpoint_identity_changed',
'inventory_answered',
'inventory_failed',
'token_missing_after_authenticated_disconnect',
'transport_closed'
]),
evidence_sources: z
.array(
z.enum([
'authenticated_inventory',
'boot_identity',
'endpoint_identity',
'endpoint_stat',
'linux_proc_stat',
'pid_record',
'process_command_line',
'process_signal',
'process_start_time',
'token_file',
'windows_cim',
'windows_named_pipe'
])
)
.min(1)
.max(12),
state: z.enum(DAEMON_AUDIT_STATE_VALUES),
reason: z.enum(DAEMON_AUDIT_REASON_VALUES),
trigger: z.enum(DAEMON_AUDIT_TRIGGER_VALUES),
evidence_sources: z.array(z.enum(DAEMON_EVIDENCE_SOURCE_VALUES)).min(1).max(12),
protocol_generation: z.number().int().positive().max(1_000),
provider: z.literal('local-daemon'),
endpoint_kind: z.enum(['unix-socket', 'windows-named-pipe']),
@@ -473,30 +443,7 @@ const daemonAuditEligibilitySchema = z
reachability: z.enum(['authenticated', 'disconnected', 'unknown']),
inventory_authority: z.enum(['authoritative', 'unavailable']),
process_liveness: z.enum(['present', 'gone', 'unknown']),
process_reason: z
.enum([
'command_line_mismatch',
'command_line_unavailable',
'exact_identity_unavailable',
'inspection_failed',
'linux_boot_changed',
'linux_identity_incomplete',
'linux_identity_match',
'linux_start_ticks_mismatch',
'linux_zombie',
'macos_identity_match',
'macos_start_time_mismatch',
'permission_denied',
'pid_missing',
'process_start_time_unavailable',
'windows_command_line_unavailable',
'windows_creation_time_mismatch',
'windows_identity_match',
'windows_named_pipe_missing',
'windows_process_missing',
'windows_process_start_time_unavailable'
])
.nullable(),
process_reason: z.enum(DAEMON_AUDIT_PROCESS_REASON_VALUES).nullable(),
endpoint_state: z.enum(['missing', 'named-pipe', 'non-socket', 'socket', 'unknown'])
})
.strict()