diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts index e9350dea4da..05a5a403857 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts @@ -455,6 +455,7 @@ export type ResourceLinkProps = { dataTest?: string; onClick?: () => void; truncate?: boolean; + nameSuffix?: React.ReactNode; children?: React.ReactNode; }; diff --git a/frontend/packages/pipelines-plugin/locales/en/pipelines-plugin.json b/frontend/packages/pipelines-plugin/locales/en/pipelines-plugin.json index 440d4e4b245..03482b451ca 100644 --- a/frontend/packages/pipelines-plugin/locales/en/pipelines-plugin.json +++ b/frontend/packages/pipelines-plugin/locales/en/pipelines-plugin.json @@ -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", @@ -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.", @@ -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>.": "Select a Project to view the list of {{pipelineRunLabel}}<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", diff --git a/frontend/packages/pipelines-plugin/src/components/pipelineruns/detail-page-tabs/PipelineRunCustomDetails.tsx b/frontend/packages/pipelines-plugin/src/components/pipelineruns/detail-page-tabs/PipelineRunCustomDetails.tsx index 6b42b0353a0..d587c1a0fdd 100644 --- a/frontend/packages/pipelines-plugin/src/components/pipelineruns/detail-page-tabs/PipelineRunCustomDetails.tsx +++ b/frontend/packages/pipelines-plugin/src/components/pipelineruns/detail-page-tabs/PipelineRunCustomDetails.tsx @@ -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 = { @@ -59,6 +60,12 @@ const PipelineRunCustomDetails: React.FC = ({ pip namespace={pipelineRun.metadata.namespace} /> )} +
+
{t('pipelines-plugin~Vulnerabilities')}
+
+ +
+
{t('pipelines-plugin~Pipeline')}
diff --git a/frontend/packages/pipelines-plugin/src/components/pipelineruns/hooks/__tests__/usePipelineRunVulnerabilities.spec.ts b/frontend/packages/pipelines-plugin/src/components/pipelineruns/hooks/__tests__/usePipelineRunVulnerabilities.spec.ts new file mode 100644 index 00000000000..afa77cd962d --- /dev/null +++ b/frontend/packages/pipelines-plugin/src/components/pipelineruns/hooks/__tests__/usePipelineRunVulnerabilities.spec.ts @@ -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); + }); +}); diff --git a/frontend/packages/pipelines-plugin/src/components/pipelineruns/hooks/usePipelineRunVulnerabilities.ts b/frontend/packages/pipelines-plugin/src/components/pipelineruns/hooks/usePipelineRunVulnerabilities.ts new file mode 100644 index 00000000000..33da809326e --- /dev/null +++ b/frontend/packages/pipelines-plugin/src/components/pipelineruns/hooks/usePipelineRunVulnerabilities.ts @@ -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]); diff --git a/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunHeader.tsx b/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunHeader.tsx index b90ac748393..2bb1706e56d 100644 --- a/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunHeader.tsx +++ b/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunHeader.tsx @@ -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 }, }, ]; }; diff --git a/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunList.scss b/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunList.scss new file mode 100644 index 00000000000..a9a514935fa --- /dev/null +++ b/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunList.scss @@ -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; + } + } +} diff --git a/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunList.tsx b/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunList.tsx index ce3398d05ef..d6756c0653b 100644 --- a/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunList.tsx +++ b/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunList.tsx @@ -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; }; @@ -44,6 +48,21 @@ export const PipelineRunList: React.FC = (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 /> diff --git a/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunRow.tsx b/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunRow.tsx index 1e3d08d050d..c1d18ab572f 100644 --- a/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunRow.tsx +++ b/frontend/packages/pipelines-plugin/src/components/pipelineruns/list-page/PipelineRunRow.tsx @@ -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'; @@ -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'; @@ -35,32 +40,46 @@ const PLRStatus: React.FC = ({ obj, taskRuns }) => { }; const PipelineRunRow: React.FC> = ({ obj, customData }) => { + const { t } = useTranslation(); const { operatorVersion, taskRuns } = customData; const PLRTaskRuns = getTaskRunsOfPipelineRun(taskRuns, obj?.metadata?.name); + return ( <> - + +
+ {t('pipelines-plugin~Signed')} +
+ + ) : null + } />
- + - + + + + - + - + - {pipelineRunDuration(obj)} - + {pipelineRunDuration(obj)} + ; +export const HighIcon = () => ; +export const MediumIcon = () => ; +export const LowIcon = () => ; + +type PipelineRunVulnerabilitiesProps = { + pipelineRun: PipelineRunKind; + condensed?: boolean; +}; + +const PipelineRunVulnerabilities: React.FC = ({ + pipelineRun, + condensed, +}) => { + const scanResults = usePipelineRunVulnerabilities(pipelineRun); + + return ( +
+ {scanResults?.vulnerabilities ? ( + <> +
+ + + {!condensed ? i18n.t('pipelines-plugin~Critical') : null} + + + {scanResults.vulnerabilities.critical} + +
+
+ + + {!condensed ? i18n.t('pipelines-plugin~High') : null} + + + {scanResults.vulnerabilities.high} + +
+
+ + + {!condensed ? i18n.t('pipelines-plugin~Medium') : null} + + + {scanResults.vulnerabilities.medium} + +
+
+ + + {!condensed ? i18n.t('pipelines-plugin~Low') : null} + + + {scanResults.vulnerabilities.low} + +
+ + ) : ( + '-' + )} +
+ ); +}; + +export default PipelineRunVulnerabilities; diff --git a/frontend/packages/pipelines-plugin/src/test-data/pipeline-data.ts b/frontend/packages/pipelines-plugin/src/test-data/pipeline-data.ts index 039b2cbaae9..2021997df39 100644 --- a/frontend/packages/pipelines-plugin/src/test-data/pipeline-data.ts +++ b/frontend/packages/pipelines-plugin/src/test-data/pipeline-data.ts @@ -3206,6 +3206,366 @@ export const PipeLineRunWithRepoMetadata: Record = { }, }; +export enum PipeLineRunWithVulnerabilitiesNames { + ScanOutput = 'scan-output', + MyScanOutput = 'my-scan-output', + InvalidScanOutput = 'invalid-scan-output', + MultipleScanOutput = 'multiple-scan-output', +} + +export const PipeLineRunWithVulnerabilitiesData: Record = { + [PipeLineRunWithVulnerabilitiesNames.ScanOutput]: { + kind: 'PipelineRun', + apiVersion: 'tekton.dev/v1', + metadata: { + annotations: { + 'chains.tekton.dev/signed': 'true', + 'results.tekton.dev/log': + 'jeff-project/results/2e22062d-6a05-4f62-b348-12da06fadbab/logs/3826b8ee-d80e-34ba-828d-7b8ab32e37f0', + 'results.tekton.dev/record': + 'jeff-project/results/2e22062d-6a05-4f62-b348-12da06fadbab/records/2e22062d-6a05-4f62-b348-12da06fadbab', + 'results.tekton.dev/result': 'jeff-project/results/2e22062d-6a05-4f62-b348-12da06fadbab', + }, + resourceVersion: '317768', + name: 'pipelinerun-with-scan-task', + uid: '2e22062d-6a05-4f62-b348-12da06fadbab', + creationTimestamp: '2023-11-13T12:32:19Z', + generation: 1, + namespace: 'jeff-project', + finalizers: ['chains.tekton.dev/pipelinerun'], + labels: { + 'tekton.dev/pipeline': 'pipelinerun-with-scan-task', + }, + }, + spec: { + pipelineSpec: { + results: [ + { + description: 'The common vulnerabilities and exposures (CVE) result', + name: 'SCAN_OUTPUT', + value: '$(tasks.scan-task.results.SCAN_OUTPUT)', + }, + ], + tasks: [ + { + name: 'scan-task', + taskRef: { + kind: 'Task', + name: 'scan-task', + }, + }, + ], + }, + taskRunTemplate: { + serviceAccountName: 'pipeline', + }, + }, + status: { + completionTime: '2023-11-13T12:32:26Z', + conditions: [ + { + lastTransitionTime: '2023-11-13T12:32:26Z', + message: 'Tasks Completed: 1 (Failed: 0, Cancelled 0), Skipped: 0', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + pipelineSpec: { + results: [ + { + description: 'The common vulnerabilities and exposures (CVE) result', + name: 'SCAN_OUTPUT', + value: '$(tasks.scan-task.results.SCAN_OUTPUT)', + }, + ], + tasks: [ + { + name: 'scan-task', + taskRef: { + kind: 'Task', + name: 'scan-task', + }, + }, + ], + }, + results: [ + { + name: 'SCAN_OUTPUT', + value: + '{"vulnerabilities":{\n"critical": 13,\n"high": 29,\n"medium": 32,\n"low": 3,\n"unknown": 0},\n"unpatched_vulnerabilities": {\n"critical": 0,\n"high": 1,\n"medium": 0,\n"low":1}\n}\n', + }, + ], + startTime: '2023-11-13T12:32:19Z', + }, + }, + [PipeLineRunWithVulnerabilitiesNames.MyScanOutput]: { + kind: 'PipelineRun', + apiVersion: 'tekton.dev/v1', + metadata: { + annotations: { + 'chains.tekton.dev/signed': 'true', + 'results.tekton.dev/log': + 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726/logs/cd4f186e-5c5c-331c-a7ff-6c990bb5b47b', + 'results.tekton.dev/record': + 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726/records/8c5a7c3f-3590-40b4-869b-5258d1450726', + 'results.tekton.dev/result': 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726', + }, + resourceVersion: '317738', + name: 'pipelinerun-with-my-scan-task', + uid: '8c5a7c3f-3590-40b4-869b-5258d1450726', + creationTimestamp: '2023-11-13T12:32:20Z', + generation: 1, + namespace: 'jeff-project', + finalizers: ['chains.tekton.dev/pipelinerun'], + labels: { + 'tekton.dev/pipeline': 'pipelinerun-with-scan-task-1', + }, + }, + spec: { + pipelineSpec: { + results: [ + { + description: 'The common vulnerabilities and exposures (CVE) result', + name: 'MY_SCAN_OUTPUT', + value: '$(tasks.scan-task.results.MY_SCAN_OUTPUT)', + }, + ], + tasks: [ + { + name: 'scan-task', + taskRef: { + kind: 'Task', + name: 'scan-task', + }, + }, + ], + }, + taskRunTemplate: { + serviceAccountName: 'pipeline', + }, + }, + status: { + completionTime: '2023-11-13T12:32:25Z', + conditions: [ + { + lastTransitionTime: '2023-11-13T12:32:25Z', + message: 'Tasks Completed: 1 (Failed: 0, Cancelled 0), Skipped: 0', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + pipelineSpec: { + results: [ + { + description: 'The common vulnerabilities and exposures (CVE) result', + name: 'MY_SCAN_OUTPUT', + value: '$(tasks.scan-task.results.MY_SCAN_OUTPUT)', + }, + ], + tasks: [ + { + name: 'scan-task', + taskRef: { + kind: 'Task', + name: 'scan-task', + }, + }, + ], + }, + results: [ + { + name: 'MY_SCAN_OUTPUT', + value: + '{"vulnerabilities":{\n"critical": 0,\n"high": 9,\n"medium": 2,\n"low": 13,\n"unknown": 0},\n"unpatched_vulnerabilities": {\n"critical": 0,\n"high": 1,\n"medium": 0,\n"low":1}\n}\n', + }, + ], + startTime: '2023-11-13T12:32:20Z', + }, + }, + [PipeLineRunWithVulnerabilitiesNames.InvalidScanOutput]: { + kind: 'PipelineRun', + apiVersion: 'tekton.dev/v1', + metadata: { + annotations: { + 'chains.tekton.dev/signed': 'true', + 'results.tekton.dev/log': + 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726/logs/cd4f186e-5c5c-331c-a7ff-6c990bb5b47b', + 'results.tekton.dev/record': + 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726/records/8c5a7c3f-3590-40b4-869b-5258d1450726', + 'results.tekton.dev/result': 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726', + }, + resourceVersion: '317738', + name: 'pipelinerun-with-invalid-scan-task', + uid: '8c5a7c3f-3590-40b4-869b-5258d1450729', + creationTimestamp: '2023-11-13T12:32:20Z', + generation: 1, + namespace: 'jeff-project', + finalizers: ['chains.tekton.dev/pipelinerun'], + labels: { + 'tekton.dev/pipeline': 'pipelinerun-with-scan-task-1', + }, + }, + spec: { + pipelineSpec: { + results: [ + { + description: 'The common vulnerabilities and exposures (CVE) result', + name: 'INVALID_SCAN_OUTPUTS', + value: '$(tasks.scan-task.results.INVALID_SCAN_OUTPUTS)', + }, + ], + tasks: [ + { + name: 'scan-task', + taskRef: { + kind: 'Task', + name: 'scan-task', + }, + }, + ], + }, + taskRunTemplate: { + serviceAccountName: 'pipeline', + }, + }, + status: { + completionTime: '2023-11-13T12:32:25Z', + conditions: [ + { + lastTransitionTime: '2023-11-13T12:32:25Z', + message: 'Tasks Completed: 1 (Failed: 0, Cancelled 0), Skipped: 0', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + pipelineSpec: { + results: [ + { + description: 'The common vulnerabilities and exposures (CVE) result', + name: 'INVALID_SCAN_OUTPUTS', + value: '$(tasks.scan-task.results.INVALID_SCAN_OUTPUTS)', + }, + ], + tasks: [ + { + name: 'scan-task', + taskRef: { + kind: 'Task', + name: 'scan-task', + }, + }, + ], + }, + results: [ + { + name: 'INVALID_SCAN_OUTPUTS', + value: + '{"vulnerabilities":{\n"critical": 0,\n"high": 9,\n"medium": 2,\n"low": 13,\n"unknown": 0},\n"unpatched_vulnerabilities": {\n"critical": 0,\n"high": 1,\n"medium": 0,\n"low":1}\n}\n', + }, + ], + startTime: '2023-11-13T12:32:20Z', + }, + }, + [PipeLineRunWithVulnerabilitiesNames.MultipleScanOutput]: { + kind: 'PipelineRun', + apiVersion: 'tekton.dev/v1', + metadata: { + annotations: { + 'chains.tekton.dev/signed': 'true', + 'results.tekton.dev/log': + 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726/logs/cd4f186e-5c5c-331c-a7ff-6c990bb5b47b', + 'results.tekton.dev/record': + 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726/records/8c5a7c3f-3590-40b4-869b-5258d1450726', + 'results.tekton.dev/result': 'jeff-project/results/8c5a7c3f-3590-40b4-869b-5258d1450726', + }, + resourceVersion: '317738', + name: 'pipelinerun-with-invalid-scan-task', + uid: '8c5a7c3f-3590-40b4-869b-5258d1450729', + creationTimestamp: '2023-11-13T12:32:20Z', + generation: 1, + namespace: 'jeff-project', + finalizers: ['chains.tekton.dev/pipelinerun'], + labels: { + 'tekton.dev/pipeline': 'pipelinerun-with-scan-task-1', + }, + }, + spec: { + pipelineSpec: { + results: [ + { + description: 'The common vulnerabilities and exposures (CVE) result', + name: 'INVALID_SCAN_OUTPUTS', + value: '$(tasks.scan-task.results.INVALID_SCAN_OUTPUTS)', + }, + ], + tasks: [ + { + name: 'scan-task', + taskRef: { + kind: 'Task', + name: 'scan-task', + }, + }, + ], + }, + taskRunTemplate: { + serviceAccountName: 'pipeline', + }, + }, + status: { + completionTime: '2023-11-13T12:32:25Z', + conditions: [ + { + lastTransitionTime: '2023-11-13T12:32:25Z', + message: 'Tasks Completed: 1 (Failed: 0, Cancelled 0), Skipped: 0', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + pipelineSpec: { + results: [ + { + description: 'The common vulnerabilities and exposures (CVE) result', + name: 'INVALID_SCAN_OUTPUTS', + value: '$(tasks.scan-task.results.INVALID_SCAN_OUTPUTS)', + }, + ], + tasks: [ + { + name: 'scan-task', + taskRef: { + kind: 'Task', + name: 'scan-task', + }, + }, + ], + }, + results: [ + { + name: 'SCAN_OUTPUT', + value: + '{"vulnerabilities":{\n"critical": 13,\n"high": 29,\n"medium": 32,\n"low": 3,\n"unknown": 0},\n"unpatched_vulnerabilities": {\n"critical": 0,\n"high": 1,\n"medium": 0,\n"low":1}\n}\n', + }, + { + name: 'MY_SCAN_OUTPUT', + value: + '{"vulnerabilities":{\n"critical": 0,\n"high": 9,\n"medium": 2,\n"low": 13,\n"unknown": 0},\n"unpatched_vulnerabilities": {\n"critical": 0,\n"high": 1,\n"medium": 0,\n"low":1}\n}\n', + }, + { + name: 'INVALID_SCAN_OUTPUTS', + value: + '{"vulnerabilities":{\n"critical": 0,\n"high": 9,\n"medium": 2,\n"low": 13,\n"unknown": 0},\n"unpatched_vulnerabilities": {\n"critical": 0,\n"high": 1,\n"medium": 0,\n"low":1}\n}\n', + }, + ], + startTime: '2023-11-13T12:32:20Z', + }, + }, +}; + export const mockRepositories: RepositoryKind[] = [ { apiVersion: 'pipelinesascode.tekton.dev/v1alpha1', diff --git a/frontend/packages/pipelines-plugin/src/utils/pipeline-actions.tsx b/frontend/packages/pipelines-plugin/src/utils/pipeline-actions.tsx index df30b0e8863..c3e9aeb2a60 100644 --- a/frontend/packages/pipelines-plugin/src/utils/pipeline-actions.tsx +++ b/frontend/packages/pipelines-plugin/src/utils/pipeline-actions.tsx @@ -17,9 +17,10 @@ import { } from '../components/pipelines/modals'; import { getPipelineRunData } from '../components/pipelines/modals/common/utils'; import { getTaskRunsOfPipelineRun } from '../components/taskruns/useTaskRuns'; -import { EventListenerModel, PipelineModel, PipelineRunModel } from '../models'; +import { EventListenerModel, PipelineModel, PipelineRunModel, TaskRunModel } from '../models'; import { PipelineKind, PipelineRunKind, TaskRunKind } from '../types'; import { shouldHidePipelineRunStop, shouldHidePipelineRunCancel } from './pipeline-augment'; +import { getSbomTaskRun } from './pipeline-utils'; export const handlePipelineRunSubmit = (pipelineRun: PipelineRunKind) => { history.push( @@ -260,6 +261,27 @@ export const cancelPipelineRunFinally: KebabAction = ( }; }; +export const viewPipelineRunSBOM: KebabAction = ( + kind: K8sKind, + pipelineRun: PipelineRunKind, + taskRuns: TaskRunKind[], +) => { + const PLRTasks = getTaskRunsOfPipelineRun(taskRuns, pipelineRun?.metadata?.name); + const sbomTaskRun = getSbomTaskRun(PLRTasks); + + return { + labelKey: 'pipelines-plugin~View SBOM', + callback: () => { + history.push( + `/k8s/ns/${sbomTaskRun.metadata.namespace}/${referenceForModel(TaskRunModel)}/${ + sbomTaskRun.metadata.name + }/logs`, + ); + }, + hidden: !sbomTaskRun, + }; +}; + const addTrigger: KebabAction = (kind: K8sKind, pipeline: PipelineKind) => ({ // t('pipelines-plugin~Add Trigger') labelKey: 'pipelines-plugin~Add Trigger', @@ -319,6 +341,7 @@ export const getPipelineRunKebabActions = ( ? (model, pipelineRun) => rerunPipelineRunAndRedirect(model, pipelineRun) : (model, pipelineRun) => reRunPipelineRun(model, pipelineRun), (model, pipelineRun) => stopPipelineRun(model, pipelineRun, operatorVersion, taskRuns), + (model, pipelineRun) => viewPipelineRunSBOM(model, pipelineRun, taskRuns), (model, pipelineRun) => cancelPipelineRunFinally(model, pipelineRun, taskRuns), Kebab.factory.Delete, ]; diff --git a/frontend/public/components/utils/resource-link.tsx b/frontend/public/components/utils/resource-link.tsx index 031f95ccc79..92925959dac 100644 --- a/frontend/public/components/utils/resource-link.tsx +++ b/frontend/public/components/utils/resource-link.tsx @@ -76,6 +76,7 @@ export const ResourceLink: React.FC = ({ groupVersionKind, linkTo = true, name, + nameSuffix, namespace, hideIcon, title, @@ -108,6 +109,7 @@ export const ResourceLink: React.FC = ({ onClick={onClick} > {value} + {nameSuffix} ) : ( = ({ data-test={dataTest ?? value} > {value} + {nameSuffix} )} {children}