Skip to content

Commit

Permalink
Merge pull request #500 from devtron-labs/feat/scanning-breakdown
Browse files Browse the repository at this point in the history
feat: bifurcate image cards in imageScan codeScan and manifestScan
  • Loading branch information
arunjaindev authored Jan 13, 2025
2 parents b63105d + b813db8 commit 1fda105
Show file tree
Hide file tree
Showing 24 changed files with 441 additions and 166 deletions.
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"
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
2 changes: 2 additions & 0 deletions src/Shared/Components/Security/SecurityDetailsCards/index.tsx
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 {
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

0 comments on commit 1fda105

Please sign in to comment.