Skip to content

Commit c1bb51c

Browse files
fix(ui): collection of UI bug fixes and improvements (#9346)
1 parent a4e12a9 commit c1bb51c

File tree

25 files changed

+961
-626
lines changed

25 files changed

+961
-626
lines changed

ui/actions/overview/attack-surface.adapter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface AttackSurfaceItem {
1818
label: string;
1919
failedFindings: number;
2020
totalFindings: number;
21+
checkIds: string[];
2122
}
2223

2324
const ATTACK_SURFACE_LABELS: Record<AttackSurfaceId, string> = {
@@ -41,6 +42,7 @@ function mapAttackSurfaceItem(item: AttackSurfaceOverview): AttackSurfaceItem {
4142
label: ATTACK_SURFACE_LABELS[id] || item.id,
4243
failedFindings: item.attributes.failed_findings,
4344
totalFindings: item.attributes.total_findings,
45+
checkIds: item.attributes.check_ids ?? [],
4446
};
4547
}
4648

Lines changed: 92 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { getProviderDisplayName } from "@/types/providers";
22

3-
import {
4-
FindingsSeverityOverviewResponse,
5-
ProviderOverview,
6-
ProvidersOverviewResponse,
7-
} from "./types";
8-
93
export interface SankeyNode {
104
name: string;
115
}
@@ -27,44 +21,16 @@ export interface SankeyData {
2721
zeroDataProviders: ZeroDataProvider[];
2822
}
2923

30-
export interface SankeyFilters {
31-
providerTypes?: string[];
32-
/** All selected provider types - used to show missing providers in legend */
33-
allSelectedProviderTypes?: string[];
24+
export interface SeverityData {
25+
critical: number;
26+
high: number;
27+
medium: number;
28+
low: number;
29+
informational: number;
3430
}
3531

36-
interface AggregatedProvider {
37-
id: string;
38-
displayName: string;
39-
pass: number;
40-
fail: number;
41-
}
42-
43-
// API can return multiple entries for the same provider type, so we sum their findings
44-
function aggregateProvidersByType(
45-
providers: ProviderOverview[],
46-
): AggregatedProvider[] {
47-
const aggregated = new Map<string, AggregatedProvider>();
48-
49-
for (const provider of providers) {
50-
const { id, attributes } = provider;
51-
52-
const existing = aggregated.get(id);
53-
54-
if (existing) {
55-
existing.pass += attributes.findings.pass;
56-
existing.fail += attributes.findings.fail;
57-
} else {
58-
aggregated.set(id, {
59-
id,
60-
displayName: getProviderDisplayName(id),
61-
pass: attributes.findings.pass,
62-
fail: attributes.findings.fail,
63-
});
64-
}
65-
}
66-
67-
return Array.from(aggregated.values());
32+
export interface SeverityByProviderType {
33+
[providerType: string]: SeverityData;
6834
}
6935

7036
const SEVERITY_ORDER = [
@@ -75,142 +41,122 @@ const SEVERITY_ORDER = [
7541
"Informational",
7642
] as const;
7743

44+
const SEVERITY_KEYS: (keyof SeverityData)[] = [
45+
"critical",
46+
"high",
47+
"medium",
48+
"low",
49+
"informational",
50+
];
51+
7852
/**
79-
* Adapts providers overview and findings severity API responses to Sankey chart format.
80-
* Severity distribution is calculated proportionally based on each provider's fail count.
53+
* Adapts severity by provider type data to Sankey chart format.
8154
*
82-
* @param providersResponse - The providers overview API response
83-
* @param severityResponse - The findings severity API response
84-
* @param filters - Optional filters to restrict which providers are shown.
85-
* When filters are set, only selected providers are shown.
86-
* When no filters, all providers are shown.
55+
* @param severityByProviderType - Severity breakdown per provider type from the API
56+
* @param selectedProviderTypes - Provider types that were selected but may have no data
8757
*/
88-
export function adaptProvidersOverviewToSankey(
89-
providersResponse: ProvidersOverviewResponse | undefined,
90-
severityResponse?: FindingsSeverityOverviewResponse | undefined,
91-
filters?: SankeyFilters,
58+
export function adaptToSankeyData(
59+
severityByProviderType: SeverityByProviderType,
60+
selectedProviderTypes?: string[],
9261
): SankeyData {
93-
if (!providersResponse?.data || providersResponse.data.length === 0) {
94-
return { nodes: [], links: [], zeroDataProviders: [] };
95-
}
96-
97-
const aggregatedProviders = aggregateProvidersByType(providersResponse.data);
98-
99-
// Filter providers based on selection:
100-
// - If providerTypes filter is set: show only those provider types
101-
// - Otherwise: show all providers from the API response
102-
const hasProviderTypeFilter =
103-
filters?.providerTypes && filters.providerTypes.length > 0;
104-
105-
let providersToShow: AggregatedProvider[];
106-
if (hasProviderTypeFilter) {
107-
// Show only selected provider types
108-
providersToShow = aggregatedProviders.filter((p) =>
109-
filters.providerTypes!.includes(p.id.toLowerCase()),
110-
);
111-
} else {
112-
// No provider type filter - show all providers from the API response
113-
// Providers with no findings (pass=0, fail=0) will appear in the legend
114-
providersToShow = aggregatedProviders;
62+
if (Object.keys(severityByProviderType).length === 0) {
63+
// No data - check if there are selected providers to show as zero-data
64+
const zeroDataProviders: ZeroDataProvider[] = (
65+
selectedProviderTypes || []
66+
).map((type) => ({
67+
id: type.toLowerCase(),
68+
displayName: getProviderDisplayName(type),
69+
}));
70+
return { nodes: [], links: [], zeroDataProviders };
11571
}
11672

117-
if (providersToShow.length === 0) {
118-
return { nodes: [], links: [], zeroDataProviders: [] };
73+
// Calculate total fails per provider to identify which have data
74+
const providersWithData: {
75+
id: string;
76+
displayName: string;
77+
totalFail: number;
78+
}[] = [];
79+
const providersWithoutData: ZeroDataProvider[] = [];
80+
81+
for (const [providerType, severity] of Object.entries(
82+
severityByProviderType,
83+
)) {
84+
const totalFail =
85+
severity.critical +
86+
severity.high +
87+
severity.medium +
88+
severity.low +
89+
severity.informational;
90+
91+
const normalizedType = providerType.toLowerCase();
92+
93+
if (totalFail > 0) {
94+
providersWithData.push({
95+
id: normalizedType,
96+
displayName: getProviderDisplayName(normalizedType),
97+
totalFail,
98+
});
99+
} else {
100+
providersWithoutData.push({
101+
id: normalizedType,
102+
displayName: getProviderDisplayName(normalizedType),
103+
});
104+
}
119105
}
120106

121-
// Separate providers with and without failures
122-
const providersWithFailures = providersToShow.filter((p) => p.fail > 0);
123-
const providersWithoutFailures = providersToShow.filter((p) => p.fail === 0);
124-
125-
// Zero-data providers to show as legends below the chart
126-
const zeroDataProviders: ZeroDataProvider[] = providersWithoutFailures.map(
127-
(p) => ({
128-
id: p.id,
129-
displayName: p.displayName,
130-
}),
131-
);
132-
133-
// Add selected provider types that are completely missing from API response
134-
// (these are providers with zero findings - not even in the response)
135-
if (
136-
filters?.allSelectedProviderTypes &&
137-
filters.allSelectedProviderTypes.length > 0
138-
) {
107+
// Add selected provider types that are not in the response at all
108+
if (selectedProviderTypes && selectedProviderTypes.length > 0) {
139109
const existingProviderIds = new Set(
140-
aggregatedProviders.map((p) => p.id.toLowerCase()),
110+
Object.keys(severityByProviderType).map((t) => t.toLowerCase()),
141111
);
142112

143-
for (const selectedType of filters.allSelectedProviderTypes) {
113+
for (const selectedType of selectedProviderTypes) {
144114
const normalizedType = selectedType.toLowerCase();
145115
if (!existingProviderIds.has(normalizedType)) {
146-
// This provider type was selected but has no data at all
147-
zeroDataProviders.push({
116+
providersWithoutData.push({
148117
id: normalizedType,
149118
displayName: getProviderDisplayName(normalizedType),
150119
});
151120
}
152121
}
153122
}
154123

155-
// If no providers have failures, return empty chart with legends
156-
if (providersWithFailures.length === 0) {
157-
return { nodes: [], links: [], zeroDataProviders };
124+
// If no providers have failures, return empty chart with zero-data legends
125+
if (providersWithData.length === 0) {
126+
return { nodes: [], links: [], zeroDataProviders: providersWithoutData };
158127
}
159128

160-
// Only include providers WITH failures in the chart
161-
const providerNodes: SankeyNode[] = providersWithFailures.map((p) => ({
129+
// Build nodes: providers first, then severities
130+
const providerNodes: SankeyNode[] = providersWithData.map((p) => ({
162131
name: p.displayName,
163132
}));
164133
const severityNodes: SankeyNode[] = SEVERITY_ORDER.map((severity) => ({
165134
name: severity,
166135
}));
167136
const nodes = [...providerNodes, ...severityNodes];
137+
138+
// Build links
168139
const severityStartIndex = providerNodes.length;
169140
const links: SankeyLink[] = [];
170141

171-
if (severityResponse?.data?.attributes) {
172-
const { critical, high, medium, low, informational } =
173-
severityResponse.data.attributes;
174-
175-
const severityValues = [critical, high, medium, low, informational];
176-
const totalSeverity = severityValues.reduce((sum, v) => sum + v, 0);
177-
178-
if (totalSeverity > 0) {
179-
const totalFails = providersWithFailures.reduce(
180-
(sum, p) => sum + p.fail,
181-
0,
182-
);
183-
184-
providersWithFailures.forEach((provider, sourceIndex) => {
185-
const providerRatio = provider.fail / totalFails;
186-
187-
severityValues.forEach((severityValue, severityIndex) => {
188-
const value = Math.round(severityValue * providerRatio);
189-
190-
if (value > 0) {
191-
links.push({
192-
source: sourceIndex,
193-
target: severityStartIndex + severityIndex,
194-
value,
195-
});
196-
}
197-
});
142+
providersWithData.forEach((provider, sourceIndex) => {
143+
const severity =
144+
severityByProviderType[provider.id] ||
145+
severityByProviderType[provider.id.toUpperCase()];
146+
147+
if (severity) {
148+
SEVERITY_KEYS.forEach((key, severityIndex) => {
149+
const value = severity[key];
150+
if (value > 0) {
151+
links.push({
152+
source: sourceIndex,
153+
target: severityStartIndex + severityIndex,
154+
value,
155+
});
156+
}
198157
});
199158
}
200-
} else {
201-
// Fallback when no severity data available
202-
const failNode: SankeyNode = { name: "Fail" };
203-
nodes.push(failNode);
204-
const failIndex = nodes.length - 1;
205-
206-
providersWithFailures.forEach((provider, sourceIndex) => {
207-
links.push({
208-
source: sourceIndex,
209-
target: failIndex,
210-
value: provider.fail,
211-
});
212-
});
213-
}
159+
});
214160

215-
return { nodes, links, zeroDataProviders };
161+
return { nodes, links, zeroDataProviders: providersWithoutData };
216162
}

0 commit comments

Comments
 (0)