Skip to content

Commit

Permalink
Merge pull request #13329 from jeff-phillips-18/pipeline-cves
Browse files Browse the repository at this point in the history
ODC-7421: Show vulnerability column in the pipelinerun list page
  • Loading branch information
openshift-merge-bot[bot] committed Nov 16, 2023
2 parents 100ffa6 + 434c2cf commit b85b6f7
Show file tree
Hide file tree
Showing 15 changed files with 686 additions and 26 deletions.
Expand Up @@ -455,6 +455,7 @@ export type ResourceLinkProps = {
dataTest?: string;
onClick?: () => void;
truncate?: boolean;
nameSuffix?: React.ReactNode;
children?: React.ReactNode;
};

Expand Down
Expand Up @@ -85,6 +85,7 @@
"Pull request review comment": "Pull request review comment",
"Push": "Push",
"Status": "Status",
"Vulnerabilities": "Vulnerabilities",
"Download SBOM": "Download SBOM",
"Copy": "Copy",
"Copied": "Copied",
Expand All @@ -103,6 +104,7 @@
"Namespace": "Namespace",
"Task status": "Task status",
"Started": "Started",
"Signed": "Signed",
"Unknown failure condition": "Unknown failure condition",
"Failure on task {{taskName}} - check logs for details.": "Failure on task {{taskName}} - check logs for details.",
"Error downloading logs.": "Error downloading logs.",
Expand All @@ -115,13 +117,16 @@
"Failure - check logs for details.": "Failure - check logs for details.",
"Message": "Message",
"Log snippet": "Log snippet",
"Signed": "Signed",
"Parameters": "Parameters",
"Logs": "Logs",
"Value": "Value",
"No parameters are associated with this PipelineRun.": "No parameters are associated with this PipelineRun.",
"Select a Project to view the list of {{pipelineRunLabel}}<2></2>.": "Select a Project to view the list of {{pipelineRunLabel}}<2></2>.",
"View logs": "View logs",
"Critical": "Critical",
"High": "High",
"Medium": "Medium",
"Low": "Low",
"View GitHub App": "View GitHub App",
"EventListener details": "EventListener details",
"URL": "URL",
Expand Down
Expand Up @@ -23,6 +23,7 @@ import WorkspaceResourceLinkList from '../../shared/workspaces/WorkspaceResource
import { useTaskRuns } from '../../taskruns/useTaskRuns';
import { getPLRLogSnippet } from '../logs/pipelineRunLogSnippet';
import RunDetailsErrorLog from '../logs/RunDetailsErrorLog';
import PipelineRunVulnerabilities from '../status/PipelineRunVulnerabilities';
import TriggeredBySection from './TriggeredBySection';

export type PipelineRunCustomDetailsProps = {
Expand Down Expand Up @@ -59,6 +60,12 @@ const PipelineRunCustomDetails: React.FC<PipelineRunCustomDetailsProps> = ({ pip
namespace={pipelineRun.metadata.namespace}
/>
)}
<dl>
<dt>{t('pipelines-plugin~Vulnerabilities')}</dt>
<dd>
<PipelineRunVulnerabilities pipelineRun={pipelineRun} />
</dd>
</dl>
<dl>
<dt>{t('pipelines-plugin~Pipeline')}</dt>
<dd>
Expand Down
@@ -0,0 +1,58 @@
import { testHook } from '../../../../../../../__tests__/utils/hooks-utils';
import {
PipeLineRunWithVulnerabilitiesData,
PipeLineRunWithVulnerabilitiesNames,
} from '../../../../test-data/pipeline-data';
import { usePipelineRunVulnerabilities } from '../usePipelineRunVulnerabilities';

describe('usePLRVulnerabilities', () => {
it('should return vulnerability scan results', () => {
const {
result: { current: scanResults },
} = testHook(() =>
usePipelineRunVulnerabilities(
PipeLineRunWithVulnerabilitiesData[PipeLineRunWithVulnerabilitiesNames.ScanOutput],
),
);
expect(scanResults.vulnerabilities.critical).toBe(13);
expect(scanResults.vulnerabilities.high).toBe(29);
expect(scanResults.vulnerabilities.medium).toBe(32);
expect(scanResults.vulnerabilities.low).toBe(3);
});
it('should accept any scan results', () => {
const {
result: { current: scanResults },
} = testHook(() =>
usePipelineRunVulnerabilities(
PipeLineRunWithVulnerabilitiesData[PipeLineRunWithVulnerabilitiesNames.MyScanOutput],
),
);
expect(scanResults.vulnerabilities.critical).toBe(0);
expect(scanResults.vulnerabilities.high).toBe(9);
expect(scanResults.vulnerabilities.medium).toBe(2);
expect(scanResults.vulnerabilities.low).toBe(13);
});
it('should ignore improper scan results', () => {
const {
result: { current: scanResults },
} = testHook(() =>
usePipelineRunVulnerabilities(
PipeLineRunWithVulnerabilitiesData[PipeLineRunWithVulnerabilitiesNames.InvalidScanOutput],
),
);
expect(scanResults.vulnerabilities).toBeUndefined();
});
it('should aggregate vulnerability scan results', () => {
const {
result: { current: scanResults },
} = testHook(() =>
usePipelineRunVulnerabilities(
PipeLineRunWithVulnerabilitiesData[PipeLineRunWithVulnerabilitiesNames.MultipleScanOutput],
),
);
expect(scanResults.vulnerabilities.critical).toBe(13);
expect(scanResults.vulnerabilities.high).toBe(38);
expect(scanResults.vulnerabilities.medium).toBe(34);
expect(scanResults.vulnerabilities.low).toBe(16);
});
});
@@ -0,0 +1,44 @@
import * as React from 'react';
import { PipelineRunKind } from '../../../types';

const SCAN_OUTPUT_SUFFIX = 'SCAN_OUTPUT';

export type ScanResults = {
vulnerabilities?: {
critical: number;
high: number;
medium: number;
low: number;
};
};

export const getPipelineRunVulnerabilities = (pipelineRun: PipelineRunKind): ScanResults => {
return pipelineRun.status?.results?.reduce((acc, result) => {
if (result.name?.endsWith(SCAN_OUTPUT_SUFFIX)) {
if (!acc.vulnerabilities) {
acc.vulnerabilities = { critical: 0, high: 0, medium: 0, low: 0 };
}
try {
const taskVulnerabilities = JSON.parse(result.value);
if (taskVulnerabilities.vulnerabilities) {
acc.vulnerabilities.critical += taskVulnerabilities.vulnerabilities.critical || 0;
acc.vulnerabilities.high += taskVulnerabilities.vulnerabilities.high || 0;
acc.vulnerabilities.medium += taskVulnerabilities.vulnerabilities.medium || 0;
acc.vulnerabilities.low += taskVulnerabilities.vulnerabilities.low || 0;
}
} catch (e) {
// ignore
}
}
return acc;
}, {} as ScanResults);
};

export const usePipelineRunVulnerabilities = (pipelineRun: PipelineRunKind): ScanResults =>
React.useMemo(() => {
if (!pipelineRun) {
return null;
}

return getPipelineRunVulnerabilities(pipelineRun);
}, [pipelineRun]);
Expand Up @@ -8,42 +8,48 @@ const PipelineRunHeader = () => {
title: i18n.t('pipelines-plugin~Name'),
sortField: 'metadata.name',
transforms: [sortable],
props: { className: tableColumnClasses[0] },
props: { className: tableColumnClasses.name },
},
{
title: i18n.t('pipelines-plugin~Namespace'),
sortField: 'metadata.namespace',
transforms: [sortable],
props: { className: tableColumnClasses[1] },
props: { className: tableColumnClasses.namespace },
id: 'namespace',
},
{
title: i18n.t('pipelines-plugin~Vulnerabilities'),
sortFunc: 'vulnerabilities',
transforms: [sortable],
props: { className: tableColumnClasses.vulnerabilities },
},
{
title: i18n.t('pipelines-plugin~Status'),
sortField: 'status.conditions[0].reason',
transforms: [sortable],
props: { className: tableColumnClasses[2] },
props: { className: tableColumnClasses.status },
},
{
title: i18n.t('pipelines-plugin~Task status'),
sortField: 'status.conditions[0].reason',
transforms: [sortable],
props: { className: tableColumnClasses[3] },
props: { className: tableColumnClasses.taskStatus },
},
{
title: i18n.t('pipelines-plugin~Started'),
sortField: 'status.startTime',
transforms: [sortable],
props: { className: tableColumnClasses[4] },
props: { className: tableColumnClasses.started },
},
{
title: i18n.t('pipelines-plugin~Duration'),
sortField: 'status.completionTime',
transforms: [sortable],
props: { className: tableColumnClasses[5] },
props: { className: tableColumnClasses.duration },
},
{
title: '',
props: { className: tableColumnClasses[6] },
props: { className: tableColumnClasses.actions },
},
];
};
Expand Down
@@ -0,0 +1,12 @@
.opp-pipeline-run-list {
&__signed-indicator {
display: inline-block;
--pf-c-table--cell--Color: var(--pf-global--BackgroundColor--dark-transparent-100);
margin-left: var(--pf-global--spacer-sm);
> img {
height: var(--pf-global--FontSize--lg);
position: relative;
top: 4px;
}
}
}
Expand Up @@ -7,11 +7,15 @@ import { Table } from '@console/internal/components/factory';
import { useUserSettings } from '@console/shared/src';
import { PREFERRED_DEV_PIPELINE_PAGE_TAB_USER_SETTING_KEY } from '../../../const';
import { PipelineRunModel } from '../../../models';
import { PipelineRunKind } from '../../../types';
import { usePipelineOperatorVersion } from '../../pipelines/utils/pipeline-operator';
import { useTaskRuns } from '../../taskruns/useTaskRuns';
import { getPipelineRunVulnerabilities } from '../hooks/usePipelineRunVulnerabilities';
import PipelineRunHeader from './PipelineRunHeader';
import PipelineRunRow from './PipelineRunRow';

import './PipelineRunList.scss';

type PipelineRunListProps = {
namespace: string;
};
Expand Down Expand Up @@ -44,6 +48,21 @@ export const PipelineRunList: React.FC<PipelineRunListProps> = (props) => {
defaultSortOrder={SortByDirection.desc}
Header={PipelineRunHeader}
Row={PipelineRunRow}
customSorts={{
vulnerabilities: (obj: PipelineRunKind) => {
const scanResults = getPipelineRunVulnerabilities(obj);
if (!scanResults?.vulnerabilities) {
return -1;
}
// Expect no more than 999 of any one severity
return (
(scanResults.vulnerabilities.critical ?? 0) * 1000000000 +
(scanResults.vulnerabilities.high ?? 0) * 1000000 +
(scanResults.vulnerabilities.medium ?? 0) * 1000 +
(scanResults.vulnerabilities.low ?? 0)
);
},
}}
customData={{ operatorVersion, taskRuns: taskRunsLoaded ? taskRuns : [] }}
virtualize
/>
Expand Down
@@ -1,7 +1,10 @@
import * as React from 'react';
import { Tooltip } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { TableData, RowFunctionArgs } from '@console/internal/components/factory';
import { ResourceLink, Timestamp } from '@console/internal/components/utils';
import { Timestamp, ResourceLink } from '@console/internal/components/utils';
import { referenceForModel } from '@console/internal/module/k8s';
import * as SignedPipelinerunIcon from '../../../images/signed-badge.svg';
import { PipelineRunModel } from '../../../models';
import { PipelineRunKind, TaskRunKind } from '../../../types';
import { getPipelineRunKebabActions } from '../../../utils/pipeline-actions';
Expand All @@ -10,9 +13,11 @@ import {
pipelineRunTitleFilterReducer,
} from '../../../utils/pipeline-filter-reducer';
import { pipelineRunDuration } from '../../../utils/pipeline-utils';
import { chainsSignedAnnotation } from '../../pipelines/const';
import { getTaskRunsOfPipelineRun } from '../../taskruns/useTaskRuns';
import LinkedPipelineRunTaskStatus from '../status/LinkedPipelineRunTaskStatus';
import PipelineRunStatus from '../status/PipelineRunStatus';
import PipelineRunVulnerabilities from '../status/PipelineRunVulnerabilities';
import { ResourceKebabWithUserLabel } from '../triggered-by';
import { tableColumnClasses } from './pipelinerun-table';

Expand All @@ -35,32 +40,46 @@ const PLRStatus: React.FC<PLRStatusProps> = ({ obj, taskRuns }) => {
};

const PipelineRunRow: React.FC<RowFunctionArgs<PipelineRunKind>> = ({ obj, customData }) => {
const { t } = useTranslation();
const { operatorVersion, taskRuns } = customData;
const PLRTaskRuns = getTaskRunsOfPipelineRun(taskRuns, obj?.metadata?.name);

return (
<>
<TableData className={tableColumnClasses[0]}>
<TableData className={tableColumnClasses.name}>
<ResourceLink
kind={pipelinerunReference}
name={obj.metadata.name}
namespace={obj.metadata.namespace}
data-test-id={obj.metadata.name}
nameSuffix={
obj?.metadata?.annotations?.[chainsSignedAnnotation] === 'true' ? (
<Tooltip content={t('pipelines-plugin~Signed')}>
<div className="opp-pipeline-run-list__signed-indicator">
<img src={SignedPipelinerunIcon} alt={t('pipelines-plugin~Signed')} />
</div>
</Tooltip>
) : null
}
/>
</TableData>
<TableData className={tableColumnClasses[1]} columnID="namespace">
<TableData className={tableColumnClasses.namespace} columnID="namespace">
<ResourceLink kind="Namespace" name={obj.metadata.namespace} />
</TableData>
<TableData className={tableColumnClasses[2]}>
<TableData className={tableColumnClasses.vulnerabilities}>
<PipelineRunVulnerabilities pipelineRun={obj} condensed />
</TableData>
<TableData className={tableColumnClasses.status}>
<PLRStatus obj={obj} taskRuns={PLRTaskRuns} />
</TableData>
<TableData className={tableColumnClasses[3]}>
<TableData className={tableColumnClasses.taskStatus}>
<LinkedPipelineRunTaskStatus pipelineRun={obj} taskRuns={PLRTaskRuns} />
</TableData>
<TableData className={tableColumnClasses[4]}>
<TableData className={tableColumnClasses.started}>
<Timestamp timestamp={obj.status && obj.status.startTime} />
</TableData>
<TableData className={tableColumnClasses[5]}>{pipelineRunDuration(obj)}</TableData>
<TableData className={tableColumnClasses[6]}>
<TableData className={tableColumnClasses.duration}>{pipelineRunDuration(obj)}</TableData>
<TableData className={tableColumnClasses.actions}>
<ResourceKebabWithUserLabel
actions={getPipelineRunKebabActions(operatorVersion, taskRuns)}
kind={pipelinerunReference}
Expand Down
@@ -1,11 +1,12 @@
import { Kebab } from '@console/internal/components/utils';

export const tableColumnClasses = [
'', // name
'', // namespace
'pf-m-hidden pf-m-visible-on-sm', // status
'pf-m-hidden pf-m-visible-on-lg', // task status
'pf-m-hidden pf-m-visible-on-lg', // started
'pf-m-hidden pf-m-visible-on-xl', // duration
Kebab.columnClass,
];
export const tableColumnClasses = {
name: '',
namespace: '',
vulnerabilities: 'pf-m-hidden pf-m-visible-on-md',
status: 'pf-m-hidden pf-m-visible-on-sm',
taskStatus: 'pf-m-hidden pf-m-visible-on-lg',
started: 'pf-m-hidden pf-m-visible-on-lg',
duration: 'pf-m-hidden pf-m-visible-on-xl',
actions: Kebab.columnClass,
};
@@ -0,0 +1,20 @@
.opp-vulnerabilities {
display: flex;
flex-wrap: wrap;
gap: var(--pf-global--spacer--sm);
&__severity {
align-items: center;
display: flex;
flex-wrap: nowrap;
gap: var(--pf-global--spacer--xs);
}
&__severity-status {
align-items: center;
display: flex;
flex-wrap: nowrap;
gap: var(--pf-global--spacer--xs);
}
&__severity-count {
font-weight: var(--pf-global--FontWeight--bold);
}
}

0 comments on commit b85b6f7

Please sign in to comment.