From 5d7db5bc1886aaafa67a835dd471fc94143fadd9 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Wed, 30 Sep 2026 13:05:54 +0200 Subject: [PATCH 1/2] chore(deps): pin undici to ^7.29.1 Fixes GHSA-rfgv-xxqx-mfg5 and GHSA-w293-vg96-wgc3 pulled in transitively via @module-federation/dts-plugin, which fail the high-severity audit gate. --- package.json | 3 ++- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8f9a868..8d52227 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "@types/react-dom": "^18.0.0", "@testing-library/react": "^16.0.0", "adm-zip": "^0.6.1", - "js-cookie": "^3.0.6" + "js-cookie": "^3.0.6", + "undici": "^7.29.1" }, "dependencies": { "@backstage/core-components": "^0.18.14", diff --git a/yarn.lock b/yarn.lock index 860c066..0879d2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20921,10 +20921,10 @@ __metadata: languageName: node linkType: hard -"undici@npm:7.29.0": - version: 7.29.0 - resolution: "undici@npm:7.29.0" - checksum: 10c0/85ea96e91e7f3de24de678cbbc54230fdd641a3b0643005f2aef044af1f1a2db8fbecb88b28e6fa78bb41ffeafda4479b57075b6b6ab92beac4b7b5ab53f9b10 +"undici@npm:^7.29.1": + version: 7.30.0 + resolution: "undici@npm:7.30.0" + checksum: 10c0/011a321a9198532b0f0504740faa43b9ab8fcc0e4f60104e5c9cb808a61faeb03e4be4aec4f0c65f47913d7c7d9d91591793a6cb9ab843f98ca2c3d711c4a34c languageName: node linkType: hard From 9dc9aeef4308b6786a71c00cd2c6bb1e7e313c44 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Wed, 30 Sep 2026 13:06:17 +0200 Subject: [PATCH 2/2] feat(runtime): align runtime scans table with Sysdig Secure The same image running in several clusters showed up as seemingly duplicated rows with different in-use counts, the columns weren't self-explanatory, and severity chips were unreadable in dark mode. Mirror the Secure runtime view so users can map what they see in Backstage to Secure: cluster/namespace per row, In Use / Vulnerabilities / Policy Evaluation / Component Lifecycle / Exceptions columns with the same order, compact severity bars and header tooltips. Accepted (exception) results show as passed with an exception marker. Sorting on rendered columns never worked because cells were JSX and the column definitions were rebuilt every render; sort on raw values with a severity-aware comparator instead. --- package.json | 2 +- .../SysdigVMPipelineFetchComponent.test.tsx | 2 +- .../SysdigVMPipelineFetchComponent.tsx | 23 +-- .../SysdigVMRegistryFetchComponent.tsx | 15 +- .../SysdigVMRuntimeFetchComponent.test.tsx | 58 +++++++- .../SysdigVMRuntimeFetchComponent.tsx | 62 ++++++--- src/lib/index.ts | 6 + src/lib/ui.test.tsx | 37 +++++ src/lib/ui.tsx | 131 ++++++++++++++++-- 9 files changed, 282 insertions(+), 54 deletions(-) create mode 100644 src/lib/ui.test.tsx diff --git a/package.json b/package.json index 8d52227..e7b3ddd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sysdig/backstage-plugin-sysdig", - "version": "1.5.4", + "version": "1.6.0", "main": "dist/index.esm.js", "types": "dist/index.d.ts", "license": "Apache-2.0", diff --git a/src/components/SysdigVMPipelineFetchComponent/SysdigVMPipelineFetchComponent.test.tsx b/src/components/SysdigVMPipelineFetchComponent/SysdigVMPipelineFetchComponent.test.tsx index eff5547..e22ac58 100644 --- a/src/components/SysdigVMPipelineFetchComponent/SysdigVMPipelineFetchComponent.test.tsx +++ b/src/components/SysdigVMPipelineFetchComponent/SysdigVMPipelineFetchComponent.test.tsx @@ -120,7 +120,7 @@ describe('SysdigVMPipelineFetchComponent', () => { ); expect(await screen.findByText('ghcr.io/sysdiglabs/sample-app:latest')).toBeInTheDocument(); - expect(screen.getByText('failed')).toBeInTheDocument(); + expect(screen.getByText('Failed')).toBeInTheDocument(); }); it('filters out rows with null policyEvaluationResult', async () => { diff --git a/src/components/SysdigVMPipelineFetchComponent/SysdigVMPipelineFetchComponent.tsx b/src/components/SysdigVMPipelineFetchComponent/SysdigVMPipelineFetchComponent.tsx index d2e0b2e..4cd4ce9 100644 --- a/src/components/SysdigVMPipelineFetchComponent/SysdigVMPipelineFetchComponent.tsx +++ b/src/components/SysdigVMPipelineFetchComponent/SysdigVMPipelineFetchComponent.tsx @@ -25,9 +25,10 @@ import { SYSDIG_IMAGE_FREETEXT_ANNOTATION, // methods - getStatusColorSpan, + getPolicyEvaluation, getTitleWithBacklink, getChips, + compareSeverities, getBacklink } from '../../lib' import { sysdigApiRef } from '../../api'; @@ -72,23 +73,25 @@ type DenseTableProps = { ... */ -export const DenseTable = ({ pipelineScans, title }: DenseTableProps) => { - const columns: TableColumn[] = [ - { title: 'Status', field: 'policyEvalStatus', width: "2%" }, - { title: 'Image ID', field: 'imageId', width: "23%" }, - { title: 'Asset Name', field: 'asset', width: "35%" }, - { title: 'Vulnerabilities', field: 'vulns', width: "35%" }, +// defined once: material-table resets its sort state when column definitions change between renders +const columns: TableColumn[] = [ + { title: 'Asset Name', field: 'asset', width: "40%" }, + { title: 'Image ID', field: 'imageId', width: "25%" }, + { title: 'Vulnerabilities', field: 'vulns', width: "25%", render: (row: any) => getChips(row.vulns), customSort: (a: any, b: any) => compareSeverities(a.vulns, b.vulns) }, + { title: 'Policy Evaluation', field: 'policyEvalStatus', width: "10%", render: (row: any) => getPolicyEvaluation(row.policyEvalStatus) }, // { title: 'Last Evaluated At', field: 'lastEvaluatedAt', width: "15%" }, // { title: 'URL', field: "url", width: "10%" }, - ]; +]; + +export const DenseTable = ({ pipelineScans, title }: DenseTableProps) => { const data = pipelineScans.filter(scan => { return scan.policyEvaluationResult !== null && scan.policyEvaluationResult !== '' }) .flatMap(scan => { return { - policyEvalStatus: getStatusColorSpan(scan.policyEvaluationResult), + policyEvalStatus: scan.policyEvaluationResult, imageId: {scan.imageId}, asset: scan.pullString, - vulns: getChips(scan.vulnTotalBySeverity), + vulns: scan.vulnTotalBySeverity, // convert image.lastEvaluatedAt to a date string // lastEvaluatedAt: getDate(image.lastEvaluatedAt * 1000), // https://prodmon.app.sysdig.com/secure/#/scanning/scan-results/quay.io%2Fsysdig%2Fsysdigcloud-backend%3A5.1.0.10598-sysdig-meerkat-collector/id/497c07ec287acc1800dc84a91ac1260e910c603cabc8febd754b909f406a6e26/summaries diff --git a/src/components/SysdigVMRegistryFetchComponent/SysdigVMRegistryFetchComponent.tsx b/src/components/SysdigVMRegistryFetchComponent/SysdigVMRegistryFetchComponent.tsx index d975b8c..4e42226 100644 --- a/src/components/SysdigVMRegistryFetchComponent/SysdigVMRegistryFetchComponent.tsx +++ b/src/components/SysdigVMRegistryFetchComponent/SysdigVMRegistryFetchComponent.tsx @@ -28,6 +28,7 @@ import { // methods getChips, + compareSeverities, getTitleWithBacklink, getBacklink } from '../../lib'; @@ -70,19 +71,21 @@ type DenseTableProps = { ... */ +// defined once: material-table resets its sort state when column definitions change between renders +const columns: TableColumn[] = [ + { title: 'Asset Name', field: 'asset', width: "45%" }, + { title: 'Image ID', field: 'imageId', width: "25%" }, + { title: 'Vulnerabilities', field: 'severity', width: "30%", render: (row: any) => getChips(row.severity), customSort: (a: any, b: any) => compareSeverities(a.severity, b.severity) }, +]; + export const DenseTable = ({ registryScans, title }: DenseTableProps) => { - const columns: TableColumn[] = [ - { title: 'Image ID', field: 'imageId', width: "20%" }, - { title: 'Asset Name', field: 'asset', width: "35%" }, - { title: 'Severity', field: 'severity', width: "30%" }, - ]; const data = registryScans.filter(scan => { return scan.imageId !== '' }) .flatMap(scan => { return { imageId: {scan.imageId}, asset: scan.pullString, - severity: getChips(scan.vulnTotalBySeverity) + severity: scan.vulnTotalBySeverity }; }); diff --git a/src/components/SysdigVMRuntimeFetchComponent/SysdigVMRuntimeFetchComponent.test.tsx b/src/components/SysdigVMRuntimeFetchComponent/SysdigVMRuntimeFetchComponent.test.tsx index 03d9e51..a551eaf 100644 --- a/src/components/SysdigVMRuntimeFetchComponent/SysdigVMRuntimeFetchComponent.test.tsx +++ b/src/components/SysdigVMRuntimeFetchComponent/SysdigVMRuntimeFetchComponent.test.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { screen } from '@testing-library/react'; +import { screen, fireEvent, within } from '@testing-library/react'; import { SysdigVMRuntimeFetchComponent } from './SysdigVMRuntimeFetchComponent'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; @@ -131,7 +131,61 @@ describe('SysdigVMRuntimeFetchComponent', () => { ); expect(await screen.findByText('nginx:latest')).toBeInTheDocument(); - expect(screen.getByText('failed')).toBeInTheDocument(); + expect(screen.getByText('Failed')).toBeInTheDocument(); + expect(screen.getByText('test-cluster')).toBeInTheDocument(); + expect(screen.getByText('test-namespace')).toBeInTheDocument(); + + fireEvent.mouseOver(screen.getByText('Policy Evaluation')); + expect(await screen.findByText(/Failed = at least one policy rule was violated/)).toBeInTheDocument(); + }); + + it('shows accepted results as passed with an exception, and past EOL dates', async () => { + const acceptedScan = { ...mockRuntimeScanV1, policyEvaluationResult: 'accepted', endOfLifeDate: '2018-05-01T00:00:00Z' }; + const apiWithData = { + ...mockSysdigApi, + fetchVulnRuntime: jest.fn().mockResolvedValue({ data: [acceptedScan] }), + }; + + await renderInTestApp( + + + + + + ); + + expect(await screen.findByText('Passed')).toBeInTheDocument(); + expect(screen.getByText('EOL')).toBeInTheDocument(); + expect(screen.getByLabelText(/Risk accepted/)).toBeInTheDocument(); + }); + + it('falls back to region when scope has no kubernetes cluster', async () => { + const hostScan = { + ...mockRuntimeScanV1, + scope: { 'asset.type': 'host', 'cloudProvider.region': 'phx' }, + }; + const apiWithData = { + ...mockSysdigApi, + fetchVulnRuntime: jest.fn().mockResolvedValue({ data: [hostScan] }), + }; + + await renderInTestApp( + + + + + + ); + + const row = (await screen.findByText('phx')).closest('tr')!; + // columns: Asset Name, Cluster, Namespace, ... + expect(within(row).getAllByRole('cell')[2]).toHaveTextContent(/^-$/); }); it('filters out rows with null policyEvaluationResult', async () => { diff --git a/src/components/SysdigVMRuntimeFetchComponent/SysdigVMRuntimeFetchComponent.tsx b/src/components/SysdigVMRuntimeFetchComponent/SysdigVMRuntimeFetchComponent.tsx index 6eafd69..cd7e2b1 100644 --- a/src/components/SysdigVMRuntimeFetchComponent/SysdigVMRuntimeFetchComponent.tsx +++ b/src/components/SysdigVMRuntimeFetchComponent/SysdigVMRuntimeFetchComponent.tsx @@ -30,16 +30,22 @@ import { SYSDIG_CUSTOM_FILTER_ANNOTATION, // methods - getStatusColorSpan, + getPolicyEvaluation, + getLifecycle, + getException, getChips, + IN_USE_SEVERITIES, + compareSeverities, getDetails, getTitleWithBacklink, + getHeaderWithTooltip, getBacklink } from '../../lib' import { sysdigApiRef } from '../../api'; type RuntimeScan = { + endOfLifeDate?: string, isRiskSpotlightEnabled: boolean, mainAssetName: string, policyEvaluationResult: string, @@ -55,11 +61,13 @@ type RuntimeScan = { sbomId: string, scope: { "asset.type": string, - "kubernetes.cluster.name": string, - "kubernetes.namespace.name": string, - "kubernetes.pod.container.name": string, - "kubernetes.workload.name": string, - "kubernetes.workload.type": string + "kubernetes.cluster.name"?: string, + "kubernetes.namespace.name"?: string, + "kubernetes.pod.container.name"?: string, + "kubernetes.workload.name"?: string, + "kubernetes.workload.type"?: string, + "agent.tag.cluster"?: string, + "cloudProvider.region"?: string }, vulnTotalBySeverity: { critical: number, @@ -110,25 +118,39 @@ type DenseTableProps = { ... */ -export const DenseTable = ({ runtimeScans, title }: DenseTableProps) => { - const columns: TableColumn[] = [ - { title: 'Status', field: 'policyEvalStatus', width: "2%" }, - { title: 'Asset Name', field: 'asset', width: "18%" }, - { title: 'Severity', field: 'severity', width: "35%" }, - { title: 'In Use', field: 'inUse', width: "35%" }, - { title: 'Details', field: 'details', width: "10%" }, +// Same image can run in several clusters/namespaces, each with its own in-use vulns +const getLocation = (scope: RuntimeScan['scope'] | undefined) => ({ + cluster: scope?.["kubernetes.cluster.name"] ?? scope?.["agent.tag.cluster"] ?? scope?.["cloudProvider.region"] ?? '-', + namespace: scope?.["kubernetes.namespace.name"] ?? '-', +}); + +// defined once: material-table resets its sort state when column definitions change between renders. +// no defaultSort: in material-table 3.x it breaks the sort cycle of other columns, data is pre-sorted instead +const columns: TableColumn[] = [ + { title: 'Asset Name', field: 'asset', width: "23%" }, + { title: 'Cluster', field: 'cluster', width: "12%" }, + { title: 'Namespace', field: 'namespace', width: "12%" }, + { title: getHeaderWithTooltip('In Use', 'Only vulnerabilities in packages loaded in memory at runtime.'), field: 'inUse', width: "14%", render: (row: any) => getChips(row.inUse, IN_USE_SEVERITIES), customSort: (a: any, b: any) => compareSeverities(a.inUse, b.inUse) }, + { title: getHeaderWithTooltip('Vulnerabilities', 'All vulnerabilities found in the image.'), field: 'severity', width: "14%", render: (row: any) => getChips(row.severity), customSort: (a: any, b: any) => compareSeverities(a.severity, b.severity) }, + { title: getHeaderWithTooltip('Policy Evaluation', 'Result of the vulnerability policy evaluation. Failed = at least one policy rule was violated.'), field: 'policyEvalStatus', width: "9%", render: (row: any) => getPolicyEvaluation(row.policyEvalStatus) }, + { title: getHeaderWithTooltip('Component Lifecycle', 'Active = still supported. EOL = the image base OS or runtime is past its end-of-life date and no longer receives security fixes.'), field: 'endOfLifeDate', width: "7%", render: (row: any) => getLifecycle(row.endOfLifeDate), customSort: (a: any, b: any) => (a.endOfLifeDate ?? '9999').localeCompare(b.endOfLifeDate ?? '9999') }, + { title: 'Exceptions', field: 'exception', width: "5%", render: (row: any) => getException(row.policyEvalStatus), customSort: (a: any, b: any) => Number(a.policyEvalStatus === 'accepted') - Number(b.policyEvalStatus === 'accepted') }, + { title: 'Details', field: 'details', width: "4%", sorting: false }, // { title: 'Last Evaluated At', field: 'lastEvaluatedAt', width: "15%" }, // { title: 'URL', field: "url", width: "10%" }, - ]; +]; + +export const DenseTable = ({ runtimeScans, title }: DenseTableProps) => { const data = runtimeScans.filter(scan => { return scan.policyEvaluationResult !== null && scan.policyEvaluationResult !== '' }) .flatMap(scan => { return { - policyEvalStatus: getStatusColorSpan(scan.policyEvaluationResult), + policyEvalStatus: scan.policyEvaluationResult, + endOfLifeDate: scan.endOfLifeDate, asset: scan.mainAssetName, -// scope: JSON.stringify(scan.scope), - severity: getChips(scan.vulnTotalBySeverity), - inUse: getChips(scan.runningVulnTotalBySeverity), + ...getLocation(scan.scope), + severity: scan.vulnTotalBySeverity, + inUse: scan.runningVulnTotalBySeverity, details: getDetails(scan) // convert image.lastEvaluatedAt to a date string // lastEvaluatedAt: getDate(image.lastEvaluatedAt * 1000), @@ -136,7 +158,9 @@ export const DenseTable = ({ runtimeScans, title }: DenseTableProps) => { // url: getUrl('https://prodmon.app.sysdig.com/api/scanning/v1/images/by_id/' + image.imageId + '?fulltag=' + image.repo + ':' + image.tag), // url: getUrl('https://prodmon.app.sysdig.com/secure/#/scanning/scan-results/' + urlEncode(image.repo + ':' + image.tag) +' /id/' + image.imageId + '/summaries'), }; - }); + }) + // same initial order as Sysdig Secure: most in-use vulnerabilities first + .sort((a, b) => compareSeverities(b.inUse, a.inUse)); return ( + ({ critical, high, medium, low, negligible }); + +describe('compareSeverities', () => { + it('orders by critical first, then lower severities as tie-breakers', () => { + const sorted = [counts(0, 50), counts(2, 0), counts(2, 5), counts(0, 50, 1)].sort(compareSeverities); + expect(sorted).toEqual([counts(0, 50), counts(0, 50, 1), counts(2, 0), counts(2, 5)]); + }); + + it('sorts missing data below any counts', () => { + expect(compareSeverities(undefined, counts(0))).toBeLessThan(0); + expect(compareSeverities(counts(0), undefined)).toBeGreaterThan(0); + }); +}); + +describe('getLifecycle', () => { + it('shows EOL for past dates, Active for future ones, nothing when unknown', () => { + const { rerender, container } = render(<>{getLifecycle('2018-05-01T00:00:00Z')}); + expect(screen.getByText('EOL')).toBeInTheDocument(); + rerender(<>{getLifecycle('2999-01-01T00:00:00Z')}); + expect(screen.getByText('Active')).toBeInTheDocument(); + rerender(<>{getLifecycle(undefined)}); + expect(container).toBeEmptyDOMElement(); + }); +}); + +describe('getChips', () => { + it('only renders the requested severities', () => { + render(<>{getChips(counts(2, 17, 22, 4, 16), IN_USE_SEVERITIES)}); + expect(screen.getByText('22')).toBeInTheDocument(); + expect(screen.queryByText('4')).not.toBeInTheDocument(); + expect(screen.queryByText('16')).not.toBeInTheDocument(); + }); +}); diff --git a/src/lib/ui.tsx b/src/lib/ui.tsx index 7b00b43..4d4a9a7 100644 --- a/src/lib/ui.tsx +++ b/src/lib/ui.tsx @@ -1,3 +1,4 @@ +import Box from '@mui/material/Box'; import Chip from '@mui/material/Chip'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; @@ -5,34 +6,118 @@ import InfoIcon from '@mui/icons-material/Info'; import Typography from '@mui/material/Typography'; import { Gauge } from '@backstage/core-components'; import OpenInNew from '@mui/icons-material/OpenInNew'; +import InfoOutlined from '@mui/icons-material/InfoOutlined'; +import HighlightOff from '@mui/icons-material/HighlightOff'; +import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline'; +import WbTwilight from '@mui/icons-material/WbTwilight'; +import GppGoodOutlined from '@mui/icons-material/GppGoodOutlined'; const interleave = (arr: any, thing: any) => [].concat(...arr.map((n: any) => [n, thing])).slice(0, -1) const getStatusColorSpan = (status: string) => { - let hStyle; + // theme palette colors adapt to light/dark themes + let color; if (status === 'failed' || status === 'false') { - hStyle = { color: 'red' }; + color = 'error.main'; } else if (status === 'passed' || status === 'true') { - hStyle = { color: 'green' }; + color = 'success.main'; } else { - hStyle = { color: 'gray' }; + color = 'text.secondary'; } - return {status}; + return {status}; }; -function getChips(severities: any) { - if (!severities) { return } +const badgeStyle = { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '2px 8px', borderRadius: 4, fontSize: 13, whiteSpace: 'nowrap' as const }; +const iconStyle = { fontSize: 16 }; + +// policy evaluation badge (like Sysdig Secure); "accepted" means an exception made the result pass +const getPolicyEvaluation = (status: string) => { + if (status === 'failed') { + return Failed; + } + if (status === 'passed' || status === 'accepted') { + return Passed; + } + return {status}; +}; + +// component lifecycle badge: EOL once past the end-of-life date, Active before it, nothing if unknown +const getLifecycle = (endOfLifeDate?: string) => { + if (!endOfLifeDate) return null; + const date = endOfLifeDate.slice(0, 10); + if (new Date(endOfLifeDate) <= new Date()) { + return ( + + EOL + + ); + } + return ( + + Active + + ); +}; + +const getException = (status: string) => { + if (status !== 'accepted') return null; + return ( + + + + ); +}; + +// fixed text color so chips stay readable in both light and dark themes +const chipStyle = (backgroundColor: string) => ({ backgroundColor, color: 'black' }); + +const SEVERITIES = [ + { key: 'critical', label: 'Critical', bg: '#A13CC4', fg: 'white' }, + { key: 'high', label: 'High', bg: '#D32F2F', fg: 'white' }, + { key: 'medium', label: 'Medium', bg: '#F28C28', fg: 'black' }, + { key: 'low', label: 'Low', bg: '#F5C518', fg: 'black' }, + { key: 'negligible', label: 'Negligible', bg: '#9E9E9E', fg: 'black' }, +]; + +const segmentStyle = { minWidth: 36, padding: '2px 6px', fontSize: 12, fontWeight: 600, textAlign: 'center' as const, whiteSpace: 'nowrap' as const }; + +// compact severity bar (like Sysdig Secure): counts only, severity name on hover, "-" for zero +// Sysdig Secure only shows critical, high and medium for in-use vulnerabilities +const IN_USE_SEVERITIES = ['critical', 'high', 'medium']; + +function getChips(severities: any, keys: string[] = SEVERITIES.map(sev => sev.key)) { + if (!severities) { + return ( + + N/A + + ); + } return ( - - - - - - + + {SEVERITIES.filter(sev => keys.includes(sev.key)).map(({ key, label, bg, fg }) => { + const count = severities[key] ?? 0; + return ( + + + {count || '-'} + + + ); + })} ); } +// sort by most severe first: critical, then high, medium, low, negligible +const compareSeverities = (a: any, b: any) => { + for (const { key } of SEVERITIES) { + const diff = (a?.[key] ?? -1) - (b?.[key] ?? -1); + if (diff !== 0) return diff; + } + return 0; +}; + function getScope(scope: any) { const textScope = []; for (const key in scope) { @@ -92,7 +177,7 @@ function truncate(str: string, n: number){ function getFailed(failed: any) { const result: JSX.Element[] = []; for (const policy of failed.filter((f: { [x: string]: any; }) => !f.pass)) { - result.push(); + result.push(); } return result; } @@ -100,7 +185,7 @@ function getFailed(failed: any) { function getPassed(passed: any) { const result: JSX.Element[] = []; for (const policy of passed.filter((f: { [x: string]: any; }) => f.pass)) { - result.push(); + result.push(); } return result; } @@ -160,6 +245,16 @@ const getUrl = (url: string) => { return Link to Scan Result; }; +// column header with an explanatory tooltip +const getHeaderWithTooltip = (title: string, help: string) => ( + + + {title} + + + +); + // URL encode a string const urlEncode = (str: string) => { return encodeURIComponent(str); @@ -167,7 +262,12 @@ const urlEncode = (str: string) => { export { getStatusColorSpan, + getPolicyEvaluation, + getLifecycle, + getException, getChips, + IN_USE_SEVERITIES, + compareSeverities, getDetails, getDate, getUrl, @@ -177,5 +277,6 @@ export { getPassed, getResourceName, getTitleWithBacklink, + getHeaderWithTooltip, urlEncode };