The Security & Compliance Lead applies this checklist to every PR
before posting an Approve / Request changes / Comment review.
Reference: ACCEPTANCE.md §4, the role's
AGENT.md "Mandatory invariants" section, and
SECURITY.md.
Each line below is binary. If any item marked [BLOCK] fails, the verdict is Request changes, not Approve. Items marked [NOTE] become non-blocking comments + follow-up handoffs.
- Tier 1 (docs / forum / folder READMEs)
- Tier 2 (skeletons / stubs / typings)
- Tier 3 (backend code, no security surface)
- Tier 4 (frontend code)
- Tier 5 (any of:
SECURITY.md,LICENSE,pyproject.tomldeps, frontend rootpackage.jsondeps,.github/workflows/, CSRF/auth code, serializer denylist,conf.pydefaults, new public URL patterns, the autonomy policy itself) - Tier 6 (release / publish)
Tier 5+ requires human approval in addition to my Security review. For tier 5 PRs I never auto-merge; I post my review and the human takes it from there.
- [BLOCK]
git diff --cached | grep -iE '(ghp_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|ghs_[A-Za-z0-9]{30,}|aws_secret_access_key|begin (rsa|ec|openssh) private)'returns nothing on the PR's diff. - [BLOCK] No
.env/.env.*/*.pem/*.key/*.crtis added or modified. - [BLOCK] No partial token redactions (
ghp_…XYZ) anywhere in the diff, even in markdown or forum posts. - [BLOCK] No
git configoutput, nogit remote -voutput, noprintenv/envoutput in the diff. - [NOTE] Any new env-var name introduced by the diff is documented (in the relevant README /
SECURITY.md). - [NOTE] Any new
os.environ[...]access is documented as a config knob, not a secret.
- [BLOCK] Every new or changed view consults
permissions.is_admin_user(request)(or equivalent gate that chains throughAdminSite.has_permission) before any model access. (ACCEPTANCE §4.1 S-1 … S-5) - [BLOCK] Every new mutation view consults the matching
ModelAdmin.has_*_permission(request, obj=None)and passes the object for object-level checks. (ACCEPTANCE §4.2 S-6, S-7) - [BLOCK] No new code path uses
user.has_perm(...)directly — always viaModelAdmin. - [BLOCK] Frontend never trusts client-side
permissionscache for server decisions — the backend re-checks every operation. - [NOTE] If the diff adds a new public URL pattern under the package, confirm it sits behind the same gate.
- [BLOCK] Models not in
admin.site._registryreturn 404. The 404 envelope never reveals existence vs registration. (ACCEPTANCE §4.3 S-11 … S-14) - [BLOCK] No
import_string,apps.get_model,__import__,getattr(module, name)called on client-supplied strings. - [BLOCK] No
admin.site.register(...)or@admin.registerindjango_admin_react/(the package reads_registry, never writes).
- [BLOCK] Every list / detail path starts from
ModelAdmin.get_queryset(request). NoModel.objects.all()/objects.filter()indjango_admin_react/api/. (ACCEPTANCE §4.4 S-15 … S-19) - [BLOCK] Search uses
ModelAdmin.get_search_results(...). - [BLOCK] Detail fetch uses
get_queryset(request).get(pk=...)(notModel.objects.get). - [NOTE] Page size is clamped to
MAX_PAGE_SIZE(default 200). If the diff changes this, it has a decision entry.
- [BLOCK] Create / update go through
ModelAdmin.get_form(request, obj)+form.is_valid(). Nosetattr(obj, name, json_value). (ACCEPTANCE §4.5 S-20…S-25) - [BLOCK] PATCH merges initial-from-instance with incoming payload, then validates.
- [BLOCK] Fields in
get_readonly_fieldsandget_excludecannot be written. 400 on attempt; value unchanged. - [BLOCK] DELETE calls
ModelAdmin.delete_model(...). Noobj.delete()shortcuts.
- [BLOCK] No
@csrf_exemptanywhere in the diff. - [BLOCK] Unsafe methods (
POST/PATCH/PUT/DELETE) reject missing or invalidX-CSRFTokenwith 403. (ACCEPTANCE §4.6 S-26 … S-30) - [BLOCK] SPA shell view sets the CSRF cookie via Django's
middleware (
@ensure_csrf_cookieor equivalent). - [BLOCK] No
request.session[...] = ...writes from the package. No reads ofSESSION_COOKIE_*to override Django. - [NOTE] Permission-denied responses include
Cache-Control: no-store.
- [BLOCK] The sensitive-field denylist exists, matches at
least
password / secret / token / api_key / apikey / hash / private_key / session / nonce / salt, case-insensitive, substring match. Applied on top of the admin'sexcludeandreadonly_fields. (ACCEPTANCE §4.7 S-31 … S-36) - [BLOCK] Unknown / unhandled types fall back to
str(value)(neverrepr(value)). - [BLOCK] ForeignKey →
{id, label}, no nested object. - [BLOCK] ManyToMany →
type: "unsupported", never editable in v1. - [BLOCK] No
model._meta.private_fieldsusage for output.
- [BLOCK] No
/api/v1/__debug__/-style endpoints. - [BLOCK] No default open CORS in the package
(
django-cors-headersis the consumer's call). - [BLOCK] 500 envelope never includes stack trace, exception
message, or raw payload — even under
DEBUG=True. - [BLOCK]
PUT/TRACE/CONNECTreturn 405. - [BLOCK] Logs do not include request bodies, response
bodies, cookies,
Authorization/Cookie/X-CSRFTokenheaders, or full query strings.
- [BLOCK] For every new / changed endpoint, the matrix in
ACCEPTANCE §4.15 is fully covered in
tests/test_security.py(or the endpoint-specific test file with explicit reference). - [BLOCK] No
@pytest.mark.skip/@pytest.mark.xfailon a security test without a linked GitHub issue and human approval. - [NOTE] Coverage for
permissions.pyandserializers.pyis at 100 % statements + branches; forviews/*≥ 95 %.
- [BLOCK] Any addition / removal / bump in
pyproject.tomlruntime deps orfrontend/**/package.jsondeps has adocs/agents/decisions.mdentry. - [BLOCK]
poetry run pip-auditreturns 0 findings of severity ≥ HIGH (run afterpoetry lock). - [BLOCK]
pnpm audit --prodreturns 0 findings of severity ≥ HIGH (infrontend/). - [BLOCK] No new runtime dep on
djangorestframework, OAuth / JWT libraries, or any auth framework.
- [BLOCK] No real emails, phone numbers, IBANs, card
numbers, addresses, or names in
tests/**andexamples/**fixtures. - [BLOCK] Example apps use clearly synthetic identifiers
(Alice / Bob / Carol,
example.com, fake IBAN tagged# fake).
- [BLOCK]
scripts/deploy.shrefuses to run withoutPOETRY_PYPI_TOKEN_PYPI; never echoes it. - [BLOCK] Released wheel embeds the pre-built SPA only; no
frontend/,node_modules/, or*.ts/*.tsxin the wheel. - [BLOCK] Human approval is the trigger. Agents do not tag or publish.
- [NOTE] If the PR creates a cross-role dependency (e.g., this PR exposes a new endpoint that needs Architect perf review), open a handoff in the Issue queue.
- [NOTE] If the PR resolves a handoff to Security, mark it
donein the same diff.
- Run the checklist locally.
- Post the review using
gh pr review <N> --approve / --request-changes / --comment. - The review body must:
- Cite the ACCEPTANCE.md / SECURITY.md sections checked.
- Quote specific file + line for every requested change.
- Be at least one sentence — never just "LGTM" or "👍".
- If the verdict is
Request changes, drop a forum file the PR review comment summarising the changes asked for. - If the verdict is
Approveon a tier-5 PR, the body says "Approved subject to human sign-off — tier 5 requires repo owner approval before merge."
- "We'll add tests in a follow-up PR."
- "It's only a refactor, no review needed."
- "CI will catch it." (CI runs the gate (#452), but a green pipeline is the floor, not the review — dataflow/logic bugs still need human eyes.)
- "The frontend handles it." (Defense in depth; backend must enforce.)
- "It's behind staff auth." (Necessary, not sufficient — the
backend still must obey
ModelAdmin.) - "
# noqa: S101" on a security-relevant rule. (Open an issue instead.)