From 745236257d4ee539285695c79d64a5cb7e2424a8 Mon Sep 17 00:00:00 2001 From: C-Valen Date: Tue, 14 Jan 2025 16:51:09 +0100 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20Add=20Zod=20CWE=20M?= =?UTF-8?q?itre=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/zodMitre.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/types/zodMitre.ts diff --git a/src/types/zodMitre.ts b/src/types/zodMitre.ts new file mode 100644 index 000000000..4a192e08a --- /dev/null +++ b/src/types/zodMitre.ts @@ -0,0 +1,4 @@ +export interface ZodCWEMemberType { + id: string; + name: string; +} From 26e68262027bf0292abdaacd74412514d150fb7f Mon Sep 17 00:00:00 2001 From: C-Valen Date: Fri, 24 Jan 2025 12:04:46 +0100 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9B=20Correct=20CWE=20Service=20lo?= =?UTF-8?q?gic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/CweService.ts | 65 +++++++++++++++++++++++++++++--------- src/types/mitreCwe.ts | 3 ++ src/types/zodMitre.ts | 4 --- 3 files changed, 53 insertions(+), 19 deletions(-) delete mode 100644 src/types/zodMitre.ts diff --git a/src/services/CweService.ts b/src/services/CweService.ts index 7433ec200..01d4d3b76 100644 --- a/src/services/CweService.ts +++ b/src/services/CweService.ts @@ -1,7 +1,7 @@ import { osimRuntime } from '@/stores/osimRuntime'; import type { CWEMemberType } from '@/types/mitreCwe'; -const DATA_KEY = 'CWE:API-DATA'; +export const DATA_KEY = 'CWE:API-DATA'; const VERSION_KEY = 'CWE:API-VERSION'; interface CweViews { @@ -15,10 +15,28 @@ interface CweViews { interface CweCategories { Categories: { ID: string; + MappingNotes: { + Usage: string; + }; Name: string; + Relationships: Array; + Status: string; + Summary: string; }[]; } +interface CweWeaknesses { + Weaknesses: { + Description: string; + ID: string; + MappingNotes: { + Usage: string; + }; + Name: string; + Status: string; + }[]; +}; + export async function updateCWEData() { const baseUrl = osimRuntime.value.backends.mitre; try { @@ -56,33 +74,50 @@ async function checkNewVersion(baseUrl: string): Promise<[string, boolean]> { } async function fetchAndCache(baseUrl: string) { - const cweIds = await fetchCweIds(baseUrl); - const cweData = await fetchCweNames(baseUrl, cweIds); - localStorage.setItem(DATA_KEY, JSON.stringify(cweData)); + const cweView = await fetchCweView(baseUrl); + const cweCategories = await fetchCweCategories(baseUrl, cweView); + const cweWeaknesses = await fetchCweWeaknesses(baseUrl, cweCategories); + localStorage.setItem(DATA_KEY, JSON.stringify(cweWeaknesses)); console.debug('✅ CWE API cache updated.'); } -async function fetchCweIds(baseUrl: string) { +async function fetchCweView(baseUrl: string) { // 699 is the id for the "Software Development" CWE view const view = await fetch(`${baseUrl}/cwe/view/699`); if (!view.ok) { throw new Error('Failed to fetch CWE OpenAPI data'); } const cweData: CweViews = await view.json(); - const cweIds = cweData.Views[0].Members.map(member => member.CweID); - return cweIds; + const cweCategoryIds = cweData.Views[0].Members.map(member => member.CweID); + return cweCategoryIds; +} + +async function fetchCweCategories(baseUrl: string, cweCategoryIds: string[]) { + const response = await fetch(`${baseUrl}/cwe/category/${cweCategoryIds.join(',')}`); + if (!response.ok) { + throw new Error('Failed to fetch CWE categories data'); + } + + const cweCategoryData: CweCategories = await response.json(); + const cweWeaknessIds = cweCategoryData.Categories.flatMap(member => + member.Relationships.map(relationship => relationship.CweID), + ); + return cweWeaknessIds; } -async function fetchCweNames(baseUrl: string, cweIds: string[]) { - const detailedResponse = await fetch(`${baseUrl}/cwe/category/${cweIds.join(',')}`); - if (!detailedResponse.ok) { +async function fetchCweWeaknesses(baseUrl: string, cweIds: string[]) { + const response = await fetch(`${baseUrl}/cwe/weakness/${cweIds.join(',')}`); + if (!response.ok) { throw new Error('Failed to fetch detailed CWE data'); } - const detailedData: CweCategories = await detailedResponse.json(); - const customStructure: CWEMemberType[] = detailedData.Categories.map(category => ({ - id: category.ID, - name: category.Name, + const cweWeaknessesData: CweWeaknesses = await response.json(); + const cweWeaknesses: CWEMemberType[] = cweWeaknessesData.Weaknesses.map(weakness => ({ + id: weakness.ID, + name: weakness.Name, + status: weakness.Status, + summary: weakness.Description, + usage: weakness.MappingNotes.Usage, })); - return customStructure; + return cweWeaknesses; } diff --git a/src/types/mitreCwe.ts b/src/types/mitreCwe.ts index 1f958df12..1e8496e6e 100644 --- a/src/types/mitreCwe.ts +++ b/src/types/mitreCwe.ts @@ -1,4 +1,7 @@ export interface CWEMemberType { id: string; name: string; + status: string; + summary: string; + usage: string; } diff --git a/src/types/zodMitre.ts b/src/types/zodMitre.ts deleted file mode 100644 index 4a192e08a..000000000 --- a/src/types/zodMitre.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ZodCWEMemberType { - id: string; - name: string; -} From a71bb84c177876ba9680a84f8e6f62bd106c24b5 Mon Sep 17 00:00:00 2001 From: C-Valen Date: Mon, 13 Jan 2025 12:10:26 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=A8=20Provide=20CWE=20Selector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/main.css | 12 ++ src/components/CweSelector/CweSelector.vue | 191 +++++++++++++++++++++ src/components/FlawForm/FlawForm.vue | 4 +- 3 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/components/CweSelector/CweSelector.vue diff --git a/src/assets/main.css b/src/assets/main.css index 09cf2b54d..666b3ede3 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -76,3 +76,15 @@ table { pointer-events: none; opacity: 0.5; } + +.flex-1 { + flex: 1; +} + +.flex-2 { + flex: 2; +} + +.flex-3 { + flex: 3; +} diff --git a/src/components/CweSelector/CweSelector.vue b/src/components/CweSelector/CweSelector.vue new file mode 100644 index 000000000..a5ce64208 --- /dev/null +++ b/src/components/CweSelector/CweSelector.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/src/components/FlawForm/FlawForm.vue b/src/components/FlawForm/FlawForm.vue index bcb8a7eaa..aca4655b5 100644 --- a/src/components/FlawForm/FlawForm.vue +++ b/src/components/FlawForm/FlawForm.vue @@ -17,6 +17,7 @@ import FlawHistory from '@/components/FlawHistory/FlawHistory.vue'; import FlawContributors from '@/components/FlawContributors/FlawContributors.vue'; import CvssExplainForm from '@/components/CvssExplainForm/CvssExplainForm.vue'; import FlawAffects from '@/components/FlawAffects/FlawAffects.vue'; +import CweSelector from '@/components/CweSelector/CweSelector.vue'; import { useFlawModel } from '@/composables/useFlawModel'; @@ -244,10 +245,9 @@ const createdDate = computed(() => { :allCvss="flaw.cvss_scores" :nistCvss="nvdCvss3String" /> - Date: Tue, 14 Jan 2025 12:30:22 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9C=85=20Update=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/__tests__/CweSelector.spec.ts | 47 +++++++++++++++++++ src/components/__tests__/FlawForm.spec.ts | 5 +- .../__snapshots__/CweSelector.spec.ts.snap | 23 +++++++++ .../__snapshots__/FlawForm.spec.ts.snap | 30 +++++++----- .../__snapshots__/FlawCreateView.spec.ts.snap | 30 +++++++----- 5 files changed, 107 insertions(+), 28 deletions(-) create mode 100644 src/components/__tests__/CweSelector.spec.ts create mode 100644 src/components/__tests__/__snapshots__/CweSelector.spec.ts.snap diff --git a/src/components/__tests__/CweSelector.spec.ts b/src/components/__tests__/CweSelector.spec.ts new file mode 100644 index 000000000..efd71c8ff --- /dev/null +++ b/src/components/__tests__/CweSelector.spec.ts @@ -0,0 +1,47 @@ +import { describe, it, beforeEach } from 'vitest'; +import { flushPromises, type VueWrapper } from '@vue/test-utils'; + +import CweSelector from '@/components/CweSelector/CweSelector.vue'; + +import { mountWithConfig } from '@/__tests__/helpers'; +import { DATA_KEY } from '@/services/CweService'; + +describe('cweSelector.vue', () => { + let wrapper: VueWrapper; + + beforeEach(() => { + wrapper = mountWithConfig(CweSelector); + vi.useFakeTimers(); + }); + + it('renders correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('loads CWE data on component mount', async () => { + const data = JSON.stringify([{ id: '123', name: 'Test CWE', status: 'Draft', summary: '', usage: '' }]); + localStorage.setItem(DATA_KEY, data); + wrapper = mountWithConfig(CweSelector); + expect(wrapper.vm.cweData).toEqual([{ id: '123', name: 'Test CWE', status: 'Draft', summary: '', usage: '' }]); + }); + + it('filters suggestions correctly and updates model on suggestion click', async () => { + wrapper.vm.cweData = [ + { id: '123', name: 'Test CWE', status: 'Draft', summary: '', usage: '' }, + { id: '456', name: 'Another CWE', status: 'Draft', summary: '', usage: '' }, + ]; + const input = wrapper.find('input'); + await input.setValue('123'); + + vi.runAllTimers(); + await flushPromises(); + expect(wrapper.text()).toContain('CWE-123'); + + const suggestionRow = wrapper.findAll('.dropdown-menu .item'); + await suggestionRow[0].trigger('click'); + expect(wrapper.vm.modelValue).toBe('CWE-123'); + expect(wrapper.vm.queryRef).toBe('CWE-123'); + expect(wrapper.vm.suggestions).toEqual([]); + }); +}); diff --git a/src/components/__tests__/FlawForm.spec.ts b/src/components/__tests__/FlawForm.spec.ts index d08cc2b2d..1307d298b 100644 --- a/src/components/__tests__/FlawForm.spec.ts +++ b/src/components/__tests__/FlawForm.spec.ts @@ -8,6 +8,7 @@ import FlawForm from '@/components/FlawForm/FlawForm.vue'; import CvssCalculator from '@/components/CvssCalculator/CvssCalculator.vue'; import FlawFormOwner from '@/components/FlawFormOwner/FlawFormOwner.vue'; import IssueFieldEmbargo from '@/components/IssueFieldEmbargo/IssueFieldEmbargo.vue'; +import CweSelector from '@/components/CweSelector/CweSelector.vue'; import { blankFlaw } from '@/composables/useFlaw'; @@ -106,7 +107,7 @@ describe('flawForm', () => { expect(nvdCvssField?.exists()).toBe(true); const cweIdField = subject - .findAllComponents(LabelEditable) + .findAllComponents(CweSelector) .find(component => component.props().label === 'CWE ID'); expect(cweIdField?.exists()).toBe(true); @@ -187,7 +188,7 @@ describe('flawForm', () => { expect(nvdCvssField?.exists()).toBe(true); const cweIdField = subject - .findAllComponents(LabelEditable) + .findAllComponents(CweSelector) .find(component => component.props().label === 'CWE ID'); expect(cweIdField?.exists()).toBe(true); diff --git a/src/components/__tests__/__snapshots__/CweSelector.spec.ts.snap b/src/components/__tests__/__snapshots__/CweSelector.spec.ts.snap new file mode 100644 index 000000000..94a45f4f4 --- /dev/null +++ b/src/components/__tests__/__snapshots__/CweSelector.spec.ts.snap @@ -0,0 +1,23 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`cweSelector.vue > renders correctly 1`] = ` +"
+
+
+ +
+ + +
+ +
+
+ + +
+
+
+
" +`; diff --git a/src/components/__tests__/__snapshots__/FlawForm.spec.ts.snap b/src/components/__tests__/__snapshots__/FlawForm.spec.ts.snap index 05e418793..ba7479c70 100644 --- a/src/components/__tests__/__snapshots__/FlawForm.spec.ts.snap +++ b/src/components/__tests__/__snapshots__/FlawForm.spec.ts.snap @@ -196,22 +196,26 @@ exports[`flawForm > mounts and renders 1`] = ` -