@skyhook-io/radar-app 1.13.3 → 1.13.4
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 +6 -6
- package/src/App.tsx +68 -23
- package/src/RadarApp.tsx +12 -1
- package/src/api/client.ts +24 -10
- package/src/components/diagnose/DiagnoseContext.test.ts +95 -0
- package/src/components/diagnose/DiagnoseContext.tsx +402 -77
- package/src/components/diagnose/DiagnoseSurface.test.tsx +53 -34
- package/src/components/diagnose/DiagnoseSurface.tsx +167 -92
- package/src/components/diagnose/Home.test.tsx +23 -1
- package/src/components/diagnose/Home.tsx +49 -3
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +665 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +631 -25
- package/src/components/diagnose/InvestigationView.tsx +5 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +2 -3
- package/src/components/diagnose/investigationEvidence.test.ts +2102 -9
- package/src/components/diagnose/investigationEvidence.ts +1357 -65
- package/src/components/diagnose/investigationSourceFocus.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +55 -1
- package/src/components/diagnose/parts.tsx +68 -1
- package/src/components/resource/HPACharts.render.test.tsx +77 -0
- package/src/components/resource/HPACharts.tsx +32 -25
- package/src/components/resources/ResourcesView.tsx +27 -2
- package/src/index.ts +7 -5
|
@@ -12,6 +12,7 @@ import { clsx } from "clsx";
|
|
|
12
12
|
import {
|
|
13
13
|
Activity,
|
|
14
14
|
AlertTriangle,
|
|
15
|
+
BellRing,
|
|
15
16
|
Boxes,
|
|
16
17
|
Bug,
|
|
17
18
|
CheckCircle2,
|
|
@@ -20,8 +21,10 @@ import {
|
|
|
20
21
|
FileClock,
|
|
21
22
|
FileSearch,
|
|
22
23
|
Info,
|
|
24
|
+
KeyRound,
|
|
23
25
|
ListTree,
|
|
24
26
|
Network,
|
|
27
|
+
Package,
|
|
25
28
|
ScrollText,
|
|
26
29
|
SearchCheck,
|
|
27
30
|
ShieldAlert,
|
|
@@ -79,8 +82,15 @@ export {
|
|
|
79
82
|
} from "./useDisclosureReveal";
|
|
80
83
|
export const VISIBLE_LOG_EVIDENCE_LINES = 12;
|
|
81
84
|
|
|
85
|
+
/** The Timeline scope a changes card can open: one resource name in one namespace. */
|
|
86
|
+
export interface InvestigationTimelineScope {
|
|
87
|
+
namespace?: string;
|
|
88
|
+
name: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
const EvidenceNavigationContext = createContext<{
|
|
83
92
|
onOpenResource?: (ref: DiagnosisResourceRef) => void;
|
|
93
|
+
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
84
94
|
revealSourceId?: string;
|
|
85
95
|
revealRequestId?: number;
|
|
86
96
|
expandedGroupIds?: ReadonlySet<string>;
|
|
@@ -91,7 +101,7 @@ const EvidenceNavigationContext = createContext<{
|
|
|
91
101
|
function evidenceTypePrefersFullRow(
|
|
92
102
|
type: InvestigationEvidenceData["type"],
|
|
93
103
|
): boolean {
|
|
94
|
-
return type === "logs" || type === "events";
|
|
104
|
+
return type === "logs" || type === "events" || type === "alerts";
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
// Supporting evidence becomes a two-column grid when the pane is wide enough.
|
|
@@ -134,6 +144,10 @@ export function partitionInvestigationEvidence(
|
|
|
134
144
|
const collectionByGroup = new Map<string, EvidenceCollection>();
|
|
135
145
|
const adverse = (group: InvestigationEvidenceGroup) =>
|
|
136
146
|
["error", "alert", "warning"].includes(group.latest.tone);
|
|
147
|
+
// Broader cards are facts about something other than the target. They are
|
|
148
|
+
// withheld unless cited, and the pane says how many were withheld so a
|
|
149
|
+
// reader knows the agent looked at things it did not build its case on.
|
|
150
|
+
let hiddenBroader = 0;
|
|
137
151
|
for (const group of groups) {
|
|
138
152
|
const broader = group.latest.relevance === "broader";
|
|
139
153
|
// Citations select tool results, not individual rows in a broad search.
|
|
@@ -142,22 +156,20 @@ export function partitionInvestigationEvidence(
|
|
|
142
156
|
const link = resolution?.links.find(
|
|
143
157
|
(item) => item.originalGroupId === group.id,
|
|
144
158
|
);
|
|
145
|
-
const focused =
|
|
146
|
-
group.latest.data.type,
|
|
147
|
-
);
|
|
159
|
+
const focused = FOCUSED_EVIDENCE_TYPES.includes(group.latest.data.type);
|
|
148
160
|
const sourceGroups = link
|
|
149
161
|
? groups.filter(
|
|
150
162
|
(candidate) =>
|
|
151
|
-
|
|
152
|
-
candidate.latest.data.type,
|
|
153
|
-
) &&
|
|
163
|
+
FOCUSED_EVIDENCE_TYPES.includes(candidate.latest.data.type) &&
|
|
154
164
|
candidate.observations.some(
|
|
155
165
|
(observation) => observation.source.id === link.source.id,
|
|
156
166
|
),
|
|
157
167
|
)
|
|
158
168
|
: [];
|
|
159
|
-
if (!selected.has(group.id) || !focused || sourceGroups.length !== 1)
|
|
169
|
+
if (!selected.has(group.id) || !focused || sourceGroups.length !== 1) {
|
|
170
|
+
if (!group.historical) hiddenBroader += 1;
|
|
160
171
|
continue;
|
|
172
|
+
}
|
|
161
173
|
}
|
|
162
174
|
const main =
|
|
163
175
|
!group.historical &&
|
|
@@ -180,9 +192,21 @@ export function partitionInvestigationEvidence(
|
|
|
180
192
|
Number(adverse(right)) - Number(adverse(left)) ||
|
|
181
193
|
left.firstOrder - right.firstOrder,
|
|
182
194
|
);
|
|
183
|
-
return { ...collections, collectionByGroup };
|
|
195
|
+
return { ...collections, collectionByGroup, hiddenBroader };
|
|
184
196
|
}
|
|
185
197
|
|
|
198
|
+
// Kinds whose card is one unambiguous subject, so a citation of their source
|
|
199
|
+
// can promote exactly that fact. A broad search (issues, inventory, events)
|
|
200
|
+
// yields many rows per source; a citation cannot pick one of those.
|
|
201
|
+
const FOCUSED_EVIDENCE_TYPES: readonly InvestigationEvidenceData["type"][] = [
|
|
202
|
+
"resource",
|
|
203
|
+
"logs",
|
|
204
|
+
"crash",
|
|
205
|
+
"helm",
|
|
206
|
+
"alerts",
|
|
207
|
+
"permissions",
|
|
208
|
+
];
|
|
209
|
+
|
|
186
210
|
export function investigationEvidenceRevealCollection(
|
|
187
211
|
projection: InvestigationEvidenceProjection,
|
|
188
212
|
sourceId: string,
|
|
@@ -210,6 +234,7 @@ export function InvestigationEvidencePane({
|
|
|
210
234
|
onViewSource,
|
|
211
235
|
onViewActivity,
|
|
212
236
|
onOpenResource,
|
|
237
|
+
onOpenTimeline,
|
|
213
238
|
afterEvidence,
|
|
214
239
|
revealRequest,
|
|
215
240
|
onRevealReady,
|
|
@@ -227,6 +252,8 @@ export function InvestigationEvidencePane({
|
|
|
227
252
|
onViewActivity: () => void;
|
|
228
253
|
/** Opens an evidence subject in Radar when the producer identified it exactly. */
|
|
229
254
|
onOpenResource?: (ref: DiagnosisResourceRef) => void;
|
|
255
|
+
/** Opens Radar's Timeline filtered to the resource a changes card is about. */
|
|
256
|
+
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
230
257
|
/** Actions follow the complete evidence section, including its disclosures. */
|
|
231
258
|
afterEvidence?: ReactNode;
|
|
232
259
|
/** Explicit Activity → Findings navigation, including repeat clicks. */
|
|
@@ -388,6 +415,17 @@ export function InvestigationEvidencePane({
|
|
|
388
415
|
))}
|
|
389
416
|
</div>
|
|
390
417
|
|
|
418
|
+
{partition.hiddenBroader > 0 ? (
|
|
419
|
+
<p
|
|
420
|
+
className="text-xs text-theme-text-tertiary"
|
|
421
|
+
data-testid="investigation-hidden-evidence"
|
|
422
|
+
>
|
|
423
|
+
{partition.hiddenBroader === 1
|
|
424
|
+
? "1 result about another resource is not shown; it appears here when the assessment cites it."
|
|
425
|
+
: `${partition.hiddenBroader} results about other resources are not shown; they appear here when the assessment cites them.`}
|
|
426
|
+
</p>
|
|
427
|
+
) : null}
|
|
428
|
+
|
|
391
429
|
{!hasCurrentEvidence ? (
|
|
392
430
|
<EmptyCollection
|
|
393
431
|
collecting={collecting}
|
|
@@ -426,6 +464,7 @@ export function InvestigationEvidencePane({
|
|
|
426
464
|
<EvidenceNavigationContext.Provider
|
|
427
465
|
value={{
|
|
428
466
|
onOpenResource,
|
|
467
|
+
onOpenTimeline,
|
|
429
468
|
expandedGroupIds,
|
|
430
469
|
onGroupOpenChange,
|
|
431
470
|
revealSourceId: revealRequest?.sourceId,
|
|
@@ -633,6 +672,7 @@ function CoverageStrip({
|
|
|
633
672
|
}) {
|
|
634
673
|
const hasError = groups.some((group) => group.hasError);
|
|
635
674
|
const historyOnly = groups.every((group) => group.historyOnly);
|
|
675
|
+
const quiet = historyOnly;
|
|
636
676
|
const { elementRef, revealAfterToggle } =
|
|
637
677
|
useDisclosureReveal<HTMLDivElement>();
|
|
638
678
|
const regionId = "investigation-evidence-coverage";
|
|
@@ -652,7 +692,7 @@ function CoverageStrip({
|
|
|
652
692
|
}}
|
|
653
693
|
className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left hover:bg-theme-hover/50"
|
|
654
694
|
>
|
|
655
|
-
{
|
|
695
|
+
{quiet ? (
|
|
656
696
|
<Info
|
|
657
697
|
className="h-4 w-4 shrink-0 text-theme-text-tertiary"
|
|
658
698
|
aria-hidden
|
|
@@ -669,7 +709,7 @@ function CoverageStrip({
|
|
|
669
709
|
<span
|
|
670
710
|
className={clsx(
|
|
671
711
|
"block text-xs",
|
|
672
|
-
|
|
712
|
+
quiet
|
|
673
713
|
? "font-medium text-theme-text-secondary"
|
|
674
714
|
: "font-semibold text-theme-text-primary",
|
|
675
715
|
)}
|
|
@@ -735,6 +775,65 @@ function CoverageGroupRow({
|
|
|
735
775
|
const regionId = useId();
|
|
736
776
|
const { elementRef, revealAfterToggle } =
|
|
737
777
|
useDisclosureReveal<HTMLDivElement>();
|
|
778
|
+
// One limitation whose message is the whole summary would repeat itself as
|
|
779
|
+
// its own detail row; show its source link on the summary row instead.
|
|
780
|
+
const single =
|
|
781
|
+
group.limitations.length === 1 &&
|
|
782
|
+
group.limitations[0].message === group.summary
|
|
783
|
+
? group.limitations[0]
|
|
784
|
+
: undefined;
|
|
785
|
+
const anchors = sourceIds.map((id) => (
|
|
786
|
+
<span
|
|
787
|
+
key={id}
|
|
788
|
+
id={investigationEvidenceSourceDomId(id)}
|
|
789
|
+
className="sr-only scroll-mt-14"
|
|
790
|
+
aria-hidden
|
|
791
|
+
/>
|
|
792
|
+
));
|
|
793
|
+
if (single) {
|
|
794
|
+
return (
|
|
795
|
+
<div
|
|
796
|
+
ref={elementRef}
|
|
797
|
+
data-evidence-source-container
|
|
798
|
+
tabIndex={-1}
|
|
799
|
+
aria-label={`Evidence limitation for ${group.label}: ${group.summary}`}
|
|
800
|
+
className="flex items-center gap-2 px-3 py-2.5 text-xs outline-none focus:ring-2 focus:ring-accent/40"
|
|
801
|
+
>
|
|
802
|
+
{anchors}
|
|
803
|
+
{group.hasError ? (
|
|
804
|
+
<CircleAlert
|
|
805
|
+
className="h-3.5 w-3.5 shrink-0 text-red-400"
|
|
806
|
+
aria-hidden
|
|
807
|
+
/>
|
|
808
|
+
) : single.kind === "truncated" ? (
|
|
809
|
+
<AlertTriangle
|
|
810
|
+
className="h-3.5 w-3.5 shrink-0 text-amber-500"
|
|
811
|
+
aria-hidden
|
|
812
|
+
/>
|
|
813
|
+
) : (
|
|
814
|
+
<Info
|
|
815
|
+
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
816
|
+
aria-hidden
|
|
817
|
+
/>
|
|
818
|
+
)}
|
|
819
|
+
<p className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
|
|
820
|
+
<span className="font-medium text-theme-text-primary">
|
|
821
|
+
{group.label}:
|
|
822
|
+
</span>{" "}
|
|
823
|
+
{group.summary}
|
|
824
|
+
</p>
|
|
825
|
+
{single.sources.at(-1) ? (
|
|
826
|
+
<SourceButton
|
|
827
|
+
ariaLabel={`View source for ${group.label}`}
|
|
828
|
+
buttonLabel={
|
|
829
|
+
single.sources.length > 1 ? "View latest in Activity" : undefined
|
|
830
|
+
}
|
|
831
|
+
onClick={() => onViewSource(single.sources.at(-1)!.id)}
|
|
832
|
+
/>
|
|
833
|
+
) : null}
|
|
834
|
+
</div>
|
|
835
|
+
);
|
|
836
|
+
}
|
|
738
837
|
return (
|
|
739
838
|
<div
|
|
740
839
|
ref={elementRef}
|
|
@@ -743,14 +842,7 @@ function CoverageGroupRow({
|
|
|
743
842
|
aria-label={`Evidence limitation for ${group.label}: ${group.summary}`}
|
|
744
843
|
className="outline-none focus:ring-2 focus:ring-accent/40"
|
|
745
844
|
>
|
|
746
|
-
{
|
|
747
|
-
<span
|
|
748
|
-
key={id}
|
|
749
|
-
id={investigationEvidenceSourceDomId(id)}
|
|
750
|
-
className="sr-only scroll-mt-14"
|
|
751
|
-
aria-hidden
|
|
752
|
-
/>
|
|
753
|
-
))}
|
|
845
|
+
{anchors}
|
|
754
846
|
<button
|
|
755
847
|
type="button"
|
|
756
848
|
aria-expanded={open}
|
|
@@ -891,6 +983,7 @@ function EvidenceCard({
|
|
|
891
983
|
}) {
|
|
892
984
|
const {
|
|
893
985
|
onOpenResource,
|
|
986
|
+
onOpenTimeline,
|
|
894
987
|
revealSourceId,
|
|
895
988
|
revealRequestId,
|
|
896
989
|
citedOrderByGroup,
|
|
@@ -1072,6 +1165,12 @@ function EvidenceCard({
|
|
|
1072
1165
|
onOpenResource={onOpenResource}
|
|
1073
1166
|
compact
|
|
1074
1167
|
/>
|
|
1168
|
+
{observation.data.type === "changes" && observation.data.subject ? (
|
|
1169
|
+
<OpenTimelineButton
|
|
1170
|
+
scope={observation.data.subject}
|
|
1171
|
+
onOpenTimeline={onOpenTimeline}
|
|
1172
|
+
/>
|
|
1173
|
+
) : null}
|
|
1075
1174
|
</div>
|
|
1076
1175
|
</div>
|
|
1077
1176
|
{canExpand ? (
|
|
@@ -1146,6 +1245,36 @@ function OpenResourceButton({
|
|
|
1146
1245
|
);
|
|
1147
1246
|
}
|
|
1148
1247
|
|
|
1248
|
+
function OpenTimelineButton({
|
|
1249
|
+
scope,
|
|
1250
|
+
onOpenTimeline,
|
|
1251
|
+
}: {
|
|
1252
|
+
scope: InvestigationTimelineScope;
|
|
1253
|
+
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
1254
|
+
}) {
|
|
1255
|
+
if (!onOpenTimeline) return null;
|
|
1256
|
+
const identity = `${scope.namespace ? `${scope.namespace}/` : ""}${scope.name}`;
|
|
1257
|
+
const label = `Open the Timeline for ${identity}`;
|
|
1258
|
+
return (
|
|
1259
|
+
<Tooltip
|
|
1260
|
+
content={label}
|
|
1261
|
+
delay={350}
|
|
1262
|
+
position="left"
|
|
1263
|
+
wrapperClassName="flex shrink-0"
|
|
1264
|
+
>
|
|
1265
|
+
<button
|
|
1266
|
+
type="button"
|
|
1267
|
+
aria-label={label}
|
|
1268
|
+
onClick={() => onOpenTimeline(scope)}
|
|
1269
|
+
className="flex h-7 shrink-0 items-center justify-center gap-1 rounded px-2 text-xs font-medium text-theme-text-tertiary transition-colors hover:bg-theme-hover hover:text-accent-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
|
1270
|
+
>
|
|
1271
|
+
<FileClock className="h-3.5 w-3.5" aria-hidden />
|
|
1272
|
+
<span className="hidden @min-[540px]/card:inline">Timeline</span>
|
|
1273
|
+
</button>
|
|
1274
|
+
</Tooltip>
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1149
1278
|
function uniquePrimarySources(
|
|
1150
1279
|
group: InvestigationEvidenceGroup,
|
|
1151
1280
|
): InvestigationEvidenceSource[] {
|
|
@@ -1197,6 +1326,12 @@ function EvidenceBody({
|
|
|
1197
1326
|
return (
|
|
1198
1327
|
<p className="text-xs text-theme-text-secondary">{data.message}</p>
|
|
1199
1328
|
);
|
|
1329
|
+
case "alerts":
|
|
1330
|
+
return <AlertsBody data={data} />;
|
|
1331
|
+
case "helm":
|
|
1332
|
+
return <HelmBody data={data} />;
|
|
1333
|
+
case "permissions":
|
|
1334
|
+
return <PermissionsBody data={data} />;
|
|
1200
1335
|
}
|
|
1201
1336
|
}
|
|
1202
1337
|
|
|
@@ -1211,10 +1346,12 @@ function evidenceHasDetails(
|
|
|
1211
1346
|
const message = data.issue.message?.trim();
|
|
1212
1347
|
return Boolean(
|
|
1213
1348
|
(cause && cause !== summary) ||
|
|
1214
|
-
(message && message !== summary && message !== cause)
|
|
1349
|
+
(message && message !== summary && message !== cause) ||
|
|
1350
|
+
data.pods?.length,
|
|
1215
1351
|
);
|
|
1216
1352
|
}
|
|
1217
1353
|
case "startup":
|
|
1354
|
+
return (data.pods?.length ?? 0) > 1;
|
|
1218
1355
|
case "receipt":
|
|
1219
1356
|
return false;
|
|
1220
1357
|
case "resource": {
|
|
@@ -1271,6 +1408,18 @@ function IssueBody({
|
|
|
1271
1408
|
{issue.namespace ? `${issue.namespace}/` : ""}
|
|
1272
1409
|
{issue.name}
|
|
1273
1410
|
</Badge>
|
|
1411
|
+
{data.pods && data.pods.length > 0 ? (
|
|
1412
|
+
<>
|
|
1413
|
+
<Badge tone="structural" size="sm">
|
|
1414
|
+
{data.pods.length === 1 ? "1 Pod" : `${data.pods.length} Pods`}
|
|
1415
|
+
</Badge>
|
|
1416
|
+
{data.pods.map((pod) => (
|
|
1417
|
+
<Badge key={pod} tone="structural" size="sm">
|
|
1418
|
+
{pod}
|
|
1419
|
+
</Badge>
|
|
1420
|
+
))}
|
|
1421
|
+
</>
|
|
1422
|
+
) : null}
|
|
1274
1423
|
</div>
|
|
1275
1424
|
{showCause ? (
|
|
1276
1425
|
<p className="text-sm font-medium leading-relaxed text-theme-text-primary">
|
|
@@ -1288,15 +1437,18 @@ function IssueBody({
|
|
|
1288
1437
|
|
|
1289
1438
|
function StartupBody({ data }: { data: EvidenceDataOf<"startup"> }) {
|
|
1290
1439
|
const blocker = data.blocker;
|
|
1440
|
+
const pods = data.pods && data.pods.length > 1 ? data.pods : [blocker.name];
|
|
1291
1441
|
return (
|
|
1292
1442
|
<div className="space-y-2">
|
|
1293
1443
|
<div className="flex flex-wrap gap-1.5">
|
|
1294
1444
|
<Badge tone="structural" size="sm">
|
|
1295
|
-
{blocker.kind}
|
|
1296
|
-
</Badge>
|
|
1297
|
-
<Badge tone="structural" size="sm">
|
|
1298
|
-
{blocker.name}
|
|
1445
|
+
{pods.length > 1 ? `${pods.length} ${blocker.kind}s` : blocker.kind}
|
|
1299
1446
|
</Badge>
|
|
1447
|
+
{pods.map((pod) => (
|
|
1448
|
+
<Badge key={pod} tone="structural" size="sm">
|
|
1449
|
+
{pod}
|
|
1450
|
+
</Badge>
|
|
1451
|
+
))}
|
|
1300
1452
|
<Badge severity={severityBadge(blocker.severity)} size="sm">
|
|
1301
1453
|
{blocker.severity}
|
|
1302
1454
|
</Badge>
|
|
@@ -1585,7 +1737,11 @@ function EventsBody({ data }: { data: EvidenceDataOf<"events"> }) {
|
|
|
1585
1737
|
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
|
1586
1738
|
<span className="text-xs font-semibold text-theme-text-primary">
|
|
1587
1739
|
{event.reason}
|
|
1588
|
-
{event.count > 1 ?
|
|
1740
|
+
{event.count > 1 ? (
|
|
1741
|
+
<span className="ml-1.5 font-mono text-[11px] font-normal text-theme-text-tertiary">
|
|
1742
|
+
×{event.count}
|
|
1743
|
+
</span>
|
|
1744
|
+
) : null}
|
|
1589
1745
|
</span>
|
|
1590
1746
|
<Tooltip
|
|
1591
1747
|
content={new Date(event.lastTimestamp).toLocaleString()}
|
|
@@ -2003,6 +2159,444 @@ function InventoryBody({ data }: { data: EvidenceDataOf<"inventory"> }) {
|
|
|
2003
2159
|
);
|
|
2004
2160
|
}
|
|
2005
2161
|
|
|
2162
|
+
const VISIBLE_ALERT_LABELS = 6;
|
|
2163
|
+
|
|
2164
|
+
function alertStateSeverity(state: string | undefined) {
|
|
2165
|
+
switch ((state ?? "").toLowerCase()) {
|
|
2166
|
+
case "firing":
|
|
2167
|
+
return "error" as const;
|
|
2168
|
+
case "pending":
|
|
2169
|
+
return "warning" as const;
|
|
2170
|
+
case "inactive":
|
|
2171
|
+
return "success" as const;
|
|
2172
|
+
default:
|
|
2173
|
+
return "neutral" as const;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function AlertsBody({ data }: { data: EvidenceDataOf<"alerts"> }) {
|
|
2178
|
+
const { rule, instances, annotations } = data;
|
|
2179
|
+
const health = rule.health?.toLowerCase();
|
|
2180
|
+
// Target instances lead; the rest stay visible in producer order.
|
|
2181
|
+
const ordered = [...instances].sort(
|
|
2182
|
+
(left, right) => Number(right.namesTarget) - Number(left.namesTarget),
|
|
2183
|
+
);
|
|
2184
|
+
const annotationEntries = Object.entries(annotations);
|
|
2185
|
+
return (
|
|
2186
|
+
<div className="space-y-2.5">
|
|
2187
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
2188
|
+
{rule.state ? (
|
|
2189
|
+
<Badge severity={alertStateSeverity(rule.state)} size="sm">
|
|
2190
|
+
{rule.state}
|
|
2191
|
+
</Badge>
|
|
2192
|
+
) : null}
|
|
2193
|
+
{rule.labels.severity ? (
|
|
2194
|
+
<Badge severity={severityBadge(rule.labels.severity)} size="sm">
|
|
2195
|
+
severity {rule.labels.severity}
|
|
2196
|
+
</Badge>
|
|
2197
|
+
) : null}
|
|
2198
|
+
<Badge tone="structural" size="sm">
|
|
2199
|
+
{rule.group}
|
|
2200
|
+
</Badge>
|
|
2201
|
+
{health && health !== "ok" ? (
|
|
2202
|
+
<Badge severity={health === "err" ? "error" : "neutral"} size="sm">
|
|
2203
|
+
health {health}
|
|
2204
|
+
</Badge>
|
|
2205
|
+
) : null}
|
|
2206
|
+
</div>
|
|
2207
|
+
{ordered.length > 0 ? (
|
|
2208
|
+
<ol className="max-h-64 space-y-1.5 overflow-y-auto pr-1">
|
|
2209
|
+
{ordered.map((instance, index) => {
|
|
2210
|
+
const labels = Object.entries(instance.labels).filter(
|
|
2211
|
+
([key]) => key !== "alertname" && key !== "severity",
|
|
2212
|
+
);
|
|
2213
|
+
const hidden = labels.length - VISIBLE_ALERT_LABELS;
|
|
2214
|
+
return (
|
|
2215
|
+
<li
|
|
2216
|
+
key={`${instance.state}-${index}-${labels.map(([k, v]) => `${k}=${v}`).join(",")}`}
|
|
2217
|
+
className="rounded-md border border-theme-border/70 bg-theme-base/30 px-2.5 py-2"
|
|
2218
|
+
>
|
|
2219
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
2220
|
+
<Badge
|
|
2221
|
+
severity={alertStateSeverity(instance.state)}
|
|
2222
|
+
size="sm"
|
|
2223
|
+
>
|
|
2224
|
+
{instance.state}
|
|
2225
|
+
</Badge>
|
|
2226
|
+
{instance.namesTarget ? (
|
|
2227
|
+
<Badge severity="info" size="sm">
|
|
2228
|
+
names this resource
|
|
2229
|
+
</Badge>
|
|
2230
|
+
) : null}
|
|
2231
|
+
{instance.value ? (
|
|
2232
|
+
<span className="font-mono text-xs text-theme-text-secondary">
|
|
2233
|
+
value {instance.value}
|
|
2234
|
+
</span>
|
|
2235
|
+
) : null}
|
|
2236
|
+
{instance.activeAt ? (
|
|
2237
|
+
<Tooltip
|
|
2238
|
+
content={new Date(instance.activeAt).toLocaleString()}
|
|
2239
|
+
delay={150}
|
|
2240
|
+
position="left"
|
|
2241
|
+
wrapperClassName="ml-auto"
|
|
2242
|
+
>
|
|
2243
|
+
<time
|
|
2244
|
+
dateTime={instance.activeAt}
|
|
2245
|
+
className="text-xs text-theme-text-tertiary"
|
|
2246
|
+
>
|
|
2247
|
+
active {formatRelativeAgeTime(instance.activeAt)}
|
|
2248
|
+
</time>
|
|
2249
|
+
</Tooltip>
|
|
2250
|
+
) : null}
|
|
2251
|
+
</div>
|
|
2252
|
+
{labels.length > 0 ? (
|
|
2253
|
+
<div className="mt-1.5 flex flex-wrap gap-1 font-mono text-[11px] text-theme-text-secondary">
|
|
2254
|
+
{labels
|
|
2255
|
+
.slice(0, VISIBLE_ALERT_LABELS)
|
|
2256
|
+
.map(([key, value]) => (
|
|
2257
|
+
<span
|
|
2258
|
+
key={key}
|
|
2259
|
+
className="rounded bg-theme-base px-1.5 py-0.5 [overflow-wrap:anywhere]"
|
|
2260
|
+
>
|
|
2261
|
+
{key}={value}
|
|
2262
|
+
</span>
|
|
2263
|
+
))}
|
|
2264
|
+
{hidden > 0 ? (
|
|
2265
|
+
<span className="px-1 py-0.5 text-theme-text-tertiary">
|
|
2266
|
+
+{hidden} more
|
|
2267
|
+
</span>
|
|
2268
|
+
) : null}
|
|
2269
|
+
</div>
|
|
2270
|
+
) : null}
|
|
2271
|
+
</li>
|
|
2272
|
+
);
|
|
2273
|
+
})}
|
|
2274
|
+
</ol>
|
|
2275
|
+
) : (
|
|
2276
|
+
<p className="text-xs italic text-theme-text-tertiary">
|
|
2277
|
+
No active instances were reported for this rule.
|
|
2278
|
+
</p>
|
|
2279
|
+
)}
|
|
2280
|
+
{annotationEntries.length > 0 ? (
|
|
2281
|
+
<dl className="space-y-1 text-xs">
|
|
2282
|
+
{annotationEntries.map(([key, value]) => (
|
|
2283
|
+
<div key={key} className="flex min-w-0 gap-2">
|
|
2284
|
+
<dt className="shrink-0 text-theme-text-tertiary">{key}</dt>
|
|
2285
|
+
<dd className="min-w-0 leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
2286
|
+
{value}
|
|
2287
|
+
</dd>
|
|
2288
|
+
</div>
|
|
2289
|
+
))}
|
|
2290
|
+
</dl>
|
|
2291
|
+
) : null}
|
|
2292
|
+
{rule.query ? (
|
|
2293
|
+
<TerminalBlock label="Rule expression">{rule.query}</TerminalBlock>
|
|
2294
|
+
) : null}
|
|
2295
|
+
</div>
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function helmStatusSeverity(status: string) {
|
|
2300
|
+
const normalized = status.toLowerCase();
|
|
2301
|
+
if (normalized === "deployed") return "success" as const;
|
|
2302
|
+
if (normalized.includes("failed")) return "error" as const;
|
|
2303
|
+
if (normalized.startsWith("pending") || normalized === "uninstalling")
|
|
2304
|
+
return "info" as const;
|
|
2305
|
+
return "warning" as const;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function HelmBody({ data }: { data: EvidenceDataOf<"helm"> }) {
|
|
2309
|
+
const { onOpenResource } = useContext(EvidenceNavigationContext);
|
|
2310
|
+
const { release } = data;
|
|
2311
|
+
const operation = release.lastOperation;
|
|
2312
|
+
return (
|
|
2313
|
+
<div className="space-y-3">
|
|
2314
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
2315
|
+
<Badge severity={helmStatusSeverity(release.status)} size="sm">
|
|
2316
|
+
{release.status}
|
|
2317
|
+
</Badge>
|
|
2318
|
+
<Badge tone="structural" size="sm">
|
|
2319
|
+
{release.chart}
|
|
2320
|
+
{release.chartVersion ? ` ${release.chartVersion}` : ""}
|
|
2321
|
+
</Badge>
|
|
2322
|
+
<Badge tone="structural" size="sm">
|
|
2323
|
+
revision {release.revision}
|
|
2324
|
+
</Badge>
|
|
2325
|
+
{release.appVersion ? (
|
|
2326
|
+
<Badge tone="note" size="sm">
|
|
2327
|
+
app {release.appVersion}
|
|
2328
|
+
</Badge>
|
|
2329
|
+
) : null}
|
|
2330
|
+
{release.resourceHealth ? (
|
|
2331
|
+
<Badge
|
|
2332
|
+
severity={
|
|
2333
|
+
mapHealthToTone(release.resourceHealth) === "healthy"
|
|
2334
|
+
? "success"
|
|
2335
|
+
: mapHealthToTone(release.resourceHealth) === "unhealthy"
|
|
2336
|
+
? "error"
|
|
2337
|
+
: "warning"
|
|
2338
|
+
}
|
|
2339
|
+
size="sm"
|
|
2340
|
+
>
|
|
2341
|
+
resources {release.resourceHealth}
|
|
2342
|
+
</Badge>
|
|
2343
|
+
) : null}
|
|
2344
|
+
<Tooltip
|
|
2345
|
+
content={new Date(release.updated).toLocaleString()}
|
|
2346
|
+
delay={150}
|
|
2347
|
+
position="left"
|
|
2348
|
+
wrapperClassName="ml-auto"
|
|
2349
|
+
>
|
|
2350
|
+
<time
|
|
2351
|
+
dateTime={release.updated}
|
|
2352
|
+
className="text-xs text-theme-text-tertiary"
|
|
2353
|
+
>
|
|
2354
|
+
updated {formatRelativeAgeTime(release.updated)}
|
|
2355
|
+
</time>
|
|
2356
|
+
</Tooltip>
|
|
2357
|
+
</div>
|
|
2358
|
+
{release.healthIssue ? (
|
|
2359
|
+
<p className="text-xs leading-relaxed text-warning-text">
|
|
2360
|
+
{release.healthIssue}
|
|
2361
|
+
</p>
|
|
2362
|
+
) : null}
|
|
2363
|
+
{release.healthSummary &&
|
|
2364
|
+
release.healthSummary !== release.healthIssue ? (
|
|
2365
|
+
<p className="text-xs leading-relaxed text-theme-text-secondary">
|
|
2366
|
+
{release.healthSummary}
|
|
2367
|
+
</p>
|
|
2368
|
+
) : null}
|
|
2369
|
+
{operation ? (
|
|
2370
|
+
<div className="rounded-md border border-theme-border bg-theme-base/40 px-2.5 py-2">
|
|
2371
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
2372
|
+
<span className="text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
2373
|
+
Last operation
|
|
2374
|
+
</span>
|
|
2375
|
+
<Badge tone="note" size="sm">
|
|
2376
|
+
{operation.kind.replaceAll("_", " ")}
|
|
2377
|
+
</Badge>
|
|
2378
|
+
<Badge
|
|
2379
|
+
severity={
|
|
2380
|
+
operation.status === "completed"
|
|
2381
|
+
? "success"
|
|
2382
|
+
: operation.status === "failed"
|
|
2383
|
+
? "error"
|
|
2384
|
+
: "warning"
|
|
2385
|
+
}
|
|
2386
|
+
size="sm"
|
|
2387
|
+
>
|
|
2388
|
+
{operation.status.replaceAll("_", " ")}
|
|
2389
|
+
</Badge>
|
|
2390
|
+
</div>
|
|
2391
|
+
{operation.message ? (
|
|
2392
|
+
<p className="mt-1 text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
2393
|
+
{operation.message}
|
|
2394
|
+
</p>
|
|
2395
|
+
) : null}
|
|
2396
|
+
</div>
|
|
2397
|
+
) : null}
|
|
2398
|
+
{release.description ? (
|
|
2399
|
+
<p className="text-xs text-theme-text-tertiary [overflow-wrap:anywhere]">
|
|
2400
|
+
{release.description}
|
|
2401
|
+
</p>
|
|
2402
|
+
) : null}
|
|
2403
|
+
{release.storageNamespace &&
|
|
2404
|
+
release.storageNamespace !== release.namespace ? (
|
|
2405
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
2406
|
+
Release metadata stored in namespace {release.storageNamespace}
|
|
2407
|
+
</p>
|
|
2408
|
+
) : null}
|
|
2409
|
+
{release.managedByFluxHelmRelease ? (
|
|
2410
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
2411
|
+
Managed by Flux HelmRelease {release.managedByFluxHelmRelease}
|
|
2412
|
+
</p>
|
|
2413
|
+
) : null}
|
|
2414
|
+
{release.resources.length > 0 ? (
|
|
2415
|
+
<div className="max-h-52 overflow-y-auto rounded-md border border-theme-border">
|
|
2416
|
+
{release.resources.map((owned, index) => (
|
|
2417
|
+
<div
|
|
2418
|
+
key={`${owned.kind}-${owned.namespace}-${owned.name}`}
|
|
2419
|
+
className={clsx(
|
|
2420
|
+
"flex min-w-0 items-center gap-2 px-2.5 py-1.5",
|
|
2421
|
+
index > 0 && "border-t border-theme-border/60",
|
|
2422
|
+
)}
|
|
2423
|
+
>
|
|
2424
|
+
<StatusDot
|
|
2425
|
+
tone={mapHealthToTone(
|
|
2426
|
+
owned.issue ? "unhealthy" : (owned.status ?? ""),
|
|
2427
|
+
)}
|
|
2428
|
+
className="shrink-0"
|
|
2429
|
+
/>
|
|
2430
|
+
<Badge tone="structural" size="sm">
|
|
2431
|
+
{owned.kind}
|
|
2432
|
+
</Badge>
|
|
2433
|
+
<span className="min-w-0 flex-1">
|
|
2434
|
+
<span className="block truncate font-mono text-xs">
|
|
2435
|
+
<ResourceLink
|
|
2436
|
+
name={owned.name}
|
|
2437
|
+
kind={owned.kind}
|
|
2438
|
+
namespace={owned.namespace}
|
|
2439
|
+
group={
|
|
2440
|
+
owned.apiVersion
|
|
2441
|
+
? apiVersionToGroup(owned.apiVersion)
|
|
2442
|
+
: undefined
|
|
2443
|
+
}
|
|
2444
|
+
label={`${owned.namespace ? `${owned.namespace}/` : ""}${owned.name}`}
|
|
2445
|
+
onNavigate={
|
|
2446
|
+
owned.apiVersion && onOpenResource
|
|
2447
|
+
? (ref) => onOpenResource(ref)
|
|
2448
|
+
: undefined
|
|
2449
|
+
}
|
|
2450
|
+
/>
|
|
2451
|
+
</span>
|
|
2452
|
+
{owned.issue ? (
|
|
2453
|
+
<span className="block truncate text-xs text-warning-text">
|
|
2454
|
+
{owned.issue}
|
|
2455
|
+
</span>
|
|
2456
|
+
) : owned.summary || owned.message ? (
|
|
2457
|
+
<span className="block truncate text-xs text-theme-text-tertiary">
|
|
2458
|
+
{owned.summary || owned.message}
|
|
2459
|
+
</span>
|
|
2460
|
+
) : null}
|
|
2461
|
+
</span>
|
|
2462
|
+
{owned.ready || owned.status ? (
|
|
2463
|
+
<span className="ml-auto shrink-0 font-mono text-xs text-theme-text-tertiary">
|
|
2464
|
+
{owned.ready || owned.status}
|
|
2465
|
+
</span>
|
|
2466
|
+
) : null}
|
|
2467
|
+
</div>
|
|
2468
|
+
))}
|
|
2469
|
+
</div>
|
|
2470
|
+
) : null}
|
|
2471
|
+
</div>
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function PermissionsBody({ data }: { data: EvidenceDataOf<"permissions"> }) {
|
|
2476
|
+
const { subject, accessCheck } = data;
|
|
2477
|
+
const subjectBadges = (
|
|
2478
|
+
<>
|
|
2479
|
+
<Badge tone="structural" size="sm">
|
|
2480
|
+
{subject.kind}
|
|
2481
|
+
</Badge>
|
|
2482
|
+
<span className="font-mono text-xs text-theme-text-secondary">
|
|
2483
|
+
{subject.namespace ? `${subject.namespace}/` : ""}
|
|
2484
|
+
{subject.name}
|
|
2485
|
+
</span>
|
|
2486
|
+
</>
|
|
2487
|
+
);
|
|
2488
|
+
if (accessCheck) {
|
|
2489
|
+
const facts = [
|
|
2490
|
+
["Verb", accessCheck.verb],
|
|
2491
|
+
["Resource", accessCheck.resource],
|
|
2492
|
+
["Subresource", accessCheck.subresource],
|
|
2493
|
+
["API group", accessCheck.group || "core"],
|
|
2494
|
+
["Namespace", accessCheck.namespace || "cluster-wide"],
|
|
2495
|
+
["Name", accessCheck.resourceName],
|
|
2496
|
+
] as const;
|
|
2497
|
+
return (
|
|
2498
|
+
<div className="space-y-2.5">
|
|
2499
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
2500
|
+
{subjectBadges}
|
|
2501
|
+
<Badge
|
|
2502
|
+
severity={accessCheck.allowed ? "success" : "warning"}
|
|
2503
|
+
size="sm"
|
|
2504
|
+
>
|
|
2505
|
+
{accessCheck.allowed
|
|
2506
|
+
? "allowed"
|
|
2507
|
+
: accessCheck.denied
|
|
2508
|
+
? "denied"
|
|
2509
|
+
: "not allowed"}
|
|
2510
|
+
</Badge>
|
|
2511
|
+
</div>
|
|
2512
|
+
<dl className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
|
|
2513
|
+
{facts.map(([label, value]) => (
|
|
2514
|
+
<ResourceFact key={label} label={label} value={value} />
|
|
2515
|
+
))}
|
|
2516
|
+
</dl>
|
|
2517
|
+
{accessCheck.reason ? (
|
|
2518
|
+
<p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
2519
|
+
{accessCheck.reason}
|
|
2520
|
+
</p>
|
|
2521
|
+
) : null}
|
|
2522
|
+
{accessCheck.evaluationError ? (
|
|
2523
|
+
<p className="text-xs leading-relaxed text-semantic-error">
|
|
2524
|
+
{accessCheck.evaluationError}
|
|
2525
|
+
</p>
|
|
2526
|
+
) : null}
|
|
2527
|
+
</div>
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
const bindings = data.bindings ?? [];
|
|
2531
|
+
const usedByPods = data.usedByPods ?? [];
|
|
2532
|
+
return (
|
|
2533
|
+
<div className="space-y-2.5">
|
|
2534
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
2535
|
+
{subjectBadges}
|
|
2536
|
+
<Badge tone="note" size="sm">
|
|
2537
|
+
{data.flatRulesCount ?? 0}
|
|
2538
|
+
{data.truncated ? "+" : ""} effective rules
|
|
2539
|
+
</Badge>
|
|
2540
|
+
{data.truncated ? (
|
|
2541
|
+
<Badge severity="warning" size="sm">
|
|
2542
|
+
rule list truncated
|
|
2543
|
+
</Badge>
|
|
2544
|
+
) : null}
|
|
2545
|
+
</div>
|
|
2546
|
+
{bindings.length > 0 ? (
|
|
2547
|
+
<div className="max-h-52 overflow-y-auto rounded-md border border-theme-border">
|
|
2548
|
+
{bindings.map((binding, index) => (
|
|
2549
|
+
<div
|
|
2550
|
+
key={`${binding.bindingKind}-${binding.bindingNamespace ?? ""}-${binding.bindingName}`}
|
|
2551
|
+
className={clsx(
|
|
2552
|
+
"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 text-xs",
|
|
2553
|
+
index > 0 && "border-t border-theme-border/60",
|
|
2554
|
+
)}
|
|
2555
|
+
>
|
|
2556
|
+
<Badge tone="structural" size="sm">
|
|
2557
|
+
{binding.bindingKind}
|
|
2558
|
+
</Badge>
|
|
2559
|
+
<span className="min-w-0 truncate font-mono text-theme-text-secondary">
|
|
2560
|
+
{binding.bindingNamespace ? `${binding.bindingNamespace}/` : ""}
|
|
2561
|
+
{binding.bindingName}
|
|
2562
|
+
</span>
|
|
2563
|
+
<span className="text-theme-text-tertiary">→</span>
|
|
2564
|
+
<Badge tone="structural" size="sm">
|
|
2565
|
+
{binding.roleKind}
|
|
2566
|
+
</Badge>
|
|
2567
|
+
<span className="min-w-0 truncate font-mono text-theme-text-primary">
|
|
2568
|
+
{binding.roleNamespace ? `${binding.roleNamespace}/` : ""}
|
|
2569
|
+
{binding.roleName}
|
|
2570
|
+
</span>
|
|
2571
|
+
<span className="ml-auto shrink-0 font-mono text-theme-text-tertiary">
|
|
2572
|
+
{binding.rulesCount} rule{binding.rulesCount === 1 ? "" : "s"}
|
|
2573
|
+
</span>
|
|
2574
|
+
{binding.inheritedFromGroup ? (
|
|
2575
|
+
<Badge tone="note" size="sm">
|
|
2576
|
+
via {binding.inheritedFromGroup}
|
|
2577
|
+
</Badge>
|
|
2578
|
+
) : null}
|
|
2579
|
+
</div>
|
|
2580
|
+
))}
|
|
2581
|
+
</div>
|
|
2582
|
+
) : (
|
|
2583
|
+
<p className="text-xs italic text-theme-text-tertiary">
|
|
2584
|
+
No RoleBinding or ClusterRoleBinding grants this subject anything.
|
|
2585
|
+
</p>
|
|
2586
|
+
)}
|
|
2587
|
+
{usedByPods.length > 0 ? (
|
|
2588
|
+
<p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
2589
|
+
<span className="text-theme-text-tertiary">Used by pods: </span>
|
|
2590
|
+
<span className="font-mono">{usedByPods.join(", ")}</span>
|
|
2591
|
+
{data.podsTotal && data.podsTotal > usedByPods.length
|
|
2592
|
+
? ` and ${data.podsTotal - usedByPods.length} more`
|
|
2593
|
+
: ""}
|
|
2594
|
+
</p>
|
|
2595
|
+
) : null}
|
|
2596
|
+
</div>
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2006
2600
|
function RevisionHistory({
|
|
2007
2601
|
observations,
|
|
2008
2602
|
citedOrder,
|
|
@@ -2111,6 +2705,12 @@ function EvidenceCaveat({ data }: { data: InvestigationEvidenceData }) {
|
|
|
2111
2705
|
} else if (data.type === "relationships" || data.type === "topology") {
|
|
2112
2706
|
text =
|
|
2113
2707
|
"This shows direct relationships Radar found, not an inferred blast radius.";
|
|
2708
|
+
} else if (data.type === "alerts") {
|
|
2709
|
+
text =
|
|
2710
|
+
"Instances are matched to this investigation by their Prometheus labels; a firing rule alone does not establish the cause.";
|
|
2711
|
+
} else if (data.type === "permissions") {
|
|
2712
|
+
text =
|
|
2713
|
+
"This is what RBAC grants the subject, not what the workload has exercised.";
|
|
2114
2714
|
}
|
|
2115
2715
|
if (!text) return null;
|
|
2116
2716
|
return (
|
|
@@ -2210,6 +2810,12 @@ function evidenceIcon(type: InvestigationEvidenceData["type"]) {
|
|
|
2210
2810
|
return ListTree;
|
|
2211
2811
|
case "receipt":
|
|
2212
2812
|
return CheckCircle2;
|
|
2813
|
+
case "alerts":
|
|
2814
|
+
return BellRing;
|
|
2815
|
+
case "helm":
|
|
2816
|
+
return Package;
|
|
2817
|
+
case "permissions":
|
|
2818
|
+
return KeyRound;
|
|
2213
2819
|
}
|
|
2214
2820
|
}
|
|
2215
2821
|
|