In short

A security requirement states an outcome that a test or a review can check, such as "a user can read only invoices in their own tenant", together with its source and an ASVS 5.0 requirement ID. Choose an ASVS level for the application, take the chapters the feature touches, add requirements from abuse cases and the threat model, and write each one as acceptance criteria that include the case that must fail. A short security Definition of Done then checks that every requirement in scope has a passing test or recorded evidence before the story closes.

  • Do state the security objective and leave the mechanism to the design: "the tenant comes from the session" is a requirement, "use middleware X" is not.
  • Do pick an ASVS 5.0 level per application (most should aim for level 2) and cite requirements with the version, for example v5.0.0-8.2.2.
  • Do add requirements from abuse cases, the threat model and past findings; a checklist alone misses the feature's own logic.
  • Do write acceptance criteria with the negative case and an observable result, then automate them so removing the control fails the build.
  • Don't let a story close on "security reviewed": the Definition of Done should ask for a traced test or dated evidence per requirement.

Verify it

  1. Pick three recently closed stories that changed authentication, authorization or data access and check that each lists its security requirements with ASVS IDs and links a passing test or dated evidence.
  2. On a branch, remove one control (for example the tenant filter) and confirm that its requirement test fails and the trace check blocks the merge.
  3. Search the backlog for requirements such as "must be secure" or "OWASP Top 10 compliant" and rewrite each as an observable outcome.
  4. Check that every application has a recorded ASVS level and that the level is reviewed when the data it handles changes.
  5. Confirm that every manual requirement names its evidence and that the evidence exists for the last release.
Applies to
  • OWASP ASVS 5.0.0 (May 2025)
  • OWASP SAMM 2.2.0, Security Requirements practice
  • NIST SSDF 1.1 (PO.1, PW.1)
  • Examples Python 3.14, Flask 3.1.3, pytest 9.1.1, PyYAML 6.0.3 (tested)

Product teams writing stories and acceptance criteria, and the AppSec engineers who help them. The examples use Python, Flask and pytest; the method works with any stack and test framework.

Part of
Workflow: Review a design
Learning goals and prerequisitesAbout 38 minutes: 10 to read, 6 on the code and 22 for the exercise

What you will learn

  • Rewrite vague security statements as testable requirements with a source and an ASVS 5.0 ID.
  • Choose an ASVS level for an application and select the requirements one feature needs.
  • Turn requirements into acceptance criteria and tests that fail when the control is removed.
  • Apply a security Definition of Done and a trace check before a story closes.

Before you start

Product owners, developers, testers and AppSec engineers who write and accept stories

  • Know how your team writes stories and acceptance criteria.
  • Be able to read a short Python test.
  • For the code: Python 3.11 or later with Flask, pytest and PyYAML.

Refresh first: Choose a threat modeling method and turn its findings into tests

Go to the guided practice

What makes a security requirement testable?

A requirement is testable when someone other than its author can decide, from the running system or its records, whether it holds. OWASP SAMM's first Security Requirements activity puts it this way: a requirement states the objective, such as protecting personal data in transit, and not the measure that achieves it. It also warns against requirements so general that they add nothing, such as "protect against the OWASP Top 10".

Write each requirement as one sentence with a subject, an outcome and a boundary. Give it an ID, a source (ASVS, an abuse case, a threat, a law or a past finding) and a verification method: an automated test, a review or a manual test with evidence. If you cannot say how it will be verified, it is not finished.

What makes a security requirement testable?
VagueTestable rewriteVerified by
Invoices must be secure.A user can read an invoice only if it belongs to their tenant; any other ID returns the same 404 as a missing invoice.API test with two tenants
Use strong passwords.New passwords shorter than 15 characters, the length ASVS strongly recommends, are rejected with a message that states the minimum (v5.0.0-6.2.1 requires at least 8).Unit test of the policy
Prevent brute force.After 10 failed sign-ins for one account in 15 minutes, further attempts are delayed and the owner is notified.Integration test and log check
Log security events.Every denied read is logged with user, resource and UTC time, without the record contents (v5.0.0-16.3.2).Test that inspects the log
Follow secure coding standards.Not a requirement: move it to the Definition of Done as "no new SAST finding at blocking severity".CI gate

Where do security requirements come from?

Use four sources and record which one produced each requirement, so a reviewer can see why it exists. SAMM's second-level activity asks for the same breadth: policies and legislation, known problems in the application, and feedback from metrics and incidents, written in the notation the team already uses for other requirements.

Which ASVS level should an application target?

ASVS 5.0.0 has three levels. Level 1 holds about 20% of the requirements and is meant as a low-barrier starting point. Level 2 adds about half of the requirements, so complying with it means meeting about 70% of the standard, and ASVS says most applications should strive for it. Level 3 adds the final 30% for applications that must show the highest assurance. ASVS leaves the choice to the organization's risk analysis; its own example is a startup holding little sensitive data starting at level 1, and a bank struggling to justify less than level 3 for online banking.

Record the level per application, not per team, and revisit it when the data or exposure changes. ASVS encourages organizations to fork the standard and drop sections that do not apply, such as GraphQL or WebSockets, as long as an ID keeps its meaning. A fork should start from level 1 and add requirements by risk.

Which ASVS level should an application target?
ApplicationStarting levelWhy
Internal tool, no personal data, behind SSOLevel 1Low impact; level 1 covers the first layer of defense
Customer-facing SaaS with tenant dataLevel 2Tenant isolation, MFA and logging requirements sit at level 2
Payments, health records or admin consoles for many tenantsLevel 2 plus selected level 3Add level 3 items such as multi-user approval for high-value flows (v5.0.0-2.3.5)
Identity provider or banking coreLevel 3A compromise affects every user and relying party

How do you pick ASVS requirements for one feature?

Start from what the feature does, not from the chapter list. Map each capability to the chapters that govern it, then keep only the requirements at or below the application's level. Most stories touch two to four chapters. Cite every requirement with the version prefix, because ASVS says IDs may change between versions and an unversioned ID refers to the latest release.

Prefer requirements that state a verifiable behavior, such as v5.0.0-8.2.2 (data-specific access restricted to consumers with explicit permission), over documentation requirements such as v5.0.0-8.1.1, which belong to the design record rather than to a single story.

How do you pick ASVS requirements for one feature?
The featureASVS 5.0 chaptersExamples to consider
Reads or changes records by IDV8 Authorization8.2.1 function level (L1), 8.2.2 data specific (L1), 8.2.3 field level (L2), 8.4.1 cross-tenant (L2)
Sign-in, reset or MFAV6 Authentication, V7 Session Management6.3.1 brute force controls (L1), 6.4.3 reset does not bypass MFA (L2), 7.2.4 new session token on sign-in (L1)
Multi-step flows, coupons, quotasV2 Validation and Business Logic2.3.1 steps in order (L1), 2.3.3 transactions (L2), 2.3.4 locking limited resources (L2), 2.4.1 anti-automation (L2)
Uploads and downloadsV5 File Handling5.2.1 size limit (L1), 5.2.2 extension and content checks (L1), 5.3.1 no execution from public folders (L1)
New API or message formatV4 API and Web Service, V15 Secure Coding and Architecture4.1.4 only supported HTTP methods (L3), 15.3.3 mass assignment (L2)
Any security decisionV16 Security Logging and Error Handling16.3.2 failed authorization logged (L2), 16.5.1 generic error messages (L2)

How do you write security acceptance criteria?

Write criteria in the format the team already uses. Given/When/Then works well because the "Then" forces an observable result. Four rules keep them useful: one behavior per scenario; always include the case that must be refused; state what the caller can observe (status, body, log entry), not the implementation; and tag the scenario with the requirement and ASVS IDs so tests and reports can be traced.

The example covers story EXP-14 at ASVS level 2. Note the second scenario: it does not only check that another tenant's invoice is refused, it checks that the refusal looks identical to a missing invoice, so IDs cannot be probed, as /domains/appsec/authorization/object-level-authorization/ explains.

invoice_download.featureGherkin
# Story EXP-14: download an invoice. ASVS 5.0.0 level 2.
Feature: Invoice download stays inside the caller's tenant

  Background:
    Given Ana is signed in as a member of tenant "red"
    And invoice "inv-1" belongs to tenant "red"
    And invoice "inv-2" belongs to tenant "blue"

  @SR-1 @v5.0.0-8.2.2
  Scenario: A member reads an invoice in their own tenant
    When Ana requests invoice "inv-1"
    Then the response status is 200
    And the response contains the invoice total

  @SR-1 @v5.0.0-8.2.2
  Scenario: Another tenant's invoice looks the same as a missing one
    When Ana requests invoice "inv-2"
    And Ana requests invoice "inv-999"
    Then both responses have status 404 and identical bodies

  @SR-2 @v5.0.0-8.3.1
  Scenario: A tenant named in the request is ignored
    When Ana requests invoice "inv-2" with the query parameter tenant=blue
    Then the response status is 404

  @SR-3 @v5.0.0-16.3.2
  Scenario: A denied read is logged without the invoice contents
    When Ana requests invoice "inv-2"
    Then the security log has an "authz_denied" entry naming user "ana" and resource "invoice:inv-2"

How do the criteria become automated tests?

Each scenario becomes a test that fails if the control is removed. The pytest tests below run against a small Flask service whose handler loads the invoice and compares its tenant with the tenant of the authenticated user, returning 404 in both the missing and the foreign case. Each test records the requirement it proves with pytest's record_property, which writes a property into the JUnit XML report.

Run them with pytest -o junit_family=xunit1 --junitxml=junit.xml; pytest warns that record_property is not compatible with the default xunit2 report family. Removing the tenant comparison from the handler makes three of the four tests fail, which is the point: a requirement test that still passes without the control proves nothing.

Testtest_invoice_requirements.pyPython
import logging

import pytest

from app import app

ANA = {"Authorization": "Bearer token-ana"}  # tenant red


@pytest.fixture
def client():
    return app.test_client()


def test_owner_can_read_own_invoice(client, record_property):
    record_property("requirement", "SR-1")
    response = client.get("/invoices/inv-1", headers=ANA)
    assert response.status_code == 200
    assert response.get_json()["total"] == 40


def test_other_tenant_invoice_looks_missing(client, record_property):
    record_property("requirement", "SR-1")  # v5.0.0-8.2.2
    other = client.get("/invoices/inv-2", headers=ANA)
    missing = client.get("/invoices/inv-999", headers=ANA)
    assert other.status_code == missing.status_code == 404
    assert other.data == missing.data


def test_client_supplied_tenant_is_ignored(client, record_property):
    record_property("requirement", "SR-2")  # v5.0.0-8.3.1
    response = client.get("/invoices/inv-2?tenant=blue", headers=ANA)
    assert response.status_code == 404


def test_denied_access_is_logged(client, caplog, record_property):
    record_property("requirement", "SR-3")  # v5.0.0-16.3.2
    with caplog.at_level(logging.WARNING, logger="security"):
        client.get("/invoices/inv-2", headers=ANA)
    assert "authz_denied user=ana resource=invoice:inv-2" in caplog.text

How do you trace requirements to evidence?

Keep the requirements next to the code, in a file that both people and CI can read. Each entry carries its ID, its text, its source, the ASVS IDs it covers and how it is verified. Requirements that cannot be automated, such as a rate limit proven by a load test, name the evidence instead. NIST SSDF task PW.1.2 asks you to track and maintain the software's security requirements, risks and design decisions; a version-controlled file does that and shows every change in review.

security/requirements.yamlYAML
# Security requirements for story EXP-14 "Download an invoice".
# Each one names its source, its ASVS 5.0.0 requirement and how it is verified.
feature: invoice-download
asvs_level: 2
requirements:
  - id: SR-1
    text: A user can read an invoice only if it belongs to their tenant; any other invoice ID gets the same 404 as a missing one.
    source: [abuse-case AC-EXP-1 "read another tenant's invoice by changing the ID"]
    asvs: [v5.0.0-8.2.2]
    verification: automated
  - id: SR-2
    text: The tenant used for the check comes from the authenticated session, never from a request parameter, header or body.
    source: [threat-model T3]
    asvs: [v5.0.0-8.3.1]
    verification: automated
  - id: SR-3
    text: Every denied read is logged with user, resource and UTC time, without the invoice contents.
    source: [incident-response need]
    asvs: [v5.0.0-16.3.2]
    verification: automated
  - id: SR-4
    text: Bulk invoice export is limited to 20 requests per user per minute.
    source: [abuse-case AC-EXP-3 "scrape every invoice through the export API"]
    asvs: [v5.0.0-2.4.1]
    verification: manual
    evidence: Load-test report attached to the release ticket.

How do you stop a story closing without its tests?

Add a CI step after the tests that compares the requirements file with the JUnit report. It fails when an automated requirement has no test recording it, when any of its tests failed, or when a manual requirement has no evidence. With the service above it prints 4/4 requirements traced; with the tenant check removed it lists SR-1, SR-2 and SR-3 and exits with status 1.

The gate checks that tests exist and pass; it cannot judge whether a test is meaningful. Review new requirement tests the way you review the control itself, and break the control once on a branch to see the test fail.

check_trace.pyPython
"""Fail the build when an automated security requirement has no passing test.

Usage: python check_trace.py requirements.yaml junit.xml
"""
import sys
import xml.etree.ElementTree as ET

import yaml


def main(requirements_path, junit_path):
    spec = yaml.safe_load(open(requirements_path, encoding="utf-8"))
    passed, failed = {}, {}
    for case in ET.parse(junit_path).iter("testcase"):
        ok = not any(case.find(tag) is not None for tag in ("failure", "error", "skipped"))
        for prop in case.iter("property"):
            if prop.get("name") == "requirement":
                (passed if ok else failed).setdefault(prop.get("value"), []).append(case.get("name"))
    problems = []
    for req in spec["requirements"]:
        rid = req["id"]
        if req["verification"] == "automated":
            if rid in failed:
                problems.append(f"{rid}: failing test {failed[rid]}")
            elif rid not in passed:
                problems.append(f"{rid}: no test records this requirement")
        elif not req.get("evidence"):
            problems.append(f"{rid}: manual requirement without evidence")
    for line in problems:
        print("FAIL", line)
    print(f"{len(spec['requirements']) - len(problems)}/{len(spec['requirements'])} requirements traced")
    return 1 if problems else 0


if __name__ == "__main__":
    sys.exit(main(*sys.argv[1:3]))

What goes in a security Definition of Done?

A Definition of Done is the checklist every item must pass before it counts as finished. Keep the security part short and binary, and split it by level: what a story needs before it closes, what a feature needs before customers see it, and what a release needs before it ships. Items that apply to everything, such as "no new SAST finding at blocking severity", belong here rather than in every story's requirements.

Make each item checkable by someone outside the team, and automate the ones you can. The trace gate covers the first three story items; scanners cover the SAST and dependency items; the rest are short review questions in the pull request or release template.

security-dod.yamlYAML
# Security Definition of Done. Every item is a yes/no check with an owner.
story:   # before the story moves to Done
  - Security requirements are listed on the story with an ASVS 5.0.0 ID or "local" and a source.
  - Each automated requirement has a test that fails when the control is removed.
  - The security trace check passes (no automated requirement without a passing test).
  - New endpoints and fields are covered by the authorization tests for their resource.
  - No new SAST or secret-scanning finding at or above the team's blocking severity.
  - Abuse cases raised in refinement are either tested or recorded as accepted risk by the owner.
feature: # before the feature flag is turned on for customers
  - The threat model for the feature is updated and its open threats have owners.
  - Manual requirements have evidence attached (test report, review record or screenshot).
  - Security events the feature adds are logged and reach the log pipeline.
  - Dependencies added by the feature are in the SBOM and have no known exploited vulnerability.
release: # before a release is published
  - Every requirement at the application's ASVS level is traced or has a dated exception.
  - Exceptions have an owner and an expiry date.
  - Release notes or an advisory describe any security fix customers must act on.

What do SSDF, SAMM and PCI DSS expect here?

The practice above satisfies several frameworks without extra paperwork. Cite the framework edition when an assessor asks.

What do SSDF, SAMM and PCI DSS expect here?
FrameworkWhat it asksEvidence from this practice
NIST SSDF 1.1 PO.1.2Identify and document all security requirements for organization-developed software, and maintain them over timeThe ASVS level per application and the requirements files
NIST SSDF 1.1 PW.1.2Track and maintain the software's security requirements, risks and design decisionsRequirements files in version control, linked to threats
OWASP SAMM 2.2.0 Security Requirements, stream ALevel 1: map security objectives to functional requirements. Level 2: standardize them and integrate them into how teams specify workRequirements on stories; a shared ASVS-based baseline
OWASP SAMM 2.2.0 Requirements-driven TestingStream A, control verification, and stream B, misuse and abuse testingRequirement tests, refused-case scenarios and the trace gate
PCI DSS v4.0.1 6.2.4Engineering techniques that prevent or mitigate common attacks in bespoke and custom softwareRequirements and tests for each attack class in payment code

Where do security requirements go wrong?

  • Copying the whole ASVS into every story. Select by feature and level; a 300-line checklist gets ticked, not read.
  • Writing mechanisms instead of outcomes. "Use the auth middleware" passes even when a new route forgets it; "every route under /invoices checks the tenant" fails.
  • Testing only the happy path. Most security requirements are about what must not happen, so the refused case is the test that matters.
  • Counting "security reviewed" as evidence. Name the reviewer, the date and what was checked, or automate it.
  • Letting requirements drift from code. Keep them in the repository and review changes to them like code.
  • Treating the Definition of Done as the requirements. The DoD applies to everything; requirements are specific to the feature and its risks.

Guided practice

A team is refining story INV-7 for a SaaS product at ASVS level 2: tenant admins can invite a teammate by email. The draft has one security requirement. Rewrite it, choose the ASVS requirements, write criteria and decide whether the story can close. The product and data are fictional.

Supplied scenario material
Plain text
Story INV-7: "As a tenant admin, I can invite a teammate by email. The invitee follows a link, sets a password and joins my tenant."
Draft security requirement: "Invites must be secure."
Abuse cases from refinement:
  AC1 A member who is not an admin sends invites by calling the API directly.
  AC2 An admin of tenant red changes tenant_id in the request and invites someone into tenant blue.
  AC3 An invite link is used twice, or used three weeks later after being forwarded.
  AC4 A script sends 5,000 invites an hour to unrelated addresses (spam from our domain).
Application ASVS level: 2
Trace report after implementation (junit.xml summary):
  test_member_cannot_invite            requirement=SR-1  passed
  test_tenant_from_session_only        requirement=SR-2  passed
  test_invite_link_single_use          requirement=SR-3  passed
  (no test records SR-4)
Requirements file: SR-4 verification: manual, evidence: (empty)
  1. 1. Reject the draft

    Explain why "Invites must be secure" cannot be accepted.

    Expected result and reasoning

    It names no outcome, no boundary and no verification method, so nobody can decide whether it holds. Replace it with one requirement per abuse case, each with a source and a test or evidence.

  2. 2. Write the requirements

    Turn AC1 to AC4 into testable requirements with ASVS 5.0.0 IDs.

    Expected result and reasoning

    SR-1: only tenant admins can create invites; a member gets 403 (v5.0.0-8.2.1). SR-2: the invite's tenant comes from the admin's session, never from the request (v5.0.0-8.3.1, and v5.0.0-8.4.1 for cross-tenant). SR-3: an invite token is random, single-use and expires after 72 hours (v5.0.0-6.4.1 covers system-generated activation codes that are random and expire after a short period or first use). SR-4: each tenant can send at most 50 invites an hour (v5.0.0-2.4.1). The 72-hour and 50-per-hour values are local choices.

  3. 3. Write one scenario

    Write the Given/When/Then criteria for SR-3, including the refused cases.

    Expected result and reasoning

    Given an invite issued to dana@example.test; When Dana accepts it, Then she joins tenant red; When the same link is used again, Then the response is 410 and no account is created; Given an invite issued 73 hours ago, When it is used, Then the response is 410. Tag it @SR-3 @v5.0.0-6.4.1.

  4. 4. Decide whether the story is done

    Apply the story-level Definition of Done to the trace report.

    Expected result and reasoning

    Not done. SR-4 is marked manual but has no evidence, so the trace gate fails. Either automate it (a test that the 51st invite in an hour returns 429) or attach load-test evidence. SR-1 to SR-3 pass, but confirm each test fails when its control is removed.

  5. 5. Place the generic items

    The team wants to add "no secrets in code" and "dependencies scanned" as requirements. Where do they belong?

    Expected result and reasoning

    In the Definition of Done, because they apply to every story; requirements stay specific to this feature's risks.

Your deliverable: A requirements file for INV-7 with four requirements, their ASVS IDs, sources and verification methods, one Gherkin scenario for SR-3, and a one-line decision on whether the story can close.

The rate limit and expiry values are local choices, not ASVS values. ASVS 6.4.1 is written for initial passwords and activation codes; applying it to invites is a reasonable reading, not an explicit ASVS statement.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. OWASP ASVS 5.0.0: Using the ASVS (levels and requirement identifiers). Version 5.0.0.
  2. OWASP ASVS 5.0.0: V8 Authorization. Version 5.0.0.
  3. OWASP ASVS 5.0.0: V6 Authentication. Version 5.0.0.
  4. OWASP ASVS 5.0.0: V16 Security Logging and Error Handling. Version 5.0.0.
  5. OWASP SAMM: Security Requirements, stream A (Software Requirements)
  6. NIST SP 800-218: Secure Software Development Framework 1.1. Version 1.1 (February 2022).
  7. OWASP Abuse Case Cheat Sheet
  8. Cucumber: Gherkin reference
  9. pytest: record_property and JUnit XML output

Reference information

Download this guide as Markdown