Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions apps/api/src/daemonResponseValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const PROVIDER_STATE_SLOT_SET = {
python_processes: true,
native_processes: true,
project_npm: true,
cron: true,
} as const satisfies Record<ProviderSlot, true>;
const PROVIDER_STATE_SLOTS = Object.keys(PROVIDER_STATE_SLOT_SET) as ProviderSlot[];
const U32_MAX = 4_294_967_295;
Expand Down Expand Up @@ -91,6 +92,12 @@ const V3_EVIDENCE_EDGE = {
npm_package_manifest_dependency: { relationship: "depends_on", sourcePrefix: "npm_project_", targetPrefix: "npm_package_" },
} as const;

// Version four is a parsed cron declaration from Cron's own scheduler slot.
// It makes no execution, successful-run, or host-health claim.
const V4_EVIDENCE_EDGE = {
cron_schedule_declaration: { relationship: "runs_on", sourcePrefix: "scheduled_job_", targetPrefix: "host_", target: "host_local" },
} as const;

function hasCompleteProviderStateVector(payload: unknown): boolean {
if (!payload || typeof payload !== "object") return false;
const providerStates = (payload as { providerStates?: unknown }).providerStates;
Expand Down Expand Up @@ -188,16 +195,24 @@ function hasCoherentRuntimeEvidence(payload: unknown): boolean {
&& value.assertionKind === "declared"
&& value.providerSlot === "project_npm"
&& (value.freshness === "fresh" || value.freshness === "stale" || value.freshness === "timed_out");
if (!isV1 && !isV2 && !isV3) return false;
const isV4 = value.version === 4
&& value.provider === "cron"
&& value.assertionKind === "declared"
&& value.providerSlot === "cron"
&& (value.freshness === "fresh" || value.freshness === "stale" || value.freshness === "timed_out");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind V4 evidence freshness to the Cron provider state

When a daemon returns internally inconsistent Cron data, this branch accepts the evidence freshness independently of the cron entry in providerStates; the added test at security.test.ts:1299-1307 even changes only the evidence to stale or timed_out while leaving the slot fresh. The real daemon derives the evidence freshness, providerRevision, and collectedAt from that exact slot in bind_cron_evidence, so a malformed daemon response can now publish impossible Cron provenance. Require these evidence fields to agree with the corresponding Cron provider state.

Useful? React with 👍 / 👎.

if (!isV1 && !isV2 && !isV3 && !isV4) return false;
const expected = typeof value.kind === "string"
? (isV1
? V1_EVIDENCE_EDGE[value.kind as keyof typeof V1_EVIDENCE_EDGE]
: isV2
? V2_EVIDENCE_EDGE[value.kind as keyof typeof V2_EVIDENCE_EDGE]
: V3_EVIDENCE_EDGE[value.kind as keyof typeof V3_EVIDENCE_EDGE])
: isV3
? V3_EVIDENCE_EDGE[value.kind as keyof typeof V3_EVIDENCE_EDGE]
: V4_EVIDENCE_EDGE[value.kind as keyof typeof V4_EVIDENCE_EDGE])
: undefined;
if (!expected || candidate.relationship !== expected.relationship || typeof candidate.source !== "string" || typeof candidate.target !== "string") return false;
if (value.subjectRef !== candidate.source || !candidate.source.startsWith(expected.sourcePrefix) || !candidate.target.startsWith(expected.targetPrefix)) return false;
if (isV4 && candidate.target !== "host_local") return false;
if (value.kind === "docker_daemon_state_bind_mount" && candidate.target !== "host_risk_docker_daemon_state") return false;
if (candidate.source === candidate.target) return false;
// An opaque observation token must never be the collection timestamp
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ function getMockResponse<T>(path: string): T {
providerStates: [
unavailableProviderState("network_infrastructure"), unavailableProviderState("host_scoped"), unavailableProviderState("systemd"),
unavailableProviderState("python_processes"), unavailableProviderState("native_processes"),
unavailableProviderState("project_npm")
unavailableProviderState("project_npm"), unavailableProviderState("cron")
],
source: "mock"
};
Expand Down
91 changes: 91 additions & 0 deletions apps/api/test/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,70 @@ test("runtime evidence is required and fails closed before browser publication",
assert.ok(malformedNpmEdge);
malformedNpmEdge.target = "docker_container_not_a_package";
assert.throws(() => validateDaemonResponse("/daemon/runtime/map", wrongNpmEndpoint));

const cronEdge = fixture.edges.find((edge: { source?: unknown }) => edge.source === "scheduled_job_fixture_daily_backup");
assert.ok(cronEdge, "canonical daemon fixture carries a V4 Cron schedule declaration");
assert.doesNotThrow(() => validateDaemonResponse("/daemon/runtime/map", fixture));
for (const freshness of ["stale", "timed_out"] as const) {
const retainedCron = structuredClone(fixture);
const edge = retainedCron.edges.find((candidate: { source?: unknown }) => candidate.source === "scheduled_job_fixture_daily_backup");
assert.ok(edge);
edge.evidenceRefs[0].freshness = freshness;
assert.doesNotThrow(
() => validateDaemonResponse("/daemon/runtime/map", retainedCron),
`v4 cron evidence may retain ${freshness} data from its own scheduler slot`
);
}
for (const [field, value] of [
["provider", "systemd"],
["kind", "systemd_requires"],
["assertionKind", "observed"],
["providerSlot", "host_scoped"],
["freshness", "unavailable"],
["version", 3]
] as const) {
const malformedCron = structuredClone(fixture);
const edge = malformedCron.edges.find((candidate: { source?: unknown }) => candidate.source === "scheduled_job_fixture_daily_backup");
assert.ok(edge);
edge.evidenceRefs[0][field] = value;
assert.throws(
() => validateDaemonResponse("/daemon/runtime/map", malformedCron),
`v4 cron evidence must reject fabricated ${field}`
);
}
const timestampAliasedCron = structuredClone(fixture);
const timestampAliasedCronEdge = timestampAliasedCron.edges.find((candidate: { source?: unknown }) => candidate.source === "scheduled_job_fixture_daily_backup");
assert.ok(timestampAliasedCronEdge);
timestampAliasedCronEdge.evidenceRefs[0].providerRevision = String(timestampAliasedCronEdge.evidenceRefs[0].collectedAt);
assert.throws(() => validateDaemonResponse("/daemon/runtime/map", timestampAliasedCron));
const unboundedCron = structuredClone(fixture);
const unboundedCronEdge = unboundedCron.edges.find((candidate: { source?: unknown }) => candidate.source === "scheduled_job_fixture_daily_backup");
assert.ok(unboundedCronEdge);
unboundedCronEdge.evidenceRefs[0].providerRevision = "x".repeat(260);
assert.throws(() => validateDaemonResponse("/daemon/runtime/map", unboundedCron));
const unsafeTimestampCron = structuredClone(fixture);
const unsafeTimestampCronEdge = unsafeTimestampCron.edges.find((candidate: { source?: unknown }) => candidate.source === "scheduled_job_fixture_daily_backup");
assert.ok(unsafeTimestampCronEdge);
unsafeTimestampCronEdge.evidenceRefs[0].collectedAt = Number.MAX_SAFE_INTEGER + 1;
assert.throws(() => validateDaemonResponse("/daemon/runtime/map", unsafeTimestampCron));
const extraCronField = structuredClone(fixture);
const extraCronFieldEdge = extraCronField.edges.find((candidate: { source?: unknown }) => candidate.source === "scheduled_job_fixture_daily_backup");
assert.ok(extraCronFieldEdge);
extraCronFieldEdge.evidenceRefs[0].rawSchedule = "* * * * * secret";
assert.throws(() => validateDaemonResponse("/daemon/runtime/map", extraCronField));
for (const [field, value] of [
["target", "host_other"],
["relationship", "depends_on"]
] as const) {
const malformedCron = structuredClone(fixture);
const edge = malformedCron.edges.find((candidate: { source?: unknown }) => candidate.source === "scheduled_job_fixture_daily_backup");
assert.ok(edge);
edge[field] = value;
assert.throws(
() => validateDaemonResponse("/daemon/runtime/map", malformedCron),
`v4 cron evidence must reject a noncanonical ${field}`
);
}
});

test("fabricated runtime evidence is rejected over the authenticated API boundary", async () => {
Expand Down Expand Up @@ -1344,6 +1408,32 @@ test("fabricated V3 NPM evidence is rejected neutrally over the authenticated AP
assert.doesNotMatch(JSON.stringify(body), new RegExp(sentinel));
});

test("fabricated V4 Cron evidence is rejected neutrally over the authenticated API boundary", async () => {
const fixture = JSON.parse(await readFile(
new URL("../../../tests/fixtures/contracts/runtime-map-daemon-emitted.json", import.meta.url),
"utf8"
));
const sentinel = "DOCKERMAP_TEST_FAKE_CRON_EVIDENCE_SECRET";
const cronEdge = fixture.edges.find((edge: { source?: unknown }) => edge.source === "scheduled_job_fixture_daily_backup");
assert.ok(cronEdge, "canonical fixture must exercise the V4 browser boundary");
cronEdge.target = `host_${sentinel}`;
cronEdge.evidenceRefs[0].subjectRef = cronEdge.source;
cronEdge.evidenceRefs[0].providerSlot = "host_scoped";
const daemon = await startStubDaemon((req, res) => {
if (req.url === "/daemon/runtime/map") return sendJson(res, 200, fixture);
return sendJson(res, 404, { code: "not_found", message: "missing" });
});
const api = await startApi({ DOCKERMAP_DAEMON_URL: `http://127.0.0.1:${daemon.port}`, DOCKERMAP_API_TOKEN: "test-token" });
const response = await request(api, "/api/v1/runtime/map", { headers: { Authorization: "Bearer test-token" } });
assert.equal(response.status, 502);
const body = await response.json();
assert.deepEqual(body, {
code: "daemon_invalid_response",
message: "Daemon response did not match its declared contract"
});
assert.doesNotMatch(JSON.stringify(body), new RegExp(sentinel));
});

test("actual canonical and v1 SSE snapshot/error frames use their declared payload schemas", async () => {
const health = JSON.parse(await readFile(new URL("../../../tests/fixtures/contracts/health-response.json", import.meta.url), "utf8"));
const healthyDaemon = await startStubDaemon((req, res) => {
Expand Down Expand Up @@ -2230,6 +2320,7 @@ test("API publishes redacted and normalized daemon data on every response route"
providerStates: [
{ slot: "network_infrastructure", state: "unavailable", lastAttemptMs: null, lastSuccessMs: null, lastDurationMs: null, consecutiveFailureCount: 0, dataRevision: null, statusReason: "initial" },
{ slot: "host_scoped", state: "unavailable", lastAttemptMs: null, lastSuccessMs: null, lastDurationMs: null, consecutiveFailureCount: 0, dataRevision: null, statusReason: "initial" },
{ slot: "cron", state: "unavailable", lastAttemptMs: null, lastSuccessMs: null, lastDurationMs: null, consecutiveFailureCount: 0, dataRevision: null, statusReason: "initial" },
{ slot: "systemd", state: "unavailable", lastAttemptMs: null, lastSuccessMs: null, lastDurationMs: null, consecutiveFailureCount: 0, dataRevision: null, statusReason: "initial" },
{ slot: "python_processes", state: "unavailable", lastAttemptMs: null, lastSuccessMs: null, lastDurationMs: null, consecutiveFailureCount: 0, dataRevision: null, statusReason: "initial" },
{ slot: "native_processes", state: "unavailable", lastAttemptMs: null, lastSuccessMs: null, lastDurationMs: null, consecutiveFailureCount: 0, dataRevision: null, statusReason: "initial" },
Expand Down
43 changes: 43 additions & 0 deletions tests/fixtures/contracts/runtime-map-daemon-emitted.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,28 @@
"dependencies": [],
"dependents": []
}
},
{
"id": "host_local",
"provider": "host",
"type": "host",
"label": "host",
"status": "online",
"layer": "host",
"metadata": {}
},
{
"id": "scheduled_job_fixture_daily_backup",
"provider": "scheduled_job",
"type": "scheduled_job",
"label": "daily backup",
"status": "scheduled",
"layer": "process",
"metadata": {
"source": "fixture crontab",
"line": "1",
"command": "daily backup"
}
}
],
"edges": [
Expand Down Expand Up @@ -589,6 +611,27 @@
"freshness": "fresh"
}
]
},
{
"source": "scheduled_job_fixture_daily_backup",
"target": "host_local",
"relationship": "runs_on",
"metadata": {},
"evidenceRefs": [
{
"version": 4,
"id": "fixture-cron-schedule-declaration-daily-backup",
"provider": "cron",
"kind": "cron_schedule_declaration",
"assertionKind": "declared",
"summary": "cron declared a scheduled job",
"subjectRef": "scheduled_job_fixture_daily_backup",
"collectedAt": 1787196125766,
"providerRevision": "fixture-cron-observation-1",
"providerSlot": "cron",
"freshness": "fresh"
}
]
}
],
"diagnostics": [],
Expand Down
Loading