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
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:
@@ -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">
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user