| name | threat-model |
| description | Derive a project's security contract from its source and docs, then emit it as structured data other skills can cite. Records what the project assumes about callers and inputs, what properties it claims and disclaims, which code is out of scope, and which recurring tool findings are known-safe. This is not a vulnerability scan; it produces the trust map that security-deep-dive loads instead of re-deriving boundaries per run. |
| license | MIT |
| compatibility | Reads ./src only. Optionally pulls repo-overview and advisories from the scrutineer API for context. No registry or upstream network access required. |
| metadata | {"scrutineer.version":1,"scrutineer.output_file":"report.json","scrutineer.output_kind":"threat_model"} |
threat-model
Produce the security contract this project implicitly offers its callers: what it assumes, what it guarantees under those assumptions, what it explicitly leaves to the integrator, and which reported "findings" are noise given the model. The output is data, not an audit. Do not hunt for bugs, do not list CVEs, do not recommend fixes. Describe the project as it is.
You are running headless with no maintainer to consult. Every claim you write is therefore either documented (you can cite a file and line in the repo, a manpage, a header comment, an FAQ, a closed issue) or inferred (you reasoned it from code structure or domain knowledge). There is no third state. Anything inferred also goes in open_questions so a human can later confirm or strike it; until then, downstream skills treat inferred claims as working hypothesis rather than citable fact.
Workspace
./src is the cloned repository.
./context.json has repository.url, repository.full_name, and a scrutineer block with api_base, token, repository_id. If scrutineer.scan_subpath is set, scope the model to that subtree and say so in the header.
./report.json is the structured contract; write it to match ./schema.json. This is the only file the worker keeps.
Scrutineer API (optional, Authorization: Bearer {token}):
GET {api_base}/repositories/{repository_id}/scans?skill=repo-overview&status=done then GET /scans/{id} for the one-paragraph project description if you would otherwise have to write one cold.
GET {api_base}/repositories/{repository_id}/advisories for prior published advisories. A pattern across past CVEs ("historically overflows on 32-bit size_t") is a model claim; the CVE list itself is not.
If either returns empty or non-200, carry on without it.
Orient
Spend minutes, not hours. You are forming hypotheses, not findings.
Read README*, SECURITY*, THREAT*, anything under docs/ or doc/ with security in the name, header-file commentary, FAQ, NOTES, CAVEATS, LIMITATIONS, CHANGELOG entries that explain why behaviour is the way it is. Search the issue tracker references in the repo (issue numbers in comments and commit messages) for "wontfix", "by design", "not a bug", "out of scope", "won't fix". These are maintainer positions already on record; they are documented sources, the highest authority you have. A claim you find here is not inferred even if you would have guessed the same thing.
If SECURITY.md or an equivalent contains threat-model content (what the project trusts, what counts as a vulnerability, examples of non-vulnerabilities), lift every such statement directly into the matching report field with a citation. Your output must be a strict superset of what SECURITY.md already asserts about scope. If you think an existing claim is wrong, record that in open_questions; do not silently override it.
Identify the public surface: exported symbols, CLI subcommands, listening ports, file formats consumed and produced, environment variables read.
Carve the project into component families with distinct trust profiles. A typical library has a pure-computation core, a convenience layer that touches the OS (file helpers, env readers, socket wrappers), and shipped-but-unsupported code (contrib/, examples/, test/, vendor/, third_party/, bindings/, demo apps). A daemon or service usually has client-facing endpoints, an admin or operator surface, and peer-to-peer protocol handling. Model each family at its own trust level; do not average them.
Note what the project clearly is not. "Parser, not a network service." "In-process library, not a sandbox." This shapes everything below.
What to record
Fill every field below. A field that does not apply gets not_applicable: true with a one-line reason, never silence.
Components
One entry per family from the orient pass. Name, representative entry points, whether it touches anything outside the process (filesystem, network, env, child processes, signals, global state), and whether it is in or out of this model. Anything marked out here must have a reason and reappears in out_of_scope.
Out of scope
Use cases the project does not aim to support. Threats it does not defend against, with the reason ("not a security boundary", "wrong layer", "caller's job"). Directories that ship in the repo but are not covered: state the policy plainly so deep-dive does not extend the core's guarantees to examples/ by association.
Trust boundaries
Where the line sits. For most libraries: "the public API surface; everything inside assumes the caller authenticated the data." For a service: name each role (unauthenticated client, authenticated client, operator, peer) and what each is trusted with. For each component family, state the reachability precondition a finding must meet to matter: "a finding in inflate.c is in-model only if reachable from compressed input bytes; a finding in gzopen path handling is in-model only if the path is sourced from outside the calling process."
Entry-point trust table
The single most-consumed field downstream. One row per parameter of every public entry point that accepts external data:
| entry_point | parameter | attacker_controllable | caller_must_enforce | provenance |
For a library, entry_point is a function or method. For a service, it is a route, RPC, or protocol message, and rows cover headers and connection metadata, not just bodies. attacker_controllable is yes, no, or conditional with the condition. caller_must_enforce is the invariant the caller is on the hook for ("output buffer >= len", "path not from untrusted user", "format string is a literal"). Group by component family if the table is large. Prose is not enough here; deep-dive looks up exact parameters.
Environment assumptions
OS, runtime, allocator, threading, integer width, byte order, clock, filesystem semantics the code relies on. Then the negative inventory: what the project does not do to its host. Open sockets, spawn processes, install signal handlers, read env beyond a documented allowlist, write to stdout/stderr outside a logger, mutate locale or FPU or other process-wide state. These are negative claims and almost never written down, so they will be mostly inferred. Record them anyway; an integrator embedding the project needs the list.
Build and configuration variants
Compile-time defines, feature flags, cargo features, build tags, runtime config keys that change which security properties hold. For each: name, default value, effect on the model, whether docs discourage it. If the default is the less-secure value, record that explicitly; deep-dive needs to know whether a finding under the default is valid or out_of_model_non_default_build. If no such knobs exist, say so.
Adversary model
Who the assumed attacker is (network peer, user of the embedding app, local unprivileged process, co-tenant, authenticated-but-byzantine peer). What capabilities they have and lack. Which actors are explicitly out: "an attacker already in the calling process has won and is not modelled." For consensus or replicated systems, state the honest-fraction threshold under which the model holds.
Properties provided
Each property the project actually commits to, in docs, tests, or recorded maintainer statement. Do not invent properties. For each: the property and conditions; the violation symptom (crash, OOB read, OOB write, info leak, hang, wrong output, unbounded allocation, fork/divergence for distributed systems); a severity tier (security, meaning a break warrants coordinated disclosure, or correctness, meaning ordinary bug); and provenance. For resource properties, state the threshold, not just the direction: "super-linear in input size is a bug, constant-factor blowup is not", or "no resource guarantee is made". If you cannot find a documented threshold, mark the field inferred and record the open question.
Properties not provided
The companion list, and the most useful one for an integrator. State each plainly: "no constant-time guarantees", "no decompression-bomb defence", "no input authentication". Call out false friends separately: features that look like a security property but are not one. The shape is "X is provided for purpose A; it is sometimes mistaken for B, which it does not satisfy." A CRC that looks like a MAC, a non-cryptographic hash that looks collision-resistant, a PRNG that looks like a CSPRNG, a sandbox flag that isolates resources but not security. Then name the well-known attack classes against this category of project that the project leaves to the caller: compression-oracle for compressors, XXE for XML, ReDoS for regex engines, billion-laughs for recursive formats, SSRF for URL fetchers. One line each.
Downstream responsibilities
What the caller (for a library) or operator (for a service) must do for the assumptions above to hold. Action-oriented, one line each. Every build variant whose default you marked dev-only reappears here as "set X before exposing the service."
Known misuse
Ways the API permits being called that violate the assumptions. Passing untrusted data to a trusted-only parameter, using the project as a security boundary it is not, exceeding documented limits, mixing modes that are not synchronised. For each: what it looks like, why it is unsafe, what to do instead.
Known non-findings
Patterns that scanners, fuzzers, or reviewers repeatedly flag against this project that are not bugs given the model. For each: what the tool reports (function, file, message pattern), why it is safe (cite the entry-point row or property that discharges it), and where applicable a suppression hint. Sources: comments in the code that say "this is safe because", // nolint / # noqa / // NOSONAR annotations with reasons, closed issues where a maintainer explained why a scanner hit is not a bug, prior advisories that were rejected. This list is fed back to semgrep and deep-dive as a negative filter; populate it even if short.
Open questions
Every inferred claim in the fields above gets an entry here: the claim, which field it lives in, and the proposed answer a maintainer would confirm or correct. This is a flat list, not a dialogue. Downstream skills read it to know which parts of the model are load-bearing guesses.
Triage dispositions
These are the labels deep-dive's ruled_out[].reason should use when this model is loaded. They are fixed; record them in report.json so the consumer does not hardcode them.
| label | meaning | backed by |
|---|
valid | violates a properties_provided entry via an in-scope adversary and attacker-controllable input | properties_provided, entry_points, adversaries |
valid_hardening | no claimed property violated, but the API makes a known_misuse easy enough that a fix is reasonable | known_misuse |
out_of_model_trusted_input | requires attacker control of a parameter the entry-point table marks no | entry_points |
out_of_model_adversary | requires an attacker capability adversaries.out_of_scope excludes | adversaries |
out_of_model_unsupported_component | lands in code out_of_scope places outside the model | out_of_scope, components |
out_of_model_non_default_build | only manifests under a discouraged or non-default build_variants entry | build_variants |
by_design_disclaimed | concerns a property properties_not_provided explicitly disclaims | properties_not_provided |
known_non_finding | matches a known_non_findings entry | known_non_findings |
model_gap | cannot be routed to any of the above; the model needs revising | open_questions |
Output
Write ./report.json matching ./schema.json. Shape:
{
"spec_version": 1,
"repository": "https://github.com/owner/repo",
"commit": "abc1234",
"date": "2026-05-08",
"scope_subpath": null,
"description": "one paragraph, what this project is, for someone who has never seen it",
"confidence": {"documented": 14, "inferred": 22},
"components": [
{"name": "core", "entry_points": ["inflate", "deflate"], "touches": [], "in_scope": true, "provenance": "documented", "source": "README.md:12"},
{"name": "gz file helpers", "entry_points": ["gzopen", "gzread"], "touches": ["filesystem"], "in_scope": true, "provenance": "inferred"},
{"name": "contrib", "entry_points": [], "touches": [], "in_scope": false, "reason": "separately authored, no review", "provenance": "documented", "source": "contrib/README"}
],
"out_of_scope": [
{"item": "contrib/", "reason": "separately authored minizip etc; threat-model separately", "provenance": "documented", "source": "contrib/README"},
{"item": "side-channel adversaries", "reason": "no constant-time claims", "provenance": "inferred"}
],
"trust_boundaries": [
{"component": "core", "boundary": "public API surface; caller is trusted, input bytes are not", "reachability_precondition": "reachable from the compressed input stream", "provenance": "inferred"}
],
"entry_points": [
{"entry_point": "gzopen", "parameter": "path", "attacker_controllable": "no", "caller_must_enforce": "path sanitisation if sourced from user", "provenance": "inferred"},
{"entry_point": "gzread", "parameter": "file contents", "attacker_controllable": "yes", "caller_must_enforce": "output buffer >= len", "provenance": "documented", "source": "zlib.h:1400"},
{"entry_point": "gzprintf", "parameter": "format", "attacker_controllable": "no", "caller_must_enforce": "literal only, never from input", "provenance": "documented", "source": "zlib.h:1467"}
],
"environment": {
"assumes": ["conformant C runtime", "size_t large enough for input"],
"does_not": ["open sockets", "spawn processes", "install signal handlers", "read env"],
"provenance": "inferred"
},
"build_variants": [
{"name": "ZLIB_INSECURE", "default": "off", "effect": "removes gzprintf overflow guard", "discouraged": true, "provenance": "documented", "source": "zconf.h:88"}
],
"adversaries": {
"in_scope": ["whoever supplies the compressed input"],
"out_of_scope": ["caller already in the host process", "side-channel observer"],
"provenance": "inferred"
},
"properties_provided": [
{"property": "memory safety on well-formed bounded input", "conditions": "supported platform, conformant C runtime, stream correctly initialised", "violation_symptom": "OOB read/write", "severity_tier": "security", "provenance": "documented", "source": "SECURITY.md:8"}
],
"properties_not_provided": [
{"property": "bounded output size on hostile input", "reason": "decompression bombs are caller's problem", "false_friend": false, "provenance": "documented", "source": "FAQ:33"},
{"property": "integrity against malicious sender", "reason": "CRC-32 is error detection, not a MAC", "false_friend": true, "provenance": "inferred"}
],
"attack_classes": ["compression oracle (CRIME/BREACH) when compressing secrets alongside attacker-chosen plaintext"],
"downstream_responsibilities": [
"cap decompressed output size or wall-clock budget",
"do not pass gz* file paths sourced from untrusted users without sanitisation"
],
"known_misuse": [
{"pattern": "treating gzip CRC as integrity against a malicious sender", "why_unsafe": "CRC-32 is not a MAC", "instead": "HMAC the compressed stream before decoding"}
],
"known_non_findings": [
{"reported_as": "unchecked malloc return in examples/", "why_safe": "examples/ is out of scope", "cites": "out_of_scope[0]"},
{"reported_as": "strcpy in gzlib.c", "why_safe": "length bounded by GZBUFSIZE check at gzlib.c:112", "cites": "properties_provided[0]"}
],
"dispositions": ["valid", "valid_hardening", "out_of_model_trusted_input", "out_of_model_adversary", "out_of_model_unsupported_component", "out_of_model_non_default_build", "by_design_disclaimed", "known_non_finding", "model_gap"],
"open_questions": [
{"claim": "gzopen path is caller-trusted", "field": "entry_points", "proposed": "yes, caller chooses the file; library does not sanitise"},
{"claim": "no resource bound on inflate output", "field": "properties_not_provided", "proposed": "confirmed by FAQ but no explicit threshold given"}
]
}
Set repository to context.json's repository.url and commit to git -C ./src rev-parse HEAD. Use today's date.
What to leave out
CVE lists. Code-review findings ("function X does not check return of Y"). Build and release hygiene (action pinning, signing, 2FA). Anything the README already says verbatim. "Use defence in depth." Speculation about future features. If you find yourself writing "the project should", stop; that is audit output.
Self-check
Before writing the report: every field is substantive or marked not applicable with a reason. Every claim has a provenance tag. Every inferred claim has a matching open_questions entry. properties_not_provided and downstream_responsibilities are at least as long as properties_provided; if not, you have under-specified. The entry-point table has rows, not prose. Components with different trust profiles are separated, not averaged. A reader who has never seen the project can answer "which threats has this taken on, which are mine."