mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 18:27:45 +02:00
[eric] swarm: scan workspace file bytes for secrets on export, not just payload keys
This commit is contained in:
@@ -79,3 +79,21 @@ def find_denied_keys(value: Any, _path: str = "") -> list[str]:
|
||||
for i, v in enumerate(value):
|
||||
found.extend(find_denied_keys(v, f"{_path}[{i}]"))
|
||||
return found
|
||||
|
||||
|
||||
def _looks_secret(text: str) -> bool:
|
||||
return any(pat.search(text) for pat in _CONTENT_PATTERNS)
|
||||
|
||||
|
||||
def find_secrets_in_files(files: dict[str, bytes]) -> list[str]:
|
||||
"""Paths of any file whose text body holds a secret-shaped literal. Payloads
|
||||
get scrubbed key-and-content, but raw workspace files (an app's source) were
|
||||
only key-scanned, so a key hardcoded in a .js would slip. Binary files are
|
||||
skipped (a null byte means it isn't text someone pasted a token into)."""
|
||||
hits: list[str] = []
|
||||
for path, data in files.items():
|
||||
if b"\x00" in data[:4096]:
|
||||
continue
|
||||
if _looks_secret(data.decode("utf-8", errors="ignore")):
|
||||
hits.append(path)
|
||||
return hits
|
||||
|
||||
@@ -12,7 +12,7 @@ import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
from .redact import find_denied_keys
|
||||
from .redact import find_denied_keys, find_secrets_in_files
|
||||
|
||||
MANIFEST_NAME = "manifest.json"
|
||||
|
||||
@@ -46,6 +46,12 @@ def pack(manifest: dict, payloads: dict[str, dict], files: dict[str, bytes]) ->
|
||||
raise BundleError(
|
||||
f"refusing to export: secret-shaped field(s) in {bid}: {leaked[:3]}"
|
||||
)
|
||||
leaky_files = find_secrets_in_files(files)
|
||||
if leaky_files:
|
||||
raise BundleError(
|
||||
f"refusing to export: a secret-shaped value is in {leaky_files[0]}; "
|
||||
"remove it (use an environment variable) and try again"
|
||||
)
|
||||
entries: dict[str, bytes] = {}
|
||||
for bid, payload in payloads.items():
|
||||
entries[f"entities/{bid}/payload.json"] = json.dumps(payload, indent=2).encode("utf-8")
|
||||
|
||||
@@ -99,6 +99,19 @@ def test_pack_refuses_denied_key():
|
||||
pack({"format_version": 1}, {"bid1": {"api_key": "leak"}}, {})
|
||||
|
||||
|
||||
def test_pack_refuses_secret_in_workspace_file():
|
||||
# A key hardcoded in app source (not .env) must not ride along; pack scans
|
||||
# file bytes, not just payload keys.
|
||||
leak = b"const KEY = 'sk-ant-api03-AAAAAAAAAAAAAAAAAAAAAAAA';\n"
|
||||
with pytest.raises(BundleError):
|
||||
pack({"format_version": 1}, {"bid1": {"name": "ok"}}, {"entities/bid1/files/config.js": leak})
|
||||
|
||||
|
||||
def test_pack_allows_clean_workspace_file():
|
||||
raw = pack({"format_version": 1}, {"bid1": {"name": "ok"}}, {"entities/bid1/files/app.js": b"export default 1"})
|
||||
assert zipfile.is_zipfile(io.BytesIO(raw))
|
||||
|
||||
|
||||
def test_app_export_drops_machine_env(tmp_path, monkeypatch):
|
||||
# The live .env holds the source machine's absolute paths + pinned port; it
|
||||
# must never ride along. .env.example (portable) does.
|
||||
|
||||
Reference in New Issue
Block a user