feat(security): expose scanner failures in focused cockpit
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (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 / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
This commit is contained in:
@@ -150,6 +150,28 @@ _SIEM_STAGES = (
|
||||
"verify_learn",
|
||||
)
|
||||
|
||||
_PUBLIC_DISCOVERY_COLLECTOR_IDS = frozenset(
|
||||
{
|
||||
"k8s_replicasets",
|
||||
"k8s_nodes",
|
||||
"k8s_pods",
|
||||
"k8s_deployments",
|
||||
"k8s_statefulsets",
|
||||
"k8s_daemonsets",
|
||||
"k8s_services",
|
||||
"k8s_configmaps",
|
||||
"k8s_persistentvolumeclaims",
|
||||
"k8s_ingresses",
|
||||
"k8s_cronjobs",
|
||||
"prometheus_targets",
|
||||
"database_catalog",
|
||||
"ai_catalog",
|
||||
"gitea_bundle_backup",
|
||||
"gitea_ci",
|
||||
"domain_tls_inventory",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _value(row: Any, name: str, default: Any = None) -> Any:
|
||||
if row is None:
|
||||
@@ -162,6 +184,28 @@ def _value(row: Any, name: str, default: Any = None) -> Any:
|
||||
return getattr(row, name, default)
|
||||
|
||||
|
||||
def _public_discovery_failure(error: Any) -> tuple[str | None, list[str]]:
|
||||
"""Project only fixed collector identities, never raw scanner errors."""
|
||||
if not isinstance(error, str) or not error:
|
||||
return None, []
|
||||
|
||||
marker = "asset_collector_failures="
|
||||
if marker not in error:
|
||||
return "asset_scan_failed_public_details_unavailable", []
|
||||
|
||||
candidates = error.split(marker, 1)[1].split(",")
|
||||
collector_ids = sorted(
|
||||
{
|
||||
candidate.strip()
|
||||
for candidate in candidates
|
||||
if candidate.strip() in _PUBLIC_DISCOVERY_COLLECTOR_IDS
|
||||
}
|
||||
)
|
||||
if not collector_ids:
|
||||
return "asset_scan_failed_public_details_unavailable", []
|
||||
return "asset_collector_failures", collector_ids
|
||||
|
||||
|
||||
def _count(rows: Iterable[Any], key: str, value: str, count_key: str = "cnt") -> int:
|
||||
return sum(
|
||||
int(_value(row, count_key, 0) or 0)
|
||||
@@ -684,8 +728,7 @@ def build_iwooos_security_asset_control_plane(
|
||||
for row in inventory_source
|
||||
)
|
||||
package_digest_unpinned_count = sum(
|
||||
int(_value(row, "unpinned_package_count", 0) or 0)
|
||||
for row in inventory_source
|
||||
int(_value(row, "unpinned_package_count", 0) or 0) for row in inventory_source
|
||||
)
|
||||
certificate_live_unverified_count = sum(
|
||||
int(_value(row, "unverified_certificate_count", 0) or 0)
|
||||
@@ -726,10 +769,14 @@ def build_iwooos_security_asset_control_plane(
|
||||
discovery_fresh = (
|
||||
discovery_age is not None and discovery_age <= _DISCOVERY_FRESHNESS_SECONDS
|
||||
)
|
||||
discovery_status = str(
|
||||
_value(discovery_row, "latest_status", "missing") or "missing"
|
||||
)
|
||||
failure_reason_code, failed_collector_ids = _public_discovery_failure(
|
||||
_value(discovery_row, "latest_error") if discovery_status == "failed" else None
|
||||
)
|
||||
discovery = {
|
||||
"latest_status": str(
|
||||
_value(discovery_row, "latest_status", "missing") or "missing"
|
||||
),
|
||||
"latest_status": discovery_status,
|
||||
"latest_scan_depth": str(
|
||||
_value(discovery_row, "latest_scan_depth", "unknown") or "unknown"
|
||||
),
|
||||
@@ -747,6 +794,10 @@ def build_iwooos_security_asset_control_plane(
|
||||
"age_seconds": discovery_age,
|
||||
"freshness_slo_seconds": _DISCOVERY_FRESHNESS_SECONDS,
|
||||
"fresh": discovery_fresh,
|
||||
"failure_reason_code": failure_reason_code,
|
||||
"failed_collector_count": len(failed_collector_ids),
|
||||
"failed_collector_ids": failed_collector_ids,
|
||||
"raw_error_returned": False,
|
||||
}
|
||||
|
||||
coverage = _rows_by_dimension(coverage_source, _COVERAGE_DIMENSIONS)
|
||||
@@ -897,8 +948,7 @@ def build_iwooos_security_asset_control_plane(
|
||||
github_supply_chain_compliant_percent=_percent(
|
||||
max(
|
||||
0,
|
||||
by_type.get("package", 0)
|
||||
- forbidden_github_supply_chain_asset_count,
|
||||
by_type.get("package", 0) - forbidden_github_supply_chain_asset_count,
|
||||
),
|
||||
by_type.get("package", 0),
|
||||
),
|
||||
@@ -909,8 +959,7 @@ def build_iwooos_security_asset_control_plane(
|
||||
certificate_live_verified_percent=_percent(
|
||||
max(
|
||||
0,
|
||||
by_type.get("certificate", 0)
|
||||
- certificate_live_unverified_count,
|
||||
by_type.get("certificate", 0) - certificate_live_unverified_count,
|
||||
),
|
||||
by_type.get("certificate", 0),
|
||||
),
|
||||
@@ -963,8 +1012,7 @@ def build_iwooos_security_asset_control_plane(
|
||||
)
|
||||
)
|
||||
supply_chain_policy_gap_count = (
|
||||
forbidden_github_supply_chain_asset_count
|
||||
+ package_digest_unpinned_count
|
||||
forbidden_github_supply_chain_asset_count + package_digest_unpinned_count
|
||||
)
|
||||
if supply_chain_policy_gap_count > 0:
|
||||
work_items.append(
|
||||
@@ -1151,9 +1199,7 @@ def build_iwooos_security_asset_control_plane(
|
||||
"forbidden_github_supply_chain_asset_count": (
|
||||
forbidden_github_supply_chain_asset_count
|
||||
),
|
||||
"certificate_live_unverified_count": (
|
||||
certificate_live_unverified_count
|
||||
),
|
||||
"certificate_live_unverified_count": (certificate_live_unverified_count),
|
||||
"certificate_live_unhealthy_count": certificate_live_unhealthy_count,
|
||||
"automation_coverage_green_percent": _percent(
|
||||
coverage_green, coverage_total
|
||||
@@ -1241,13 +1287,11 @@ def build_iwooos_security_asset_control_plane(
|
||||
"certificate_asset_count": by_type.get("certificate", 0),
|
||||
"live_tls_verified_count": max(
|
||||
0,
|
||||
by_type.get("certificate", 0)
|
||||
- certificate_live_unverified_count,
|
||||
by_type.get("certificate", 0) - certificate_live_unverified_count,
|
||||
),
|
||||
"live_tls_evidenced_count": max(
|
||||
0,
|
||||
by_type.get("certificate", 0)
|
||||
- certificate_live_unverified_count,
|
||||
by_type.get("certificate", 0) - certificate_live_unverified_count,
|
||||
),
|
||||
"live_tls_healthy_count": max(
|
||||
0,
|
||||
@@ -1272,6 +1316,7 @@ def build_iwooos_security_asset_control_plane(
|
||||
"aggregate_only": True,
|
||||
"raw_asset_identity_returned": False,
|
||||
"raw_event_payload_returned": False,
|
||||
"raw_discovery_error_returned": False,
|
||||
"secret_value_collection_allowed": False,
|
||||
"live_scan_triggered": False,
|
||||
"runtime_action_triggered": False,
|
||||
@@ -1340,6 +1385,10 @@ def build_unavailable_iwooos_security_asset_control_plane(
|
||||
"age_seconds": None,
|
||||
"freshness_slo_seconds": _DISCOVERY_FRESHNESS_SECONDS,
|
||||
"fresh": False,
|
||||
"failure_reason_code": reason_code,
|
||||
"failed_collector_count": 0,
|
||||
"failed_collector_ids": [],
|
||||
"raw_error_returned": False,
|
||||
},
|
||||
"asset_scopes": [
|
||||
{
|
||||
@@ -1532,6 +1581,7 @@ def build_unavailable_iwooos_security_asset_control_plane(
|
||||
"aggregate_only": True,
|
||||
"raw_asset_identity_returned": False,
|
||||
"raw_event_payload_returned": False,
|
||||
"raw_discovery_error_returned": False,
|
||||
"secret_value_collection_allowed": False,
|
||||
"live_scan_triggered": False,
|
||||
"runtime_action_triggered": False,
|
||||
@@ -1656,11 +1706,12 @@ class IwoooSSecurityAssetControlPlaneService:
|
||||
latest.new_assets AS latest_new_assets,
|
||||
latest.modified_assets AS latest_modified_assets,
|
||||
latest.disappeared_assets AS latest_disappeared_assets,
|
||||
latest.error AS latest_error,
|
||||
successful.ended_at AS latest_success_ended_at
|
||||
FROM (SELECT 1) seed
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT status, scan_depth, started_at, ended_at, total_assets,
|
||||
new_assets, modified_assets, disappeared_assets
|
||||
new_assets, modified_assets, disappeared_assets, error
|
||||
FROM asset_discovery_run
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 1
|
||||
|
||||
@@ -26,10 +26,15 @@ def _row(**values):
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
def _ready_payload(source_health: list[dict] | None = None) -> dict:
|
||||
def _ready_payload(
|
||||
source_health: list[dict] | None = None,
|
||||
*,
|
||||
discovery_status: str = "success",
|
||||
discovery_error: str | None = None,
|
||||
) -> dict:
|
||||
now = datetime(2026, 7, 11, 4, 0, tzinfo=UTC)
|
||||
discovery = _row(
|
||||
latest_status="success",
|
||||
latest_status=discovery_status,
|
||||
latest_scan_depth="full",
|
||||
latest_started_at=now - timedelta(minutes=31),
|
||||
latest_ended_at=now - timedelta(minutes=30),
|
||||
@@ -38,6 +43,7 @@ def _ready_payload(source_health: list[dict] | None = None) -> dict:
|
||||
latest_new_assets=2,
|
||||
latest_modified_assets=1,
|
||||
latest_disappeared_assets=0,
|
||||
latest_error=discovery_error,
|
||||
)
|
||||
inventory = [
|
||||
_row(asset_type="host", cnt=3, unowned_count=1, stale_count=0),
|
||||
@@ -150,6 +156,10 @@ def test_builds_live_asset_siem_audit_and_ai_control_plane_without_false_closure
|
||||
assert payload["completion"]["strict_runtime_closure_percent"] == 0
|
||||
assert payload["completion"]["dimension_count"] == 6
|
||||
assert payload["discovery"]["fresh"] is True
|
||||
assert payload["discovery"]["failure_reason_code"] is None
|
||||
assert payload["discovery"]["failed_collector_count"] == 0
|
||||
assert payload["discovery"]["failed_collector_ids"] == []
|
||||
assert payload["discovery"]["raw_error_returned"] is False
|
||||
assert payload["ai_automation"]["same_run_closed_loop_proven"] is False
|
||||
assert payload["ai_automation"]["same_run_closed_loop_count"] == 0
|
||||
assert len(payload["nist_csf_functions"]) == 6
|
||||
@@ -187,6 +197,45 @@ def test_builds_live_asset_siem_audit_and_ai_control_plane_without_false_closure
|
||||
assert 'secret_value_collection_allowed": true' not in public_text.lower()
|
||||
|
||||
|
||||
def test_discovery_failure_exposes_only_allowlisted_collector_ids() -> None:
|
||||
raw_error = (
|
||||
"RuntimeError: asset_collector_failures=domain_tls_inventory,"
|
||||
"prometheus_targets,postgresql://operator:raw-secret@private-host/runtime"
|
||||
)
|
||||
payload = _ready_payload(
|
||||
discovery_status="failed",
|
||||
discovery_error=raw_error,
|
||||
)
|
||||
|
||||
assert payload["discovery"]["failure_reason_code"] == "asset_collector_failures"
|
||||
assert payload["discovery"]["failed_collector_count"] == 2
|
||||
assert payload["discovery"]["failed_collector_ids"] == [
|
||||
"domain_tls_inventory",
|
||||
"prometheus_targets",
|
||||
]
|
||||
assert payload["discovery"]["raw_error_returned"] is False
|
||||
assert payload["boundaries"]["raw_discovery_error_returned"] is False
|
||||
|
||||
public_text = json.dumps(payload, ensure_ascii=False)
|
||||
assert "raw-secret" not in public_text
|
||||
assert "private-host" not in public_text
|
||||
assert "postgresql://" not in public_text
|
||||
|
||||
|
||||
def test_discovery_failure_fails_closed_for_unknown_raw_error() -> None:
|
||||
payload = _ready_payload(
|
||||
discovery_status="failed",
|
||||
discovery_error="RuntimeError: /private/path leaked provider detail",
|
||||
)
|
||||
|
||||
assert payload["discovery"]["failure_reason_code"] == (
|
||||
"asset_scan_failed_public_details_unavailable"
|
||||
)
|
||||
assert payload["discovery"]["failed_collector_count"] == 0
|
||||
assert payload["discovery"]["failed_collector_ids"] == []
|
||||
assert "/private/path" not in json.dumps(payload)
|
||||
|
||||
|
||||
def test_runtime_image_policy_gaps_are_visible_and_never_false_green() -> None:
|
||||
now = datetime(2026, 7, 11, 4, 0, tzinfo=UTC)
|
||||
payload = build_iwooos_security_asset_control_plane(
|
||||
@@ -398,9 +447,7 @@ def test_unhealthy_live_tls_evidence_remains_a_p0_gap() -> None:
|
||||
domains = {
|
||||
domain["domain_id"]: domain for domain in payload["security_program_domains"]
|
||||
}
|
||||
assert domains["network_dns_tls"]["gap_code"] == (
|
||||
"certificate_live_tls_unhealthy"
|
||||
)
|
||||
assert domains["network_dns_tls"]["gap_code"] == ("certificate_live_tls_unhealthy")
|
||||
|
||||
|
||||
def test_fully_evidenced_healthy_tls_inventory_proves_health() -> None:
|
||||
@@ -438,8 +485,7 @@ def test_fully_evidenced_healthy_tls_inventory_proves_health() -> None:
|
||||
assert payload["certificate_inventory"]["live_tls_unhealthy_count"] == 0
|
||||
assert payload["certificate_inventory"]["live_tls_health_proven"] is True
|
||||
assert all(
|
||||
item["work_item_id"] != "AIA-P0-006-02B"
|
||||
for item in payload["work_items"]
|
||||
item["work_item_id"] != "AIA-P0-006-02B" for item in payload["work_items"]
|
||||
)
|
||||
|
||||
|
||||
@@ -517,6 +563,7 @@ def test_unavailable_payload_is_fixed_degraded_contract_without_raw_error() -> N
|
||||
assert payload["discovery"]["fresh"] is False
|
||||
assert payload["boundaries"]["raw_asset_identity_returned"] is False
|
||||
assert payload["boundaries"]["raw_event_payload_returned"] is False
|
||||
assert payload["boundaries"]["raw_discovery_error_returned"] is False
|
||||
|
||||
|
||||
def test_service_redacts_database_exception_details(monkeypatch) -> None:
|
||||
|
||||
@@ -6826,14 +6826,7 @@ function IwoooSWorkspaceSwitcher({
|
||||
|
||||
return (
|
||||
<section style={{ ...band, padding: 14, marginBottom: 14 }} data-testid="iwooos-workspace-switcher">
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(0, 1.1fr) minmax(0, 2fr)',
|
||||
gap: 14,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-3 sm:grid-cols-[minmax(0,1.1fr)_minmax(0,2fr)] sm:items-center">
|
||||
<div style={{ minWidth: 0 }} title={t('subtitle')}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#315f45', fontSize: 12, fontWeight: 800 }}>
|
||||
<ShieldCheck size={16} color="#2f7d48" />
|
||||
@@ -25126,47 +25119,32 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
|
||||
return (
|
||||
<AppLayout locale={params.locale} fullBleed>
|
||||
<main style={page} data-iwooos-security-ledger-revision="backup-restore-owner-response-acceptance-v1">
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 180px), 1fr))',
|
||||
gap: 14,
|
||||
marginBottom: 14,
|
||||
}}
|
||||
>
|
||||
<div style={{ ...band, padding: 20 }} title={t('subtitle')}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
|
||||
<ShieldCheck size={22} color="#1f7a4d" />
|
||||
<span style={{ fontSize: 12, color: '#6f6d66' }}>{t('eyebrow')}</span>
|
||||
<section className="mb-3 grid grid-cols-1 gap-px overflow-hidden border border-[#d8d3c7] bg-[#d8d3c7] sm:grid-cols-[minmax(0,0.75fr)_minmax(0,1.25fr)]">
|
||||
<div className="flex min-w-0 items-center gap-3 bg-white px-4 py-3" title={t('subtitle')}>
|
||||
<ShieldCheck className="h-5 w-5 shrink-0 text-[#1f7a4d]" aria-hidden="true" />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[11px] text-[#6f6d66]">{t('eyebrow')}</div>
|
||||
<h1 className="mt-1 truncate text-[22px] font-semibold leading-none text-[#141413]">{t('title')}</h1>
|
||||
</div>
|
||||
<h1 style={{ margin: 0, fontSize: 34, lineHeight: 1, color: '#141413' }}>{t('title')}</h1>
|
||||
</div>
|
||||
<div
|
||||
style={{ ...band, padding: 18, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}
|
||||
title={t('boundary.detail')}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6f6d66', fontSize: 12 }}>
|
||||
<Lock size={16} />
|
||||
{t('boundary.label')}
|
||||
<div className="flex min-w-0 items-center gap-3 bg-white px-4 py-3" title={t('boundary.detail')}>
|
||||
<Lock className="h-4 w-4 shrink-0 text-[#6f6d66]" aria-hidden="true" />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[11px] text-[#6f6d66]">{t('boundary.label')}</div>
|
||||
<div className="mt-1 break-words text-[16px] font-semibold leading-tight text-[#141413]">{t('boundary.state')}</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: '#141413', marginTop: 12 }}>{t('boundary.state')}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SecurityAssetControlPlaneCockpit locale={params.locale} className="mb-4" />
|
||||
<IwoooSSecurityToolClosureBoard />
|
||||
<IwoooSWorkspaceSwitcher active={workspaceView} onChange={setWorkspaceView} />
|
||||
<IwoooSManagerCockpit />
|
||||
|
||||
<IwoooSWorkspacePanel active={workspaceView === 'overview'}>
|
||||
<IwoooSCommandRail />
|
||||
<IwoooSProgressIntegrityRibbon />
|
||||
<IwoooSExecutiveSnapshotBoard />
|
||||
<IwoooSRuntimeSecurityReadbackBoard />
|
||||
<IwoooSSecurityControlCoverageBoard />
|
||||
<IwoooSManagerCockpit />
|
||||
</IwoooSWorkspacePanel>
|
||||
|
||||
<IwoooSWorkspacePanel active={workspaceView === 'runtime'}>
|
||||
<IwoooSSecurityToolClosureBoard />
|
||||
<IwoooSCommandRail />
|
||||
<IwoooSAgentBountySecurityOnboardingBoard />
|
||||
<IwoooSRolloutRiskReadOnlyBoard />
|
||||
@@ -25203,6 +25181,8 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
|
||||
</IwoooSWorkspacePanel>
|
||||
|
||||
<IwoooSWorkspacePanel active={workspaceView === 'ledger'}>
|
||||
<IwoooSSecurityToolClosureBoard />
|
||||
<IwoooSManagerCockpit />
|
||||
<IwoooSCommandRail />
|
||||
<IwoooSProgressIntegrityRibbon />
|
||||
<IwoooSExecutiveSnapshotBoard />
|
||||
|
||||
@@ -27,6 +27,9 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
type Copy = {
|
||||
title: string;
|
||||
commandView: string;
|
||||
assetView: string;
|
||||
automationView: string;
|
||||
live: string;
|
||||
degraded: string;
|
||||
overall: string;
|
||||
@@ -64,6 +67,8 @@ type Copy = {
|
||||
stale: string;
|
||||
success: string;
|
||||
failed: string;
|
||||
failedCollectors: string;
|
||||
failedCollectorsUnavailable: string;
|
||||
sameRunMissing: string;
|
||||
unavailable: string;
|
||||
refresh: string;
|
||||
@@ -72,6 +77,9 @@ type Copy = {
|
||||
const COPY: Record<"zh" | "en", Copy> = {
|
||||
zh: {
|
||||
title: "AI 資安控制平面",
|
||||
commandView: "指揮總覽",
|
||||
assetView: "資產與供應鏈",
|
||||
automationView: "SIEM 與 AI",
|
||||
live: "LIVE",
|
||||
degraded: "DEGRADED",
|
||||
overall: "整體完成度",
|
||||
@@ -109,12 +117,17 @@ const COPY: Record<"zh" | "en", Copy> = {
|
||||
stale: "逾期資產",
|
||||
success: "成功",
|
||||
failed: "失敗",
|
||||
failedCollectors: "失敗 Collector",
|
||||
failedCollectorsUnavailable: "失敗原因待安全分類",
|
||||
sameRunMissing: "尚無同 run 閉環證據",
|
||||
unavailable: "Live readback 無法取得",
|
||||
refresh: "重新整理資安控制平面",
|
||||
},
|
||||
en: {
|
||||
title: "AI Security Control Plane",
|
||||
commandView: "Command",
|
||||
assetView: "Assets & supply chain",
|
||||
automationView: "SIEM & AI",
|
||||
live: "LIVE",
|
||||
degraded: "DEGRADED",
|
||||
overall: "Overall completion",
|
||||
@@ -152,12 +165,26 @@ const COPY: Record<"zh" | "en", Copy> = {
|
||||
stale: "Stale assets",
|
||||
success: "Success",
|
||||
failed: "Failed",
|
||||
failedCollectors: "Failed collectors",
|
||||
failedCollectorsUnavailable: "Failure detail awaiting safe classification",
|
||||
sameRunMissing: "No same-run closure evidence",
|
||||
unavailable: "Live readback unavailable",
|
||||
refresh: "Refresh security control plane",
|
||||
},
|
||||
};
|
||||
|
||||
type CockpitView = "command" | "assets" | "automation";
|
||||
|
||||
const COCKPIT_VIEWS: Array<{
|
||||
key: CockpitView;
|
||||
icon: typeof ShieldCheck;
|
||||
label: keyof Pick<Copy, "commandView" | "assetView" | "automationView">;
|
||||
}> = [
|
||||
{ key: "command", icon: Radar, label: "commandView" },
|
||||
{ key: "assets", icon: Boxes, label: "assetView" },
|
||||
{ key: "automation", icon: Bot, label: "automationView" },
|
||||
];
|
||||
|
||||
const TONE_CLASS = {
|
||||
healthy: "border-[#8fc39c] bg-[#f2faf4] text-[#17602a]",
|
||||
warning: "border-[#d9b36f] bg-[#fff8e8] text-[#8a5a08]",
|
||||
@@ -177,7 +204,9 @@ function freshnessLabel(ageSeconds: number | null) {
|
||||
async function fetchControlPlane(): Promise<SecurityAssetControlPlanePayload | null> {
|
||||
const base = getRuntimeApiBaseUrl();
|
||||
const path = "/api/v1/iwooos/security-asset-control-plane";
|
||||
const urls = Array.from(new Set([path, base ? `${base}${path}` : ""].filter(Boolean)));
|
||||
const urls = Array.from(
|
||||
new Set([path, base ? `${base}${path}` : ""].filter(Boolean)),
|
||||
);
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
@@ -230,7 +259,7 @@ function TruthCell({
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 items-center gap-2 bg-white px-3 py-2.5",
|
||||
wideOnMobile && "last:col-span-2 sm:last:col-span-1"
|
||||
wideOnMobile && "last:col-span-2 lg:last:col-span-1",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
@@ -240,12 +269,14 @@ function TruthCell({
|
||||
? "text-[#2f8a50]"
|
||||
: tone === "critical"
|
||||
? "text-[#c9584d]"
|
||||
: "text-[#77736a]"
|
||||
: "text-[#77736a]",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[9px] font-medium text-[#77736a]">{label}</div>
|
||||
<div className="truncate text-[9px] font-medium text-[#77736a]">
|
||||
{label}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 truncate font-mono text-xs font-semibold",
|
||||
@@ -253,7 +284,7 @@ function TruthCell({
|
||||
? "text-[#17602a]"
|
||||
: tone === "critical"
|
||||
? "text-[#9f2f25]"
|
||||
: "text-[#141413]"
|
||||
: "text-[#141413]",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
@@ -273,7 +304,8 @@ function CertificateTruthStrip({
|
||||
const certificate = payload?.certificate_inventory;
|
||||
const assetCount = certificate?.certificate_asset_count ?? 0;
|
||||
const evidencedCount =
|
||||
certificate?.live_tls_evidenced_count ?? certificate?.live_tls_verified_count;
|
||||
certificate?.live_tls_evidenced_count ??
|
||||
certificate?.live_tls_verified_count;
|
||||
const healthyCount = certificate?.live_tls_healthy_count;
|
||||
const healthProven = certificate?.live_tls_health_proven === true;
|
||||
|
||||
@@ -335,7 +367,9 @@ function StageStrip({
|
||||
}) {
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">{title}</div>
|
||||
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">
|
||||
{title}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1 sm:grid-cols-7 lg:grid-cols-8">
|
||||
{stages.map((stage) => {
|
||||
const observed = stage.status === "observed";
|
||||
@@ -346,11 +380,13 @@ function StageStrip({
|
||||
"min-w-0 border px-1.5 py-2 text-center",
|
||||
observed
|
||||
? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"
|
||||
: "border-[#e2a29b] bg-[#fff2f0] text-[#9f2f25]"
|
||||
: "border-[#e2a29b] bg-[#fff2f0] text-[#9f2f25]",
|
||||
)}
|
||||
title={`${stage.label}: ${stageCount(stage)}`}
|
||||
>
|
||||
<div className="truncate text-[10px] font-semibold">{stage.label}</div>
|
||||
<div className="truncate text-[10px] font-semibold">
|
||||
{stage.label}
|
||||
</div>
|
||||
<div className="mt-1 font-mono text-xs">{stageCount(stage)}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -368,8 +404,10 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
className?: string;
|
||||
}) {
|
||||
const copy = COPY[locale.toLowerCase().startsWith("zh") ? "zh" : "en"];
|
||||
const [payload, setPayload] = useState<SecurityAssetControlPlanePayload | null>(null);
|
||||
const [payload, setPayload] =
|
||||
useState<SecurityAssetControlPlanePayload | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [view, setView] = useState<CockpitView>("command");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -387,13 +425,16 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
const tone = payload ? securityControlPlaneTone(payload) : "critical";
|
||||
const priorityItems = useMemo(
|
||||
() => (payload ? topSecurityControlPlaneWorkItems(payload, 3) : []),
|
||||
[payload]
|
||||
[payload],
|
||||
);
|
||||
|
||||
if (!payload && !loading) {
|
||||
return (
|
||||
<section
|
||||
className={cn("border border-[#e2a29b] bg-[#fff2f0] px-4 py-4", className)}
|
||||
className={cn(
|
||||
"border border-[#e2a29b] bg-[#fff2f0] px-4 py-4",
|
||||
className,
|
||||
)}
|
||||
data-testid="security-asset-control-plane-cockpit"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-[#9f2f25]">
|
||||
@@ -408,19 +449,24 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
<section
|
||||
className={cn(
|
||||
"overflow-hidden border border-[#d8d3c7] bg-[#f6f4ee]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
data-testid="security-asset-control-plane-cockpit"
|
||||
aria-busy={loading}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#d8d3c7] bg-[#141413] px-4 py-3 text-white">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<ShieldCheck className="h-5 w-5 shrink-0 text-[#7fd29b]" aria-hidden="true" />
|
||||
<h2 className="truncate text-sm font-semibold tracking-normal">{copy.title}</h2>
|
||||
<ShieldCheck
|
||||
className="h-5 w-5 shrink-0 text-[#7fd29b]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2 className="truncate text-sm font-semibold tracking-normal">
|
||||
{copy.title}
|
||||
</h2>
|
||||
<span
|
||||
className={cn(
|
||||
"border px-2 py-0.5 font-mono text-[10px] font-semibold",
|
||||
TONE_CLASS[tone]
|
||||
TONE_CLASS[tone],
|
||||
)}
|
||||
>
|
||||
{payload?.status === "ready" ? copy.live : copy.degraded}
|
||||
@@ -433,167 +479,334 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
aria-label={copy.refresh}
|
||||
title={copy.refresh}
|
||||
>
|
||||
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} aria-hidden="true" />
|
||||
<RefreshCw
|
||||
className={cn("h-4 w-4", loading && "animate-spin")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CertificateTruthStrip copy={copy} payload={payload} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-px bg-[#d8d3c7] sm:grid-cols-3 lg:grid-cols-6">
|
||||
<MetricCell
|
||||
label={copy.overall}
|
||||
value={payload ? `${payload.summary.overall_security_completion_percent}%` : "--"}
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.assetTypes}
|
||||
value={payload ? `${payload.summary.asset_type_coverage_percent}%` : "--"}
|
||||
icon={Activity}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.nist}
|
||||
value={payload ? `${payload.summary.nist_controlled_percent}%` : "--"}
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.siem}
|
||||
value={payload ? `${payload.summary.siem_observed_stage_count}/${payload.summary.siem_stage_count}` : "--"}
|
||||
icon={Radar}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.aiLoop}
|
||||
value={payload ? `${payload.summary.ai_loop_observed_stage_count}/${payload.summary.ai_loop_stage_count}` : "--"}
|
||||
icon={Bot}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.verified}
|
||||
value={payload?.summary.verified_remediation_receipt_count_24h ?? "--"}
|
||||
icon={CheckCircle2}
|
||||
/>
|
||||
</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"
|
||||
className="grid grid-cols-3 gap-px border-b border-[#d8d3c7] bg-[#d8d3c7]"
|
||||
role="tablist"
|
||||
aria-label={copy.title}
|
||||
>
|
||||
<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"}
|
||||
wideOnMobile
|
||||
/>
|
||||
{COCKPIT_VIEWS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const selected = view === item.key;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={selected}
|
||||
onClick={() => setView(item.key)}
|
||||
className={cn(
|
||||
"flex min-h-11 min-w-0 items-center justify-center gap-2 px-2 py-2 text-[11px] font-semibold",
|
||||
selected
|
||||
? "bg-white text-[#17602a]"
|
||||
: "bg-[#eeece5] text-[#68655d] hover:bg-[#f8f7f3]",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
||||
<span className="truncate">{copy[item.label]}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[#d8d3c7] bg-[#f6f4ee] px-3 py-2">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-[10px] font-semibold text-[#4f4d47]">
|
||||
<Boxes className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{copy.supplyChain}
|
||||
</div>
|
||||
<div
|
||||
className="grid grid-cols-2 gap-px bg-[#d8d3c7] sm:grid-cols-4"
|
||||
data-testid="security-supply-chain-truth"
|
||||
>
|
||||
<TruthCell
|
||||
label={copy.runtimeImages}
|
||||
value={payload?.supply_chain?.package_asset_count ?? "--"}
|
||||
icon={Boxes}
|
||||
tone={
|
||||
payload?.supply_chain?.package_inventory_evidenced
|
||||
? "healthy"
|
||||
: "critical"
|
||||
}
|
||||
/>
|
||||
<TruthCell
|
||||
label={copy.digestPinned}
|
||||
{view === "assets" ? (
|
||||
<CertificateTruthStrip copy={copy} payload={payload} />
|
||||
) : null}
|
||||
|
||||
{view === "command" ? (
|
||||
<div className="grid grid-cols-3 gap-px bg-[#d8d3c7] lg:grid-cols-6">
|
||||
<MetricCell
|
||||
label={copy.overall}
|
||||
value={
|
||||
payload?.supply_chain
|
||||
? `${payload.supply_chain.package_digest_pinned_count}/${payload.supply_chain.package_asset_count}`
|
||||
payload
|
||||
? `${payload.summary.overall_security_completion_percent}%`
|
||||
: "--"
|
||||
}
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.assetTypes}
|
||||
value={
|
||||
payload ? `${payload.summary.asset_type_coverage_percent}%` : "--"
|
||||
}
|
||||
icon={Activity}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.nist}
|
||||
value={
|
||||
payload ? `${payload.summary.nist_controlled_percent}%` : "--"
|
||||
}
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.siem}
|
||||
value={
|
||||
payload
|
||||
? `${payload.summary.siem_observed_stage_count}/${payload.summary.siem_stage_count}`
|
||||
: "--"
|
||||
}
|
||||
icon={Radar}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.aiLoop}
|
||||
value={
|
||||
payload
|
||||
? `${payload.summary.ai_loop_observed_stage_count}/${payload.summary.ai_loop_stage_count}`
|
||||
: "--"
|
||||
}
|
||||
icon={Bot}
|
||||
/>
|
||||
<MetricCell
|
||||
label={copy.verified}
|
||||
value={
|
||||
payload?.summary.verified_remediation_receipt_count_24h ?? "--"
|
||||
}
|
||||
icon={CheckCircle2}
|
||||
tone={
|
||||
payload?.supply_chain?.package_inventory_evidenced &&
|
||||
payload.supply_chain.package_digest_unpinned_count === 0
|
||||
? "healthy"
|
||||
: "critical"
|
||||
}
|
||||
/>
|
||||
<TruthCell
|
||||
label={copy.forbiddenSources}
|
||||
value={
|
||||
payload?.supply_chain?.forbidden_github_supply_chain_asset_count ??
|
||||
"--"
|
||||
}
|
||||
icon={ShieldAlert}
|
||||
tone={
|
||||
payload?.supply_chain?.package_inventory_evidenced &&
|
||||
payload.supply_chain.forbidden_github_supply_chain_asset_count === 0
|
||||
? "healthy"
|
||||
: "critical"
|
||||
}
|
||||
/>
|
||||
<TruthCell
|
||||
label={copy.imagePolicy}
|
||||
value={
|
||||
!payload?.supply_chain?.package_inventory_evidenced
|
||||
? copy.notEvidenced
|
||||
: payload.supply_chain.runtime_image_policy_compliant
|
||||
? copy.compliant
|
||||
: copy.repair
|
||||
}
|
||||
icon={
|
||||
payload?.supply_chain?.runtime_image_policy_compliant
|
||||
? ShieldCheck
|
||||
: TriangleAlert
|
||||
}
|
||||
tone={
|
||||
payload?.supply_chain?.runtime_image_policy_compliant
|
||||
? "healthy"
|
||||
: "critical"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{view === "command" ? (
|
||||
<div
|
||||
className="grid grid-cols-3 gap-px border-t border-[#d8d3c7] bg-[#d8d3c7] lg: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"
|
||||
}
|
||||
wideOnMobile
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{view === "assets" ? (
|
||||
<div className="border-t border-[#d8d3c7] bg-[#f6f4ee] px-3 py-2">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-[10px] font-semibold text-[#4f4d47]">
|
||||
<Boxes className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{copy.supplyChain}
|
||||
</div>
|
||||
<div
|
||||
className="grid grid-cols-2 gap-px bg-[#d8d3c7] sm:grid-cols-4"
|
||||
data-testid="security-supply-chain-truth"
|
||||
>
|
||||
<TruthCell
|
||||
label={copy.runtimeImages}
|
||||
value={payload?.supply_chain?.package_asset_count ?? "--"}
|
||||
icon={Boxes}
|
||||
tone={
|
||||
payload?.supply_chain?.package_inventory_evidenced
|
||||
? "healthy"
|
||||
: "critical"
|
||||
}
|
||||
/>
|
||||
<TruthCell
|
||||
label={copy.digestPinned}
|
||||
value={
|
||||
payload?.supply_chain
|
||||
? `${payload.supply_chain.package_digest_pinned_count}/${payload.supply_chain.package_asset_count}`
|
||||
: "--"
|
||||
}
|
||||
icon={CheckCircle2}
|
||||
tone={
|
||||
payload?.supply_chain?.package_inventory_evidenced &&
|
||||
payload.supply_chain.package_digest_unpinned_count === 0
|
||||
? "healthy"
|
||||
: "critical"
|
||||
}
|
||||
/>
|
||||
<TruthCell
|
||||
label={copy.forbiddenSources}
|
||||
value={
|
||||
payload?.supply_chain
|
||||
?.forbidden_github_supply_chain_asset_count ?? "--"
|
||||
}
|
||||
icon={ShieldAlert}
|
||||
tone={
|
||||
payload?.supply_chain?.package_inventory_evidenced &&
|
||||
payload.supply_chain
|
||||
.forbidden_github_supply_chain_asset_count === 0
|
||||
? "healthy"
|
||||
: "critical"
|
||||
}
|
||||
/>
|
||||
<TruthCell
|
||||
label={copy.imagePolicy}
|
||||
value={
|
||||
!payload?.supply_chain?.package_inventory_evidenced
|
||||
? copy.notEvidenced
|
||||
: payload.supply_chain.runtime_image_policy_compliant
|
||||
? copy.compliant
|
||||
: copy.repair
|
||||
}
|
||||
icon={
|
||||
payload?.supply_chain?.runtime_image_policy_compliant
|
||||
? ShieldCheck
|
||||
: TriangleAlert
|
||||
}
|
||||
tone={
|
||||
payload?.supply_chain?.runtime_image_policy_compliant
|
||||
? "healthy"
|
||||
: "critical"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{payload ? (
|
||||
<div className="grid gap-4 p-4 xl:grid-cols-[1.1fr_1.4fr]">
|
||||
<div className="min-w-0 space-y-4">
|
||||
<div className="p-4">
|
||||
{view === "command" ? (
|
||||
<div
|
||||
className="grid gap-3 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.6fr)]"
|
||||
data-testid="security-command-attention"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 border p-3",
|
||||
payload.discovery.latest_status === "success"
|
||||
? "border-[#9bc7a4] bg-[#f0faf2]"
|
||||
: "border-[#e2a29b] bg-[#fff2f0]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold text-[#4f4d47]">
|
||||
{payload.discovery.latest_status === "success" ? (
|
||||
<CheckCircle2
|
||||
className="h-4 w-4 text-[#2f8a50]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<TriangleAlert
|
||||
className="h-4 w-4 text-[#c9584d]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{copy.failedCollectors}
|
||||
<span className="ml-auto font-mono text-xs text-[#141413]">
|
||||
{payload.discovery.failed_collector_count}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex min-h-6 flex-wrap gap-1.5">
|
||||
{payload.discovery.failed_collector_ids.length > 0 ? (
|
||||
payload.discovery.failed_collector_ids.map(
|
||||
(collectorId) => (
|
||||
<span
|
||||
key={collectorId}
|
||||
className="max-w-full truncate border border-[#e2a29b] bg-white px-2 py-1 font-mono text-[10px] text-[#9f2f25]"
|
||||
title={collectorId}
|
||||
>
|
||||
{collectorId}
|
||||
</span>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<span className="text-[10px] text-[#6f6d66]">
|
||||
{payload.discovery.latest_status === "success"
|
||||
? copy.success
|
||||
: copy.failedCollectorsUnavailable}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 border border-[#d8d3c7] bg-white p-3">
|
||||
<div className="mb-2 flex items-center justify-between gap-2 text-[11px] font-semibold text-[#4f4d47]">
|
||||
<span>{copy.priority}</span>
|
||||
<span className="font-mono text-[#77736a]">
|
||||
{priorityItems.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-1.5 sm:grid-cols-3">
|
||||
{priorityItems.map((item) => (
|
||||
<div
|
||||
key={item.work_item_id}
|
||||
className="flex min-w-0 items-center gap-2 border border-[#ebe8df] px-2 py-2"
|
||||
>
|
||||
<span className="border border-[#e2a29b] bg-[#fff2f0] px-1.5 py-0.5 font-mono text-[9px] font-semibold text-[#9f2f25]">
|
||||
{item.priority}
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-[10px] font-medium text-[#34332f]"
|
||||
title={item.title}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="font-mono text-[10px] text-[#77736a]">
|
||||
{item.gap_count}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={cn("min-w-0 space-y-4", view !== "assets" && "hidden")}
|
||||
>
|
||||
<div>
|
||||
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">{copy.functions}</div>
|
||||
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">
|
||||
{copy.functions}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1 sm:grid-cols-6">
|
||||
{payload.nist_csf_functions.map((item) => (
|
||||
<div key={item.function_id} className="border border-[#d8d3c7] bg-white px-2 py-2">
|
||||
<div className="truncate text-[10px] text-[#6f6d66]">{item.label}</div>
|
||||
<div
|
||||
key={item.function_id}
|
||||
className="border border-[#d8d3c7] bg-white px-2 py-2"
|
||||
>
|
||||
<div className="truncate text-[10px] text-[#6f6d66]">
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="mt-1 font-mono text-sm font-semibold text-[#141413]">
|
||||
{item.controlled_percent}%
|
||||
</div>
|
||||
@@ -605,9 +818,11 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
? "bg-[#2f8a50]"
|
||||
: item.controlled_percent > 0
|
||||
? "bg-[#c58a2e]"
|
||||
: "bg-[#c9584d]"
|
||||
: "bg-[#c9584d]",
|
||||
)}
|
||||
style={{ width: `${Math.min(100, Math.max(0, item.controlled_percent))}%` }}
|
||||
style={{
|
||||
width: `${Math.min(100, Math.max(0, item.controlled_percent))}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -638,7 +853,7 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
? "bg-[#2f8a50]"
|
||||
: domain.status === "partial"
|
||||
? "bg-[#c58a2e]"
|
||||
: "bg-[#c9584d]"
|
||||
: "bg-[#c9584d]",
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate text-[9px] text-[#4f4d47]">
|
||||
@@ -657,13 +872,19 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
<div className="mb-2 flex items-center justify-between gap-3 text-[11px] font-semibold text-[#4f4d47]">
|
||||
<span>{copy.scope}</span>
|
||||
<span className="font-mono text-[#77736a]">
|
||||
{payload.summary.controlled_scope_count}/{payload.summary.scope_count}
|
||||
{payload.summary.controlled_scope_count}/
|
||||
{payload.summary.scope_count}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-1 sm:grid-cols-2">
|
||||
{payload.asset_scopes.map((scope) => (
|
||||
<div key={scope.scope_id} className="flex items-center gap-2 border border-[#d8d3c7] bg-white px-2 py-2">
|
||||
<span className="min-w-0 flex-1 truncate text-[10px] text-[#4f4d47]">{scope.label}</span>
|
||||
<div
|
||||
key={scope.scope_id}
|
||||
className="flex items-center gap-2 border border-[#d8d3c7] bg-white px-2 py-2"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate text-[10px] text-[#4f4d47]">
|
||||
{scope.label}
|
||||
</span>
|
||||
<span className="font-mono text-[11px] font-semibold text-[#141413]">
|
||||
{scope.managed_asset_count}
|
||||
</span>
|
||||
@@ -674,7 +895,7 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
? "bg-[#2f8a50]"
|
||||
: scope.status === "partial"
|
||||
? "bg-[#c58a2e]"
|
||||
: "bg-[#c9584d]"
|
||||
: "bg-[#c9584d]",
|
||||
)}
|
||||
title={`${scope.type_coverage_percent}%`}
|
||||
/>
|
||||
@@ -684,13 +905,23 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-4">
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 space-y-4",
|
||||
view !== "automation" && "hidden",
|
||||
)}
|
||||
>
|
||||
<StageStrip title={copy.siemFlow} stages={payload.siem.pipeline} />
|
||||
<StageStrip title={copy.aiFlow} stages={payload.ai_automation.stages} />
|
||||
<StageStrip
|
||||
title={copy.aiFlow}
|
||||
stages={payload.ai_automation.stages}
|
||||
/>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-[1fr_1.35fr]">
|
||||
<div className="border border-[#d8d3c7] bg-white p-3">
|
||||
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">{copy.audit}</div>
|
||||
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">
|
||||
{copy.audit}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2 text-center">
|
||||
<div>
|
||||
<div className="font-mono text-sm font-semibold text-[#141413]">
|
||||
@@ -708,13 +939,18 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
<div className="font-mono text-sm font-semibold text-[#141413]">
|
||||
{payload.security_audit.ai_trace_count_24h}
|
||||
</div>
|
||||
<div className="mt-1 text-[9px] text-[#77736a]">{copy.aiTrace}</div>
|
||||
<div className="mt-1 text-[9px] text-[#77736a]">
|
||||
{copy.aiTrace}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-mono text-sm font-semibold text-[#141413]">
|
||||
{payload.summary.ready_source_count}/{payload.summary.source_count}
|
||||
{payload.summary.ready_source_count}/
|
||||
{payload.summary.source_count}
|
||||
</div>
|
||||
<div className="mt-1 text-[9px] text-[#77736a]">
|
||||
{copy.sources}
|
||||
</div>
|
||||
<div className="mt-1 text-[9px] text-[#77736a]">{copy.sources}</div>
|
||||
</div>
|
||||
</div>
|
||||
{payload.summary.unavailable_source_count > 0 ? (
|
||||
@@ -728,32 +964,50 @@ export function SecurityAssetControlPlaneCockpit({
|
||||
className="inline-flex min-w-0 items-center gap-1 border border-[#e2a29b] bg-[#fff2f0] px-1.5 py-0.5 font-mono text-[9px] text-[#9f2f25]"
|
||||
title={source.reason_code ?? source.source_id}
|
||||
>
|
||||
<Database className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
<span className="max-w-32 truncate">{source.source_id}</span>
|
||||
<Database
|
||||
className="h-3 w-3 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="max-w-32 truncate">
|
||||
{source.source_id}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{!payload.ai_automation.same_run_closed_loop_proven ? (
|
||||
<div className="mt-3 flex items-center gap-1.5 border-t border-[#ebe8df] pt-2 text-[10px] font-medium text-[#9f2f25]">
|
||||
<TriangleAlert className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
||||
<TriangleAlert
|
||||
className="h-3.5 w-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{copy.sameRunMissing}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#d8d3c7] bg-white p-3">
|
||||
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">{copy.priority}</div>
|
||||
<div className="mb-2 text-[11px] font-semibold text-[#4f4d47]">
|
||||
{copy.priority}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{priorityItems.map((item) => (
|
||||
<div key={item.work_item_id} className="flex min-w-0 items-center gap-2">
|
||||
<div
|
||||
key={item.work_item_id}
|
||||
className="flex min-w-0 items-center gap-2"
|
||||
>
|
||||
<span className="border border-[#e2a29b] bg-[#fff2f0] px-1.5 py-0.5 font-mono text-[9px] font-semibold text-[#9f2f25]">
|
||||
{item.priority}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-[10px] font-medium text-[#34332f]" title={item.title}>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-[10px] font-medium text-[#34332f]"
|
||||
title={item.title}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="font-mono text-[10px] text-[#77736a]">{item.gap_count}</span>
|
||||
<span className="font-mono text-[10px] text-[#77736a]">
|
||||
{item.gap_count}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,10 @@ function payload(): SecurityAssetControlPlanePayload {
|
||||
new_assets: 2,
|
||||
modified_assets: 22,
|
||||
disappeared_assets: 1,
|
||||
failure_reason_code: null,
|
||||
failed_collector_count: 0,
|
||||
failed_collector_ids: [],
|
||||
raw_error_returned: false,
|
||||
},
|
||||
asset_scopes: [],
|
||||
nist_csf_functions: [],
|
||||
@@ -77,7 +81,10 @@ function payload(): SecurityAssetControlPlanePayload {
|
||||
{ dimension_id: "security_program", completion_percent: 68 },
|
||||
{ dimension_id: "siem_pipeline", completion_percent: 100 },
|
||||
{ dimension_id: "ai_loop_stages", completion_percent: 100 },
|
||||
{ dimension_id: "strict_same_run_runtime_closure", completion_percent: 0 },
|
||||
{
|
||||
dimension_id: "strict_same_run_runtime_closure",
|
||||
completion_percent: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
siem: { pipeline: [] },
|
||||
@@ -154,6 +161,7 @@ function payload(): SecurityAssetControlPlanePayload {
|
||||
aggregate_only: true,
|
||||
raw_asset_identity_returned: false,
|
||||
raw_event_payload_returned: false,
|
||||
raw_discovery_error_returned: false,
|
||||
secret_value_collection_allowed: false,
|
||||
live_scan_triggered: false,
|
||||
runtime_action_triggered: false,
|
||||
@@ -189,7 +197,31 @@ describe("security asset control plane projection", () => {
|
||||
private_key_material_read: true,
|
||||
},
|
||||
};
|
||||
expect(isSecurityAssetControlPlanePayload(unsafeCertificateInventory)).toBe(false);
|
||||
expect(isSecurityAssetControlPlanePayload(unsafeCertificateInventory)).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
const unsafeDiscovery = {
|
||||
...safe,
|
||||
discovery: {
|
||||
...safe.discovery,
|
||||
raw_error_returned: true,
|
||||
raw_error: "private runtime detail",
|
||||
},
|
||||
};
|
||||
expect(isSecurityAssetControlPlanePayload(unsafeDiscovery)).toBe(false);
|
||||
|
||||
const unsafeCollectorIdentity = {
|
||||
...safe,
|
||||
discovery: {
|
||||
...safe.discovery,
|
||||
failed_collector_count: 1,
|
||||
failed_collector_ids: ["postgresql://operator@private-host/runtime"],
|
||||
},
|
||||
};
|
||||
expect(isSecurityAssetControlPlanePayload(unsafeCollectorIdentity)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show healthy before same-run closure is proven", () => {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
export type SecurityControlStatus = "ready" | "degraded";
|
||||
|
||||
const PUBLIC_DISCOVERY_COLLECTOR_IDS = new Set([
|
||||
"k8s_replicasets",
|
||||
"k8s_nodes",
|
||||
"k8s_pods",
|
||||
"k8s_deployments",
|
||||
"k8s_statefulsets",
|
||||
"k8s_daemonsets",
|
||||
"k8s_services",
|
||||
"k8s_configmaps",
|
||||
"k8s_persistentvolumeclaims",
|
||||
"k8s_ingresses",
|
||||
"k8s_cronjobs",
|
||||
"prometheus_targets",
|
||||
"database_catalog",
|
||||
"ai_catalog",
|
||||
"gitea_bundle_backup",
|
||||
"gitea_ci",
|
||||
"domain_tls_inventory",
|
||||
]);
|
||||
|
||||
export type SecurityControlPlaneSummary = {
|
||||
source_count: number;
|
||||
ready_source_count: number;
|
||||
@@ -119,6 +139,10 @@ export type SecurityAssetControlPlanePayload = {
|
||||
new_assets: number;
|
||||
modified_assets: number;
|
||||
disappeared_assets: number;
|
||||
failure_reason_code: string | null;
|
||||
failed_collector_count: number;
|
||||
failed_collector_ids: string[];
|
||||
raw_error_returned: false;
|
||||
};
|
||||
asset_scopes: SecurityControlPlaneScope[];
|
||||
nist_csf_functions: SecurityControlPlaneFunction[];
|
||||
@@ -182,6 +206,7 @@ export type SecurityAssetControlPlanePayload = {
|
||||
aggregate_only: boolean;
|
||||
raw_asset_identity_returned: boolean;
|
||||
raw_event_payload_returned: boolean;
|
||||
raw_discovery_error_returned: boolean;
|
||||
secret_value_collection_allowed: boolean;
|
||||
live_scan_triggered: boolean;
|
||||
runtime_action_triggered: boolean;
|
||||
@@ -194,11 +219,27 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
}
|
||||
|
||||
export function isSecurityAssetControlPlanePayload(
|
||||
value: unknown
|
||||
value: unknown,
|
||||
): value is SecurityAssetControlPlanePayload {
|
||||
if (!isRecord(value)) return false;
|
||||
if (value.schema_version !== "iwooos_security_asset_control_plane_v1") return false;
|
||||
if (value.schema_version !== "iwooos_security_asset_control_plane_v1")
|
||||
return false;
|
||||
if (!isRecord(value.summary) || !isRecord(value.boundaries)) return false;
|
||||
if (
|
||||
!isRecord(value.discovery) ||
|
||||
!Array.isArray(value.discovery.failed_collector_ids) ||
|
||||
!value.discovery.failed_collector_ids.every(
|
||||
(collectorId) =>
|
||||
typeof collectorId === "string" &&
|
||||
PUBLIC_DISCOVERY_COLLECTOR_IDS.has(collectorId),
|
||||
) ||
|
||||
typeof value.discovery.failed_collector_count !== "number" ||
|
||||
value.discovery.failed_collector_count !==
|
||||
value.discovery.failed_collector_ids.length ||
|
||||
value.discovery.raw_error_returned !== false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!Array.isArray(value.asset_scopes) ||
|
||||
!Array.isArray(value.nist_csf_functions) ||
|
||||
@@ -206,8 +247,12 @@ export function isSecurityAssetControlPlanePayload(
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!isRecord(value.siem) || !Array.isArray(value.siem.pipeline)) return false;
|
||||
if (!isRecord(value.ai_automation) || !Array.isArray(value.ai_automation.stages)) {
|
||||
if (!isRecord(value.siem) || !Array.isArray(value.siem.pipeline))
|
||||
return false;
|
||||
if (
|
||||
!isRecord(value.ai_automation) ||
|
||||
!Array.isArray(value.ai_automation.stages)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (value.supply_chain !== undefined) {
|
||||
@@ -234,12 +279,16 @@ export function isSecurityAssetControlPlanePayload(
|
||||
typeof value.certificate_inventory.certificate_asset_count !== "number" ||
|
||||
typeof value.certificate_inventory.live_tls_verified_count !== "number" ||
|
||||
(value.certificate_inventory.live_tls_evidenced_count !== undefined &&
|
||||
typeof value.certificate_inventory.live_tls_evidenced_count !== "number") ||
|
||||
typeof value.certificate_inventory.live_tls_evidenced_count !==
|
||||
"number") ||
|
||||
(value.certificate_inventory.live_tls_healthy_count !== undefined &&
|
||||
typeof value.certificate_inventory.live_tls_healthy_count !== "number") ||
|
||||
typeof value.certificate_inventory.live_tls_unverified_count !== "number" ||
|
||||
typeof value.certificate_inventory.live_tls_healthy_count !==
|
||||
"number") ||
|
||||
typeof value.certificate_inventory.live_tls_unverified_count !==
|
||||
"number" ||
|
||||
(value.certificate_inventory.live_tls_unhealthy_count !== undefined &&
|
||||
typeof value.certificate_inventory.live_tls_unhealthy_count !== "number") ||
|
||||
typeof value.certificate_inventory.live_tls_unhealthy_count !==
|
||||
"number") ||
|
||||
typeof value.certificate_inventory.live_tls_health_proven !== "boolean" ||
|
||||
value.certificate_inventory.raw_certificate_path_returned !== false ||
|
||||
value.certificate_inventory.certificate_material_read !== false ||
|
||||
@@ -260,6 +309,7 @@ export function isSecurityAssetControlPlanePayload(
|
||||
value.boundaries.aggregate_only === true &&
|
||||
value.boundaries.raw_asset_identity_returned === false &&
|
||||
value.boundaries.raw_event_payload_returned === false &&
|
||||
value.boundaries.raw_discovery_error_returned === false &&
|
||||
value.boundaries.secret_value_collection_allowed === false &&
|
||||
value.boundaries.live_scan_triggered === false &&
|
||||
value.boundaries.runtime_action_triggered === false
|
||||
@@ -269,7 +319,7 @@ export function isSecurityAssetControlPlanePayload(
|
||||
export type SecurityControlPlaneTone = "healthy" | "warning" | "critical";
|
||||
|
||||
export function securityControlPlaneTone(
|
||||
payload: SecurityAssetControlPlanePayload
|
||||
payload: SecurityAssetControlPlanePayload,
|
||||
): SecurityControlPlaneTone {
|
||||
if (payload.status !== "ready" || !payload.discovery.fresh) return "critical";
|
||||
if (
|
||||
@@ -312,14 +362,14 @@ export function securityControlPlaneTone(
|
||||
|
||||
export function topSecurityControlPlaneWorkItems(
|
||||
payload: SecurityAssetControlPlanePayload,
|
||||
limit = 3
|
||||
limit = 3,
|
||||
): SecurityControlPlaneWorkItem[] {
|
||||
const rank: Record<string, number> = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
||||
return [...payload.work_items]
|
||||
.sort(
|
||||
(left, right) =>
|
||||
(rank[left.priority] ?? 99) - (rank[right.priority] ?? 99) ||
|
||||
left.work_item_id.localeCompare(right.work_item_id)
|
||||
left.work_item_id.localeCompare(right.work_item_id),
|
||||
)
|
||||
.slice(0, Math.max(0, limit));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user