fix(security): reconcile asset control plane truth
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m27s
CD Pipeline / build-and-deploy (push) Failing after 3m57s
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
ogt
2026-07-14 10:47:33 +08:00
parent 467542a018
commit b6ddee7984
9 changed files with 869 additions and 170 deletions

View File

@@ -43,6 +43,13 @@ type Copy = {
audit: string;
aiTrace: string;
sources: string;
latestScan: string;
freshness: string;
runAssets: string;
retired: string;
stale: string;
success: string;
failed: string;
sameRunMissing: string;
unavailable: string;
refresh: string;
@@ -68,6 +75,13 @@ const COPY: Record<"zh" | "en", Copy> = {
audit: "稽核軌跡",
aiTrace: "AI 軌跡",
sources: "資料源",
latestScan: "最新盤點",
freshness: "新鮮度",
runAssets: "本輪資產",
retired: "本輪退役",
stale: "逾期資產",
success: "成功",
failed: "失敗",
sameRunMissing: "尚無同 run 閉環證據",
unavailable: "Live readback 無法取得",
refresh: "重新整理資安控制平面",
@@ -91,6 +105,13 @@ const COPY: Record<"zh" | "en", Copy> = {
audit: "Audit trail",
aiTrace: "AI trace",
sources: "Sources",
latestScan: "Latest scan",
freshness: "Freshness",
runAssets: "Run assets",
retired: "Retired",
stale: "Stale assets",
success: "Success",
failed: "Failed",
sameRunMissing: "No same-run closure evidence",
unavailable: "Live readback unavailable",
refresh: "Refresh security control plane",
@@ -107,6 +128,12 @@ function stageCount(stage: SecurityControlPlaneStage) {
return stage.evidence_count_24h ?? stage.receipt_count_24h ?? 0;
}
function freshnessLabel(ageSeconds: number | null) {
if (ageSeconds === null) return "--";
if (ageSeconds < 60) return `${Math.max(0, Math.round(ageSeconds))}s`;
return `${Math.round(ageSeconds / 60)}m`;
}
async function fetchControlPlane(): Promise<SecurityAssetControlPlanePayload | null> {
const base = getRuntimeApiBaseUrl();
const path = "/api/v1/iwooos/security-asset-control-plane";
@@ -146,6 +173,49 @@ function MetricCell({
);
}
function TruthCell({
label,
value,
icon: Icon,
tone = "neutral",
}: {
label: string;
value: string | number;
icon: typeof ShieldCheck;
tone?: "neutral" | "healthy" | "critical";
}) {
return (
<div className="flex min-w-0 items-center gap-2 bg-white px-3 py-2.5 last:col-span-2 sm:last:col-span-1">
<Icon
className={cn(
"h-3.5 w-3.5 shrink-0",
tone === "healthy"
? "text-[#2f8a50]"
: tone === "critical"
? "text-[#c9584d]"
: "text-[#77736a]"
)}
aria-hidden="true"
/>
<div className="min-w-0">
<div className="truncate text-[9px] font-medium text-[#77736a]">{label}</div>
<div
className={cn(
"mt-0.5 truncate font-mono text-xs font-semibold",
tone === "healthy"
? "text-[#17602a]"
: tone === "critical"
? "text-[#9f2f25]"
: "text-[#141413]"
)}
>
{value}
</div>
</div>
</div>
);
}
function StageStrip({
title,
stages,
@@ -286,6 +356,47 @@ export function SecurityAssetControlPlaneCockpit({
/>
</div>
<div
className="grid grid-cols-2 gap-px border-t border-[#d8d3c7] bg-[#d8d3c7] sm:grid-cols-5"
data-testid="security-asset-discovery-truth"
>
<TruthCell
label={copy.latestScan}
value={
payload?.discovery.latest_status === "success"
? copy.success
: payload
? copy.failed
: "--"
}
icon={Radar}
tone={payload?.discovery.latest_status === "success" ? "healthy" : "critical"}
/>
<TruthCell
label={copy.freshness}
value={payload ? freshnessLabel(payload.discovery.age_seconds) : "--"}
icon={RefreshCw}
tone={payload?.discovery.fresh ? "healthy" : "critical"}
/>
<TruthCell
label={copy.runAssets}
value={payload?.discovery.latest_total_assets ?? "--"}
icon={Database}
/>
<TruthCell
label={copy.retired}
value={payload?.discovery.disappeared_assets ?? "--"}
icon={Boxes}
tone={(payload?.discovery.disappeared_assets ?? 0) > 0 ? "healthy" : "neutral"}
/>
<TruthCell
label={copy.stale}
value={payload?.summary.stale_asset_count ?? "--"}
icon={TriangleAlert}
tone={(payload?.summary.stale_asset_count ?? 0) > 0 ? "critical" : "healthy"}
/>
</div>
{payload ? (
<div className="grid gap-4 p-4 xl:grid-cols-[1.1fr_1.4fr]">
<div className="min-w-0 space-y-4">

View File

@@ -46,10 +46,17 @@ function payload(): SecurityAssetControlPlanePayload {
},
discovery: {
latest_status: "success",
latest_scan_depth: "shallow",
latest_started_at: "2026-07-11T03:49:00+00:00",
latest_ended_at: "2026-07-11T03:50:00+00:00",
latest_success_at: "2026-07-11T03:50:00+00:00",
age_seconds: 600,
freshness_slo_seconds: 7200,
fresh: true,
latest_total_assets: 24,
new_assets: 2,
modified_assets: 22,
disappeared_assets: 1,
},
asset_scopes: [],
nist_csf_functions: [],

View File

@@ -77,10 +77,17 @@ export type SecurityAssetControlPlanePayload = {
summary: SecurityControlPlaneSummary;
discovery: {
latest_status: string;
latest_scan_depth: string;
latest_started_at: string | null;
latest_ended_at: string | null;
latest_success_at: string | null;
age_seconds: number | null;
freshness_slo_seconds: number;
fresh: boolean;
latest_total_assets: number;
new_assets: number;
modified_assets: number;
disappeared_assets: number;
};
asset_scopes: SecurityControlPlaneScope[];
nist_csf_functions: SecurityControlPlaneFunction[];