Prepare compatible audit receiver with verified container lifecycle
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
f1b0577095
commit
cb23dc82fd
8 changed files with 426 additions and 17 deletions
|
|
@ -1,18 +1,30 @@
|
|||
# Base pinned by digest, not a mutable tag (AUDIT-WP-0005-T03).
|
||||
# Same python:3.12-slim digest as the deployed user-engine image.
|
||||
FROM python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9
|
||||
# Shared Python/Alpine baseline used by the factory approval services.
|
||||
# Digest and dependency hashes are release inputs; scan the resulting image.
|
||||
FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a AS build
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/opt/venv/bin:$PATH
|
||||
RUN python -m venv /opt/venv
|
||||
COPY requirements.lock /build/requirements.lock
|
||||
RUN pip install --no-cache-dir --require-hashes --only-binary=:all: -r /build/requirements.lock
|
||||
WORKDIR /build
|
||||
COPY pyproject.toml README.md LICENSE ./
|
||||
COPY audit_core ./audit_core
|
||||
RUN pip install --no-cache-dir --no-deps .
|
||||
|
||||
FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a
|
||||
ARG GIT_COMMIT=unknown
|
||||
LABEL org.opencontainers.image.title="audit-core" \
|
||||
org.opencontainers.image.source="https://forgejo.coulomb.social/coulomb/audit-core" \
|
||||
org.opencontainers.image.revision="${GIT_COMMIT}"
|
||||
|
||||
RUN useradd --system --uid 10001 --create-home audit-core
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/opt/venv/bin:$PATH
|
||||
RUN apk add --no-cache 'libuuid>=2.42.3-r1' \
|
||||
&& addgroup -S -g 10001 audit-core \
|
||||
&& adduser -S -u 10001 -G audit-core -H audit-core
|
||||
COPY --from=build /opt/venv /opt/venv
|
||||
RUN rm -rf /usr/local/bin/pip* /usr/local/lib/python3.12/site-packages/pip* \
|
||||
/usr/local/lib/python3.12/site-packages/setuptools* /usr/local/lib/python3.12/site-packages/pkg_resources \
|
||||
/opt/venv/bin/pip* /opt/venv/lib/python3.12/site-packages/pip* \
|
||||
/opt/venv/lib/python3.12/site-packages/setuptools* /opt/venv/lib/python3.12/site-packages/pkg_resources
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml README.md LICENSE ./
|
||||
COPY audit_core ./audit_core
|
||||
RUN pip install --no-cache-dir ".[serve,postgres]"
|
||||
USER 10001:10001
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
EXPOSE 8080
|
||||
CMD ["audit-core-ingest"]
|
||||
|
|
|
|||
|
|
@ -643,10 +643,21 @@ def serve(app, host: str, port: int, threads: int, timeout: int) -> None:
|
|||
return
|
||||
|
||||
log.info("serving on waitress host=%s port=%s threads=%s", host, port, threads)
|
||||
waitress_serve(
|
||||
app, host=host, port=port, threads=threads,
|
||||
channel_timeout=timeout, ident="audit-core",
|
||||
)
|
||||
# PID 1 ignores the default SIGTERM disposition. Raising SystemExit lets
|
||||
# Waitress stop its dispatcher and wait for active workers (bounded by its
|
||||
# shutdown timeout). A sender without an acknowledgement must still retry
|
||||
# the same event id; shutdown never substitutes for durable acceptance.
|
||||
def terminate(signum, frame):
|
||||
raise SystemExit(0)
|
||||
|
||||
previous = signal.signal(signal.SIGTERM, terminate)
|
||||
try:
|
||||
waitress_serve(
|
||||
app, host=host, port=port, threads=threads,
|
||||
channel_timeout=timeout, ident="audit-core",
|
||||
)
|
||||
finally:
|
||||
signal.signal(signal.SIGTERM, previous)
|
||||
|
||||
|
||||
def _serve_fallback(app, host: str, port: int, timeout: int) -> None:
|
||||
|
|
|
|||
109
docs/evidence/2026-09-11-factory-receiver-release.json
Normal file
109
docs/evidence/2026-09-11-factory-receiver-release.json
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"status": "release_prepared",
|
||||
"source_base": "f1b05770956a251da1b9de274f6bd1de5e325b5f",
|
||||
"receiver_tests": {
|
||||
"passed": 229,
|
||||
"command": "PYTHONPATH=../infospace-bench/src AUDIT_CORE_TEST_DATABASE_URL=<disposable-postgres> python -m pytest -q -ra --ignore=tests/test_migrate_store.py",
|
||||
"excluded": "tests/test_migrate_store.py: five pre-existing failures, one passing empty case. AUDIT-IN-0004 owns repair. No native import."
|
||||
},
|
||||
"container": {
|
||||
"image": "audit-core:hfact-20260911-hardened",
|
||||
"native_credentials_used": false,
|
||||
"checks": {
|
||||
"operational_ready": true,
|
||||
"nonroot_readonly": true,
|
||||
"approval-engine": {
|
||||
"accepted": 202,
|
||||
"duplicate": 200,
|
||||
"wrong_source": 400,
|
||||
"wrong_tenant": 400,
|
||||
"read_routes_denied": 7,
|
||||
"independent_read": 200,
|
||||
"redacted": true,
|
||||
"stored_once": true
|
||||
},
|
||||
"informed-decision": {
|
||||
"accepted": 202,
|
||||
"duplicate": 200,
|
||||
"wrong_source": 400,
|
||||
"wrong_tenant": 400,
|
||||
"read_routes_denied": 7,
|
||||
"independent_read": 200,
|
||||
"redacted": true,
|
||||
"stored_once": true
|
||||
},
|
||||
"sigterm": {
|
||||
"exit_code": 0,
|
||||
"inflight_committed_once": true,
|
||||
"restart_retry_duplicate": true,
|
||||
"chain_intact": true
|
||||
}
|
||||
},
|
||||
"started_at": "2026-09-11T04:43:46.088591+00:00",
|
||||
"status": "passed",
|
||||
"own_fixture_cleanup": true
|
||||
},
|
||||
"schema_comparison": {
|
||||
"live": {
|
||||
"0001-events": "e613b4bfb54914fbd98a4abd1fd6017e37108df43ffc306ab825f2d5a8afdd3c",
|
||||
"0002-append-only": "29251f9300f33cfb90fd40677f5fedbe40057e1c0c866cd29a1d2cb160c4a762",
|
||||
"0003-dead-letters": "a82024b5b4137aecc7885ac8e1212b1711dd06bc5806a01e79882c4a1092d341",
|
||||
"0004-secret-findings": "f269be3e19d2f69673b96a65fa33a2b1f863a39dfff6f9937062ab6aa58f63e5",
|
||||
"0005-runtime-grants": "008247e2daeae5a286cd6e46328fbf0d15fa70c6a99b69cadb296a0c713791fe",
|
||||
"0006-chain": "a984f8226a5d407f990447110f0dd61e400ef4d9da952a268872e5e75f81db2d"
|
||||
},
|
||||
"candidate": {
|
||||
"0001-events": "e613b4bfb54914fbd98a4abd1fd6017e37108df43ffc306ab825f2d5a8afdd3c",
|
||||
"0002-append-only": "29251f9300f33cfb90fd40677f5fedbe40057e1c0c866cd29a1d2cb160c4a762",
|
||||
"0003-dead-letters": "a82024b5b4137aecc7885ac8e1212b1711dd06bc5806a01e79882c4a1092d341",
|
||||
"0004-secret-findings": "f269be3e19d2f69673b96a65fa33a2b1f863a39dfff6f9937062ab6aa58f63e5",
|
||||
"0005-runtime-grants": "008247e2daeae5a286cd6e46328fbf0d15fa70c6a99b69cadb296a0c713791fe",
|
||||
"0006-chain": "a984f8226a5d407f990447110f0dd61e400ef4d9da952a268872e5e75f81db2d"
|
||||
}
|
||||
},
|
||||
"native_before": {
|
||||
"ready": {
|
||||
"status": "ok",
|
||||
"custody_class": "operational",
|
||||
"durable": true,
|
||||
"tamper_evidence": true,
|
||||
"recoverable_days": 30,
|
||||
"recoverable_source": "resource-control/data/capability/platform-audit-storage.json#provisions[capability=data.backup]",
|
||||
"recoverable_basis": "measured"
|
||||
},
|
||||
"chain": {
|
||||
"intact": true,
|
||||
"events": 32,
|
||||
"head": "9fc0b4f25c562cec037a5077f00ee63380db8b5c4e85b6016fc40b301431e4fd",
|
||||
"head_event_id": "whitehat-e2-event-b-20260822-03",
|
||||
"head_accepted_at": "2026-08-22T22:09:37+00:00",
|
||||
"first_break": null,
|
||||
"attestation_match": null
|
||||
},
|
||||
"image": "forgejo.coulomb.social/coulomb/audit-core@sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6",
|
||||
"generation": 15,
|
||||
"deployment_uid": "b85fe3d0-75c9-4e0d-8c34-c6f1df0881bb"
|
||||
},
|
||||
"scan": {
|
||||
"candidate": {
|
||||
"sha256": "33be06088970b025af916a81ad3833313749c2ff25f0fd99641b6fc4823c1ff3",
|
||||
"vulnerabilities": {
|
||||
"LOW": 85,
|
||||
"HIGH": 81,
|
||||
"MEDIUM": 99,
|
||||
"UNKNOWN": 6,
|
||||
"CRITICAL": 3
|
||||
}
|
||||
},
|
||||
"hardened": {
|
||||
"sha256": "c7598f203067f36be1d0c0f2a7b33c3e02b13583d605d7091baea1bb380f604f",
|
||||
"vulnerabilities": {}
|
||||
},
|
||||
"scanner": "aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969",
|
||||
"warning": "OS lifecycle list does not yet include Alpine 3.24; vulnerability scanner recognized Alpine 3.24 repository."
|
||||
},
|
||||
"old_candidate_sigterm_exit": 137,
|
||||
"native_rollout": "pending",
|
||||
"factory_attempts": 0,
|
||||
"paid_model_calls": 0
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ In-cluster URL:
|
|||
http://audit-core.audit-core.svc.cluster.local:8080
|
||||
```
|
||||
|
||||
Reachability is part of the threat model. Ingress is allowed only from the
|
||||
`user-engine` namespace (write) and from namespaces labelled
|
||||
Reachability is part of the threat model. Ingress is allowed from the declared producer peers in
|
||||
`deploy/networkpolicies.yaml` (write) and from namespaces labelled
|
||||
`railiance.io/audit-core-reader=true` (operator read). From a workstation,
|
||||
`kubectl -n audit-core port-forward svc/audit-core 8080:8080` is the usual
|
||||
path and does not require that label.
|
||||
|
|
@ -261,3 +261,32 @@ narrows a column must replace that note before release.
|
|||
5. Deployment. `/readyz` must report `custody_class=operational` and
|
||||
`recoverable_days=30`.
|
||||
6. In-pod `MODE=remote DISRUPT=0` failure matrix. Evidence goes to NK-WP-0024.
|
||||
|
||||
## Factory sender receiver release (2026-09-11)
|
||||
|
||||
Build from a committed source with `make image-build`. `requirements.lock` pins
|
||||
the five runtime packages with hashes; refresh intentionally with
|
||||
`uv pip compile pyproject.toml --extra serve --extra postgres --generate-hashes --output-file requirements.lock`.
|
||||
The image uses the factory Python 3.12 Alpine baseline, non-root UID 10001 and
|
||||
no runtime package installer. Scan every candidate; a clean vulnerability result
|
||||
is time-bound and not a claim that all future defects are absent.
|
||||
|
||||
The release check script uses only a labelled disposable local PostgreSQL
|
||||
container, synthetic sender identities, and a fresh schema. It proves exact
|
||||
source/tenant refusal, all seven evidence read routes denied to writers,
|
||||
independent readback/redaction, duplicate handling, and a database write in
|
||||
flight during SIGTERM. Waitress has a bounded five-second worker drain. Requests
|
||||
without an acknowledgement still require idempotent retry; do not promise that
|
||||
every queued or long-running request finishes inside a shutdown window.
|
||||
|
||||
The original c2fe39a image and this release have identical migrations 0001-0006.
|
||||
This receiver upgrade requires no schema migration job. Rollback to c2fe39a is
|
||||
structurally compatible, but loses evidence classification support: stop the two
|
||||
new factory producers first and retain the custody records, then re-admit their
|
||||
sender contract before resuming. Do not claim load-bearing admission while the
|
||||
legacy receiver is running.
|
||||
|
||||
The historical SQLite importer has five PostgreSQL test failures tracked by
|
||||
AUDIT-IN-0004. It is outside the live receiver path; do not use it as a recovery
|
||||
procedure until that record closes. The release test result must state this
|
||||
exclusion explicitly, never describe the entire suite as green.
|
||||
|
|
|
|||
|
|
@ -177,3 +177,31 @@ resolution: 'Assent. The split is right: approval-engine owns the operative stat
|
|||
recorded_in: history/2026-08-28-approval-evidence-assent.md
|
||||
state_hub_intake_id: "01a04d8f-be67-75ed-a221-d50f99dbb78e"
|
||||
```
|
||||
|
||||
## AUDIT-IN-0004 — Repair SQLite import into chained PostgreSQL custody
|
||||
|
||||
```yaml
|
||||
id: AUDIT-IN-0004
|
||||
kind: intake
|
||||
title: Repair SQLite import into chained PostgreSQL custody
|
||||
status: open
|
||||
origin: residual
|
||||
origin_ref: AUDIT-WP-0005
|
||||
priority: medium
|
||||
owner: audit-core
|
||||
requested_by: codex
|
||||
created: '2026-09-11'
|
||||
updated: '2026-09-11'
|
||||
description: >
|
||||
The 2026-09-11 receiver release rehearsal enables real PostgreSQL tests.
|
||||
Five tests in tests/test_migrate_store.py fail because _import_event omits
|
||||
chain_hash/chain_prev, now NOT NULL. This predates the receiver release and
|
||||
is not used by the native PostgreSQL receiver or its unchanged schema.
|
||||
Design the historic accepted_at versus chain ordering rule before fixing
|
||||
the importer: do not disable append-only protection or rewrite existing
|
||||
event timestamps to make tests pass. Cover empty and populated destinations,
|
||||
concurrency with live append, idempotent replay, conflict preservation, and
|
||||
an intact chain. Also evaluate repeated dead-letter/finding import counting.
|
||||
Run the full PostgreSQL suite with no migration exclusions for closure.
|
||||
No native import is authorized or attempted by this release.
|
||||
```
|
||||
|
|
|
|||
77
requirements.lock
Normal file
77
requirements.lock
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml --extra serve --extra postgres --generate-hashes --output-file requirements.lock
|
||||
psycopg==3.3.5 \
|
||||
--hash=sha256:ce5aa5cdb4f9379f00f487590e5890bfa7df9a164648c969ffa628505e21af4e \
|
||||
--hash=sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4
|
||||
# via audit-core (pyproject.toml)
|
||||
psycopg-binary==3.3.5 \
|
||||
--hash=sha256:0249c3e960cdee686000eb77169fb6590105c05bacc37e057ccdffdcd8e6ebde \
|
||||
--hash=sha256:04f64b39830887c2c737b522cbfd6ad215d65e67ebfff674aa4cf21c02af487b \
|
||||
--hash=sha256:06de14ac978a2d53e864069fb5487075c6e3cfb0740f1bfd7017bc8b9942067f \
|
||||
--hash=sha256:0d8a4b7ae47f3381e2ded89891d2455b809f4afb7e5b58086844abb8cfa420ea \
|
||||
--hash=sha256:1344fd57a19737554670e67aecabd4fb37cd7937f2925840009645d641117e5a \
|
||||
--hash=sha256:14f432430fd9e1a9e7d9ab2fe14956c77f5d074ebdc556a1ad04e9a1bd3fca04 \
|
||||
--hash=sha256:14fdfd65a96ecbd8b586d14546105641f4a6ac7cbe335c786830ea4de94bbe60 \
|
||||
--hash=sha256:19e5bf9872dbd164c220567fd385ba2309c7d9df1541f78343510c6b0f36a1b7 \
|
||||
--hash=sha256:1ef2e498be47800f6202b9a2304c22646325ca6d54001b7c785bcfdb24a1e8ab \
|
||||
--hash=sha256:2111f880add40fb03c60556069ad68e884a0908a74d2debafc603caf93b73552 \
|
||||
--hash=sha256:25105f9b46bdf2a30fcb67f56976ed66f6855941ae16bc024192609b917d493c \
|
||||
--hash=sha256:2719fe19a4da752c4110cc767716d0a5bdb760d1153d89018bb7c9c61717bde5 \
|
||||
--hash=sha256:358748fc4c8ccdc0e2bdf55420494930e19c3ade586ea9c3a6de3dad1f897311 \
|
||||
--hash=sha256:35885e333020fc152d27bea1a494bef13b2e68f6fd92b6229015e93539152008 \
|
||||
--hash=sha256:39e70c8e3b5fad70e2970ea4cc502bf3b612018f128aa1c670f1fa78b9774543 \
|
||||
--hash=sha256:40505676b1526b9ea387dace034040a8c8b0bcf984cd6bd4720a2ab15e813586 \
|
||||
--hash=sha256:40f8b132c7243ef5f503f0b6f986bf16d38a51b0df1c6ba2577743f128be03e3 \
|
||||
--hash=sha256:479b96fd78149cfa10369dc53fbfb89ee729be13146b584a23dbc7e164c0cf1e \
|
||||
--hash=sha256:4901e5b9a31c230211a1871263b6594373bacd770b14ad9b14d349716cb69cbe \
|
||||
--hash=sha256:553b5443cbc94fdb9b0e31b62acdf615e0780982d6b13752a15eb3d6c0dfd0d2 \
|
||||
--hash=sha256:5698ab5941a4d138c30fef858588e651fe7d583280cd6e41832825ad9e747750 \
|
||||
--hash=sha256:5816472e3bb05615f33a741e0835043d1f4bf9709ff30d2f4aed71815cfc6b5e \
|
||||
--hash=sha256:5b981d25fc2dd13fa7328e40703ea8a03f3e9d855ec946431e96a23206d3b9fd \
|
||||
--hash=sha256:682a17a57415c3ca1731eec018ed031f012ffcb81ba74806eb219cb396065672 \
|
||||
--hash=sha256:6e85d50b87257fb117675a19ee59daa7bf9a57f6431500adf7059df799232ef4 \
|
||||
--hash=sha256:7b443f943abfe35aa5a776630cea27c9348aa66659286cee0b99084332252080 \
|
||||
--hash=sha256:88e01aa2e938a45655a8a5213fc3a44ba78cb4cab8a569b3e0bcb3d1d0eaba16 \
|
||||
--hash=sha256:893ce86a4b997f6ca1261a7826db2506727332a6ff66646fa7f024b39b5e630e \
|
||||
--hash=sha256:8dbd694f3741dd4ac5bc60b70e17f7841aefb3f0f38cef4d2756de270e03af43 \
|
||||
--hash=sha256:972cc28e943746e71ede254a4dfd1fdfcdd6dcadd6f375703849859f09377f24 \
|
||||
--hash=sha256:98a388509306e5e08a4203253ac52846bc1b034e5cbd0ae6da1211593cc28594 \
|
||||
--hash=sha256:9c071bf78e5c2e6efa40bc9089a954d7b41221347a72f35c6bf2d8c96e632f75 \
|
||||
--hash=sha256:a5e45e4bb68656253ce5c7a344c0a425c293581eba954d7fd7c2e4b2dc9f3038 \
|
||||
--hash=sha256:ab39e2794b95af61a2ff69e33e5ab6ac5df36e9ffea9a3b18e38b2aaca8c5ad5 \
|
||||
--hash=sha256:ae67072db949d0c094b747a8ec52ad0fa3c42b27842a5f746f3613d54dde3fba \
|
||||
--hash=sha256:af5084124fb2fd16557073822519dfe8c389636a16adef661a4c0c3918733171 \
|
||||
--hash=sha256:ba466011569297114449df9d523438e1adeedf3e4f31ffb78e897ec3fef3076b \
|
||||
--hash=sha256:c065531e8c1815276f50dbfa283e3a7f022671414cdda6fa9a16794dd53b28f9 \
|
||||
--hash=sha256:c09775c549b40b274206e1b043c5e5b5af39666e85c98382a30bd05d23ab677b \
|
||||
--hash=sha256:c0cac998b9b1e82dec853d2e53b3d34d56a525cf231f9441a636cfd5992929a9 \
|
||||
--hash=sha256:c6bd84e4cf67930f26f015dec33f615472b9c5871d46408efe112dbc1bc021de \
|
||||
--hash=sha256:ca8af7c0454cdce235d4aedcb5528857468f1490202d25e24fc7af40e176d563 \
|
||||
--hash=sha256:cb3b3bffebfe07110730626e76238161124f35ac87b748d663316a28d22f58b0 \
|
||||
--hash=sha256:cd0faa2475ab254ad1b507430131cf7f7f0be927ffdc03c32ad3b33d2ef63f42 \
|
||||
--hash=sha256:cf0e5e63ee86098299c673992053d556c489ba9ae6aca6cb6e24d16a8e0b09e6 \
|
||||
--hash=sha256:d06da67e9c687c6a6fdac9da4b17cbeb296ddd59bd01f6416ed4294bc57c5faf \
|
||||
--hash=sha256:d2a61e8147902771df7efe14062a3c8736347850d0d8befcf048235752504f2e \
|
||||
--hash=sha256:d8b66353b20e79bf7ac0a80f03ae97f522ccbbf909d687eec62f112e56c0276d \
|
||||
--hash=sha256:df209e64674a34b41662c67fdc8b4e0ffd77d2136393790691d086a09f9a6cab \
|
||||
--hash=sha256:df9853b832b7b916e02ef68e0d5403a7dab2d5c1ddfe94f22b1b155eb862622f \
|
||||
--hash=sha256:e5becd311f9af8d180bad372f51fb2252fd02cb2073056e2b170c9274f95fe7f \
|
||||
--hash=sha256:f45d77e398542ce0937d9fa3cd9d84e9c5fc6b34c50a66404ae840bada312750 \
|
||||
--hash=sha256:f7e1e45aad410e20de45df2b159df68ff6c8dbf47a3501f806c4489b27f4ad2b \
|
||||
--hash=sha256:fd5b047c9fd887b767d063845413e405f5de8ce1dc7a9d0da0637b77b836b469 \
|
||||
--hash=sha256:fdbeb38c9b7ca8fa57a7bda3802bedb62f4494ad3dd46c7dd36dc3f77fd5093f
|
||||
# via psycopg
|
||||
psycopg-pool==3.3.1 \
|
||||
--hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \
|
||||
--hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c
|
||||
# via psycopg
|
||||
typing-extensions==4.16.0 \
|
||||
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
|
||||
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
|
||||
# via
|
||||
# psycopg
|
||||
# psycopg-pool
|
||||
waitress==3.0.2 \
|
||||
--hash=sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f \
|
||||
--hash=sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e
|
||||
# via audit-core (pyproject.toml)
|
||||
127
scripts/release_container_check.py
Normal file
127
scripts/release_container_check.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Exercise a candidate against a labelled disposable local PostgreSQL container.
|
||||
|
||||
No native credentials or cluster access. Requires psycopg on the host. Keeps
|
||||
only a JSON receipt; deletes only the receiver and database schema it creates.
|
||||
"""
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import psycopg
|
||||
|
||||
|
||||
def run(argv, **kwargs):
|
||||
return subprocess.run(argv, check=True, capture_output=True, text=True, **kwargs).stdout
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--image', required=True)
|
||||
p.add_argument('--postgres-container', required=True)
|
||||
p.add_argument('--postgres-port', type=int, required=True)
|
||||
p.add_argument('--receipt', type=Path, required=True)
|
||||
a = p.parse_args()
|
||||
pg = json.loads(run(['docker', 'inspect', a.postgres_container]))[0]
|
||||
assert pg['Config']['Labels'].get('hfact.task') == 'audit-release-20260911', 'local test fixture required'
|
||||
suffix = uuid.uuid4().hex[:12]
|
||||
schema = 'release_' + suffix
|
||||
name = 'audit-release-check-' + suffix
|
||||
dsn = f'postgresql://postgres:local-test-only@127.0.0.1:{a.postgres_port}/audit_core'
|
||||
registry = [{'name': n, 'tokens': [n+'-fixture'], 'sources': [n],
|
||||
'tenants': ['tenant:platform'], 'may_write': True, 'may_read': False,
|
||||
'evidence_kind': 'load-bearing', 'secret_policy': 'redact'}
|
||||
for n in ['approval-engine', 'informed-decision']]
|
||||
registry.append({'name':'independent-reader','tokens':['reader-fixture'],'sources':['*'],
|
||||
'tenants':['*'],'may_write':False,'may_read':True})
|
||||
proof = {'image': a.image, 'native_credentials_used': False, 'checks': {},
|
||||
'started_at': datetime.now(timezone.utc).isoformat()}
|
||||
created = False
|
||||
def request(method, path, token='', body=None):
|
||||
# Values here are local synthetic fixtures, never native credentials.
|
||||
payload=json.dumps({'method':method,'path':path,'token':token,'body':body})
|
||||
code='''import json,sys,urllib.request,urllib.error
|
||||
x=json.load(sys.stdin); b=x['body']
|
||||
r=urllib.request.Request('http://127.0.0.1:8080'+x['path'],method=x['method'],data=json.dumps(b).encode() if b is not None else None,headers={'Content-Type':'application/json','Authorization':'Bearer '+x['token'],'Idempotency-Key':b['id'] if b else ''})
|
||||
try:
|
||||
with urllib.request.urlopen(r,timeout=15) as f: print(json.dumps([f.status,json.load(f)]))
|
||||
except urllib.error.HTTPError as e: print(json.dumps([e.code,json.load(e)]))
|
||||
'''
|
||||
return json.loads(run(['docker','exec','-i',name,'python','-c',code],input=payload))
|
||||
def event(sender, suffix):
|
||||
return {'id':suffix,'source':sender,'type':'factory.custody-check',
|
||||
'subject':'synthetic-release-check','tenant':'tenant:platform','correlation_id':'release-'+suffix,
|
||||
'occurred_at':datetime.now(timezone.utc).isoformat(),
|
||||
'data':{'synthetic':True,'auth_token':'fixture-must-be-redacted'}}
|
||||
try:
|
||||
run(['docker','run','-d','--name',name,'--network','container:'+a.postgres_container,
|
||||
'--read-only','--tmpfs','/tmp','--cap-drop','ALL','--security-opt','no-new-privileges',
|
||||
'-e','AUDIT_CORE_DATABASE_URL=postgresql://postgres:local-test-only@127.0.0.1:5432/audit_core',
|
||||
'-e','AUDIT_CORE_DATABASE_SCHEMA='+schema,'-e','AUDIT_CORE_REQUIRE_CUSTODY_CLASS=operational',
|
||||
'-e','AUDIT_CORE_SENDERS='+json.dumps(registry),a.image])
|
||||
created = True
|
||||
for _ in range(50):
|
||||
try:
|
||||
status,body=request('GET','/readyz')
|
||||
if status==200: break
|
||||
except (subprocess.CalledProcessError, ValueError): time.sleep(.2)
|
||||
else: raise AssertionError('receiver readiness timed out')
|
||||
proof['checks']['operational_ready']=body['custody_class']=='operational'
|
||||
assert proof['checks']['operational_ready']
|
||||
uid=run(['docker','exec',name,'id','-u']).strip(); assert uid=='10001'
|
||||
proof['checks']['nonroot_readonly']=True
|
||||
for sender in ['approval-engine','informed-decision']:
|
||||
e=event(sender,sender+'-'+suffix); token=sender+'-fixture'
|
||||
assert request('POST','/v1/events',token,e)[0]==202
|
||||
assert request('POST','/v1/events',token,e)[0]==200
|
||||
assert request('POST','/v1/events',token,dict(e,source='sibling'))[0]==400
|
||||
assert request('POST','/v1/events',token,dict(e,tenant='tenant:sibling'))[0]==400
|
||||
for path in ['/v1/events/'+e['id'],'/v1/events?correlation_id=fixture','/v1/stats','/v1/integrity','/v1/dead-letters','/v1/secret-findings','/v1/stream-findings']:
|
||||
assert request('GET',path,token)[0]==403, path
|
||||
status,stored=request('GET','/v1/events/'+e['id'],'reader-fixture')
|
||||
assert status==200 and 'fixture-must-be-redacted' not in json.dumps(stored)
|
||||
with psycopg.connect(dsn) as c:
|
||||
assert c.execute(f'SELECT count(*) FROM {schema}.events WHERE event_id=%s',(e['id'],)).fetchone()[0]==1
|
||||
proof['checks'][sender]={'accepted':202,'duplicate':200,'wrong_source':400,'wrong_tenant':400,'read_routes_denied':7,'independent_read':200,'redacted':True,'stored_once':True}
|
||||
e=event('approval-engine','shutdown-'+suffix)
|
||||
with psycopg.connect(dsn,autocommit=True) as lock, ThreadPoolExecutor() as pool:
|
||||
lock.execute('SELECT pg_advisory_lock(%s)',(0xA0D17007,))
|
||||
pending=pool.submit(request,'POST','/v1/events','approval-engine-fixture',e)
|
||||
for _ in range(100):
|
||||
if lock.execute("SELECT count(*) FROM pg_locks WHERE locktype='advisory' AND NOT granted").fetchone()[0]: break
|
||||
time.sleep(.05)
|
||||
else: raise AssertionError('in-flight database write not observed')
|
||||
stop=pool.submit(run,['docker','stop','--timeout','10',name])
|
||||
time.sleep(.5)
|
||||
lock.execute('SELECT pg_advisory_unlock(%s)',(0xA0D17007,))
|
||||
try: pending.result()
|
||||
except subprocess.CalledProcessError: pass # Lost acknowledgement requires retry.
|
||||
stop.result()
|
||||
code=int(run(['docker','inspect',name,'--format','{{.State.ExitCode}}']).strip()); assert code==0, code
|
||||
with psycopg.connect(dsn) as c:
|
||||
assert c.execute(f'SELECT count(*) FROM {schema}.events WHERE event_id=%s',(e['id'],)).fetchone()[0]==1
|
||||
run(['docker','start',name])
|
||||
for _ in range(50):
|
||||
try:
|
||||
if request('GET','/readyz')[0]==200: break
|
||||
except subprocess.CalledProcessError: pass
|
||||
time.sleep(.2)
|
||||
assert request('POST','/v1/events','approval-engine-fixture',e)[0]==200
|
||||
status,integrity=request('GET','/v1/integrity','reader-fixture')
|
||||
assert status==200 and integrity['intact'] and integrity['events']==3
|
||||
proof['checks']['sigterm']={'exit_code':code,'inflight_committed_once':True,'restart_retry_duplicate':True,'chain_intact':True}
|
||||
proof['status']='passed'
|
||||
finally:
|
||||
if created: run(['docker','rm','-f',name])
|
||||
with psycopg.connect(dsn) as c: c.execute(f'DROP SCHEMA IF EXISTS {schema} CASCADE')
|
||||
proof['own_fixture_cleanup']=True
|
||||
with a.receipt.open('x') as f: json.dump(proof,f,indent=2); f.write('\n')
|
||||
print(json.dumps(proof,indent=2))
|
||||
|
||||
|
||||
if __name__=='__main__': main()
|
||||
|
|
@ -456,8 +456,8 @@ CCR-2026-0021 (approval-engine) and CCR-2026-0022 (informed-decision), with
|
|||
exact-path ESO projections and CAS-preserving first provision/resume. Its
|
||||
receiver check refuses the current image before credential access. Seventeen
|
||||
new tests, including eleven local OpenBao cases, and 53 existing credential-change
|
||||
tests pass. Named owner reviews, compatible receiver release and target namespace
|
||||
readiness remain prerequisites. No native sender credential or deployment changed.
|
||||
tests pass. The user approved both named owner review sets on 2026-09-11.
|
||||
Compatible receiver release and target namespace readiness remain prerequisites.
|
||||
See railiance-platform/docs/credential-lane-designs/factory-audit-senders-review.md
|
||||
and its dated preparation evidence. Existing sender/source scopes are preserved.
|
||||
|
||||
|
|
@ -483,3 +483,19 @@ to cover.
|
|||
T05's dependency is real and should not be worked around. audit-core argued the
|
||||
cadence obligation up to a MUST; inventing a local cadence shape ahead of the
|
||||
Taxonomy schema would fragment the very interface it asked for.
|
||||
|
||||
### Compatible receiver release rehearsal — 2026-09-11
|
||||
|
||||
229 receiver/contract tests pass with real disposable PostgreSQL. The full suite
|
||||
exposed five historical SQLite-import failures, now handed off as AUDIT-IN-0004;
|
||||
that importer is not used by the native receiver. No migration SQL changed.
|
||||
The new image uses the factory Alpine baseline and hashed runtime dependencies.
|
||||
Candidate scan went from 81 High/3 Critical to zero reported vulnerabilities,
|
||||
with the Alpine lifecycle-list warning retained. SIGTERM previously killed
|
||||
PID 1 with exit 137; explicit handling now exits 0, preserves one in-flight
|
||||
write and returns duplicate on restart/retry. Both synthetic senders pass exact
|
||||
scope, redaction, independent readback and seven read-route denials.
|
||||
|
||||
[Rehearsal evidence](../docs/evidence/2026-09-11-factory-receiver-release.json).
|
||||
Publish and native rollout are the next steps; T09/T11 remain progress until
|
||||
real sender custody, delivery and owner acceptance are evidenced.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue