Skip to content

Security

What protects this project, what will protect the application, and how to report a problem privately.

Reporting a vulnerability

Report suspected vulnerabilities privately through GitHub's security advisories, at https://github.com/manifest-identity/manifest-identity/security/advisories/new, which is the Security tab of this repository, then "Report a vulnerability." Do not open a public issue, pull request, or discussion for a suspected vulnerability before it has been triaged.

A useful report includes the affected file or behavior, the impact as you understand it, reproduction steps or a minimal proof of concept, and any conditions required. Do not include real credentials or personal data in a report.

What to expect after reporting: acknowledgment within seven days, a triage answer within fourteen, and a status note at least every two weeks while a fix is in progress. Confirmed vulnerabilities are fixed before they are discussed publicly, and public disclosure is coordinated with the reporter, with ninety days as the default outer bound if a fix stalls. Reporters are credited in the advisory unless they ask not to be. Some reports will be answered with a pointer to a documented, accepted risk; those are listed in THREAT-MODEL.md, and that answer arrives with the same timelines. This is one person's project: there is no bounty program, and the timelines above are commitments of attention, not of around-the-clock response.

Controls in place

The application exists and grows subphase by subphase; the controls here guard the repository, the pipeline, and the application code that has landed. Each is a mechanism that runs, not a rule that hopes.

Control What it guards against
TruffleHog pre-commit hook, offline mode A credential reaching a commit on this machine (D-002)
TruffleHog in continuous integration, verification on, less the one detector whose pattern is this repository's own action pinning A credential in any pushed history, checked against its provider to learn whether it is live; the excluded detector's own pattern is covered at the server layer (D-059)
Vale pre-commit hook and continuous integration job Writing-rule violations reaching history
Deferred-work marker gate, pre-commit and continuous integration Stub markers standing in for finished work or recorded decisions
Continuous integration actions pinned by full commit hash, refused by a gate if any use is not A moved tag changing what the pipeline runs; the gate reads the workflows rather than a second copy of each pin (D-061)
Pipeline tools downloaded from canonical releases and checksum verified A substituted tool running inside the pipeline
Every dependency pinned by hash, installs refuse anything else A package differing from the reviewed tree, from any index, for any reason
Software bill of materials, regenerated by every pipeline run A stale or hand-edited inventory; the current one is the sbom artifact on the latest checks run, and GitHub's dependency graph offers its own export built from the same pinned file
Automated update review across pip, actions, and container digests, monthly for version updates and immediate for advisories A pin going stale silently; updates arrive as reviewable pull requests, and a vulnerability does not wait for the version schedule (D-060)
Container bases pinned by digest, each digest with one home the pipeline reads A moved tag changing what builds or runs, and a half-applied update landing because a second copy of the digest went unmoved (D-061)
Deep static analysis on code and workflows, per push and weekly A dataflow-level flaw the linter's pattern rules cannot see (D-025)
Workflow lint and security audit in the pipeline A mistake in the files that gate everything else; both tools found and fixed real findings here before adoption
Posture scorecard, externally run and published A silent drift in the repository's own practices; the score is checkable, not claimed
Container file linted in the pipeline A container-build mistake the reviewed file carries silently
Base image operating system packages scanned, blocking on critical findings that have fixes on the schedule and on main, and the built image scanned the same way on every change, with Debian's updates applied at build time A vulnerable base staying pinned after its fix shipped, and a shipped image carrying a fix Debian has published; unfixed findings are reported, not alarmed on, and a pull request is blocked only by what it can fix (D-037, D-041, D-055)
The checks workflow runs weekly on a clock beside its change triggers A fix or advisory appearing during a quiet week going unseen until an unrelated pull request takes the blame (D-043)
Coverage floor on the test suite The suite quietly shrinking below what the controls tables claim it proves
Mutation check: seven controls broken one at a time, the suite must notice each A control whose proving test is a claim; the check found and closed one such gap at adoption (D-041)
The two import parsers fuzzed under AddressSanitizer, on every change touching them and weekly An input from another system that escapes the parsers' named refusals and reaches an exception nobody wrote (D-054)
Release assets and the published container image attested; the image digest verifiable against the transparency log A consumer unable to check what was released, or a registry image differing from the reviewed build (D-050, D-054)
Code-owner review required by the ruleset A change merging without the named owner's approval (D-054); the up-to-date requirement that came with it was withdrawn after its cost was measured (D-058)

At the repository's visibility flip, the server layer joined: GitHub secret scanning and push protection, completing the three scanning layers.

Application controls in place, each with its proving test

A control listed without its test is a claim, not a control, so every row names the tests that fail if the control disappears. Threat numbers refer to the ranked threats in THREAT-MODEL.md.

Control Threat it answers Proven by
Authentication on every request; 401 without a valid session 2, 6 tests/test_matrix.py, tests/test_auth.py
The role matrix as one source driving enforcement and tests, with a drift test refusing ungoverned routes 2 tests/test_matrix.py
Timing-equalized login: unknown names pay the same bcrypt cost and receive the identical body 2 tests/test_auth.py
Sessions stored only as token hashes, individually revocable, absolute expiry (D-026) 6 tests/test_auth.py
Sign-in rate limiting per username and per address, failures only (D-027) 2 tests/test_ratelimit.py
Bounded, in-memory, claim-verifying ingestion on both file types (D-008, D-030) 4 tests/test_ingest.py, tests/test_ingest_authz.py, both property suites
Append-only observations; re-imports rejected; state derived at read (D-006) 3, 5 tests/test_ingest.py
Audit rows in the acting transaction, attributed (D-011) 8 tests/test_auth.py, tests/test_ingest.py
Audit rows hash-chained, the chain head anchored in every evidence export, a walk that names the first altered or removed row (D-057) 3, 8 tests/test_audit_chain.py
Validation and login failures echo nothing the caller sent 2 tests/test_validation.py, both property suites
Identities keyed immutably; resurrection mints a new identity and is surfaced (D-016, D-029) 11 tests/test_ingest.py, tests/test_ingest_authz.py
The page renders every value as text and contains no markup sink; a scan gates it (D-036) 7 tests/test_frontend.py
Content policy forbids inline script and style; the page needs neither 7 tests/test_frontend.py
The session token lives in memory, never in browser storage (D-036) 6 verified in the browser at the subphase review
Governance records attributed and audited in the acting transaction; owner typed, assigned outranking the tag, disagreement surfaced (D-038) 8 tests/test_governance.py
Campaigns freeze their population at creation, one item one decision, no bulk path, close refuses open items (D-039) 8 tests/test_campaigns.py
Spreadsheet formula escaping on every CSV cell 7 tests/test_reports.py
The risk report renders through an engine that escapes by default; no script in the artifact (D-040) 7 tests/test_reports.py
The evidence export states population, coverage, and every decision with actor and time 8 tests/test_reports.py
A per-user write budget on imports and campaign creation; keep-alive bounded in the serve command (D-041) 9 tests/test_ratelimit.py
The documented route surface asserted against the live route table, in both directions 2 tests/test_matrix.py
Authority is a binding at a scope node, answered in one function over the node, its ancestors, and the global node (D-072) 2 tests/test_scope.py
Every scoped write refuses a caller whose binding sits outside the target's scope, and says it is the scope 2 tests/test_scope.py
Administration is global in this version: an administrator bound at an account is refused every administrative route (D-070) 2 tests/test_scope.py
Bindings are revoked, never deleted, so who could act and when survives the end of the grant (D-006, D-072) 8 tests/test_scope.py, tests/test_admin_users.py
An authorization names an authorizer taken from the session; no request model carries a field for one (threat 14, D-073) 8 tests/test_authorizations.py
Authorizations are append-only: a renewal supersedes and a revocation is a new row, so no past state is erasable (D-006) 3, 8 tests/test_authorizations.py
Expiry is the clock compared to a column, so a lapsed authorization cannot read as live because a job did not run 15 tests/test_authorizations.py
A person may own an authorization only with a second owner named (D-038) 8 tests/test_authorizations.py
Required fields ship strict, are the administrator's to relax, and every change is audited with its old and new value (D-070) 8 tests/test_authorizations.py
A setting outside the registry cannot be written, and one bad value in a batch changes none of it 8 tests/test_authorizations.py
A file import reads through a named mapping, and every row written names the batch and the mapping that produced it (D-074) 3, 8 tests/test_csv_import.py
A mapping missing a required field refuses the file before a row is read; a missing optional column is reported, never hidden 4 tests/test_csv_import.py
Dates are parsed by a format the mapping declares and never inferred 4 tests/test_csv_import.py
The dry run applies every rule the write applies and writes nothing, so a preview cannot promise a row the write refuses 8 tests/test_csv_import.py
Imported rows pass the same checks the form applies, through the same code, so a door cannot be the weaker one 8 tests/test_csv_import.py
The file reader is bounded on size, rows, columns, and cell length, in memory, refusing the whole file at a bound (D-030) 4, 9 tests/test_csv_import.py
Reading the observed side in the authorized side's shape writes nothing: it prefills a decision and never makes one (D-024) 8 tests/test_from_observed.py
The export of observed grants passes the same formula escaping as every other spreadsheet exit 7 tests/test_from_observed.py
Access arriving through a group is offered with its hop recorded, so an export cannot silently omit a class of privilege 8 tests/test_from_observed.py
The delta is computed at read and stored nowhere, so it cannot go stale, be edited, or disagree with the records it came from (D-006) 3 tests/test_delta.py
Every delta finding carries the last word from each side, so a stale record cannot make a difference look like agreement 15 tests/test_delta.py
The runtime database role holds data rights only, no schema and no deletes; migrations run separately as the owner (D-013, D-051) 3 the pipeline probe: schema change attempted as the runtime role must be refused
An administrator ends all of a user's sessions in one audited act 6 tests/test_admin_users.py
Read-only root filesystems, dropped capabilities, no privilege escalation, bounded resources, and no host-published database port (D-042) 1, 9 verifiable by the commands in the README, run against the live stack at adoption
The compose file keeps those properties: every service drops all capabilities, refuses privilege escalation, and mounts a read-only root 1, 9 scripts/check_compose_hardening.py, in the pipeline and the commit hooks

Controls still planned

Mapped threat-by-threat in THREAT-MODEL.md: deployment-layer encryption at rest (D-020) arrives with the deployment phases, and step-up authentication arrives with the first action that changes a cloud account, which version one deliberately does not contain. The backup, restore, and retention procedures are in README.md, each run against a live stack.

Held to its own bar

This repository requires provenance of everything it consumes: canonical sources, hash-pinned artifacts, checksum-verified tools, maintained upstreams. Asked the same questions, its answers follow, each failure recorded rather than denied (D-044):

Its own requirement Does this repository pass?
A canonical, pinnable released artifact Yes. Releases exist from D-050: signed tags, checksummed artifacts, and build provenance attestations verifiable against the platform's transparency log, and from D-054 the container image publishes to the registry under the version tag with an attested digest, so a consumer can pull and verify rather than build.
Cryptographic authorship Partially. The agent's commits are unsigned, because an app installation pushing over git has no signing key the platform accepts (D-087); the maintainer's merge commits are signed by the platform, every release starts from the maintainer's signed tag, and every artifact carries a build provenance attestation. History before D-044 is likewise unsigned and rests on account control, stated rather than hidden, because backfilling signatures would mean rewriting published history.
A maintained upstream with more than one set of eyes No, one human. Since D-045 the author of record and the approving human are different parties, and code-owner review is required (D-054), but both sides of that review belong to one person's program; stated here and visible in the published scorecard rather than claimed away.
Generation provenance Coarse. The commit trailer names the assisting system but cannot name the exact model per commit, because the harness varies it; the dependency tree resolves to exact artifacts, the authorship record does not.
A software bill of materials Yes, delivered fresh by every pipeline run.
Its own supply chain verified Yes. Dependencies checked against canonical sources and hash-pinned, pipeline tools checksum-verified, actions pinned by commit, bases pinned by digest.

The failures are documented choices for a one-person learning project, each with its reason and, where one exists, its schedule. The row that matters is the difference between this table existing and not existing: an undocumented gap and a considered exclusion look identical from outside, and only the record distinguishes them.

Supported versions

Fixes land on the latest commit of the main branch. There are no versioned releases yet; when releases exist, this section will state which ones receive fixes.