Writing Security Requirements: Choosing a Level with OWASP ASVS 5.0, Acceptance Criteria and Security Champions
A security requirement means choosing an ASVS level in writing and verifying every selected item with a test or evidence, not filling in 345 rows. For Bookstore: an ASVS 5.0 Level 2 target plus one Level 3 item taken on purpose, a requirements file, security acceptance criteria and an abuse story, the Definition of Done, an xUnit test that keeps the file and the tests in step, the security champion and a NIST SSDF mapping.
Version note: The code in this post was built and run with .NET 10; the tests ran with xUnit v3 4.0.3, and the requirements follow ASVS 5.0.0, released on 30 May 2025.
Guess First
For Bookstore, the online bookstore used as the shared example across this site's series, we made a set of decisions in the second post: the person who requests an institutional purchase cannot approve it, a request that changes after approval waits for a new approval, the webhook signature is verified. These decisions live in three places right now: the threat model, a few tests and Emre's head. Three months from now Ayşe will add a new "unit manager" role for institutions. Where will she learn about the four-eyes rule?
Guess one more thing: the application security standard ASVS 5.0 contains 345 requirements in total. How many of them should Bookstore meet, and how should it show that? The answer is not "all of them"; it is "choose in writing which level you aim for, and show the ones you chose with a test or evidence". In this post we turn the decisions from the threat model into written security requirements with ASVS 5.0 and tie them to user stories, the Definition of Done and tests.
A Short Look at ASVS 5.0
OWASP released version 5.0.0 of ASVS on 30 May 2025. The post on OWASP's other lists in the Web Security (OWASP Top 10) series gives a general overview of ASVS; here we take only what this post needs. 5.0 consists of 17 chapters (from chapter 1, Encoding and Sanitization, to chapter 17, WebRTC) and 345 requirements in total: 70 at Level 1, 183 at Level 2 and 92 at Level 3. Every requirement has an identifier, and when you refer to it from another document you are asked to include the version: v5.0.0-8.2.2 means item 2 of section 2 of chapter 8 (Authorization) in version 5.0.0.
For those coming from 4.0.3 the change is big. According to the standard's own transition chapter, only 11 of the 286 requirements in 4.0.3 stayed as they were; 109 are no longer separate requirements (50 deleted, 28 removed as duplicates, 31 merged into others). New chapters were added for OAuth and OpenID Connect, WebRTC, self-contained tokens, web frontend security, and secure coding and architecture; the old architecture chapter was removed. CWE and NIST mappings were taken out from next to the requirements. Two changes matter especially for this post. The first is "documented security decisions": the first section of each chapter now asks the application to define a rule in writing; it continues the threat modeling and policy requirements of 4.0. The second is that Level 1 is no longer defined as the requirements "testable from outside, without looking at the source code"; levels are set by balancing risk reduction against the cost of implementation.
Choosing a Level: Why Level 2 for Bookstore?
ASVS 5.0 describes the levels like this. Level 1 is the minimum starting point: about a fifth of the requirements, a first layer of defence against common attacks that need no preconditions. Level 2 is the level most applications should aim for; it adds about half of the requirements on top of Level 1. Level 3 is the rest: defence in depth or controls that are hard to implement, for systems that need the highest assurance.
Bookstore takes payments, holds customers' personal data and creates orders worth thousands of liras on behalf of institutions. Level 1 is too little for that; all of Level 3 is disproportionate for a small team. Decision: target Level 2. But choosing a level does not mean taking nothing from the level above. v5.0.0-2.3.5 asks for high-value business flows to require approval by more than one person, and it is at Level 3. The Institutional Order is exactly such a flow, and threat T09 from the second post says the same. So we added this single requirement to the list on purpose and wrote the reason in the file.
The standard also openly encourages one thing: an ASVS tailored by the organisation to its own needs. Chapters you do not use (WebRTC and GraphQL in Bookstore) drop out of the list; the identifiers stay unchanged so that "we meet 8.2.2" means the same everywhere.
Writing the Requirements into the Repo
In Bookstore the selected requirements live in a JSON file. The file does not copy ASVS text; it keeps only the identifier, the level and how it is verified. The text is in the standard itself; copied text goes stale.
{
"standard": "OWASP ASVS 5.0.0",
"targetLevel": 2,
"scope": "Checkout, Payments, Shipping and Institutional purchase approval (Bookstore.Api)",
"requirements": [
{ "id": "v5.0.0-2.1.3", "level": 2, "verification": "review", "evidence": "docs/security/authorization-rules.md" },
{ "id": "v5.0.0-2.3.1", "level": 1, "verification": "test" },
{ "id": "v5.0.0-2.3.2", "level": 2, "verification": "test" },
{ "id": "v5.0.0-2.3.4", "level": 2, "verification": "test" },
{ "id": "v5.0.0-2.3.5", "level": 3, "verification": "test", "note": "L3 requirement taken on purpose: institutional orders above the unit limit" },
{ "id": "v5.0.0-2.4.1", "level": 2, "verification": "planned", "backlog": "SEC-14" },
{ "id": "v5.0.0-8.1.1", "level": 1, "verification": "review", "evidence": "docs/security/authorization-rules.md" },
{ "id": "v5.0.0-8.2.1", "level": 1, "verification": "test" },
{ "id": "v5.0.0-8.2.2", "level": 1, "verification": "test" },
{ "id": "v5.0.0-8.3.1", "level": 1, "verification": "test" },
{ "id": "v5.0.0-16.3.2", "level": 2, "verification": "planned", "backlog": "SEC-15" }
]
}
There are three kinds of verification. test: at least one automated test verifies the requirement. review: the requirement asks for a document and the evidence is that document; 8.1.1, for example, asks for the authorization rules to be written down, and docs/security/authorization-rules.md does exactly that. planned: not met yet, but there is a backlog item. Writing down what is planned instead of dropping it from the list is honesty itself: when an institutional customer asks "what is missing for Level 2?", the answer is in this file. Summarising the list with jq is easy too:
jq -r '.requirements | group_by(.verification)[] | "\(.[0].verification)\t\(length)\t\([.[].id] | join(" "))"' docs/security/asvs-requirements.json
planned 2 v5.0.0-2.4.1 v5.0.0-16.3.2
review 2 v5.0.0-2.1.3 v5.0.0-8.1.1
test 7 v5.0.0-2.3.1 v5.0.0-2.3.2 v5.0.0-2.3.4 v5.0.0-2.3.5 v5.0.0-8.2.1 v5.0.0-8.2.2 v5.0.0-8.3.1
Tying a Requirement to a User Story
The requirements file answers "what"; developers, however, work through stories. So every story carries its own security acceptance criteria and writes the ASVS identifier next to them. The technique for writing acceptance criteria is in the acceptance criteria and example mapping post of the Requirements Analysis series; here we only add the security side:
# KS-12 Purchase approval above the unit limit
As an institution approver, I want requests above my unit's limit to wait for my approval,
so that the school does not spend more than its budget allows without a second person.
## Acceptance criteria
- A request whose total is above the unit's approval limit is created as `PendingApproval`.
- An approver of the same institution can approve it; the request becomes `Approved`.
- The order can be placed only for an `Approved` request.
## Security acceptance criteria
- The requester cannot approve their own request, even with both roles. (ASVS v5.0.0-2.3.5)
- A buyer without the approver role gets 403 on approve. (ASVS v5.0.0-8.2.1)
- An approver of another institution gets 404, not 403. (ASVS v5.0.0-8.2.2)
- Calling "place order" on a `PendingApproval` request returns 409. (ASVS v5.0.0-2.3.1)
- Changing the lines after approval needs a new approval. (threat T10)
## Abuse story
As a buyer who wants a large order without waiting, I split it into several requests under the limit.
Decision: accepted for release 1, see `threat-model/risk-register.md` R-02.
An abuse story is the reverse of a normal story: it is written from the point of view of someone who wants to misuse the system. It is the security document Product Owner Selin understands most easily, because it speaks business language instead of technical terms. Every abuse story should end in a decision: it either becomes an acceptance criterion or goes into the risk register.
Security Items in the Definition of Done
Items written per story can be forgotten; the Definition of Done is the contract that stays the same for every story. The security items added to Bookstore's Definition of Done are short and checkable:
# Definition of Done
A backlog item is done when:
- The acceptance criteria pass as automated tests on the pull request build.
- Code review is done by someone who did not write the code.
- Threat model check in the PR template is answered; a new or changed data flow is in `threat-model/`.
- Every ASVS requirement the story names has a test (`[Trait("asvs", "v5.0.0-x.y.z")]`) or an entry with
`"verification": "review"` and its evidence in `docs/security/asvs-requirements.json`.
- No new High or Critical finding from the pipeline's security scans, or an exception with an owner and a date.
- Security-relevant events of the story are logged (see `docs/security/authorization-rules.md`).
There is no item like "secure code was written", because nobody can check it. Every item can be answered yes or no on a PR. Scan thresholds in the pipeline and exception management were covered in the end-to-end pipeline post of the DevSecOps series.
The Link Between Requirements and Tests
The file and the tests drift apart over time: someone deletes a test, someone adds a new identifier to the file, and nobody notices. We close that gap with a test. Tests are marked with [Trait("asvs", "v5.0.0-8.2.2")]; SecurityRequirementsTests scans the test assembly with reflection and compares it with the file:
private static readonly IReadOnlyList<(string Test, string Id)> AsvsTraits =
[
.. typeof(SecurityRequirementsTests).Assembly.GetTypes()
.SelectMany(t => t.GetMethods())
.SelectMany(m => m.GetCustomAttributes<TraitAttribute>()
.Where(a => a.Name == "asvs")
.Select(a => ($"{m.DeclaringType!.Name}.{m.Name}", a.Value)))
];
[Fact]
public void Every_requirement_verified_by_test_has_a_test()
{
string[] missing =
[
.. File.Requirements
.Where(r => r.Verification == "test")
.Select(r => r.Id)
.Where(id => !AsvsTraits.Any(t => t.Id == id))
];
Assert.True(missing.Length == 0, $"No test with [Trait(\"asvs\", ...)] for: {string.Join(", ", missing)}");
}
The same class has two more tests: one checks that every asvs trait points to an identifier in the file, the other that review and planned rows name their evidence or backlog item. When we first wrote the file there was no test for 8.2.1 (function-level access); nobody had tried calling the approve endpoint as someone with only the buyer role:
dotnet test --filter-class "*SecurityRequirementsTests"
failed Bookstore.Api.Tests.SecurityRequirementsTests.Every_requirement_verified_by_test_has_a_test (7ms)
No test with [Trait("asvs", ...)] for: v5.0.0-8.2.1
Test run summary: Failed!
total: 3
failed: 1
succeeded: 2
The missing test has Ada, who has no approver role, call the approve endpoint:
[Fact]
[Trait("asvs", "v5.0.0-8.2.1")]
public async Task Buyer_without_the_approver_role_cannot_approve()
{
PurchaseRequestView request = await CreateAsync(factory.CreateClientAs(TestUsers.Linus), (Middlemarch, 20));
HttpResponseMessage approve = await factory.CreateClientAs(TestUsers.Ada)
.PostAsync($"/api/institutions/me/purchase-requests/{request.Id}/approve", null, ct);
Assert.Equal(HttpStatusCode.Forbidden, approve.StatusCode);
}
Test run summary: Passed!
total: 20
failed: 0
succeeded: 20
skipped: 0
duration: 987ms
The code was already correct; what was missing was the evidence. That is exactly the value of this test: it moves the difference between "I think it works" and "there is a test that shows it works" into the build. You can run the tests of a single requirement with dotnet test --filter-trait "asvs=v5.0.0-2.3.5"; when an auditor or an institutional customer asks, the evidence is one command away.
The Security Champion and a 15-Minute Refinement Step
In small teams without a separate security team, who does this work? Bookstore's answer is the security champion model: one developer from the team (Emre here) spends part of their time, roughly a fifth in Bookstore, on security. A champion is not a gatekeeper and does not approve every PR. Their job is to run the threat modeling sessions, own the asvs-requirements.json file, make sure findings from the pipeline do not stay without an owner and answer the rest of the team's questions. Nobody expects the champion to know everything alone; they are expected to know when to ask for outside help.
Turning threat modeling into a separate meeting for every story is not sustainable. In Bookstore, backlog refinement ends with a step of at most 15 minutes per story, with three questions: Does this story cross a trust boundary or add a new data flow? Is there new personal data or a money movement? Does it need a new authorization rule? A story with "no" to all three goes on as it is. If there is a "yes", the story gets security acceptance criteria and ASVS identifiers; if the answer is big (a new payment method, a new integration), a separate threat modeling session like the one in the first post is planned.
Mapping to NIST SSDF
Institutional customers and auditors sometimes ask "which framework does your secure development process follow?". One of the most cited is NIST's SSDF: SP 800-218, version 1.1, February 2022. It groups its practices into four: Prepare the Organization (PO), Protect the Software (PS), Produce Well-Secured Software (PW) and Respond to Vulnerabilities (RV). SSDF says "what", not "how", so mapping what Bookstore does onto it is easy:
| SSDF task | In short | What it is in Bookstore |
|---|---|---|
PO.1.2 | Define security requirements for software the organisation develops | docs/security/asvs-requirements.json, the security acceptance criteria in stories |
PW.1.1 | Find design risks with risk modelling such as threat modeling | threat-model/, the first and second posts |
RV.1.3 | Vulnerability disclosure and remediation policy | The fourth post of the series: security.txt and the advisory process |
RV.3 | Analyse the root causes of vulnerabilities and improve the process | The fifth and sixth posts: pentest findings and the aftermath of an incident |
NIST published the draft of SP 800-218 Rev. 1, as SSDF 1.2, for comment on 17 December 2025; when this post was written the final version had not been released. When you keep a mapping table, note which version it was written against.
In Production
The requirements file is tied to a version. When the next ASVS version comes out, identifiers may change; that is why the file has the v5.0.0- prefix. Moving to a new version should be a separate, planned piece of work, not a silent find-and-replace.
review rows go stale over time. If the authorization rules document is not updated when a new role is added, the evidence proves the wrong thing. In Bookstore the champion opens the evidence of the review rows before every release and checks the date; Ayşe's new role went into the document during this check.
Institutional customers' security questionnaires may ask "are you ASVS Level 2 certified?". OWASP certifies no one; the standard's assessment chapter also asks whoever does the verification to state the scope and the level explicitly. The right answer is "we aim for Level 2; here is the scope, the requirements met and planned are in this file, and these tests are the evidence".
Common Pitfalls
The most common pitfall is using ASVS as a form to fill in from start to finish. Writing "Yes" into a 345-row table takes two days and changes nothing; the "Yes" rows have no evidence. Choose your own scope, drop the irrelevant chapters and write a verification method for every row.
The second pitfall is copying ASVS text into stories verbatim. The standard's language is generic; a story's language should be specific to Bookstore: "an approver of another institution gets 404" instead of "data-specific access is restricted". Writing the identifier next to it keeps the link.
The third is making the security champion solely responsible. A champion is a facilitator; the team makes the security decisions and whoever writes the code fixes it. Security should not stop in the week the champion is on leave.
A Small Experiment
★☆☆ v5.0.0-2.4.1 is currently planned. Add a rate limiter to the order endpoint (as in the rate limiting post of the ASP.NET Core Security series), write the test, add the Trait and turn the row into test. When is the traceability test red, and when is it green?
★★☆ Read chapter V8 (Authorization) of ASVS 5.0. Find a requirement that is not on the list but should be for Bookstore's Level 2 target, add it to the file and decide on its verification kind.
★★★ Add one more rule to the traceability test: if a requirement's level is above targetLevel, its note field cannot be empty. Why do we want such a rule?
So far we have looked at threats we found ourselves. One day someone outside will find one and want to tell us. In the fourth post of the series we talk about how Bookstore receives such a report: security.txt, coordinated disclosure, GitHub Security Advisories and CVEs.