diff --git a/frontend/src/components/Markdown/Toolbar.vue b/frontend/src/components/Markdown/Toolbar.vue index 2db1cac28..8e592fae9 100644 --- a/frontend/src/components/Markdown/Toolbar.vue +++ b/frontend/src/components/Markdown/Toolbar.vue @@ -19,7 +19,7 @@ @@ -258,4 +259,8 @@ function emitCreateComment() { text-indent: -10px; margin: 0 0.5em; } + +.finding-reference-list { + max-height: 70vh; +} diff --git a/frontend/src/composables/lockedit.ts b/frontend/src/composables/lockedit.ts new file mode 100644 index 000000000..8c1c7d045 --- /dev/null +++ b/frontend/src/composables/lockedit.ts @@ -0,0 +1,192 @@ +import urlJoin from "url-join"; +import { formatISO9075 } from "date-fns"; +import { levelNameFromLevelNumber } from '@base/utils/cvss'; +import { + ProjectTypeScope, + UploadedFileType, + type MarkdownEditorMode, + type PentestProject, + type ProjectType, + type UploadedFileInfo, +} from "#imports"; + + +export function useProjectTypeLockEdit(options: { + projectType: Ref, + performSave?: (data: ProjectType) => Promise; + performDelete?: (data: ProjectType) => Promise; + hasEditPermissions?: ComputedRef; + errorMessage?: ComputedRef; +}) { + const auth = useAuth(); + const projectType = computed(() => options.projectType.value); + + const baseUrl = computed(() => `/api/v1/projecttypes/${projectType.value.id}/`); + useHeadExtended({ + title: projectType.value.name + }); + + const hasEditPermissions = computed(() => { + if (options.hasEditPermissions && !options.hasEditPermissions.value) { + return false; + } + return (projectType.value.scope === ProjectTypeScope.GLOBAL && auth.permissions.value.designer) || + (projectType.value.scope === ProjectTypeScope.PRIVATE && auth.permissions.value.private_designs) || + (projectType.value.scope === ProjectTypeScope.PROJECT && projectType.value.source === SourceEnum.CUSTOMIZED); + }); + const errorMessage = computed(() => { + if (options.errorMessage?.value) { + return options.errorMessage.value; + } + if (projectType.value.scope === ProjectTypeScope.PROJECT) { + if (projectType.value.source === SourceEnum.SNAPSHOT) { + return `This design cannot be edited because it is a snapshot from ${projectType.value.created.split('T')[0]}.` + } else if (projectType.value.source === SourceEnum.IMPORTED_DEPENDENCY) { + return 'This design cannot be edited because it is an imported snapshot.'; + } else if (projectType.value.source !== SourceEnum.CUSTOMIZED) { + return 'This design is readonly and cannot be edited.'; + } + } + return null; + }) + + const deleteConfirmInput = computed(() => projectType.value.name); + return { + ...useLockEdit({ + performSave: options.performSave, + performDelete: options.performDelete, + deleteConfirmInput, + data: projectType, + baseUrl, + hasEditPermissions, + errorMessage, + }), + projectType, + } +} + +export async function useProjectTypeLockEditOptions(options: {save?: boolean, delete?: boolean, saveFields?: string[], id?: string}) { + const route = useRoute(); + const projectTypeId = options.id || route.params.projectTypeId; + const projectType = await useFetchE(`/api/v1/projecttypes/${projectTypeId}/`, { method: 'GET', deep: true }); + const projectTypeStore = useProjectTypeStore(); + + async function performSave(data: ProjectType) { + await projectTypeStore.partialUpdate(data, options.saveFields); + } + async function performDelete(data: ProjectType) { + await projectTypeStore.delete(data); + await navigateTo('/designs/'); + } + + return { + projectType, + ...(options.save ? { performSave } : {}), + ...(options.delete ? { performDelete } : {}) + }; +} + +export function useProjectEditBase(options: { + project: ComputedRef|Ref, + projectType?: ComputedRef|Ref, + historyDate?: string, + canUploadFiles?: boolean, + spellcheckEnabled?: Ref; + markdownEditorMode?: Ref; +}) { + const route = useRoute(); + const auth = useAuth(); + const localSettings = useLocalSettings(); + const projectStore = useProjectStore(); + + const projectId = computed(() => options.project.value?.id || route.params.projectId); + + const hasEditPermissions = computed(() => { + if (options.historyDate) { + return false; + } else if (options.project.value && !options.project.value.readonly) { + return false; + } else if (!auth.permissions.value.edit_projects) { + return false; + } + return true; + }); + const errorMessage = computed(() => { + if (options.historyDate) { + return `You are comparing a historic version from ${formatISO9075(new Date(options.historyDate))} to the current version.`; + } else if (options.project.value?.readonly) { + return 'This project is finished and cannot be changed anymore. In order to edit this project, re-activate it in the project settings.' + } else if (!auth.permissions.value.edit_projects) { + return 'You do not have permissions to edit this resource.'; + } + return null; + }); + + const projectUrl = computed(() => `/api/v1/pentestprojects/${projectId.value}/`); + const projectTypeUrl = computed(() => options.project.value ? `/api/v1/projecttypes/${options.project.value.project_type}/` : null); + + async function uploadFile(file: File) { + const uploadUrl = urlJoin(projectUrl.value, options.canUploadFiles ? '/upload/' : '/images/'); + const res = await uploadFileHelper(uploadUrl, file); + if (res.resource_type === UploadedFileType.IMAGE) { + return `![](/images/name/${res.name}){width="auto"}`; + } else { + return `[${res.name}](/files/name/${res.name})`; + } + } + function rewriteFileUrl(fileSrc: string) { + if (fileSrc.startsWith('/assets/')) { + return urlJoin(projectTypeUrl.value || '', fileSrc) + } else { + return urlJoin(projectUrl.value, fileSrc); + } + } + + const referenceItems = computed(() => { + return projectStore.findings(options.project.value?.id || '', { projectType: options.projectType?.value }) + .map(f => ({ + id: f.id, + title: f.data.title, + severity: options.projectType?.value ? + levelNameFromLevelNumber(getFindingRiskLevel({ finding: f, projectType: options.projectType?.value }) as any)?.toLowerCase() : + undefined, + })) + }); + function rewriteReferenceLink(refId: string) { + const findingRef = referenceItems.value.find(f => f.id === refId); + if (findingRef) { + return { + href: `/projects/${options.project.value!.id}/reporting/findings/${findingRef.id}/`, + title: `[Finding ${findingRef.title}]`, + }; + } + return null; + } + + const spellcheckEnabled = options.spellcheckEnabled || computed({ + get: () => localSettings.reportingSpellcheckEnabled && !options.historyDate, + set: (val: boolean) => { localSettings.reportingSpellcheckEnabled = val; }, + }); + const markdownEditorMode = options.markdownEditorMode || computed({ + get: () => localSettings.reportingMarkdownEditorMode, + set: (val: MarkdownEditorMode) => { localSettings.reportingMarkdownEditorMode = val; }, + }) + const inputFieldAttrs = computed(() => ({ + lang: options.project.value?.language || 'en-US', + selectableUsers: [...(options.project.value?.members || []), ...(options.project.value?.imported_members || [])], + referenceItems: referenceItems.value, + spellcheckEnabled: spellcheckEnabled.value, + 'onUpdate:spellcheckEnabled': (val: boolean) => { spellcheckEnabled.value = val; }, + markdownEditorMode: markdownEditorMode.value, + 'onUpdate:markdownEditorMode': (val: MarkdownEditorMode) => { markdownEditorMode.value = val; }, + uploadFile, + rewriteFileUrl, + rewriteReferenceLink, + })); + + return { + hasEditPermissions, + errorMessage, + inputFieldAttrs, + } +} diff --git a/packages/nuxt-base-layer/src/composables/lockedit.ts b/packages/nuxt-base-layer/src/composables/lockedit.ts index 5e4892dd8..49f2d00ae 100644 --- a/packages/nuxt-base-layer/src/composables/lockedit.ts +++ b/packages/nuxt-base-layer/src/composables/lockedit.ts @@ -2,17 +2,7 @@ import urlJoin from "url-join"; import { cloneDeep } from "lodash-es"; import { onBeforeRouteLeave, onBeforeRouteUpdate } from "#vue-router"; import type { VForm } from "vuetify/lib/components/index.mjs"; -import { levelNameFromScore } from '@base/utils/cvss'; -import { formatISO9075 } from "date-fns"; -import { - EditMode, - ProjectTypeScope, - UploadedFileType, - type MarkdownEditorMode, - type PentestProject, - type ProjectType, - type UploadedFileInfo, -} from "#imports"; +import { EditMode } from "#imports"; import type { EditToolbar } from "#components"; // @ts-expect-error typeof generic component @@ -165,183 +155,3 @@ export function useLockEdit(options: LockEditOptions) { editMode, }; } - -export function useProjectTypeLockEdit(options: { - projectType: Ref, - performSave?: (data: ProjectType) => Promise; - performDelete?: (data: ProjectType) => Promise; - hasEditPermissions?: ComputedRef; - errorMessage?: ComputedRef; -}) { - const auth = useAuth(); - const projectType = computed(() => options.projectType.value); - - const baseUrl = computed(() => `/api/v1/projecttypes/${projectType.value.id}/`); - useHeadExtended({ - title: projectType.value.name - }); - - const hasEditPermissions = computed(() => { - if (options.hasEditPermissions && !options.hasEditPermissions.value) { - return false; - } - return (projectType.value.scope === ProjectTypeScope.GLOBAL && auth.permissions.value.designer) || - (projectType.value.scope === ProjectTypeScope.PRIVATE && auth.permissions.value.private_designs) || - (projectType.value.scope === ProjectTypeScope.PROJECT && projectType.value.source === SourceEnum.CUSTOMIZED); - }); - const errorMessage = computed(() => { - if (options.errorMessage?.value) { - return options.errorMessage.value; - } - if (projectType.value.scope === ProjectTypeScope.PROJECT) { - if (projectType.value.source === SourceEnum.SNAPSHOT) { - return `This design cannot be edited because it is a snapshot from ${projectType.value.created.split('T')[0]}.` - } else if (projectType.value.source === SourceEnum.IMPORTED_DEPENDENCY) { - return 'This design cannot be edited because it is an imported snapshot.'; - } else if (projectType.value.source !== SourceEnum.CUSTOMIZED) { - return 'This design is readonly and cannot be edited.'; - } - } - return null; - }) - - const deleteConfirmInput = computed(() => projectType.value.name); - return { - ...useLockEdit({ - performSave: options.performSave, - performDelete: options.performDelete, - deleteConfirmInput, - data: projectType, - baseUrl, - hasEditPermissions, - errorMessage, - }), - projectType, - } -} - -export async function useProjectTypeLockEditOptions(options: {save?: boolean, delete?: boolean, saveFields?: string[], id?: string}) { - const route = useRoute(); - const projectTypeId = options.id || route.params.projectTypeId; - const projectType = await useFetchE(`/api/v1/projecttypes/${projectTypeId}/`, { method: 'GET', deep: true }); - const projectTypeStore = useProjectTypeStore(); - - async function performSave(data: ProjectType) { - await projectTypeStore.partialUpdate(data, options.saveFields); - } - async function performDelete(data: ProjectType) { - await projectTypeStore.delete(data); - await navigateTo('/designs/'); - } - - return { - projectType, - ...(options.save ? { performSave } : {}), - ...(options.delete ? { performDelete } : {}) - }; -} - -export function useProjectEditBase(options: { - project: ComputedRef|Ref, - projectType?: ComputedRef|Ref, - historyDate?: string, - canUploadFiles?: boolean, - spellcheckEnabled?: Ref; - markdownEditorMode?: Ref; -}) { - const route = useRoute(); - const auth = useAuth(); - const localSettings = useLocalSettings(); - const projectStore = useProjectStore(); - - const projectId = computed(() => options.project.value?.id || route.params.projectId); - - const hasEditPermissions = computed(() => { - if (options.historyDate) { - return false; - } else if (options.project.value && !options.project.value.readonly) { - return false; - } else if (!auth.permissions.value.edit_projects) { - return false; - } - return true; - }); - const errorMessage = computed(() => { - if (options.historyDate) { - return `You are comparing a historic version from ${formatISO9075(new Date(options.historyDate))} to the current version.`; - } else if (options.project.value?.readonly) { - return 'This project is finished and cannot be changed anymore. In order to edit this project, re-activate it in the project settings.' - } else if (!auth.permissions.value.edit_projects) { - return 'You do not have permissions to edit this resource.'; - } - return null; - }); - - const projectUrl = computed(() => `/api/v1/pentestprojects/${projectId.value}/`); - const projectTypeUrl = computed(() => options.project.value ? `/api/v1/projecttypes/${options.project.value.project_type}/` : null); - - async function uploadFile(file: File) { - const uploadUrl = urlJoin(projectUrl.value, options.canUploadFiles ? '/upload/' : '/images/'); - const res = await uploadFileHelper(uploadUrl, file); - if (res.resource_type === UploadedFileType.IMAGE) { - return `![](/images/name/${res.name}){width="auto"}`; - } else { - return `[${res.name}](/files/name/${res.name})`; - } - } - function rewriteFileUrl(fileSrc: string) { - if (fileSrc.startsWith('/assets/')) { - return urlJoin(projectTypeUrl.value || '', fileSrc) - } else { - return urlJoin(projectUrl.value, fileSrc); - } - } - - const referenceItems = computed(() => { - return projectStore.findings(options.project.value?.id || '', { projectType: options.projectType?.value }) - .map(f => ({ - id: f.id, - title: f.data.title, - severity: options.projectType?.value ? - levelNameFromScore(getFindingRiskLevel({ finding: f, projectType: options.projectType?.value }) as any)?.toLowerCase() : - undefined, - })) - }); - function rewriteReferenceLink(refId: string) { - const findingRef = referenceItems.value.find(f => f.id === refId); - if (findingRef) { - return { - href: `/projects/${options.project.value!.id}/reporting/findings/${findingRef.id}/`, - title: `[Finding ${findingRef.title}]`, - }; - } - return null; - } - - const spellcheckEnabled = options.spellcheckEnabled || computed({ - get: () => localSettings.reportingSpellcheckEnabled && !options.historyDate, - set: (val: boolean) => { localSettings.reportingSpellcheckEnabled = val; }, - }); - const markdownEditorMode = options.markdownEditorMode || computed({ - get: () => localSettings.reportingMarkdownEditorMode, - set: (val: MarkdownEditorMode) => { localSettings.reportingMarkdownEditorMode = val; }, - }) - const inputFieldAttrs = computed(() => ({ - lang: options.project.value?.language || 'en-US', - selectableUsers: [...(options.project.value?.members || []), ...(options.project.value?.imported_members || [])], - referenceItems: referenceItems.value, - spellcheckEnabled: spellcheckEnabled.value, - 'onUpdate:spellcheckEnabled': (val: boolean) => { spellcheckEnabled.value = val; }, - markdownEditorMode: markdownEditorMode.value, - 'onUpdate:markdownEditorMode': (val: MarkdownEditorMode) => { markdownEditorMode.value = val; }, - uploadFile, - rewriteFileUrl, - rewriteReferenceLink, - })); - - return { - hasEditPermissions, - errorMessage, - inputFieldAttrs, - } -} diff --git a/packages/nuxt-base-layer/src/utils/cvss/index.ts b/packages/nuxt-base-layer/src/utils/cvss/index.ts index c47581895..fb9280ca4 100644 --- a/packages/nuxt-base-layer/src/utils/cvss/index.ts +++ b/packages/nuxt-base-layer/src/utils/cvss/index.ts @@ -73,8 +73,13 @@ export function levelNumberFromScore(score?: number|null) { } } +export function levelNameFromLevelNumber(levelNumber?: number|null) { + const levelIndex = Math.min(Math.max((levelNumber || 1) - 1, 0), 4); + return ['Info', 'Low', 'Medium', 'High', 'Critical'][levelIndex]; +} + export function levelNameFromScore(score?: number|null) { - return ['Info', 'Low', 'Medium', 'High', 'Critical'][levelNumberFromScore(score) - 1]; + return levelNameFromLevelNumber(levelNumberFromScore(score) - 1); } export function levelNumberFromLevelName(levelName?: string|null) {