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, | |
postVulnerability, | |
} from '../../../services/vulnerabilities'; | |
import CVSSCalculator from '../components/CVSSCalculator'; | |
import MultiCheckboxButton from '../components/MultiCheckboxButton'; | |
type CWEData = { | |
priority: number; | |
label: string; | |
score: number; | |
}; | |
type CWERelated = { | |
cwe: string; | |
cweParent?: string; | |
cweGrandParent?: string; | |
}; | |
type Details = { | |
locale: string; | |
title: string; | |
vulnType: string; | |
description: string; | |
observation: string; | |
remediation: string; | |
cwes: string[]; | |
references: string[]; | |
customFields: string[]; | |
}; | |
type AddVulnerabilityData = { | |
priority: number | ''; | |
remediationComplexity: number | ''; | |
details: Details[]; | |
category: string | null; | |
}; | |
type ListItem = { | |
id: number; | |
value: string; | |
label?: string; | |
locale?: string; | |
}; | |
type AddVulnerabilityProps = { | |
isOpen: boolean; | |
handlerIsOpen: React.Dispatch<React.SetStateAction<boolean>>; | |
categoryVuln: ListItem | null; | |
currentLang: ListItem | null; | |
languages: ListItem[]; | |
types: ListItem[]; | |
refreshVulns: () => void; | |
handleOnSuccess: (message: string) => void; | |
}; | |
type PostDescription = { | |
vuln: string; | |
}; | |
const AddVulnerability: React.FC<AddVulnerabilityProps> = ({ | |
isOpen, | |
handlerIsOpen, | |
categoryVuln, | |
currentLang, | |
languages, | |
types, | |
refreshVulns, | |
handleOnSuccess, | |
}) => { | |
const [openModal, setOpenModal] = useState(false); | |
const [changed, setChanged] = useState<boolean>(false); | |
const [language, setLanguage] = useState<ListItem>( | |
currentLang ?? languages[0], | |
); | |
const [selectedType, setSelectedType] = useState<ListItem | null>(null); | |
const [typeListFiltered, setTypeListFiltered] = useState<ListItem[]>( | |
types.filter(typeIter => typeIter.locale === language.value), | |
); | |
const [complexity, setComplexity] = useState<ListItem | null>(null); | |
const [priority, setPriority] = useState<ListItem | null>(null); | |
const [cweLoading, setCweLoading] = useState<boolean>(false); | |
const [cweRecommendationSelected, setCweRecommendationSelected] = useState< | |
string[] | |
>([]); | |
const [cweRecommended, setCweRecommended] = useState<CWERelated[]>([]); | |
const [cvssVector, setCvssVector] = useState<string>(''); | |
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 [data, setData] = useState<AddVulnerabilityData>({ | |
remediationComplexity: '', | |
details: [], | |
category: categoryVuln ? categoryVuln.value : '', | |
priority: '', | |
}); | |
const [details, setDetails] = useState<Details[]>([]); | |
const [detail, setDetail] = useState<Details>({ | |
locale: language.value, | |
title: '', | |
vulnType: '', | |
description: '', | |
observation: '', | |
remediation: '', | |
cwes: [], | |
references: [], | |
customFields: [], | |
}); | |
const closeSlidingPage = () => { | |
setOpenModal(false); | |
handlerIsOpen(false); | |
}; | |
const addNewDetail = (localeValue: string) => { | |
const newDetail: Details = { | |
locale: localeValue, | |
title: '', | |
vulnType: '', | |
description: '', | |
observation: '', | |
remediation: '', | |
cwes: [], | |
references: [], | |
customFields: [], | |
}; | |
setDetail(newDetail); | |
}; | |
const handleTypeChange = (item: ListItem) => { | |
if (changed === false) { | |
setChanged(true); | |
} | |
setDetail(prevDetail => ({ | |
...prevDetail, | |
vulnType: item.value, | |
})); | |
setSelectedType(item); | |
}; | |
const handleLanguageChange = (item: ListItem) => { | |
if (changed === false) { | |
setChanged(true); | |
} | |
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); | |
} | |
setLanguage(item); | |
setTypeListFiltered( | |
types.filter(typeIter => typeIter.locale === item.value), | |
); | |
}; | |
const handleDropdownChange = (name: string, item: ListItem) => { | |
if (changed === false) { | |
setChanged(true); | |
} | |
setData(prevData => ({ | |
...prevData, | |
[name]: item.id, | |
})); | |
name === 'priority' ? setPriority(item) : setComplexity(item); | |
}; | |
const handleInputChange = (field: string, value: string) => { | |
if (changed === false) { | |
setChanged(true); | |
} | |
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 handleCvssRecomendation = () => { | |
if (detail.description === '' || detail.description === '<p><br></p>') { | |
toast.error(t('err.descriptionRequired')); | |
return ''; | |
} else { | |
return detail.description; | |
} | |
}; | |
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 handleSubmitNewVulnerability = async () => { | |
if (!details.some(detail => detail.title !== '')) { | |
toast.error(t('err.titleRequired')); | |
setTitleRequiredAlert(true); | |
return; | |
} | |
const newVuln = { | |
cvssv3: cvssVector !== 'CVSS:3.1/' ? cvssVector : '', | |
remediationComplexity: data.remediationComplexity, | |
details: [...details], | |
category: data.category ?? null, | |
priority: data.priority, | |
}; | |
try { | |
const response = await postVulnerability([newVuln]); | |
if (response.status === 'success') { | |
handleOnSuccess(t('msg.vulnerabilityCreatedOk')); | |
} | |
} catch (error) { | |
if ( | |
error instanceof Error && | |
error.message === 'Vulnerability title already exists' | |
) { | |
toast.error(t('vulnerabilityTitleAlreadyExists')); | |
} else { | |
toast.error(t('err.failedUpdateVulnerability')); | |
} | |
console.error('Error:', error); | |
return; | |
} | |
refreshVulns(); | |
handlerIsOpen(!isOpen); | |
}; | |
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('addVulnerability')}</h4> | |
<span className="px-4 rounded-md border-0 text-white"> | |
{categoryVuln ? categoryVuln.label : t('noCategory')} | |
</span> | |
</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 mx-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 mx-4"> | |
<SelectDropdown | |
items={typeListFiltered} | |
onChange={handleTypeChange} | |
placeholder="" | |
selected={selectedType} | |
title={t('type')} | |
/> | |
</div> | |
<div className="w-1/6 mx-4 relative"> | |
<SelectDropdown | |
items={languages} | |
onChange={handleLanguageChange} | |
selected={language} | |
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 | |
handleCvssChange={(newCvssVector: string) => | |
setCvssVector(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={handleSubmitNewVulnerability}> | |
<span>{t('addVulnerability')}</span> | |
</PrimaryButton> | |
</div> | |
</div> | |
</> | |
); | |
}; | |
export default AddVulnerability; | |