Spaces:
Runtime error
Runtime error
import { ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/outline'; | |
import { t } from 'i18next'; | |
import { useEffect, useState } from 'react'; | |
import { toast, Toaster } from 'sonner'; | |
import PrimaryButton from '../../../components/button/PrimaryButton'; | |
import SelectDropdown from '../../../components/dropdown/SelectDropdown'; | |
import SimpleInput from '../../../components/input/SimpleInput'; | |
import Modal from '../../../components/modal/Modal'; | |
import RichText from '../../../components/text/RichText'; | |
import TextArea from '../../../components/text/TextArea'; | |
import GetCWENameByLanguage from '../../../services/getCWEName'; | |
import { | |
postDescriptionCWE, | |
updateVulnerability, | |
} from '../../../services/vulnerabilities'; | |
import CVSSCalculator from '../components/CVSSCalculator'; | |
import MultiCheckboxButton from '../components/MultiCheckboxButton'; | |
import { useVulnState } from '../hooks/useVulnState'; | |
type CWEData = { | |
priority: number; | |
label: string; | |
score: number; | |
}; | |
type Details = { | |
locale: string; | |
title?: string; | |
vulnType?: string; | |
description?: string; | |
observation?: string; | |
remediation?: string; | |
cwes: string[]; | |
references: string[]; | |
customFields: string[]; | |
}; | |
type VulnerabilityData = { | |
_id: string; | |
cvssv3: string | null; | |
priority?: number | ''; | |
remediationComplexity?: number | ''; | |
details: Details[]; | |
status?: number; | |
category?: string | null; | |
__v: number; | |
createdAt?: string; | |
updatedAt?: string; | |
}; | |
type VulnerabilityDataProps = { | |
priority?: number | ''; | |
remediationComplexity?: number | ''; | |
category?: string | null; | |
}; | |
type ListItem = { | |
id: number; | |
value: string; | |
label?: string; | |
locale?: string; | |
}; | |
type ListItemCategory = { | |
id: number; | |
value: string; | |
label?: string; | |
isNull?: boolean; | |
}; | |
type EditVulnerabilityProps = { | |
isOpen: boolean; | |
handlerIsOpen: React.Dispatch<React.SetStateAction<boolean>>; | |
categories: ListItemCategory[]; | |
languages: ListItem[]; | |
types: ListItem[]; | |
refreshVulns: () => void; | |
currentVuln: VulnerabilityData; | |
handleOnSuccess: (message: string) => void; | |
}; | |
type PostDescription = { | |
vuln: string; | |
}; | |
const EditVulnerability: React.FC<EditVulnerabilityProps> = ({ | |
isOpen, | |
handlerIsOpen, | |
categories, | |
languages, | |
types, | |
refreshVulns, | |
currentVuln, | |
handleOnSuccess, | |
}) => { | |
const { | |
openModal, | |
setOpenModal, | |
changed, | |
setChanged, | |
categorySelected, | |
setCategorySelected, | |
categoryChanged, | |
setCategoryChanged, | |
selectedLanguage, | |
setSelectedLanguage, | |
selectedType, | |
setSelectedType, | |
typesFiltered, | |
setTypesFiltered, | |
cweLoading, | |
setCweLoading, | |
cweRecommendationSelected, | |
setCweRecommendationSelected, | |
cweRecommended, | |
setCweRecommended, | |
} = useVulnState({ currentVuln, languages, types }); | |
const [titleRequiredAlert, setTitleRequiredAlert] = useState<boolean>(false); | |
const complexityOptions = [ | |
{ id: 1, value: t('easy') }, | |
{ id: 2, value: t('medium') }, | |
{ id: 3, value: t('complex') }, | |
]; | |
const priorityOptions = [ | |
{ id: 1, value: t('low') }, | |
{ id: 2, value: t('medium') }, | |
{ id: 3, value: t('high') }, | |
{ id: 4, value: t('urgent') }, | |
]; | |
const [complexity, setComplexity] = useState<ListItem | null>( | |
currentVuln.remediationComplexity | |
? complexityOptions[currentVuln.remediationComplexity - 1] | |
: null, | |
); | |
const [priority, setPriority] = useState<ListItem | null>( | |
currentVuln.priority ? priorityOptions[currentVuln.priority - 1] : null, | |
); | |
const [data, setData] = useState<VulnerabilityDataProps>({ | |
remediationComplexity: currentVuln.remediationComplexity ?? '', | |
category: currentVuln.category ?? null, | |
priority: currentVuln.priority ?? '', | |
}); | |
const [details, setDetails] = useState<Details[]>( | |
currentVuln.details.map(detailIter => ({ | |
locale: detailIter.locale, | |
title: detailIter.title ?? '', | |
vulnType: detailIter.vulnType ?? '', | |
description: detailIter.description ?? '', | |
observation: detailIter.observation ?? '', | |
remediation: detailIter.remediation ?? '', | |
cwes: detailIter.cwes, | |
references: detailIter.references, | |
customFields: detailIter.customFields, | |
})), | |
); | |
const [detail, setDetail] = useState<Details>(details[0]); | |
const [cvssStringInitial, setCvssStringInitial] = useState<string>( | |
currentVuln.cvssv3 ?? '', | |
); | |
const closeSlidingPage = () => { | |
setOpenModal(false); | |
handlerIsOpen(false); | |
}; | |
const checkChanged = () => { | |
if (changed === false) { | |
setChanged(true); | |
} | |
}; | |
const addNewDetail = (localeValue: string) => { | |
const newDetail: Details = { | |
locale: localeValue, | |
title: '', | |
vulnType: '', | |
description: '', | |
observation: '', | |
remediation: '', | |
cwes: [], | |
references: [], | |
customFields: [], | |
}; | |
setDetail(newDetail); | |
}; | |
const handlerCategoryChange = (item: ListItem) => { | |
setCategorySelected(item); | |
setCategoryChanged(true); | |
checkChanged(); | |
}; | |
const handleTypeChange = (item: ListItem) => { | |
checkChanged(); | |
setDetail(prevDetail => ({ | |
...prevDetail, | |
vulnType: item.value, | |
})); | |
setSelectedType(item); | |
}; | |
const handleLanguageChange = (item: ListItem) => { | |
checkChanged(); | |
setCweRecommendationSelected([]); | |
setCweRecommended([]); | |
const findLangItem = details.findIndex( | |
detailLocale => detailLocale.locale === item.value, | |
); | |
if (findLangItem !== -1) { | |
setDetail(details[findLangItem]); | |
setSelectedType( | |
types.find( | |
typeIter => typeIter.locale === details[findLangItem].locale, | |
) ?? null, | |
); | |
} else { | |
addNewDetail(item.value); | |
setSelectedType(null); | |
} | |
setSelectedLanguage(item); | |
setTypesFiltered(types.filter(typeIter => typeIter.locale === item.value)); | |
}; | |
const handleDropdownChange = (name: string, item: ListItem) => { | |
checkChanged(); | |
setData(prevData => ({ | |
...prevData, | |
[name]: item.id, | |
})); | |
name === 'priority' ? setPriority(item) : setComplexity(item); | |
}; | |
const handleInputChange = (field: string, value: string) => { | |
checkChanged(); | |
setDetail(prevDetail => ({ | |
...prevDetail, | |
[field]: | |
field === 'cwes' || field === 'references' ? value.split('\n') : value, | |
})); | |
}; | |
const handleCWERecomendation = async () => { | |
if (detail.description === '' || detail.description === '<p><br></p>') { | |
toast.error(t('err.descriptionRequired')); | |
// Cambiar el estado de required | |
return; | |
} | |
const descriptionCWE: PostDescription = { | |
vuln: detail.description ?? '', | |
}; | |
try { | |
setCweLoading(true); | |
setCweRecommendationSelected([]); | |
const responseCWE = await postDescriptionCWE(descriptionCWE); | |
const sortedResult = responseCWE.result.sort( | |
(a: CWEData, b: CWEData) => b.score - a.score, | |
); | |
const recommendedCWEs = GetCWENameByLanguage(detail.locale, sortedResult); | |
setCweRecommended(recommendedCWEs); | |
} catch (error) { | |
console.error('Error:', error); | |
toast.error(t('err.failedCWERecommendation')); | |
} finally { | |
setCweLoading(false); | |
} | |
}; | |
const handlerSubmitCWE = () => { | |
setDetail(prevDetail => ({ | |
...prevDetail, | |
cwes: [...cweRecommendationSelected, ...prevDetail.cwes], | |
})); | |
setCweRecommendationSelected([]); | |
setCweRecommended([]); | |
}; | |
useEffect(() => { | |
const findLang = details.findIndex( | |
detailLocale => detailLocale.locale === detail.locale, | |
); | |
if (findLang !== -1) { | |
const updatedDetails = [...details]; | |
if (JSON.stringify(updatedDetails[findLang]) !== JSON.stringify(detail)) { | |
updatedDetails[findLang] = { ...detail }; | |
setDetails(updatedDetails); | |
} | |
} else { | |
details.push(detail); | |
} | |
}, [detail, details]); | |
const updaterVulnData = () => { | |
const filteredDetails: Details[] = details.map( | |
({ locale, cwes, references, customFields, ...rest }) => { | |
const filteredRest = Object.fromEntries( | |
Object.entries(rest).filter(([_, value]) => value !== ''), | |
); | |
return { | |
locale, | |
...filteredRest, | |
cwes, | |
references, | |
customFields, | |
}; | |
}, | |
); | |
const editvulnData: VulnerabilityData = { | |
_id: currentVuln._id, | |
__v: currentVuln.__v || 0, | |
cvssv3: cvssStringInitial !== 'CVSS:3.1/' ? cvssStringInitial : null, | |
status: currentVuln.status ?? 0, | |
details: [...filteredDetails], | |
...(data.remediationComplexity && { | |
remediationComplexity: data.remediationComplexity, | |
}), | |
...(data.priority && { | |
priority: data.priority, | |
}), | |
createdAt: currentVuln.createdAt ?? new Date().toISOString(), | |
updatedAt: new Date().toISOString(), | |
...(categoryChanged | |
? categorySelected?.isNull === true | |
? { category: null } | |
: { category: categorySelected?.value } | |
: data.category | |
? { category: data.category } | |
: {}), | |
}; | |
return editvulnData; | |
}; | |
const handleSubmitUpdateVulnerability = async () => { | |
if (!details.some(detail => detail.title !== '')) { | |
toast.error(t('err.titleRequired')); | |
setTitleRequiredAlert(true); | |
return; | |
} | |
const editvulnData = updaterVulnData(); | |
try { | |
const response = await updateVulnerability(editvulnData); | |
if (response.status === 'success') { | |
handleOnSuccess(t('msg.vulnerabilityUpdatedOk')); | |
} | |
} catch (error) { | |
if ( | |
error instanceof Error && | |
error.message === 'Vulnerability title already exists' | |
) { | |
toast.error(t('vulnerabilityTitleAlreadyExists')); | |
} else { | |
toast.error(t('failedCreateVulnerability')); | |
} | |
console.error('Error:', error); | |
return; | |
} | |
refreshVulns(); | |
handlerIsOpen(!isOpen); | |
}; | |
const handleCvssRecomendation = () => { | |
if (detail.description === '' || detail.description === '<p><br></p>') { | |
toast.error(t('err.descriptionRequired')); | |
return ''; | |
} else { | |
return detail.description ?? ''; | |
} | |
}; | |
return ( | |
<> | |
<div | |
aria-hidden="true" | |
className={`fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0'}`} | |
onClick={() => (changed ? setOpenModal(true) : closeSlidingPage())} | |
/> | |
<div className="fixed z-10"> | |
<Modal | |
cancelText={t('btn.stay')} | |
disablehr | |
isOpen={openModal} | |
onCancel={() => setOpenModal(false)} | |
onSubmit={closeSlidingPage} | |
submitText={t('btn.confirm')} | |
title={t('msg.doYouWantToLeave')} | |
> | |
<span /> | |
</Modal> | |
</div> | |
<Toaster /> | |
<div | |
className={`fixed top-0 right-0 h-full w-1/2 bg-gray-700 shadow-lg transform transition-transform duration-300 ${ | |
isOpen ? 'translate-x-0 overflow-y-auto' : 'translate-x-full' | |
}`} | |
> | |
<div className="ml-3 mt-2 flex justify-between items-center"> | |
<div className="flex items-center"> | |
<h4 className="text-xl font-bold p-1">{t('editVulnerability')}</h4> | |
<span className="px-4 rounded-md border-0 text-white"> | |
{currentVuln.category ?? t('noCategory')} | |
</span> | |
<span className="w-px h-12 bg-gray-300" /> | |
<div className="mx-2 relative"> | |
<SelectDropdown | |
items={categories} | |
onChange={value => handlerCategoryChange(value)} | |
placeholder={t('changeCategory')} | |
selected={categorySelected} | |
title="" | |
/> | |
</div> | |
</div> | |
<button | |
className="bg-transparent text-white p-2 rounded mx-3" | |
onClick={() => (changed ? setOpenModal(true) : closeSlidingPage())} | |
type="button" | |
> | |
<XMarkIcon className="h-6 w-6" /> | |
</button> | |
</div> | |
<hr className="h-1 my-3 bg-gray-600 border-0 rounded" /> | |
<div className="flex items-center"> | |
<div className="w-2/3 p-4"> | |
<SimpleInput | |
id="title" | |
label={t('title')} | |
name="title" | |
onChange={value => handleInputChange('title', value)} | |
placeholder="" | |
requiredAlert={titleRequiredAlert} | |
requiredField={true} | |
type="text" | |
value={detail.title ?? ''} | |
/> | |
</div> | |
<div className="w-1/6 p-4"> | |
<SelectDropdown | |
items={typesFiltered} | |
onChange={handleTypeChange} | |
placeholder="" | |
selected={selectedType} | |
title={t('type')} | |
/> | |
</div> | |
<div className="w-1/6 p-4 relative"> | |
<SelectDropdown | |
items={languages} | |
onChange={handleLanguageChange} | |
selected={selectedLanguage} | |
title={t('language')} | |
/> | |
</div> | |
</div> | |
<div> | |
<RichText | |
label={t('description')} | |
onChange={value => handleInputChange('description', value)} | |
placeholder="" | |
value={detail.description ?? ''} | |
/> | |
</div> | |
<div> | |
<RichText | |
label={t('observation')} | |
onChange={value => handleInputChange('observation', value)} | |
placeholder="" | |
value={detail.observation ?? ''} | |
/> | |
</div> | |
<div> | |
<RichText | |
label={t('remediation')} | |
onChange={value => handleInputChange('remediation', value)} | |
placeholder="" | |
value={detail.remediation ?? ''} | |
/> | |
</div> | |
<div className="mx-4 flex justify-center"> | |
<CVSSCalculator | |
cvssStringInitial={cvssStringInitial} | |
handleCvssChange={newCvssVector => | |
setCvssStringInitial(newCvssVector) | |
} | |
handleCvssRecomendation={handleCvssRecomendation} | |
/> | |
</div> | |
<div className="flex"> | |
<div className="w-1/2 p-4"> | |
<SelectDropdown | |
items={complexityOptions} | |
onChange={value => | |
handleDropdownChange('remediationComplexity', value) | |
} | |
selected={complexity} | |
title={t('remediationComplexity')} | |
/> | |
</div> | |
<div className="w-1/2 p-4 relative"> | |
<SelectDropdown | |
items={priorityOptions} | |
onChange={value => handleDropdownChange('priority', value)} | |
selected={priority} | |
title={t('remediationPriority')} | |
/> | |
</div> | |
</div> | |
<div className="m-4"> | |
<TextArea | |
id="references" | |
label={t('references')} | |
name="references" | |
onChange={value => handleInputChange('references', value)} | |
placeholder="" | |
rows={4} | |
value={ | |
Array.isArray(detail.references) | |
? detail.references.join('\n') | |
: detail.references | |
} | |
/> | |
</div> | |
<div className="m-4"> | |
<TextArea | |
id="cwes" | |
label="CWEs" | |
name="cwes" | |
onChange={value => handleInputChange('cwes', value)} | |
placeholder="" | |
rows={4} | |
value={ | |
Array.isArray(detail.cwes) ? detail.cwes.join('\n') : detail.cwes | |
} | |
/> | |
</div> | |
<div className="mb-2 mx-4 flex"> | |
<PrimaryButton onClick={() => handleCWERecomendation()}> | |
<span>{t('recommendCwe')}</span> | |
</PrimaryButton> | |
{cweLoading ? ( | |
<span className="ml-2"> | |
<ArrowPathIcon className="h-8 w-8 animate-spin text-blue-500" /> | |
</span> | |
) : null} | |
</div> | |
{cweRecommended.length > 0 ? ( | |
<div className="mx-4 mb-4"> | |
<MultiCheckboxButton | |
cweRecommendationSelected={cweRecommendationSelected} | |
cwesRecommended={cweRecommended} | |
setCweRecommendationSelected={setCweRecommendationSelected} | |
/> | |
{cweRecommendationSelected.length > 0 ? ( | |
<PrimaryButton | |
onClick={() => { | |
handlerSubmitCWE(); | |
}} | |
> | |
{t('btn.confirmSelection')} | |
</PrimaryButton> | |
) : null} | |
</div> | |
) : null} | |
<div className="mb-2 mx-4 flex justify-end"> | |
<PrimaryButton onClick={handleSubmitUpdateVulnerability}> | |
<span>{t('editVulnerability')}</span> | |
</PrimaryButton> | |
</div> | |
</div> | |
</> | |
); | |
}; | |
export default EditVulnerability; | |