# `Relyra.Security.XML.CorpusGate`
[🔗](https://github.com/szTheory/relyra/blob/v1.1.0/lib/relyra/security/xml/corpus_gate.ex#L1)

Runtime security-corpus gate for the Phase 21 scheduled-refresh path
per D-21.

Every existing security-corpus fixture acts as a refusal trigger on the
scheduled path. If freshly-fetched metadata trips a known-bad shape
(xml-crypto 2025 signature-wrapping family, PortSwigger Fragile-Lock
namespace-confusion shapes, etc.), this gate refuses the apply with
`{:error, %Relyra.Error{type: :corpus_violation}}` so the wrapper sets
`auto_suspended_reason: :corpus_violation` per the LOCKED enum.

The corpus manifest physically lives at `priv/security_corpus.json` so
both this runtime gate AND the test corpus reader at
`test/security/xml/corpus_security_test.exs` can read the same source
of truth without crossing the lib/test boundary in either direction.

## Detection model

The gate evaluates the candidate XML against byte-level refusal shapes
derived from each manifest fixture's `expected_error_type`. The
pre-parse refusal shapes (`:doctype_forbidden`, `:entity_expansion_forbidden`,
`:payload_too_large`) catch the trust-boundary attacks the gate is
responsible for at the metadata-XML byte level — these are the same
pre-parse early-rejection conditions the hardened parser
(`Relyra.Security.XML.PureBeam`) already enforces, but routed through
the Phase-21 `:corpus_violation` typed error so the wrapper can set the
matching `auto_suspended_reason`.

Pure: no Repo, no Req, no telemetry. Reads `priv/security_corpus.json`
once at compile time (a `@manifest` module attribute).

# `check`

```elixir
@spec check(
  binary(),
  keyword()
) :: :ok | {:error, Relyra.Error.t()}
```

Checks freshly-fetched metadata XML against the LOCKED security-corpus
regression fixtures. Returns `:ok` if no fixture's refusal-shape
matches; `{:error, %Relyra.Error{type: :corpus_violation, details:
%{matched_fixture_id: id, class: class, expected_error_type: type}}}`
if any does.

`xml` is the raw bytes; `opts` accepts `:max_bytes` to override the
default 5 MB cap that mirrors the Phase-21 stricter Req profile (D-20).

# `manifest`

```elixir
@spec manifest() :: [map()]
```

Returns the loaded manifest fixtures (one map per fixture).

# `manifest_path`

```elixir
@spec manifest_path() :: String.t()
```

Returns the path the gate reads the manifest from at compile time.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
