SIGN IN SIGN UP

fix(networkstream): make the tree budget an actual bound (JSON escaping) (SUB-7786)

The budget was not a bound. estimateTreeBytes charged len(s), but that is not what
encoding/json emits: it escapes " and \\, every control byte, and -- because
Marshal enables HTML escaping -- <, > and &, which real command lines are full of
(sh -c 'cmd > /dev/null 2>&1'). Invalid UTF-8 is replaced byte-for-byte with the
6-byte \ufffd, and process argv is arbitrary kernel bytes, not guaranteed UTF-8.
Each such byte costs up to six where len() counted one.

Measured: 629 processes with 1 KB of non-UTF-8 argv each estimated at 772 KB --
29% of the budget -- so the under-budget fast path shipped every tree, dropped
nothing and logged nothing, while the real message was 5.37 MB after base64. The
broker rejects that, the snapshot is dropped, and the node loses its ENTIRE
interval of traffic. Reachable by an ordinary shell loop, and reachable
deliberately by anyone who can exec in a container on the node -- which made it a
detection-evasion primitive with a wider blast radius than the data-loss bug this
branch fixes. It also undercounted ordinary traffic: a realistic single node
estimated 372 against 395 marshalled.

escapedLen now charges the true escaped cost, rounding every escape up to 6 bytes,
and processNodeOverheadBytes goes 200 -> 320, measured against a fully-populated
node rather than a sparse test one. The estimator now overestimates by ~19% for
realistic trees. TestEstimateTreeBytes_NeverUnderestimates enforces the direction
across 15 shapes -- escape-heavy, non-UTF-8, wide, over-deep, legacy Children --
and fails on 10 of them with the old len()-based estimate.

Ranking changed from most-connections-first to smallest-tree-first. The old
rationale was backwards for the threat it named: a low-and-slow beacon opens
exactly one connection per interval, so it sorted last and lost its tree first --
the precise case reputation attribution exists to catch. Smallest-first maximises
the number of processes keeping a tree, which is the best objective available when
the sensor cannot know which process matters.

Also closes test gaps a mutation pass found: the determinism contract was unpinned
(removing both ref tie-breaks passed the suite), as were skip-vs-break packing, the
estimator's legacy-Children branch, the wrapper-field copy and countConnections.
All numbers in the code comments and the feature doc are re-derived from one
measured set -- the previous 4,000-connection row claimed 3.85 MB using lean test
entries when production-weight entries put it at 6.01 MB, over the limit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Alon <alon@armosec.io>
A
Alon committed
733d9bac0c58874e0656c1bd4098cfb1a68af699
Parent: b32773d