Kaballas's picture
initialize project structure with essential configurations and components
56b6519
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;