Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Security model

The repository being scanned is treated as the attacker. Its documents, paths, Git objects, packfiles, index, and policy file all came from whoever wrote the pull request, and the scanner’s whole job is to be a safe, pure function of those hostile bytes.

The engine executes nothing. No plugin system, no configurable commands, no formatter calls, no git subprocess. A policy file that names a command or a plugin is not a feature request to decline politely: the field is unknown, the configuration is invalid, the run ends incomplete, and the emitted report cannot be mistaken for a complete pass. Process creation belongs to the separate amiss-bootstrap executable; it is not a capability exposed by the scanner engine.

The engine has no network acquisition interface and does not fetch missing objects. It never writes to the repository, which the no-write tests check both by comparing the tree and by scanning a read-only repository. Attempts to make it read outside the repository run into the never-follow-links rule described in Snapshots.

Parsers are the biggest attack surface and receive fuzz targets and pinned conformance corpora. Document byte admission is charged before parsing. Parser node and nesting totals, however, are measured and charged only after the grammar returns; they are output budgets, not a general CPU deadline inside the parser. The order is explicit in the scan pipeline. One budget does act inside the parse: every candidate close of an MDX code region charges the accumulated region against the aggregate-embedded-code-evaluation-bytes-per-snapshot ceiling before the lexical scan reads it, which bounds the one measured quadratic case. The history of that case is in the corpus notes.

A Markdown parser panic is caught and converted to PARSER_PANIC against the document that caused it instead of aborting the process. The known panic fixtures live in the conformance corpus and tests pin that classification. This protects the run from that failure mode; it does not turn the post-parse node limits into a wall-clock guarantee.

Output is part of the surface too. Repository paths end up in terminals and CI logs, so the human format escapes every byte outside printable ASCII. An ANSI escape sequence, a carriage return, or a forged ::error:: workflow command embedded in a filename reaches the log only as harmless \uXXXX text. A path that is raw bytes rather than text renders each such byte as the two-digit escape of its value, never inventing a character the bytes never encoded. The JSON report keeps fidelity its own way, the exact original string for a UTF-8 path and a bytes_hex object for anything else, because the log needs safety and the report needs fidelity, and those are different channels with different rules. The Action separately HTML-escapes repository-controlled targets before placing them in its Markdown summary and applies GitHub workflow-command escaping to annotation paths and messages.

Two delivery paths need different trust descriptions. The root Action dispatcher makes conventional source release tags usable by delegating to the same version’s immutable action/vX.Y.Z runtime. That immutable second ref is part of a source-tag or source-commit pin; users that require one complete tree can pin the generated runtime tag or commit directly. The runtime is a GitHub convenience event wrapper. It verifies the selected engine’s digest against the release manifest carried in the same action tree, then launches the engine directly. That detects an inconsistent tree, but the manifest is not an independently acquired trust anchor, and this lane does not use bootstrap’s supervisor; it enforces its own wall-clock watchdog, 120 seconds unless the workflow sets the watchdog-seconds input, and a scan that outlives the window ends with no result. The manifest’s build-source host is supplied explicitly and its repository identity is forge-neutral, as pinned by the release validation tests; that prevents a format-level github.com assumption but does not authenticate the supplied identity.

The separately executable amiss-bootstrap implements the stronger local handoff. It bounded-captures a canonical request triplet for commit-pair materialization; requires a complete repository, URL-dialect, candidate-ref, target-ref, and default-ref identity; matches the embedded execution constraint and trusted-time provider/run tuple; verifies that both commit objects were acquired before launch; and validates the action tree and runtime closure. It then starts only the verified engine, in the supplied repository, with a cleared environment and one private argument. A magic value, three bounded lengths, and the exact request bytes travel in evaluation/snapshot/controls order over stdin. Arbitrary engine arguments are not part of this path.

After the run, bootstrap acceptance rejects an unavailable hybrid and binds the engine, profile, commits, candidate and protected target refs, and candidate identity recomputed from the report. It requires exact organization-floor, debt-snapshot, and waiver-bundle presence, digest, and trust source; binds the provider run and trusted instant; and checks the execution-constraint and trusted-time digests against their recomputed semantics. Constraint trust source is bound too. Acceptance also requires the report to retain self-asserted sandbox assurance, local-process enforcement, and no sandbox verification. Clearing the environment, fixing the executable and input, validating runtime closure, and enforcing the 120-second watchdog are meaningful controls; they are not an OCI sandbox or microVM and must not be reported as a provider-verified sandbox. The accepted engine envelope is republished unchanged, so it does not gain an authenticated signature merely by passing through bootstrap.

Provider authentication belongs outside both executables. The controller/ crates keep the raw delivery untrusted until a configured verifier accepts it, keeps provider and storage dependencies out of the scanner, and stops when ownership cannot be proven. Controller delivery defines the neutral record, heartbeat, race, and retry rules.

The source-built provider services are the concrete independent lanes. Their bounded plaintext listeners must sit behind an operator-controlled TLS terminator that also bounds connection concurrency and header, body, idle, and slow-body time. GitHub, Gitea, and Forgejo authenticate the exact webhook body before saving it, acknowledge only durable input, and authenticate the saved bytes again in the worker. GitLab instead authenticates the policy job’s short-lived OIDC token and keeps the request synchronous so only the exact passing result makes that protected job succeed. Each endpoint takes a configured in-process permit before reading the body and holds it through durable admission or synchronous evaluation. That cap does not replace the public connection and slow-client limits at the TLS edge.

Each adapter refreshes the repository, change, commits, trees, and protected merge rule through a controller-owned credential. GitHub requires a strict required check bound to its App and writes the Check Run on the test merge. GitLab requires an enforced merge train and independently owned pipeline execution policy, binds the job’s policy origin and runner, and uses the policy job’s result as evidence. Gitea and Forgejo require one approval restricted to the service’s dedicated reviewer and write the final review through that account. A missing, weakened, bypassable, or changed rule stays fail closed.

All lanes acquire exact SHA-1 repository and action commits through Git protocol v2, with fixed pack, object, inflated-byte, resolved-byte, delta-depth, and indexing-thread limits. They invoke the supervised bootstrap and refresh provider state again before accepting or publishing the result. Closure, changed head or gate, removed authorization, missing output, timeout, runtime tampering, or a wrong identity stays fail closed. The pinned action repository must be on the same provider instance.

The runner independently reopens the acquired repositories and checks the exact commit-tree roots. It derives the sealed job, matches the bootstrap to the pinned execution constraint, clears the child environment and standard streams, retains both bounded output handles, and uses ProcessKit’s cross-platform process-tree boundary. Every terminal path hard-kills and drains the group before output is accepted. Lease loss cancels the same tree. These rules cover ordinary process and ownership races; they do not promise that a host kernel operation can be interrupted if that operation itself never returns.

The provider API credential, webhook key ring or OIDC keys, artifact bearer token, execution constraint, optional controls, bootstrap, TLS terminator, scratch directory, raw inbox where used, delivery ledger, and artifact root are trust roots. Provider and repository administrators who can change the protected merge rule, integration or policy owners, reviewer-account owners, key issuers, and configured bypass actors are also inside the boundary. Repository bytes are not. A deployment is only as independent as its host and those operator-controlled inputs. Self-hosted instances must expose the exact APIs required by their lane and a certificate chain accepted by the Rust TLS clients; there is no insecure-TLS mode.

The inbox and ledger use checksummed ordinary files, not SQL or a database. Their roots must be pre-created private local directories outside the repository and action tree; shared and network filesystems are unsupported. Checksums detect damage, not a malicious local writer. A webhook inbox removes raw bytes after controller completion. The ledger retains running and saved work and keeps GitHub and Gitea-family exact-body completion markers permanently, because those signatures contain no trusted delivery time. A full store rejects new identities instead of evicting accepted work.

The separate artifact store retains exact report and external-assessment bytes only for its configured lifetime. It is bearer-authenticated and bounded by records, total bytes, bytes per evaluation, and expiry. It does not decide replay, freshness, debt, waivers, or acceptance. Corrupt, absent, expired, or full artifact state fails a new publication closed rather than publishing a locator the service cannot honor.

The resulting Check Run, policy-job result, or dedicated review is provider evidence, but the engine report remains an unchanged, self-asserted envelope. The controller neither signs it nor upgrades its sandbox claim. Provider-verified controls gives the exact setup, configuration, limits, storage rules, freshness and retry limits, rotation, and report distinction. No provider update is atomic with the local ledger; each provider page states its reconciliation limit. The GitHub convenience Action still invokes the public scanner directly and does not gain this trust boundary.

Verified consumption

The convenience Action’s manifest check is a coherence check, not a boundary: the manifest and the binaries arrive in the same tag tree, so whoever could rewrite one could rewrite both. The boundary a workflow can hold without operating a provider lane is the release attestation, whose trust root is sigstore and GitHub’s build records rather than anything this repository ships. Every release binary is attested at build, the release workflow verifies its own uploads before finishing, and with immutable releases the tag and its assets are locked at the platform after publication.

To run only what the attestation vouches for:

- env:
    GH_TOKEN: ${{ github.token }}
  run: |
    gh release download v<reviewed-version> --repo HardMax71/amiss --pattern amiss-linux-x86_64
    gh attestation verify amiss-linux-x86_64 --repo HardMax71/amiss \
      --signer-workflow HardMax71/amiss/.github/workflows/release.yml
    chmod +x amiss-linux-x86_64
    ./amiss-linux-x86_64 check --repo . --object-format sha1 \
      --base "$(git rev-parse HEAD^)" --candidate "$(git rev-parse HEAD)" \
      --profile enforce --format json > amiss-report.json

The verification proves the binary came from this repository’s release workflow on GitHub’s runners, byte for byte. It does not authenticate what the workflow around it does: a required check whose inputs and selection an opposing author cannot influence is still the provider lanes’ job. There is no launcher in the action tree; a verifier the artifact itself supplies could never vouch for the artifact, which is why this lane is a recipe over platform primitives rather than a file in the release.

Last change: , commit: b8c2d324