blob: 0f6ed6e8c3c3aa3b5cb52951d45c55d421c42813 [file] [view]
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Security model preparation](#security-model-preparation)
- [Why a project wants one at all](#why-a-project-wants-one-at-all)
- [The rubric is external, and stays external](#the-rubric-is-external-and-stays-external)
- [Discoverability: the one hard gate](#discoverability-the-one-hard-gate)
- [Why mail, and not a public issue](#why-mail-and-not-a-public-issue)
- [Public-surface discipline](#public-surface-discipline)
- [Draft-first, and the provenance tags that make it safe](#draft-first-and-the-provenance-tags-that-make-it-safe)
- [The asymmetry that governs every judgement call](#the-asymmetry-that-governs-every-judgement-call)
- [The update loop, and why §1.15 is fenced](#the-update-loop-and-why-115-is-fenced)
- [Adopter configuration](#adopter-configuration)
- [Cross-references](#cross-references)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
# Security model preparation
The three `security-model-*` skills cover one lifecycle: getting a project's
security model **written**, **findable**, and **current**. This page is the
rationale behind them — the decisions that are the same for every adopter, kept
in one place so the skills do not each re-argue them.
| Stage | Skill | Question it answers |
|---|---|---|
| Produce | [`security-model-prepare`](../../skills/security-model-prepare/SKILL.md) | The project has no written model. What does one say, and how does it land without burning maintainer goodwill? |
| Verify | [`security-model-verify`](../../skills/security-model-verify/SKILL.md) | Can an agent find the model, and does it say enough to route a finding? |
| Update | [`security-model-update`](../../skills/security-model-update/SKILL.md) | Six months of triage decisions have happened. What does the model now say that it did not? |
## Why a project wants one at all
A security model is the *implicit contract* between a project and its downstream
users: the assumptions it makes about its environment, callers, and inputs; the
properties it upholds; the properties it explicitly does not; and the misuses
that are syntactically possible but outside the intended use.
It has two readers, and they need different things from the same document:
- The **downstream integrator**, deciding which threats the project has taken on
and which are left to them.
- The **triager** — maintainer, security team, or automated pipeline — who has to
classify an inbound report, a scanner finding, or an AI-generated analysis as
valid, out of model, or disclaimed by design, *and cite the section that
justifies the call*.
The second reader is why this matters more every year. Automated analysis
produces findings at a rate no maintainer can absorb by hand, and the only thing
that makes that volume survivable is a document the triage step can consult
before a human does. A project without one does not get fewer findings; it gets
the same findings with nobody able to close them cheaply.
## The rubric is external, and stays external
The specification of what a model contains is maintained publicly by
**Alpha-Omega**:
> <https://github.com/alpha-omega-security/threat-model>
That repository ships an orchestrator plus specialists — recon, surface,
interview, authoring, backtest, sidecar, triage — with the section structure
(§1.1–§1.19), the provenance tag set, and the closed disposition set with its
precedence order in `skills/threat-model/references/`.
Magpie **references it by URL and does not vendor, mirror, or fork it.** Two
reasons:
1. **A second copy of a specification is a second specification.** It drifts, and
the drift shows up as a maintainer being told their model is incomplete
against a rubric nobody upstream recognises.
2. **The rubric is the part most likely to improve.** Pinning a snapshot of it
inside Magpie would freeze the least stable and most valuable input.
What Magpie owns is everything *around* the rubric: the consent conversation, the
discoverability chain, the PR mechanics, the confidentiality scrubbing, and the
feedback loop from triage history back into the document. Those are governance
and workflow concerns, which is Magpie's half of the problem.
The skills therefore cite section numbers (§1.15 known non-findings, §1.17
dispositions) as coordinates into the upstream document. When upstream renumbers,
the fix is to update the citing tables — not to start keeping a local copy.
## Discoverability: the one hard gate
An agent finds a project's model by following a fixed chain:
```text
AGENTS.md -> SECURITY.md -> <the model>
```
Any of four terminations is fine: the model embedded in `SECURITY.md`, an in-repo
file, a project-site URL, or an umbrella model in another repository (the normal
shape for build tooling, language ports, and other satellites).
This is the only check that is allowed to *block*. The reasoning is narrow: a
model an agent cannot reach is not a worse model, it is an absent one — the
scanner has to treat every component as in scope and every property as unclaimed,
and the output is the hundred-finding review the model existed to prevent.
Completeness, by contrast, is graded: gaps are proposals the maintainer decides
on. Nothing in the completeness rubric is a precondition for anything.
The chain is also cheap to fix, which is why it gets a PR rather than a
conversation. Adding one section to `AGENTS.md` needs no maintainer input, and
the diff is small enough to review in the time it takes to read the title.
## Why mail, and not a public issue
Everything substantive — model gaps, missing sections, a broken link the project
owns — goes to the project's private security list, never to a public tracker.
Three independent reasons, any one of which would be sufficient:
1. **Many projects have no usable public tracker.** Issues are disabled on the
forge, or the project tracks work in a different system entirely. The issue
cannot be filed.
2. **A public list of a project's threat-model gaps is an inventory.** "The
maintainers acknowledge they do not check X" is exactly what a hostile
researcher would mine, and publishing it is a favour to them. The private list
keeps the same content among people the project has already vetted.
3. **Maintainers who read mail may never see a tracker notification.** The
conversation almost always starts as mail; continuing it there reaches
everyone, including the members who never look at the forge.
PRs are the deliberate exception. They need a repository write anyway, the diff
*is* the artefact being ratified, and "add one link line" carries no inventory
worth withholding.
## Public-surface discipline
PR titles, PR bodies, commit messages, and branch names on a target repository
are public forever. Two rules apply to all of them:
- **No scan-programme, vendor, or engagement identity.** The public-facing
rationale is *"improving the discoverability of the project's security model
for automated scanners"*. Who is running the review and under what arrangement
is information the security team controls the disclosure of; putting it in a
commit message forecloses later choices and hands anyone a single string to
grep for. On the private list it is fine — that surface is already inside the
trust boundary.
- **No tracker contents.** A tracker URL or `#NNN` reference is a stable
identifier and is public-safe; the page behind it stays access-gated. Its
*contents* — issue bodies, comment text, team debate, labels, assignees — are
not. See the confidentiality rules in [`AGENTS.md`](../../AGENTS.md).
This bites hardest in `security-model-update`, whose entire input is private
tracker material and whose entire output is a public document. Its scrub step is
not a formality.
## Draft-first, and the provenance tags that make it safe
A blank-page request — *"could you write up your threat model?"* — is an
unbounded ask, and it is why most of these efforts produce nothing. A tagged
draft is a bounded one: the maintainer reads claims someone else wrote and says
yes, no, or *not quite* per line. Reacting is far cheaper than composing, and the
corrections are where the real model surfaces.
That inverts the usual risk, though. A draft written by someone who is not the
maintainer, in the maintainer's voice, is a document that can quietly launder a
guess into the project's official position. The provenance tags are what prevent
it:
| Tag | Means | Licenses closing a report? |
|---|---|---|
| documented | Lifted from a project artefact, cited | Yes |
| maintainer | Stated by a maintainer, dated | Yes |
| assumption | A working premise with an open question | Only under a declared relaxed policy, low blast radius, never security-critical |
| inferred | The drafter's guess, with an open question | **No** — it escalates, never closes |
No hedge variants. Every assumption and inferred claim resolves to a numbered
open question. A first draft with no inferred tags is overclaiming.
## The asymmetry that governs every judgement call
It appears in the backtest, in the update loop's regression check, and in the
rules for what may become a known non-finding. It is one sentence:
> Wrongly escalating a non-finding wastes a maintainer's afternoon. Wrongly
> closing a real vulnerability hands a reporter "not a bug" on a live issue.
So: when the evidence is ambiguous, prefer the wording that leaves reports
escalating. Narrow a claim to resolve a conflict; never widen one. A model that
closes an item the project historically fixed does not ship — that is the single
blocking outcome in both the initial backtest and every later update.
Disclaiming is the cheapest way to make an awkward routing disappear, which makes
it the easiest way to pass a quality gate while making the model worse. A
disclaimer added *because* an item routed badly is reverse-engineered from the
answer; it still has to be true of the project as it is, cite a real source, and
stay inside the scope that source covers.
## The update loop, and why §1.15 is fenced
Known non-findings (§1.15) are the recurring-false-positive list, and the section
an automated triager is fed verbatim as a negative prompt. They are also **first**
in the disposition precedence order, which means an entry there pre-empts every
scope, configuration, dependency, adversary, and property check below it.
A loose entry therefore does not mis-classify one report — it suppresses a class
of them, ahead of every other safeguard the model has. Hence the fence:
- Only two routes feed §1.15: a recurring `BY-DESIGN: property-disclaimed` close,
and an already-established known non-finding recurring again.
- Nothing under `OUT-OF-MODEL:*` is ever promoted. Those routes sit *below* §1.15
in the precedence order; relabelling one lifts it above the checks that decided
it, and the class widens without anyone deciding to widen it.
- Match conditions describe the behaviour of the code, never the quality of the
report. *No reproducer* is not a disposition — an unreproduced report stays
open pending a reproducer.
- The discharging claim must exist in the model and must cover the entry's
component.
- Two independent occurrences minimum. One close is a decision; two with the same
discharging claim is a pattern.
The other half of the update loop is the **gap list**, and the uncomfortable
entries are the valuable ones. A model that routes an item the opposite way to
how the team actually resolved it, or a rejection a reporter successfully argued
the team out of, is the model failing in the expensive direction under real
conditions with an independent reviewer. Two reversals against the same claim
mean that claim is wrong.
A canned response in
[`<project-config>/canned-responses.md`](../../projects/_template/canned-responses.md)
that cites no model section is a gap by definition — the project is stating a
position in reporter mail that its own model does not hold. That is precisely
what the drafting rule in
[`<project-config>/security-model.md`](../../projects/_template/security-model.md)
warns about, and this loop is what closes it.
## Adopter configuration
The three skills read
[`<project-config>/security-model.md`](../../projects/_template/security-model.md):
the authoritative model URL, the repositories in scope and their shape (in-repo
model or pointer to an umbrella), the private list to route substantive
conversation to, and the branch and license-header conventions the PR helper
needs. Everything else comes from `project.md`.
## Cross-references
- [`README.md`](README.md) — the security skill family index.
- [`process.md`](process.md) — the report-handling lifecycle whose dispositions
feed the update loop.
- [`confidentiality.md`](../confidentiality.md) — what may cross to a public
surface.
- [`poc-handling-policy.md`](poc-handling-policy.md) — what an agent may do with
reporter-supplied proof-of-concept code encountered while mining the corpus.