Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d4d930b29 | |||
| 3866461bf4 | |||
| 664c67a272 | |||
| 26a1a51cbb | |||
| e82bbb587f | |||
| c89a0d334a | |||
| ac9b6d593f |
+16
-23
@@ -82,6 +82,14 @@ class EgressAddon:
|
|||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
{"Content-Type": "text/plain; charset=utf-8"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _block(self, flow: http.HTTPFlow, reason: str) -> None:
|
||||||
|
sys.stderr.write(f"{reason}\n")
|
||||||
|
flow.response = http.Response.make(
|
||||||
|
403,
|
||||||
|
reason.encode("utf-8"),
|
||||||
|
{"Content-Type": "text/plain; charset=utf-8"},
|
||||||
|
)
|
||||||
|
|
||||||
def request(self, flow: http.HTTPFlow) -> None:
|
def request(self, flow: http.HTTPFlow) -> None:
|
||||||
request_path, _, query = flow.request.path.partition("?")
|
request_path, _, query = flow.request.path.partition("?")
|
||||||
|
|
||||||
@@ -100,25 +108,18 @@ class EgressAddon:
|
|||||||
scan_text = auth_header + "\n" + body
|
scan_text = auth_header + "\n" + body
|
||||||
dlp_result = scan_outbound(route, scan_text, os.environ)
|
dlp_result = scan_outbound(route, scan_text, os.environ)
|
||||||
if dlp_result is not None and dlp_result.severity == "block":
|
if dlp_result is not None and dlp_result.severity == "block":
|
||||||
flow.response = http.Response.make(
|
self._block(flow, f"egress DLP: {dlp_result.reason}")
|
||||||
403,
|
|
||||||
f"egress DLP: {dlp_result.reason}".encode("utf-8"),
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Strip inbound Authorization — agent cannot smuggle tokens.
|
# Strip inbound Authorization — agent cannot smuggle tokens.
|
||||||
flow.request.headers.pop("authorization", None)
|
flow.request.headers.pop("authorization", None)
|
||||||
|
|
||||||
if is_git_push_request(request_path, query):
|
if is_git_push_request(request_path, query):
|
||||||
flow.response = http.Response.make(
|
self._block(
|
||||||
403,
|
flow,
|
||||||
(
|
"egress: git push over HTTPS is not supported; "
|
||||||
b"egress: git push over HTTPS is not supported; "
|
"use the bottle.git SSH path (gitleaks-scanned by "
|
||||||
b"use the bottle.git SSH path (gitleaks-scanned by "
|
"git-gate's pre-receive hook).",
|
||||||
b"git-gate's pre-receive hook)."
|
|
||||||
),
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -135,11 +136,7 @@ class EgressAddon:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if decision.action == "block":
|
if decision.action == "block":
|
||||||
flow.response = http.Response.make(
|
self._block(flow, decision.reason)
|
||||||
403,
|
|
||||||
decision.reason.encode("utf-8"),
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if decision.inject_authorization is not None:
|
if decision.inject_authorization is not None:
|
||||||
@@ -159,11 +156,7 @@ class EgressAddon:
|
|||||||
if result is None:
|
if result is None:
|
||||||
return
|
return
|
||||||
if result.severity == "block":
|
if result.severity == "block":
|
||||||
flow.response = http.Response.make(
|
self._block(flow, f"egress DLP: {result.reason}")
|
||||||
403,
|
|
||||||
f"egress DLP: {result.reason}".encode("utf-8"),
|
|
||||||
{"Content-Type": "text/plain; charset=utf-8"},
|
|
||||||
)
|
|
||||||
elif result.severity == "warn":
|
elif result.severity == "warn":
|
||||||
sys.stderr.write(f"egress DLP warn: {result.reason}\n")
|
sys.stderr.write(f"egress DLP warn: {result.reason}\n")
|
||||||
|
|
||||||
|
|||||||
@@ -120,11 +120,10 @@ class TestSandboxEscape(unittest.TestCase):
|
|||||||
# is intentionally unreachable — the pre-receive
|
# is intentionally unreachable — the pre-receive
|
||||||
# gitleaks hook must reject BEFORE git-gate
|
# gitleaks hook must reject BEFORE git-gate
|
||||||
# attempts the upstream push.
|
# attempts the upstream push.
|
||||||
"git": {"remotes": {
|
"git-gate": {"repos": {
|
||||||
"unreachable.invalid": {
|
"throwaway": {
|
||||||
"Name": "throwaway",
|
"url": "ssh://git@unreachable.invalid:22/throwaway.git",
|
||||||
"Upstream": "ssh://git@unreachable.invalid:22/throwaway.git",
|
"identity": str(cls._key_path),
|
||||||
"IdentityFile": str(cls._key_path),
|
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -110,10 +110,10 @@ class TestSmolmachinesLaunch(unittest.TestCase):
|
|||||||
# (high-numbered) so we're confirming TSI refusal, not
|
# (high-numbered) so we're confirming TSI refusal, not
|
||||||
# just "no service listening."
|
# just "no service listening."
|
||||||
r = self.bottle.exec(
|
r = self.bottle.exec(
|
||||||
"wget -T 3 -t 1 -O - http://127.0.0.1:9 2>&1 || true"
|
"curl -s --show-error --max-time 3 http://127.0.0.1:9 2>&1 || true"
|
||||||
)
|
)
|
||||||
# `wget` to a denied destination produces a connect error.
|
# `curl` to a denied destination produces a connect error.
|
||||||
# The exact phrasing varies (busybox vs gnu); we assert
|
# The exact phrasing varies by curl version; we assert
|
||||||
# the response is NOT the body of any real service.
|
# the response is NOT the body of any real service.
|
||||||
self.assertNotIn("hello-from-vm", r.stdout)
|
self.assertNotIn("hello-from-vm", r.stdout)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
@@ -126,10 +126,10 @@ class TestSmolmachinesLaunch(unittest.TestCase):
|
|||||||
|
|
||||||
def test_prompt_file_lands_in_guest(self):
|
def test_prompt_file_lands_in_guest(self):
|
||||||
# provision_prompt copies the host-side prompt.txt into the
|
# provision_prompt copies the host-side prompt.txt into the
|
||||||
# guest at /root/.bot-bottle-prompt.txt. The content
|
# guest at /home/node/.bot-bottle-prompt.txt. The content
|
||||||
# must match what the manifest declared so claude-code's
|
# must match what the manifest declared so claude-code's
|
||||||
# --append-system-prompt-file reads the right text.
|
# --append-system-prompt-file reads the right text.
|
||||||
r = self.bottle.exec("cat /root/.bot-bottle-prompt.txt")
|
r = self.bottle.exec("cat /home/node/.bot-bottle-prompt.txt")
|
||||||
self.assertEqual(0, r.returncode, msg=r.stderr)
|
self.assertEqual(0, r.returncode, msg=r.stderr)
|
||||||
self.assertEqual(_AGENT_PROMPT, r.stdout.rstrip("\n"))
|
self.assertEqual(_AGENT_PROMPT, r.stdout.rstrip("\n"))
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ class TestSmolmachinesLaunch(unittest.TestCase):
|
|||||||
# connect fails, which is the property chunk 3 will
|
# connect fails, which is the property chunk 3 will
|
||||||
# preserve once egress is actually running.
|
# preserve once egress is actually running.
|
||||||
r = self.bottle.exec(
|
r = self.bottle.exec(
|
||||||
f"wget -T 3 -t 1 -O - http://{self.plan.bundle_ip}:9099 "
|
f"curl -s --show-error --max-time 3 http://{self.plan.bundle_ip}:9099 "
|
||||||
"2>&1 || true"
|
"2>&1 || true"
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
|
|||||||
Reference in New Issue
Block a user