Conversation
* test: pin the RedMesh plugin's source default of TUNNEL_ENGINE_ENABLED=False
What changed:
- New tests/test_plugin_config.py asserts PENTESTER_API_01's _CONFIG explicitly sets
TUNNEL_ENGINE_ENABLED to False (the framework default is True) and leaves PORT to the
framework (None -> a port drawn from 30000-32500 on every start).
Why:
RM-075 Phase 1: the plugin HTTP API is a private-network service and the node's network
policy is its security boundary (hub ADR 0001). That boundary assumes the plugin never
opens a tunnel on its own. This pins the source default; a deploy-time instance config can
still override any _CONFIG key, so the deployed value is verified by the
redmesh-boundary-check runbook, not by this test.
Checks:
- .venv/bin/python -m pytest extensions/business/cybersec/red_mesh/tests/test_plugin_config.py:
2 passed.
- Mutation check: flipping the source default to True fails 1 of 2; restored.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat(auth): require and verify the channel token on every RedMesh endpoint; resolve launch actors server-side
What changed:
- tenancy/channel.py: @channel_token_required, a functools.wraps-based innermost decorator
that runs validate_backend_token before the body. The framework's require_token=True
only rejects a *missing* bearer and binds the value to `token`; it never compares it
(basic_server.j2 get_bearer_token). The comparison now happens once, here, for all 52
endpoints instead of inline in two.
- pentester_api_01.py: every endpoint carries require_token=True and the decorator; 30
bare `@BasePlugin.endpoint` uses become the call form; 50 endpoints gain `token: str`
as their first parameter (the framework raises ValueError at plugin init otherwise);
the two inline token checks and their import are gone.
- Launch attribution: the four launch_* endpoints accept `actor: {account_id}` and resolve
it through a new _resolve_launch_actor seam -> tenancy/identity.resolve_actor over a
minimal cstore-auth reader (tenancy/adapters/cstore_identity.py, needs only
R1EN_CSTORE_AUTH_HKEY; never reads passwords). created_by_name/created_by_id are
derived from the resolved account and the request's values are ignored. Missing,
unknown, tombstoned ('null') and non-active accounts get the contract's unified
not-found denial; a store failure fails closed as 503. Records without schemaVersion
are accepted as version 0 (Navigator writes none); an unrecognised present value is
rejected.
- Tests: conftest's fake endpoint decorator now mirrors the real attributes
(__endpoint__, __http_method__, __require_token__) and an autouse fixture sets
REDMESH_BACKEND_TOKEN; TEST_CHANNEL_TOKEN threaded through 77 positional call sites
in 7 files; test_authz_surface.py enumerates the plugin the framework's way and pins
count, require_token, token-first, the decorator marker, per-endpoint wrong-token
denial with the body untouched, and launch actor denial/derivation;
test_tenancy_channel.py and test_tenancy_identity.py cover the pieces; the native-IPC
harness renders analyze_job with require_token like the real server.
Why:
RM-075 Phase 2, edge-node side. On a private network the shared credential is the
mitigation for other workloads on the same segment - but only if its value is checked,
and only if every endpoint checks it. Launch attribution had been an unverified body
string since the beginning.
Checks:
- pytest extensions/business/cybersec/red_mesh/tests: 2489 passed, 3 skipped (was 2416/3).
- python -m py_compile pentester_api_01.py: compiles.
- Mutation checks on test_authz_surface.py: dropping @channel_token_required from one
endpoint fails 2 (guard + that endpoint's wrong-token subtest); dropping
require_token=True from one endpoint fails 1. Restored.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address execute-plan review round 1 - internal callers must not re-enter guarded endpoints
What changed:
- services/control.py: stop_and_delete_job and purge_all called owner.purge_job(job_id) /
owner.stop_and_delete_job(job_id) - the endpoint methods, which since the sweep take
`token` first and run the channel guard. The job id landed in the token slot, the guard
returned a 403 dict, and the job was stopped but never purged; purge_all counted every job
through that error. They now call the module-level purge_job / stop_and_delete_job.
- services/launch_api.py: the launch_test compatibility shim called owner.launch_webapp_scan
/ owner.launch_network_scan with keyword arguments only - a TypeError (HTTP 500) on every
/launch_test, and no actor forwarded even if it had not been. It now calls the module-level
launchers; the guard and actor resolution already ran once at the launch_test endpoint and
the derived created_by_* are forwarded.
- model_testing/security.py: the deployed token is trimmed before comparison; Navigator trims
its copy, so a trailing newline in the secret would have been a 403 on every call with no
diagnostic.
- tests: test_authz_surface.py gains a caller-side check - no code under pentester_api_01.py,
services/, mixins/, api_mixins/ or worker/ may call an endpoint method on owner/self/plugin
- and a routing test that drives launch_test through the real shim into a patched
module launcher and asserts derived attribution arrives. test_integration.py's
stop_and_delete_job test patched the module seam (it had been changed to assert the broken
one-argument endpoint call); TestPurgeAllJobs and the launch_test routing test route the
module seams to the MagicMock attributes they stub, in the historical shape, so their
assertions stay meaningful. test_tenancy_channel.py covers the trailing-newline token.
Why:
Both independent reviewers found the same class of defect the surface test cannot see: the
plugin re-entering its own endpoints. The suite had been green because the harness injected
the token at exactly that seam. The caller-side test makes the next such regression fail.
Checks:
- pytest extensions/business/cybersec/red_mesh/tests: 2492 passed, 3 skipped.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* test: caller-side re-entry check tolerates whitespace before the call paren
What changed:
- TestInternalCallersDoNotReenterEndpoints matches `owner.<endpoint> (` as well as
`owner.<endpoint>(`, so a spaced call form cannot slip past the check.
Why:
Round-2 verification (main-thread) of the round-1 fix: the package-wide search found no
remaining re-entry - the only same-named hit, repositories/cstore.py:334
self.get_rulebook_review, is JobStateRepository's own method - but the regex had one gap.
Checks:
- pytest tests/test_authz_surface.py: 11 passed, 56 subtests.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: reject explicitly inactive launch actors
What changed:
- Distinguish absent account state from explicit null in the CStore identity reader.
- Exercise all four launch endpoints through the real account reader for inactive states.
Why:
- RM-075 independent review found that null state allowed launches although Navigator rejects it.
Checks:
- Regression reproduced four null-state failures before the fix.
- Focused identity and endpoint tests: 23 passed, 84 subtests passed.
- Full red_mesh suite: 2493 passed, 3 skipped, 391 subtests passed.
- Independent patch review: PASS; task closure remains pending.
* fix: retain launch identity while deferring universal channel tokens
What changed:
Restore the original 52 endpoint signatures and declarations, adding only actor on four launches. Keep the original two model token guards ahead of account lookup, account-derived attribution, tunnel pin, and internal module calls. Remove obsolete channel guards and global token fixtures. Reject malformed stored schema versions uniformly.
Why:
Complete the approved paired RM-075 backend delivery while deferring new universal shared-token requirements and preserving existing model-operation protections.
Checks:
Focused checks: 430 passed, 2 skipped, 83 subtests. Schema regression red reproduced, then 32 passed with 112 subtests. Final full RedMesh suite: 2492 passed, 3 skipped, 4 warnings, 419 subtests in 147.60s. Full signature/decorator comparison against pre-RM-075 baseline passed. git diff --check and changed production py_compile passed. Native Ratio1 runtime source fixture remains unavailable; no live verification.
* feat: preserve tenant membership scope in account resolution
What changed:
- Extend account views with immutable tenant role and scope pairs.
- Reject malformed metadata and memberships without restoring legacy admin authority.
- Preserve active-account attribution and existing endpoint token behavior.
Why:
- Establish RM-026 identity integrity before tenant policy and persistence depend on it.
Checks:
- Full backend: 2498 passed, 3 skipped, 446 subtests.
- Focused identity/surface/config: 38 passed, 139 subtests.
- Two independent implementation reviewers: PASS after paired SDK-boundary correction.
- Staged diff check passed.
* feat: add tenant-scoped named-role policy evaluator
What changed:
- Added a pure existing-active-tenant policy with immutable decisions and scoped role composition.
- Added role matrix, fail-closed context, task ownership, pentesting gate and real-reader regression tests.
Why:
- Advance RM-026 policy foundations without prematurely wiring endpoints or claiming persistent tenant isolation.
- Keep capability exceptions, lifecycle, storage and deployment changes outside this slice.
Checks:
- Focused policy/identity/surface/config: 50 passed, 308 subtests.
- Full backend: 2510 passed, 615 subtests, 3 existing skips, 4 warnings.
- Two independent implementation reviews: PASS; staged whitespace check: PASS.
* feat: resolve stored tenant authorization facts
What changed:
Add read-only scoped tenant and asset projections, a storage reader port, and an authorization resolver. Reuse shared policy prechecks and add boundary regression tests.
Why:
Connect stored identity and resource ownership to policy without trusting caller authority or enabling live endpoints before publication and deployment prerequisites exist.
Checks:
69 focused tests and 402 subtests; full backend 2529 tests and 709 subtests, 3 existing skips, 4 warnings. Two independent reviews PASS. Policy baseline comparisons and staged whitespace checks pass.
* feat: persist tenant administration with verified publication
What changed:
Added seven disabled-by-default tenant administration endpoints, namespace-bound receipts and reservations, verified activation, scoped reads and membership approvals. Added account incarnation projections and regression coverage.
Why:
RM-026 needs persistent administration while retaining identity-provider membership ownership, safe creation retries and the owner's explicitly accepted CStore consistency limitations.
Checks:
Full backend pytest: 2561 passed, 769 subtests, 3 existing skips, 4 warnings. Both independent paired source/spec reviews PASS after publication-binding and Navigator integration corrections. Original policy tests and two model-token gates unchanged. git diff --check passed. Navigator final aggregate verification remains paired phase evidence, not a deployment claim.
* feat: persist scoped tenant pentester policy changes
What changed:
- Add backend-authorized desired-state Allow Pentester updates and capability hints.
- Preserve publication metadata, no-op attribution, and strict JSON boolean input.
Why:
- Deliver the settled RM-026 policy control without enabling unfinished tenant execution.
Checks:
- Focused administration/store/policy/surface: 66 passed, 357 subtests.
- Full RedMesh: 2570 passed, 798 subtests, 3 existing skips, 4 warnings.
- Two independent xhigh source/spec reviews passed; git diff --check passed.
* feat: persist scoped tenant node assignments
What changed:
- Added STA-only desired-state assignment APIs and tenant-filtered node views.
- Added shared record/address validation, lazy peer eligibility and verified per-node writes.
- Tested plugin JSON transport, stored scope, corruption, retries and uncertain writes.
Why:
- Establish basic CStore subfleet bindings without enabling unfinished tenant execution paths.
Checks:
- Focused: 81 tests and 460 subtests passed.
- Full backend: 2585 tests and 901 subtests passed; 3 existing skips and 4 warnings.
- Independent xhigh source review and independent Navigator-agent cross-review passed.
- Staged whitespace check passed; second explicit-xhigh reviewer substitution awaits owner approval.
* test: isolate fingerprint deadline timing from prior garbage
What changed:
- Collect accumulated cyclic garbage before the existing deadline stopwatch and verify shutdown afterward.
Why:
- A reproduced full-suite failure measured a 144.9 ms generation-2 collection inside the timed call; connection cleanup took less than 1 ms.
- Keep GC enabled, the real reader, 200 ms delayed stream, 10 ms budget and less-than-100 ms assertion unchanged. This isolates the fixture, not production scheduling.
Checks:
- Exact deadline test and independent reciprocal/xhigh reviews pass.
- Fingerprint suite: 74 tests and 60 subtests pass, including real sockets.
- Full backend suite with pending asset slice: 2614 tests and 1164 subtests pass; 3 existing skips, 4 warnings.
- git diff --check passes.
* feat: persist and authorize tenant preset assets
What changed:
- Add scoped create/list/detail/update/deactivate APIs for network, web/API and model presets.
- Validate canonical targets and complete CStore records; preserve creation intent, replay, target digests and stale-update guards.
- Add service/store/plugin JSON transport regression coverage and subsystem memory.
Why:
- Supply the approved tenant preset prerequisite using stored identity and scoped STA/SP authority.
- Preserve fixed network IP/per-job ports, explicit web path prefixes and full model endpoint bindings without target probes or new credentials.
- This is administration only, not tenant launch/worker enforcement or serving cutover.
Checks:
- Focused: 102 tests and 695 subtests pass; independent source/spec/security reviews pass.
- Full backend: 2614 tests and 1164 subtests pass; 3 existing skips and 4 warnings.
- Actual Python/TypeScript normalization agrees on 1782 URL combinations and 20 Unicode/name cases.
- Paired Navigator 8c76f9d: 159 suites/1720 tests, typecheck, lint and build pass.
- Fingerprint timing fixture independently stabilized in 948cb555; assertions preserved.
- git diff --check passes. No push, deployment or live scan.
* feat: persist tenant node failure policy
What changed:
- Add strict stop/continue administration endpoint and current-actor capability.
- Allow scoped Super-Tenant Admins and Tenant Admins to update the future-job preference.
- Preserve publication, attribution and verified-write semantics with transport/service tests.
Why:
- Establish the approved tenant setting before launch snapshots and lifecycle enforcement.
- No running-job behavior or stronger coordination is enabled by this slice.
Checks:
- Focused: 74 tests, 430 subtests passed.
- Full unchanged rerun: 2622 tests, 1213 subtests passed; 3 skips and 4 warnings.
- First full run had a native IPC timeout; exact test and full rerun passed unchanged, cause unproven.
- Two independent spec/quality reviews and git diff --check passed.
* feat: add tenant execution admission and binding values
What changed: Resolve current stored tenant, asset, actor and eligible node facts; preserve strict immutable binding values through scan/model config and archive serializers and CStore models. Reject corrupt explicit identity generation and archive binding mutations.
Why: Establish the reviewed prerequisite for tenant-bound launch and worker enforcement without activating endpoints or execution.
Checks: RED/GREEN admission, serialization and review regressions; two independent repeat reviews PASS; focused215 tests/633 subtests; full2637 tests/1309 subtests, 3 skips/4 warnings; diff check PASS. No activation or deployment.
* feat: enforce tenant-bound job execution
What changed:
- Complete RM-026 B2 paired launch, worker, destination, native transport and archive boundaries on the approved 026 branch.
- Reject cached foreign archive identities, malformed counters and stale state observed by the commit writer; retain finalized-stub idempotency.
- Add permanent production-path regressions and component/agent boundary documentation.
Why:
- Saved tenant assets and subfleet assignments must govern executable effects and preserve original attribution through finalization.
- Close the owner-approved review findings without adding schemas, dependencies or distributed coordination. Tenant execution remains source-disabled.
Checks:
- Full RedMesh pytest suite: 2819 passed, 1730 subtests passed, 3 existing skips, 4 warnings.
- Focused archive/model/legacy: 128 passed, 267 subtests passed; native semaphore contract: 4 passed.
- RED reproductions preceded fixes; compilation and staged diff checks passed.
- Resumed review round 2: three cross-assigned reviewers PASS, two non-author approvals per slice; documentation reviews PASS.
- No Navigator/core edits, live target/provider calls, deployment, migration or activation.
* feat: expose tenant execution display hints
What changed:
Add same-account active-asset canLaunchJobs and a strict source configuration capability hint, with policy and projection regressions.
Why:
Support RM-026 B3.1 transport without granting authorization or enabling execution.
Checks:
2823 backend tests and 1765 subtests passed; four native semaphore checks passed; two independent final reviewers passed; git diff --cached --check passed.
* feat: authorize tenant job read snapshots
What changed:
- Resolve current readers before tenant job access and return strictly validated detached snapshots.
- Preserve stable enumeration and sanitize storage failures with permanent race regressions.
Why:
- Establish the RM-026 I1a.1 foundation without reusing launch authority or enabling endpoints.
Checks:
- Full backend: 2847 tests and 1871 subtests passed; 3 existing skips and 4 warnings.
- Focused: 20 tests and 106 subtests passed; baseline and adjacent suites passed.
- Compilation, diff checks, and two independent cumulative reviews passed.
* feat: bind tenant queries and artifacts to checked jobs
What changed:
- Add checked-snapshot query projections, assigned progress reads and archive-owned triage state.
- Validate explicit artifact associations, inline analysis and bounded traversal before publication.
- Document dormant endpoint boundary and preserve legacy omission paths.
Why:
- Deliver RM-026 I1a.2 without global recovery, arbitrary CID access or reports-to-audit escalation.
Checks:
- Full backend: 2892 tests and 2022 subtests passed; 3 existing skips and 4 warnings.
- New suites: 45 tests and 151 subtests passed; adjacent and actual-finalizer checks passed.
- Compilation, whitespace checks and both independent cumulative reviews passed.
* feat: add checked legacy read compatibility
What changed:
- Resolve fresh stored legacy provenance and current rollout before detached job snapshots.
- Extend checked query and typed artifact projections with explicit unbound compatibility mode.
- Add race, alias, mode and partial-model-config regressions and boundary documentation.
Why:
- Establish I1a.3a compatibility inputs before paired requester transport without permitting bound-data fallback.
Checks:
- Full backend: 2929 tests and 2296 subtests passed; 3 existing skips and 4 warnings.
- Combined read suites: 106 tests and 531 subtests; adjacent legacy suites: 372 and 117.
- Compilation and diff checks passed; two independent cumulative round-2 reviews passed.
* feat: bind native ordinary reads to current requester authority
What changed:
Wire ten ordinary POST reads through checked tenant or proven legacy snapshots, associated reports and filtered audit. Validate generated HTTP requests and preserve legal list aliases through the supported response hook.
Why:
Prevent unscoped or stale-authority reads without changing native core or token policy. This is native local delivery; paired Navigator transport and deferred admission remain pending.
Checks:
Full backend: 3343 tests and 2296 subtests passed (3 skips, 4 existing warnings). Focused native 118, root 170/142; compilation and diff checks passed. Two independent cumulative round-2 reviews passed after permanent alias-collision and malformed-stream regressions.
* feat: authorize legacy correlation status reads
What changed:
- Admit current legacy readers and project checked detached correlation status.
- Pair strict native POST/no-store and sanitized model errors with Navigator.
- Cover native fixture lifetime and document the boundary.
Why:
- Close RM-026 I1a.3c.1 without enabling tenant integrations or read effects.
Checks:
- Canonical backend: 3422 passed, 2310 subtests, 3 skips, 4 warnings.
- Navigator: 170 suites / 2682 tests; build, typecheck, lint pass.
- Compilation, diff checks and both cumulative independent reviews pass.
* feat: authorize legacy export status reads
What changed:
- Protect four export-status endpoints with current legacy admission and detached metadata.
- Reject identity/control collisions; preserve producer dry-run semantics.
- Pair strict native POST/no-store with Navigator and document corrected inventory.
Why:
- Deliver RM-026 I1a.3c.2 without changing export mutations or tenant activation.
Checks:
- Full backend: 3738 tests / 2310 subtests pass; 3 skips, 4 warnings.
- Navigator: 171 suites / 2986 tests plus build/typecheck/lint pass.
- Both cumulative independent reviews, real producer wire replays, compilation/diff pass.
* feat: authorize and validate legacy rulebook reads
What changed:
- Pair two checked requester-bearing POST reads with exact native manifests and typed errors.
- Validate copied metadata, review, audit and submission records before publication; reuse checked staleness traversal.
- Add native producer, corruption, alias, null and finite-number regression coverage.
Why:
- Close direct native requester and record-association bypasses without authorizing tenant effects or changing mutations.
Checks:
- Full RedMesh plus native semaphore: 4270 tests, 2310 subtests passed (3 skips, 4 existing warnings).
- Focused 534 passed; compilation and diff checks passed.
- Two independent cumulative xhigh spec, quality and security reviews passed; paired Navigator replays passed.
* feat: authorize legacy MISP configuration status
What changed:
- Require requester-bearing POST and fresh legacy identity/rollout admission.
- Validate and detach the exact four-field public configuration projection before native wrapping.
- Cover roles, flags, severities, malformed transport, producer corruption and zero-domain-effect denials.
Why:
- Close actorless configuration reads without granting tenant integration or export authority.
Checks:
- Canonical backend 4530 tests / 2310 subtests passed (3 skips, 4 existing warnings).
- Broad focused 739 tests / 102 subtests and expanded configuration suite 244 passed.
- Compilation/diff and two independent cumulative spec, quality and security reviews passed.
* fix: contain unscoped public LLM diagnostics
What changed:
Make llm_health an exact actor-only POST that returns static unavailable without domain diagnostics. Preserve internal health helpers and document the intentional public compatibility break.
Why:
No scoped diagnostic permission or safe provider response contract is approved; ordinary read access must not authorize provider testing.
Checks:
TDD direct and real RAW/WRAPPED native regressions; 795 focused tests and 102 subtests; full backend 4582 tests and 2310 subtests, 3 existing skips and 4 warnings; compile/diff checks; two independent spec, quality and security reviews PASS.
* fix: keep public finding triage unavailable
What changed:
Replace unscoped triage mutation with exact actor-only POST denial; reject legacy mutation bodies and preserve checked reads, internal persistence and submission fences. Document future UI ownership in RM-076.
Why:
The owner deferred triage until its tenant UI, role authority, audit-return and SOC effects have an approved paired contract.
Checks:
TDD direct/native/persistence/inventory cases; 1057 focused tests and 104 subtests; canonical RedMesh backend 4614 tests and 2310 subtests with 3 existing skips and 4 warnings; two isolated spec, quality and security reviews PASS; explicit negative-control probes, compile and staged whitespace checks.
* feat: authorize legacy MISP JSON download
What changed:
- export_misp_json becomes a checked POST (job_id, pass_nr, request_actor),
admitted through _read_operation with the new reports:export operation
- legacy read admission maps stored role literally: role=admin wins, else
app_role=pentester, else user; memberships-present still denies
- explicit pass selection takes the first match, omitted takes the last,
matching the existing export resolver rather than analysis_pass()
- disabled returns {status:"disabled"} after admission and before the
scan-family check; enabled model jobs return typed 400 unsupported
- an existing selected running pass with a null/empty CID now fails closed
as 503 corrupt-storage instead of 404
- consumed config/pass/aggregate payloads are checked for numeric
finiteness before PyMISP stringifies risk/CVSS values
- internal config accessor stays bound-checked and is excluded from report()
Why:
- RM-026 slice I1a.3c.7: the endpoint previously accepted a job ID with no
requester and then read global job/config/pass/aggregate data. Preserves
the legacy download while placing it behind a checked requester and
artifact-ownership edges. Tenant export activation remains I1b.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4790 passed, 3 skipped,
2337 subtests (pre-slice baseline 4782)
- focused envelope suites: 790 passed, 308 subtests
- two independent non-author round-2 reviews: PASS, no blocking findings
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: serve configuration-only legacy integration readiness
What changed:
- get_integration_status becomes a checked actor-only POST admitted through
the legacy seam: fresh stored actor, literal membership-key absence and
current rollout, with all three legacy roles allowed
- new get_public_integration_config rebuilds from the six existing base
builders and never calls _load_status_record or _merge_record, so no
persisted delivery history can reach the public view
- the projection publishes exactly eleven pinned keys per integration and
fails closed on any builder-contract deviation, including an error class
outside the six configuration codes
- status is derived from the booleans rather than passed through, so a
delivery-health lifecycle string cannot transit
- the historical producer is retained, documented and still under test as
F2 groundwork; record writes and cooldown policy are unchanged
Why:
- RM-026 slice I1a.3c.8. The endpoint was unauthenticated and returned
persisted last_event_id/last_artifact_cid aggregated across jobs the
caller may not own. The owner chose to omit unowned history while
retaining safe configuration/readiness status.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4836 passed,
3 skipped, 2337 subtests (4790 before this slice)
- new suite is verified RED: 11 of 30 fail against the old history
producer, and all 18 denial cases fail if the projection is moved
above the admission block
- two independent non-author reviews: spec compliance PASS, no blocking
findings; the mis-targeted ordering guard they found is fixed here
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: add effect state and the effect operation seam
What changed:
- new tenancy/effects.py: EffectState (none/persisted/delivered), a
monotonic EffectLedger, and classify_effect_failure
- extract _admitted_snapshot so _read_operation and the new
_effect_operation share exactly one admission implementation
- _effect_operation classifies failure on the ledger rather than on the
caught exception, so a partially completed effect is never reported
as unavailable
- a denial raised after an effect landed reports the incomplete effect,
not a clean denial
Why:
- RM-026 I1b contract 6. _read_operation ends in a blanket
except Exception -> 503 "unavailable", which asserts that nothing
happened. That is true for a read and false for an effect that may
have already written a bundle or delivered it off-node; a caller that
retries on 503 would duplicate it.
- the state cannot ride the return value: a service that raises after
persisting returns nothing. The ledger is therefore passed into the
service and stays readable after a raise, which the tests pin.
Checks:
- new focused suite: 7 passed, written RED first (module absent)
- existing read admission unchanged: 486 passed across
test_tenant_read_native and test_legacy_read_access
- pytest red_mesh/tests + native semaphore contract: 4843 passed,
3 skipped, 2337 subtests (4836 before)
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit and scope the persisted-artifact effects (B1)
What changed:
- dry_run_opencti_export, dry_run_taxii_export, export_stix_bundle and
test_event_export now carry request_actor and run through
_effect_operation (test_event_export via _admit_actor_only, since it
has no job to scope against)
- extract _admit_actor_only from the two endpoints that inlined it, and
give it the reports:export role gate so a job-less effect cannot be
reached by an account a job-bearing one would refuse
- replace the unscoped global job lookup on these paths: the checked
snapshot now threads through _prepare_*_export, build_stix_bundle and
_resolve_pass_data
- record EffectState.PERSISTED once a bundle is on disk
- add public_effect_result: the framework maps any dict carrying an
`error` key to HTTP 503, so a plain "integration disabled" outcome
would reach the caller as a server failure. Results now use `status`
and `configuration_error`, matching export_misp_json's precedent
- declare the four routes in _EFFECT_FIELDS rather than widening what
_READ_FIELDS means; they share the read guard's strict transport
Why:
- RM-026 I1b B1. These endpoints accepted a job id with no requester and
read the job globally. Two also wrote integration status records on
their denial paths, so probing job ids polluted the failure counters
that feed cooldown policy.
Checks:
- new B1 suite: 36 passed, including denial-path canaries asserting
record_integration_status is never called on any of 7 fault classes
- pytest red_mesh/tests + native semaphore contract: 4879 passed,
3 skipped, 2337 subtests (4843 before)
- fixed a cross-module sentinel bug found by the suite: each service
defines its own _UNSET, so forwarding one into build_stix_bundle made
it compare against a different object and treat the sentinel as a job
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B1 review round 1
What changed:
- record EffectState.PERSISTED before the job-record mutation regardless
of `persist`, and EffectState.DELIVERED after the SOC emission. With
persist=False a packet could leave the node and a later failure still
reported "unavailable"
- public_effect_result becomes a whitelist and validates
configuration_error against the same set the readiness view uses. It
previously stripped only `error`, so probe_opencti/probe_taxii's
detail: str(exc) - carrying exception prose, the configured host and
an env var name - travelled to the browser at HTTP 200
- implement contract 4: EffectLedger.checkpoint() re-resolves the
requester and re-checks the job binding, called immediately before
each persist and before the emission. A no-op for internal callers
- _effect_operation refuses a tenant_id rather than handing a
tenant_bound snapshot raw to services that would bypass
checked_job_snapshot and TenantJobArtifacts
- make the effect signature assertions live: the loop filtered on
ROUTES + READ_ROUTES, so the new branch was dead code
- fix the vacuous scoping canary: the fixture's integrations resolve to
disabled, so _prepare_* returned before the job read and the canary
passed without exercising the scoping it names
Why:
- two independent non-author reviews both reported FAIL on contracts 4,
6 and 7, and both found the dead transport branch.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4884 passed,
3 skipped, 2337 subtests (4879 before)
- B1 suite 41 passed, including a persist=False test proving a delivered
event reports effect_incomplete/delivered rather than unavailable
- signature branch proven live: corrupting an expectation now fails
Corrected claim: effect routes are NOT part of the generated read API.
Adding them to the install-time model validator broke 261 tests because
no model is generated for them. Listing them in _READ_PATHS only shapes
transport errors as JSON with no-store; it does not give them the
generated routes' field validation. The comment now says so.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B1 review round 2
What changed:
- let a typed 500 effect_incomplete survive the read guard. Listing the
effect paths in _READ_PATHS put them under protected_http, which
coerces any status outside its map to 503 "unavailable" - so over real
HTTP the entire effect-state mechanism reported the exact falsehood it
exists to prevent, and Navigator's effect_incomplete was unreachable.
An unrecognised effect_state still collapses to 503
- cover the dry-run job-record seam in both OpenCTI and TAXII: the
round-1 fix had landed only in export_stix_bundle, so both still wrote
the job record with the ledger at NONE when _persist_bundle returned
falsy
- gate DELIVERED on the emission result. emit_export_status_event never
raises and returns {"status": "skipped"} when SOC export is disabled -
the common configuration - so recording it unconditionally claimed a
delivery that never happened
- thread a ledger through test_event_export and bring the service call
inside the guard: deliver_redmesh_event performs a real send (dry_run
only affects the status stamp) and a raise previously escaped to the
framework, which prints the exception string
- restore stix_bundle and last_exported_at to the whitelist. Dropping
them broke the STIX manual Download button outright
- add the config codes these services actually emit (missing_url,
missing_server_url, missing_collection_id, unsupported_mode). The
whitelist held none of them, so every real misconfiguration published
configuration_error null and the UI read "unknown_error"
Why:
- both independent reviewers returned FAIL. Three of the six round-1
fixes were real code but not load-bearing.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4890 passed,
3 skipped, 2337 subtests (4884 before)
- B1 suite 47 passed, and the controls are now mutation-proven:
neutering checkpoint() fails a test (previously 48/48 passed), and
removing the new config codes fails the drift guard
- transport passthrough tested directly, including that a spoofed
effect_state still collapses to 503
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B1 review round 3
What changed:
- handle the RAW detail shape in protected_http. The typed passthrough
worked only in WRAPPED: RAW unwraps the plugin dict, so the framework
reduces the detail to the bare error string and destroys effect_state,
and the guard fell through to 503 "unavailable". RAW now publishes
effect_state "unknown" - the persisted/delivered distinction is lost
there, but "something landed, do not blindly retry" survives
- register effect routes in the rendered native harness and the test
scheduler, so they have end-to-end HTTP coverage at all. Neither the
round-2 nor the round-3 defect was visible to tests that call plugin
methods directly
- add an end-to-end test parametrized over RAW and WRAPPED; removing the
raw branch fails RAW and leaves WRAPPED passing
- gate test_event_export's DELIVERED on status == "sent".
deliver_redmesh_event returns "sent" | "disabled" | "error" and never
"skipped", so the previous gate recorded a delivery when the
integration was disabled and nothing left the node
- pin the whitelist against its UI consumers, so dropping a field they
read fails a test rather than breaking a button silently
- correct the _READ_PATHS comment: it understated the guard, which does
enforce POST-only, no query string and exact field names and types
Why:
- round 3 returned PROCEED from the implementation reviewer and REVISE
from the security auditor, who reproduced the RAW collapse end to end.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4894 passed,
3 skipped, 2337 subtests (4890 before)
- B1 suite 51 passed; the RAW branch, the checkpoint, the DELIVERED gate
and the config-code drift guard are each mutation-proven
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit and instrument the OpenCTI and TAXII pushes (B2 half 1)
What changed:
- push_to_opencti and publish_to_taxii carry request_actor and run
through _effect_operation with the checked snapshot and a ledger
- implement the returned-failure mechanism: services return delivery
failures rather than raising, so the except arms never saw them and a
failed push after a successful persist carried no effect_state at all.
The predicate discriminates on the failure class, not the ledger
alone - a typed configuration code keeps its code and gains
effect_state, while an untypable failure with a landed effect becomes
effect_incomplete
- record PERSISTED after each persist, checkpoint immediately before the
outbound call, and record DELIVERED only on acceptance evidence:
OpenCTI needs 2xx, no GraphQL errors array and an upload id, since
GraphQL returns 200 with errors; TAXII accepts 200/201/202, because
202 means the objects left the node
- register both routes in _EFFECT_FIELDS and EFFECT_ROUTES so they have
end-to-end HTTP coverage
Why:
- RM-026 I1b B2. These endpoints put data on third-party systems, where
reporting "nothing happened" after a landed effect invites a retry
that duplicates it on someone else's server.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4902 passed,
3 skipped, 2337 subtests (4894 before)
- new B2 suite 8 passed, written RED first: the four over-the-wire
delivery tests failed before the mechanism existed
- both halves of the predicate mutation-proven: removing it fails the
delivery tests, removing the typed-code carve-out fails the
configuration tests
export_misp is deliberately not registered yet. It is still a bare GET
decorator, and the native renderer raises on one; it joins in B2's MISP
phase with its GET->POST conversion, per the plan's fallback split.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit the MISP push and delete the swallowing mixin (B2 half 2)
What changed:
- export_misp becomes a checked POST carrying request_actor, running
through _effect_operation, and is registered in _EFFECT_FIELDS and
EFFECT_ROUTES now that it is no longer a bare GET decorator
- push_to_misp gains checked_job/snapshot_mode/ledger, which its two
siblings already had, and both its scoping seams are closed: the entry
read and build_misp_event, which reaches the store again through
_resolve_pass_data
- delete _export_to_misp and its now-dead import. Its blanket
except Exception -> {"error": str(exc)} both leaked exception prose to
the response and the log and destroyed the raise the ledger depends on
- replace MISP's prose errors with typed codes: missing_credentials,
connection_failed, api_error. The connection error text can carry the
configured MISP URL
- the disabled path no longer emits. Routing through _effect_operation
made that branch live for the first time, and emitting a SOC event
plus a job-record write on a disabled integration would be behaviour
OpenCTI and TAXII do not have
- record DELIVERED at the first accepted add_object, and on a MISPEvent
with a uuid. PyMISP has no status code, so that is the only acceptance
evidence; the re-export loop means the remote can hold part of an
export before the update completes
Why:
- RM-026 I1b B2, MISP phase. A probe could previously name a real job it
did not own and cause a SOC emission plus a job-record write.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4904 passed,
3 skipped, 2337 subtests (4902 before)
- test_misp_export, native execution and authz surface: 139 passed
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B2 security review
What changed:
- add the MISP checkpoints contract 4 required and B2 shipped without.
The review demonstrated a revoked requester's payload still reaching
the MISP server, while the identical race against OpenCTI stopped
before the outbound call. Checkpoints now sit before the transport is
constructed and around the job-document mutation in
_record_export_status, which also records PERSISTED
- fix vacuous acceptance evidence on the re-export branch: add_object
and update_event results were never inspected and response_event was
assigned the locally held event, so a fully rejected re-export
reported status "ok" with a DELIVERED ledger
- give MISP the denial matrix, contract-4 test, transport-failure and
rejected-re-export tests it had none of. B2 shipped its highest-risk
endpoint with zero coverage, which is the failure mode this slice's
own plan opens by warning about
- type OpenCTI's connection fault: it returned type(exc).__name__, which
nothing could publish, so every connection failure showed the generic
fallback
- extend the config-code drift guard to the delivery producers, not just
_config_error. It immediately caught push_failed and the denial codes
- replace the last MISP prose with a typed push_failed
- prune the dead _export_to_misp reference from the config-read canary
Why:
- the security review returned FAIL on two High findings: contract 4 was
absent on the least recoverable endpoint, and that endpoint had no
tests at all.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4914 passed,
3 skipped, 2337 subtests (4904 before)
- B2 suite 20 passed, B1 suite 51; the MISP checkpoint is mutation-proven
(removing it reproduces the demonstrated defect) and so is the extended
drift guard
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B2 implementation review
What changed:
- carry configuration_error through the transport guard. _read_error_response
rebuilt the body from scratch, so the typed code never reached the wire:
for OpenCTI and TAXII the persist always precedes the outbound call, so
every delivery failure was post-persist and lost its reason. Prose is
still refused by a conservative shape check
- stop the not-configured branch emitting. It is the twin of the disabled
branch and was equally dead before this slice, so it was shipping the
exact behaviour change the plan rejected for its twin - a SOC event and
a job-record write on a misconfigured integration
- require a uuid, not just isinstance, before recording MISP DELIVERED:
str(None) would otherwise publish "None" as an event id
- publish a typed disabled_reason so the MISP panel can say why, instead
of showing a generic failure
- add the tests mutation testing proved were missing: the pre-outbound
checkpoint on both HTTP services, both MISP scoping seams, MISP
delivery-on-acceptance, and the misconfigured-path effect freedom
Why:
- the implementation review found the published-delivery-code decision
inert over HTTP, and six controls surviving mutation with a green suite.
Checks:
- pytest red_mesh/tests + native semaphore contract: 4920 passed,
3 skipped, 2337 subtests (4914 before)
- the three previously-surviving mutations now die: reverting MISP seam
one, deleting MISP's DELIVERED record, and deleting OpenCTI's
pre-outbound checkpoint each fail a named test
This is the third time a control has been live in the plugin and inert
over the wire - B1 round 2, B1 round 3, and now the configuration_error
rebuild. Every later slice should assert the response at the HTTP
boundary, not from a direct plugin call.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit the ingest endpoints (B3)
What changed:
- correlate_suricata_eve and upload_authorization carry request_actor;
correlate runs through _effect_operation with the checked snapshot,
upload through _admit_actor_only since it has no job
- close the unscoped read that fed a cross-tenant write: an unadmitted
caller could overwrite the detection_correlation field of any job id
it could guess
- checkpoints and ledger records at all three seams: the artifact
persist, the job-record write, and the R1FS document write
- stop reporting ok when _write_job_record returns None without writing.
The binding guard can trip, and the caller was told the opposite of
what happened
- record the derived account as uploaded_by in the document envelope, so
RM-078's deferral is a stored fact rather than a plan sentence. Never
caller-supplied
- publish parse failures through an allowlist
Why:
- RM-026 I1b B3. Both endpoints were unauthenticated, and the correlate
denial path wrote a node-global status record routed through the
cooldown policy, so probing job ids was an availability effect for
every tenant.
Checks:
- canonical backend suite: 4936 passed, 3 skipped, 2337 subtests
(4920 before)
- new B3 suite 16 passed; the scoping fix and the no-op-write fix are
each mutation-proven
Correction carried into the code: earlier revisions called the parse
failure a plain exception-string leak. _parse_eve_jsonl raises only
eve_jsonl_too_large or invalid_jsonl_line_<n>, neither carrying caller
content, and the line number is useful diagnostics. The allowlist keeps
it while ensuring a future raise that does carry prose cannot publish.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B3 security review
What changed:
- publish `correlation` and the parse/write outcome codes. Routing
correlate through public_effect_result dropped the payload the
operator reads, so over the wire a rejected EVE upload and a
successful zero-match correlation were both HTTP 200 with nothing to
tell them apart - and the UI checks only res.ok, so a truncated last
line in a live EVE log reported a clean zero-match result
- add the over-the-wire tests the plan required first and the slice
shipped without. The omission was not harmless: they are exactly what
would have caught the above
- record PERSISTED only after _write_job_record actually returns. It was
recorded before the attempt, so a refused write reported effect_state
"persisted" when nothing had landed
- route the upload result and its failures through the projection. A
bare `error` key makes the framework raise, which diverged RAW (503)
from WRAPPED (R1FS exception prose verbatim at HTTP 200)
- fix a vacuous canary: the upload denial test patched the service
module while the plugin imports the symbol into its own namespace, so
assert_not_called bound nothing. Same trap as an earlier slice
- replace a tautological assertion that passed on the broken behaviour
- assert the parse allowlist at its real sink, the node-global status
record, rather than at a response the projection filters anyway
- assert the bound-job refusal the plan listed as an obligation
Why:
- the security review returned spec-compliance FAIL with two High
findings, and demonstrated the operator-facing consequence rather than
asserting it.
Checks:
- canonical backend suite: 4946 passed, 3 skipped, 2337 subtests
(4936 before)
- B3 suite 26 passed; both halves of the blocking defect are
mutation-proven across RAW and WRAPPED
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit the rulebook review mutations (B4)
What changed:
- all four mutations carry request_actor and admit through a new
_review_operation, which reuses _admitted_snapshot rather than
_effect_operation. The effect wrapper rewrites any status:error dict
to effect_incomplete once the ledger is non-NONE, which would collapse
the seventeen typed conflict codes whose retryable flag and fence
detail are the revision-fence UX
- the signer is derived from the admitted account at the parameter
boundary, so all twelve storage sites follow. A rulebook review is an
attestation and the backend previously accepted whatever name it was
handed
- admission precedes the caller-keyed submission lock, so probing job
ids can no longer grow an unbounded module-global dict
- the checked snapshot replaces the unscoped job read in all four
- narrow update_rulebook_review to {draft, reviewed} as defence in
depth: the only caller already clamps to these, so this closes nothing
reachable but aligns the backend with the BFF
- carry typed conflict codes as error_code. Registering these endpoints
made the framework map their `error` key to a plugin error, which the
read guard collapsed to 503 - destroying the codes this slice exists
to protect
Why:
- RM-026 I1b B4. These are the four endpoints the I1a exit gate recorded
when it noted that a parameter named actor is not evidence of
admission.
Checks:
- canonical backend suite: 4988 passed, 3 skipped, 2337 subtests
(4946 before)
- boundary tests written FIRST and RED: all 42 failed before the
implementation, including the payload-survives-the-wire cases that
caught the 503 collapse before it was committed
The 503 collapse is the fifth instance of a control live in the plugin
and inert over the wire. It is the first one caught before commit,
because the boundary test existed first.
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B4 security review
What changed:
- checkpoint and record the ledger at BOTH write helpers. The ledger was
threaded into all four service signatures and referenced nowhere in
their bodies, so a landed review write whose audit append failed was
reported as unavailable - "no trace" - while the attestation's state
had already changed. The plan named this twin as the single most
likely place to add a control in one helper and miss the other, and
the first attempt missed it at both
- replace the signer test. It was vacuous three ways and substituting
the literal "attacker" for the derived account left all 42 tests
green. It now asserts the stored reviewer end to end where the write
is reachable, and the derived signer at the endpoint-to-service
boundary for the rest; the mutation now fails 5 tests
- drop the interpolated validation message. _validate_review_answers
raises f-strings embedding question_id and answer_value from the
request body, and both endpoints passed str(exc) into the response,
which the UI prefers for display. The typed code survives
Why:
- the security review returned two HIGH findings, both failures to
implement things this slice's own plan specified.
Checks:
- canonical backend suite: 4990 passed, 3 skipped, 2337 subtests
- B4 suite 44 passed; the signer control is now mutation-proven
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit rulebook assessment generation (B5)
What changed:
- generate_rulebook_assessment carries request_actor and admits through
_review_operation, keeping its own rich response shape like the B4 four
- close the seam B4 budgeted and did not reach: the checked snapshot now
threads through build_rulebook_assessment into _resolve_scan_context,
which read the job from the store directly
- checkpoint and record the ledger before the content-addressed artifact
persist
Why:
- RM-026 I1b B5. The endpoint was unauthenticated, built an assessment
from an unscoped job read, persisted an artifact and wrote assessment
metadata three times.
Checks:
- canonical backend suite: 5001 passed, 3 skipped, 2337 subtests
(4990 before)
- boundary tests written FIRST and RED: all 11 failed before the
implementation
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit manual analysis and revalidate at completion (B6)
What changed:
- analyze_job takes request_actor and resolves one fresh admission decision before the busy check
and the drain, so an unadmitted caller can neither observe nor disturb the global analysis slot.
- The checked snapshot is threaded into _prepare_manual_analysis instead of a second unscoped
_get_job_from_cstore read.
- The requester travels on the analysis state, and _finalize_manual_analysis re-checks it before any
completion effect. _ManualAnalysisWork is frozen and holds no plugin reference, so the worker
cannot revalidate; the finalize step on the serialized plugin loop is the only seam that can.
- analyze_job joins the strict read transport (_EFFECT_FIELDS, EFFECT_ROUTES). Without it the
framework mapped the denial dict to a plugin error and answered 503, so a 403/404 admission
decision reached the caller as a generic outage.
- Three existing suites stand admission up explicitly rather than inheriting it: the native IPC
scheduler bypass is scoped to the stub owner by identity so the real get_job_status admission in
the same test stays live.
Why:
- RM-026 I1b B6. Legacy half only; the owner also granted manual analysis to scoped STA/SP and
Tenant Pentesters, and that half is RM-078.
- The boundary test was written before the implementation it guards, across RAW and WRAPPED. It
found the sixth control in this series that was live in the plugin and inert over the wire --
this time before the commit rather than after.
Checks:
- pytest extensions/business/cybersec/red_mesh/tests: 5011 passed, 3 skipped, 2337 subtests
- pytest tests/test_manual_analysis_b6.py: 14 passed
- mutation: neutering submit-time admission fails 12 of 14; removing the completion revalidation
fails the revalidation test (canary bound on the instance, not the class)
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B6 review findings
What changed:
- analyze_job comes back off the strict read transport. Putting it there collapsed its typed
operational codes -- analysis_busy 409, analysis_state_changed 409 retryable,
analysis_request_unavailable 410, analysis_input_too_large 413, analysis_input_invalid 422,
analysis_timeout 504 -- to a generic 503, because _read_error_response allowlists only
{400,401,403,404,405,503}. The generated server already honours a plugin's own status_code, and
that is what carries the admission 403/404 to the caller.
- New UNGUARDED_EFFECT_ROUTES renders analyze_job in the native fixture with the guard installed,
so a wire test can see the collapse. The suite that asserts 409 over HTTP renders main.py without
install_generated_read_api, which is why a green suite could not.
- checked_job is now required on _prepare_manual_analysis. Deleting it at the call site previously
left all 5,011 tests green: the only test for that control asserted the parameter name was in the
signature, which cannot see the call site.
- _finalize_manual_analysis reuses the snapshot its revalidation just produced instead of issuing a
second unscoped read of the same record.
- The admission stubs in the amended suites no longer return None. Every happy path was running the
removed checked_job fallback, so no test anywhere exercised a real snapshot reaching the
preparation step; that is now asserted by object identity.
- The stale comment claiming I1 had not yet supplied admission is corrected, and the
execution_binding branch is documented as the fail-closed residue for RM-078.
Why:
- Two independent reviews. Security: PASS, no blocking findings. Implementation:
DONE_WITH_CONCERNS, spec compliance PASS, no blocking findings. Both named the wire collapse; the
implementation review also produced the surviving mutation (M1) that this commit closes.
Checks:
- pytest, 15 file groups covering all 143 test files: 5,004 passed, 3 skipped, ~2,300 subtests
- 6 failures in one group are identical at HEAD -- pre-existing order dependence in
test_tenant_execution_native.py, which passes alone (78) and with its usual neighbours (583)
- mutation: re-adding analyze_job to _EFFECT_FIELDS fails the new wire test in RAW and WRAPPED;
dropping checked_job= at the call site fails the new threading test
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit restricted raw model-test evidence (B7)
What changed:
- get_raw_model_test_evidence takes request_actor and routes through _read_operation with
operation="reports:export". It was the last GET on the surface, took only a job_id, and returned
the decrypted restricted artifact -- the raw prompts and model responses a model test captured.
- GET becomes POST, so a job id no longer travels in a URL into proxy logs, browser history and
referrer headers for the most restricted payload the plugin serves.
- The job-type check now runs on the admitted snapshot instead of a second unscoped
_get_job_from_cstore read, and a denial outranks it: unsupported_job_type distinguished a real job
from an absent one for a caller with no authority at all.
- The pre-existing happy-path test now runs through admission with a real legacy reader rather than
around it.
Why:
- RM-026 I1b B7. The gate decides itself rather than needing an owner call: audit:view requires a
super_tenant_admin membership, which the legacy seam forbids outright, so it is unreachable in
this half; reports:export is the strictest gate that can be satisfied. RM-078 owns the
tenant-scoped half.
- Deliberately NOT on the strict read transport, and the tests say so. The guard rebuilds error
bodies and allowlists only {400,401,403,404,405,503}, so it would collapse unsupported_job_type
and the three raw_evidence_* codes -- the regression B6 had to revert. The generated server
already honours the plugin's own status_code, which is what carries the 403/404.
- Contracts 4 and 6 are vacuous here: a read lands no effect and has nothing to revalidate at
completion, so the slice gets admission, POST and a wire assertion and no further ceremony.
Checks:
- pytest, 16 file groups covering all 144 test files: 5,018 passed, 3 skipped, ~2,300 subtests
- 6 failures in one group are identical at HEAD (pre-existing order dependence in
test_tenant_execution_native.py)
- mutation: reverting the endpoint body to its unscoped read fails 13 of the 14 new tests
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B7 review findings
What changed:
- Cache-Control: no-store now covers the two admitted endpoints that stay off the strict read
transport. _NO_STORE_PATHS stamps the header on http.response.start without entering
_read_error_response, so the typed codes survive and the decrypted restricted artifact is no
longer the one response on this surface a caching intermediary could store.
- The transport rationale is corrected. It was written as if RAW and WRAPPED behaved alike. They do
not: in RAW a top-level `error` becomes a 500 the guard would rewrite to `unavailable`; in WRAPPED
on_response nests the dict under `result`, no HTTPException is raised, and the guard never sees
it. Both formats are now asserted instead of assumed symmetric.
Why:
- Two independent reviews of 4f5b2b09. Security: PASS, no blocking findings, with no-store as its
one substantive concern. Implementation: spec compliance FAIL on the missing Navigator pairing
(fixed in the redmesh-navigator repo), plus the half-true transport claim and the same no-store
gap.
Checks:
- pytest, 16 file groups covering all 144 test files: 5,022 passed, 3 skipped, ~2,300 subtests
- 6 failures in one group are identical at HEAD (pre-existing order dependence in
test_tenant_execution_native.py)
- mutation: emptying _NO_STORE_PATHS fails 6 tests across RAW and WRAPPED
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* feat: admit GDPR engagement deletion and derive its audit actor (B8)
What changed:
- delete_job_engagement takes request_actor and is admitted at reports:export before it reads the
job, loads the JobConfig, or deletes anything. The redaction moved to _redact_job_engagement,
which consumes the admitted snapshot instead of its own unscoped state-repository read.
- requested_by is gone. It was a caller-supplied string written straight into the deletion audit
record as the attributed actor (services/engagement_deletion.py:315-316); the attribution now
comes from the resolved account and nothing else.
- _review_operation takes an optional projection. B8 passes a pass-through: its typed codes must
keep travelling under `error`, which is what its only client reads, and it reports explicit
per-stage counts rather than one code, so the rename has nothing to protect.
- The ledger is recorded at the two irreversible seams -- PERSISTED once the sanitized JobConfig is
on disk, DELIVERED per document actually deleted -- so an unexpected raise after either cannot
reach the caller as "nothing happened".
- Four failure branches no longer interpolate the exception into the returned message; the prose
stays in the node log.
Why:
- RM-026 I1b B8. This is the B4 defect class on a destructive operation: a parameter named after an
identity is not evidence of one, and here it decided who a GDPR deletion was attributed to.
- Not _effect_operation: it rewrites any `status: "error"` dict to effect_incomplete once the ledger
is non-NONE, which would collapse the per-stage counts. A partial redaction has to say which
documents went and which did not.
- Not on the strict read transport, for the same reason: the guard rebuilds the body and would
discard those counts along with the typed code.
Checks:
- pytest, 16 file groups covering all 145 test files: 5,036 passed, 3 skipped, ~2,300 subtests
- 6 failures in one group are identical at HEAD (pre-existing order dependence in
test_tenant_execution_native.py)
- mutation: substituting the attributed actor fails test_the_audit_record_carries_the_resolved_account
-- the same mutation that was vacuous three ways in B4
Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC
* fix: address B8 review findings
What changed:
- The response uses the review projection after all. The pass-through kept the typed code under
`error`, and the framework maps any dict carrying `error` to a plugin error -- in RAW the template
then reduces the whole body to the bare code string, discarding documents_deleted,
documents_failed and new_job_config_cid on exactly the partial outcomes that exist to report them.
Under the projection the response stays 2xx and carries every field in both formats.
- ledger.checkpoint() before the irreversible delete loop. `record` does not run the checkpoint
callback -- only `checkpoint` does -- so authority was established once, before three storage
round-trips, and never rechecked, unlike services/authorization_upload.py:218.
- Admission narrows to role == "admin" via a new _review_operation(require_admin=True). The shared
reports:export gate admits an admin OR an app_role pentester, which is right for the rulebook
mutations and broader than the owner's recorded B8 decision, on an endpoint that irreversibly
deletes authorization documents. Deny-by-default until the owner says otherwise.
- The `project` parameter added by the previous commit is gone; it had no caller and was used as a
boolean, so a real projection passed later would have been silently ignored.
- Two more {exc} interpolations removed, in services/engagement_deletion.py.
- /delete_job_engagement joins _NO_STORE_PATHS. The comment above UNGUARDED_ROUTES claimed all its
entries got no-store, which was false for this one.
- The pre-admission `if not job_id` short-circuit is gone, so a blank job id answers
400 invalid_request from admission like every other endpoint.
Why:
- Two independent reviews. Security: PASS, no blocking findings. Implementation: spec compliance
FAIL on the RAW count loss and the service-layer prose, plus the missing checkpoint and no-store.
- Four tests were vacuous or absent, and the reviews proved it by mutation rather than assertion:
test_failures_do_not_publish_exception_prose patched a symbol called outside any try, so the
wrapper's generic handler satisfied it and none of the four de-prosed branches was covered; and
the delivered-state test …
… MISP floor (#500) * fix: redact credential pairs in NIS2 rulebook evidence refs What changed: - services/rulebook_assessment.py: `_safe_text` now applies the shared `redact_credential_text` rule before its own keyword/PEM/bearer patterns. - tests/test_rulebook_assessment.py: generated assessment with a default-credential finding title carries `user:***`, never the secret. Why: - RM-064 Phase 1 (reopened 2026-09-03): the delivered PDF's §3.7.3 printed both default-credential pairs in cleartext, twice each. This module carried its own sanitiser whose patterns require a `password=`-style prefix and never matched a bare `user:secret` pair in a finding title. Reusing the shared rule instead of adding a fourth regime. Checks: - pytest tests/test_rulebook_assessment.py: 35 passed - client job 6cc55610 titles through `_safe_text`: 25 pairs -> 0 (scratchpad, by path only) Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * fix: close three credential-redaction regex defects What changed: - credential_redaction.py: post-colon `(?![\s/])` guard so the `with`/`accepts` leads no longer destroy URLs; a continuation rule masks every pair in a comma/semicolon/and-separated list, not only the first; comments record why there is deliberately no port guard on this rule. - mixins/report.py `_CRED_RE`: secret quantifier `{3,64}` -> `{3,}` so a secret longer than 64 characters no longer publishes its tail; cross-reference to the five-digit port concession and to the blackbox path. - tests/test_credential_redaction.py (new): direct unit tests pinning every probe phrasing, the URL guard, list masking, `admin:1234`. - tests/test_normalization.py: 100-character secret masked to the end. Why: - RM-064 Phase 1 items found 2026-09-04: (a) `with https://host:8080/x` became `with https:***`; (b) `admin:admin, admin:password` left the second pair intact; (c) `user:***AAAAAA` for long secrets. The architect review proposed a port guard on the ambiguous leads; rejected because the HTTP Basic evidence reads `GET … with admin:1234 -> HTTP 200` and that pair is in the default list — a port guard there would publish it. Checks: - pytest test_credential_redaction.py test_normalization.py test_rulebook_assessment.py test_event_redaction.py: 86 passed - old `{3,64}` rule reproduced the tail leak on a 100-char secret before the fix Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * fix: redact credential pairs inside the LLM input builders What changed: - llm_input_builder.py `_sanitize`: applies `redact_credential_text` to every string that flows through `build_llm_input`, so all three callers (pentester_api_01, services/llm_structured, redmesh_llm_agent_api) are covered without each having to redact first. - mixins/redmesh_llm_agent.py `_sanitize_untrusted_text` and the graybox `top_vulnerable_titles` summary: same rule. This mixin hand-builds its payload without `build_llm_input`. - tests: default-credential pair never reaches the LLM input payload; the agent sanitizer masks a pair. Why: - RM-064 Phase 1 `[~]` item (found 2026-09-04 in the #491 review): only one of three `build_llm_input` call sites redacted the report first, and the narrative the model writes is persisted separately from the findings, so a finding-level fix never reached it. Checks: - pytest test_llm_input_isolation.py test_llm_agent_injection.py test_llm_agent_validator.py test_hardening.py test_secret_isolation.py test_credential_redaction.py: 87 passed - both new tests red before the change Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * fix: deny-by-default credential redaction across the three scrubbers What changed: - credential_redaction.py: `redact_credential_strings(obj)` walks dicts, lists and tuples and applies the phrasing rule to every string leaf except under `IDENTITY_KEYS` (hashes, ids, enums). - Wired as the final step of `_scrub_flat_finding` (graybox storage boundary), `_redact_report` (whole redacted report, after the enumerated shape-based walks) and `build_finding_event` (SIEM payload). - Tests: a pair under a key none of the three scrubbers enumerate is masked on each path; whole-report data without credential phrasing is untouched. Why: - RM-064 Phase 1 (found 2026-09-04): all three scrubbers were field allowlists, and each carried a comment naming a field that leaked because it was not on the list (`evidence_items`, `affected_assets`, `vulnerabilities`, `accepted`, `web_tests_info`). The next field added leaked by default. Walking every string is safe because the rule is phrasing-anchored; the URL guard from the previous commit is what makes that hold, so this commit depends on it. Checks: - pytest test_findings_redaction.py test_normalization.py test_event_redaction.py test_credential_redaction.py: 97 passed - full red_mesh suite with the hooks in place: 5518 passed, 3 skipped, the single failure being this commit's own graybox test before its fixture was corrected (`error` never reaches the flat contract) Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * feat: derive CVSS base scores and withhold contradicting probe templates What changed: - cvss.py (new): CVSS v3.1 base score from a vector and the qualitative band, tested against published scores. - findings.py `enrich_finding_for_probe`: populates `cvss_score` from the finding's vector when the probe supplied none (NVD scores on CVE findings are kept); a probe `cvss_template` is now attached only when its band agrees with the finding's severity label. - tests: scored template, withheld contradicting template, probe-supplied vector scored, supplied score not overwritten. Why: - RM-064 item 3 / Phase 3 `[~]`: every finding on the client job carried a vector with `cvss_score` null, and 31 of 61 carried a vector whose band contradicted the label. An AST scan of `worker/` on 2026-09-21 found 91 of 168 statically resolvable `Finding(...)` sites in a different band from their probe's template — the template is the probe's worst case, the label is per finding. Re-labelling 91 sites is a product decision; withholding a vector that contradicts the label removes the contradiction now, and any probe that wants a vector on such a finding sets its own. - The score is set before identity stamping, so `finding_signature` moves once for findings that gain a score (content change); `finding_id` is unaffected. Checks: - pytest test_cvss.py test_probe_registry.py test_finding_identity_contract.py test_probes.py: 317 passed Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * feat: record the MISP severity floor and counts on the export record What changed: - services/misp_export.py: `build_misp_event` returns the `MIN_SEVERITY` floor it applied plus exported/total finding counts; `push_to_misp` stores them on the job's `misp_export` record; `get_misp_export_status` surfaces them (None on records written before this). - tests: stored record after a successful push carries the floor; status surfaces it, and reports None rather than a default for older records. Why: - RM-064 item 7: the export intentionally omits findings below `MIN_SEVERITY` (56 objects against 61 findings on job 6cc55610) and the report never said so, because nothing recorded the floor at export time. The Navigator already reads this status; the report can now disclose the floor from it. Checks: - pytest test_misp_export.py test_tenant_export_status.py test_integration_status.py: 327 passed Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * feat: gate default-credential findings on the negative control (RM-069) What changed: - worker/service/common.py: `_default_credential_findings` builds the SSH/FTP/Telnet default-credential findings after each probe's existing random-pair test. Random pair rejected -> CRITICAL, `certain` when an authenticated action completed (`id` over SSH, `PWD` over FTP, `id`/`uname` over Telnet), `firm` on a bare handshake. Random pair accepted -> INFO, `tentative`, title marked inconclusive, pointing at the "accepts arbitrary credentials" finding. `raw_data.auth_control` records the control outcome. - tests/test_default_credential_control.py (new): verdict in both directions, proof gating, and the SSH probe wired end to end with paramiko mocked. - tests/test_credential_redaction.py: the new title and evidence shapes are masked. Why: - RM-069, promised in the 2026-08-26 client reply: a host accepting arbitrary credentials also produced CRITICAL default-credential findings asserting a genuine weakness, and a successful handshake was the entire evidence. The control already ran in every probe; its outcome never reached the verdict. Execution order is unchanged, so the number of authentication attempts per host is unchanged. Checks: - pytest test_default_credential_control.py test_credential_redaction.py test_probes.py test_normalization.py: 308 passed Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * feat: package-aware CVE matching with explicit backport status (RM-070) What changed: - cve_db.py: `DistroPackage` + `parse_distro_package` read the distribution suffix a banner announces (`OpenSSH_8.9p1 Ubuntu-3ubuntu0.10`, `Debian-2+deb12u2`); `_compare_debian_revision` orders package revisions dpkg-style (`0.3 < 0.10`, `~` before release); `BACKPORT_FIXES` is a small static advisory table (USN-6859-1 rows for CVE-2024-6387 on 22.04/24.04); `check_cves(..., package=)` drops matches the package revision has fixed and states `backport_status` (`not_fixed` -> `firm`, `unknown` -> `tentative`, with the reason in the description) on the rest. - findings.py: `Finding.backport_status` field. - worker/service/common.py: the SSH probe passes the parsed package to `check_cves` and records `raw_data.ssh_package`; `_ssh_identify_library` is unchanged. - tests/test_cve_backport.py (new): real banner shapes, revision ordering, status resolution, the client job banner no longer reporting regreSSHion, callers without a package unchanged. Why: - RM-070, promised in the 2026-08-26 client reply. The matcher compared the bare upstream version, so a package that had the fix backported was reported vulnerable with only a prose disclaimer. Decision: a static table now, a USN/DSA/OVAL feed as the recorded follow-up; anything the table does not cover is `unknown`, never silently fixed. Confidence stays `tentative` on unknown, so the risk score already weights it at half. Checks: - pytest test_cve_backport.py test_check_cves_enrichment.py test_probes.py test_finding_identity_contract.py test_default_credential_control.py: 340 passed Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * fix: profile the SSH proof-action timeout through the resolver What changed: - `_ssh_authenticated_action` takes its timeout from the caller's `_target_timeout(3)` instead of a literal. Why: - The full suite's timeout call-site audit (`test_network_worker_waits_use_resolver_except_timing_probe`) allows one literal network wait in `worker/`; the RM-069 proof action added a second. Checks: - pytest test_timeout_profile.py test_default_credential_control.py: 15 passed - full red_mesh suite before this fix: 5548 passed, 3 skipped, this one failure Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * fix: address execute-plan review round 1 (backend) What changed: - credential_redaction.py: an empty secret is masked too (`\S*?`), so `Auth OK for root:` — `("root", "")` is a live MySQL default — no longer discloses that the password is blank; the walker docstring records the `with X:Y` over-masking cost. - worker/service/common.py: the negative control is tri-state (`rejected` | `accepted` | `not_run`); a control whose attempt failed before the server answered caps the default-credential finding at `firm` and says so, instead of reading as a passed control. Telnet cannot distinguish the two and is documented as such. - services/misp_export.py: the duplicated `findings_exported` / `findings_total` keys in `build_misp_event`'s result are gone (the counts were already there; only `min_severity` was new). Why: - Reviewer findings 2, 5 and 8 from the independent implementation review of `feat/rm-064-closeout`; none blocking, all cheap, two of them credential-verdict honesty items worth landing before the client rerun. Checks: - pytest test_default_credential_control.py test_credential_redaction.py test_misp_export.py test_normalization.py test_event_redaction.py test_rulebook_assessment.py test_llm_input_isolation.py test_findings_redaction.py test_probes.py test_timeout_profile.py: 470 passed Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * fix: accept an empty credential secret only where probes print one What changed: - _PAIR_TEMPLATE: the secret is one or more characters, or empty only at the end of the text or before " (" (the `root:` and `ftp: (anonymous)` shapes). - test_credential_redaction pins four benign "with/for <word>: <text>" labels. Why: - The review-round-1 empty-secret allowance turned every `with <word>: <text>` into `<word>:*** <text>`. No leak, but the deny-by-default walker runs the rule over every string, so finding prose was corrupted. Found by the closeout e2e on a graybox report line. Checks: - pytest red_mesh/tests: 5553 passed, 3 skipped - live archives 267a1f40 / 9b6f4928 and both rendered PDFs: 0 unmasked pairs - git diff --check: clean Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * fix: fail a graybox job whose first pass aborted before any probe ran What changed: - graybox worker records abort_reason_class on its report; the aggregation rules and AggregatedScanData carry the abort state (the archive round-trip dropped it before). - PassReport carries aborted / abort_reason / abort_phase / abort_reason_class. - finalization: _pass_abort_state derives an "empty abort" (aggregated.aborted and no probe attempted). On pass 1 it is a FAILED job via _mark_scan_aborted (failure_class scan_aborted, redacted message naming the phase, timeline scan_aborted, lifecycle redmesh.job.failed, archive kept); later passes emit pass_aborted and continue. An empty abort skips risk scoring and the LLM stage; an abort after probes ran finalizes as before with the fields set. Why: - A scan that failed to log in finalized as a completed job with a risk score computed from its own "Scan aborted" finding and four minutes of analysis of nothing; the console, the PDF and the e2e harness all read it as a successful scan (RM-085, found by the closeout live e2e). Checks: - pytest test_api.py test_finalization_aggregation.py test_worker.py: 279 passed - pytest red_mesh/tests: 5557 passed, 3 skipped - live graybox a5fb8293 against the 500-on-login fixture: FAILED in 17 s, failure_class scan_aborted, no ANALYZING stage - git diff --check: clean Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * fix: address review of the scan-aborted finalization rule What changed: - A later monitoring pass that aborted with no probe attempted no longer overwrites the job's headline risk_score (the pass record still says 0). - An empty abort submits no terminal attestation and is not counted as a failed required attestation; the job fails as scan_aborted instead. - _abort_reason_text: credential redaction, control bytes stripped, 240-char cap, used for failure_message and both abort timeline labels (the same treatment the LLM boundary gives abort_reason). - Tests: risk_score preserved on pass 2; no attestation call on an empty abort; the sanitizer's bounds. Why: - Independent review of 623b029 (RM-085): readers take pass_reports[-1] as the job's score, an on-chain "0 vulnerabilities" record for a scan that never logged in is a worse artifact than none, and two _abort callers embed exception text or the operator's target URL. Checks: - pytest test_api.py test_finalization_aggregation.py test_worker.py: 281 passed - pytest red_mesh/tests: full suite result recorded in RM-085 - git diff --check: clean Claude-Session: https://claude.ai/code/session_01Ep4r5HaHr1rF7Kc27YHdmC * chore: increment version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.