Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bifurcate image cards in imageScan codeScan and manifestScan #500

Merged
merged 12 commits into from
Jan 13, 2025
Merged
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devtron-labs/devtron-fe-common-lib",
"version": "1.4.0-patch-1",
"version": "1.4.1",
"description": "Supporting common component library",
"type": "module",
"main": "dist/index.js",
Expand Down
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-shield-check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-shield-warning-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 32 additions & 12 deletions src/Common/SegmentedBarChart/SegmentedBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const SegmentedBarChart: React.FC<SegmentedBarChartProps> = ({
countClassName,
labelClassName,
isProportional,
swapLegendAndBar = false,
}) => {
const total = entities.reduce((sum, entity) => entity.value + sum, 0)
const filteredEntities = entities.filter((entity) => entity.value)
Expand Down Expand Up @@ -70,20 +71,39 @@ const SegmentedBarChart: React.FC<SegmentedBarChartProps> = ({
return null
}

const renderLegend = () => (
<div className={`flexbox flex-wrap dc__row-gap-4 ${isProportional ? 'dc__gap-24' : 'dc__gap-16'}`}>
{renderContent()}
</div>
)

const renderBar = () => (
<div className="flexbox dc__gap-2">
{filteredEntities?.map((entity, index, map) => (
<div
key={entity.label}
className={`h-8 ${index === 0 ? 'dc__left-radius-4' : ''} ${
index === map.length - 1 ? 'dc__right-radius-4' : ''
}`}
style={{ backgroundColor: entity.color, width: calcSegmentWidth(entity) }}
/>
))}
</div>
)

return (
<div className={`flexbox-col w-100 dc__gap-12 ${rootClassName}`}>
<div className={`flexbox ${isProportional ? 'dc__gap-24' : 'dc__gap-16'}`}>{renderContent()}</div>
<div className="flexbox dc__gap-2">
{filteredEntities?.map((entity, index, map) => (
<div
key={entity.label}
className={`h-8 ${index === 0 ? 'dc__left-radius-4' : ''} ${
index === map.length - 1 ? 'dc__right-radius-4' : ''
}`}
style={{ backgroundColor: entity.color, width: calcSegmentWidth(entity) }}
/>
))}
</div>
{swapLegendAndBar ? (
<>
{renderBar()}
{renderLegend()}
</>
) : (
<>
{renderLegend()}
{renderBar()}
</>
)}
</div>
)
}
Expand Down
1 change: 1 addition & 0 deletions src/Common/SegmentedBarChart/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export interface SegmentedBarChartProps {
countClassName?: string
labelClassName?: string
isProportional?: boolean
swapLegendAndBar?: boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { SegmentedBarChart } from '@Common/SegmentedBarChart'
import { ReactComponent as ICShieldWarning } from '@Icons/ic-shield-warning-outline.svg'
import { ReactComponent as ICShieldSecure } from '@Icons/ic-shield-check.svg'
import { ReactComponent as ICArrowRight } from '@Icons/ic-caret-down-small.svg'
import { SecurityCardProps } from './types'
import { SUB_CATEGORIES } from '../SecurityModal/types'
import { SEVERITIES } from '../SecurityModal/constants'
import './securityCard.scss'
import { getTotalSeverities } from '../utils'
import { SECURITY_CONFIG } from '../constants'

const SecurityCard = ({ category, subCategory, severityCount = {}, handleCardClick }: SecurityCardProps) => {
const totalCount = getTotalSeverities(severityCount)

const hasThreats: boolean = !!totalCount

const entities = Object.entries(SEVERITIES)
.map(([key, severity]) => ({
...severity,
value: severityCount[key],
}))
.filter((entity) => !!entity.value)

const getTitleSubtitle = (): { title: string; subtitle?: string } => {
switch (subCategory) {
case SUB_CATEGORIES.EXPOSED_SECRETS:
return hasThreats
? { title: `${totalCount} exposed secrets` }
: {
title: 'No exposed secrets',
subtitle: 'No exposed secrets like passwords, api keys, and tokens found',
}
case SUB_CATEGORIES.LICENSE:
return hasThreats
? { title: `${totalCount} license risks` }
: { title: 'No license risks', subtitle: 'No license risks or compliance issues found' }
case SUB_CATEGORIES.MISCONFIGURATIONS:
return hasThreats
? { title: `${totalCount} misconfigurations` }
: { title: 'No misconfiguration', subtitle: 'No configuration issues detected in scanned files' }
default:
return hasThreats
? { title: `${totalCount} vulnerabilities` }
: { title: 'No vulnerabilities', subtitle: 'No vulnerabilities or potential threats found' }
}
}

const { title, subtitle } = getTitleSubtitle()

const onKeyDown = (event) => {
if (event.key === 'Enter') {
handleCardClick()
}
}

return (
<div
className={`w-100 bcn-0 p-20 flexbox-col dc__gap-16 br-8 dc__border security-card security-card${hasThreats ? '--threat' : '--secure'}`}
role="button"
AbhishekA1509 marked this conversation as resolved.
Show resolved Hide resolved
tabIndex={0}
onClick={handleCardClick}
onKeyDown={onKeyDown}
>
<div className="flexbox dc__content-space">
<div className="flexbox-col">
<span className="fs-12 fw-4 lh-1-5 cn-7">{SECURITY_CONFIG[category].label}</span>
<div className="fs-15 fw-6 lh-1-5 cn-9 flex">
<span className="security-card-title">{title}</span>
<ICArrowRight className="icon-dim-20 dc__flip-270 scb-5 arrow-right" />
</div>
</div>
{hasThreats ? (
<ICShieldWarning className="icon-dim-24 scr-5 dc__no-shrink" />
) : (
<ICShieldSecure className="icon-dim-24 scg-5 dc__no-shrink" />
)}
</div>
<div className="flexbox-col dc__gap-12">
{hasThreats || severityCount.success ? (
<SegmentedBarChart
entities={entities}
labelClassName="fs-13 fw-4 lh-20 cn-9"
countClassName="fs-13 fw-6 lh-20 cn-7"
swapLegendAndBar
/>
) : (
<div className="bcn-1 br-4 h-8" />
)}
{subtitle && <span className="cn-9 fs-13 lh-20">{subtitle}</span>}
</div>
</div>
)
}

export default SecurityCard
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ScannedByToolModal } from '@Shared/Components/ScannedByToolModal'
import { EMPTY_STATE_STATUS, SCAN_TOOL_ID_CLAIR, SCAN_TOOL_ID_TRIVY } from '@Shared/constants'
import { useState } from 'react'
import { GenericEmptyState } from '@Common/index'
import { ReactComponent as NoVulnerability } from '@Icons/ic-vulnerability-not-found.svg'
import SecurityCard from './SecurityCard'
import { CATEGORIES, SecurityModalStateType, SUB_CATEGORIES } from '../SecurityModal/types'
import { SecurityCardProps, SecurityDetailsCardsProps } from './types'
import { SecurityModal } from '../SecurityModal'
import { DEFAULT_SECURITY_MODAL_IMAGE_STATE } from '../SecurityModal/constants'
import { ScanCategories, ScanSubCategories } from '../types'
import { getSecurityConfig, getCompiledSecurityThreats, getTotalSeverities } from '../utils'
import './securityCard.scss'

const SecurityDetailsCards = ({ scanResult, Sidebar }: SecurityDetailsCardsProps) => {
const [showSecurityModal, setShowSecurityModal] = useState<boolean>(false)
const [modalState, setModalState] = useState<SecurityModalStateType>(DEFAULT_SECURITY_MODAL_IMAGE_STATE)
const { imageScan, codeScan, kubernetesManifest } = scanResult

const scanThreats = getCompiledSecurityThreats(scanResult)
const threatCount = getTotalSeverities(scanThreats)

if (!threatCount) {
return (
<GenericEmptyState
SvgImage={NoVulnerability}
title={EMPTY_STATE_STATUS.CI_DEATILS_NO_VULNERABILITY_FOUND.TITLE}
subTitle={EMPTY_STATE_STATUS.CI_DEATILS_NO_VULNERABILITY_FOUND.SUBTITLE}
/>
)
}

const SECURITY_CONFIG = getSecurityConfig({
imageScan: !!imageScan,
codeScan: !!codeScan,
kubernetesManifest: !!kubernetesManifest,
})

const getScanToolId = (category: string) => {
switch (category) {
case CATEGORIES.CODE_SCAN:
return codeScan?.scanToolName === 'TRIVY' ? SCAN_TOOL_ID_TRIVY : SCAN_TOOL_ID_CLAIR
case CATEGORIES.KUBERNETES_MANIFEST:
return kubernetesManifest?.scanToolName === 'TRIVY' ? SCAN_TOOL_ID_TRIVY : SCAN_TOOL_ID_CLAIR
case CATEGORIES.IMAGE_SCAN:
return imageScan?.vulnerability?.list?.[0].scanToolName === 'TRIVY'
? SCAN_TOOL_ID_TRIVY
: SCAN_TOOL_ID_CLAIR
default:
return SCAN_TOOL_ID_TRIVY
}
}

const handleOpenModal = (
category: SecurityCardProps['category'],
subCategory: SecurityCardProps['subCategory'],
) => {
setShowSecurityModal(true)
setModalState({
category,
subCategory,
detailViewData: null,
})
}

const handleCardClick =
(category: SecurityCardProps['category'], subCategory: SecurityCardProps['subCategory']) => () =>
handleOpenModal(category, subCategory)

const handleModalClose = () => {
setShowSecurityModal(false)
}

return (
<>
<div className="flexbox-col dc__gap-20 mw-600 dc__mxw-1200">
{Object.keys(SECURITY_CONFIG).map((category: ScanCategories) => (
<div className="flexbox-col dc__gap-12" key={category}>
<div className="flexbox dc__content-space pb-8 dc__border-bottom-n1">
<span className="fs-13 fw-6 lh-1-5 cn-9">{SECURITY_CONFIG[category].label}</span>
<ScannedByToolModal scanToolId={getScanToolId(category)} />
</div>
<div className="dc__grid security-cards">
{SECURITY_CONFIG[category].subCategories.map((subCategory: ScanSubCategories) => {
const severityCount =
subCategory === SUB_CATEGORIES.MISCONFIGURATIONS
? scanResult[category][subCategory]?.misConfSummary?.status
: scanResult[category][subCategory]?.summary?.severities

return (
<SecurityCard
category={category}
subCategory={subCategory}
severityCount={severityCount}
handleCardClick={handleCardClick(category, subCategory)}
/>
)
})}
</div>
</div>
))}
</div>
{showSecurityModal && (
<SecurityModal
isLoading={false}
error={null}
responseData={scanResult}
handleModalClose={handleModalClose}
Sidebar={Sidebar}
defaultState={modalState}
/>
)}
</>
)
}

export default SecurityDetailsCards
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as SecurityCard } from './SecurityCard'
export { default as SecurityDetailsCards } from './SecurityDetailsCards'
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.security-card {
&--threat {
background: radial-gradient(25.91% 100% at 100% 0%, var(--R100) 0%, var(--N0) 100%);
}
&--secure {
AbhishekA1509 marked this conversation as resolved.
Show resolved Hide resolved
background: radial-gradient(25.91% 100% at 100% 0%, var(--G100) 0%, var(--N0) 100%);
}

.arrow-right {
visibility: hidden;
}

&:hover {
.arrow-right {
visibility: visible;
}

.security-card-title {
color: var(--B500);
}
}
}

.security-cards {
grid-template-columns: 1fr 1fr;
grid-row-gap: 12px;
grid-column-gap: 12px;
}
13 changes: 13 additions & 0 deletions src/Shared/Components/Security/SecurityDetailsCards/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ScanResultDTO, SecurityModalPropsType, SeveritiesDTO } from '../SecurityModal/types'
import { ScanCategories, ScanSubCategories } from '../types'

export interface SecurityCardProps {
category: ScanCategories
subCategory: ScanSubCategories
severityCount: Partial<Record<SeveritiesDTO, number>>
handleCardClick: () => void
}

export interface SecurityDetailsCardsProps extends Pick<SecurityModalPropsType, 'Sidebar'> {
scanResult: ScanResultDTO
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
stopPropagation,
VisibleModal2,
} from '@Common/index'
import { ReactComponent as ICClose } from '@Icons/ic-cross.svg'
import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
import { ReactComponent as ICBack } from '@Icons/ic-caret-left-small.svg'
import { Button, ButtonStyleType, ButtonVariantType } from '@Shared/Components/Button'
import { ComponentSizeType } from '@Shared/constants'
Expand Down Expand Up @@ -40,6 +40,7 @@ const SecurityModal: React.FC<SecurityModalPropsType> = ({
error,
responseData,
hidePolicy = false,
defaultState,
}) => {
const data = responseData ?? null

Expand All @@ -50,7 +51,9 @@ const SecurityModal: React.FC<SecurityModalPropsType> = ({
kubernetesManifest: !!data?.kubernetesManifest,
}

const [state, setState] = useState<SecurityModalStateType>(getDefaultSecurityModalState(categoriesConfig))
const [state, setState] = useState<SecurityModalStateType>(
defaultState ?? getDefaultSecurityModalState(categoriesConfig),
)

const setDetailViewData = (detailViewData: DetailViewDataType) => {
setState((prevState) => ({
Expand All @@ -76,7 +79,7 @@ const SecurityModal: React.FC<SecurityModalPropsType> = ({
onClick={handleModalClose}
showAriaLabelInTippy={false}
size={ComponentSizeType.xs}
style={ButtonStyleType.neutral}
style={ButtonStyleType.negativeGrey}
variant={ButtonVariantType.borderLess}
/>
</div>
Expand Down
Loading
Loading