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,
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;