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
24 changes: 23 additions & 1 deletion apps/web/src/lib/findingPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Finding } from "@dockermap/contracts";
*/
export interface FindingPresentation {
title: string;
summary: string;
category: string;
hint: string;
recommendation: string;
Expand Down Expand Up @@ -60,6 +61,14 @@ const SPECS: readonly FindingSpec[] = [
evidenceCount: 1, evidence: { version: 1, provider: "docker", kind: "docker_daemon_state_bind_mount", assertionKind: "observed", providerSlot: null },
title: "Docker daemon-state access needs review", category: "Docker daemon state", hint: "Observed Docker fact", tone: "warn", severityLabel: "Warning"
},
{
ruleId: "docker.daemon_state_bind_mount_publishes_port", severity: "warning",
summary: "A container with Docker daemon state access also has a published host port.",
recommendation: "Review whether the daemon-state access and host-port publication are both intended.",
idPrefix: "finding_docker_daemon_state_bind_mount_publishes_port_", subjectPrefix: "docker_container_", targetRef: "host_risk_docker_daemon_state",
evidenceCount: 2, evidence: { version: 1, provider: "docker", kind: "docker_daemon_state_bind_mount", assertionKind: "observed", providerSlot: null },
title: "Docker daemon-state and host-port publication need review", category: "Docker daemon state + host port", hint: "Observed Docker facts", tone: "warn", severityLabel: "Warning", inspectChanges: true
},
{
ruleId: "docker.compose_declared_target_not_active", severity: "advisory",
summary: "A running Docker Compose service declares a dependency whose container is not active.",
Expand Down Expand Up @@ -100,7 +109,7 @@ export function presentationForFinding(value: unknown): FindingPresentation | nu
// The API permits the Compose observation's legacy absent slot as well as
// null. Both mean the Docker-wide collector, never a provider-supplied
// slot name; all other supported shapes require their exact slot value.
|| (spec.ruleId === "docker.compose_declared_target_not_active"
|| ((spec.ruleId === "docker.compose_declared_target_not_active" || spec.ruleId === "docker.daemon_state_bind_mount_publishes_port")
? evidence.providerSlot !== undefined && evidence.providerSlot !== null
: evidence.providerSlot !== spec.evidence.providerSlot)
|| evidence.freshness !== "fresh"
Expand All @@ -115,5 +124,18 @@ export function presentationForFinding(value: unknown): FindingPresentation | nu
|| port.subjectRef !== finding.subjectRef) return null;
}

// The daemon-state + host-port advisory is a paired observation from one
// Docker collection. The UI never renders either fact's values, but it must
// not present the static warning if the opaque pair is incomplete, crossed,
// stale, or from different observations. Version-one Docker evidence may
// omit its legacy null slot at the browser boundary.
if (spec.ruleId === "docker.daemon_state_bind_mount_publishes_port") {
const port = record(finding.evidenceRefs[1]);
if (!port || port.version !== 1 || port.provider !== "docker" || port.kind !== "docker_port_publication"
|| port.assertionKind !== "observed" || (port.providerSlot !== undefined && port.providerSlot !== null)
|| port.freshness !== "fresh" || port.subjectRef !== finding.subjectRef
|| port.collectedAt !== evidence.collectedAt || port.providerRevision !== evidence.providerRevision) return null;
}

return spec;
}
1 change: 1 addition & 0 deletions apps/web/src/screens/Findings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default function Findings() {
if (!presentation) return null;
return <Panel key={`${finding.ruleId}-${index}`} title={presentation.title} icon="alert" hint={presentation.hint}>
<div className="tag-wrap"><Tag tone={presentation.tone}>{presentation.severityLabel}</Tag><Tag tone="muted">{presentation.category}</Tag></div>
<p className="muted-copy">{presentation.summary}</p>
<p className="muted-copy">{presentation.recommendation}</p>
{presentation.inspectChanges && <Link className="ghost-link" to="/changes">Inspect recent changes <Icon name="arrow" size={14} /></Link>}
</Panel>;
Expand Down
76 changes: 74 additions & 2 deletions apps/web/src/screens/findings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,86 @@ describe("Findings screen", () => {
expect(html).toContain("Review the declared dependency and the target container state.");
});

it("renders daemon-state host-port advice with static generic copy and a changes link", () => {
const daemonStatePort = structuredClone(findings);
daemonStatePort.findings[0] = {
id: "finding_docker_daemon_state_bind_mount_publishes_port_docker_container_redacted",
ruleId: "docker.daemon_state_bind_mount_publishes_port",
severity: "warning",
summary: "A container with Docker daemon state access also has a published host port.",
recommendation: "Review whether the daemon-state access and host-port publication are both intended.",
subjectRef: "docker_container_redacted", targetRef: "host_risk_docker_daemon_state",
evidenceRefs: [
{ version: 1, id: "mount-path-secret", provider: "docker", kind: "docker_daemon_state_bind_mount", assertionKind: "observed", summary: "provider-text-secret", subjectRef: "docker_container_redacted", collectedAt: 1, providerRevision: "opaque-pair", providerSlot: null, freshness: "fresh" },
{ version: 1, id: "host-port-secret", provider: "docker", kind: "docker_port_publication", assertionKind: "observed", summary: "provider-text-secret", subjectRef: "docker_container_redacted", collectedAt: 1, providerRevision: "opaque-pair", providerSlot: null, freshness: "fresh" }
]
};
const html = render({ findings: daemonStatePort });
expect(html).toContain("Docker daemon-state and host-port publication need review");
expect(html).toContain("A container with Docker daemon state access also has a published host port.");
expect(html).toContain("Review whether the daemon-state access and host-port publication are both intended.");
expect(html).toContain('href="/changes"');
for (const hidden of ["docker_container_redacted", "mount-path-secret", "host-port-secret", "provider-text-secret", "opaque-pair"]) expect(html).not.toContain(hidden);
});

it("accepts the legacy omitted V1 Docker provider slots for a coherent daemon-state host-port pair", () => {
const daemonStatePort = structuredClone(findings);
daemonStatePort.findings[0] = {
id: "finding_docker_daemon_state_bind_mount_publishes_port_docker_container_legacy",
ruleId: "docker.daemon_state_bind_mount_publishes_port", severity: "warning",
summary: "A container with Docker daemon state access also has a published host port.",
recommendation: "Review whether the daemon-state access and host-port publication are both intended.",
subjectRef: "docker_container_legacy", targetRef: "host_risk_docker_daemon_state",
evidenceRefs: [
{ version: 1, id: "opaque-mount", provider: "docker", kind: "docker_daemon_state_bind_mount", assertionKind: "observed", summary: "opaque", subjectRef: "docker_container_legacy", collectedAt: 1, providerRevision: "opaque", providerSlot: null, freshness: "fresh" },
{ version: 1, id: "opaque-port", provider: "docker", kind: "docker_port_publication", assertionKind: "observed", summary: "opaque", subjectRef: "docker_container_legacy", collectedAt: 1, providerRevision: "opaque", providerSlot: null, freshness: "fresh" }
]
};
for (const evidence of daemonStatePort.findings[0].evidenceRefs) delete (evidence as { providerSlot?: unknown }).providerSlot;
const html = render({ findings: daemonStatePort });
expect(html).toContain("Docker daemon-state and host-port publication need review");
});

it("fails closed for malformed or mismatched daemon-state host-port pairs", () => {
const daemonStatePort = structuredClone(findings);
daemonStatePort.findings[0] = {
id: "finding_docker_daemon_state_bind_mount_publishes_port_docker_container_pair",
ruleId: "docker.daemon_state_bind_mount_publishes_port", severity: "warning",
summary: "A container with Docker daemon state access also has a published host port.",
recommendation: "Review whether the daemon-state access and host-port publication are both intended.",
subjectRef: "docker_container_pair", targetRef: "host_risk_docker_daemon_state",
evidenceRefs: [
{ version: 1, id: "opaque-mount", provider: "docker", kind: "docker_daemon_state_bind_mount", assertionKind: "observed", summary: "opaque", subjectRef: "docker_container_pair", collectedAt: 1, providerRevision: "first", providerSlot: null, freshness: "fresh" },
{ version: 1, id: "opaque-port", provider: "docker", kind: "docker_port_publication", assertionKind: "observed", summary: "opaque", subjectRef: "docker_container_other", collectedAt: 2, providerRevision: "second", providerSlot: null, freshness: "fresh" }
]
};
const html = render({ findings: daemonStatePort });
expect(html).not.toContain("Docker daemon-state and host-port publication need review");
expect(html).not.toContain("docker_container_pair");
expect(html).not.toContain("docker_container_other");
});

it("suppresses findings in demo and mock contexts even if fixture data is injected", () => {
const daemonStatePort = structuredClone(findings);
daemonStatePort.findings[0] = {
id: "finding_docker_daemon_state_bind_mount_publishes_port_docker_container_fixture",
ruleId: "docker.daemon_state_bind_mount_publishes_port", severity: "warning",
summary: "A container with Docker daemon state access also has a published host port.",
recommendation: "Review whether the daemon-state access and host-port publication are both intended.",
subjectRef: "docker_container_fixture", targetRef: "host_risk_docker_daemon_state",
evidenceRefs: [
{ version: 1, id: "opaque-mount", provider: "docker", kind: "docker_daemon_state_bind_mount", assertionKind: "observed", summary: "opaque", subjectRef: "docker_container_fixture", collectedAt: 1, providerRevision: "opaque", providerSlot: null, freshness: "fresh" },
{ version: 1, id: "opaque-port", provider: "docker", kind: "docker_port_publication", assertionKind: "observed", summary: "opaque", subjectRef: "docker_container_fixture", collectedAt: 1, providerRevision: "opaque", providerSlot: null, freshness: "fresh" }
]
};
for (const context of [
{ evidenceMode: "demo" as const, modelProvenance: "demo" as const },
{ evidenceMode: "mock" as const, modelProvenance: "mock" as const }
]) {
const html = render({ findings, ...context });
const html = render({ findings: daemonStatePort, ...context });
expect(html).toContain("Live evidence is not established");
expect(html).not.toContain("Declared dependency needs review");
expect(html).not.toContain("Docker daemon-state and host-port publication need review");
expect(html).not.toContain("docker_container_fixture");
}
});
});
Loading