@skyhook-io/radar-app 1.13.4 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,292 @@
1
+ import { displayKind } from "@skyhook-io/k8s-ui";
2
+ import { apiVersionToGroup } from "../../../../utils/navigation";
3
+ import {
4
+ ProjectionBuilder,
5
+ evidenceTierForRelevance,
6
+ invalidPayload,
7
+ } from "../observations";
8
+ import {
9
+ nonEmptyString,
10
+ nonNegativeInteger,
11
+ record,
12
+ stringArray,
13
+ } from "../parse";
14
+ import {
15
+ type InvestigationAccessCheck,
16
+ type InvestigationEvidenceObservation,
17
+ type InvestigationEvidenceRelevance,
18
+ type InvestigationEvidenceSource,
19
+ type InvestigationKubernetesResource,
20
+ type InvestigationPermissionBinding,
21
+ type InvestigationPermissionSubject,
22
+ } from "../types";
23
+
24
+ function permissionSubject(
25
+ value: unknown,
26
+ ): InvestigationPermissionSubject | undefined {
27
+ const candidate = record(value);
28
+ if (
29
+ !candidate ||
30
+ !nonEmptyString(candidate.kind) ||
31
+ !nonEmptyString(candidate.name) ||
32
+ (candidate.namespace !== undefined &&
33
+ typeof candidate.namespace !== "string")
34
+ ) {
35
+ return undefined;
36
+ }
37
+ return {
38
+ kind: candidate.kind,
39
+ name: candidate.name,
40
+ ...(nonEmptyString(candidate.namespace)
41
+ ? { namespace: candidate.namespace }
42
+ : {}),
43
+ };
44
+ }
45
+ export function accessCheck(
46
+ value: unknown,
47
+ ): InvestigationAccessCheck | undefined {
48
+ const candidate = record(value);
49
+ if (
50
+ !candidate ||
51
+ !nonEmptyString(candidate.verb) ||
52
+ !nonEmptyString(candidate.resource) ||
53
+ typeof candidate.namespace !== "string" ||
54
+ typeof candidate.allowed !== "boolean" ||
55
+ (candidate.denied !== undefined && typeof candidate.denied !== "boolean")
56
+ ) {
57
+ return undefined;
58
+ }
59
+ for (const field of [
60
+ "group",
61
+ "subresource",
62
+ "resourceName",
63
+ "reason",
64
+ "evaluationError",
65
+ ] as const) {
66
+ if (candidate[field] !== undefined && typeof candidate[field] !== "string")
67
+ return undefined;
68
+ }
69
+ return {
70
+ ...(candidate as unknown as InvestigationAccessCheck),
71
+ denied: candidate.denied === true,
72
+ };
73
+ }
74
+ function permissionBinding(
75
+ value: unknown,
76
+ ): InvestigationPermissionBinding | undefined {
77
+ const candidate = record(value);
78
+ if (
79
+ !candidate ||
80
+ !nonEmptyString(candidate.bindingKind) ||
81
+ !nonEmptyString(candidate.bindingName) ||
82
+ !nonEmptyString(candidate.roleKind) ||
83
+ !nonEmptyString(candidate.roleName) ||
84
+ !nonNegativeInteger(candidate.rulesCount)
85
+ ) {
86
+ return undefined;
87
+ }
88
+ for (const field of [
89
+ "bindingNamespace",
90
+ "roleNamespace",
91
+ "inheritedFromGroup",
92
+ ] as const) {
93
+ if (candidate[field] !== undefined && typeof candidate[field] !== "string")
94
+ return undefined;
95
+ }
96
+ return candidate as unknown as InvestigationPermissionBinding;
97
+ }
98
+ // Kinds whose spec carries a PodSpec, by API group. Anything else with a
99
+ // `spec.template.spec` (an ApplicationSet, for one) templates something that
100
+ // is not a Pod and runs as no account at all.
101
+ const POD_TEMPLATE_KINDS: Readonly<Record<string, readonly string[]>> = {
102
+ "": ["Pod"],
103
+ apps: ["Deployment", "StatefulSet", "DaemonSet", "ReplicaSet"],
104
+ batch: ["Job", "CronJob"],
105
+ "argoproj.io": ["Rollout"],
106
+ };
107
+ function podSpecServiceAccount(
108
+ resource: InvestigationKubernetesResource,
109
+ ): string | undefined {
110
+ const group = apiVersionToGroup(resource.apiVersion);
111
+ if (!POD_TEMPLATE_KINDS[group]?.includes(resource.kind)) return undefined;
112
+ const spec = record(resource.spec);
113
+ const podSpec =
114
+ resource.kind === "Pod"
115
+ ? spec
116
+ : resource.kind === "CronJob"
117
+ ? record(
118
+ record(record(record(spec?.jobTemplate)?.spec)?.template)?.spec,
119
+ )
120
+ : record(record(spec?.template)?.spec);
121
+ if (!podSpec) return undefined;
122
+ // An unset serviceAccountName runs as the namespace's `default` account.
123
+ return nonEmptyString(podSpec.serviceAccountName)
124
+ ? podSpec.serviceAccountName
125
+ : "default";
126
+ }
127
+ /**
128
+ * The ServiceAccount the target runs as, read from a resource observation of
129
+ * the target in the same turn. Only the captured resource can state this; the
130
+ * permission subject is never assumed related by name alone.
131
+ */
132
+ function targetServiceAccountName(
133
+ builder: ProjectionBuilder,
134
+ source: InvestigationEvidenceSource,
135
+ ): string | undefined {
136
+ let newest: InvestigationEvidenceObservation | undefined;
137
+ for (const group of builder.groups) {
138
+ if (group.kind !== "resource") continue;
139
+ for (const observation of group.observations) {
140
+ if (
141
+ observation.source.turnIndex !== source.turnIndex ||
142
+ observation.relevance !== "target" ||
143
+ observation.data.type !== "resource" ||
144
+ (newest && observation.source.order < newest.source.order)
145
+ ) {
146
+ continue;
147
+ }
148
+ newest = observation;
149
+ }
150
+ }
151
+ if (!newest || newest.data.type !== "resource") return undefined;
152
+ return (
153
+ newest.data.resourceContext?.uses?.serviceAccount?.name ??
154
+ podSpecServiceAccount(newest.data.resource)
155
+ );
156
+ }
157
+ export function adaptSubjectPermissions(
158
+ builder: ProjectionBuilder,
159
+ source: InvestigationEvidenceSource,
160
+ payload: unknown,
161
+ ): void {
162
+ const value = record(payload);
163
+ const subject = value ? permissionSubject(value.subject) : undefined;
164
+ if (!value || !subject) {
165
+ invalidPayload(builder, source);
166
+ return;
167
+ }
168
+ // Only a captured resource can tie a principal to the target. Any other
169
+ // subject the agent chose to check is context, however adverse the answer.
170
+ const relevance: InvestigationEvidenceRelevance =
171
+ subject.kind === "ServiceAccount" &&
172
+ subject.namespace !== undefined &&
173
+ subject.namespace === builder.target.namespace &&
174
+ targetServiceAccountName(builder, source) === subject.name
175
+ ? "target"
176
+ : "broader";
177
+ const subjectLabel = `${displayKind(subject.kind)} ${subject.namespace ? `${subject.namespace}/` : ""}${subject.name}`;
178
+ const subjectKey = `${subject.kind}:${subject.namespace ?? ""}:${subject.name}`;
179
+
180
+ if (value.accessCheck !== undefined) {
181
+ const check = accessCheck(value.accessCheck);
182
+ if (!check) {
183
+ invalidPayload(builder, source, "Access check");
184
+ return;
185
+ }
186
+ const resourceLabel = `${check.resource}${check.subresource ? `/${check.subresource}` : ""}${check.group ? `.${check.group}` : ""}`;
187
+ // An authorizer that could not decide has established neither answer. The
188
+ // evaluation error already reaches the coverage strip below, but a card
189
+ // reading "cannot verb resource" is the part an operator acts on, and a
190
+ // webhook returning partial data is not a denial.
191
+ // Only when the authorizer decided nothing. Kubernetes returns this error
192
+ // alongside a real verdict too — one webhook failing while another allows —
193
+ // and rewriting a decided allow or deny as "could not check" loses the
194
+ // answer and puts an allow in the warning list.
195
+ const unresolved =
196
+ nonEmptyString(check.evaluationError) && !check.allowed && !check.denied;
197
+ const denied = !check.allowed && !unresolved;
198
+ const verdict = unresolved
199
+ ? `Could not be evaluated: ${check.evaluationError}`
200
+ : check.reason ||
201
+ (check.allowed
202
+ ? "Allowed by RBAC"
203
+ : check.denied
204
+ ? "Explicitly denied"
205
+ : "No RBAC rule allows it");
206
+ builder.observe(
207
+ `permissions:check:${subjectKey}:${check.verb}:${check.group ?? ""}:${check.resource}:${check.subresource ?? ""}:${check.namespace}:${check.resourceName ?? ""}`,
208
+ "permissions",
209
+ source,
210
+ {
211
+ tier: evidenceTierForRelevance(
212
+ denied || unresolved ? "supporting" : "context",
213
+ relevance,
214
+ ),
215
+ relevance,
216
+ tone: denied || unresolved ? "warning" : "info",
217
+ title: unresolved
218
+ ? `Could not check whether ${subjectLabel} can ${check.verb} ${resourceLabel}`
219
+ : `${subjectLabel} ${denied ? "cannot" : "can"} ${check.verb} ${resourceLabel}`,
220
+ summary: `${verdict} · ${check.namespace ? `namespace ${check.namespace}` : "cluster-wide"}${check.resourceName ? ` · ${check.resourceName}` : ""}`,
221
+ data: { type: "permissions", subject, accessCheck: check },
222
+ },
223
+ );
224
+ if (nonEmptyString(check.evaluationError)) {
225
+ builder.limit(source, "Permissions", check.evaluationError, "error");
226
+ }
227
+ return;
228
+ }
229
+
230
+ const bindingsRaw = value.bindings;
231
+ if (!Array.isArray(bindingsRaw)) {
232
+ invalidPayload(builder, source);
233
+ return;
234
+ }
235
+ const bindings = bindingsRaw
236
+ .map(permissionBinding)
237
+ .filter((item): item is InvestigationPermissionBinding => Boolean(item));
238
+ if (
239
+ bindings.length !== bindingsRaw.length ||
240
+ !Array.isArray(value.flatRules) ||
241
+ !value.flatRules.every((rule) => stringArray(record(rule)?.verbs)) ||
242
+ (value.truncated !== undefined && typeof value.truncated !== "boolean") ||
243
+ (value.podsTotal !== undefined && !nonNegativeInteger(value.podsTotal)) ||
244
+ (value.usedByPods !== undefined && !stringArray(value.usedByPods))
245
+ ) {
246
+ invalidPayload(builder, source);
247
+ return;
248
+ }
249
+ const truncated = value.truncated === true;
250
+ const flatRulesCount = value.flatRules.length;
251
+ const usedByPods = stringArray(value.usedByPods) ?? [];
252
+ builder.observe(`permissions:subject:${subjectKey}`, "permissions", source, {
253
+ tier: evidenceTierForRelevance("context", relevance),
254
+ relevance,
255
+ tone: "info",
256
+ title: `Permissions of ${subjectLabel}`,
257
+ summary: `${bindings.length} binding${bindings.length === 1 ? "" : "s"} · ${flatRulesCount}${truncated ? "+" : ""} effective rule${flatRulesCount === 1 && !truncated ? "" : "s"}`,
258
+ data: {
259
+ type: "permissions",
260
+ subject,
261
+ bindings,
262
+ flatRulesCount,
263
+ truncated,
264
+ usedByPods,
265
+ podsTotal: value.podsTotal as number | undefined,
266
+ },
267
+ });
268
+ if (truncated) {
269
+ builder.limit(
270
+ source,
271
+ "Permissions",
272
+ nonEmptyString(value.narrowHint)
273
+ ? value.narrowHint
274
+ : "The effective rule list was truncated; it is not the subject's complete permission set.",
275
+ "truncated",
276
+ );
277
+ }
278
+ if (nonEmptyString(value.subjectWarning)) {
279
+ builder.limit(source, "Permissions", value.subjectWarning, "unknown");
280
+ }
281
+ if (
282
+ typeof value.podsTotal === "number" &&
283
+ value.podsTotal > usedByPods.length
284
+ ) {
285
+ builder.limit(
286
+ source,
287
+ "Permissions",
288
+ `Only ${usedByPods.length} of ${value.podsTotal} pods running as this subject were listed.`,
289
+ "truncated",
290
+ );
291
+ }
292
+ }