Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions cornucopia.owasp.org/data/website/pages/play/en/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,40 @@ Long-time project contributor Max Alejandro Gómez Sánchez Vergaray has created
src="https://www.youtube.com/embed/vLYzId7-ijI?si=yh4vHK7VfO9a5l6s" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen >
<p>You cannot view this video directly because iframes are disabled. Click <a href="https://www.youtube.com/watch?v=vLYzId7-ijI" title="How to play OWASP Cornucopia" target="_blank" rel="noopener">here</a> to watch the video on YouTube.</p></iframe>

## How to play EoP

Draw a diagram of the system you want to threat model before you deal the cards.

Deal the deck to 3-6 players. Play starts with the 3 of Tampering. Play clockwise, and each player in turn continues using the suit if they have a card in that suit. If the player doesn't have a card from that suit, the player can use another suit. Each round is won by the highest card played in the suit that was led, unless an Elevation of Privilege (EoP) card is played. In that case the high value EoP card wins.

To play a card, read the card, announce your threat and record it. If the player can't link the threat to the system, play proceeds.

The winner of a hand selects the card (and suit) to lead the next hand. Take a few minutes between hands to think about threats.

### Points

- 1 for a threat on your card
- +1 for taking the trick

Threats should be articulated clearly, testable, and addressable. In the event that a threat leads to an argument, you can resolve it by asking the question: “Would we take an actionable bug, feature request or design change for that?” If the answer is yes, it is a real threat. (This doesn't mean that threats outside of that aren't real, it's simply a way to focus discussion on actionable threats.) Questions that start with “There's a way” should be read as “There's a way … and here's how …” while questions that start with “Your code” should be read “The code we're collectively creating … and here's how.”

The deck contains a number of special cards: trumps and open threats. EoP cards are trumps: they take the trick even if they have a lower value than the suit that was led. The ace of each suit is an open threat card. When played, the player must identify a threat not listed on another card.

When all the cards have been played, whoever has the most points wins.

Remember to have fun!

### Optional variants

- You may pass cards after the third trick. This is helpful if you have cards that you can't tie to the system. Someone else may be able to.
- Double the number of points, and give one point for threats on other people's cards.
- Other players may “riff” on the threat and if they do, they get one point per additional threat.
- Limit riffing to no more than 60 seconds.
- Mark up the diagram where the threat occurs.
- Questions are listed on the threat cards to help with the aces.

Thanks to Laurie Williams for inspiration.

## Alternative game rules

If you are new to the game, remove the two Joker cards to begin with. Add the Joker cards back in once people become more familiar with the process. Apart from the “trumps card game” rules described above which are very similar to the EoP, the deck can also be played as the “twenty-one card game” (also known as “pontoon” or “blackjack”) which normally reduces the number of cards played in each round.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Cornucopia is developed, maintained, updated and promoted by a worldwide team of
- Kéren A. Saint-Hilaire
- Max Alejandro Gómez Sánchez Vergaray
- Tao Sauvage
- Swaraj Singh
- Riccardo Sirigu
- Prasun Srivastav
- Aditya Srivastava
Expand Down
4 changes: 4 additions & 0 deletions cornucopia.owasp.org/decks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ decks:
descriptionHeadingKey: cards.h2.1
descriptionBodyKey: cards.p2
taxonomyTranslationKey: cards.mappings
howToPlayLink: /how-to-play

versions:
- version: "2.2"
Expand All @@ -37,6 +38,7 @@ decks:
descriptionHeadingKey: cards.h2.2
descriptionBodyKey: cards.p3
taxonomyTranslationKey: cards.mappings
howToPlayLink: /how-to-play
versions:
- version: "1.1"
- version: "2.0"
Expand All @@ -56,6 +58,7 @@ decks:
descriptionHeadingKey: cards.h2.3
descriptionBodyKey: cards.p4
taxonomyTranslationKey: cards.mappings
howToPlayLink: /how-to-play
versions:
- version: "1.0"

Expand All @@ -70,6 +73,7 @@ decks:
descriptionHeadingKey: cards.h2.4
descriptionBodyKey: cards.p5
taxonomyTranslationKey: cards.mappings
howToPlayLink: /how-to-play#How-to-play-EoP
versions:
- version: "5.0"

Expand Down
6 changes: 4 additions & 2 deletions cornucopia.owasp.org/src/lib/components/cardFound.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
cardImages?: Record<string, Record<string, CardImage>>;
suitStyling?: Record<string, Record<string, SuitStyling>>;
asvsVersion?: string;
howToPlayLink: string;
}

let {
Expand All @@ -38,7 +39,8 @@
capecData = undefined,
cardImages = undefined,
suitStyling = undefined,
asvsVersion
asvsVersion,
howToPlayLink
}: Props = $props();

const controller = $derived(new MappingController(mappingData));
Expand Down Expand Up @@ -75,7 +77,7 @@
>
🔗 {$t('cards.cardFound.copy')}
</button>
<a title="How to play OWASP Cornucopia" class="link" href="/how-to-play">{$t('cards.cardFound.a')}</a>
<a title="How to play OWASP Cornucopia" class="link" href={howToPlayLink}>{$t('cards.cardFound.a')}</a>
<Concept card={card}></Concept>
<Explanation card={card}></Explanation>
<Taxonomy
Expand Down
37 changes: 32 additions & 5 deletions cornucopia.owasp.org/src/lib/services/deckConfigService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,26 @@ decks:
buttonLabelKey: cards.button.1
descriptionHeadingKey: cards.h2.1
descriptionBodyKey: cards.p2
howToPlayLink: /how-to-play
versions:
- version: "2.2"
draftLanguages: [hu]
- version: "3.0"

- edition: eop
displayName: "Elevation of Privilege"
fullName: "Elevation of Privilege Edition"
cre:
name: "Elevation of Privilege Edition"
category: "Elevation of Privilege"
defaultPreviewCard: SP2
buttonLabelKey: cards.button.4
descriptionHeadingKey: cards.h2.4
descriptionBodyKey: cards.p5
howToPlayLink: /how-to-play#How-to-play-EoP
versions:
- version: "5.0"

- edition: dbd
displayName: "Cornucopia"
fullName: "Digital Benefits Deck Edition"
Expand Down Expand Up @@ -175,14 +190,15 @@ decks:
expect(() => DeckConfigService.getDeckConfigs()).not.toThrow();
});

it.each(['defaultPreviewCard', 'buttonLabelKey', 'descriptionHeadingKey', 'descriptionBodyKey'] as const)(
it.each(['defaultPreviewCard', 'buttonLabelKey', 'descriptionHeadingKey', 'descriptionBodyKey', 'howToPlayLink'] as const)(
'should throw when a non-external deck is missing "%s"',
(missingField) => {
const renderFields: Record<string, string> = {
defaultPreviewCard: 'VE2',
buttonLabelKey: 'cards.button.1',
descriptionHeadingKey: 'cards.h2.1',
descriptionBodyKey: 'cards.p2'
descriptionBodyKey: 'cards.p2',
howToPlayLink: '/how-to-play'
};
delete renderFields[missingField];
const renderFieldsYaml = Object.entries(renderFields)
Expand Down Expand Up @@ -218,6 +234,7 @@ decks:
buttonLabelKey: cards.button.1
descriptionHeadingKey: cards.h2.1
descriptionBodyKey: cards.p2
howToPlayLink: /how-to-play
versions:
- version: "3.0"

Expand Down Expand Up @@ -246,8 +263,8 @@ decks:
describe('getDeckConfigs', () => {
it('should parse every deck entry from decks.yaml', () => {
const configs = DeckConfigService.getDeckConfigs();
expect(configs).toHaveLength(2);
expect(configs.map((c) => c.edition)).toEqual(['webapp', 'dbd']);
expect(configs).toHaveLength(3);
expect(configs.map((c) => c.edition)).toEqual(['webapp', 'eop', 'dbd']);
});

it('should only read the file once and cache the result', () => {
Expand All @@ -270,7 +287,7 @@ decks:
describe('getBrowsableDecks', () => {
it('should exclude decks marked external', () => {
const editions = DeckConfigService.getBrowsableDecks().map((d) => d.edition);
expect(editions).toEqual(['webapp']);
expect(editions).toEqual(['webapp', 'eop']);
});
});

Expand Down Expand Up @@ -338,6 +355,16 @@ decks:
});
});

describe('getHowToPlayLink', () => {
it('should return the configured link for an edition', () => {
expect(DeckConfigService.getHowToPlayLink('eop')).toBe('/how-to-play#How-to-play-EoP');
});

it('should fall back to the general instructions for an unknown edition', () => {
expect(DeckConfigService.getHowToPlayLink('unknown')).toBe('/how-to-play');
});
});

describe('getAsvsVersion', () => {
it('should look up the version from standards.asvs.versionMap', () => {
expect(DeckConfigService.getAsvsVersion('webapp', '2.2')).toBe('4.0.3');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface DeckConfig {
descriptionHeadingKey?: string;
descriptionBodyKey?: string;
taxonomyTranslationKey?: string;
howToPlayLink?: string;

versions: DeckVersionConfig[];
}
Expand Down Expand Up @@ -59,7 +60,7 @@ function collectDeckConfigErrors(deck: Partial<DeckConfig>, index: number): stri
}

if (!deck.external) {
(['defaultPreviewCard', 'buttonLabelKey', 'descriptionHeadingKey', 'descriptionBodyKey'] as const).forEach((field) => {
(['defaultPreviewCard', 'buttonLabelKey', 'descriptionHeadingKey', 'descriptionBodyKey', 'howToPlayLink'] as const).forEach((field) => {
if (typeof deck[field] !== 'string' || !deck[field]) {
errors.push(`${errorLabel} is not external but missing "${field}"`);
}
Expand Down Expand Up @@ -123,6 +124,10 @@ export class DeckConfigService {
return DeckConfigService.getDeckConfig(edition)?.fullName ?? edition;
}

public static getHowToPlayLink(edition: string): string {
return DeckConfigService.getDeckConfig(edition)?.howToPlayLink ?? '/how-to-play';
}

public static getCreCategory(edition: string): string | undefined {
return DeckConfigService.getDeckConfig(edition)?.cre.category;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export const load = (async ({ params }) => {
capecData,
cardImages: getCardImagesByEdition(),
suitStyling: getSuitStylingByEdition(),
editionName: DeckConfigService.getFullName(edition)
editionName: DeckConfigService.getFullName(edition),
howToPlayLink: DeckConfigService.getHowToPlayLink(edition)
};
}) satisfies PageServerLoad;
function legacyCardCodeFix(card: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
cardImages={data.cardImages}
suitStyling={data.suitStyling}
asvsVersion={data.asvsVersion}
howToPlayLink={data.howToPlayLink}
/>

{:else}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@ export const load = (({ params }) => {
capecData,
cardImages: getCardImagesByEdition(),
suitStyling: getSuitStylingByEdition(),
editionName: DeckConfigService.getFullName(edition)
editionName: DeckConfigService.getFullName(edition),
howToPlayLink: DeckConfigService.getHowToPlayLink(edition)
};

function legacyCardCodeFix(card: string) {
return card.replace('COM', 'CM').replace('CO', 'C').replace('DVE', 'VE').replace('AC', 'AT');
}

}) satisfies PageServerLoad;
}) satisfies PageServerLoad;
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{#if data.metadata}<Metadata metadata={data.metadata} />{/if}
<div>
{#if cardFound()}
<CardFound routes={data.routes} {cards} {card} {versions} mappingData={data.mappingData.get(card.edition)} {languages} {language} capecData={data.capecData} cardImages={data.cardImages} suitStyling={data.suitStyling} asvsVersion={data.asvsVersion} />
<CardFound routes={data.routes} {cards} {card} {versions} mappingData={data.mappingData.get(card.edition)} {languages} {language} capecData={data.capecData} cardImages={data.cardImages} suitStyling={data.suitStyling} asvsVersion={data.asvsVersion} howToPlayLink={data.howToPlayLink} />
{:else}
<CardNotFound card={data.card} />
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ export const load = (({ params }) => {
capecData,
cardImages: getCardImagesByEdition(),
suitStyling: getSuitStylingByEdition(),
editionName: DeckConfigService.getFullName(edition)
editionName: DeckConfigService.getFullName(edition),
howToPlayLink: DeckConfigService.getHowToPlayLink(edition)
};

function legacyCardCodeFix(card: string) {
return card.replace('COM', 'CM').replace('CO', 'C').replace('DVE', 'VE').replace('AC', 'AT');
}

}) satisfies PageServerLoad;
}) satisfies PageServerLoad;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
{#if data.metadata}<Metadata metadata={data.metadata} />{/if}
<div>
{#if cardFound()}
<CardFound routes={data.routes} {cards} {card} {versions} mappingData={data.mappingData.get(card.edition)} {languages} {language} capecData={data.capecData} cardImages={data.cardImages} suitStyling={data.suitStyling} asvsVersion={data.asvsVersion} />
<CardFound routes={data.routes} {cards} {card} {versions} mappingData={data.mappingData.get(card.edition)} {languages} {language} capecData={data.capecData} cardImages={data.cardImages} suitStyling={data.suitStyling} asvsVersion={data.asvsVersion} howToPlayLink={data.howToPlayLink} />
{:else}
<CardNotFound card={data.card} />
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ export const load = (({ params }) => {
capecData,
cardImages: getCardImagesByEdition(),
suitStyling: getSuitStylingByEdition(),
editionName: DeckConfigService.getFullName(edition)
editionName: DeckConfigService.getFullName(edition),
howToPlayLink: DeckConfigService.getHowToPlayLink(edition)
};

function legacyCardCodeFix(card: string) {
return card.replace('COM', 'CM').replace('CO', 'C').replace('DVE', 'VE').replace('AC', 'AT');
}

});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
{#if data.metadata}<Metadata metadata={data.metadata} />{/if}
<div>
{#if cardFound()}
<CardFound routes={data.routes} {cards} {card} {versions} mappingData={data.mappingData.get(card.edition)} {languages} {language} capecData={data.capecData} cardImages={data.cardImages} suitStyling={data.suitStyling} asvsVersion={data.asvsVersion} />
<CardFound routes={data.routes} {cards} {card} {versions} mappingData={data.mappingData.get(card.edition)} {languages} {language} capecData={data.capecData} cardImages={data.cardImages} suitStyling={data.suitStyling} asvsVersion={data.asvsVersion} howToPlayLink={data.howToPlayLink} />
{:else}
<CardNotFound card={data.card} />
{/if}
Expand Down
Loading