From 8b53e5b92a267a93b5356fafee3f5198b5fd5eb7 Mon Sep 17 00:00:00 2001 From: Shivanand Sonnad Date: Mon, 7 Oct 2024 12:03:26 +0000 Subject: [PATCH] feat: [AH-341]: support download SLSA report from artifact registry (#2771) * feat: [AH-341]: update spelling mistake * Merge branch 'feat-AH-341' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into feat-AH-341 * feat: [AH-341]: update api schema to fix type errors * feat: [AH-341]: support download SLSA report from artifact registry * feat: [AH-341]: update api schema to fix type errors * feat: [AH-341]: support download SLSA report from artifact registry --- web/package.json | 3 +- web/src/ar/app/App.tsx | 17 ++++-- .../OverviewCards/OverviewCards.tsx | 1 + .../SupplyChainCard.module.scss | 5 ++ .../SupplyChainCard.module.scss.d.ts | 1 + .../SupplyChainCard/SupplyChainCard.tsx | 24 +++++--- .../hooks/useDownloadSLSAProvenance.ts | 56 +++++++++++++++++++ .../version-details/strings/strings.en.yaml | 1 + web/src/ar/strings/types.ts | 1 + web/src/ar/utils/downloadRawFile.ts | 34 +++++++++++ web/yarn.lock | 13 +++-- 11 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 web/src/ar/pages/version-details/hooks/useDownloadSLSAProvenance.ts create mode 100644 web/src/ar/utils/downloadRawFile.ts diff --git a/web/package.json b/web/package.json index 1156f7513..c2d6781d9 100644 --- a/web/package.json +++ b/web/package.json @@ -51,7 +51,8 @@ "@codemirror/view": "^6.9.6", "@harnessio/design-system": "^2.1.1", "@harnessio/icons": "^2.1.7", - "@harnessio/react-har-service-client": "^0.0.22", + "@harnessio/react-har-service-client": "^0.0.23", + "@harnessio/react-ssca-manager-client": "^0.65.0", "@harnessio/uicore": "^4.1.2", "@tanstack/react-query": "4.20.4", "@types/dompurify": "^3.0.2", diff --git a/web/src/ar/app/App.tsx b/web/src/ar/app/App.tsx index ca2a5169c..49ec7b96b 100644 --- a/web/src/ar/app/App.tsx +++ b/web/src/ar/app/App.tsx @@ -14,9 +14,10 @@ * limitations under the License. */ -import React, { PropsWithChildren, Suspense, useEffect, useRef } from 'react' +import React, { PropsWithChildren, Suspense, useEffect, useMemo, useRef } from 'react' import { Page } from '@harnessio/uicore' import { HARServiceAPIClient } from '@harnessio/react-har-service-client' +import { SSCAManagerAPIClient } from '@harnessio/react-ssca-manager-client' import { QueryClientProvider } from '@tanstack/react-query' import { StringsContextProvider } from '@ar/frameworks/strings/StringsContextProvider' @@ -57,10 +58,10 @@ export default function ChildApp(props: PropsWithChildren): React.R } = props const { ModalProvider } = customComponents - const appStoreData = React.useContext(parentContextObj.appStoreContext) - useRef( - new HARServiceAPIClient({ + + const apiClientOptions = useMemo( + () => ({ responseInterceptor: (response: Response): Response => { if (!response.ok && response.status === 401) { on401() @@ -71,7 +72,7 @@ export default function ChildApp(props: PropsWithChildren): React.R urlInterceptor: (url: string) => { return customUtils.getApiBaseUrl(url) }, - requestInterceptor(request) { + requestInterceptor(request: Request) { request.headers.delete('Authorization') // add custom headers if available const customHeader = customUtils.getCustomHeaders() @@ -80,9 +81,13 @@ export default function ChildApp(props: PropsWithChildren): React.R }) return request } - }) + }), + [] ) + useRef(new HARServiceAPIClient(apiClientOptions)) + useRef(new SSCAManagerAPIClient(apiClientOptions)) + useEffect( () => () => { if (typeof appStoreData.updateAppStore === 'function' && parent !== Parent.Enterprise) { diff --git a/web/src/ar/pages/version-details/DockerVersion/components/OverviewCards/OverviewCards.tsx b/web/src/ar/pages/version-details/DockerVersion/components/OverviewCards/OverviewCards.tsx index 5c96d10c7..b82a98b9d 100644 --- a/web/src/ar/pages/version-details/DockerVersion/components/OverviewCards/OverviewCards.tsx +++ b/web/src/ar/pages/version-details/DockerVersion/components/OverviewCards/OverviewCards.tsx @@ -100,6 +100,7 @@ export default function DockerVersionOverviewCards() { artifactId: responseData.sbomDetails?.artifactId }) }} + provenanceId={defaultTo(responseData.slsaDetails?.provenanceId, '')} className={css.card} totalComponents={defaultTo(responseData.sbomDetails?.componentsCount, 0)} allowListCount={defaultTo(responseData.sbomDetails?.allowListViolations, 0)} diff --git a/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.module.scss b/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.module.scss index fc3472bc7..38d5981b7 100644 --- a/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.module.scss +++ b/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.module.scss @@ -25,3 +25,8 @@ } } } + +.downloadSlsaBtn { + --font-size: 12px !important; + --padding: 0 !important; +} diff --git a/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.module.scss.d.ts b/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.module.scss.d.ts index d31fcc725..25d228713 100644 --- a/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.module.scss.d.ts +++ b/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.module.scss.d.ts @@ -18,4 +18,5 @@ // This is an auto-generated file export declare const column: string export declare const container: string +export declare const downloadSlsaBtn: string export declare const primaryColumn: string diff --git a/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.tsx b/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.tsx index f6c5a625d..96cfa0bfe 100644 --- a/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.tsx +++ b/web/src/ar/pages/version-details/components/SupplyChainCard/SupplyChainCard.tsx @@ -16,10 +16,12 @@ import React from 'react' import classNames from 'classnames' -import { Card, Layout, Text } from '@harnessio/uicore' +import { Button, ButtonSize, ButtonVariation, Card, Layout, Text } from '@harnessio/uicore' import { Color, FontVariation } from '@harnessio/design-system' +import { killEvent } from '@ar/common/utils' import { useStrings } from '@ar/frameworks/strings' +import useDownloadSLSAProvenance from '@ar/pages/version-details/hooks/useDownloadSLSAProvenance' import SecurityItem from '../SecurityTestsCard/SecurityItem' import { SecurityTestSatus } from '../SecurityTestsCard/types' @@ -33,13 +35,16 @@ interface SupplyChainCardProps { allowListCount: number denyListCount: number sbomScore: string | number + provenanceId: string onClick?: () => void } export default function SupplyChainCard(props: SupplyChainCardProps) { - const { title, totalComponents, allowListCount, denyListCount, className, sbomScore, onClick } = props + const { title, totalComponents, allowListCount, denyListCount, className, sbomScore, onClick, provenanceId } = props const { getString } = useStrings() + const { download, loading } = useDownloadSLSAProvenance() + return ( @@ -59,13 +64,18 @@ export default function SupplyChainCard(props: SupplyChainCardProps) { value={sbomScore} status={SecurityTestSatus.Green} /> - + variation={ButtonVariation.LINK} + loading={loading} + onClick={evt => { + killEvent(evt) + download(provenanceId) + }}> {getString('versionDetails.cards.supplyChain.slsaProvenance')} - + diff --git a/web/src/ar/pages/version-details/hooks/useDownloadSLSAProvenance.ts b/web/src/ar/pages/version-details/hooks/useDownloadSLSAProvenance.ts new file mode 100644 index 000000000..12895d467 --- /dev/null +++ b/web/src/ar/pages/version-details/hooks/useDownloadSLSAProvenance.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Harness, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState } from 'react' +import { defaultTo } from 'lodash-es' +import { useToaster } from '@harnessio/uicore' +import { getProvenance } from '@harnessio/react-ssca-manager-client' + +import { useAppStore } from '@ar/hooks' +import { useStrings } from '@ar/frameworks/strings' +import { downloadRawFile } from '@ar/utils/downloadRawFile' + +export default function useDownloadSLSAProvenance() { + const [loading, setLoading] = useState(false) + const { scope } = useAppStore() + const { showError } = useToaster() + const { getString } = useStrings() + + const download = async (provenanceId: string) => { + setLoading(true) + return getProvenance({ + org: defaultTo(scope.orgIdentifier, ''), + project: defaultTo(scope.projectIdentifier, ''), + provenance: provenanceId + }) + .then(data => { + const content = defaultTo(data.content.provenance, '') + if (!content) { + throw new Error(getString('versionDetails.cards.supplyChain.provenanceDataNotAvailable')) + } + return downloadRawFile(content, `provenance_${provenanceId}.json`) + }) + .catch((err: Error) => { + showError(defaultTo(err?.message, err)) + return false + }) + .finally(() => { + setLoading(false) + }) + } + + return { download, loading } +} diff --git a/web/src/ar/pages/version-details/strings/strings.en.yaml b/web/src/ar/pages/version-details/strings/strings.en.yaml index 6ef420328..a9c044843 100644 --- a/web/src/ar/pages/version-details/strings/strings.en.yaml +++ b/web/src/ar/pages/version-details/strings/strings.en.yaml @@ -25,6 +25,7 @@ cards: totalComponents: Total Dependencies sbomScore: SBOM Score slsaProvenance: SLSA Provenance + provenanceDataNotAvailable: Provenance data is not available allowList: Allow List denyListViolation: Deny List Violation container: diff --git a/web/src/ar/strings/types.ts b/web/src/ar/strings/types.ts index 24db65685..715957262 100644 --- a/web/src/ar/strings/types.ts +++ b/web/src/ar/strings/types.ts @@ -161,6 +161,7 @@ export interface StringsMap { 'versionDetails.cards.securityTests.totalCount': string 'versionDetails.cards.supplyChain.allowList': string 'versionDetails.cards.supplyChain.denyListViolation': string + 'versionDetails.cards.supplyChain.provenanceDataNotAvailable': string 'versionDetails.cards.supplyChain.sbomScore': string 'versionDetails.cards.supplyChain.slsaProvenance': string 'versionDetails.cards.supplyChain.title': string diff --git a/web/src/ar/utils/downloadRawFile.ts b/web/src/ar/utils/downloadRawFile.ts new file mode 100644 index 000000000..aea522589 --- /dev/null +++ b/web/src/ar/utils/downloadRawFile.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Harness, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const downloadRawFile = (content: string, filename: string, fileType = 'text/json') => { + return new Promise((resolve, reject) => { + try { + const url = URL.createObjectURL(new Blob([content], { type: fileType })) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + setTimeout(() => { + URL.revokeObjectURL(url) + a.remove() + }, 150) + resolve() + } catch (err) { + reject(err) + } + }) +} diff --git a/web/yarn.lock b/web/yarn.lock index a8aa00bd6..db5f24b9a 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1654,10 +1654,15 @@ resolved "https://registry.yarnpkg.com/@harnessio/icons/-/icons-2.1.7.tgz#21c37bad4291e1ed4ea730f6ed1f7612934e8b3f" integrity sha512-+vugtcJR47pQZj2IMYRt7XSNpjW/m9iSeLjQMG2Hja3Hq9J+zfDsXh2aRor3ICrLSrkQtRWpBbWn+WmsoN3lVg== -"@harnessio/react-har-service-client@^0.0.22": - version "0.0.22" - resolved "https://registry.yarnpkg.com/@harnessio/react-har-service-client/-/react-har-service-client-0.0.22.tgz#f946ef4378fecc5ecbffa46ea664446b93b01f79" - integrity sha512-MzTcA3xcaQbuYVDe9v5gPaHSCUs+7gpLt4F5/Bh26hbhzPSpGK2nxqh37sxIq3fVNkpGWXpScrnCxV1FDGNHvw== +"@harnessio/react-har-service-client@^0.0.23": + version "0.0.23" + resolved "https://registry.yarnpkg.com/@harnessio/react-har-service-client/-/react-har-service-client-0.0.23.tgz#ea4f5705155b9d7a87a475f5a45adb9d72c2d8ab" + integrity sha512-fXcW3WylHLCpPf1Ce/z1GoKcy/1jPk7tzlXMUzqTnooFlnhpOPV5fXOpVyGP8i8mmKIW17vsLLedgf+LgRhD3w== + +"@harnessio/react-ssca-manager-client@^0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@harnessio/react-ssca-manager-client/-/react-ssca-manager-client-0.65.0.tgz#8088869e282c5268bf1fefb9715652e0fc1a8940" + integrity sha512-sNLDf1lyBfdzD9TqlrsMib61YffrEpb5WJwhRKuPGKD9Po0peAqE6NGLHVFbyra4tRGvvg8gnNpzzp4hMBYeCQ== "@harnessio/uicore@^4.1.2": version "4.1.2"