Lesson 03 / 11
Security Requirements
Testability is derived from three parts and run on 20 security requirements: 13 are automatically testable, 3 are checked by eye because of a single missing part, and 4 can never move into any class without being rewritten, because of their qualitative predicate. Five abuse cases grow the list by 40 percent, and 7 of the 8 incoming requirements are in denying mode; the 40-hour load of manual checking drops entirely once the missing parts are written.
Published Updated
In this lesson
- Derives a security requirement's automatic testability from its observable-predicate, threshold, and scope parts
- Separates a requirement checked by eye because of a missing part from one that can never be tested because of its qualitative predicate
- Counts the requirements an abuse case adds to the list: how many are new, how many are in denying mode, how many are automatically testable
Contents
The previous lesson counted that a quarter of the 250 hours prevented comes from items born in the requirement stage, but left the check that catches those items in a single table row: eighteen requirement reviews, five alerts, three real items. How a review catches an item was never opened up. It has exactly one condition for catching one: the requirement has to be written in a form where whether it is met is testable. The line “the system must be secure” produces no item in any review, because there is no observation that would show it has been violated.
This lesson takes testability out of being an adjective and splits it into three parts, then runs it on a list of twenty requirements. In the second half it asks where the list comes from: the requirements written by reading a feature list and the requirements written by considering an actor pursuing a target the system must deny do not produce the same set.
- DA12. The requirement list is fictional and is modeled with
node. Each requirement is written with four fields: subject, predicate type, threshold, scope. - DA13. Testability is not given, it is derived: if the predicate is observable and both the threshold and the scope are written, the requirement is automatically testable. If one of the parts is missing it is checked by eye; if the predicate is qualitative it can never be tested.
- DA14. The predicate-type and mode fields are a record of how the requirement is written, not a judgment of its content’s value. A requirement written untestably is not unimportant; the only thing it lacks is that it cannot be checked.
- DA15. An abuse case is modeled with an actor and a target; no step is given in an executable form. Trying out the scenario outside an organization’s own system requires written authorization and a defined scope; otherwise it is both illegal and unprofessional.
- DA16. The run counts and the manual-check minutes are model values; they are consistent with the change and deployment counts from the previous lessons.
The Three Parts of Testability
For a requirement to be automatically testable, three things are needed: an observable predicate where the violation would be seen, a threshold that says it has been met, and a scope that says where it applies. If one of the three is missing, testing is handed to a human.
// requirement.mjs — security requirements of the measurement network software (model) // row: name | subject | predicate type | threshold | scope | mode // predicate: event = observable event, state = auditable state, qualitative = qualitative adjective // mode: provides = the work the system does, denies = the work the system refuses const parse = (s) => s.trim().split('\n').map((r) => { const [name, subject, predicate, threshold, scope, mode] = r.trim().split(/\s+/); return { name, subject, predicate, threshold, scope, mode }; }); export const REQUIREMENT = parse(` session-duration-limit session state 30-min subscriber-portal denies failed-login-limit login-endpoint event 5-attempts subscriber-portal denies transport-encryption external-connection state required all-endpoints provides measurement-data-retention measurement-record state 24-months measurement-store provides authorization-check-coverage read-endpoint event every-request billing provides secret-in-source source-file state 0-count all-stores denies personal-data-in-logs log-line state 0-fields all-services denies dependency-age dependency state 180-days all-builds provides admin-endpoint-access admin-endpoint event internal-network meter-collection denies backup-encryption backup-file state required billing-db provides request-rate-limit general-endpoint event 60-requests-min subscriber-portal denies session-termination logout-action event 1-request subscriber-portal provides file-upload-size upload-endpoint state 10-mb field-app denies error-message-content error-response state - all-endpoints denies audit-log-retention authorization-change event every-change - provides password-storage-format subscriber-record state - subscriber-portal provides authentication-strength subscriber-login qualitative - subscriber-portal provides interface-security field-app qualitative - - provides data-integrity measurement-record qualitative - - provides separation-of-duties billing-approval qualitative - billing denies`); // Testability is derived: with an observable predicate + threshold + scope, it is tested automatically. export function classify(g) { if (g.predicate === 'qualitative') return 'untestable'; const missing = ['threshold', 'scope'].filter((a) => g[a] === '-'); return missing.length === 0 ? 'automatic' : 'manual'; } export const missingPart = (g) => ['threshold', 'scope'].filter((a) => g[a] === '-'); // Abuse case: an actor pursues a target the system must deny. // Written as a model; no step is given in an executable form. export const SCENARIO = [ ['subscriber-account-takeover', 'external-actor', ['failed-login-limit', 'session-duration-limit', 'session-termination', 'login-notification']], ['reading-someone-elses-bill', 'authorized-subscriber', ['authorization-check-coverage', 'record-id-guessability']], ['altering-a-measurement-record', 'field-staff', ['audit-log-retention', 'measurement-write-authorization', 'measurement-change-approval']], ['bulk-subscriber-data-pull', 'authorized-subscriber', ['request-rate-limit', 'bulk-download-limit', 'outbound-volume-threshold']], ['device-registration-spoofing', 'external-actor', ['device-registration-verification', 'registration-rate-limit']], ]; // Requirements that come out of the scenarios and are not on the list, written the same way: export const NEW = parse(` login-notification successful-login event every-login subscriber-portal provides record-id-guessability billing-record state not-guessable billing denies measurement-write-authorization measurement-record event role-matched measurement-store denies measurement-change-approval measurement-correction event two-signatures measurement-store denies bulk-download-limit external-export state - subscriber-portal denies outbound-volume-threshold external-export state 50-mb-day all-endpoints denies device-registration-verification device-registration event field-approval field-app denies registration-rate-limit device-registration event 10-registrations-hour - denies`);
// run1.mjs — requirements are split by testability; missing parts are counted import { REQUIREMENT, classify, missingPart } from './requirement.mjs'; const count = (l, s) => l.filter((g) => classify(g) === s).length; console.log(`${REQUIREMENT.length} requirements; automatic ${count(REQUIREMENT, 'automatic')}, ` + `manual ${count(REQUIREMENT, 'manual')}, untestable ${count(REQUIREMENT, 'untestable')}`); console.log('\nclass requirements has threshold has scope observable predicate'); for (const s of ['automatic', 'manual', 'untestable']) { const l = REQUIREMENT.filter((g) => classify(g) === s); console.log(s.padEnd(12) + String(l.length).padEnd(14) + String(l.filter((g) => g.threshold !== '-').length).padEnd(15) + String(l.filter((g) => g.scope !== '-').length).padEnd(11) + l.filter((g) => g.predicate !== 'qualitative').length); } console.log('\nrequirements that do not reach automatic, and their missing parts'); for (const g of REQUIREMENT.filter((x) => classify(x) !== 'automatic')) console.log(' ' + g.name.padEnd(25) + classify(g).padEnd(12) + (g.predicate === 'qualitative' ? 'qualitative predicate: cannot change class without a rewrite' : `missing: ${missingPart(g).join(', ')}`)); const singleMissing = REQUIREMENT.filter((g) => classify(g) === 'manual' && missingPart(g).length === 1); console.log(`\n${singleMissing.length} requirements move to automatic by adding a single part; ` + `${count(REQUIREMENT, 'untestable')} requirements need a rewrite`); console.log(`after the fix, automatic ${count(REQUIREMENT, 'automatic') + singleMissing.length}/` + `${REQUIREMENT.length}, remaining ${REQUIREMENT.length - count(REQUIREMENT, 'automatic') - singleMissing.length}`);
20 requirements; automatic 13, manual 3, untestable 4 class requirements has threshold has scope observable predicate automatic 13 13 13 13 manual 3 1 2 3 untestable 4 0 2 0 requirements that do not reach automatic, and their missing parts error-message-content manual missing: threshold audit-log-retention manual missing: scope password-storage-format manual missing: threshold authentication-strength untestable qualitative predicate: cannot change class without a rewrite interface-security untestable qualitative predicate: cannot change class without a rewrite data-integrity untestable qualitative predicate: cannot change class without a rewrite separation-of-duties untestable qualitative predicate: cannot change class without a rewrite 3 requirements move to automatic by adding a single part; 4 requirements need a rewrite after the fix, automatic 16/20, remaining 4
Thirteen of the twenty requirements are automatically testable, three are checked by eye, four can
never be tested. The real information is in what separates the second row from the third. The
three requirements checked by eye have observable predicates; what is missing is a single
field. error-message-content says what an error response must not contain but writes no
threshold; audit-log-retention asks for a log entry of every authorization change but does not
say which services it applies to. Both move to automatic by adding one line.
The four untestable ones are a different case. data-integrity or interface-security are not
requirements with a missing field; their predicates do not correspond to an observation. Writing a
threshold for them would not help either, because it is not clear what would be measured. What
they need to change class is not completion but a rewrite: each one has to be translated into
an event or a state whose violation can be seen, and this translation usually splits a single
requirement into several.
The decision counterpart of the distinction is clear. For three requirements the job is filling in a field; for four requirements the job is a design discussion. After the fix the list splits into 16 automatic and 4 untestable requirements — and these four requirements produce no alert in the code stage no matter how much checking is set up.
A warning is needed here: testability is not correctness. Having written thirty minutes as the session duration limit makes it automatically testable, but it does not show that thirty minutes is the right number; a wrong threshold is tested just as flawlessly. This classification measures whether a requirement is fit for checking, not whether it is well-aimed. Mixing the two up produces a common flaw: the threshold gets written so that it is testable, and where the number came from is never discussed.
What an Abuse Case Adds
The twenty requirements above are written by reading the feature list: they are derived from the work the system must do. An abuse case starts from the opposite direction — an actor pursues a target the system must deny, and the requirements needed to deny that target are written down.
// run2.mjs — how much and in what form abuse cases grow the list import { REQUIREMENT, NEW, SCENARIO, classify } from './requirement.mjs'; const existing = new Set(REQUIREMENT.map((g) => g.name)); console.log('scenario actor requirements on list new'); let total = 0, inList = 0; for (const [name, actor, produced] of SCENARIO) { const v = produced.filter((x) => existing.has(x)).length; total += produced.length; inList += v; console.log(name.padEnd(31) + actor.padEnd(23) + String(produced.length).padEnd(14) + String(v).padEnd(9) + (produced.length - v)); } console.log(`${SCENARIO.length} scenarios, ${total} requirement references, ${inList} on the list, ` + `${total - inList} new; the list grows ${(((total - inList) / REQUIREMENT.length) * 100).toFixed(0)}%`); const count = (l, s) => l.filter((g) => classify(g) === s).length; const modeCount = (l, k) => l.filter((g) => g.mode === k).length; console.log('\nset requirements automatic manual untestable denies denies %'); for (const [name, l] of [['from feature list', REQUIREMENT], ['from scenarios', NEW]]) console.log(name.padEnd(19) + String(l.length).padEnd(14) + String(count(l, 'automatic')).padEnd(11) + String(count(l, 'manual')).padEnd(8) + String(count(l, 'untestable')).padEnd(12) + String(modeCount(l, 'denies')).padEnd(8) + ((modeCount(l, 'denies') / l.length) * 100).toFixed(0)); // Testability class determines which stage a requirement is tested in, and its cost. const DEPLOY_RUNS = 60, CHANGE_RUNS = 420, REVIEW_MIN = 8; console.log('\nclass tested in stage run count human minutes'); const all = [...REQUIREMENT, ...NEW]; for (const [s, stage, runs, min] of [['automatic', 'code', CHANGE_RUNS, 0], ['manual', 'deployment', DEPLOY_RUNS, REVIEW_MIN], ['untestable', 'runtime', 0, 0]]) console.log(s.padEnd(12) + stage.padEnd(17) + String(count(all, s) * runs).padEnd(14) + count(all, s) * runs * min); const manualMinutes = count(all, 'manual') * DEPLOY_RUNS * REVIEW_MIN; console.log(`\n${all.length} requirements: ${count(all, 'automatic')} tested in the code stage, ` + `${count(all, 'manual')} in the deployment stage, ${count(all, 'untestable')} never tested`); console.log(`manual checking's yearly total is ${manualMinutes} minutes (${(manualMinutes / 60).toFixed(1)} hours); ` + `if the missing parts are written, all ${(manualMinutes / 60).toFixed(1)} hours drop`);
scenario actor requirements on list new subscriber-account-takeover external-actor 4 3 1 reading-someone-elses-bill authorized-subscriber 2 1 1 altering-a-measurement-record field-staff 3 1 2 bulk-subscriber-data-pull authorized-subscriber 3 1 2 device-registration-spoofing external-actor 2 0 2 5 scenarios, 14 requirement references, 6 on the list, 8 new; the list grows 40% set requirements automatic manual untestable denies denies % from feature list 20 13 3 4 9 45 from scenarios 8 6 2 0 7 88 class tested in stage run count human minutes automatic code 7980 0 manual deployment 300 2400 untestable runtime 0 0 28 requirements: 19 tested in the code stage, 5 in the deployment stage, 4 never tested manual checking's yearly total is 2400 minutes (40.0 hours); if the missing parts are written, all 40.0 hours drop
Five abuse cases touch fourteen requirements; six of them are already on the list, eight are new. The list grows by forty percent, and where the growth concentrates matters: both of the two requirements the last scenario produces are new. Device registration is a feature on the feature list, and that feature’s requirements are written; being able to register a device under someone else’s name is not a feature, so it cannot be derived from any feature-list line.
A terminology distinction is needed here. A use case writes out, step by step, a use the system supports and wants that use to produce the right outcome. An abuse case uses the same form, but its owner sits on the system’s opposing side, and the outcome it wants is the outcome the system must not give. The second table turns the difference into a number: 45% of the twenty requirements from the feature list are in denying mode, versus 88% of the eight requirements from the scenarios. If the share of denying mode in a requirement list is low, what is missing is not security attention, it is source.
Six of the eight requirements coming from the scenarios are written directly as automatically testable. This is not because the writer was more careful, it is the shape of the problem: a target that must be denied already demands an observation, a threshold, and a scope. The sentence “device registration requires field approval” carries both the event where the violation would be seen and the threshold; “the interface must be secure” carries neither.
There is something the table does not measure, and that is this lesson’s limit: where the five scenarios came from. The scenarios are on the list because someone thought of them. The table has no answer to how many more requirements a sixth scenario would add, because the scenario set itself was not produced by a systematic method. What is measured is the gain per scenario, not the share of targets covered; when the two get conflated, a team that has written five scenarios considers its job done.
The Stage Counterpart of Testability
The last table ties back into this course’s measurement axis. Nineteen of the twenty-eight requirements are automatically testable, so they are tested in the code stage and produce 7,980 runs a year; the human cost of these runs is zero. Five requirements can only be checked by eye, so the point of testing shifts to the deployment stage: 300 checks a year, 2,400 minutes, that is 40 hours.
The two numbers should be read side by side. An automatically tested requirement is tested at the first opportunity after the stage it is born in, and on every change; its violation is caught in the code stage. A requirement checked by eye is only looked at while a deployment is being prepared, and its violation shows up in the previous lesson’s table as an item caught three stages late. Testability looks like a property of how something is written, but its consequence is directly the detection stage.
The four untestable requirements sit in the third row and produce no runs at all. They are not ignored by the system; their violations are only reported by an incident, not a check. In the previous lesson’s language: these four requirements are the requirement-stage source of the item class no check ever sees. A requirement list’s security value is measured not by how many lines it contains, but by how many of those lines correspond to a run count; in this list, that ratio is nineteen out of twenty-eight.
Summary
- Automatic testability is derived from three parts: an observable predicate, a threshold, a scope. If one of the three is missing the requirement is checked by eye; if the predicate is qualitative it can never be tested.
- Of the 20 requirements, 13 are automatic, 3 are checked by eye, 4 are untestable. The 3 checked by eye move to automatic by adding a single field; the 4 untestable ones cannot change class without a rewrite.
- 5 abuse cases touch 14 requirements, 6 of which are already on the list, 8 of which are new; the list grows by 40 percent, and the scenario that is entirely new is the one with no counterpart feature on the feature list.
- 45% of the requirements from the feature list are in denying mode, versus 88% of those from the scenarios; a low share of denying mode reflects the list’s source, not the writer’s attention.
- 19 of the 28 requirements are tested in the code stage 7,980 times a year at zero human cost; 5 are tested in the deployment stage, consuming 40 hours across 300 checks, and 4 are never tested.
- When the missing parts are written, the entire 40-hour load of manual checking drops; the four untestable requirements, meanwhile, keep producing no alert in any check.
Next Step
This lesson measured how a requirement should be written and produced two jobs: writing the missing field into three requirements, rethinking four requirements. Both are jobs someone has to do, and who that someone is was never asked. The person who will write the missing threshold, the person who will run the abuse case, and the person who will notice an unobservable predicate in a design session are not the same person, and they are not present on all five teams. The previous two lessons spread the checks across stages, this lesson turned requirements into a testable form — but both assumed that the competence to do the work is ready and waiting on every team. The next lesson measures that assumption: how many teams there are, how many of them have someone with a security eye, how many teams a single person’s capacity can cover, and how many reviews go through without a security eye when capacity falls short.
To keep your progress and take notes, Log in
My notes
Log in to take notes.