@skyhook-io/k8s-ui 1.11.0 → 1.13.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 +4 -4
- package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
- package/src/components/gitops/GitOpsTableView.tsx +22 -2
- package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
- package/src/components/gitops/detail-helpers.ts +4 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +412 -5
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
- package/src/components/resources/renderers/PolicySection.tsx +197 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
- package/src/components/resources/resource-utils-cnpg.ts +324 -3
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +12 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +3 -0
- package/src/types/core.ts +29 -0
- package/src/types/gitops-flux-status.test.ts +163 -0
- package/src/types/gitops.ts +80 -24
- package/src/types/index.ts +1 -0
- package/src/types/policy.ts +193 -0
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- package/src/utils/resource-icons.ts +29 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { PolicySection } from './PolicySection'
|
|
4
|
+
import type { PolicyResourceResponse } from '../../../types/policy'
|
|
5
|
+
|
|
6
|
+
const base: PolicyResourceResponse = {
|
|
7
|
+
evaluated: true,
|
|
8
|
+
status: 'ready',
|
|
9
|
+
liveUpdates: true,
|
|
10
|
+
counts: { pass: 0, fail: 0, warn: 0, error: 0, skip: 0 },
|
|
11
|
+
findings: [],
|
|
12
|
+
}
|
|
13
|
+
const resp = (over: Partial<PolicyResourceResponse>): PolicyResourceResponse => ({ ...base, ...over })
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// The unhappy paths carry this section. An empty finding list means several
|
|
17
|
+
// unrelated things, and rendering them all as blank space would tell an
|
|
18
|
+
// operator they are compliant in the cases where nothing was checked.
|
|
19
|
+
describe('PolicySection — what an empty result means', () => {
|
|
20
|
+
it('says which rules failed when there are violations', () => {
|
|
21
|
+
const html = renderToString(<PolicySection data={resp({
|
|
22
|
+
counts: { pass: 2, fail: 1, warn: 0, error: 0, skip: 0 },
|
|
23
|
+
findings: [{ policy: 'require-run-as-nonroot', result: 'fail', message: 'must set runAsNonRoot' }],
|
|
24
|
+
})} />)
|
|
25
|
+
expect(html).toContain('require-run-as-nonroot')
|
|
26
|
+
expect(html).toContain('must set runAsNonRoot')
|
|
27
|
+
// The passing checks are counted, not listed — otherwise two failures drown
|
|
28
|
+
// in a wall of green on a heavily-policed workload.
|
|
29
|
+
expect(html).toContain('2 other checks passing')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('distinguishes "everything passed" from "nothing was checked"', () => {
|
|
33
|
+
const passing = renderToString(<PolicySection data={resp({
|
|
34
|
+
counts: { pass: 5, fail: 0, warn: 0, error: 0, skip: 0 },
|
|
35
|
+
})} />)
|
|
36
|
+
expect(passing).toContain('All 5 checks passing')
|
|
37
|
+
|
|
38
|
+
const unchecked = renderToString(<PolicySection data={resp({})} />)
|
|
39
|
+
expect(unchecked).toContain('No policy applies')
|
|
40
|
+
expect(unchecked).not.toContain('passing')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('never claims compliance when the results could not be read', () => {
|
|
44
|
+
const html = renderToString(<PolicySection data={resp({
|
|
45
|
+
evaluated: false, status: 'deferred', reasonCode: 'rbac_denied',
|
|
46
|
+
})} />)
|
|
47
|
+
expect(html).toContain('has not been checked')
|
|
48
|
+
expect(html).toContain('rbac_denied')
|
|
49
|
+
expect(html).not.toContain('passing')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('says results are still loading during warmup rather than reporting clean', () => {
|
|
53
|
+
const html = renderToString(<PolicySection data={resp({ evaluated: false, status: 'warmup' })} />)
|
|
54
|
+
expect(html).toContain('still loading')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders nothing at all when no policy engine is installed', () => {
|
|
58
|
+
// A disclaimer on every cluster without a policy engine is pure noise.
|
|
59
|
+
expect(renderToString(<PolicySection data={resp({ evaluated: false, status: 'not_installed' })} />)).toBe('')
|
|
60
|
+
expect(renderToString(<PolicySection data={null} />)).toBe('')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('PolicySection — partial coverage', () => {
|
|
65
|
+
it('admits when some report families were unreadable, even while passing', () => {
|
|
66
|
+
// "All checks passing" plus an unreadable family is a claim of coverage we
|
|
67
|
+
// do not have. The note alone does not undo it — "All" is the word that gets
|
|
68
|
+
// read, and the note is the small print under a green shield.
|
|
69
|
+
const html = renderToString(<PolicySection data={resp({
|
|
70
|
+
counts: { pass: 3, fail: 0, warn: 0, error: 0, skip: 0 },
|
|
71
|
+
deniedGroups: ['openreports.io'],
|
|
72
|
+
})} />)
|
|
73
|
+
expect(html).toContain('3 checks passing')
|
|
74
|
+
expect(html).not.toContain('All 3 checks passing')
|
|
75
|
+
expect(html).toContain('may be incomplete')
|
|
76
|
+
expect(html).toContain('openreports.io')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Staleness dates the answer, it does not shrink it: "as of the last sync, all
|
|
80
|
+
// passed" is still a true sentence, so this one keeps the word.
|
|
81
|
+
it('still says all passed when the only caveat is a frozen index', () => {
|
|
82
|
+
const html = renderToString(<PolicySection data={resp({
|
|
83
|
+
counts: { pass: 3, fail: 0, warn: 0, error: 0, skip: 0 }, liveUpdates: false,
|
|
84
|
+
})} />)
|
|
85
|
+
expect(html).toContain('All 3 checks passing')
|
|
86
|
+
expect(html).toContain('not updating live')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('admits when the results are frozen', () => {
|
|
90
|
+
const html = renderToString(<PolicySection data={resp({
|
|
91
|
+
counts: { pass: 1, fail: 0, warn: 0, error: 0, skip: 0 }, liveUpdates: false,
|
|
92
|
+
})} />)
|
|
93
|
+
expect(html).toContain('not updating live')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('PolicySection — request states', () => {
|
|
98
|
+
it('shows loading rather than an empty result', () => {
|
|
99
|
+
expect(renderToString(<PolicySection data={null} loading />)).toContain('Loading policy results')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('treats a permission denial as an expected state, not a red failure', () => {
|
|
103
|
+
const denied = Object.assign(new Error('forbidden'), { status: 403 })
|
|
104
|
+
const html = renderToString(<PolicySection data={null} error={denied} />)
|
|
105
|
+
expect(html).toContain('don’t have permission')
|
|
106
|
+
expect(html).not.toContain('text-red')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('keeps a genuine fault loud', () => {
|
|
110
|
+
const html = renderToString(<PolicySection data={null} error={new Error('connection reset')} />)
|
|
111
|
+
expect(html).toContain('connection reset')
|
|
112
|
+
expect(html).toContain('text-red')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Findings the caller may not read are dropped from `findings` AND from
|
|
118
|
+
* `counts`, so the screen has nothing left to reveal that anything is missing.
|
|
119
|
+
* A resource whose only failure came from a report family this identity cannot
|
|
120
|
+
* read then renders as passing everything — an all-clear on a security surface,
|
|
121
|
+
* derived from an answer that was never fully asked.
|
|
122
|
+
*/
|
|
123
|
+
describe('when results were withheld from this caller', () => {
|
|
124
|
+
const withheld: PolicyResourceResponse = {
|
|
125
|
+
...base,
|
|
126
|
+
counts: { pass: 3, fail: 0, warn: 0, error: 0, skip: 0 },
|
|
127
|
+
findings: [],
|
|
128
|
+
withheldByFamily: 1,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
it('does not claim everything passed', () => {
|
|
132
|
+
const html = renderToString(<PolicySection data={withheld} />)
|
|
133
|
+
expect(html).not.toContain('All 3 checks passing')
|
|
134
|
+
expect(html).toContain('3 checks passing')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('says what is missing and why', () => {
|
|
138
|
+
const html = renderToString(<PolicySection data={withheld} />)
|
|
139
|
+
expect(html).toContain('not shown here')
|
|
140
|
+
expect(html).toContain('permission')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('agrees in number', () => {
|
|
144
|
+
const html = renderToString(
|
|
145
|
+
<PolicySection data={{ ...withheld, withheldByFamily: 2 }} />,
|
|
146
|
+
)
|
|
147
|
+
expect(html).toContain('2 results are not shown')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// The ordinary case must keep its plain claim — the note only appears when
|
|
151
|
+
// something was actually held back.
|
|
152
|
+
it('still claims all-clear when nothing was withheld', () => {
|
|
153
|
+
const html = renderToString(
|
|
154
|
+
<PolicySection data={{ ...base, counts: { ...base.counts, pass: 3 } }} />,
|
|
155
|
+
)
|
|
156
|
+
expect(html).toContain('All 3 checks passing')
|
|
157
|
+
expect(html).not.toContain('not shown here')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { ShieldCheck, ShieldAlert, ShieldQuestion } from 'lucide-react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Section } from '../../ui/drawer-components'
|
|
4
|
+
import { SEVERITY_BADGE } from '../../../utils/badge-colors'
|
|
5
|
+
import { isForbiddenError } from '../../../types/fetch-error'
|
|
6
|
+
import type { PolicyResourceResponse, PolicyResourceFinding } from '../../../types/policy'
|
|
7
|
+
|
|
8
|
+
interface PolicySectionProps {
|
|
9
|
+
data: PolicyResourceResponse | null
|
|
10
|
+
loading?: boolean
|
|
11
|
+
error?: Error | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TITLE = 'Policy'
|
|
15
|
+
|
|
16
|
+
// Result → badge tone. `error` means the engine could not evaluate the rule,
|
|
17
|
+
// which is not the same as the rule failing, so it does not share fail's red.
|
|
18
|
+
function resultTone(result: string): { badge: string; label: string } {
|
|
19
|
+
switch (result.toLowerCase()) {
|
|
20
|
+
case 'fail':
|
|
21
|
+
return { badge: SEVERITY_BADGE.error, label: 'Fail' }
|
|
22
|
+
case 'warn':
|
|
23
|
+
return { badge: SEVERITY_BADGE.warning, label: 'Warn' }
|
|
24
|
+
case 'error':
|
|
25
|
+
return { badge: SEVERITY_BADGE.alert, label: 'Error' }
|
|
26
|
+
case 'skip':
|
|
27
|
+
return { badge: SEVERITY_BADGE.neutral, label: 'Skipped' }
|
|
28
|
+
default:
|
|
29
|
+
return { badge: SEVERITY_BADGE.neutral, label: result }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* PolicySection reports what the cluster's policy engine says about this one
|
|
35
|
+
* resource.
|
|
36
|
+
*
|
|
37
|
+
* The unhappy paths carry the weight here. An empty finding list means one of
|
|
38
|
+
* several unrelated things, and rendering them all as blank space would tell an
|
|
39
|
+
* operator they are compliant in the cases where nothing was actually checked —
|
|
40
|
+
* the failure mode this section exists to avoid. Each state therefore says which
|
|
41
|
+
* one it is, in its own words, and the red treatment is reserved for genuine
|
|
42
|
+
* faults. Same division the RBAC sections already draw.
|
|
43
|
+
*/
|
|
44
|
+
export function PolicySection({ data, loading, error }: PolicySectionProps) {
|
|
45
|
+
if (loading) {
|
|
46
|
+
return (
|
|
47
|
+
<Section title={TITLE} icon={ShieldQuestion}>
|
|
48
|
+
<div className="text-sm text-theme-text-tertiary">Loading policy results…</div>
|
|
49
|
+
</Section>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (error) {
|
|
54
|
+
// A denial is an expected state on a namespace-scoped account, not a fault.
|
|
55
|
+
if (isForbiddenError(error)) {
|
|
56
|
+
return (
|
|
57
|
+
<Section title={TITLE} icon={ShieldQuestion}>
|
|
58
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
59
|
+
You don’t have permission to view policy results in this namespace.
|
|
60
|
+
</div>
|
|
61
|
+
</Section>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<Section title={TITLE} icon={ShieldAlert}>
|
|
66
|
+
<div className="text-sm text-red-400">{`Could not load policy results: ${error.message}`}</div>
|
|
67
|
+
</Section>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// No engine installed — the section is genuinely not applicable, so it is
|
|
72
|
+
// absent rather than showing a reassuring "nothing to report". A disclaimer on
|
|
73
|
+
// every cluster without a policy engine would be noise.
|
|
74
|
+
if (!data || data.status === 'not_installed') return null
|
|
75
|
+
|
|
76
|
+
if (!data.evaluated) {
|
|
77
|
+
return (
|
|
78
|
+
<Section title={TITLE} icon={ShieldQuestion}>
|
|
79
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
80
|
+
{(data.status === 'warmup'
|
|
81
|
+
? 'Policy results are still loading from the cluster.'
|
|
82
|
+
: 'Policy results are unavailable, so this resource has not been checked.') +
|
|
83
|
+
(data.reasonCode ? ` (${data.reasonCode})` : '')}
|
|
84
|
+
</div>
|
|
85
|
+
</Section>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { counts, findings } = data
|
|
90
|
+
const checked = counts.pass + counts.fail + counts.warn + counts.error + counts.skip
|
|
91
|
+
|
|
92
|
+
// Evaluated, but no policy selected this resource at all. Distinct from
|
|
93
|
+
// "passing everything": nothing examined it, so there is nothing to pass.
|
|
94
|
+
if (checked === 0) {
|
|
95
|
+
return (
|
|
96
|
+
<Section title={TITLE} icon={ShieldQuestion}>
|
|
97
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
98
|
+
No policy applies to this resource.
|
|
99
|
+
</div>
|
|
100
|
+
<PartialCoverageNote data={data} />
|
|
101
|
+
</Section>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const failing = findings.length
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Section
|
|
109
|
+
title={TITLE}
|
|
110
|
+
icon={failing > 0 ? ShieldAlert : ShieldCheck}
|
|
111
|
+
defaultExpanded={failing > 0}
|
|
112
|
+
>
|
|
113
|
+
{failing === 0 ? (
|
|
114
|
+
<div className="text-sm text-theme-text-secondary">
|
|
115
|
+
{/* "All" is a claim about the resource, and it only holds when the
|
|
116
|
+
caller was shown everything. With results withheld it becomes an
|
|
117
|
+
all-clear on a question that was never fully asked. */}
|
|
118
|
+
{coverageIsPartial(data)
|
|
119
|
+
? `${counts.pass} ${counts.pass === 1 ? 'check' : 'checks'} passing.`
|
|
120
|
+
: `All ${counts.pass} ${counts.pass === 1 ? 'check' : 'checks'} passing.`}
|
|
121
|
+
</div>
|
|
122
|
+
) : (
|
|
123
|
+
<div className="space-y-2">
|
|
124
|
+
{findings.map((f, i) => (
|
|
125
|
+
<PolicyFindingRow key={`${f.policy}/${f.rule}/${i}`} finding={f} />
|
|
126
|
+
))}
|
|
127
|
+
{counts.pass > 0 && (
|
|
128
|
+
<div className="text-xs text-theme-text-tertiary pt-1">
|
|
129
|
+
{`${counts.pass} other ${counts.pass === 1 ? 'check' : 'checks'} passing`}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
<PartialCoverageNote data={data} />
|
|
135
|
+
</Section>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function PolicyFindingRow({ finding }: { finding: PolicyResourceFinding }) {
|
|
140
|
+
const tone = resultTone(finding.result)
|
|
141
|
+
return (
|
|
142
|
+
<div className="flex items-start gap-2 min-w-0">
|
|
143
|
+
<span className={clsx('badge shrink-0', tone.badge)}>{tone.label}</span>
|
|
144
|
+
<div className="min-w-0">
|
|
145
|
+
<div className="text-sm text-theme-text-primary truncate" title={finding.policy}>
|
|
146
|
+
{finding.policy}
|
|
147
|
+
{finding.rule && finding.rule !== finding.policy && (
|
|
148
|
+
<span className="text-theme-text-tertiary"> · {finding.rule}</span>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
{finding.message && (
|
|
152
|
+
<div className="text-xs text-theme-text-secondary">{finding.message}</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether findings are missing from this answer. Two different gaps reach here:
|
|
161
|
+
* `deniedGroups` is a family radar's own probe could not read, `withheldByFamily`
|
|
162
|
+
* one THIS caller may not — and the counts exclude both, so a resource whose only
|
|
163
|
+
* failure came from either reads as passing everything.
|
|
164
|
+
*
|
|
165
|
+
* Staleness is deliberately not one of them. A frozen index dates the answer
|
|
166
|
+
* rather than shrinking it, and "as of the last sync, all passed" is still true.
|
|
167
|
+
*/
|
|
168
|
+
function coverageIsPartial(data: PolicyResourceResponse): boolean {
|
|
169
|
+
return (data.withheldByFamily ?? 0) > 0 || (data.deniedGroups ?? []).length > 0
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A result can be real and incomplete at the same time. Saying "all checks
|
|
174
|
+
* passing" without mentioning that claims coverage we do not have — so this note
|
|
175
|
+
* and the word "All" above it read the same predicate rather than each deciding
|
|
176
|
+
* for itself.
|
|
177
|
+
*/
|
|
178
|
+
function PartialCoverageNote({ data }: { data: PolicyResourceResponse }) {
|
|
179
|
+
const denied = data.deniedGroups ?? []
|
|
180
|
+
const withheld = data.withheldByFamily ?? 0
|
|
181
|
+
if (!coverageIsPartial(data) && data.liveUpdates) return null
|
|
182
|
+
return (
|
|
183
|
+
<div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-tertiary">
|
|
184
|
+
{withheld > 0 && (
|
|
185
|
+
<div>
|
|
186
|
+
{withheld === 1
|
|
187
|
+
? '1 result is not shown here because you don’t have permission to read the report it came from.'
|
|
188
|
+
: `${withheld} results are not shown here because you don’t have permission to read the reports they came from.`}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
{denied.length > 0 && (
|
|
192
|
+
<div>{`Some policy results could not be read (${denied.join(', ')}), so this list may be incomplete.`}</div>
|
|
193
|
+
)}
|
|
194
|
+
{!data.liveUpdates && <div>These results are not updating live.</div>}
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|