@skyhook-io/radar-app 1.13.5 → 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 (201) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/api/client.authRedirect.test.ts +12 -1
  4. package/src/api/client.prometheusStatus.test.ts +24 -0
  5. package/src/api/client.ts +321 -16
  6. package/src/api/diagnose.ts +1 -0
  7. package/src/api/drain-plan.test.ts +86 -0
  8. package/src/api/quotas.test.ts +26 -0
  9. package/src/api/quotas.ts +20 -0
  10. package/src/api/workloadMetrics.test.ts +40 -0
  11. package/src/api/workloadMetrics.ts +68 -0
  12. package/src/components/CloudConnectFlow.tsx +179 -42
  13. package/src/components/CloudFunnelButton.tsx +107 -36
  14. package/src/components/SyncProgressPanel.tsx +73 -0
  15. package/src/components/audit/AuditView.tsx +2 -0
  16. package/src/components/cloudConnectHandoff.test.ts +140 -0
  17. package/src/components/cloudConnectHandoff.ts +124 -0
  18. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  19. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  20. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  21. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  22. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  23. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  24. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  25. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  26. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  27. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  28. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  29. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  35. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  36. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  37. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  38. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  39. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  40. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  41. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  42. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  43. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  44. package/src/components/diagnose/target.test.ts +19 -0
  45. package/src/components/diagnose/target.ts +3 -2
  46. package/src/components/gitops/GitOpsView.tsx +7 -0
  47. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  48. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  49. package/src/components/resource/PrometheusCharts.tsx +4 -3
  50. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  51. package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
  52. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  53. package/src/components/resource/RightsizingStrip.tsx +15 -3
  54. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  55. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  56. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  57. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  58. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  59. package/src/components/resource/workloadMetricValues.ts +30 -0
  60. package/src/components/resources/ResourcesView.tsx +44 -6
  61. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  62. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  63. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  64. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  65. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  66. package/src/components/settings/SettingsDialog.tsx +94 -14
  67. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  68. package/src/components/traffic/TrafficGraph.tsx +6 -6
  69. package/src/components/traffic/TrafficView.tsx +10 -9
  70. package/src/components/traffic/trafficFilters.test.ts +28 -1
  71. package/src/components/traffic/trafficFilters.ts +31 -0
  72. package/src/components/workload/WorkloadView.tsx +19 -8
  73. package/src/context/ConnectionContext.tsx +48 -3
  74. package/src/context/DiagnoseCustomization.tsx +1 -0
  75. package/src/index.css +17 -0
  76. package/src/utils/navigation.ts +1 -1
  77. package/src/utils/topology-namespace.test.ts +64 -0
  78. package/src/utils/topology-namespace.ts +18 -0
  79. package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
  80. package/src/components/diagnose/investigationEvidence.ts +0 -5572
  81. package/src/components/gitops/GitOpsActions.tsx +0 -1
  82. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  83. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  84. package/src/components/gitops/SyncCountdown.tsx +0 -1
  85. package/src/components/gitops/index.ts +0 -4
  86. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  87. package/src/components/logs/JsonLogLine.tsx +0 -1
  88. package/src/components/logs/LogCore.tsx +0 -2
  89. package/src/components/logs/useLogBuffer.ts +0 -2
  90. package/src/components/logs/useLogSearch.ts +0 -1
  91. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  92. package/src/components/resources/drawer-components.tsx +0 -1
  93. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  94. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  95. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  96. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  97. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  98. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  99. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  100. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  101. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  102. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  103. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  104. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  105. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  106. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  107. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  108. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  109. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  110. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  111. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  112. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  113. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  114. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  115. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  116. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  117. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  118. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  119. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  120. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  121. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  122. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  162. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  169. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  170. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  171. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  172. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  173. package/src/components/resources/renderers/index.ts +0 -94
  174. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  175. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  176. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  177. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  178. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  179. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  180. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  181. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  182. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  183. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  184. package/src/components/resources/resource-utils-argo.ts +0 -2
  185. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  186. package/src/components/resources/resource-utils-eso.ts +0 -2
  187. package/src/components/resources/resource-utils-flux.ts +0 -2
  188. package/src/components/resources/resource-utils-istio.ts +0 -2
  189. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  190. package/src/components/resources/resource-utils-knative.ts +0 -2
  191. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  192. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  193. package/src/components/resources/resource-utils-traefik.ts +0 -1
  194. package/src/components/resources/resource-utils-trivy.ts +0 -2
  195. package/src/components/resources/resource-utils-velero.ts +0 -2
  196. package/src/components/shared/EditableYamlView.tsx +0 -24
  197. package/src/components/timeline/DiffViewer.tsx +0 -1
  198. package/src/components/traffic/index.ts +0 -3
  199. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  200. package/src/components/ui/MetricsChart.tsx +0 -1
  201. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,528 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ investigationEvidenceSubjectRef,
4
+ projectInvestigationEvidence,
5
+ } from "../index";
6
+ import { deployment, groupsOf, project, tool } from "../evidenceFixtures";
7
+
8
+ const deploymentWithServiceAccount = {
9
+ ...deployment,
10
+ spec: { template: { spec: { serviceAccountName: "api-sa" } } },
11
+ };
12
+
13
+ const permissionsArgs = JSON.stringify({
14
+ kind: "ServiceAccount",
15
+ namespace: "shop",
16
+ name: "api-sa",
17
+ verb: "get",
18
+ resource: "secrets",
19
+ });
20
+
21
+ describe("subject permissions adapter", () => {
22
+ it("does not turn an unevaluated access check into a denial", () => {
23
+ // A webhook authorizer returning partial data has established neither
24
+ // answer. Reading it as "cannot" sends an operator after an RBAC grant
25
+ // that may already exist.
26
+ const projection = project([
27
+ tool("diagnose", "diagnose", {
28
+ resource: deploymentWithServiceAccount,
29
+ resourceContext: { tier: "basic" },
30
+ }),
31
+ tool(
32
+ "perm",
33
+ "get_subject_permissions",
34
+ {
35
+ subject: {
36
+ kind: "ServiceAccount",
37
+ namespace: "shop",
38
+ name: "api-sa",
39
+ },
40
+ accessCheck: {
41
+ verb: "get",
42
+ resource: "secrets",
43
+ namespace: "shop",
44
+ allowed: false,
45
+ denied: false,
46
+ reason: "",
47
+ evaluationError: "webhook authorizer returned partial data",
48
+ },
49
+ },
50
+ { summary: permissionsArgs },
51
+ ),
52
+ ]);
53
+ const [group] = groupsOf(projection.groups, "permissions");
54
+ expect(group.latest.title).toContain("Could not check whether");
55
+ expect(group.latest.title).not.toContain("cannot get");
56
+ expect(group.latest.summary).toContain(
57
+ "webhook authorizer returned partial data",
58
+ );
59
+ expect(group.latest.summary).not.toContain("No RBAC rule allows it");
60
+ // The gap still reaches the coverage strip as well.
61
+ expect(projection.limitations).toContainEqual(
62
+ expect.objectContaining({ source: "Permissions", kind: "error" }),
63
+ );
64
+ });
65
+
66
+ it("keeps a decided verdict that arrived with an evaluation error", () => {
67
+ // Kubernetes returns the error alongside a real answer too — one webhook
68
+ // failing while another allows. That answer is still the answer.
69
+ const projection = project([
70
+ tool("diagnose", "diagnose", {
71
+ resource: deploymentWithServiceAccount,
72
+ resourceContext: { tier: "basic" },
73
+ }),
74
+ tool(
75
+ "perm",
76
+ "get_subject_permissions",
77
+ {
78
+ subject: {
79
+ kind: "ServiceAccount",
80
+ namespace: "shop",
81
+ name: "api-sa",
82
+ },
83
+ accessCheck: {
84
+ verb: "get",
85
+ resource: "secrets",
86
+ namespace: "shop",
87
+ allowed: true,
88
+ denied: false,
89
+ reason: "allowed by one authorizer",
90
+ evaluationError: "a second webhook did not answer",
91
+ },
92
+ },
93
+ { summary: permissionsArgs },
94
+ ),
95
+ ]);
96
+ const [group] = groupsOf(projection.groups, "permissions");
97
+ expect(group.latest.title).toContain("can get");
98
+ expect(group.latest.title).not.toContain("Could not check");
99
+ expect(group.latest.tone).toBe("info");
100
+ // And the error is still reported, just not as the verdict.
101
+ expect(projection.limitations).toContainEqual(
102
+ expect.objectContaining({ source: "Permissions", kind: "error" }),
103
+ );
104
+ });
105
+
106
+ it("binds an access check to the target's ServiceAccount seen in the same turn", () => {
107
+ const check = {
108
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "api-sa" },
109
+ accessCheck: {
110
+ verb: "get",
111
+ resource: "secrets",
112
+ namespace: "shop",
113
+ allowed: false,
114
+ reason: "",
115
+ },
116
+ };
117
+ const projection = project([
118
+ tool("diagnose", "diagnose", {
119
+ resource: deploymentWithServiceAccount,
120
+ resourceContext: { tier: "basic" },
121
+ }),
122
+ tool("perm", "get_subject_permissions", check, {
123
+ summary: permissionsArgs,
124
+ }),
125
+ tool(
126
+ "perm-other",
127
+ "get_subject_permissions",
128
+ {
129
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "other" },
130
+ accessCheck: {
131
+ verb: "list",
132
+ group: "apps",
133
+ resource: "deployments",
134
+ subresource: "scale",
135
+ namespace: "",
136
+ allowed: true,
137
+ reason: 'RBAC: allowed by ClusterRoleBinding "admin"',
138
+ },
139
+ },
140
+ {
141
+ summary: JSON.stringify({
142
+ kind: "ServiceAccount",
143
+ namespace: "shop",
144
+ name: "other",
145
+ }),
146
+ },
147
+ ),
148
+ ]);
149
+ const permissions = groupsOf(projection.groups, "permissions");
150
+ expect(permissions.map((group) => group.latest.relevance)).toEqual([
151
+ "target",
152
+ "broader",
153
+ ]);
154
+ expect(permissions[0].latest).toMatchObject({
155
+ tier: "supporting",
156
+ tone: "warning",
157
+ title: "Service Account shop/api-sa cannot get secrets",
158
+ summary: "No RBAC rule allows it · namespace shop",
159
+ });
160
+ expect(permissions[0].latest.data).toMatchObject({
161
+ type: "permissions",
162
+ accessCheck: { allowed: false, denied: false },
163
+ });
164
+ expect(permissions[1].latest).toMatchObject({
165
+ tier: "context",
166
+ tone: "info",
167
+ title: "Service Account shop/other can list deployments/scale.apps",
168
+ summary: 'RBAC: allowed by ClusterRoleBinding "admin" · cluster-wide',
169
+ });
170
+ expect(investigationEvidenceSubjectRef(permissions[0].latest.data)).toEqual(
171
+ { kind: "ServiceAccount", namespace: "shop", name: "api-sa" },
172
+ );
173
+ });
174
+
175
+ it("uses the producer's serviceAccount reference and the default account when the template omits one", () => {
176
+ const withContext = project([
177
+ tool("diagnose", "diagnose", {
178
+ resource: deployment,
179
+ resourceContext: {
180
+ tier: "diagnostic",
181
+ uses: {
182
+ serviceAccount: {
183
+ kind: "ServiceAccount",
184
+ namespace: "shop",
185
+ name: "ctx-sa",
186
+ },
187
+ },
188
+ },
189
+ }),
190
+ tool(
191
+ "perm",
192
+ "get_subject_permissions",
193
+ {
194
+ subject: {
195
+ kind: "ServiceAccount",
196
+ namespace: "shop",
197
+ name: "ctx-sa",
198
+ },
199
+ bindings: [],
200
+ flatRules: [],
201
+ },
202
+ { summary: permissionsArgs },
203
+ ),
204
+ ]);
205
+ expect(
206
+ groupsOf(withContext.groups, "permissions")[0].latest.relevance,
207
+ ).toBe("target");
208
+
209
+ const withDefault = project([
210
+ tool("resource", "get_resource", {
211
+ ...deployment,
212
+ spec: { template: { spec: {} } },
213
+ }),
214
+ tool(
215
+ "perm",
216
+ "get_subject_permissions",
217
+ {
218
+ subject: {
219
+ kind: "ServiceAccount",
220
+ namespace: "shop",
221
+ name: "default",
222
+ },
223
+ bindings: [],
224
+ flatRules: [],
225
+ },
226
+ { summary: permissionsArgs },
227
+ ),
228
+ ]);
229
+ expect(
230
+ groupsOf(withDefault.groups, "permissions")[0].latest.relevance,
231
+ ).toBe("target");
232
+
233
+ const priorTurn = project(
234
+ [
235
+ tool("diagnose", "diagnose", {
236
+ resource: deploymentWithServiceAccount,
237
+ resourceContext: { tier: "basic" },
238
+ }),
239
+ ],
240
+ [
241
+ tool(
242
+ "perm",
243
+ "get_subject_permissions",
244
+ {
245
+ subject: {
246
+ kind: "ServiceAccount",
247
+ namespace: "shop",
248
+ name: "api-sa",
249
+ },
250
+ bindings: [],
251
+ flatRules: [],
252
+ },
253
+ { summary: permissionsArgs },
254
+ ),
255
+ ],
256
+ );
257
+ expect(groupsOf(priorTurn.groups, "permissions")[0].latest.relevance).toBe(
258
+ "broader",
259
+ );
260
+
261
+ // The newest captured read of the target decides which account it runs as.
262
+ const rotated = project([
263
+ tool("resource-old", "get_resource", deploymentWithServiceAccount),
264
+ tool("resource-new", "get_resource", {
265
+ ...deployment,
266
+ spec: { template: { spec: { serviceAccountName: "api-sa-v2" } } },
267
+ }),
268
+ tool(
269
+ "perm-old",
270
+ "get_subject_permissions",
271
+ {
272
+ subject: {
273
+ kind: "ServiceAccount",
274
+ namespace: "shop",
275
+ name: "api-sa",
276
+ },
277
+ bindings: [],
278
+ flatRules: [],
279
+ },
280
+ { summary: permissionsArgs },
281
+ ),
282
+ tool(
283
+ "perm-new",
284
+ "get_subject_permissions",
285
+ {
286
+ subject: {
287
+ kind: "ServiceAccount",
288
+ namespace: "shop",
289
+ name: "api-sa-v2",
290
+ },
291
+ bindings: [],
292
+ flatRules: [],
293
+ },
294
+ { summary: permissionsArgs },
295
+ ),
296
+ ]);
297
+ expect(
298
+ groupsOf(rotated.groups, "permissions").map(
299
+ (group) => group.latest.relevance,
300
+ ),
301
+ ).toEqual(["broader", "target"]);
302
+ });
303
+
304
+ it("reads a PodSpec only from kinds that carry one", () => {
305
+ const check = (name: string) => ({
306
+ subject: { kind: "ServiceAccount", namespace: "shop", name },
307
+ accessCheck: {
308
+ verb: "get",
309
+ resource: "secrets",
310
+ namespace: "shop",
311
+ allowed: false,
312
+ },
313
+ });
314
+ const applicationSet = project([
315
+ tool("resource", "get_resource", {
316
+ apiVersion: "argoproj.io/v1alpha1",
317
+ kind: "ApplicationSet",
318
+ metadata: { namespace: "shop", name: "api" },
319
+ spec: { template: { spec: { project: "default" } } },
320
+ }),
321
+ tool("perm", "get_subject_permissions", check("default"), {
322
+ summary: permissionsArgs,
323
+ }),
324
+ ]);
325
+ expect(
326
+ groupsOf(applicationSet.groups, "permissions")[0].latest.relevance,
327
+ ).toBe("broader");
328
+
329
+ const cronJob = projectInvestigationEvidence(
330
+ [
331
+ {
332
+ timeline: [
333
+ tool("resource", "get_resource", {
334
+ apiVersion: "batch/v1",
335
+ kind: "CronJob",
336
+ metadata: { namespace: "shop", name: "api" },
337
+ spec: {
338
+ jobTemplate: {
339
+ spec: {
340
+ template: { spec: { serviceAccountName: "cron-sa" } },
341
+ },
342
+ },
343
+ },
344
+ }),
345
+ tool("perm", "get_subject_permissions", check("cron-sa"), {
346
+ summary: permissionsArgs,
347
+ }),
348
+ ],
349
+ },
350
+ ],
351
+ { kind: "CronJob", group: "batch", namespace: "shop", name: "api" },
352
+ );
353
+ expect(groupsOf(cronJob.groups, "permissions")[0].latest.relevance).toBe(
354
+ "target",
355
+ );
356
+ });
357
+
358
+ it("keeps a denial for an unrelated principal out of the lead evidence", () => {
359
+ const projection = project([
360
+ tool("diagnose", "diagnose", {
361
+ resource: deploymentWithServiceAccount,
362
+ resourceContext: { tier: "basic" },
363
+ }),
364
+ tool(
365
+ "perm-cross",
366
+ "get_subject_permissions",
367
+ {
368
+ subject: {
369
+ kind: "ServiceAccount",
370
+ namespace: "unrelated",
371
+ name: "other",
372
+ },
373
+ accessCheck: {
374
+ verb: "get",
375
+ resource: "secrets",
376
+ namespace: "unrelated",
377
+ allowed: false,
378
+ },
379
+ },
380
+ {
381
+ summary: JSON.stringify({
382
+ kind: "ServiceAccount",
383
+ namespace: "unrelated",
384
+ name: "other",
385
+ }),
386
+ },
387
+ ),
388
+ ]);
389
+ const denial = groupsOf(projection.groups, "permissions")[0].latest;
390
+ expect(denial.relevance).toBe("broader");
391
+ expect(denial.tier).toBe("context");
392
+ expect(denial.tone).toBe("warning");
393
+ });
394
+
395
+ it("renders the subject response as counts and keeps every coverage caveat", () => {
396
+ const projection = project([
397
+ tool(
398
+ "perm",
399
+ "get_subject_permissions",
400
+ {
401
+ subject: {
402
+ kind: "ServiceAccount",
403
+ namespace: "shop",
404
+ name: "api-sa",
405
+ },
406
+ bindings: [
407
+ {
408
+ bindingKind: "RoleBinding",
409
+ bindingNamespace: "shop",
410
+ bindingName: "api-reader",
411
+ roleKind: "Role",
412
+ roleNamespace: "shop",
413
+ roleName: "reader",
414
+ rulesCount: 3,
415
+ },
416
+ {
417
+ bindingKind: "ClusterRoleBinding",
418
+ bindingName: "system:basic-user",
419
+ roleKind: "ClusterRole",
420
+ roleName: "system:basic-user",
421
+ rulesCount: 1,
422
+ inheritedFromGroup: "system:authenticated",
423
+ },
424
+ ],
425
+ flatRules: [
426
+ { verbs: ["get"], resources: ["pods"] },
427
+ { verbs: ["list"], resources: ["pods"] },
428
+ ],
429
+ truncated: true,
430
+ usedByPods: ["shop/api-68c7b766dc-fmphn"],
431
+ podsTotal: 3,
432
+ narrowHint:
433
+ "rule list truncated — the subject has more rules than shown; do not treat this list as the subject's complete permissions",
434
+ },
435
+ { summary: permissionsArgs },
436
+ ),
437
+ tool(
438
+ "perm-missing",
439
+ "get_subject_permissions",
440
+ {
441
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "ghost" },
442
+ bindings: [],
443
+ flatRules: [],
444
+ subjectWarning:
445
+ 'no ServiceAccount "ghost" exists in namespace "shop" — this empty result reflects a subject that was never found, not an account without permissions. Check the name and namespace.',
446
+ },
447
+ { summary: permissionsArgs },
448
+ ),
449
+ ]);
450
+ const permissions = groupsOf(projection.groups, "permissions");
451
+ expect(permissions[0].latest).toMatchObject({
452
+ tier: "context",
453
+ tone: "info",
454
+ relevance: "broader",
455
+ title: "Permissions of Service Account shop/api-sa",
456
+ summary: "2 bindings · 2+ effective rules",
457
+ });
458
+ expect(permissions[0].latest.data).toMatchObject({
459
+ type: "permissions",
460
+ flatRulesCount: 2,
461
+ truncated: true,
462
+ usedByPods: ["shop/api-68c7b766dc-fmphn"],
463
+ podsTotal: 3,
464
+ });
465
+ expect(permissions[1].latest.summary).toBe(
466
+ "0 bindings · 0 effective rules",
467
+ );
468
+ expect(
469
+ projection.limitations.map((limitation) => [
470
+ limitation.source,
471
+ limitation.kind,
472
+ ]),
473
+ ).toEqual([
474
+ ["Permissions", "truncated"],
475
+ ["Permissions", "truncated"],
476
+ ["Permissions", "unknown"],
477
+ ]);
478
+ expect(projection.limitations[0].message).toContain("rule list truncated");
479
+ expect(projection.limitations[1].message).toBe(
480
+ "Only 1 of 3 pods running as this subject were listed.",
481
+ );
482
+ });
483
+
484
+ it("rejects malformed permission payloads", () => {
485
+ const projection = project([
486
+ tool(
487
+ "perm-bad",
488
+ "get_subject_permissions",
489
+ { subject: { kind: "ServiceAccount" }, bindings: [], flatRules: [] },
490
+ { summary: permissionsArgs },
491
+ ),
492
+ tool(
493
+ "perm-bad-check",
494
+ "get_subject_permissions",
495
+ {
496
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "x" },
497
+ accessCheck: { verb: "get", resource: "secrets", namespace: "shop" },
498
+ },
499
+ { summary: permissionsArgs },
500
+ ),
501
+ tool(
502
+ "perm-bad-binding",
503
+ "get_subject_permissions",
504
+ {
505
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "x" },
506
+ bindings: [{ bindingKind: "RoleBinding" }],
507
+ flatRules: [],
508
+ },
509
+ { summary: permissionsArgs },
510
+ ),
511
+ tool(
512
+ "perm-bad-rule",
513
+ "get_subject_permissions",
514
+ {
515
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "x" },
516
+ bindings: [],
517
+ flatRules: [{ resources: ["pods"] }],
518
+ },
519
+ { summary: permissionsArgs },
520
+ ),
521
+ ]);
522
+ expect(projection.groups).toHaveLength(0);
523
+ expect(
524
+ projection.limitations.map((limitation) => limitation.source),
525
+ ).toEqual(["Permissions", "Access check"]);
526
+ expect(projection.limitations[0].sources).toHaveLength(3);
527
+ });
528
+ });