# Pre-GA Penetration Test Signoff Record

This file is the formal release-blocker controlled by V1/TODOS.md §28.20. A GA
release is **not authorised** unless every cell below is filled in with a real
engagement record. CI parses this file (see
`scripts/security/pentest/verify-signoff.mjs`) and rejects a release tag when
any of the required fields are missing or stale.

## Engagement record schema

Each engagement adds a new row to the table below. Rows are append-only — never
edit a past row. The fields:

| Field             | Notes                                                                                                            |
| ----------------- | ---------------------------------------------------------------------------------------------------------------- |
| `engagement_id`   | Vendor's reference for the engagement (e.g. `ACME-2026-04`).                                                     |
| `scope_version`   | Version of `pre-ga-scope.md` the engagement was run against.                                                     |
| `window_start`    | ISO date the active testing window opened.                                                                       |
| `window_end`      | ISO date the active testing window closed.                                                                       |
| `findings_count`  | `critical/high/medium/low/info` counts at engagement close.                                                      |
| `open_at_release` | Count still open at the release tag (must be 0 critical, ≤1 high, no AuthN/RBAC/privacy regardless of severity). |
| `release_tag`     | The release this engagement signs off. Must match the Git tag being cut, e.g. `vN.M.P`.                          |
| `signoff_sec`     | Name + ISO date – head of security.                                                                              |
| `signoff_eng`     | Name + ISO date – head of engineering.                                                                           |
| `signoff_priv`    | Name + ISO date – head of privacy (required if any privacy-tagged finding).                                      |

## Engagements

<!-- BEGIN ENGAGEMENTS -->

| engagement_id | scope_version | window_start | window_end | findings_count | open_at_release | release_tag | signoff_sec | signoff_eng | signoff_priv |
| ------------- | ------------- | ------------ | ---------- | -------------- | --------------- | ----------- | ----------- | ----------- | ------------ |
| _none yet_    | _–_           | _–_          | _–_        | _–_            | _–_             | _–_         | _–_         | _–_         | _–_          |

<!-- END ENGAGEMENTS -->

## Release gate rules

A release tag is **valid** only if the most recent engagement row satisfies
_all_ of the following:

1. `release_tag` exactly matches the tag being cut.
2. `open_at_release.critical == 0`.
3. `open_at_release.high <= 1`.
4. No open finding in the AuthN, RBAC, or privacy categories regardless of
   severity. This cannot be read off the row, so it is carried by a mandatory
   `tags-summary:` footer below the table (see "Category footer"). A missing,
   empty, or incomplete footer fails the gate — absence is not a clean result.
5. `signoff_sec` and `signoff_eng` are non-empty. `signoff_priv` is required
   whenever the footer declares any privacy state other than `privacy:none`.
6. Each non-empty signoff date is within 14 days of `window_end`. The gate runs
   while the tag is being cut and so cannot read a tag date; `window_end` is the
   anchor it enforces, and it is what keeps a signature from being older than
   the engagement it signs.

Failing any rule above prints the failing rule to stderr and exits with a
non-zero code — the release pipeline then refuses to flip the traffic gate.

## Category footer

Every engagement row must be followed by one machine-readable line declaring a
state for each of the three blocking categories:

```
tags-summary: authn:closed, rbac:closed, privacy:none
```

Permitted states are `open`, `closed`, and `none` (no finding in that category
was raised). `<category>:open` blocks the release under rule 4. Any privacy
state other than `privacy:none` makes `signoff_priv` mandatory under rule 5.

## Red-team exercises

See `../red-team/scenarios.md` for the matching red-team scenario set; the
red-team result file references this record by `engagement_id`.
