Skip to content

#352: the beacon sensor graded a supervised Worker as if nobody had started it - #68

Merged
opencdlee-dotcom merged 1 commit into
agent/fable-precision/assemblyfrom
agent/precision-s5/beacon-parents
Sep 24, 2026
Merged

opencdlee-dotcom merged 1 commit into
agent/fable-precision/assemblyfrom
agent/precision-s5/beacon-parents

Conversation

@opencdlee-dotcom

Copy link
Copy Markdown
Owner

#352: "Persistent outbound connection (beacon shape)" was HIGH with no rung on ~/actions-runners/professor-os/bin.2.336.0/Runner.Worker. In the same scan, the process sensor graded those same bytes supervised.

Cause

  • check_processes passes _grade_binary the ancestor exe list, so the process sensor sees that the operator-vouched Runner.Listener started the Worker from its own install directory.
  • The network sensors never passed parents, and _outbound_rows threw away the pid that would have let them.
  • Beacon records carried no ancestry, so the replay could not re-derive the grade either.

Change

  • _parents_by_pid(exe_by_pid=None) is now the one place that answers "who started this pid", used by all three sensors. It reuses _ancestry and its pid-reuse guard. It builds the ancestry table at most once, only when a pid is actually asked about. On a host with no vouch (or a tampered vouch store) it answers [] and never reads the process table. check_processes now calls it too; its behaviour is unchanged.
  • _outbound_rows returns (path, ip, port, pid) on all three platforms.
  • check_outbound keeps the pids beside the rows, never in them. The stored beacon history stays (path, ip, port, trust), because a pid would turn every restart into a new row. It passes an ancestry(path, ip, port) lookup to _outbound_findings and _beacon_recurrence.
  • Both beacon emitters (fixed endpoint and rotating) and _outbound_findings:
    • pass parents to _grade_binary;
    • record ancestry (exe paths) on the finding when it is non-empty;
    • hand parents to _class_facts, as the process sensor does. _outbound_findings never called _class_facts and still does not.
  • Replay: _reobserve now reads recorded ancestry for net-beacon and net-outbound (_REOBSERVE_ANCESTRY_CATEGORIES), not only for process. A beacon record without ancestry, where a vouched program could have earned it the rung, is counted field missing: ancestry instead of being graded as if nobody started it.
  • Listener: checked, not wired. diff_listeners grades through _grade_binary without parents, but its snapshot is keyed path:port and drops the pid by design, so that a restart is not a new listener. Wiring it would need a pid side-channel out of three platform snapshotters. No case in the corpus needs it: the Worker does not listen, and the Listener is vouched itself.

Tests (tests/test_beacon_parents.py, written first; 8 of 9 failed before the change)

These drive the real check_outbound over a fixed process, ancestry and outbound table.

  • A Worker whose ancestor is the vouched Listener in the same install dir → beacon MEDIUM supervised, outbound LOW supervised, and ancestry == [Listener].
  • Ancestor not vouched → HIGH, no rung; the ancestry is still recorded as evidence.
  • A vouched ancestor in another directory → HIGH, no rung.
  • A row with no pid → HIGH, no rung, no ancestry, and no table read.
  • No vouch on the host → 0 ancestry-table reads and 0 process-table reads, and no ancestry field.
  • Two beacon rows → the ancestry table is read once.
  • Replay: a record with ancestry re-derives supervised; a record without it is replayed as recorded with field missing: ancestry.

Verification (this Mac)

  • Affected test files: 653 passed, 2 skipped, 30 xfailed, 10 subtests.
  • Full suite: 2 failed, 2471 passed, 6 skipped, 30 xfailed, 83 subtests passed in 1221.46s. Tree hashes matched before and after the run. Both failures are pre-existing on the assembly base: they fail the same way on an untouched git archive 19e6608.
    • TestHostileArgsSeverity::test_benign_interpreter_agent_stays_low
    • TestExpandedHostileArgs::test_benign_args_stay_low
    • Both are 'INFO' != 'LOW' for a /bin/echo launchd job. That is persistence grading, which this change does not touch.
  • Paired backtest replay --days 30 --reobserve, run concurrently on one snapshot (62,799 events, 214 noise-labelled), 19e6608 vs this branch:
    • Interrupts: unchanged (total 54; process 6; net-beacon 3). Open cases 57. Assay recall 9/21.
    • Noise re-opened: 54/214 before and after, but the split moves. Before: re-derived 4 (#352 #382 #432 #452), as recorded 50. After: re-derived 3 (#382 #432 #452), as recorded 51.
    • #352 is now as recorded · field missing: ancestry, and that reason covers 6 incidents: #289 #290 #351 #352 #384 #534.
    • This is the expected outcome, since the recorded beacons carry no ancestry: #352 leaves the re-derived target, but cannot be shown fixed from history.
  • Live evidence (the sandbox scan driver, run against this tree and a copy of the live state; no job was running, so no Worker existed):
    • rc 0, scan took 65s. It opened 0 incidents, and the real custody ledger stayed at 151 lines before and after.
    • With vouches present on this host, the real network findings now carry ancestry. Example: net-beacon LOW operator-vouched for professor-os/bin/Runner.Listener has ancestry ['./externals/node20/bin/node', '/bin/bash', '/sbin/launchd'], and the VS Code extension beacons carry their Code Helper chain.
    • A Worker beacon needs a running job, so #352's live re-grade was not observed.

🤖 Generated with Claude Code

#352: a Runner.Worker's "Persistent outbound connection (beacon shape)",
HIGH with no rung, while the process sensor graded the same bytes
`supervised` in the same scan. check_processes handed _grade_binary the
ancestor exe list; the network sensors never did, and _outbound_rows
dropped the pid that would have let them.

_parents_by_pid is now the one spelling of "who started this pid" for all
three sensors: _ancestry's walk (pid-reuse guard included), the ancestry
table built at most once and only on the first pid actually asked about,
and never on a host with no vouch. _outbound_rows keeps the pid; both
beacon emitters and _outbound_findings pass `parents` and record
`ancestry` (exe paths); the pid is kept beside the stored beacon history,
never in it. The replay now reads recorded ancestry for net-beacon and
net-outbound as it does for process, so a beacon without it where a vouched
program could have earned the rung is counted `field missing: ancestry`.

The listener is not wired: its snapshot is keyed path:port and drops the
pid by design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@opencdlee-dotcom
opencdlee-dotcom merged commit 81f48e6 into agent/fable-precision/assembly Sep 24, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant