diff --git a/assets/design-system/src/components/table/Table.tsx b/assets/design-system/src/components/table/Table.tsx index 86ff05c8c1..1115873ab5 100644 --- a/assets/design-system/src/components/table/Table.tsx +++ b/assets/design-system/src/components/table/Table.tsx @@ -129,6 +129,8 @@ function Table({ string | null >(null) + useEffect(() => setFixedGridTemplateColumns(null), [columns.length]) + const { rows: tableRows } = table.getRowModel() const getItemKey = useCallback< Parameters[0]['getItemKey'] diff --git a/assets/src/components/ai/agent-runs/AgentRunFixButton.tsx b/assets/src/components/ai/agent-runs/AgentRunFixButton.tsx deleted file mode 100644 index c356c8c128..0000000000 --- a/assets/src/components/ai/agent-runs/AgentRunFixButton.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { - AiSparkleFilledIcon, - ArrowTopRightIcon, - Button, - ButtonProps, - Card, - CloseIcon, - DiscoverIcon, - Flex, - FormField, - IconFrame, - Markdown, - SelectButton, -} from '@pluralsh/design-system' -import { AgentRunInfoCard } from 'components/ai/agent-runs/AgentRunInfoDisplays' -import { useOutsideClick } from 'components/hooks/useOutsideClick' -import { SimplePopupMenu } from 'components/layout/HeaderPopupMenu' -import { GqlError } from 'components/utils/Alert' -import { EditableDiv } from 'components/utils/EditableDiv' -import { FillLevelDiv } from 'components/utils/FillLevelDiv' -import { StretchedFlex } from 'components/utils/StretchedFlex' -import { StackedText } from 'components/utils/table/StackedText' -import { InlineLink } from 'components/utils/typography/InlineLink' -import { - AgentRunFragment, - AgentRunMode, - useCreateAgentRunMutation, -} from 'generated/graphql' -import { useRef, useState } from 'react' -import { Link } from 'react-router-dom' -import { getAgentRunAbsPath } from 'routes/aiRoutesConsts' -import { AI_SETTINGS_AGENT_RUNTIMES_ABS_PATH } from 'routes/settingsRoutesConst' -import styled, { useTheme } from 'styled-components' -import { AIAgentRuntimesSelector } from './AIAgentRuntimesSelector' -import { AgentRunRepoSelector } from './AgentRunRepoSelector' - -export function AgentRunFixButton({ - headerTitle, - initialRepo, - initialPrompt, - ...props -}: { - headerTitle: string - initialPrompt: string - initialRepo?: Nullable -} & ButtonProps) { - const { colors } = useTheme() - const [dropdownOpen, setDropdownOpen] = useState(false) - const [prompt, setPrompt] = useState(initialPrompt) - - const menuBtnRef = useRef(null) - useOutsideClick(menuBtnRef, () => setDropdownOpen(false)) - - const [agentRun, setAgentRun] = useState(null) - - return ( -
- - - ) : ( - - )} - -
- ) -} - -function AgentRunForm({ - initialRepo, - prompt, - setPrompt, - setAgentRun, -}: { - initialRepo?: Nullable - prompt: string - setPrompt: (prompt: string) => void - setAgentRun: (agentRun: AgentRunFragment) => void -}) { - const [runtimeId, setRuntimeId] = useState('') - const [repository, setRepository] = useState(initialRepo ?? '') - - const [mutation, { loading, error }] = useCreateAgentRunMutation({ - variables: { - runtimeId, - attributes: { prompt, mode: AgentRunMode.Write, repository }, - }, - onCompleted: ({ createAgentRun }) => - createAgentRun && setAgentRun(createAgentRun), - }) - - const canSubmit = !!runtimeId && !!repository && !!prompt && !loading - - return ( - <> - {error && } - - Runtime settings - - } - > - - - runtimeId && setRuntimeId(runtimeId) - } - outerStyles={{ width: '100%' }} - /> - - - - - repository && setRepository(repository) - } - selectedRuntimeId={runtimeId} - triggerButton={ - {repository || 'Select repository'} - } - width={500} - leftContent={undefined} - /> - - - - - - - ) -} - -export const AgentRunFormPopupSC = styled(SimplePopupMenu)(({ theme }) => ({ - width: 578, - padding: theme.spacing.medium, - gap: theme.spacing.large, - transform: 'translateY(20px)', - marginBottom: theme.spacing.xxlarge, - boxShadow: theme.boxShadows.moderate, - zIndex: theme.zIndexes.toast, -})) - -export const PromptInputBoxSC = styled(Card)(({ theme }) => ({ - padding: `${theme.spacing.small}px ${theme.spacing.medium}px`, - '&:focus-within': { - border: theme.borders['outline-focused'], - }, -})) diff --git a/assets/src/components/ai/chatbot/input/autocomplete/EditableSkillChipTooltip.tsx b/assets/src/components/ai/chatbot/input/autocomplete/EditableSkillChipTooltip.tsx index 2fcd3bd103..48722f872b 100644 --- a/assets/src/components/ai/chatbot/input/autocomplete/EditableSkillChipTooltip.tsx +++ b/assets/src/components/ai/chatbot/input/autocomplete/EditableSkillChipTooltip.tsx @@ -6,16 +6,25 @@ import { } from 'components/utils/contentEditableChips' import { forwardRef, + ReactNode, RefObject, useEffect, useLayoutEffect, useState, } from 'react' import { MentionKind } from './mentionTypes' +import { + hasVulnerabilityChipTooltip, + VulnerabilityChipTooltipLabel, + vulnerabilityChipTooltipText, +} from './VulnerabilityChipTooltipLabel' -const SKILL_SELECTOR = `[${CHIP_DATA_ATTR}="true"][${CHIP_TAG_ATTR}="${MentionKind.Skill}"]` +const PLRL_CHIP_SELECTOR = [MentionKind.Skill, MentionKind.Vulnerability] + .map((kind) => `[${CHIP_DATA_ATTR}="true"][${CHIP_TAG_ATTR}="${kind}"]`) + .join(',') const DESC_ATTR = `${CHIP_ATTR_PREFIX}description` const NAME_ATTR = `${CHIP_ATTR_PREFIX}item-name` +const TITLE_ATTR = `${CHIP_ATTR_PREFIX}title` const TIP_ON_INPUT_STYLE = { maxWidth: 500, @@ -23,18 +32,45 @@ const TIP_ON_INPUT_STYLE = { zIndex: 11_000, } -/** Skill chip + tooltip label, resolved from pointer event target. */ -function skillHintFromTarget( +function chipHintFromTarget( eventTarget: EventTarget | null -): { chip: HTMLElement; label: string } | null { +): { chip: HTMLElement; label: ReactNode; hintKey: string } | null { if (!(eventTarget instanceof Element)) return null - const chip = eventTarget.closest(SKILL_SELECTOR) + const chip = eventTarget.closest(PLRL_CHIP_SELECTOR) if (!(chip instanceof HTMLElement)) return null - const label = - chip.getAttribute(DESC_ATTR)?.trim() || - chip.getAttribute(NAME_ATTR)?.trim() || - null - return label ? { chip, label } : null + + const tag = chip.getAttribute(CHIP_TAG_ATTR) + const itemId = chip.getAttribute(`${CHIP_ATTR_PREFIX}item-id`) ?? '' + const description = chip.getAttribute(DESC_ATTR)?.trim() + const name = chip.getAttribute(NAME_ATTR)?.trim() + const title = chip.getAttribute(TITLE_ATTR)?.trim() + + switch (tag) { + case MentionKind.Skill: { + const label = description || name + if (!label) return null + return { + chip, + label, + hintKey: `${tag}|${itemId}|${description ?? ''}|${name ?? ''}`, + } + } + case MentionKind.Vulnerability: { + if (!hasVulnerabilityChipTooltip({ title, description })) return null + return { + chip, + label: ( + + ), + hintKey: `${tag}|${itemId}|${title ?? ''}|${description ?? ''}`, + } + } + default: + return null + } } const ChipAnchorStub = forwardRef( @@ -81,19 +117,21 @@ export function EditableSkillChipTooltip({ }: { containerRef: RefObject }) { - const [hint, setHint] = useState<{ chip: HTMLElement; label: string } | null>( - null - ) + const [hint, setHint] = useState<{ + chip: HTMLElement + label: ReactNode + hintKey: string + } | null>(null) useEffect(() => { const editorRoot = containerRef.current if (!editorRoot) return const onPointerMove = (event: PointerEvent) => { - const next = skillHintFromTarget(event.target) + const next = chipHintFromTarget(event.target) setHint((current) => { if (!next) return null - return current?.chip === next.chip && current.label === next.label + return current?.chip === next.chip && current.hintKey === next.hintKey ? current : next }) @@ -111,13 +149,24 @@ export function EditableSkillChipTooltip({ if (!hint) return null + const textValue = + hint.chip.getAttribute(CHIP_TAG_ATTR) === MentionKind.Vulnerability + ? vulnerabilityChipTooltipText({ + title: hint.chip.getAttribute(TITLE_ATTR) ?? undefined, + description: hint.chip.getAttribute(DESC_ATTR) ?? undefined, + }) + : typeof hint.label === 'string' + ? hint.label + : undefined + return ( diff --git a/assets/src/components/ai/chatbot/input/autocomplete/MentionResults.tsx b/assets/src/components/ai/chatbot/input/autocomplete/MentionResults.tsx index 9e6650bc42..ff6e111a39 100644 --- a/assets/src/components/ai/chatbot/input/autocomplete/MentionResults.tsx +++ b/assets/src/components/ai/chatbot/input/autocomplete/MentionResults.tsx @@ -2,6 +2,7 @@ import { ClusterIcon, GitPullIcon, StackIcon, + WarningShieldIcon, WorkbenchIcon, } from '@pluralsh/design-system' import { ReactNode, useEffect, useRef } from 'react' @@ -17,6 +18,7 @@ const itemToIcon: Record = { [MentionKind.Service]: , [MentionKind.Stack]: , [MentionKind.Skill]: , + [MentionKind.Vulnerability]: , } function subtitleForItem(item: ChipAttrs) { @@ -29,6 +31,8 @@ function subtitleForItem(item: ChipAttrs) { return item.type ? stackTypeLabel(item.type as StackType) : undefined case MentionKind.Skill: return item.description + case MentionKind.Vulnerability: + return item.resource ?? item['vuln-id'] ?? undefined } } diff --git a/assets/src/components/ai/chatbot/input/autocomplete/PlrlChipMdRenderers.tsx b/assets/src/components/ai/chatbot/input/autocomplete/PlrlChipMdRenderers.tsx index 09d62ace10..e29a12f550 100644 --- a/assets/src/components/ai/chatbot/input/autocomplete/PlrlChipMdRenderers.tsx +++ b/assets/src/components/ai/chatbot/input/autocomplete/PlrlChipMdRenderers.tsx @@ -2,6 +2,7 @@ import { GitPullIcon, StackIcon, Tooltip, + WarningShieldIcon, WorkbenchIcon, WrapWithIf, } from '@pluralsh/design-system' @@ -22,6 +23,11 @@ import { import { getStacksAbsPath } from 'routes/stacksRoutesConsts' import styled from 'styled-components' import { chipDisplayText, ChipAttrsByKind, MentionKind } from './mentionTypes' +import { + hasVulnerabilityChipTooltip, + VulnerabilityChipTooltipLabel, + vulnerabilityChipTooltipText, +} from './VulnerabilityChipTooltipLabel' // span-only so chips can safely render inside markdown

(as opposed to our DS chip) type ChipBodyProps = { @@ -139,11 +145,63 @@ function PlrlSkillChip(props: RenderedChipProps) { ) } +function PlrlVulnerabilityChip( + props: RenderedChipProps +) { + const label = + chipDisplayText(MentionKind.Vulnerability, { + 'item-name': props['item-name'], + severity: props.severity, + }) || + props['item-name'] || + props['item-id'] + const link = props['primary-link'] + const title = props.title + const description = props.description + const hasTooltip = hasVulnerabilityChipTooltip({ title, description }) + const chip = ( + }>{label} + ) + + return ( + + } + textValue={vulnerabilityChipTooltipText({ title, description })} + placement="top" + style={{ maxWidth: 500, overflowWrap: 'break-word' }} + /> + } + > + + } + > + {chip} + + + ) +} + export const plrlChipComponents = { [MentionKind.Cluster]: PlrlClusterChip, [MentionKind.Service]: PlrlServiceChip, [MentionKind.Stack]: PlrlStackChip, [MentionKind.Skill]: PlrlSkillChip, + [MentionKind.Vulnerability]: PlrlVulnerabilityChip, } satisfies { [K in MentionKind]: ComponentType> } @@ -169,3 +227,8 @@ const ChipLinkSC = styled(Link)(({ theme }) => ({ textDecoration: 'none', '&:hover > *': { background: theme.colors['fill-two-hover'] }, })) + +const ChipExternalLinkSC = styled.a(({ theme }) => ({ + textDecoration: 'none', + '&:hover > *': { background: theme.colors['fill-two-hover'] }, +})) diff --git a/assets/src/components/ai/chatbot/input/autocomplete/VulnerabilityChipTooltipLabel.tsx b/assets/src/components/ai/chatbot/input/autocomplete/VulnerabilityChipTooltipLabel.tsx new file mode 100644 index 0000000000..da71f13805 --- /dev/null +++ b/assets/src/components/ai/chatbot/input/autocomplete/VulnerabilityChipTooltipLabel.tsx @@ -0,0 +1,68 @@ +import styled from 'styled-components' + +export function hasVulnerabilityChipTooltip({ + title, + description, +}: { + title?: Nullable + description?: Nullable +}) { + return !!(title?.trim() || description?.trim()) +} + +export function vulnerabilityChipTooltipText({ + title, + description, +}: { + title?: Nullable + description?: Nullable +}) { + return [title?.trim(), description?.trim()].filter(Boolean).join('\n\n') +} + +export function VulnerabilityChipTooltipLabel({ + title, + description, +}: { + title?: Nullable + description?: Nullable +}) { + const titleText = title?.trim() + const descriptionText = description?.trim() + if (!titleText && !descriptionText) return null + + return ( + + {titleText && {titleText}} + {descriptionText && {descriptionText}} + + ) +} + +const lineClamp = (lines: number) => ({ + display: '-webkit-box', + WebkitBoxOrient: 'vertical' as const, + WebkitLineClamp: lines, + lineClamp: lines, + overflow: 'hidden', + wordBreak: 'break-word' as const, +}) + +const WrapSC = styled.div(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing.xxsmall, + minWidth: 0, +})) + +const TitleSC = styled.div(({ theme }) => ({ + ...theme.partials.text.body2Bold, + color: theme.colors.text, + ...lineClamp(1), +})) + +const DescriptionSC = styled.div(({ theme }) => ({ + ...theme.partials.text.body2, + color: theme.colors.text, + ...lineClamp(4), +})) diff --git a/assets/src/components/ai/chatbot/input/autocomplete/mentionTypes.ts b/assets/src/components/ai/chatbot/input/autocomplete/mentionTypes.ts index 8310491ef3..9a40467699 100644 --- a/assets/src/components/ai/chatbot/input/autocomplete/mentionTypes.ts +++ b/assets/src/components/ai/chatbot/input/autocomplete/mentionTypes.ts @@ -5,6 +5,7 @@ export enum MentionKind { Service = 'plrl-service', Stack = 'plrl-stack', Skill = 'plrl-skill', + Vulnerability = 'plrl-vulnerability', } export const PLRL_CHIP_TAG_NAMES: readonly MentionKind[] = @@ -15,6 +16,7 @@ export const KIND_LABELS: Record = { [MentionKind.Service]: 'service', [MentionKind.Stack]: 'stack', [MentionKind.Skill]: 'skill', + [MentionKind.Vulnerability]: 'vulnerability', } // --- Triggers --- @@ -57,11 +59,28 @@ export type SkillChipAttrs = BaseChipAttrs & { subagents?: string } +export type VulnerabilityChipAttrs = + BaseChipAttrs & { + severity?: Nullable + 'vuln-id'?: Nullable + title?: Nullable + 'report-id'?: string + 'service-ids'?: string + 'service-names'?: string + 'cluster-ids'?: string + resource?: Nullable + 'installed-version'?: Nullable + 'fixed-version'?: Nullable + 'primary-link'?: Nullable + description?: Nullable + } + export type ChipAttrsByKind = { [MentionKind.Cluster]: ClusterChipAttrs [MentionKind.Service]: ServiceChipAttrs [MentionKind.Stack]: StackChipAttrs [MentionKind.Skill]: SkillChipAttrs + [MentionKind.Vulnerability]: VulnerabilityChipAttrs } export type ChipAttrs = ChipAttrsByKind[MentionKind] @@ -88,6 +107,22 @@ export const CHIP_ATTRIBUTE_SCHEMA: { ], [MentionKind.Stack]: ['item-id', 'item-name', 'type'], [MentionKind.Skill]: ['item-id', 'item-name', 'description', 'subagents'], + [MentionKind.Vulnerability]: [ + 'item-id', + 'item-name', + 'severity', + 'vuln-id', + 'title', + 'report-id', + 'service-ids', + 'service-names', + 'cluster-ids', + 'resource', + 'installed-version', + 'fixed-version', + 'primary-link', + 'description', + ], } type ChipAttrRecord = Record @@ -111,6 +146,11 @@ export function chipDisplayText( if (ch && name) return `${name} (${ch})` return name } + case MentionKind.Vulnerability: { + const severity = attrs.severity + if (severity && name) return `${name} (${severity})` + return name + } case MentionKind.Stack: default: return name diff --git a/assets/src/components/cd/clusters/ClusterUpgradeAgentButton.tsx b/assets/src/components/cd/clusters/ClusterUpgradeAgentButton.tsx index 00d7e6cd6f..04699bdc1d 100644 --- a/assets/src/components/cd/clusters/ClusterUpgradeAgentButton.tsx +++ b/assets/src/components/cd/clusters/ClusterUpgradeAgentButton.tsx @@ -3,6 +3,7 @@ import { AccordionItem, AiSparkleFilledIcon, Button, + Card, Chip, CloseIcon, DiscoverIcon, @@ -14,12 +15,9 @@ import { ReloadIcon, SpinnerAlt, } from '@pluralsh/design-system' -import { - AgentRunFormPopupSC, - PromptInputBoxSC, -} from 'components/ai/agent-runs/AgentRunFixButton' import { AIAgentRuntimesSelector } from 'components/ai/agent-runs/AIAgentRuntimesSelector' import { useOutsideClick } from 'components/hooks/useOutsideClick' +import { SimplePopupMenu } from 'components/layout/HeaderPopupMenu' import { GqlError } from 'components/utils/Alert' import { EditableDiv } from 'components/utils/EditableDiv' import { FillLevelDiv } from 'components/utils/FillLevelDiv' @@ -35,7 +33,7 @@ import { import { useRef, useState } from 'react' import { Link } from 'react-router-dom' import { AI_SETTINGS_AGENT_RUNTIMES_ABS_PATH } from 'routes/settingsRoutesConst' -import { useTheme } from 'styled-components' +import styled, { useTheme } from 'styled-components' type CurUpgradeStatus = 'none' | 'running' | 'completed' | 'failed' @@ -226,3 +224,20 @@ const getCurUpgradeStatus = ( return 'failed' } } + +const AgentRunFormPopupSC = styled(SimplePopupMenu)(({ theme }) => ({ + width: 578, + padding: theme.spacing.medium, + gap: theme.spacing.large, + transform: 'translateY(20px)', + marginBottom: theme.spacing.xxlarge, + boxShadow: theme.boxShadows.moderate, + zIndex: theme.zIndexes.toast, +})) + +const PromptInputBoxSC = styled(Card)(({ theme }) => ({ + padding: `${theme.spacing.small}px ${theme.spacing.medium}px`, + '&:focus-within': { + border: theme.borders['outline-focused'], + }, +})) diff --git a/assets/src/components/security/vulnerabilities/VulnDetailExpanded.tsx b/assets/src/components/security/vulnerabilities/VulnDetailExpanded.tsx index b0fcb28639..a03ad7430a 100644 --- a/assets/src/components/security/vulnerabilities/VulnDetailExpanded.tsx +++ b/assets/src/components/security/vulnerabilities/VulnDetailExpanded.tsx @@ -1,4 +1,10 @@ -import { Chip, ChipProps, Flex } from '@pluralsh/design-system' +import { + AiSparkleFilledIcon, + Button, + Chip, + ChipProps, + Flex, +} from '@pluralsh/design-system' import styled, { useTheme } from 'styled-components' import { Row } from '@tanstack/react-table' @@ -10,27 +16,17 @@ import { CvssBundle, VulnAttackVector, VulnerabilityFragment, - VulnerabilityReportFragment, } from 'generated/graphql' -import { AgentRunFixButton } from 'components/ai/agent-runs/AgentRunFixButton' -import { useMemo } from 'react' -import ejs from 'ejs' -import vulnPromptTemplate from './vulnerability-prompt.ejs?raw' export function VulnDetailExpanded({ row, - parentReport, + onFixVulnerability, }: { row: Row - parentReport: Nullable + onFixVulnerability: (vuln: VulnerabilityFragment) => void }) { const { original: v } = row - const initialPrompt = useMemo( - () => ejs.render(vulnPromptTemplate, { vuln: v, report: parentReport }), - [v, parentReport] - ) - if (!v.title && !v.description && !v.cvssSource && !v.score && !v.cvss) return No details available. @@ -45,13 +41,15 @@ export function VulnDetailExpanded({ secondPartialType="body2" css={{ maxWidth: 900 }} /> - } + onClick={(e) => { + e.stopPropagation() + onFixVulnerability(v) + }} > Fix vulnerability - + void + vulnCount: number + initialPrompt: string + flowId?: Nullable +}) { + const { colors } = useTheme() + const headerTitle = `Fix ${pluralize('vulnerability', vulnCount, vulnCount !== 1)}` + const [prompt, setPrompt] = useState(initialPrompt) + const [workbenchJob, setWorkbenchJob] = useState( + null + ) + + return ( + + + + } + iconGap="xsmall" + /> + } + onClick={onClose} + /> + + {!!workbenchJob ? ( + + ) : ( + + )} + + + ) +} + +function VulnFixForm({ + flowId, + prompt, + setPrompt, + setWorkbenchJob, +}: { + flowId?: Nullable + prompt: string + setPrompt: (prompt: string) => void + setWorkbenchJob: (job: WorkbenchJobFragment) => void +}) { + const [workbenchId, setWorkbenchId] = useState>(null) + const { workbenches, loading } = useWorkbenchOptions(flowId) + + useEffect(() => { + setWorkbenchId((current) => { + if (!workbenches.length) return null + if (workbenches.some((workbench) => workbench.id === current)) + return current + return workbenches[0]?.id ?? null + }) + }, [workbenches]) + + const [createWorkbenchJob, { loading: mutationLoading, error }] = + useCreateWorkbenchJobMutation({ + onCompleted: ({ createWorkbenchJob }) => + createWorkbenchJob && setWorkbenchJob(createWorkbenchJob), + refetchQueries: ['WorkbenchJobs', 'RecentWorkbenchJobs'], + awaitRefetchQueries: true, + }) + + const canSubmit = + !!workbenchId && !!prompt.trim() && !mutationLoading && !loading + const promptInputRef = useRef(null) + + return ( + <> + {error && } + + + + + + + + + + + + ) +} + +function useWorkbenchOptions(flowId?: Nullable) { + const { data: flowData, loading: flowLoading } = useFlowWorkbenchesQuery({ + variables: { id: flowId ?? '' }, + skip: !flowId, + }) + const { data: allWorkbenchesData, loading: allWorkbenchesLoading } = + useWorkbenchesQuery({ + skip: !!flowId, + }) + + const workbenches = useMemo(() => { + if (flowId) return (flowData?.flow?.workbenches ?? []).filter(isNonNullable) + + return mapExistingNodes(allWorkbenchesData?.workbenches) + }, [allWorkbenchesData?.workbenches, flowData?.flow?.workbenches, flowId]) + + return { + workbenches, + loading: flowId ? flowLoading && !flowData : allWorkbenchesLoading, + } +} + +function WorkbenchSelector({ + workbenchId, + setWorkbenchId, + workbenches, + loading, +}: { + workbenchId: Nullable + setWorkbenchId: (id: Nullable) => void + workbenches: WorkbenchTinyFragment[] + loading: boolean +}) { + const [isOpen, setIsOpen] = useState(false) + const selectedWorkbench = workbenches.find( + (workbench) => workbench.id === workbenchId + ) + const SelectedIcon = selectedWorkbench + ? runtimeToIcon[ + selectedWorkbench.agentRuntime?.type ?? AgentRuntimeType.Custom + ] + : null + + return ( + + ) +} + +const PromptInputBoxSC = styled(Card)(({ theme }) => ({ + padding: `${theme.spacing.small}px ${theme.spacing.medium}px`, + '&:focus-within': { + border: theme.borders['outline-focused'], + }, +})) diff --git a/assets/src/components/security/vulnerabilities/VulnReportDetails.tsx b/assets/src/components/security/vulnerabilities/VulnReportDetails.tsx index 77747bde67..eccc402633 100644 --- a/assets/src/components/security/vulnerabilities/VulnReportDetails.tsx +++ b/assets/src/components/security/vulnerabilities/VulnReportDetails.tsx @@ -3,12 +3,20 @@ import { ReturnIcon, Table, useSetBreadcrumbs, + Flex, + Button, + CopyIcon, + AiSparkleFilledIcon, } from '@pluralsh/design-system' import { ColExpander } from 'components/cd/cluster/pod/PodContainers' import { GqlError } from 'components/utils/Alert' import { StackedText } from 'components/utils/table/StackedText' - -import { useClusterQuery, useVulnerabilityReportQuery } from 'generated/graphql' +import pluralize from 'pluralize' +import { + useClusterQuery, + useVulnerabilityReportQuery, + VulnerabilityFragment, +} from 'generated/graphql' import { useCurrentFlow } from 'components/flows/hooks/useCurrentFlow' import { Link, useParams } from 'react-router-dom' import { @@ -23,11 +31,14 @@ import { ColInstalledVersion, ColPackage, ColSeverity, + ColVulnSelect, } from './VulnReportDetailsTableCols' import { getFlowBreadcrumbs } from 'components/flows/flow/Flow' import { securityVulnReportsCrumbs } from './VulnReports' -import { useMemo } from 'react' -import { AgentRunFormPopupSC } from 'components/ai/agent-runs/AgentRunFixButton' +import { useMemo, useState } from 'react' +import { VulnFixModal } from './VulnFixModal' +import { RowSelectionState } from '@tanstack/react-table' +import { buildVulnerabilityFixPrompt } from './vulnerabilityMention' export function VulnerabilityReportDetails() { const { vulnerabilityReportId, clusterId } = useParams() @@ -52,6 +63,52 @@ export function VulnerabilityReportDetails() { const loading = clusterLoading || flowLoading || reportLoading + const [bulkSelectMode, setBulkSelectMode] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [fixVulns, setFixVulns] = useState(null) + + const vulnerabilities = useMemo( + () => + (data?.vulnerabilityReport?.vulnerabilities ?? []).filter( + (v): v is NonNullable => !!v + ), + [data?.vulnerabilityReport?.vulnerabilities] + ) + const selectedVulns = useMemo( + () => vulnerabilities.filter((v) => rowSelection[v.id]), + [vulnerabilities, rowSelection] + ) + const openFix = (vulns: VulnerabilityFragment[]) => { + if (!vulns.length) return + setFixVulns(vulns) + } + + const fixPrompt = useMemo( + () => + fixVulns + ? buildVulnerabilityFixPrompt(fixVulns, data?.vulnerabilityReport) + : '', + [fixVulns, data?.vulnerabilityReport] + ) + + const columns = useMemo( + () => [ + ...(bulkSelectMode ? [ColVulnSelect] : []), + ColExpander, + ColID, + ColPackage, + ColInstalledVersion, + ColFixedVersion, + ColSeverity, + ], + [bulkSelectMode] + ) + + const exitBulkSelectMode = () => { + setBulkSelectMode(false) + setRowSelection({}) + } + useSetBreadcrumbs( useMemo( () => @@ -69,60 +126,131 @@ export function VulnerabilityReportDetails() { return ( - } - style={{ flexShrink: 0 }} - /> - } - /> - true} - renderExpanded={({ row }) => ( - + + } + style={{ flexShrink: 0 }} + /> + } + css={{ minWidth: 0, flex: 1 }} + /> + + + +
true} + renderExpanded={({ row }) => ( + openFix([vuln])} + /> + )} + onRowClick={(_, row) => row.getToggleExpandedHandler()()} + emptyStateProps={{ message: 'No vulnerabilities found.' }} + expandedBgColor="fill-zero" + expandedRowType="custom" + reactTableOptions={{ + enableRowSelection: bulkSelectMode, + enableMultiRowSelection: true, + onRowSelectionChange: setRowSelection, + state: { rowSelection }, + }} + /> + {bulkSelectMode && ( + + {selectedVulns.length} selected + + )} - onRowClick={(_, row) => row.getToggleExpandedHandler()()} - emptyStateProps={{ message: 'No vulnerabilities found.' }} - expandedBgColor="fill-zero" - expandedRowType="custom" - // should probably find a better DS solution for this - {...{ [`td:has(${AgentRunFormPopupSC})`]: { overflow: 'visible' } }} - /> + + {fixVulns && ( + setFixVulns(null)} + vulnCount={fixVulns.length} + initialPrompt={fixPrompt} + flowId={flowData?.flow?.id} + /> + )} ) } -const columns = [ - ColExpander, - ColID, - ColPackage, - ColInstalledVersion, - ColFixedVersion, - ColSeverity, -] +const HeaderSC = styled(Flex)(({ theme }) => ({ + alignItems: 'center', + justifyContent: 'space-between', + gap: theme.spacing.medium, +})) + +const TableWrapperSC = styled.div(() => ({ + position: 'relative', + flex: 1, + overflow: 'hidden', +})) + +const BulkSelectionBarSC = styled.div(({ theme }) => ({ + position: 'absolute', + bottom: theme.spacing.large, + left: '50%', + transform: 'translateX(-50%)', + display: 'flex', + alignItems: 'center', + gap: theme.spacing.large, + padding: `${theme.spacing.small}px ${theme.spacing.medium}px`, + backgroundColor: theme.colors['fill-three'], + border: theme.borders['fill-three'], + borderRadius: 12, + boxShadow: theme.boxShadows.moderate, + zIndex: theme.zIndexes.toast, + whiteSpace: 'nowrap', +})) + +const SelectionCountSC = styled.span(({ theme }) => ({ + ...theme.partials.text.body2, + color: theme.colors.text, + paddingRight: theme.spacing.medium, + borderRight: theme.borders['fill-three'], + alignSelf: 'stretch', + display: 'flex', + alignItems: 'center', +})) const WrapperSC = styled.div(({ theme }) => ({ display: 'flex', diff --git a/assets/src/components/security/vulnerabilities/VulnReportDetailsTableCols.tsx b/assets/src/components/security/vulnerabilities/VulnReportDetailsTableCols.tsx index 73dcac7837..c1effeccca 100644 --- a/assets/src/components/security/vulnerabilities/VulnReportDetailsTableCols.tsx +++ b/assets/src/components/security/vulnerabilities/VulnReportDetailsTableCols.tsx @@ -1,4 +1,10 @@ -import { Chip, ChipProps, Tooltip, WrapWithIf } from '@pluralsh/design-system' +import { + Checkbox, + Chip, + ChipProps, + Tooltip, + WrapWithIf, +} from '@pluralsh/design-system' import { createColumnHelper } from '@tanstack/react-table' import { VulnerabilityFragment, VulnSeverity } from 'generated/graphql' import { truncate } from 'lodash' @@ -6,6 +12,30 @@ import { useTheme } from 'styled-components' const columnHelper = createColumnHelper() +export const ColVulnSelect = columnHelper.display({ + id: 'select', + header: ({ table }) => ( +
e.stopPropagation()}> + +
+ ), + cell: ({ row }) => ( +
e.stopPropagation()}> + +
+ ), + meta: { gridTemplate: '48px' }, +}) + export const ColID = columnHelper.accessor((report) => report, { id: 'id', header: 'ID', diff --git a/assets/src/components/security/vulnerabilities/VulnReportsTableCols.tsx b/assets/src/components/security/vulnerabilities/VulnReportsTableCols.tsx index 562ea9f5af..ecbbdead98 100644 --- a/assets/src/components/security/vulnerabilities/VulnReportsTableCols.tsx +++ b/assets/src/components/security/vulnerabilities/VulnReportsTableCols.tsx @@ -158,13 +158,17 @@ export const ColActions = columnHelper.display({ gridTemplate: 'minmax(auto, 80px)', }, cell: function Cell({ row: { original } }) { - const { clusterId = '' } = useParams() + const { clusterId = '', flowIdOrName } = useParams<{ + clusterId?: string + flowIdOrName?: string + }>() const vulnerabilityReportId = original?.id ?? '' return ( -<%= vuln.description %> - -It is found in the following docker image: <%= report.artifactUrl %> - -Here are the recommendations for potential upgrades: - -Fix Version: <%= vuln.fixedVersion %> -Current Version: <%= vuln.installedVersion %> -Package: <%= vuln.resource %> - -Do your best to upgrade all necessary dependencies and confirm that the code still compiles (if there are compilation instructions available) and introduces no regressions in the process. If there are other docker images affected by this vulnerability, ignore them for now. \ No newline at end of file diff --git a/assets/src/components/security/vulnerabilities/vulnerabilityMention.ts b/assets/src/components/security/vulnerabilities/vulnerabilityMention.ts new file mode 100644 index 0000000000..0f3c88f73c --- /dev/null +++ b/assets/src/components/security/vulnerabilities/vulnerabilityMention.ts @@ -0,0 +1,88 @@ +import { MentionKind } from 'components/ai/chatbot/input/autocomplete/mentionTypes' +import { + VulnerabilityFragment, + VulnerabilityReportFragment, +} from 'generated/graphql' +import escape from 'lodash/escape' +import { isNonNullable } from 'utils/isNonNullable' +import pluralize from 'pluralize' + +const VULN_IDENTIFIER_PATTERN = /(?:CVE|GHSA)-[\w-]+/i + +export function vulnerabilityIdentifier(vuln: VulnerabilityFragment): string { + if (vuln.vulnId?.trim()) return vuln.vulnId.trim() + + for (const candidate of [ + vuln.primaryLink, + ...(vuln.links ?? []), + vuln.title, + ]) { + if (!candidate) continue + const match = candidate.match(VULN_IDENTIFIER_PATTERN)?.[0] + if (match) return match.toUpperCase() + } + + return vuln.resource ?? vuln.id +} + +function xmlAttr(name: string, value: Nullable): string { + if (value == null || value === '') return '' + return `${name}="${escape(value)}"` +} + +export function serializeVulnerabilityMention( + vuln: VulnerabilityFragment, + report: VulnerabilityReportFragment +): string { + const identifier = vulnerabilityIdentifier(vuln) + const services = (report.services ?? []).filter(isNonNullable) + const serviceIds = services + .map((entry) => entry.service?.id) + .filter(isNonNullable) + .join(',') + const serviceNames = services + .map((entry) => entry.service?.name) + .filter(isNonNullable) + .join(',') + const clusterIds = services + .map((entry) => entry.service?.cluster?.id) + .filter(isNonNullable) + .join(',') + + const attrs = [ + xmlAttr('item-id', vuln.id), + xmlAttr('item-name', identifier), + xmlAttr('severity', vuln.severity ?? undefined), + xmlAttr('vuln-id', vuln.vulnId ?? identifier), + xmlAttr('title', vuln.title ?? undefined), + xmlAttr('report-id', report.id), + xmlAttr('service-ids', serviceIds || undefined), + xmlAttr('service-names', serviceNames || undefined), + xmlAttr('cluster-ids', clusterIds || undefined), + xmlAttr('resource', vuln.resource ?? undefined), + xmlAttr('installed-version', vuln.installedVersion ?? undefined), + xmlAttr('fixed-version', vuln.fixedVersion ?? undefined), + xmlAttr('primary-link', vuln.primaryLink ?? undefined), + xmlAttr('description', vuln.description?.trim()), + ].filter(Boolean) + + return `<${MentionKind.Vulnerability}${attrs.length ? ' ' + attrs.join(' ') : ''}>` +} + +export function buildVulnerabilityFixPrompt( + vulns: VulnerabilityFragment[], + report: Nullable +): string { + if (!vulns.length || !report) return '' + + const multiple = vulns.length > 1 + const mentions = vulns + .map((vuln) => serializeVulnerabilityMention(vuln, report)) + .join('\n') + + return `Security scanners found ${pluralize('vulnerability', vulns.length, vulns.length !== 1)} in the ${report.artifactUrl ?? ''} Docker image: + +${mentions} + +Create a pull request to fix ${multiple ? 'them' : 'it'}. Do your best to upgrade all necessary dependencies and confirm that the code still compiles (if there are compilation instructions available) and introduces no regressions in the process. If there are other Docker images affected by ${multiple ? 'these vulnerabilities' : 'this vulnerability'}, ignore them for now.` +} diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index def9461201..a4d10c8316 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -19363,7 +19363,7 @@ export type FlowVulnerabilityReportsQueryVariables = Exact<{ }>; -export type FlowVulnerabilityReportsQuery = { __typename?: 'RootQueryType', flow?: { __typename?: 'Flow', id: string, vulnerabilityReports?: { __typename?: 'VulnerabilityReportConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'VulnerabilityReportEdge', node?: { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', name: string } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null } | null } | null> | null } | null } | null }; +export type FlowVulnerabilityReportsQuery = { __typename?: 'RootQueryType', flow?: { __typename?: 'Flow', id: string, vulnerabilityReports?: { __typename?: 'VulnerabilityReportConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'VulnerabilityReportEdge', node?: { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string } | null } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null } | null } | null> | null } | null } | null }; export type UpsertFlowMutationVariables = Exact<{ attributes: FlowAttributes; @@ -20702,11 +20702,11 @@ export type CreateInviteMutationVariables = Exact<{ export type CreateInviteMutation = { __typename?: 'RootMutationType', createInvite?: { __typename?: 'Invite', secureId: string } | null }; -export type VulnerabilityReportTinyFragment = { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', name: string } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null }; +export type VulnerabilityReportTinyFragment = { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string } | null } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null }; -export type VulnerabilityReportFragment = { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, vulnerabilities?: Array<{ __typename?: 'Vulnerability', id: string, title?: string | null, description?: string | null, severity?: VulnSeverity | null, score?: number | null, primaryLink?: string | null, links?: Array | null, target?: string | null, class?: string | null, packageType?: string | null, pkgPath?: string | null, publishedDate?: string | null, installedVersion?: string | null, fixedVersion?: string | null, lastModifiedDate?: string | null, cvssSource?: string | null, resource?: string | null, insertedAt?: string | null, updatedAt?: string | null, cvss?: { __typename?: 'CvssBundle', attackComplexity?: VulnSeverity | null, attackVector?: VulnAttackVector | null, availability?: VulnSeverity | null, confidentiality?: VulnSeverity | null, integrity?: VulnSeverity | null, privilegesRequired?: VulnSeverity | null, userInteraction?: VulnUserInteraction | null, nvidia?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null, redhat?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null } | null } | null> | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', name: string } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null }; +export type VulnerabilityReportFragment = { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, vulnerabilities?: Array<{ __typename?: 'Vulnerability', id: string, vulnId?: string | null, title?: string | null, description?: string | null, severity?: VulnSeverity | null, score?: number | null, primaryLink?: string | null, links?: Array | null, target?: string | null, class?: string | null, packageType?: string | null, pkgPath?: string | null, publishedDate?: string | null, installedVersion?: string | null, fixedVersion?: string | null, lastModifiedDate?: string | null, cvssSource?: string | null, resource?: string | null, insertedAt?: string | null, updatedAt?: string | null, cvss?: { __typename?: 'CvssBundle', attackComplexity?: VulnSeverity | null, attackVector?: VulnAttackVector | null, availability?: VulnSeverity | null, confidentiality?: VulnSeverity | null, integrity?: VulnSeverity | null, privilegesRequired?: VulnSeverity | null, userInteraction?: VulnUserInteraction | null, nvidia?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null, redhat?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null } | null } | null> | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string } | null } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null }; -export type VulnerabilityFragment = { __typename?: 'Vulnerability', id: string, title?: string | null, description?: string | null, severity?: VulnSeverity | null, score?: number | null, primaryLink?: string | null, links?: Array | null, target?: string | null, class?: string | null, packageType?: string | null, pkgPath?: string | null, publishedDate?: string | null, installedVersion?: string | null, fixedVersion?: string | null, lastModifiedDate?: string | null, cvssSource?: string | null, resource?: string | null, insertedAt?: string | null, updatedAt?: string | null, cvss?: { __typename?: 'CvssBundle', attackComplexity?: VulnSeverity | null, attackVector?: VulnAttackVector | null, availability?: VulnSeverity | null, confidentiality?: VulnSeverity | null, integrity?: VulnSeverity | null, privilegesRequired?: VulnSeverity | null, userInteraction?: VulnUserInteraction | null, nvidia?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null, redhat?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null } | null }; +export type VulnerabilityFragment = { __typename?: 'Vulnerability', id: string, vulnId?: string | null, title?: string | null, description?: string | null, severity?: VulnSeverity | null, score?: number | null, primaryLink?: string | null, links?: Array | null, target?: string | null, class?: string | null, packageType?: string | null, pkgPath?: string | null, publishedDate?: string | null, installedVersion?: string | null, fixedVersion?: string | null, lastModifiedDate?: string | null, cvssSource?: string | null, resource?: string | null, insertedAt?: string | null, updatedAt?: string | null, cvss?: { __typename?: 'CvssBundle', attackComplexity?: VulnSeverity | null, attackVector?: VulnAttackVector | null, availability?: VulnSeverity | null, confidentiality?: VulnSeverity | null, integrity?: VulnSeverity | null, privilegesRequired?: VulnSeverity | null, userInteraction?: VulnUserInteraction | null, nvidia?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null, redhat?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null } | null }; export type VulnerabilityStatisticFragment = { __typename?: 'VulnerabilityStatistic', count: number, grade: VulnReportGrade }; @@ -20714,7 +20714,7 @@ export type ClusterVulnAggregateFragment = { __typename?: 'ClusterVulnAggregate' export type CvssBundleFragment = { __typename?: 'CvssBundle', attackComplexity?: VulnSeverity | null, attackVector?: VulnAttackVector | null, availability?: VulnSeverity | null, confidentiality?: VulnSeverity | null, integrity?: VulnSeverity | null, privilegesRequired?: VulnSeverity | null, userInteraction?: VulnUserInteraction | null, nvidia?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null, redhat?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null }; -export type VulnerabilityReportConnectionFragment = { __typename?: 'VulnerabilityReportConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'VulnerabilityReportEdge', node?: { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', name: string } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null } | null } | null> | null }; +export type VulnerabilityReportConnectionFragment = { __typename?: 'VulnerabilityReportConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'VulnerabilityReportEdge', node?: { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string } | null } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null } | null } | null> | null }; export type VulnerabilityReportsQueryVariables = Exact<{ clusters?: InputMaybe> | InputMaybe>; @@ -20726,14 +20726,14 @@ export type VulnerabilityReportsQueryVariables = Exact<{ }>; -export type VulnerabilityReportsQuery = { __typename?: 'RootQueryType', vulnerabilityReports?: { __typename?: 'VulnerabilityReportConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'VulnerabilityReportEdge', node?: { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', name: string } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null } | null } | null> | null } | null }; +export type VulnerabilityReportsQuery = { __typename?: 'RootQueryType', vulnerabilityReports?: { __typename?: 'VulnerabilityReportConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'VulnerabilityReportEdge', node?: { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string } | null } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null } | null } | null> | null } | null }; export type VulnerabilityReportQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type VulnerabilityReportQuery = { __typename?: 'RootQueryType', vulnerabilityReport?: { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, vulnerabilities?: Array<{ __typename?: 'Vulnerability', id: string, title?: string | null, description?: string | null, severity?: VulnSeverity | null, score?: number | null, primaryLink?: string | null, links?: Array | null, target?: string | null, class?: string | null, packageType?: string | null, pkgPath?: string | null, publishedDate?: string | null, installedVersion?: string | null, fixedVersion?: string | null, lastModifiedDate?: string | null, cvssSource?: string | null, resource?: string | null, insertedAt?: string | null, updatedAt?: string | null, cvss?: { __typename?: 'CvssBundle', attackComplexity?: VulnSeverity | null, attackVector?: VulnAttackVector | null, availability?: VulnSeverity | null, confidentiality?: VulnSeverity | null, integrity?: VulnSeverity | null, privilegesRequired?: VulnSeverity | null, userInteraction?: VulnUserInteraction | null, nvidia?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null, redhat?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null } | null } | null> | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', name: string } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null } | null }; +export type VulnerabilityReportQuery = { __typename?: 'RootQueryType', vulnerabilityReport?: { __typename?: 'VulnerabilityReport', id: string, artifactUrl?: string | null, artifactRepoUrl?: string | null, vulnerabilities?: Array<{ __typename?: 'Vulnerability', id: string, vulnId?: string | null, title?: string | null, description?: string | null, severity?: VulnSeverity | null, score?: number | null, primaryLink?: string | null, links?: Array | null, target?: string | null, class?: string | null, packageType?: string | null, pkgPath?: string | null, publishedDate?: string | null, installedVersion?: string | null, fixedVersion?: string | null, lastModifiedDate?: string | null, cvssSource?: string | null, resource?: string | null, insertedAt?: string | null, updatedAt?: string | null, cvss?: { __typename?: 'CvssBundle', attackComplexity?: VulnSeverity | null, attackVector?: VulnAttackVector | null, availability?: VulnSeverity | null, confidentiality?: VulnSeverity | null, integrity?: VulnSeverity | null, privilegesRequired?: VulnSeverity | null, userInteraction?: VulnUserInteraction | null, nvidia?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null, redhat?: { __typename?: 'Cvss', v2Score?: number | null, v2Vector?: string | null, v3Score?: number | null, v3Vector?: string | null, v40Score?: number | null, v40Vector?: string | null } | null } | null } | null> | null, services?: Array<{ __typename?: 'ServiceVuln', service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string } | null } | null } | null> | null, namespaces?: Array<{ __typename?: 'NamespaceVuln', namespace: string } | null> | null, summary?: { __typename?: 'VulnSummary', criticalCount?: number | null, highCount?: number | null, mediumCount?: number | null, lowCount?: number | null, unknownCount?: number | null, noneCount?: number | null } | null } | null }; export type VulnerabilityStatisticsQueryVariables = Exact<{ clusters?: InputMaybe> | InputMaybe>; @@ -26175,7 +26175,11 @@ export const VulnerabilityReportTinyFragmentDoc = gql` artifactUrl services { service { + id name + cluster { + id + } } } namespaces { @@ -26222,6 +26226,7 @@ export const CvssBundleFragmentDoc = gql` export const VulnerabilityFragmentDoc = gql` fragment Vulnerability on Vulnerability { id + vulnId title description severity diff --git a/assets/src/generated/persisted-queries/client.json b/assets/src/generated/persisted-queries/client.json index 1c3799e786..e59f0acf2d 100644 --- a/assets/src/generated/persisted-queries/client.json +++ b/assets/src/generated/persisted-queries/client.json @@ -1122,10 +1122,10 @@ "name": "FlowPreviewEnvironmentTemplates", "body": "query FlowPreviewEnvironmentTemplates($id: ID!, $first: Int = 100, $after: String) {\n flow(id: $id) {\n id\n previewEnvironmentTemplates(first: $first, after: $after) {\n ...PreviewEnvironmentTemplateConnection\n __typename\n }\n __typename\n }\n}\n\nfragment PreviewEnvironmentTemplateConnection on PreviewEnvironmentTemplateConnection {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...PreviewEnvironmentTemplate\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment PreviewEnvironmentTemplate on PreviewEnvironmentTemplate {\n id\n name\n commentTemplate\n referenceService {\n id\n name\n cluster {\n id\n __typename\n }\n __typename\n }\n template {\n ...ServiceTemplateWithoutConfiguration\n __typename\n }\n __typename\n}\n\nfragment ServiceTemplateWithoutConfiguration on ServiceTemplate {\n contexts\n dependencies {\n id\n name\n status\n __typename\n }\n git {\n folder\n ref\n __typename\n }\n helm {\n chart\n git {\n folder\n ref\n __typename\n }\n ignoreCrds\n ignoreHooks\n release\n repository {\n name\n namespace\n __typename\n }\n set {\n name\n value\n __typename\n }\n url\n values\n valuesFiles\n version\n __typename\n }\n kustomize {\n path\n enableHelm\n __typename\n }\n name\n namespace\n repository {\n ...GitRepository\n __typename\n }\n repositoryId\n syncConfig {\n createNamespace\n enforceNamespace\n namespaceMetadata {\n annotations\n labels\n __typename\n }\n __typename\n }\n templated\n __typename\n}\n\nfragment GitRepository on GitRepository {\n id\n url\n health\n authMethod\n editable\n error\n insertedAt\n pulledAt\n updatedAt\n urlFormat\n httpsPath\n recurseSubmodules\n __typename\n}" }, - "sha256:516cf69d023ebb1f1444e0a9f007979163fa5853fc7b602b4629781fa678ced3": { + "sha256:3ba0a1fc72dee4d9979e76692346e9416b79688bf40f6957520b6b0c92c94238": { "type": "query", "name": "FlowVulnerabilityReports", - "body": "query FlowVulnerabilityReports($id: ID!, $first: Int = 100, $after: String) {\n flow(id: $id) {\n id\n vulnerabilityReports(first: $first, after: $after) {\n ...VulnerabilityReportConnection\n __typename\n }\n __typename\n }\n}\n\nfragment VulnerabilityReportConnection on VulnerabilityReportConnection {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...VulnerabilityReportTiny\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment VulnerabilityReportTiny on VulnerabilityReport {\n id\n artifactUrl\n services {\n service {\n name\n __typename\n }\n __typename\n }\n namespaces {\n namespace\n __typename\n }\n summary {\n criticalCount\n highCount\n mediumCount\n lowCount\n unknownCount\n noneCount\n __typename\n }\n artifactRepoUrl\n __typename\n}" + "body": "query FlowVulnerabilityReports($id: ID!, $first: Int = 100, $after: String) {\n flow(id: $id) {\n id\n vulnerabilityReports(first: $first, after: $after) {\n ...VulnerabilityReportConnection\n __typename\n }\n __typename\n }\n}\n\nfragment VulnerabilityReportConnection on VulnerabilityReportConnection {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...VulnerabilityReportTiny\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment VulnerabilityReportTiny on VulnerabilityReport {\n id\n artifactUrl\n services {\n service {\n id\n name\n cluster {\n id\n __typename\n }\n __typename\n }\n __typename\n }\n namespaces {\n namespace\n __typename\n }\n summary {\n criticalCount\n highCount\n mediumCount\n lowCount\n unknownCount\n noneCount\n __typename\n }\n artifactRepoUrl\n __typename\n}" }, "sha256:98abe49796f5c9a4b65a17c6ae91b679f2cb93e9ec62eb2c69a2d00c443dbea0": { "type": "mutation", @@ -1812,15 +1812,15 @@ "name": "CreateInvite", "body": "mutation CreateInvite($attributes: InviteAttributes!) {\n createInvite(attributes: $attributes) {\n ...Invite\n __typename\n }\n}\n\nfragment Invite on Invite {\n secureId\n __typename\n}" }, - "sha256:b726ea3e1cb7e68df12f44a5a4eca0286277df51933cb519ea7404f6dc5a9b6f": { + "sha256:f37ba8e83f953710e180c2075fa2d84e3ce319bb45e097c516f24922a5d4c439": { "type": "query", "name": "VulnerabilityReports", - "body": "query VulnerabilityReports($clusters: [ID], $namespaces: [String], $q: String, $grade: VulnReportGrade, $first: Int, $after: String) {\n vulnerabilityReports(\n clusters: $clusters\n namespaces: $namespaces\n q: $q\n grade: $grade\n first: $first\n after: $after\n ) {\n ...VulnerabilityReportConnection\n __typename\n }\n}\n\nfragment VulnerabilityReportConnection on VulnerabilityReportConnection {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...VulnerabilityReportTiny\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment VulnerabilityReportTiny on VulnerabilityReport {\n id\n artifactUrl\n services {\n service {\n name\n __typename\n }\n __typename\n }\n namespaces {\n namespace\n __typename\n }\n summary {\n criticalCount\n highCount\n mediumCount\n lowCount\n unknownCount\n noneCount\n __typename\n }\n artifactRepoUrl\n __typename\n}" + "body": "query VulnerabilityReports($clusters: [ID], $namespaces: [String], $q: String, $grade: VulnReportGrade, $first: Int, $after: String) {\n vulnerabilityReports(\n clusters: $clusters\n namespaces: $namespaces\n q: $q\n grade: $grade\n first: $first\n after: $after\n ) {\n ...VulnerabilityReportConnection\n __typename\n }\n}\n\nfragment VulnerabilityReportConnection on VulnerabilityReportConnection {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...VulnerabilityReportTiny\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment VulnerabilityReportTiny on VulnerabilityReport {\n id\n artifactUrl\n services {\n service {\n id\n name\n cluster {\n id\n __typename\n }\n __typename\n }\n __typename\n }\n namespaces {\n namespace\n __typename\n }\n summary {\n criticalCount\n highCount\n mediumCount\n lowCount\n unknownCount\n noneCount\n __typename\n }\n artifactRepoUrl\n __typename\n}" }, - "sha256:db3c341b13fc8f2f81886bb7f69779ed27974f14c95b9a9e2f2c4139bd0708d1": { + "sha256:db27bda6b3282a0e18dc6a1ff85ba3524e502adee14d43a0acde8f21bdae6f8e": { "type": "query", "name": "VulnerabilityReport", - "body": "query VulnerabilityReport($id: ID!) {\n vulnerabilityReport(id: $id) {\n ...VulnerabilityReport\n __typename\n }\n}\n\nfragment VulnerabilityReport on VulnerabilityReport {\n ...VulnerabilityReportTiny\n vulnerabilities {\n ...Vulnerability\n __typename\n }\n __typename\n}\n\nfragment VulnerabilityReportTiny on VulnerabilityReport {\n id\n artifactUrl\n services {\n service {\n name\n __typename\n }\n __typename\n }\n namespaces {\n namespace\n __typename\n }\n summary {\n criticalCount\n highCount\n mediumCount\n lowCount\n unknownCount\n noneCount\n __typename\n }\n artifactRepoUrl\n __typename\n}\n\nfragment Vulnerability on Vulnerability {\n id\n title\n description\n severity\n score\n primaryLink\n links\n target\n class\n packageType\n pkgPath\n publishedDate\n installedVersion\n fixedVersion\n lastModifiedDate\n cvss {\n ...CvssBundle\n __typename\n }\n cvssSource\n resource\n insertedAt\n updatedAt\n __typename\n}\n\nfragment CvssBundle on CvssBundle {\n attackComplexity\n attackVector\n availability\n confidentiality\n integrity\n privilegesRequired\n userInteraction\n nvidia {\n v2Score\n v2Vector\n v3Score\n v3Vector\n v40Score\n v40Vector\n __typename\n }\n redhat {\n v2Score\n v2Vector\n v3Score\n v3Vector\n v40Score\n v40Vector\n __typename\n }\n __typename\n}" + "body": "query VulnerabilityReport($id: ID!) {\n vulnerabilityReport(id: $id) {\n ...VulnerabilityReport\n __typename\n }\n}\n\nfragment VulnerabilityReport on VulnerabilityReport {\n ...VulnerabilityReportTiny\n vulnerabilities {\n ...Vulnerability\n __typename\n }\n __typename\n}\n\nfragment VulnerabilityReportTiny on VulnerabilityReport {\n id\n artifactUrl\n services {\n service {\n id\n name\n cluster {\n id\n __typename\n }\n __typename\n }\n __typename\n }\n namespaces {\n namespace\n __typename\n }\n summary {\n criticalCount\n highCount\n mediumCount\n lowCount\n unknownCount\n noneCount\n __typename\n }\n artifactRepoUrl\n __typename\n}\n\nfragment Vulnerability on Vulnerability {\n id\n vulnId\n title\n description\n severity\n score\n primaryLink\n links\n target\n class\n packageType\n pkgPath\n publishedDate\n installedVersion\n fixedVersion\n lastModifiedDate\n cvss {\n ...CvssBundle\n __typename\n }\n cvssSource\n resource\n insertedAt\n updatedAt\n __typename\n}\n\nfragment CvssBundle on CvssBundle {\n attackComplexity\n attackVector\n availability\n confidentiality\n integrity\n privilegesRequired\n userInteraction\n nvidia {\n v2Score\n v2Vector\n v3Score\n v3Vector\n v40Score\n v40Vector\n __typename\n }\n redhat {\n v2Score\n v2Vector\n v3Score\n v3Vector\n v40Score\n v40Vector\n __typename\n }\n __typename\n}" }, "sha256:31df5a43f4f42d497b4f3f5b57530027765d148c3005099ff59df1993ec7b42b": { "type": "query", diff --git a/assets/src/graph/vulnerabilities.graphql b/assets/src/graph/vulnerabilities.graphql index 78b255d751..d655cfee73 100644 --- a/assets/src/graph/vulnerabilities.graphql +++ b/assets/src/graph/vulnerabilities.graphql @@ -3,7 +3,11 @@ fragment VulnerabilityReportTiny on VulnerabilityReport { artifactUrl services { service { + id name + cluster { + id + } } } namespaces { @@ -29,6 +33,7 @@ fragment VulnerabilityReport on VulnerabilityReport { fragment Vulnerability on Vulnerability { id + vulnId title description severity diff --git a/go/deployment-operator/internal/controller/vulnerabilityreports_controller.go b/go/deployment-operator/internal/controller/vulnerabilityreports_controller.go index 360e194a6b..e46da6a454 100644 --- a/go/deployment-operator/internal/controller/vulnerabilityreports_controller.go +++ b/go/deployment-operator/internal/controller/vulnerabilityreports_controller.go @@ -179,6 +179,7 @@ func createVulnAttributes(vulnerabilityReport trivy.VulnerabilityReport, service Class: lo.ToPtr(v.Class), PackageType: lo.ToPtr(v.PackageType), PkgPath: lo.ToPtr(v.PkgPath), + VulnID: lo.ToPtr(v.VulnerabilityID), RepositoryURL: repositoryInfo.URL, AgentRuntime: repositoryInfo.AgentRuntime, }