fix(awooop): redesign iwooos security cockpit
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m4s
CD Pipeline / build-and-deploy (push) Successful in 4m44s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 18s
CD Pipeline / post-deploy-checks (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m4s
CD Pipeline / build-and-deploy (push) Successful in 4m44s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 18s
CD Pipeline / post-deploy-checks (push) Has been cancelled
This commit is contained in:
@@ -9310,6 +9310,68 @@ function stockplatformManagerStateKey(value: string | null | undefined) {
|
||||
return "review";
|
||||
}
|
||||
|
||||
function securityOperatorLabel(value: string | null | undefined, isZh: boolean) {
|
||||
const key = String(value ?? "");
|
||||
const zh: Record<string, string> = {
|
||||
blocked_reboot_auto_recovery_slo_not_ready: "重開機復原 SLO 未閉合",
|
||||
collector_readback_verified_source_required_before_relink_dry_run:
|
||||
"候選身分待確認",
|
||||
host_unreachable_after_reboot: "重開機後主機未連線",
|
||||
reboot_event_required_host_unreachable: "重開機目標不可達",
|
||||
waiting_readback: "等待正式讀回",
|
||||
windows99_remote_execution_channel_unavailable: "Windows99 通道未連線",
|
||||
windows99_vmware_autostart_readback_missing: "VMware 自啟讀回缺口",
|
||||
windows99_vmware_vmx_missing: "VMX 來源缺口",
|
||||
};
|
||||
const en: Record<string, string> = {
|
||||
blocked_reboot_auto_recovery_slo_not_ready: "Reboot recovery SLO open",
|
||||
collector_readback_verified_source_required_before_relink_dry_run:
|
||||
"Candidate identity pending",
|
||||
host_unreachable_after_reboot: "Host unreachable after reboot",
|
||||
reboot_event_required_host_unreachable: "Reboot target unreachable",
|
||||
waiting_readback: "Waiting readback",
|
||||
windows99_remote_execution_channel_unavailable: "Windows99 channel offline",
|
||||
windows99_vmware_autostart_readback_missing: "VMware autostart readback gap",
|
||||
windows99_vmware_vmx_missing: "VMX source gap",
|
||||
};
|
||||
if (isZh && zh[key]) return zh[key];
|
||||
if (!isZh && en[key]) return en[key];
|
||||
return compactBlockerLabel(key);
|
||||
}
|
||||
|
||||
function securityNextActionLabel(value: string | null | undefined, isZh: boolean) {
|
||||
const key = String(value ?? "");
|
||||
if (
|
||||
key.includes("console_candidate_identity") ||
|
||||
key.includes("candidate_identity") ||
|
||||
key.includes("relink_dry_run") ||
|
||||
key.includes("vmx_source")
|
||||
) {
|
||||
return isZh
|
||||
? "確認 Windows99 VMX 身分證據,完成 scoped relink dry-run,再重跑 no-secret collector 與 SLO scorecard"
|
||||
: "Confirm Windows99 VMX identity evidence, complete scoped relink dry-run, then rerun no-secret collector and SLO scorecard";
|
||||
}
|
||||
if (key.includes("no_secret_collector") || key.includes("scorecard")) {
|
||||
return isZh
|
||||
? "重跑 no-secret collector 與正式 scorecard,補齊可審核讀回"
|
||||
: "Rerun the no-secret collector and production scorecard for auditable readback";
|
||||
}
|
||||
if (!key || key === "--") return "--";
|
||||
return key.replace(/_/g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function compactEvidenceRef(value: string | null | undefined) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (!text) return "--";
|
||||
const scheme = text.match(/^([a-z]+):\/\//)?.[1];
|
||||
const lastSegment = text.split("/").filter(Boolean).pop() ?? text;
|
||||
return scheme ? `${scheme}://${lastSegment}` : lastSegment;
|
||||
}
|
||||
|
||||
function percentText(value: number | null | undefined) {
|
||||
return Number.isFinite(value) ? `${Math.round(Number(value))}%` : "--";
|
||||
}
|
||||
|
||||
function SecurityMainlineCockpit({
|
||||
priority,
|
||||
locale,
|
||||
@@ -9325,17 +9387,25 @@ function SecurityMainlineCockpit({
|
||||
eyebrow: isZh ? "IwoooS 資安主線" : "IwoooS security mainline",
|
||||
title: isZh ? "IwoooS 管理者作戰盤" : "IwoooS operator cockpit",
|
||||
subtitle: isZh
|
||||
? "正式環境讀回:Wazuh manager registry、Windows99 locator、P0 blocker 與高風險動作邊界集中顯示。"
|
||||
: "Production readback for Wazuh registry, Windows99 locator, P0 blockers, and high-risk guardrails.",
|
||||
? "正式環境讀回:風險態勢、處置佇列、MCP/RAG/PlayBook 證據鏈集中顯示。"
|
||||
: "Production readback for risk posture, action queue, and MCP/RAG/PlayBook evidence.",
|
||||
live: isZh ? "正式讀回" : "production",
|
||||
loading: isZh ? "讀取中" : "loading",
|
||||
p0: isZh ? "目前 P0" : "Current P0",
|
||||
posture: isZh ? "風險態勢" : "Risk posture",
|
||||
visibility: isZh ? "監測覆蓋" : "Visibility",
|
||||
runtime: isZh ? "Runtime 接受" : "Runtime accepted",
|
||||
registry: isZh ? "Wazuh 納管" : "Wazuh registry",
|
||||
locator: isZh ? "Windows99 locator" : "Windows99 locator",
|
||||
guard: isZh ? "高風險邊界" : "Risk boundary",
|
||||
next: isZh ? "下一步" : "Next action",
|
||||
queue: isZh ? "處置佇列" : "Action queue",
|
||||
evidenceFabric: isZh ? "證據鏈" : "Evidence fabric",
|
||||
externalGap: isZh ? "外部整合待 tool-result 審核" : "External tool-result review pending",
|
||||
blockers: isZh ? "阻擋項" : "blockers",
|
||||
readiness: isZh ? "準備度" : "readiness",
|
||||
needsAction: isZh ? "待處置" : "Needs action",
|
||||
stable: isZh ? "穩定" : "Stable",
|
||||
owner: isZh ? "owner export" : "owner export",
|
||||
reviewer: isZh ? "review passed" : "review passed",
|
||||
identity: isZh ? "單一身分候選" : "identity candidate",
|
||||
@@ -9345,7 +9415,12 @@ function SecurityMainlineCockpit({
|
||||
noPower: isZh ? "未改 VM power" : "no VM power change",
|
||||
locked: isZh ? "runtime gate 鎖定" : "runtime gate locked",
|
||||
deployLabel: isZh ? "部署" : "deploy",
|
||||
locatorLabel: isZh ? "定位器" : "locator",
|
||||
blockerLabel: isZh ? "主阻擋" : "primary blocker",
|
||||
identityEvidence: isZh ? "身分證據" : "identity",
|
||||
dryRun: isZh ? "Dry-run" : "Dry-run",
|
||||
verify: isZh ? "驗證讀回" : "verify",
|
||||
ready: isZh ? "已掛接" : "linked",
|
||||
pending: isZh ? "待補齊" : "pending",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -9384,6 +9459,8 @@ function SecurityMainlineCockpit({
|
||||
evidence?.active_blockers ??
|
||||
[];
|
||||
const readiness = summary?.active_p0_readiness_percent ?? 0;
|
||||
const visibilityPercent = runtimeSummary?.control_plane_visibility_percent;
|
||||
const runtimeAcceptancePercent = runtimeSummary?.actual_runtime_acceptance_percent;
|
||||
const registryAccepted =
|
||||
registrySummary?.manager_registry_accepted_count ??
|
||||
runtimeSummary?.wazuh_manager_registry_accepted_count ??
|
||||
@@ -9416,6 +9493,11 @@ function SecurityMainlineCockpit({
|
||||
summary?.windows99_vmx_source_locator_status ??
|
||||
evidence?.windows99_vmx_source_locator_status ??
|
||||
"waiting_readback";
|
||||
const primaryBlocker =
|
||||
activeBlockers[0] ??
|
||||
summary?.ai_loop_current_blocker_id ??
|
||||
runtime?.status ??
|
||||
"waiting_readback";
|
||||
const locatorCandidates =
|
||||
summary?.windows99_vmx_source_locator_candidate_source_count ??
|
||||
evidence?.windows99_vmx_source_locator_candidate_source_count ??
|
||||
@@ -9447,6 +9529,7 @@ function SecurityMainlineCockpit({
|
||||
activeItem?.next_action ??
|
||||
summary?.commander_inserted_requirement_next_action ??
|
||||
"--";
|
||||
const operatorNextAction = securityNextActionLabel(nextAction, isZh);
|
||||
const guardClear =
|
||||
runtimeGateCount === 0 &&
|
||||
hostWriteCount === 0 &&
|
||||
@@ -9459,16 +9542,42 @@ function SecurityMainlineCockpit({
|
||||
priority?.current_head?.latest_successful_deployed_source_sha ??
|
||||
priority?.current_head?.gitea_main_sha ??
|
||||
null;
|
||||
const shortSha = mainSha ? mainSha.slice(0, 10) : "--";
|
||||
const deploySha = mainSha ? mainSha.slice(0, 10) : "--";
|
||||
const ragRefs = uniqueText([
|
||||
summary?.windows99_vmx_source_repair_rag_chunk_ref,
|
||||
evidence?.windows99_vmx_source_repair_rag_chunk_ref,
|
||||
summary?.windows99_vmware_autostart_check_mode_rag_chunk_ref,
|
||||
evidence?.windows99_vmware_autostart_check_mode_rag_chunk_ref,
|
||||
]);
|
||||
const mcpRefs = uniqueText([
|
||||
summary?.windows99_vmx_source_repair_mcp_evidence_ref,
|
||||
evidence?.windows99_vmx_source_repair_mcp_evidence_ref,
|
||||
summary?.windows99_vmware_autostart_check_mode_mcp_evidence_ref,
|
||||
evidence?.windows99_vmware_autostart_check_mode_mcp_evidence_ref,
|
||||
]);
|
||||
const playbookRefs = uniqueText([
|
||||
summary?.windows99_vmx_source_repair_playbook_trust_writeback_ref,
|
||||
evidence?.windows99_vmx_source_repair_playbook_trust_writeback_ref,
|
||||
summary?.windows99_vmware_autostart_check_mode_playbook_trust_writeback_ref,
|
||||
evidence?.windows99_vmware_autostart_check_mode_playbook_trust_writeback_ref,
|
||||
]);
|
||||
const cards = [
|
||||
{
|
||||
key: "p0",
|
||||
label: copy.p0,
|
||||
value: summary?.active_p0_workplan_id ?? activeItem?.id ?? "P0",
|
||||
detail: `${copy.readiness} ${readiness}% · ${activeBlockers.length} ${copy.blockers}`,
|
||||
key: "posture",
|
||||
label: copy.posture,
|
||||
value: activeBlockers.length > 0 ? copy.needsAction : copy.stable,
|
||||
detail: `${summary?.active_p0_workplan_id ?? activeItem?.id ?? "P0"} · ${securityOperatorLabel(primaryBlocker, isZh)}`,
|
||||
icon: Radar,
|
||||
tone: activeBlockers.length > 0 ? "blocked" : "live",
|
||||
},
|
||||
{
|
||||
key: "visibility",
|
||||
label: copy.visibility,
|
||||
value: percentText(visibilityPercent),
|
||||
detail: `${copy.runtime} ${percentText(runtimeAcceptancePercent)} · ${copy.readiness} ${readiness}%`,
|
||||
icon: Gauge,
|
||||
tone: Number(runtimeAcceptancePercent ?? 0) > 0 ? "in_progress" : "blocked",
|
||||
},
|
||||
{
|
||||
key: "registry",
|
||||
label: copy.registry,
|
||||
@@ -9477,23 +9586,64 @@ function SecurityMainlineCockpit({
|
||||
icon: Network,
|
||||
tone: registryExpected > 0 && registryAccepted >= registryExpected ? "live" : "in_progress",
|
||||
},
|
||||
{
|
||||
key: "locator",
|
||||
label: copy.locator,
|
||||
value: `${locatorCandidates}`,
|
||||
detail: `${copy.identity} ${locatorIdentityCandidates} · ${copy.confirmation} ${locatorConfirmationRequired ? "yes" : "no"}`,
|
||||
icon: SearchCheck,
|
||||
tone: locatorCandidates > 0 && locatorConfirmationRequired ? "in_progress" : locatorCandidates > 0 ? "live" : "blocked",
|
||||
},
|
||||
{
|
||||
key: "guard",
|
||||
label: copy.guard,
|
||||
value: guardClear ? "0" : "!",
|
||||
detail: `${copy.noRaw} · ${copy.noWrite} · ${copy.noPower}`,
|
||||
detail: guardClear ? `${copy.noRaw} · ${copy.noWrite}` : `${copy.noWrite} · ${copy.noPower}`,
|
||||
icon: Lock,
|
||||
tone: guardClear ? "live" : "blocked",
|
||||
},
|
||||
] as const;
|
||||
const actionQueue = [
|
||||
{
|
||||
key: "identity",
|
||||
label: copy.identityEvidence,
|
||||
value: `${locatorIdentityCandidates}`,
|
||||
detail: locatorConfirmationRequired ? copy.pending : copy.ready,
|
||||
icon: Fingerprint,
|
||||
tone: locatorConfirmationRequired ? "blocked" : "live",
|
||||
},
|
||||
{
|
||||
key: "dryrun",
|
||||
label: copy.dryRun,
|
||||
value: locatorCandidates > 0 ? `${locatorCandidates}` : "--",
|
||||
detail: securityOperatorLabel(locatorStatus, isZh),
|
||||
icon: SearchCheck,
|
||||
tone: locatorCandidates > 0 ? "in_progress" : "blocked",
|
||||
},
|
||||
{
|
||||
key: "verify",
|
||||
label: copy.verify,
|
||||
value: `${activeBlockers.length}`,
|
||||
detail: activeBlockers.length > 0 ? copy.pending : copy.ready,
|
||||
icon: ListChecks,
|
||||
tone: activeBlockers.length > 0 ? "blocked" : "live",
|
||||
},
|
||||
] as const;
|
||||
const evidenceLanes = [
|
||||
{
|
||||
key: "rag",
|
||||
label: "RAG",
|
||||
count: ragRefs.length,
|
||||
ref: compactEvidenceRef(ragRefs[0]),
|
||||
icon: Database,
|
||||
},
|
||||
{
|
||||
key: "mcp",
|
||||
label: "MCP",
|
||||
count: mcpRefs.length,
|
||||
ref: compactEvidenceRef(mcpRefs[0]),
|
||||
icon: Activity,
|
||||
},
|
||||
{
|
||||
key: "playbook",
|
||||
label: "PlayBook",
|
||||
count: playbookRefs.length,
|
||||
ref: compactEvidenceRef(playbookRefs[0]),
|
||||
icon: ClipboardList,
|
||||
},
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -9501,7 +9651,7 @@ function SecurityMainlineCockpit({
|
||||
data-testid="awooop-security-mainline-cockpit"
|
||||
className="overflow-hidden border border-[#d7ddd8] bg-white"
|
||||
>
|
||||
<div className="grid gap-px bg-[#d7ddd8] lg:grid-cols-[1.15fr_2fr]">
|
||||
<div className="grid gap-px bg-[#d7ddd8] xl:grid-cols-[0.95fr_2.25fr]">
|
||||
<div className="grid gap-4 bg-[#111514] p-4 text-white">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs font-semibold text-[#b8d8c5]">
|
||||
<ShieldCheck className="h-4 w-4" aria-hidden="true" />
|
||||
@@ -9522,13 +9672,13 @@ function SecurityMainlineCockpit({
|
||||
<div className="rounded border border-white/15 bg-white/5 px-3 py-2">
|
||||
<p className="text-[10px] font-semibold text-[#aeb7ad]">{copy.deployLabel}</p>
|
||||
<p className="mt-1 truncate font-mono text-sm font-semibold text-[#b8d8c5]">
|
||||
{shortSha}
|
||||
{deploySha}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded border border-white/15 bg-white/5 px-3 py-2">
|
||||
<p className="text-[10px] font-semibold text-[#aeb7ad]">{copy.locatorLabel}</p>
|
||||
<p className="mt-1 break-words text-xs font-semibold leading-4 text-[#d7c6a9]">
|
||||
{locatorStatus}
|
||||
<p className="text-[10px] font-semibold text-[#aeb7ad]">{copy.blockerLabel}</p>
|
||||
<p className="mt-1 text-xs font-semibold leading-4 text-[#d7c6a9]">
|
||||
{securityOperatorLabel(primaryBlocker, isZh)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -9564,7 +9714,7 @@ function SecurityMainlineCockpit({
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
"break-words font-mono text-2xl font-semibold leading-none",
|
||||
"break-words text-2xl font-semibold leading-none",
|
||||
card.tone === "live" && "text-[#17602a]",
|
||||
card.tone === "in_progress" && "text-[#8a5a08]",
|
||||
card.tone === "blocked" && "text-[#9f2f25]"
|
||||
@@ -9580,16 +9730,84 @@ function SecurityMainlineCockpit({
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 rounded border border-[#d8d3c7] bg-[#faf9f3] p-3 md:grid-cols-[1fr_auto] md:items-center">
|
||||
<div className="grid gap-2 rounded border border-[#d8d3c7] bg-[#faf9f3] p-3 xl:grid-cols-[1.1fr_1.4fr]">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-semibold text-[#77736a]">
|
||||
{copy.next}
|
||||
</p>
|
||||
<p className="mt-1 break-words text-sm font-semibold leading-5 text-[#141413]">
|
||||
{nextAction}
|
||||
{operatorNextAction}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 md:justify-end">
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
{actionQueue.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={cn(
|
||||
"rounded border bg-white px-3 py-2",
|
||||
item.tone === "live" && "border-[#bddfc9]",
|
||||
item.tone === "in_progress" && "border-[#e2c08f]",
|
||||
item.tone === "blocked" && "border-[#e2a29b]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="truncate text-[10px] font-semibold text-[#77736a]">
|
||||
{item.label}
|
||||
</p>
|
||||
<Icon className="h-3.5 w-3.5 text-[#5f5b52]" aria-hidden="true" />
|
||||
</div>
|
||||
<p className="mt-1 text-lg font-semibold leading-none text-[#141413]">
|
||||
{item.value}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-[10px] font-semibold text-[#77736a]">
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 rounded border border-[#d5dbe4] bg-[#f7faff] p-3 xl:grid-cols-[auto_1fr_auto] xl:items-center">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-[#496276]">
|
||||
{copy.evidenceFabric}
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] font-semibold text-[#6a7280]">
|
||||
{copy.externalGap}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
{evidenceLanes.map((lane) => {
|
||||
const Icon = lane.icon;
|
||||
const linked = lane.count > 0;
|
||||
return (
|
||||
<div
|
||||
key={lane.key}
|
||||
className={cn(
|
||||
"rounded border bg-white px-3 py-2",
|
||||
linked ? "border-[#bddfc9]" : "border-[#d8d3c7]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[10px] font-semibold text-[#496276]">
|
||||
{lane.label}
|
||||
</p>
|
||||
<Icon className="h-3.5 w-3.5 text-[#496276]" aria-hidden="true" />
|
||||
</div>
|
||||
<p className="mt-1 text-sm font-semibold text-[#141413]">
|
||||
{linked ? `${lane.count} ${copy.ready}` : copy.pending}
|
||||
</p>
|
||||
<p className="mt-1 truncate font-mono text-[10px] text-[#6a7280]">
|
||||
{lane.ref}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 xl:justify-end">
|
||||
{[
|
||||
[copy.noRaw, !locatorRawPathOutput],
|
||||
[copy.noWrite, hostWriteCount === 0 && !locatorRemoteWrite],
|
||||
|
||||
Reference in New Issue
Block a user