Add dlp.outbound_on_match policy (block | redact | supervise)
Give each egress route a policy for what the proxy does when an outbound DLP detector matches a token, defaulting to the supervise flow added in the previous commit. The goal is cutting false-positive friction without weakening default-deny. - redact: scrub the matched value(s) from the body, non-host headers, and path/query via redact_tokens, then re-scan. Forward if clean; fail closed with a 403 if a match remains on a surface redaction can't rewrite (the hostname, or a unicode-evasion token). For routes where a token-shaped value is noise the upstream doesn't need. - block: the original hard 403, never overridable. - supervise (default, unset): hold the request for operator approval. Structural blocks (CRLF, no safelist-able value) stay hard 403s under every policy. Threads outbound_on_match from the bottle manifest (manifest_egress) through the resolved EgressRoute and rendered routes.yaml (egress.py) to the addon's Route (egress_addon_core), and round-trips it via the list-egress-routes introspection endpoint. The allow/egress-block tool descriptions document the new key. Tests: manifest parse/validation, core parse/validation, full manifest->render->addon round-trip for redact. README + PRD 0062 updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnvBjPZC5V7qeQpFbQdDmS
This commit is contained in:
@@ -329,6 +329,23 @@ class TestRenderRoutes(unittest.TestCase):
|
||||
self.assertEqual(("token_patterns",), addon_routes[0].outbound_detectors)
|
||||
self.assertEqual((), addon_routes[0].inbound_detectors)
|
||||
|
||||
def test_outbound_on_match_round_trips(self):
|
||||
from bot_bottle.egress_addon_core import load_routes
|
||||
b = _bottle([{"host": "logs.example", "dlp": {
|
||||
"outbound_on_match": "redact",
|
||||
}}])
|
||||
routes = egress_routes_for_bottle(b)
|
||||
rendered = egress_render_routes(routes)
|
||||
self.assertIn('outbound_on_match: "redact"', rendered)
|
||||
addon_routes = load_routes(rendered)
|
||||
self.assertEqual("redact", addon_routes[0].outbound_on_match)
|
||||
|
||||
def test_outbound_on_match_default_omitted_from_render(self):
|
||||
b = _bottle([{"host": "x.example"}])
|
||||
routes = egress_routes_for_bottle(b)
|
||||
rendered = egress_render_routes(routes)
|
||||
self.assertNotIn("outbound_on_match", rendered)
|
||||
|
||||
def test_git_fetch_policy_round_trips(self):
|
||||
from bot_bottle.egress_addon_core import load_routes
|
||||
b = _bottle([{"host": "github.com", "git": {"fetch": True}}])
|
||||
|
||||
@@ -269,6 +269,25 @@ class TestParseDlp(unittest.TestCase):
|
||||
"dlp": {"wat": True},
|
||||
}]})
|
||||
|
||||
def test_outbound_on_match_default_empty(self):
|
||||
routes = parse_routes({"routes": [{"host": "x.example"}]})
|
||||
self.assertEqual("", routes[0].outbound_on_match)
|
||||
|
||||
def test_outbound_on_match_parsed(self):
|
||||
for policy in ("block", "redact", "supervise"):
|
||||
routes = parse_routes({"routes": [{
|
||||
"host": "x.example",
|
||||
"dlp": {"outbound_on_match": policy},
|
||||
}]})
|
||||
self.assertEqual(policy, routes[0].outbound_on_match)
|
||||
|
||||
def test_outbound_on_match_invalid_rejected(self):
|
||||
with self.assertRaises(ValueError):
|
||||
parse_routes({"routes": [{
|
||||
"host": "x.example",
|
||||
"dlp": {"outbound_on_match": "nope"},
|
||||
}]})
|
||||
|
||||
|
||||
# --- load_routes ---------------------------------------------------------
|
||||
|
||||
|
||||
@@ -302,6 +302,24 @@ class TestDlp(unittest.TestCase):
|
||||
"bogus": True,
|
||||
}}])
|
||||
|
||||
def test_outbound_on_match_omitted_is_empty(self):
|
||||
b = _bottle([{"host": "x.example"}])
|
||||
self.assertEqual("", b.egress.routes[0].OutboundOnMatch)
|
||||
|
||||
def test_outbound_on_match_accepts_policies(self):
|
||||
for policy in ("block", "redact", "supervise"):
|
||||
with self.subTest(policy=policy):
|
||||
b = _bottle([{"host": "x.example", "dlp": {
|
||||
"outbound_on_match": policy,
|
||||
}}])
|
||||
self.assertEqual(policy, b.egress.routes[0].OutboundOnMatch)
|
||||
|
||||
def test_outbound_on_match_rejects_unknown_value(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
_bottle([{"host": "x.example", "dlp": {
|
||||
"outbound_on_match": "allow",
|
||||
}}])
|
||||
|
||||
|
||||
class TestGitPolicy(unittest.TestCase):
|
||||
def test_omitted_means_https_git_fetch_disabled(self):
|
||||
|
||||
Reference in New Issue
Block a user