@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.
- package/package.json +8 -8
- package/src/App.tsx +60 -24
- package/src/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +321 -16
- package/src/api/diagnose.ts +1 -0
- package/src/api/drain-plan.test.ts +86 -0
- package/src/api/quotas.test.ts +26 -0
- package/src/api/quotas.ts +20 -0
- package/src/api/workloadMetrics.test.ts +40 -0
- package/src/api/workloadMetrics.ts +68 -0
- package/src/components/CloudConnectFlow.tsx +179 -42
- package/src/components/CloudFunnelButton.tsx +107 -36
- package/src/components/SyncProgressPanel.tsx +73 -0
- package/src/components/audit/AuditView.tsx +2 -0
- package/src/components/cloudConnectHandoff.test.ts +140 -0
- package/src/components/cloudConnectHandoff.ts +124 -0
- package/src/components/diagnose/DiagnoseContext.tsx +2 -1
- package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
- package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
- package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
- package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
- package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
- package/src/components/diagnose/investigationEvidence/index.ts +55 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
- package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
- package/src/components/diagnose/investigationEvidence/types.ts +512 -0
- package/src/components/diagnose/target.test.ts +19 -0
- package/src/components/diagnose/target.ts +3 -2
- package/src/components/gitops/GitOpsView.tsx +7 -0
- package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
- package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
- package/src/components/resource/PrometheusCharts.tsx +4 -3
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
- package/src/components/resource/RightsizingStrip.test.ts +38 -0
- package/src/components/resource/RightsizingStrip.tsx +15 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
- package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
- package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
- package/src/components/resource/workloadMetricValues.test.ts +65 -0
- package/src/components/resource/workloadMetricValues.ts +30 -0
- package/src/components/resources/ResourcesView.tsx +44 -6
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
- package/src/components/resources/renderers/PodRenderer.tsx +4 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
- package/src/components/settings/SettingsDialog.tsx +94 -14
- package/src/components/traffic/TrafficFlowList.tsx +229 -70
- package/src/components/traffic/TrafficGraph.tsx +6 -6
- package/src/components/traffic/TrafficView.tsx +10 -9
- package/src/components/traffic/trafficFilters.test.ts +28 -1
- package/src/components/traffic/trafficFilters.ts +31 -0
- package/src/components/workload/WorkloadView.tsx +19 -8
- package/src/context/ConnectionContext.tsx +48 -3
- package/src/context/DiagnoseCustomization.tsx +1 -0
- package/src/index.css +17 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/topology-namespace.test.ts +64 -0
- package/src/utils/topology-namespace.ts +18 -0
- package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
- package/src/components/diagnose/investigationEvidence.ts +0 -5572
- package/src/components/gitops/GitOpsActions.tsx +0 -1
- package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
- package/src/components/gitops/ManagedResourcesList.tsx +0 -1
- package/src/components/gitops/SyncCountdown.tsx +0 -1
- package/src/components/gitops/index.ts +0 -4
- package/src/components/helm/ManifestDiffViewer.tsx +0 -69
- package/src/components/logs/JsonLogLine.tsx +0 -1
- package/src/components/logs/LogCore.tsx +0 -2
- package/src/components/logs/useLogBuffer.ts +0 -2
- package/src/components/logs/useLogSearch.ts +0 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
- package/src/components/resources/drawer-components.tsx +0 -1
- package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
- package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/EventRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/JobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/VPARenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
- package/src/components/resources/renderers/argo-cells.tsx +0 -1
- package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
- package/src/components/resources/renderers/eso-cells.tsx +0 -1
- package/src/components/resources/renderers/flux-cells.tsx +0 -1
- package/src/components/resources/renderers/index.ts +0 -94
- package/src/components/resources/renderers/istio-cells.tsx +0 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
- package/src/components/resources/renderers/keda-cells.tsx +0 -1
- package/src/components/resources/renderers/knative-cells.tsx +0 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
- package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
- package/src/components/resources/renderers/traefik-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-shared.tsx +0 -1
- package/src/components/resources/renderers/velero-cells.tsx +0 -1
- package/src/components/resources/resource-utils-argo.ts +0 -2
- package/src/components/resources/resource-utils-certmanager.ts +0 -2
- package/src/components/resources/resource-utils-eso.ts +0 -2
- package/src/components/resources/resource-utils-flux.ts +0 -2
- package/src/components/resources/resource-utils-istio.ts +0 -2
- package/src/components/resources/resource-utils-karpenter.ts +0 -2
- package/src/components/resources/resource-utils-knative.ts +0 -2
- package/src/components/resources/resource-utils-kyverno.ts +0 -2
- package/src/components/resources/resource-utils-prometheus.ts +0 -2
- package/src/components/resources/resource-utils-traefik.ts +0 -1
- package/src/components/resources/resource-utils-trivy.ts +0 -2
- package/src/components/resources/resource-utils-velero.ts +0 -2
- package/src/components/shared/EditableYamlView.tsx +0 -24
- package/src/components/timeline/DiffViewer.tsx +0 -1
- package/src/components/traffic/index.ts +0 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
- package/src/components/ui/MetricsChart.tsx +0 -1
- 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
|
+
}
|