PRD 0062: supervisor override for egress token blocks
When the outbound DLP catches a token, route the block through the existing supervisor approval queue instead of returning 403 outright. The egress proxy holds the request open until the operator answers, then remembers an approved value for the life of the proxy so the request -- and later ones carrying it -- flow through. Fails closed on rejection, timeout, malformed response, or when supervise is disabled. - ScanResult.matched carries the raw matched substring (sidecar-only; never logged or written to the proposal). scan_outbound and the token detectors take a safe_tokens set and skip approved values, continuing past a safelisted match so a second secret in the same request is still caught. - New egress-token-allow proposal tool, written directly to the queue by the addon (the gitleaks-allow pattern from PRD 0061). build_token_allow _payload renders host/method/path/detector reason + redacted context. - Async request hook polls the queue without stalling the proxy event loop; EGRESS_TOKEN_ALLOW_TIMEOUT_SECONDS (default 300) bounds the wait. - Supervisor TUI renders egress-token-allow like gitleaks-allow: report only, modify unavailable, approval requires a recorded reason. - Unit tests for the matched/safe-tokens plumbing, payload builder, tool constant round-trip, and TUI paths; README + PRD 0062. Closes #261. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnvBjPZC5V7qeQpFbQdDmS
This commit is contained in:
@@ -22,8 +22,10 @@ from bot_bottle.egress_addon_core import (
|
||||
MatchEntry,
|
||||
PathMatch,
|
||||
Route,
|
||||
ScanResult,
|
||||
build_inbound_scan_text,
|
||||
build_outbound_scan_text,
|
||||
build_token_allow_payload,
|
||||
decide,
|
||||
decide_git_fetch,
|
||||
evaluate_matches,
|
||||
@@ -1167,5 +1169,55 @@ class TestScanInbound(unittest.TestCase):
|
||||
self.assertEqual("block", result.severity)
|
||||
|
||||
|
||||
class TestScanOutboundSafeTokens(unittest.TestCase):
|
||||
"""PRD 0062: scan_outbound threads the supervisor-approved safe-tokens
|
||||
set into the token detectors."""
|
||||
|
||||
def test_safe_token_allows_request(self):
|
||||
text = build_outbound_scan_text(
|
||||
host="api.example.com", path="/v1/data", query="",
|
||||
headers={}, body=f"key={_AWS_KEY}",
|
||||
)
|
||||
self.assertIsNone(
|
||||
scan_outbound(_ROUTE, text, {}, safe_tokens={_AWS_KEY})
|
||||
)
|
||||
|
||||
def test_unrelated_safe_token_still_blocks(self):
|
||||
text = build_outbound_scan_text(
|
||||
host="api.example.com", path="/v1/data", query="",
|
||||
headers={}, body=f"key={_AWS_KEY}",
|
||||
)
|
||||
result = scan_outbound(_ROUTE, text, {}, safe_tokens={"ghp_" + "A" * 36})
|
||||
self.assertIsNotNone(result)
|
||||
assert result is not None
|
||||
self.assertEqual(_AWS_KEY, result.matched)
|
||||
|
||||
|
||||
class TestBuildTokenAllowPayload(unittest.TestCase):
|
||||
def test_payload_includes_context_and_no_raw_token(self):
|
||||
result = ScanResult(
|
||||
severity="block",
|
||||
reason="AWS access key found in body",
|
||||
location="body",
|
||||
context="key=******** tail",
|
||||
matched=_AWS_KEY,
|
||||
)
|
||||
payload = build_token_allow_payload(
|
||||
"api.example.com", "POST", "/v1/ingest", result,
|
||||
)
|
||||
self.assertIn("host: api.example.com", payload)
|
||||
self.assertIn("method: POST", payload)
|
||||
self.assertIn("path: /v1/ingest", payload)
|
||||
self.assertIn("AWS access key found in body", payload)
|
||||
self.assertIn("key=******** tail", payload)
|
||||
# The raw matched value must never appear in the proposal file.
|
||||
self.assertNotIn(_AWS_KEY, payload)
|
||||
|
||||
def test_payload_omits_context_line_when_empty(self):
|
||||
result = ScanResult(severity="block", reason="r", matched="x")
|
||||
payload = build_token_allow_payload("h", "GET", "/", result)
|
||||
self.assertNotIn("context:", payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user