From e82409c68c0302019540d620eb6d8a1e1ac096b9 Mon Sep 17 00:00:00 2001 From: Johan Sydseter Date: Thu, 15 Jan 2026 15:31:58 +0100 Subject: [PATCH 1/7] Adding preprendering for urls that aren't listed --- cornucopia.owasp.org/svelte.config.js | 20 +++++++++++++++++++- source/webapp-mappings-3.0.yaml | 6 +++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/cornucopia.owasp.org/svelte.config.js b/cornucopia.owasp.org/svelte.config.js index 297a1c719..ceb3af79b 100644 --- a/cornucopia.owasp.org/svelte.config.js +++ b/cornucopia.owasp.org/svelte.config.js @@ -93,7 +93,25 @@ export default { '/api/cre/webapp/pt_pt', '/api/cre/webapp/pt_br', '/api/cre/webapp/no_nb', - '/api/cre/mobileapp/en' + '/api/cre/mobileapp/en', + '/card/webapp/VE2/2.2/es', + '/card/webapp/VE2/2.2/it', + '/card/webapp/VE2/2.2/nl', + '/card/webapp/VE2/2.2/fr', + '/card/webapp/VE2/2.2/pt_pt', + '/card/webapp/VE2/2.2/pt_br', + '/card/webapp/VE2/2.2/no_nb', + '/card/webapp/VE2/2.2/ru', + '/card/webapp/VE2/3.0', + '/card/webapp/VE2/3.0/en', + '/card/webapp/VE2/3.0/es', + '/card/webapp/VE2/3.0/it', + '/card/webapp/VE2/3.0/nl', + '/card/webapp/VE2/3.0/fr', + '/card/webapp/VE2/3.0/pt_pt', + '/card/webapp/VE2/3.0/pt_br', + '/card/webapp/VE2/3.0/no_nb', + '/card/webapp/VE2/3.0/ru', ] }, csrf: { diff --git a/source/webapp-mappings-3.0.yaml b/source/webapp-mappings-3.0.yaml index 1609d858f..9f07a1ce5 100644 --- a/source/webapp-mappings-3.0.yaml +++ b/source/webapp-mappings-3.0.yaml @@ -1880,12 +1880,12 @@ suits: stride_print: [ 'Information Disclosure' ] owasp_dev_guide: [ SC1, SC2, SC3, SC4, SC5, SC6, SC7, SC8, SC9, SC10, SC11, SC12, SC13, SFL1, SFL2, SFL14, SFL15, SDC2, SDC3, SDC4, SDC5, SDC6, SDA1, PDT1, PDT2, PDT3, PDT4, PDT5, PDT6, PDT7, PDT8, PDT9, PDT10, PDT11 ] owasp_dev_guide_print: [ SC1-13, SFL1-2, SFL14-15, SDC2-6, SDA1, PDT1-11 ] - owasp_asvs: [ 12.1.1, 12.1.2, 12.1.3, 12.1.4, 12.1.5, 12.2.1, 12.2.2, 12.3.1, 12.3.2, 12.3.3, 12.3.4, 12.3.5, 13.2.1, 13.2.2, 13.2.3, 13.3.1, 13.3.2, 13.3.3, 13.3.4, 13.3.5, 13.4.1, 13.4.2, 13.4.3, 13.4.4, 13.4.5, 13.4.6, 13.4.7, 15.1.1, 15.1.2, 15.2.1, 15.2.4, 16.3.3, 16.3.4 ] - owasp_asvs_print: [ 12.1.1-5, 12.2.1-2, 12.3.1-5, 13.2.1-3, 13.3.1-5, 13.4.1-7, 15.1.1-2, 15.2.1, 15.2.4, 16.3.3-4 ] + owasp_asvs: [ 12.1.1, 12.1.2, 12.1.3, 12.1.4, 12.1.5, 12.2.1, 12.2.2, 12.3.1, 12.3.2, 12.3.3, 12.3.4, 12.3.5, 13.2.1, 13.2.2, 13.2.3, 13.3.1, 13.3.2, 13.3.3, 13.3.4, 13.4.1, 13.4.2, 13.4.3, 13.4.4, 13.4.5, 13.4.6, 13.4.7, 15.1.1, 15.1.2, 15.2.1, 15.2.4, 16.3.3, 16.3.4 ] + owasp_asvs_print: [ 12.1.1-5, 12.2.1-2, 12.3.1-5, 13.2.1-3, 13.3.1-4, 13.4.1-7, 15.1.1-2, 15.2.1, 15.2.4, 16.3.3-4 ] capec: [ 37, 121, 159, 169, 217, 220, 310, 446 ] capec_map: 37: - owasp_asvs: [ 13.2.1, 13.2.2, 13.2.3, 13.3.1, 13.3.2, 13.3.3, 13.3.4, 13.3.5, 13.4.1, 13.4.7 ] + owasp_asvs: [ 13.2.1, 13.2.2, 13.2.3, 13.3.1, 13.3.2, 13.3.3, 13.3.4, 13.4.1, 13.4.7 ] 121: owasp_asvs: [ 13.4.2 ] 169: From a2b15aed015ff6b6ada8d1fcc58342159160fd44 Mon Sep 17 00:00:00 2001 From: Johan Sydseter Date: Thu, 15 Jan 2026 16:37:14 +0100 Subject: [PATCH 2/7] Ensuring the mapping id loaded for the new endpoints. --- .../src/domain/mapping/mappingController.ts | 4 +++ .../components/mobileAppCardTaxonomy.svelte | 14 ++++++++-- .../lib/components/webAppCardTaxonomy.svelte | 20 +++++++++++-- .../src/lib/services/deckService.ts | 24 ++++++++++++++-- .../src/routes/card/[edition]/+page.server.ts | 1 - .../card/[edition]/[card]/+page.server.ts | 2 -- .../[card]/[version]/+page.server.ts | 4 --- .../[card]/[version]/[lang]/+page.server.ts | 4 --- cornucopia.owasp.org/svelte.config.js | 2 ++ source/mobileapp-mappings-1.1.yaml | 22 ++++++++++++++- source/webapp-mappings-2.2.yaml | 28 +++++++++++++++++++ 11 files changed, 104 insertions(+), 21 deletions(-) diff --git a/cornucopia.owasp.org/src/domain/mapping/mappingController.ts b/cornucopia.owasp.org/src/domain/mapping/mappingController.ts index 008fe71e7..0083cb8ea 100644 --- a/cornucopia.owasp.org/src/domain/mapping/mappingController.ts +++ b/cornucopia.owasp.org/src/domain/mapping/mappingController.ts @@ -53,6 +53,10 @@ export class MappingController { public getCardMappings(card : string, addition : number = 0) : Mapping { + if (!this.mapping || !this.mapping.suits) { + return {} as Mapping; + } + for(let i = 0 ; i < this.mapping.suits.length ; i++) { for(let j = 0 ; j < this.mapping.suits[i].cards.length ; j++) diff --git a/cornucopia.owasp.org/src/lib/components/mobileAppCardTaxonomy.svelte b/cornucopia.owasp.org/src/lib/components/mobileAppCardTaxonomy.svelte index 735958ccc..5d78cdfcf 100644 --- a/cornucopia.owasp.org/src/lib/components/mobileAppCardTaxonomy.svelte +++ b/cornucopia.owasp.org/src/lib/components/mobileAppCardTaxonomy.svelte @@ -57,25 +57,33 @@ - {#if card.value != 'A' && card.value != 'B'} + {#if mappings }

{$t('cards.mobileAppCardTaxonomy.h1.1')}

+ {#if mappings.owasp_masvs} + {/if} + {#if mappings.owasp_mastg} + {/if} + {#if mappings.capec} + {/if} + {#if mappings.safecode} {/if} + {/if} {#if card.value != 'A' && card.value != 'B'} diff --git a/cornucopia.owasp.org/src/lib/components/webAppCardTaxonomy.svelte b/cornucopia.owasp.org/src/lib/components/webAppCardTaxonomy.svelte index 2cd31998f..041fdf09a 100644 --- a/cornucopia.owasp.org/src/lib/components/webAppCardTaxonomy.svelte +++ b/cornucopia.owasp.org/src/lib/components/webAppCardTaxonomy.svelte @@ -61,6 +61,8 @@ } let mappings: WebAppMapping = $state(controller.getWebAppCardMappings(card.id)); let attacks: Attack[] = $state(GetCardAttacks(card.id)); + + let hasMappings = $derived(mappings && Object.keys(mappings).length > 1); run(() => { mappings = controller.getWebAppCardMappings(card.id); @@ -68,41 +70,53 @@ }); - {#if card.value != 'A' && card.value != 'B'} + {#if hasMappings }

{$t('cards.webAppCardTaxonomy.h1.1')}

+ {#if mappings.stride} + {/if} + {#if mappings.owasp_asvs} + {/if} + {#if mappings.capec} + {/if} + {#if mappings.owasp_dev_guide} + {/if} + {#if mappings.owasp_appsensor} + {/if} + {#if mappings.safecode} "https://safecode.org/publication/SAFECode_Agile_Dev_Security0712.pdf"} /> + {/if} {/if}

ASVS (4.0) Cheat Sheet Series Index

- {#if card.value != 'A' && card.value != 'B'} + {#if card.value != 'A' && card.value != 'B' && mappings.owasp_asvs} +String(s).split('.').slice(0, 2).join('.')))]}> {/if}

{$t('cards.webAppCardTaxonomy.h1.2')}

diff --git a/cornucopia.owasp.org/src/lib/services/deckService.ts b/cornucopia.owasp.org/src/lib/services/deckService.ts index 0c2ffde99..efbdfb740 100644 --- a/cornucopia.owasp.org/src/lib/services/deckService.ts +++ b/cornucopia.owasp.org/src/lib/services/deckService.ts @@ -115,11 +115,29 @@ export class DeckService { { const decks = new Map(); const editions = DeckService.decks; + + // Load all mappings if not already loaded + if (DeckService.mappings.length === 0) { + this.getCardMappingDataAllVersions(); + } + editions.forEach((deck) => { - decks.set( - `${deck.edition}-${deck.version}`, DeckService.mappings.find((mapping) => mapping?.version == deck.version && mapping?.edition == deck.edition)?.data || this.getCardMappingDataAllVersions() - ); + let mappingData = DeckService.mappings.find((mapping) => mapping?.version == deck.version && mapping?.edition == deck.edition)?.data; + // If not found in cache, try to load it + if (!mappingData) { + try { + const yamlData = fs.readFileSync(`${__dirname}${DeckService.path}${DeckService.getEdition(deck.edition)}-mappings-${deck.version}.yaml`, 'utf8'); + mappingData = yaml.load(yamlData); + DeckService.mappings.push({edition: deck.edition, version: deck.version, data: mappingData}); + } catch (e) { + console.error(`Failed to load mapping for ${deck.edition}-${deck.version}:`, e); + } + } + + if (mappingData) { + decks.set(`${deck.edition}-${deck.version}`, mappingData); + } }); return decks; } diff --git a/cornucopia.owasp.org/src/routes/card/[edition]/+page.server.ts b/cornucopia.owasp.org/src/routes/card/[edition]/+page.server.ts index 161953536..3ec4df2e7 100644 --- a/cornucopia.owasp.org/src/routes/card/[edition]/+page.server.ts +++ b/cornucopia.owasp.org/src/routes/card/[edition]/+page.server.ts @@ -3,7 +3,6 @@ import { error } from '@sveltejs/kit'; import { SuitController } from '$domain/suit/suitController'; import { FileSystemHelper } from '$lib/filesystem/fileSystemHelper'; -const editions = ["webapp", "mobileapp"]; export const load = (({ params }) => { const edition = params?.edition; if (!DeckService.hasEdition(edition)) error( diff --git a/cornucopia.owasp.org/src/routes/card/[edition]/[card]/+page.server.ts b/cornucopia.owasp.org/src/routes/card/[edition]/[card]/+page.server.ts index 88b8dd98f..71f85e15e 100644 --- a/cornucopia.owasp.org/src/routes/card/[edition]/[card]/+page.server.ts +++ b/cornucopia.owasp.org/src/routes/card/[edition]/[card]/+page.server.ts @@ -3,8 +3,6 @@ import { error } from '@sveltejs/kit'; import { DeckService } from "$lib/services/deckService"; import type { Route } from "$domain/routes/route"; -const editions = ["webapp", "mobileapp"]; - export const load = (({ params }) => { const edition = params?.edition; const version = edition == 'webapp' ? '2.2' : '1.1'; diff --git a/cornucopia.owasp.org/src/routes/card/[edition]/[card]/[version]/+page.server.ts b/cornucopia.owasp.org/src/routes/card/[edition]/[card]/[version]/+page.server.ts index be5a3b171..f65874b73 100644 --- a/cornucopia.owasp.org/src/routes/card/[edition]/[card]/[version]/+page.server.ts +++ b/cornucopia.owasp.org/src/routes/card/[edition]/[card]/[version]/+page.server.ts @@ -3,10 +3,6 @@ import { error } from '@sveltejs/kit'; import { DeckService } from "$lib/services/deckService"; import type { Route } from "$domain/routes/route"; -const editions = ["webapp", "mobileapp"]; -const languages = ["en", "no_nb", "nl", "es", "pt_pt", "pt_br", "ru", "fr", "it", "hu"]; -const versions = ["3.0", "2.2", "1.0"]; - export const load = (({ params }) => { const edition = params?.edition; const version = params?.version; diff --git a/cornucopia.owasp.org/src/routes/card/[edition]/[card]/[version]/[lang]/+page.server.ts b/cornucopia.owasp.org/src/routes/card/[edition]/[card]/[version]/[lang]/+page.server.ts index 9e88d3e49..282f3c4e2 100644 --- a/cornucopia.owasp.org/src/routes/card/[edition]/[card]/[version]/[lang]/+page.server.ts +++ b/cornucopia.owasp.org/src/routes/card/[edition]/[card]/[version]/[lang]/+page.server.ts @@ -3,10 +3,6 @@ import { DeckService } from "$lib/services/deckService"; import { error } from '@sveltejs/kit'; import type { Route } from "$domain/routes/route"; -const editions = ["webapp", "mobileapp"]; -const languages = ["en", "no_nb", "nl", "es", "pt_pt", "pt_br", "ru", "fr", "it", "hu"]; -const versions = ["3.0", "2.2", "1.0"]; - export const load = (({ params }) => { const edition = params?.edition; const version = params?.version; diff --git a/cornucopia.owasp.org/svelte.config.js b/cornucopia.owasp.org/svelte.config.js index ceb3af79b..ba630f3b9 100644 --- a/cornucopia.owasp.org/svelte.config.js +++ b/cornucopia.owasp.org/svelte.config.js @@ -94,6 +94,8 @@ export default { '/api/cre/webapp/pt_br', '/api/cre/webapp/no_nb', '/api/cre/mobileapp/en', + '/card/mobileapp/PC2/1.1/en', + '/card/mobileapp/PC2/1.1/ru', '/card/webapp/VE2/2.2/es', '/card/webapp/VE2/2.2/it', '/card/webapp/VE2/2.2/nl', diff --git a/source/mobileapp-mappings-1.1.yaml b/source/mobileapp-mappings-1.1.yaml index de423d99b..e0f74ec6f 100644 --- a/source/mobileapp-mappings-1.1.yaml +++ b/source/mobileapp-mappings-1.1.yaml @@ -6,7 +6,7 @@ meta: version: "1.1" layouts: ["cards", "leaflet"] templates: ["bridge_qr", "bridge", "tarot", "tarot_qr"] - languages: ["en"] + languages: ["en", "ru"] suits: - id: "PC" @@ -655,4 +655,24 @@ suits: owasp_masvs: [ "-" ] owasp_mastg: [ "-" ] capec: [ "-" ] + safecode: [ "-" ] +- + id: "WC" + name: "WILD CARD" + cards: + - + id: "JOAM" + value: "A" + url: "https://cornucopia.owasp.org/cards/JOAM" + owasp_masvs: [ "-" ] + owasp_mastg: [ "-" ] + capec: [ "-" ] + safecode: [ "-" ] + - + id: "JOBM" + value: "A" + url: "https://cornucopia.owasp.org/cards/JOBM" + owasp_masvs: [ "-" ] + owasp_mastg: [ "-" ] + capec: [ "-" ] safecode: [ "-" ] \ No newline at end of file diff --git a/source/webapp-mappings-2.2.yaml b/source/webapp-mappings-2.2.yaml index 6dc55c669..0aa82efb1 100644 --- a/source/webapp-mappings-2.2.yaml +++ b/source/webapp-mappings-2.2.yaml @@ -1298,3 +1298,31 @@ suits: owasp_appsensor: [ "-" ] capec: [ "-" ] safecode: [ "-" ] +- + id: "WC" + name: "WILD CARD" + cards: + - + id: "JOA" + value: "A" + url: "https://cornucopia.owasp.org/cards/JOA" + stride: [] + stride_print: [ ] + owasp_dev_guide: [ "-" ] + owasp_dev_guide_print: [ "-" ] + owasp_asvs: [ "-" ] + owasp_asvs_print: [ "-" ] + capec: [ 184, 242, 248, 441, 444, 523, 549, 636, 691 ] + safecode: [ "-" ] + - + id: "JOB" + value: "B" + url: "https://cornucopia.owasp.org/cards/JOB" + stride: [] + stride_print: [ ] + owasp_dev_guide: [ "-" ] + owasp_dev_guide_print: [ "-" ] + owasp_asvs: [ "-" ] + owasp_asvs_print: [ "-" ] + capec: [ 184, 242, 416, 438, 441, 444, 523, 518, 519, 548, 636, 691 ] + safecode: [ "-" ] \ No newline at end of file From 87827b587240893ae3758cecbde166ce18b543a8 Mon Sep 17 00:00:00 2001 From: Johan Sydseter Date: Thu, 15 Jan 2026 16:44:46 +0100 Subject: [PATCH 3/7] Show mapping for jokers as well --- cornucopia.owasp.org/src/lib/components/cardFound.svelte | 4 ++-- .../src/lib/components/cardPreview.svelte | 2 +- .../src/lib/components/mobileAppCardTaxonomy.svelte | 8 ++------ .../src/lib/components/webAppCardTaxonomy.svelte | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/cornucopia.owasp.org/src/lib/components/cardFound.svelte b/cornucopia.owasp.org/src/lib/components/cardFound.svelte index 51f401cd0..e77615a54 100644 --- a/cornucopia.owasp.org/src/lib/components/cardFound.svelte +++ b/cornucopia.owasp.org/src/lib/components/cardFound.svelte @@ -56,10 +56,10 @@ {$t('cards.cardFound.a')} - {#if card.edition == 'webapp' && card.value != 'A' && card.value != 'B'} + {#if card.edition == 'webapp'} {/if} - {#if card.edition == 'mobileapp' && card.value != 'A' && card.value != 'B'} + {#if card.edition == 'mobileapp'} {/if} {#key card} diff --git a/cornucopia.owasp.org/src/lib/components/cardPreview.svelte b/cornucopia.owasp.org/src/lib/components/cardPreview.svelte index 63c347cb5..88f89b324 100644 --- a/cornucopia.owasp.org/src/lib/components/cardPreview.svelte +++ b/cornucopia.owasp.org/src/lib/components/cardPreview.svelte @@ -56,7 +56,7 @@ {#if card?.edition == 'webapp' && card?.value != 'A' && card?.value != 'B'} {/if} - {#if card?.edition == 'mobileapp' && card?.value != 'A' && card?.value != 'B'} + {#if card?.edition == 'mobileapp'} {/if} {:else if card?.suitName == 'WILD CARD'} diff --git a/cornucopia.owasp.org/src/lib/components/mobileAppCardTaxonomy.svelte b/cornucopia.owasp.org/src/lib/components/mobileAppCardTaxonomy.svelte index 5d78cdfcf..114f4050c 100644 --- a/cornucopia.owasp.org/src/lib/components/mobileAppCardTaxonomy.svelte +++ b/cornucopia.owasp.org/src/lib/components/mobileAppCardTaxonomy.svelte @@ -83,15 +83,11 @@ {#if mappings.safecode} {/if} - {/if} - - {#if card.value != 'A' && card.value != 'B'} - - {/if}

{$t('cards.mobileAppCardTaxonomy.h1.2')}

- {#if card.value != 'A' && card.value != 'B'} + {#if attacks } {/if} + {/if} diff --git a/cornucopia.owasp.org/src/lib/components/webAppCardTaxonomy.svelte b/cornucopia.owasp.org/src/lib/components/webAppCardTaxonomy.svelte index 041fdf09a..70270902b 100644 --- a/cornucopia.owasp.org/src/lib/components/webAppCardTaxonomy.svelte +++ b/cornucopia.owasp.org/src/lib/components/webAppCardTaxonomy.svelte @@ -116,7 +116,7 @@ {/if}

ASVS (4.0) Cheat Sheet Series Index

- {#if card.value != 'A' && card.value != 'B' && mappings.owasp_asvs} + {#if hasMappings && mappings.owasp_asvs} +String(s).split('.').slice(0, 2).join('.')))]}> {/if}

{$t('cards.webAppCardTaxonomy.h1.2')}

From ea3e034ec735093f479df1416cae8a86aba16376 Mon Sep 17 00:00:00 2001 From: Uncle Joe <1244005+sydseter@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:20:42 +0100 Subject: [PATCH 4/7] Update svelte.config.js --- cornucopia.owasp.org/svelte.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/cornucopia.owasp.org/svelte.config.js b/cornucopia.owasp.org/svelte.config.js index ba630f3b9..38006d249 100644 --- a/cornucopia.owasp.org/svelte.config.js +++ b/cornucopia.owasp.org/svelte.config.js @@ -95,7 +95,6 @@ export default { '/api/cre/webapp/no_nb', '/api/cre/mobileapp/en', '/card/mobileapp/PC2/1.1/en', - '/card/mobileapp/PC2/1.1/ru', '/card/webapp/VE2/2.2/es', '/card/webapp/VE2/2.2/it', '/card/webapp/VE2/2.2/nl', From fea4ee7d1dcfc11cab3f31771723131b8e39a756 Mon Sep 17 00:00:00 2001 From: Johan Sydseter Date: Fri, 16 Jan 2026 13:18:50 +0100 Subject: [PATCH 5/7] Show mapping for Jokers as well --- cornucopia.owasp.org/src/lib/components/cardPreview.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cornucopia.owasp.org/src/lib/components/cardPreview.svelte b/cornucopia.owasp.org/src/lib/components/cardPreview.svelte index 88f89b324..2641cc08c 100644 --- a/cornucopia.owasp.org/src/lib/components/cardPreview.svelte +++ b/cornucopia.owasp.org/src/lib/components/cardPreview.svelte @@ -53,7 +53,7 @@ {#if mapping} {card?.card ?? card?.value}

{card?.desc}

- {#if card?.edition == 'webapp' && card?.value != 'A' && card?.value != 'B'} + {#if card?.edition == 'webapp'} {/if} {#if card?.edition == 'mobileapp'} From f9cef07d30fb328f4d29b4c3557f4a09a43670a1 Mon Sep 17 00:00:00 2001 From: Johan Sydseter Date: Fri, 16 Jan 2026 14:39:11 +0100 Subject: [PATCH 6/7] fixing the confitg --- scripts/generate_webapp_capec.py | 332 +++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 scripts/generate_webapp_capec.py diff --git a/scripts/generate_webapp_capec.py b/scripts/generate_webapp_capec.py new file mode 100644 index 000000000..e277a8389 --- /dev/null +++ b/scripts/generate_webapp_capec.py @@ -0,0 +1,332 @@ +""" +Script: generate_webapp_capec.py + +Creates a new YAML file that maps CAPEC numbers to their associated OWASP ASVS requirements. +Only includes CAPEC numbers that appear in 'capec' lists, and only includes ASVS requirements +from matching entries in 'capec_map'. + +Behavior / contract +- Input: webapp-mappings-3.0.yaml (contains cards with capec lists and capec_map entries) +- Output: webapp-capec-3.0.yaml (contains only CAPEC to ASVS mapping) + +Process: +1. First collects all CAPEC numbers found in 'capec' lists +2. For each of these numbers: + - Searches through all 'capec_map' entries in the input file + - If the number exists as a key in a capec_map + - Adds that mapping's owasp_asvs strings to our output + +Example input (webapp-mappings-3.0.yaml): + capec: [54, 113] # These numbers will be in output + capec_map: + 54: + owasp_asvs: ["4.3.2"] # Will be under "54" + 113: + owasp_asvs: ["5.1.1"] # Will be under "113" + 116: # Won't be in output (not in capec list) + owasp_asvs: ["7.1.1"] + +Example output (webapp-capec-3.0.yaml): + +CAPEC: + : + owasp_asvs: + - + +Notes / assumptions +- The script tries to be forgiving: `capec_map` values may be dicts, lists of dicts, lists of numbers/strings. +- `owasp_asvs` is expected to be a list of strings; non-list values are ignored with a warning. + +Usage: + python scripts/generate_webapp_capec.py \ + --input source/webapp-mappings-3.0.yaml \ + --output source/webapp-capec-3.0.yaml + +""" +from __future__ import annotations +import argparse +import sys +import logging +from pathlib import Path +from typing import Any, Dict, Iterable, List, Set + +try: + import yaml +except Exception as e: + print("Missing dependency: PyYAML is required. Install with `pip install pyyaml`.") + raise + + +def find_mapping_nodes_with_keys(obj: Any, keys: Iterable[str]): + """Recursively walk `obj` (list/dict) and yield dict nodes that contain ALL keys in `keys`. + + Yields the dict node itself. + """ + needed = set(keys) + + if isinstance(obj, dict): + logging.info(f"Visiting dict node with keys: {list(obj.keys())}") + if needed.issubset(obj.keys()): + logging.info(f"Found mapping node with keys {keys}: {obj}") + yield obj + for v in obj.values(): + yield from find_mapping_nodes_with_keys(v, keys) + elif isinstance(obj, list): + for item in obj: + yield from find_mapping_nodes_with_keys(item, keys) + + +def extract_capec_numbers(capec_map_value: Any) -> List[str]: + """Return a list of CAPEC numbers (as strings) found in capec_map_value. + + Accepts several shapes: + - dict mapping str->list or str->something + - list of numbers/strings or list of dicts + - a plain number/string + """ + result: List[str] = [] + + if capec_map_value is None: + return result + + if isinstance(capec_map_value, dict): + # Keys might be the capec numbers, or values may be lists of numbers + for k, v in capec_map_value.items(): + # If key looks like a number, include it + if isinstance(k, (str, int)) and str(k).strip() != "": + result.append(str(k)) + # If value is a list of numbers/strings, include them + if isinstance(v, (list, tuple)): + for item in v: + if item is None: + continue + if isinstance(item, (str, int)): + s = str(item).strip() + if s: + result.append(s) + elif isinstance(item, dict): + # include dict keys too + for kk in item.keys(): + result.append(str(kk)) + else: + # non-list value might be a single number/string + if isinstance(v, (str, int)) and str(v).strip() != "": + result.append(str(v)) + elif isinstance(capec_map_value, (list, tuple)): + for item in capec_map_value: + if isinstance(item, (str, int)): + s = str(item).strip() + if s: + result.append(s) + elif isinstance(item, dict): + # dict inside list: treat keys as numbers + for kk in item.keys(): + result.append(str(kk)) + elif isinstance(item, (list, tuple)): + # nested list + for nested in item: + if isinstance(nested, (str, int)): + result.append(str(nested)) + elif isinstance(capec_map_value, (str, int)): + s = str(capec_map_value).strip() + if s: + result.append(s) + + # normalize and deduplicate while preserving insertion order + seen: Set[str] = set() + normalized: List[str] = [] + for r in result: + if r not in seen: + seen.add(r) + normalized.append(r) + return normalized + + +def extract_owasp_asvs_list(value: Any) -> List[str]: + """Return a list of strings from owasp_asvs value; if not a list, return empty list. + Non-string items are coerced to strings. + """ + if not isinstance(value, (list, tuple)): + return [] + out: List[str] = [] + for item in value: + if item is None: + continue + out.append(str(item)) + # deduplicate while preserving order + seen: Set[str] = set() + res: List[str] = [] + for s in out: + if s not in seen: + seen.add(s) + res.append(s) + return res + + +def build_capec_mapping(data: Any) -> Dict[str, Set[str]]: + """Walk the loaded YAML and build capec_num -> set(owasp_asvs strings). + + The algorithm: + - For each node containing 'capec', look at its 'capec' list + - For each number in that 'capec' list, look up that number in the node's capec_map + - If found, add those specific owasp_asvs strings to our output mapping + """ + mapping: Dict[str, Set[str]] = {} + logging.info("Building CAPEC mapping from data") + node_count = 0 + + # First collect all CAPEC numbers from 'capec' lists + for node in find_mapping_nodes_with_keys(data, ("capec",)): + node_count += 1 + capec_list = node.get("capec", []) + + if not capec_list: + logging.debug("Skipping node with empty capec list") + continue + + # Convert capec list to set of strings for matching + capec_nums = set(str(x).strip() for x in capec_list if x != "-") + + # Initialize mapping for each CAPEC number + for num in capec_nums: + if num not in mapping: + mapping[num] = set() + logging.debug(f"Added CAPEC number {num} to mapping") + + # Then find corresponding owasp_asvs strings from capec_map entries + logging.info(f"Looking for ASVS requirements for {len(mapping)} CAPEC numbers") + for num in mapping: + logging.debug(f"Searching for CAPEC {num} in capec_map entries") + for node in find_mapping_nodes_with_keys(data, ("capec_map",)): + capec_val = node.get("capec_map", {}) + if not isinstance(capec_val, dict): + logging.warning(f"Found invalid capec_map (not a dict): {capec_val}") + continue + + if num in capec_val: + logging.debug(f"Found CAPEC {num} in capec_map entry") + mapping_entry = capec_val[num] + if not isinstance(mapping_entry, dict): + logging.warning(f"Expected dict for capec_map.{num}, got {type(mapping_entry)}") + continue + + asvs_list = mapping_entry.get("owasp_asvs", []) + if not isinstance(asvs_list, (list, tuple)): + logging.warning(f"Expected list for capec_map.{num}.owasp_asvs, got {type(asvs_list)}") + continue + + # Add all ASVS strings for this CAPEC number + valid_strings = [str(x) for x in asvs_list if x is not None and str(x).strip()] + if valid_strings: + logging.debug(f"Adding {len(valid_strings)} ASVS strings to CAPEC {num}: {valid_strings}") + mapping[num].update(valid_strings) + else: + logging.warning(f"No valid ASVS strings found for CAPEC {num}") + + return mapping + + return mapping + + +def format_output_structure(mapping: Dict[str, Set[str]]) -> Dict[str, Any]: + """Return a YAML-serializable structure matching the requested output shape. + + Output shape: + CAPEC: + : + owasp_asvs: + - + - + : + owasp_asvs: + - + ... + """ + capec_out: Dict[str, Any] = {} + # Sort CAPEC numbers numerically when possible + for num in sorted(mapping, key=lambda x: (int(x) if x.isdigit() else x)): + # Sort the ASVS strings for consistent output + asvs_list = sorted(mapping[num]) if mapping[num] else [] + capec_out[str(num)] = {"owasp_asvs": asvs_list} + return {"CAPEC": capec_out} + +def set_logging() -> None: + logging.basicConfig( + format="%(asctime)s %(filename)s | %(levelname)s | %(funcName)s | %(message)s", + ) + if gen_capec_vars.args.debug: + logging.getLogger().setLevel(logging.DEBUG) + else: + logging.getLogger().setLevel(logging.INFO) + + +class GenCapecVars: + args: argparse.Namespace + + +def parse_arguments(input_args: List[str]) -> argparse.Namespace: + p = argparse.ArgumentParser(description="Generate webapp-capec-3.0.yaml from webapp-mappings-3.0.yaml") + p.add_argument("--input", "-i", default="source/webapp-mappings-3.0.yaml", + help="Input YAML file to scan (default: source/webapp-mappings-3.0.yaml)") + p.add_argument("--output", "-o", default="source/webapp-capec-3.0.yaml", + help="Output YAML file to write (default: source/webapp-capec-3.0.yaml)") + p.add_argument("--force", "-f", action="store_true", help="Overwrite existing output without asking") + p.add_argument( + "-d", + "--debug", + action="store_true", + help="Output additional information to debug script", + ) + try: + args = p.parse_args(input_args) + except argparse.ArgumentError as exc: + # sys.tracebacklimit = 0 + logging.error(exc.message) + sys.exit() + return args + +def main(argv: List[str] | None = None) -> int: + gen_capec_vars.args = parse_arguments(sys.argv[1:]) + set_logging() + + in_path = Path(gen_capec_vars.args.input) + out_path = Path(gen_capec_vars.args.output) + + if not in_path.exists(): + print(f"Input file not found: {in_path}") + return 2 + + if out_path.exists() and not gen_capec_vars.args.force: + print(f"Output file {out_path} already exists. Use --force to overwrite.") + return 3 + + try: + with in_path.open("r", encoding="utf-8") as f: + data = yaml.safe_load(f) + except yaml.YAMLError as e: + logging.error(f"Failed to parse YAML file {in_path}: {e}") + return 1 + except Exception as e: + logging.error(f"Failed to read file {in_path}: {e}") + return 1 + + if not isinstance(data, dict): + logging.error(f"Expected YAML root to be a mapping/dict, got {type(data)}") + return 1 + + mapping = build_capec_mapping(data) + + out_struct = format_output_structure(mapping) + + # write output YAML + out_path.parent.mkdir(parents=True, exist_ok=True) + with out_path.open("w", encoding="utf-8") as f: + yaml.safe_dump(out_struct, f, sort_keys=False, allow_unicode=True) + + print(f"Wrote {out_path} with {len(mapping)} CAPEC entries.") + return 0 + + +if __name__ == "__main__": + gen_capec_vars: GenCapecVars = GenCapecVars() + raise SystemExit(main()) From 80977cf1500dd1d39974c130905e52a2971050f0 Mon Sep 17 00:00:00 2001 From: Johan Sydseter Date: Fri, 16 Jan 2026 14:41:57 +0100 Subject: [PATCH 7/7] remove script --- scripts/generate_webapp_capec.py | 332 ------------------------------- 1 file changed, 332 deletions(-) delete mode 100644 scripts/generate_webapp_capec.py diff --git a/scripts/generate_webapp_capec.py b/scripts/generate_webapp_capec.py deleted file mode 100644 index e277a8389..000000000 --- a/scripts/generate_webapp_capec.py +++ /dev/null @@ -1,332 +0,0 @@ -""" -Script: generate_webapp_capec.py - -Creates a new YAML file that maps CAPEC numbers to their associated OWASP ASVS requirements. -Only includes CAPEC numbers that appear in 'capec' lists, and only includes ASVS requirements -from matching entries in 'capec_map'. - -Behavior / contract -- Input: webapp-mappings-3.0.yaml (contains cards with capec lists and capec_map entries) -- Output: webapp-capec-3.0.yaml (contains only CAPEC to ASVS mapping) - -Process: -1. First collects all CAPEC numbers found in 'capec' lists -2. For each of these numbers: - - Searches through all 'capec_map' entries in the input file - - If the number exists as a key in a capec_map - - Adds that mapping's owasp_asvs strings to our output - -Example input (webapp-mappings-3.0.yaml): - capec: [54, 113] # These numbers will be in output - capec_map: - 54: - owasp_asvs: ["4.3.2"] # Will be under "54" - 113: - owasp_asvs: ["5.1.1"] # Will be under "113" - 116: # Won't be in output (not in capec list) - owasp_asvs: ["7.1.1"] - -Example output (webapp-capec-3.0.yaml): - -CAPEC: - : - owasp_asvs: - - - -Notes / assumptions -- The script tries to be forgiving: `capec_map` values may be dicts, lists of dicts, lists of numbers/strings. -- `owasp_asvs` is expected to be a list of strings; non-list values are ignored with a warning. - -Usage: - python scripts/generate_webapp_capec.py \ - --input source/webapp-mappings-3.0.yaml \ - --output source/webapp-capec-3.0.yaml - -""" -from __future__ import annotations -import argparse -import sys -import logging -from pathlib import Path -from typing import Any, Dict, Iterable, List, Set - -try: - import yaml -except Exception as e: - print("Missing dependency: PyYAML is required. Install with `pip install pyyaml`.") - raise - - -def find_mapping_nodes_with_keys(obj: Any, keys: Iterable[str]): - """Recursively walk `obj` (list/dict) and yield dict nodes that contain ALL keys in `keys`. - - Yields the dict node itself. - """ - needed = set(keys) - - if isinstance(obj, dict): - logging.info(f"Visiting dict node with keys: {list(obj.keys())}") - if needed.issubset(obj.keys()): - logging.info(f"Found mapping node with keys {keys}: {obj}") - yield obj - for v in obj.values(): - yield from find_mapping_nodes_with_keys(v, keys) - elif isinstance(obj, list): - for item in obj: - yield from find_mapping_nodes_with_keys(item, keys) - - -def extract_capec_numbers(capec_map_value: Any) -> List[str]: - """Return a list of CAPEC numbers (as strings) found in capec_map_value. - - Accepts several shapes: - - dict mapping str->list or str->something - - list of numbers/strings or list of dicts - - a plain number/string - """ - result: List[str] = [] - - if capec_map_value is None: - return result - - if isinstance(capec_map_value, dict): - # Keys might be the capec numbers, or values may be lists of numbers - for k, v in capec_map_value.items(): - # If key looks like a number, include it - if isinstance(k, (str, int)) and str(k).strip() != "": - result.append(str(k)) - # If value is a list of numbers/strings, include them - if isinstance(v, (list, tuple)): - for item in v: - if item is None: - continue - if isinstance(item, (str, int)): - s = str(item).strip() - if s: - result.append(s) - elif isinstance(item, dict): - # include dict keys too - for kk in item.keys(): - result.append(str(kk)) - else: - # non-list value might be a single number/string - if isinstance(v, (str, int)) and str(v).strip() != "": - result.append(str(v)) - elif isinstance(capec_map_value, (list, tuple)): - for item in capec_map_value: - if isinstance(item, (str, int)): - s = str(item).strip() - if s: - result.append(s) - elif isinstance(item, dict): - # dict inside list: treat keys as numbers - for kk in item.keys(): - result.append(str(kk)) - elif isinstance(item, (list, tuple)): - # nested list - for nested in item: - if isinstance(nested, (str, int)): - result.append(str(nested)) - elif isinstance(capec_map_value, (str, int)): - s = str(capec_map_value).strip() - if s: - result.append(s) - - # normalize and deduplicate while preserving insertion order - seen: Set[str] = set() - normalized: List[str] = [] - for r in result: - if r not in seen: - seen.add(r) - normalized.append(r) - return normalized - - -def extract_owasp_asvs_list(value: Any) -> List[str]: - """Return a list of strings from owasp_asvs value; if not a list, return empty list. - Non-string items are coerced to strings. - """ - if not isinstance(value, (list, tuple)): - return [] - out: List[str] = [] - for item in value: - if item is None: - continue - out.append(str(item)) - # deduplicate while preserving order - seen: Set[str] = set() - res: List[str] = [] - for s in out: - if s not in seen: - seen.add(s) - res.append(s) - return res - - -def build_capec_mapping(data: Any) -> Dict[str, Set[str]]: - """Walk the loaded YAML and build capec_num -> set(owasp_asvs strings). - - The algorithm: - - For each node containing 'capec', look at its 'capec' list - - For each number in that 'capec' list, look up that number in the node's capec_map - - If found, add those specific owasp_asvs strings to our output mapping - """ - mapping: Dict[str, Set[str]] = {} - logging.info("Building CAPEC mapping from data") - node_count = 0 - - # First collect all CAPEC numbers from 'capec' lists - for node in find_mapping_nodes_with_keys(data, ("capec",)): - node_count += 1 - capec_list = node.get("capec", []) - - if not capec_list: - logging.debug("Skipping node with empty capec list") - continue - - # Convert capec list to set of strings for matching - capec_nums = set(str(x).strip() for x in capec_list if x != "-") - - # Initialize mapping for each CAPEC number - for num in capec_nums: - if num not in mapping: - mapping[num] = set() - logging.debug(f"Added CAPEC number {num} to mapping") - - # Then find corresponding owasp_asvs strings from capec_map entries - logging.info(f"Looking for ASVS requirements for {len(mapping)} CAPEC numbers") - for num in mapping: - logging.debug(f"Searching for CAPEC {num} in capec_map entries") - for node in find_mapping_nodes_with_keys(data, ("capec_map",)): - capec_val = node.get("capec_map", {}) - if not isinstance(capec_val, dict): - logging.warning(f"Found invalid capec_map (not a dict): {capec_val}") - continue - - if num in capec_val: - logging.debug(f"Found CAPEC {num} in capec_map entry") - mapping_entry = capec_val[num] - if not isinstance(mapping_entry, dict): - logging.warning(f"Expected dict for capec_map.{num}, got {type(mapping_entry)}") - continue - - asvs_list = mapping_entry.get("owasp_asvs", []) - if not isinstance(asvs_list, (list, tuple)): - logging.warning(f"Expected list for capec_map.{num}.owasp_asvs, got {type(asvs_list)}") - continue - - # Add all ASVS strings for this CAPEC number - valid_strings = [str(x) for x in asvs_list if x is not None and str(x).strip()] - if valid_strings: - logging.debug(f"Adding {len(valid_strings)} ASVS strings to CAPEC {num}: {valid_strings}") - mapping[num].update(valid_strings) - else: - logging.warning(f"No valid ASVS strings found for CAPEC {num}") - - return mapping - - return mapping - - -def format_output_structure(mapping: Dict[str, Set[str]]) -> Dict[str, Any]: - """Return a YAML-serializable structure matching the requested output shape. - - Output shape: - CAPEC: - : - owasp_asvs: - - - - - : - owasp_asvs: - - - ... - """ - capec_out: Dict[str, Any] = {} - # Sort CAPEC numbers numerically when possible - for num in sorted(mapping, key=lambda x: (int(x) if x.isdigit() else x)): - # Sort the ASVS strings for consistent output - asvs_list = sorted(mapping[num]) if mapping[num] else [] - capec_out[str(num)] = {"owasp_asvs": asvs_list} - return {"CAPEC": capec_out} - -def set_logging() -> None: - logging.basicConfig( - format="%(asctime)s %(filename)s | %(levelname)s | %(funcName)s | %(message)s", - ) - if gen_capec_vars.args.debug: - logging.getLogger().setLevel(logging.DEBUG) - else: - logging.getLogger().setLevel(logging.INFO) - - -class GenCapecVars: - args: argparse.Namespace - - -def parse_arguments(input_args: List[str]) -> argparse.Namespace: - p = argparse.ArgumentParser(description="Generate webapp-capec-3.0.yaml from webapp-mappings-3.0.yaml") - p.add_argument("--input", "-i", default="source/webapp-mappings-3.0.yaml", - help="Input YAML file to scan (default: source/webapp-mappings-3.0.yaml)") - p.add_argument("--output", "-o", default="source/webapp-capec-3.0.yaml", - help="Output YAML file to write (default: source/webapp-capec-3.0.yaml)") - p.add_argument("--force", "-f", action="store_true", help="Overwrite existing output without asking") - p.add_argument( - "-d", - "--debug", - action="store_true", - help="Output additional information to debug script", - ) - try: - args = p.parse_args(input_args) - except argparse.ArgumentError as exc: - # sys.tracebacklimit = 0 - logging.error(exc.message) - sys.exit() - return args - -def main(argv: List[str] | None = None) -> int: - gen_capec_vars.args = parse_arguments(sys.argv[1:]) - set_logging() - - in_path = Path(gen_capec_vars.args.input) - out_path = Path(gen_capec_vars.args.output) - - if not in_path.exists(): - print(f"Input file not found: {in_path}") - return 2 - - if out_path.exists() and not gen_capec_vars.args.force: - print(f"Output file {out_path} already exists. Use --force to overwrite.") - return 3 - - try: - with in_path.open("r", encoding="utf-8") as f: - data = yaml.safe_load(f) - except yaml.YAMLError as e: - logging.error(f"Failed to parse YAML file {in_path}: {e}") - return 1 - except Exception as e: - logging.error(f"Failed to read file {in_path}: {e}") - return 1 - - if not isinstance(data, dict): - logging.error(f"Expected YAML root to be a mapping/dict, got {type(data)}") - return 1 - - mapping = build_capec_mapping(data) - - out_struct = format_output_structure(mapping) - - # write output YAML - out_path.parent.mkdir(parents=True, exist_ok=True) - with out_path.open("w", encoding="utf-8") as f: - yaml.safe_dump(out_struct, f, sort_keys=False, allow_unicode=True) - - print(f"Wrote {out_path} with {len(mapping)} CAPEC entries.") - return 0 - - -if __name__ == "__main__": - gen_capec_vars: GenCapecVars = GenCapecVars() - raise SystemExit(main())