Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f5cea81
feat(worker): pin immutable external web profile identity
CompleteDotTech Nov 14, 2026
3c513c1
feat(worker): bind automatic profile claims and durable callbacks
CompleteDotTech Nov 14, 2026
71fc849
fix(worker): reject contradictory automatic check observations
CompleteDotTech Nov 14, 2026
acb6794
feat(worker): execute bounded external observations with passive Checker
CompleteDotTech Nov 14, 2026
6a90bc3
build: preserve exact LF bytes of profile manifests
CompleteDotTech Nov 14, 2026
7362719
fix(worker): preserve safe profile stop categories without raw errors
CompleteDotTech Nov 14, 2026
d7df5d2
fix(worker): retain legacy admission and isolated package compatibility
CompleteDotTech Nov 14, 2026
3b0bbbd
test(worker): verify one external meter and bounded scanner dispatch
CompleteDotTech Nov 14, 2026
c15657d
Add manifest-bound external transport and shared admission boundaries
CompleteDotTech Nov 14, 2026
8fa82a5
fix(worker): bound transfer framing and preserve safe profile stops
CompleteDotTech Nov 14, 2026
4cfea84
docs(worker): explain unpublished external profile and acceptance limits
CompleteDotTech Nov 14, 2026
920c6e3
test(worker): exercise combined external admission and scanner pipeline
CompleteDotTech Nov 14, 2026
c06a684
feat(wordpress): bind unpublished external profile evidence and reser…
CompleteDotTech Nov 14, 2026
4920fd5
fix(wordpress): refuse non-string execution profiles before lookup
CompleteDotTech Nov 14, 2026
48ae54b
test(worker): scope scanner entry regressions to Linux
CompleteDotTech Nov 14, 2026
dc9a2d2
fix(deployment): package pinned external profile and exercise fixture…
CompleteDotTech Nov 14, 2026
3d6fc1a
test(wordpress): bridge actual external executor callbacks to PHP val…
CompleteDotTech Nov 14, 2026
74778be
ci: require the offline external callback PHP bridge
CompleteDotTech Nov 14, 2026
a1e4f7c
test(worker): scope Linux isolation and deadline acceptance checks
CompleteDotTech Nov 14, 2026
6da0ff6
Merge released lifecycle baseline into external profile candidate
CompleteDotTech Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
!integrations/__init__.py
!integrations/worker/
!integrations/worker/*.py
!integrations/profiles/
!integrations/profiles/external-web-v1.json
!deployment/
!deployment/Dockerfile
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.py text eol=lf
*.md text eol=lf
*.yml text eol=lf
integrations/profiles/*.json text eol=lf
*.txt text eol=lf
# Unified patch context includes a leading space before source indentation.
deployment/*.patch -whitespace
19 changes: 17 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,20 @@ jobs:
run: >-
docker run --rm --network none -v "$PWD:/work:ro" -w /work
php:8.3-cli@sha256:aafe21201943a8a6e497ddfb471e2ce68ada76adede38c7d61fede8918b24319
sh -c 'set -e; php -d zend.assertions=1 integrations/wordpress/standalone-tests/report-access.php; php -d zend.assertions=1 integrations/wordpress/standalone-tests/retention.php integrations/wordpress/ct-scan.php; php -d zend.assertions=1 integrations/wordpress/standalone-tests/tombstones.php integrations/wordpress/ct-scan.php; php -d zend.assertions=1 integrations/wordpress/standalone-tests/manual-lifecycle.php'
sh -c 'set -e; php -d zend.assertions=1 integrations/wordpress/standalone-tests/report-access.php; php -d zend.assertions=1 integrations/wordpress/standalone-tests/retention.php integrations/wordpress/ct-scan.php; php -d zend.assertions=1 integrations/wordpress/standalone-tests/tombstones.php integrations/wordpress/ct-scan.php; php -d zend.assertions=1 integrations/wordpress/standalone-tests/manual-lifecycle.php; php -d zend.assertions=1 integrations/wordpress/standalone-tests/external-profile.php'
- name: Set up Python for the cross-language profile fixture
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Check actual executor callbacks against PHP without network
run: |
bridge_php="$RUNNER_TEMP/ct-external-bridge-php"
cat > "$bridge_php" <<'SH'
#!/bin/sh
exec docker run --rm -i --network none --read-only --cap-drop ALL --security-opt no-new-privileges --pids-limit 16 --memory 128m -v "$PWD:$PWD:ro" -w "$PWD" php:8.3-cli@sha256:aafe21201943a8a6e497ddfb471e2ce68ada76adede38c7d61fede8918b24319 php "$@"
SH
chmod 700 "$bridge_php"
CT_EXTERNAL_BRIDGE_PHP="$bridge_php" python -m unittest -v test_external_php_bridge

wordpress:
name: WordPress queue and authorization
Expand All @@ -63,6 +76,8 @@ jobs:
run: docker compose -p php2shell-ci -f deployment/compose.test.yaml exec -T wordpress php /work/integrations/wordpress/tests/integration.php
- name: Test per-request admission, cancellation race and fail-closed behavior
run: docker compose -p php2shell-ci -f deployment/compose.test.yaml exec -T wordpress php /work/integrations/wordpress/tests/admission.php
- name: Test external profile reservation database compare-and-set
run: docker compose -p php2shell-ci -f deployment/compose.test.yaml exec -T wordpress php /work/integrations/wordpress/tests/external-reservations.php
- name: Test bound acceptance evidence and the accepted copy
run: docker compose -p php2shell-ci -f deployment/compose.test.yaml exec -T wordpress php /work/integrations/wordpress/tests/evidence.php
- name: Test Python to WordPress over HTTP with synthetic scan responses
Expand All @@ -87,7 +102,7 @@ jobs:
--tmpfs /tmp:rw,noexec,nosuid,size=32m,mode=1777
-v "$PWD:/app:ro" -w /app -e PYTHONDONTWRITEBYTECODE=1
python:3.12-slim-bookworm@sha256:6a0e06f5ba0b373d051aabd3fffb9b873f27f9a2a3a14bcbe37dee6f0e3bbc70
python -m unittest -v test_scan_worker test_admission test_control
python -m unittest -v test_scan_worker test_admission test_control test_external_boundary test_external_pipeline test_external_worker_wire
- name: Build and check packaged isolation
run: |
docker build --build-arg CHECKER_REVISION="$GITHUB_SHA" -f deployment/Dockerfile -t ct-worker-fixture .
Expand Down
1 change: 1 addition & 0 deletions deployment/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ WORKDIR /app
COPY wordpress_check.py LICENSE LICENSING.md /app/
COPY integrations/__init__.py /app/integrations/__init__.py
COPY integrations/worker /app/integrations/worker
COPY integrations/profiles/external-web-v1.json /app/integrations/profiles/external-web-v1.json
ARG CHECKER_REVISION
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 CT_SCAN_REVISION=${CHECKER_REVISION}
RUN test -n "$CHECKER_REVISION" && mkdir -p /var/lib/ct-scan /run/ct-scan && chmod 700 /var/lib/ct-scan /run/ct-scan
Expand Down
3 changes: 3 additions & 0 deletions deployment/compose.test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ services:
volumes:
- wp:/var/www/html
- ../integrations/wordpress:/var/www/html/wp-content/mu-plugins:ro
# The plugin's canonical sibling fallback avoids a nested mount inside
# the read-only MU-plugin directory.
- ../integrations/profiles:/var/www/html/wp-content/profiles:ro
- ..:/work:ro
networks: [fixture]
volumes:
Expand Down
8 changes: 8 additions & 0 deletions deployment/test-runtime-image.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@
from integrations.worker.admission import AdmissionDenied
from integrations.worker.runner import SubprocessRunner, _linux_preexec_limits
from integrations.worker.service import load_secret
from integrations.worker.profiles import MANIFEST_PATH, PROFILE_ID, PROFILE_SHA256, profile_manifest, validate_binding


assert os.geteuid() == 0
assert MANIFEST_PATH == Path('/app/integrations/profiles/external-web-v1.json')
manifest = profile_manifest()
assert manifest['profile_id'] == PROFILE_ID
assert validate_binding(PROFILE_ID, PROFILE_SHA256, manifest['allowed_services']) == (('http', 80), ('https', 443))
with tempfile.TemporaryDirectory() as directory:
secret = Path(directory) / 'key'
secret.write_text('synthetic-fixture-key-' * 3)
secret.chmod(0o600)
assert load_secret(secret) == b'synthetic-fixture-key-' * 3
code = '''import os,sys
from integrations.worker.profiles import PROFILE_ID, PROFILE_SHA256, profile_manifest, validate_binding
assert os.getuid() == os.getgid() == 65534
assert not os.getgroups()
assert 'CT_SCAN_SECRET_FILE' not in os.environ
assert validate_binding(PROFILE_ID, PROFILE_SHA256, profile_manifest()['allowed_services']) == (('http', 80), ('https', 443))
try:
open(sys.argv[1]).read()
except PermissionError:
Expand Down Expand Up @@ -50,3 +57,4 @@ def probe(self):
assert result['php2shell_status'] == 'unknown'
assert result['php2shell_exploitability'] == 'not_tested'
print('Packaged checker ran without network access and retained unknown outcome.')
print('Packaged immutable profile loaded and validated for controller and scanner identities; no external profile scan ran.')
46 changes: 46 additions & 0 deletions docs/EXTERNAL_PROFILE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Unpublished automatic external profile

`external-web-v1` is an integration-only candidate. The public single-file
checker keeps its existing interface. These sources do not provision a worker,
adopt an agreement, authorize a customer target, or enable the WordPress queue.

The canonical manifest is `integrations/profiles/external-web-v1.json`, pinned
by SHA-256 `1f2a19c959570157ee9df788141ae1e21b8f67aba8c6ce99b768efbd42eb7a37`.
Changing those bytes requires a separately reviewed profile and new assent;
neither configuration nor a retry can widen a saved authorization. A job binds
the manifest identity and explicit selected HTTP/80 and HTTPS/443 services.

The manifest permits robots policy, public homepage header/version triage,
security-header presence and a verified TLS certificate observation. It permits
only the selected hostname, root and robots paths. Advertised versions remain
evidence rather than verified runtime or exploitability. Internal configuration,
runtime/backport confirmation and compromise investigation remain unassessed.
No title, HTML, arbitrary header value, asset URL or raw certificate is retained
in the typed result. Possible sensitive-data or compromise indicators stop the
whole job with a fixed category; they do not initiate investigation.

The worker uses one meter and one admitted transport across every check,
redirect and selected service. DNS answers are bounded, private/reserved answers
are refused, and accepted addresses are pinned while preserving Host and TLS
SNI. A disposable resolver receives no authentication key or admission channel.
HTTP framing, header bytes, wire and decompressed body bytes and total request
time are bounded. HEAD observations do not read an unsolicited response body.

Each HTTP or TLS connection needs a fresh supervisor grant bound to the saved
profile. The manifest ceilings are 100 HTTP transactions, 20 additional TLS
checks, 600 active seconds, 5 MiB aggregate body bytes, 256 KiB per response,
32 KiB headers, ten seconds per request, one connection and at most one check
per second. The implementation can stop earlier: it reserves the full response
and time allowance before connecting, does not refund reservations or retry
automatically, and retains the existing 175-second child/180-second supervisor
limits. A restarted or uncertain job never acquires a fresh budget or rescans.

Source tests use injected transports, mocks and ephemeral local fixtures. They
do not establish production egress enforcement or customer authorization.
Before deployment, the matching WordPress binding must pass real disposable
database and controlled-network acceptance tests, including cancellation,
expiry, exact-target authority, mailbox/report recipients, budgets, retained
evidence and rollback. The current-base candidate still needs exact-head CI
and two fresh independent reviews. Site release and activation are separate
coordinated operations; keep the candidate disabled until those prerequisites
are verified.
65 changes: 65 additions & 0 deletions docs/EXTERNAL_WORDPRESS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Unpublished external WordPress binding (#27)

This future source extends the maintained queue plugin with schema 9: an exact
immutable profile digest, explicit selected services, and durable cumulative
HTTP/TLS/body/active-time reservations. The existing `r2-header-v1` profile and
wire messages retain their defaults. External claims and result callbacks add
only `profile_id`, `profile_sha256`, and `services`; start/admit messages retain
their existing shape. Missing or changed bindings refuse work. Configuration
still defaults to disabled and no production release or customer consent is
established by this source.

Before creating or continuing an external job, the core independently reads the
frozen automatic envelope through the separate G2 adapter and checks its raw
metadata, canonical release/profile snapshots, exact Target Summary, signer
and acceptance digests, encoded post representation and fixed UTC expiry.
The automatic contract's identity header ends before its separately checked
canonical Summary. Legacy contract validation retains its existing boundaries.

External results retain only the fixed typed observation vocabulary, bounded
version tokens and the maintained Checker's public interpretation vocabulary.
Unknown check fields, raw page/title/header text, arbitrary interpretation
strings, unselected services and changed manifest identities are refused.
Conservative reservations charge 256 KiB and 10 seconds per HTTP admission,
and 10 seconds per TLS-only admission, without refunds. New connections wait
until the prior reservation's bounded timeout has elapsed. These conservative
rules can stop before the disclosed maximum counts.

Pure source checks (no WordPress bootstrap, database, DNS, HTTP or mail):

```sh
php integrations/wordpress/standalone-tests/external-profile.php
php integrations/wordpress/standalone-tests/report-access.php
php integrations/wordpress/standalone-tests/manual-lifecycle.php
```

For a combined G1/G2 frozen-evidence test, pass an isolated G2 source root and
an explicitly **SYNTHETIC** canonical generator release. The fixture calls the
actual route decoder, persistence producer/readback/copy helpers, and core
`ct_scan_validate_saved_request()` using synthetic WordPress API stores. It
covers Unicode, ampersands, apostrophes, exact summary/signature/expiry binding,
immutable-field tampering, duplicate metadata and altered post representation.
It does not invoke acceptance, create a queue job, adopt a release, or mail.
The optional trusted WordPress `kses.php` path adds the real pure entity
normalizer without bootstrapping WordPress.

```sh
CT_AUTO_CROSS_G2_ROOT=/isolated/G2-source \
CT_AUTO_CROSS_RELEASE=/private/SYNTHETIC/release.json \
CT_AUTO_CROSS_KSES=/trusted/wp-includes/kses.php \
php integrations/wordpress/standalone-tests/automatic-evidence-cross.php
```

`integrations/wordpress/tests/external-reservations.php` is a separate disposable
WordPress/database fixture source. It tests real schema columns, committed
reservations, stale-row CAS refusal, inflight exclusion and restored malformed
state using an owned fixture row. It does not represent a real external
acceptance or target scan. It has not been executed locally in this recovery.
Run only through the reviewed fixture support and an allocated protected
mutation slot; never run it against production.

Actual database concurrency, the full accepted automatic route/outbox/queue
lifecycle, mailbox and authority gates, worker/result/report delivery, rollback,
exact-head CI and two fresh final reviews remain required. The upstream
lifecycle fixes also remain a dependency. This future branch does not satisfy
those gates, enable scans, or close the related issues.
1 change: 1 addition & 0 deletions integrations/profiles/external-web-v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"allowed_paths":["/","/robots.txt"],"allowed_services":[{"port":80,"scheme":"http"},{"port":443,"scheme":"https"}],"checks":[{"check_id":"robots_policy","expected_data":"Public robots permission for the selected service root.","max_body_bytes":262144,"max_header_bytes":32768,"method":"GET","path":"/robots.txt","reporting":"Permission outcome and bounded status only; no raw robots content.","request_mode":"acquire","sensitivity":"Stop on possible sensitive-data or compromise indicators; discard the body.","timeout_seconds":10},{"check_id":"public_header_triage","expected_data":"Public homepage headers and already-returned HTML markers, classified by the existing Checker.","max_body_bytes":262144,"max_header_bytes":32768,"method":"GET","path":"/","reporting":"Reported PHP and WordPress version evidence remains uncertain; exploitability is not tested. No title, asset URLs or HTML are retained.","request_mode":"acquire","sensitivity":"Stop on possible sensitive-data or compromise indicators before inspection; discard the body.","timeout_seconds":10},{"check_id":"security_header_presence","expected_data":"Presence of HSTS, CSP, frame-control and content-type options in public root response headers.","max_body_bytes":0,"max_header_bytes":32768,"method":"HEAD","path":"/","reporting":"Presence observations only, not a conclusion that configuration is secure.","request_mode":"acquire_after_robots_permission","sensitivity":"Inspect headers for stop indicators; never read or retain a HEAD response body.","timeout_seconds":10},{"check_id":"tls_certificate","expected_data":"Verified certificate hostname/chain, negotiated protocol and certificate expiry from one TLS-only connection.","max_body_bytes":0,"max_header_bytes":0,"method":"TLS","path":null,"reporting":"Connection verification and public expiry metadata only; no cipher sweep or security certification.","request_mode":"selected_https_service_only","sensitivity":"No application data, raw certificate or session keys are retained.","timeout_seconds":10}],"coverage_statement":"Bounded public external observations only; no exploitability test, compromise diagnosis or clean bill of health.","forbidden_operations":["application_write_methods","credentials_or_forms","exploit_payloads","secret_file_retrieval","suspected_shell_paths","executable_debug_probes","arbitrary_paths_or_options","access_restriction_retries","unapproved_redirect_origins","concurrent_target_connections"],"limits":{"active_seconds":600,"aggregate_body_bytes":5242880,"concurrent_connections":1,"http_transactions":100,"min_interval_ms":1000,"redirect_hops":3,"request_timeout_seconds":10,"response_body_bytes":262144,"response_header_bytes":32768,"tls_only_checks":20,"transient_retries_per_check":2},"manual_remainder":[{"check_id":"runtime_and_backport_confirmation","description":"Provider/runtime evidence and backport confirmation require reviewed corroboration; advertised banners are insufficient.","scope":"manual_review","status":"not_assessed"},{"check_id":"internal_configuration_review","description":"Internal configuration, authenticated access and filesystem inspection are not authorized by a website hostname.","scope":"separate_authorization_required","status":"not_assessed"},{"check_id":"compromise_investigation","description":"A possible indicator stops this profile; investigation needs separately reviewed scope and authority.","scope":"separate_authorization_required","status":"not_assessed"}],"profile_id":"external-web-v1","schema_version":1}
Loading
Loading