Skip to content

Commit 26b4900

Browse files
committed
EDM-3715: Vulnerability management
1 parent f3e12c9 commit 26b4900

85 files changed

Lines changed: 3138 additions & 351 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/ocp-plugin/console-extensions.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@
105105
"component": { "$codeRef": "CatalogEditFleetWizard" }
106106
}
107107
},
108+
{
109+
"type": "console.page/route",
110+
"properties": {
111+
"exact": true,
112+
"path": ["/edge/security-overview"],
113+
"component": { "$codeRef": "SecurityOverviewPage" }
114+
}
115+
},
108116
{
109117
"type": "console.page/route",
110118
"properties": {

apps/ocp-plugin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"EnrollmentRequestDetailsPage": "./src/components/EnrollmentRequests/EnrollmentRequestDetailsPage.tsx",
3030
"appContext": "./src/components/AppContext/AppContext.tsx",
3131
"OverviewTab": "./src/components/OverviewTab/OverviewTab.tsx",
32+
"SecurityOverviewPage": "./src/components/SecurityOverview/SecurityOverviewPage.tsx",
3233
"CatalogPage": "./src/components/Catalog/CatalogPage.tsx",
3334
"AddCatalogItemWizard": "./src/components/Catalog/AddCatalogItemWizard.tsx",
3435
"CatalogInstallWizard": "./src/components/Catalog/CatalogInstallWizard.tsx",

apps/ocp-plugin/src/components/AppContext/AppContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const appRoutes = {
7171
[ROUTE.CATALOG_INSTALL]: '/edge/catalog/install',
7272
[ROUTE.CATALOG_FLEET_EDIT]: '/edge/fleets/catalog',
7373
[ROUTE.CATALOG_DEVICE_EDIT]: '/edge/devices/catalog',
74+
[ROUTE.SECURITY_OVERVIEW]: '/edge/security-overview',
7475
};
7576

7677
export const useValuesAppContext = (): AppContextProps => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as React from 'react';
2+
import SecurityOverviewPage from '@flightctl/ui-components/src/components/SecurityOverview/SecurityOverviewPage';
3+
import WithPageLayout from '../common/WithPageLayout';
4+
5+
const OcpSecurityOverviewPage = () => {
6+
return (
7+
<WithPageLayout>
8+
<SecurityOverviewPage />
9+
</WithPageLayout>
10+
);
11+
};
12+
13+
export default OcpSecurityOverviewPage;

apps/ocp-plugin/src/utils/apiCalls.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ declare global {
1616
}
1717
}
1818

19-
type Api = 'flightctl' | 'imagebuilder' | 'alerts' | 'catalog';
19+
type Api = 'flightctl' | 'imagebuilder' | 'alerts' | 'catalog' | 'vulnerability';
2020

2121
const addRequiredHeaders = (options: RequestInit, api?: Api): RequestInit => {
2222
const token = getCSRFToken();
@@ -49,6 +49,7 @@ export const apiProxy = `${uiProxy}/api`;
4949
const alertsAPI = `${apiProxy}/alerts`;
5050
const imageBuilderPathRegex = /^image(builds|exports)/;
5151
const catalogPathRegex = /^(catalogs|catalogitems)/;
52+
const vulnerabilityPathRegex = /^vulnerabilities/;
5253

5354
export const wsEndpoint = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${apiServer}`;
5455

@@ -65,13 +66,14 @@ const getFullApiUrl = (path: string): { api: Api; url: string } => {
6566
if (imageBuilderPathRegex.test(path)) {
6667
return { api: 'imagebuilder', url: `${apiProxy}/imagebuilder/api/v1/${path}` };
6768
}
69+
70+
let apiName: Api = 'flightctl';
6871
if (catalogPathRegex.test(path)) {
69-
return {
70-
api: 'catalog',
71-
url: `${apiProxy}/flightctl/api/v1/${path}`,
72-
};
72+
apiName = 'catalog';
73+
} else if (vulnerabilityPathRegex.test(path)) {
74+
apiName = 'vulnerability';
7375
}
74-
return { api: 'flightctl', url: `${apiProxy}/flightctl/api/v1/${path}` };
76+
return { api: apiName, url: `${apiProxy}/flightctl/api/v1/${path}` };
7577
};
7678

7779
const handleAlertsJSONResponse = async <R>(response: Response): Promise<R> => {
@@ -94,7 +96,28 @@ const handleAlertsJSONResponse = async <R>(response: Response): Promise<R> => {
9496
throw new Error(await getErrorMsgFromAlertsApiResponse(response));
9597
};
9698

97-
export const handleApiJSONResponse = async <R>(response: Response): Promise<R> => {
99+
const handleVulnerabilityJSONResponse = async <R>(response: Response): Promise<R> => {
100+
if (response.ok) {
101+
const data = (await response.json()) as R;
102+
return data;
103+
}
104+
105+
if (response.status === 404) {
106+
throw new Error(`Error ${response.status}: ${response.statusText}`);
107+
}
108+
109+
// API returns 501 for disabled vulnerabilities API.
110+
if (response.status === 501) {
111+
throw new Error(`${response.status}`);
112+
}
113+
114+
throw new Error(await getErrorMsgFromApiResponse(response));
115+
};
116+
117+
export const handleApiJSONResponse = async <R>(api: Api, response: Response): Promise<R> => {
118+
if (api === 'vulnerability') {
119+
return handleVulnerabilityJSONResponse(response);
120+
}
98121
if (response.ok) {
99122
const data = (await response.json()) as R;
100123
return data;
@@ -125,7 +148,7 @@ const putOrPostData = async <TRequest, TResponse = TRequest>(
125148
const options = addRequiredHeaders(baseOptions, api);
126149
try {
127150
const response = await fetch(url, options);
128-
return handleApiJSONResponse(response);
151+
return handleApiJSONResponse(api, response);
129152
} catch (error) {
130153
console.error(`Error making ${method} request for ${kind}:`, error);
131154
throw error;
@@ -148,7 +171,7 @@ export const deleteData = async <R>(kind: string, abortSignal?: AbortSignal): Pr
148171
const options = addRequiredHeaders(baseOptions, api);
149172
try {
150173
const response = await fetch(url, options);
151-
return handleApiJSONResponse(response);
174+
return handleApiJSONResponse(api, response);
152175
} catch (error) {
153176
console.error('Error making DELETE request:', error);
154177
throw error;
@@ -169,7 +192,7 @@ export const patchData = async <R>(kind: string, data: PatchRequest, abortSignal
169192
const options = addRequiredHeaders(baseOptions, api);
170193
try {
171194
const response = await fetch(url, options);
172-
return handleApiJSONResponse(response);
195+
return handleApiJSONResponse(api, response);
173196
} catch (error) {
174197
console.error('Error making PATCH request:', error);
175198
throw error;
@@ -190,7 +213,7 @@ export const fetchData = async <R>(path: string, abortSignal?: AbortSignal): Pro
190213
if (api === 'alerts') {
191214
return handleAlertsJSONResponse(response);
192215
}
193-
return handleApiJSONResponse(response);
216+
return handleApiJSONResponse(api, response);
194217
} catch (error) {
195218
console.error('Error making GET request:', error);
196219
throw error;

apps/standalone/src/app/routes.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ const FleetDetails = React.lazy(
6161
);
6262

6363
const OverviewPage = React.lazy(() => import('@flightctl/ui-components/src/components/OverviewPage/OverviewPage'));
64+
const SecurityOverviewPage = React.lazy(
65+
() => import('@flightctl/ui-components/src/components/SecurityOverview/SecurityOverviewPage'),
66+
);
6467
const PendingEnrollmentRequestsBadge = React.lazy(
6568
() => import('@flightctl/ui-components/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge'),
6669
);
@@ -172,6 +175,15 @@ const getAppRoutes = (t: TFunction): ExtendedRouteObject[] => [
172175
</TitledRoute>
173176
),
174177
},
178+
{
179+
path: '/security-overview',
180+
title: t('Security overview'),
181+
element: (
182+
<TitledRoute title={t('Security overview')}>
183+
<SecurityOverviewPage />
184+
</TitledRoute>
185+
),
186+
},
175187
{
176188
// Route is only exposed for the standalone app
177189
path: '/command-line-tools',

apps/standalone/src/app/utils/apiCalls.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const apiProxy = `${uiProxy}/api`;
1717

1818
const imageBuilderPathRegex = /^image(builds|exports)/;
1919
const catalogPathRegex = /^(catalogs|catalogitems)/;
20+
const vulnerabilityPathRegex = /^vulnerabilities/;
2021

2122
export const wsEndpoint = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${apiServer}`;
2223

@@ -45,20 +46,23 @@ export const fetchUiProxy = async (endpoint: string, requestInit: RequestInit):
4546
return await fetch(`${apiProxy}/${endpoint}`, options);
4647
};
4748

48-
const getFullApiUrl = (path: string): { api: 'flightctl' | 'imagebuilder' | 'alerts' | 'catalog'; url: string } => {
49+
type Api = 'flightctl' | 'imagebuilder' | 'alerts' | 'catalog' | 'vulnerability';
50+
51+
const getFullApiUrl = (path: string): { api: Api; url: string } => {
4952
if (path.startsWith('alerts')) {
5053
return { api: 'alerts', url: `${apiProxy}/alerts/api/v2/${path}` };
5154
}
5255
if (imageBuilderPathRegex.test(path)) {
5356
return { api: 'imagebuilder', url: `${apiProxy}/imagebuilder/api/v1/${path}` };
5457
}
58+
59+
let apiName: Api = 'flightctl';
5560
if (catalogPathRegex.test(path)) {
56-
return {
57-
api: 'catalog',
58-
url: `${apiProxy}/flightctl/api/v1/${path}`,
59-
};
61+
apiName = 'catalog';
62+
} else if (vulnerabilityPathRegex.test(path)) {
63+
apiName = 'vulnerability';
6064
}
61-
return { api: 'flightctl', url: `${apiProxy}/flightctl/api/v1/${path}` };
65+
return { api: apiName, url: `${apiProxy}/flightctl/api/v1/${path}` };
6266
};
6367

6468
export const logout = async () => {
@@ -78,7 +82,10 @@ export const redirectToLogin = () => {
7882
window.location.href = '/login';
7983
};
8084

81-
const handleApiJSONResponse = async <R>(response: Response): Promise<R> => {
85+
const handleApiJSONResponse = async <R>(api: Api, response: Response): Promise<R> => {
86+
if (api === 'vulnerability') {
87+
return handleVulnerabilityJSONResponse(response);
88+
}
8289
if (response.ok) {
8390
const data = (await response.json()) as R;
8491
return data;
@@ -116,6 +123,24 @@ const handleAlertsJSONResponse = async <R>(response: Response): Promise<R> => {
116123
throw new Error(await getErrorMsgFromAlertsApiResponse(response));
117124
};
118125

126+
const handleVulnerabilityJSONResponse = async <R>(response: Response): Promise<R> => {
127+
if (response.ok) {
128+
const data = (await response.json()) as R;
129+
return data;
130+
}
131+
132+
if (response.status === 404) {
133+
throw new Error(`Error ${response.status}: ${response.statusText}`);
134+
}
135+
136+
// API returns 501 for disabled vulnerabilities API.
137+
if (response.status === 501) {
138+
throw new Error(`${response.status}`);
139+
}
140+
141+
throw new Error(await getErrorMsgFromApiResponse(response));
142+
};
143+
119144
const fetchWithRetry = async <R>(path: string, init?: RequestInit): Promise<R> => {
120145
const { api, url } = getFullApiUrl(path);
121146

@@ -136,7 +161,7 @@ const fetchWithRetry = async <R>(path: string, init?: RequestInit): Promise<R> =
136161
if (api === 'alerts') {
137162
return handleAlertsJSONResponse(response);
138163
}
139-
return handleApiJSONResponse(response);
164+
return handleApiJSONResponse(api, response);
140165
};
141166

142167
const putOrPostData = async <TRequest, TResponse = TRequest>(

eslint.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ module.exports = defineConfig([
101101
importNames: ['WizardFooterWrapper', 'WizardFooter'],
102102
message: 'Use FlightCtlWizardFooter wrapper',
103103
},
104+
{
105+
name: '@patternfly/react-core',
106+
importNames: ['Drawer', 'DrawerPanelContent'],
107+
message: 'Use FlightCtlPageDrawer wrapper',
108+
},
104109
{
105110
name: 'react-i18next',
106111
importNames: ['useTranslation'],

libs/i18n/locales/en/translation.json

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"404 Page Not Found": "404 Page Not Found",
2626
"Error page - details should be displayed here": "Error page - details should be displayed here",
2727
"Overview": "Overview",
28+
"Security overview": "Security overview",
2829
"Command line tools": "Command line tools",
2930
"Enrollment Request Details": "Enrollment Request Details",
3031
"Enrollment Request": "Enrollment Request",
@@ -289,7 +290,6 @@
289290
"You do not have permission to deploy": "You do not have permission to deploy",
290291
"A channel must be selected": "A channel must be selected",
291292
"A version must be selected": "A version must be selected",
292-
"Resize panel": "Resize panel",
293293
"Restore": "Restore",
294294
"This catalog item is managed by a resource sync and cannot be directly restored. Either remove this catalog's definition from the resource sync configuration, or delete the resource sync first.": "This catalog item is managed by a resource sync and cannot be directly restored. Either remove this catalog's definition from the resource sync configuration, or delete the resource sync first.",
295295
"Deprecate": "Deprecate",
@@ -520,6 +520,7 @@
520520
"Add label": "Add label",
521521
"Unexpected error occurred": "Unexpected error occurred",
522522
"Please reload the page and try again.": "Please reload the page and try again.",
523+
"Resize panel": "Resize panel",
523524
"Next": "Next",
524525
"Back": "Back",
525526
"Show less": "Show less",
@@ -632,13 +633,12 @@
632633
"You can add devices and label them to match fleets, or you can <2>start with a fleet</2> and add devices into it.": "You can add devices and label them to match fleets, or you can <2>start with a fleet</2> and add devices into it.",
633634
"You can add devices and label them to match fleets": "You can add devices and label them to match fleets",
634635
"No decommissioning or decommissioned devices here!": "No decommissioning or decommissioned devices here!",
635-
"Name / Alias": "Name / Alias",
636636
"Clear all filters": "Clear all filters",
637637
"Searching...": "Searching...",
638638
"No results": "No results",
639639
"Fleet and label filter toggle": "Fleet and label filter toggle",
640640
"Clear filter text": "Clear filter text",
641-
"Name and alias": "Name and alias",
641+
"Enter a valid CVE ID in the form CVE-YYYY-sequence, with sequence containing at least 4 digits (for example, CVE-2024-12345).": "Enter a valid CVE ID in the form CVE-YYYY-sequence, with sequence containing at least 4 digits (for example, CVE-2024-12345).",
642642
"Labels and fleets": "Labels and fleets",
643643
"Filter by labels and fleets": "Filter by labels and fleets",
644644
"Decommission devices": "Decommission devices",
@@ -1205,6 +1205,7 @@
12051205
"Failed": "Failed",
12061206
"Canceling": "Canceling",
12071207
"Canceled": "Canceled",
1208+
"Scanning for vulnerabilities": "Scanning for vulnerabilities",
12081209
"Converting": "Converting",
12091210
"Image built successfully": "Image built successfully",
12101211
"Export images": "Export images",
@@ -1388,6 +1389,8 @@
13881389
"This area displays current notifications about your monitored devices and fleets.": "This area displays current notifications about your monitored devices and fleets.",
13891390
"Alerts will appear here if an issue is detected.": "Alerts will appear here if an issue is detected.",
13901391
"View devices": "View devices",
1392+
"Security risks across your devices. Resolve critical vulnerabilities immediately to prevent migration failure and protect your infrastructure.": "Security risks across your devices. Resolve critical vulnerabilities immediately to prevent migration failure and protect your infrastructure.",
1393+
"View all CVEs": "View all CVEs",
13911394
"{{count}} Devices_one": "{{count}} Device",
13921395
"{{count}} Devices_other": "{{count}} Devices",
13931396
"Review pending devices_one": "Review pending device",
@@ -1500,6 +1503,46 @@
15001503
"Resource sync {{rsId}} could not be found": "Resource sync {{rsId}} could not be found",
15011504
"Resource sync {{rsId}}": "Resource sync {{rsId}}",
15021505
"Could not find the details for the resource sync <1>{rsId}</1>": "Could not find the details for the resource sync <1>{rsId}</1>",
1506+
"Vulnerability reporting is not enabled in this environment.": "Vulnerability reporting is not enabled in this environment.",
1507+
"Total active vulnerabilities.": "Total active vulnerabilities.",
1508+
"CVEs affecting images deployed across your managed fleet and devices.": "CVEs affecting images deployed across your managed fleet and devices.",
1509+
"Vulnerability counts by severity": "Vulnerability counts by severity",
1510+
"No CVEs detected": "No CVEs detected",
1511+
"All managed devices have been scanned. No CVEs were found affecting images currently deployed across your fleets and devices.": "All managed devices have been scanned. No CVEs were found affecting images currently deployed across your fleets and devices.",
1512+
"No vulnerability data to display.": "No vulnerability data to display.",
1513+
"There are currently no deployed devices. Scan results will be available once devices have been added.": "There are currently no deployed devices. Scan results will be available once devices have been added.",
1514+
"Severity": "Severity",
1515+
"Affected devices": "Affected devices",
1516+
"Affected images": "Affected images",
1517+
"Published": "Published",
1518+
"Filter by severity": "Filter by severity",
1519+
"Find by name": "Find by name",
1520+
"Vulnerabilities table": "Vulnerabilities table",
1521+
"Show more": "Show more",
1522+
"CVE record - {{ cveId }}": "CVE record - {{ cveId }}",
1523+
"{{ advisoryId }} - Red Hat Security Advisory": "{{ advisoryId }} - Red Hat Security Advisory",
1524+
"Published: {{ date }}": "Published: {{ date }}",
1525+
"Scanner name": "Scanner name",
1526+
"<0>{deviceCount}</0> devices in this fleet are running images affected by this vulnerability. Update or replace the affected images to remediate._one": "<0>{deviceCount}</0> device in this fleet is running images affected by this vulnerability. Update or replace the affected images to remediate.",
1527+
"<0>{deviceCount}</0> devices in this fleet are running images affected by this vulnerability. Update or replace the affected images to remediate._other": "<0>{deviceCount}</0> devices in this fleet are running images affected by this vulnerability. Update or replace the affected images to remediate.",
1528+
"<0>{deviceCount}</0> devices in <2>1</2> fleet are running images affected by this vulnerability. Update or replace the affected images to remediate._one": "<0>{deviceCount}</0> device in <2>1</2> fleet is running images affected by this vulnerability. Update or replace the affected images to remediate.",
1529+
"<0>{deviceCount}</0> devices in <2>1</2> fleet are running images affected by this vulnerability. Update or replace the affected images to remediate._other": "<0>{deviceCount}</0> devices in <2>1</2> fleet are running images affected by this vulnerability. Update or replace the affected images to remediate.",
1530+
"<0>{deviceCount}</0> devices across <2>{fleetCount}</2> fleets are running images affected by this vulnerability. Update or replace the affected images to remediate._one": "<0>{deviceCount}</0> device is running an image affected by this vulnerability. Update or replace the affected image to remediate.",
1531+
"<0>{deviceCount}</0> devices across <2>{fleetCount}</2> fleets are running images affected by this vulnerability. Update or replace the affected images to remediate._other": "<0>{deviceCount}</0> devices across <2>{fleetCount}</2> fleets are running images affected by this vulnerability. Update or replace the affected images to remediate.",
1532+
"This device is running an image affected by this vulnerability. Update or replace the affected image to remediate.": "This device is running an image affected by this vulnerability. Update or replace the affected image to remediate.",
1533+
"Vulnerability impact table": "Vulnerability impact table",
1534+
"Total affected fleets": "Total affected fleets",
1535+
"Total {{ count }} fleets_one": "Total {{ count }} fleet",
1536+
"Total {{ count }} fleets_other": "Total {{ count }} fleets",
1537+
"Total affected devices": "Total affected devices",
1538+
"Total {{ count }} devices_one": "Total {{ count }} device",
1539+
"Total {{ count }} devices_other": "Total {{ count }} devices",
1540+
"Total affected images": "Total affected images",
1541+
"Total {{ count }} images_one": "Total {{ count }} image",
1542+
"Total {{ count }} images_other": "Total {{ count }} images",
1543+
"Unable to load vulnerability impact data": "Unable to load vulnerability impact data",
1544+
"Impact data could not be loaded. Try again later.": "Impact data could not be loaded. Try again later.",
1545+
"Impact summary": "Impact summary",
15031546
"CPU": "CPU",
15041547
"Memory": "Memory",
15051548
"Disk": "Disk",
@@ -1586,6 +1629,8 @@
15861629
"Online": "Online",
15871630
"Pending sync": "Pending sync",
15881631
"Suspended": "Suspended",
1632+
"Name and alias": "Name and alias",
1633+
"CVE ID": "CVE ID",
15891634
"Decommissioned": "Decommissioned",
15901635
"Decommissioning": "Decommissioning",
15911636
"Enrolled": "Enrolled",
@@ -1628,5 +1673,10 @@
16281673
"Reloading": "Reloading",
16291674
"Refreshing": "Refreshing",
16301675
"Maintenance": "Maintenance",
1676+
"Undefined": "Undefined",
1677+
"Critical": "Critical",
1678+
"Important": "Important",
1679+
"Moderate": "Moderate",
1680+
"Low": "Low",
16311681
"{{ count }} devices matching the labels were selected._zero": "There are no devices matching these labels."
16321682
}

0 commit comments

Comments
 (0)