diff --git a/.changeset/heureka-mitigate-manually.md b/.changeset/heureka-mitigate-manually.md new file mode 100644 index 0000000000..215e7dcfd2 --- /dev/null +++ b/.changeset/heureka-mitigate-manually.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-app-heureka": patch +--- + +Add Mitigate Manually action to vulnerability rows. Users can now mark a CVE as manually mitigated from the popup menu in both the Active and Remediated vulnerability tabs. The CVE moves to the Remediated tab immediately and can be reverted via the Remediation History Panel. diff --git a/apps/heureka/src/components/Service/ImageDetails/FalsePositiveModal/index.tsx b/apps/heureka/src/components/Service/ImageDetails/FalsePositiveModal/index.tsx index 788f1b6d76..42c6e1b211 100644 --- a/apps/heureka/src/components/Service/ImageDetails/FalsePositiveModal/index.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/FalsePositiveModal/index.tsx @@ -3,19 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useRef, useEffect } from "react" -import { - Modal, - ModalFooter, - Button, - Stack, - Textarea, - TextInput, - DateTimePicker, - Message, -} from "@cloudoperators/juno-ui-components" -import { RemediationInput, RemediationTypeValues, SeverityValues } from "../../../../generated/graphql" -import { useAuth } from "@cloudoperators/greenhouse-auth-provider" +import React from "react" +import { RemediationInput, RemediationTypeValues } from "../../../../generated/graphql" +import { RemediationModal } from "../RemediationModal" type FalsePositiveModalProps = { open: boolean @@ -27,202 +17,13 @@ type FalsePositiveModalProps = { image: string } -const CONFIRM_LABEL = "Mark as False Positive" -const CANCEL_LABEL = "Cancel" - -const toSeverityValue = (severity: string): SeverityValues | undefined => { - if (!severity) return undefined - const normalized = severity.charAt(0).toUpperCase() + severity.slice(1).toLowerCase() - const value = normalized as SeverityValues - return Object.values(SeverityValues).includes(value) ? value : undefined -} - -export const FalsePositiveModal: React.FC = ({ - open, - onClose, - onConfirm, - vulnerability, - severity, - service, - image, -}) => { - const auth = useAuth() - const authUserId = auth.status === "authenticated" ? auth.userId || auth.userName : null - const [description, setDescription] = useState("") - const [manualUserId, setManualUserId] = useState("") - const [expirationDate, setExpirationDate] = useState(null) - const [isSubmitting, setIsSubmitting] = useState(false) - const [descriptionError, setDescriptionError] = useState("") - const [userIdError, setUserIdError] = useState("") - const [expirationDateError, setExpirationDateError] = useState("") - const [apiError, setApiError] = useState(null) - const isMountedRef = useRef(true) - - const manualUserIdTrimmed = manualUserId.trim() - const remediatedBy = authUserId ?? (manualUserIdTrimmed || undefined) - const isUserIdValid = !!remediatedBy - - useEffect(() => { - isMountedRef.current = true - return () => { - isMountedRef.current = false - } - }, []) - - useEffect(() => { - if (!open) { - setDescription("") - setManualUserId("") - setExpirationDate(null) - setDescriptionError("") - setUserIdError("") - setExpirationDateError("") - setApiError(null) - } - }, [open]) - - const descriptionTrimmed = description.trim() - - const handleConfirm = async () => { - if (!descriptionTrimmed) { - setDescriptionError("Description is required") - return - } - if (!remediatedBy) { - setUserIdError("User ID is required") - return - } - if (!expirationDate) { - setExpirationDateError("Expiration date is required") - return - } - - setDescriptionError("") - setUserIdError("") - setExpirationDateError("") - setIsSubmitting(true) - try { - const severityValue = severity ? toSeverityValue(severity) : undefined - const input: RemediationInput = { - type: RemediationTypeValues.FalsePositive, - vulnerability, - service, - image, - description: descriptionTrimmed, - ...(remediatedBy && { remediatedBy }), - ...(severityValue !== undefined && { severity: severityValue }), - expirationDate: expirationDate.toISOString(), - } - const result = await onConfirm(input) - if (result?.error) { - setApiError(result.error) - } else if (isMountedRef.current) { - setDescription("") - setManualUserId("") - setExpirationDate(null) - onClose() - } - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to create remediation" - setApiError(message) - } finally { - setIsSubmitting(false) - } - } - - const handleClose = () => { - setDescription("") - setManualUserId("") - setExpirationDate(null) - setDescriptionError("") - setUserIdError("") - setExpirationDateError("") - setApiError(null) - onClose() - } - - const handleDescriptionChange = (e: React.ChangeEvent) => { - setDescription(e.target.value) - // Clear error when user starts typing - if (descriptionError) { - setDescriptionError("") - } - } - - return ( - - -