Skip to content

Commit 856d07d

Browse files
committed
Harden sandbox audit and release verification
1 parent 72c72e2 commit 856d07d

83 files changed

Lines changed: 6315 additions & 336 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ jobs:
269269
# Verify release workflow exists and contains required supply-chain steps
270270
test -f .github/workflows/release.yml || { echo "FAIL: release.yml missing"; exit 1; }
271271
272-
for keyword in "sbom-action" "attest-build-provenance" "cosign" "cyclonedx" "SHA256SUMS"; do
272+
for keyword in "sbom-action" "attest-build-provenance" "cosign" "cyclonedx" "SHA256SUMS" "generate_custom_python_vex.py" ".vex.json"; do
273273
grep -q "${keyword}" .github/workflows/release.yml || \
274274
{ echo "FAIL: release.yml missing '${keyword}'"; exit 1; }
275275
echo "OK: release.yml contains '${keyword}'"
@@ -363,6 +363,42 @@ jobs:
363363
"
364364
echo "=== Upstreams manifest check: PASSED ==="
365365
366+
sandbox-vex-smoke:
367+
name: Sandbox OpenVEX Smoke
368+
runs-on: ubuntu-latest
369+
permissions:
370+
contents: read
371+
steps:
372+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
373+
374+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
375+
with:
376+
python-version: "3.12"
377+
378+
- name: Build sandbox images required for OpenVEX generation
379+
run: docker compose -f deploy/sandbox/compose.yaml --profile diffusion build ui agent search-mediator diffusion
380+
381+
- name: Generate sandbox OpenVEX document
382+
run: |
383+
python scripts/security/generate_custom_python_vex.py \
384+
--image secai-sandbox-ui:latest \
385+
--image secai-sandbox-agent:latest \
386+
--image secai-sandbox-search-mediator:latest \
387+
--image secai-sandbox-diffusion:latest \
388+
--include-unicode-locale-glibc \
389+
--output custom-python.vex.json
390+
391+
- name: Validate generated OpenVEX document
392+
run: |
393+
test -f custom-python.vex.json
394+
jq -e '."@context" == "https://openvex.dev/ns/v0.2.0" and (.statements | type == "array") and (.statements | length > 0)' custom-python.vex.json >/dev/null
395+
396+
- name: Upload OpenVEX artifact
397+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
398+
with:
399+
name: sandbox-vex-smoke
400+
path: custom-python.vex.json
401+
366402
security-regression:
367403
name: Security Regression Tests
368404
runs-on: ubuntu-latest

.github/workflows/release.yml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
"Dependency Vulnerability Audit"
4545
"Test Count Drift Check"
4646
"Documentation Validation"
47+
"Sandbox OpenVEX Smoke"
4748
)
4849
4950
PASS=0
@@ -148,6 +149,34 @@ jobs:
148149
name: python-sboms
149150
path: dist/
150151

152+
build-sandbox-vex:
153+
name: Build Sandbox OpenVEX Evidence
154+
needs: [preflight]
155+
runs-on: ubuntu-latest
156+
steps:
157+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
158+
159+
- name: Build sandbox images required for VEX generation
160+
run: docker compose -f deploy/sandbox/compose.yaml --profile diffusion build ui agent search-mediator diffusion
161+
162+
- name: Generate sandbox OpenVEX document
163+
run: |
164+
mkdir -p dist
165+
python3 scripts/security/generate_custom_python_vex.py \
166+
--image secai-sandbox-ui:latest \
167+
--image secai-sandbox-agent:latest \
168+
--image secai-sandbox-search-mediator:latest \
169+
--image secai-sandbox-diffusion:latest \
170+
--include-unicode-locale-glibc \
171+
--document-id "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/custom-python.vex.json" \
172+
--output dist/custom-python.vex.json
173+
174+
- name: Upload OpenVEX artifact
175+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
176+
with:
177+
name: sandbox-vex
178+
path: dist/custom-python.vex.json
179+
151180
build-iso:
152181
name: Build ISO
153182
needs: [preflight]
@@ -306,7 +335,7 @@ jobs:
306335
provenance:
307336
name: SLSA Provenance & Attestation
308337
runs-on: ubuntu-latest
309-
needs: [build-go, build-python, build-iso, build-usb-image]
338+
needs: [build-go, build-python, build-sandbox-vex, build-iso, build-usb-image]
310339
steps:
311340
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
312341

@@ -364,6 +393,15 @@ jobs:
364393
'. + [$name]')
365394
done
366395
396+
# Collect OpenVEX filenames
397+
VEX_JSON="[]"
398+
for vex in *.vex.json; do
399+
[ -f "$vex" ] || continue
400+
VEX_JSON=$(echo "$VEX_JSON" | jq \
401+
--arg name "$vex" \
402+
'. + [$name]')
403+
done
404+
367405
# Read image digest
368406
IMAGE_DIGEST="unknown"
369407
if [ -f IMAGE_DIGEST ]; then
@@ -383,6 +421,7 @@ jobs:
383421
--arg image_ref_pinned "$IMAGE_REF_PINNED" \
384422
--argjson binaries "$BINARIES_JSON" \
385423
--argjson sboms "$SBOMS_JSON" \
424+
--argjson vex "$VEX_JSON" \
386425
--arg provenance_type "https://slsa.dev/provenance/v1" \
387426
--arg checksum_file "SHA256SUMS" \
388427
--arg signature_file "SHA256SUMS.sig" \
@@ -400,6 +439,7 @@ jobs:
400439
},
401440
binaries: $binaries,
402441
sboms: $sboms,
442+
vex: $vex,
403443
provenance: {
404444
type: $provenance_type,
405445
attested: true
@@ -480,6 +520,7 @@ jobs:
480520
files: |
481521
dist/*-linux-*
482522
dist/*-sbom.cdx.json
523+
dist/*.vex.json
483524
dist/SHA256SUMS
484525
dist/SHA256SUMS.sig
485526
dist/IMAGE_DIGEST

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ dist/
3434
.DS_Store
3535
Thumbs.db
3636
/.bluebuild-scripts_*
37+
38+
# Sandbox bundle
39+
/deploy/sandbox/.env
40+
/deploy/sandbox/runtime/

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ help: ## Show available targets
3131
verify-release: ## Verify a release image (IMAGE=ghcr.io/...)
3232
@files/scripts/verify-release.sh "$(IMAGE)"
3333

34+
.PHONY: sandbox-vex
35+
sandbox-vex: ## Generate local sandbox OpenVEX document (requires built sandbox images)
36+
python scripts/security/generate_custom_python_vex.py \
37+
--image secai-sandbox-ui:latest \
38+
--image secai-sandbox-agent:latest \
39+
--image secai-sandbox-search-mediator:latest \
40+
--image secai-sandbox-diffusion:latest \
41+
--include-unicode-locale-glibc \
42+
--output custom-python.vex.json
43+
3444
.PHONY: test
3545
test: test-go test-python ## Run all tests (Go + Python)
3646

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,14 @@ The setup wizard guides you through privacy profile selection, system verificati
6969
| **Bootstrap** (Recommended) | ~30 min | Real PC or VM | Install Fedora Silverblue, run script, reboot |
7070
| **Portable USB** | ~10 min | Run directly from a USB stick | Flash the release `*-usb.raw.xz` artifact to removable media |
7171
| **Build VM locally** | ~45 min | VirtualBox / VMware / KVM | `scripts/vm/build-qcow2.sh` builds a QCOW2 from the OCI image |
72+
| **Sandbox Stack** | ~10 min | Evaluate on an existing workstation | Compose-based control-plane bundle with explicit lower-assurance limits |
7273
| **Development** | ~10 min | Service development only | No OS features; see [dev guide](docs/install/dev.md) |
7374

74-
See [docs/install/quickstart.md](docs/install/quickstart.md) for full step-by-step instructions, VM build details, and verification commands.
75+
See [docs/install/quickstart.md](docs/install/quickstart.md) for full step-by-step instructions, including the [sandbox path](docs/install/sandbox.md), VM build details, and verification commands.
7576

7677
For production deployments with digest pinning: `sudo bash secai-bootstrap.sh --digest sha256:RELEASE_DIGEST`
7778

78-
See [bare metal](docs/install/bare-metal.md) | [virtual machine](docs/install/vm.md) | [development](docs/install/dev.md) | [recovery](docs/install/recovery-bootstrap.md)
79+
See [bare metal](docs/install/bare-metal.md) | [virtual machine](docs/install/vm.md) | [sandbox](docs/install/sandbox.md) | [development](docs/install/dev.md) | [recovery](docs/install/recovery-bootstrap.md)
7980

8081
### Get Your First Model
8182

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py
2+
index 9ead2990e818e5..52cd91d3da206d 100644
3+
--- a/Lib/webbrowser.py
4+
+++ b/Lib/webbrowser.py
5+
@@ -274,7 +274,6 @@ class GenericBrowser(BaseBrowser):
6+
7+
def open(self, url, new=0, autoraise=True):
8+
sys.audit("webbrowser.open", url)
9+
- self._check_url(url)
10+
if new == 0:
11+
action = self.remote_action
12+
elif new == 1:
13+
@@ -288,8 +287,10 @@ class GenericBrowser(BaseBrowser):
14+
raise Error("Bad 'new' parameter to open(); "
15+
f"expected 0, 1, or 2, got {new}")
16+
17+
- args = [arg.replace("%s", url).replace("%action", action)
18+
+ self._check_url(url.replace("%action", action))
19+
+
20+
+ args = [arg.replace("%action", action).replace("%s", url)
21+
for arg in self.remote_args]
22+
args = [arg for arg in args if arg]
23+
success = self._invoke(args, True, autoraise, url)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c
2+
index 7b8cbf3ed96184..84d7a41ebc24c5 100644
3+
--- a/Modules/_bz2module.c
4+
+++ b/Modules/_bz2module.c
5+
@@ -569,6 +569,7 @@ decompress(BZ2Decompressor *d, char *data, size_t len, Py_ssize_t max_length)
6+
return result;
7+
8+
error:
9+
+ bzs->next_in = NULL;
10+
Py_XDECREF(result);
11+
return NULL;
12+
}
13+
diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c
14+
index 3c391675d7b93e..00ee68dcea2d0d 100644
15+
--- a/Modules/_lzmamodule.c
16+
+++ b/Modules/_lzmamodule.c
17+
@@ -1100,6 +1100,7 @@ decompress(Decompressor *d, uint8_t *data, size_t len, Py_ssize_t max_length)
18+
return result;
19+
20+
error:
21+
+ lzs->next_in = NULL;
22+
Py_XDECREF(result);
23+
return NULL;
24+
}
25+
diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c
26+
index f67434ecdc908c..9c5820fbe97a6b 100644
27+
--- a/Modules/zlibmodule.c
28+
+++ b/Modules/zlibmodule.c
29+
@@ -1669,6 +1669,7 @@ decompress(ZlibDecompressor *self, uint8_t *data,
30+
return result;
31+
32+
error:
33+
+ self->zst.next_in = NULL;
34+
Py_XDECREF(result);
35+
return NULL;
36+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
diff --git a/Modules/overlapped.c b/Modules/overlapped.c
2+
index 822e1ce4bdc28d..51aee5afd35b6d 100644
3+
--- a/Modules/overlapped.c
4+
+++ b/Modules/overlapped.c
5+
@@ -1910,6 +1910,11 @@ _overlapped_Overlapped_WSARecvFromInto_impl(OverlappedObject *self,
6+
}
7+
#endif
8+
9+
+ if (bufobj->len < (Py_ssize_t)size) {
10+
+ PyErr_SetString(PyExc_ValueError, "nbytes is greater than the length of the buffer");
11+
+ return NULL;
12+
+ }
13+
+
14+
wsabuf.buf = bufobj->buf;
15+
wsabuf.len = size;
16+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
From 05ed7ce7ae9e17c23a04085b2539fe6d6d3cef69 Mon Sep 17 00:00:00 2001
2+
From: Seth Larson <seth@python.org>
3+
Date: Fri, 10 Apr 2026 10:21:42 -0500
4+
Subject: [PATCH] gh-146211: Reject CR/LF in HTTP tunnel request headers
5+
6+
---
7+
Lib/http/client.py | 11 ++++++++++-
8+
1 file changed, 10 insertions(+), 1 deletion(-)
9+
10+
diff --git a/Lib/http/client.py b/Lib/http/client.py
11+
index 73c3256734a64f..1e1a535c4c4eb1 100644
12+
--- a/Lib/http/client.py
13+
+++ b/Lib/http/client.py
14+
@@ -976,13 +976,22 @@ def _wrap_ipv6(self, ip):
15+
return ip
16+
17+
def _tunnel(self):
18+
+ if _contains_disallowed_url_pchar_re.search(self._tunnel_host):
19+
+ raise ValueError('Tunnel host can\'t contain control characters %r'
20+
+ % (self._tunnel_host,))
21+
connect = b"CONNECT %s:%d %s\r\n" % (
22+
self._wrap_ipv6(self._tunnel_host.encode("idna")),
23+
self._tunnel_port,
24+
self._http_vsn_str.encode("ascii"))
25+
headers = [connect]
26+
for header, value in self._tunnel_headers.items():
27+
- headers.append(f"{header}: {value}\r\n".encode("latin-1"))
28+
+ header_bytes = header.encode("latin-1")
29+
+ value_bytes = value.encode("latin-1")
30+
+ if not _is_legal_header_name(header_bytes):
31+
+ raise ValueError('Invalid header name %r' % (header_bytes,))
32+
+ if _is_illegal_header_value(value_bytes):
33+
+ raise ValueError('Invalid header value %r' % (value_bytes,))
34+
+ headers.append(b"%s: %s\r\n" % (header_bytes, value_bytes))
35+
headers.append(b"\r\n")
36+
# Making a single send() call instead of one per line encourages
37+
# the host OS to use a more optimal packet size instead of
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
From 6262704b134db2a4ba12e85ecfbd968534f28b45 Mon Sep 17 00:00:00 2001
2+
From: Seth Michael Larson <seth@python.org>
3+
Date: Tue, 20 Jan 2026 14:45:42 -0600
4+
Subject: [PATCH] gh-143921: Reject control characters in IMAP commands
5+
6+
---
7+
Lib/imaplib.py | 4 +++-
8+
1 file changed, 3 insertions(+), 1 deletion(-)
9+
10+
diff --git a/Lib/imaplib.py b/Lib/imaplib.py
11+
index 22a0afcd981519..cb3edceae0d9f1 100644
12+
--- a/Lib/imaplib.py
13+
+++ b/Lib/imaplib.py
14+
@@ -129,7 +129,7 @@
15+
# We compile these in _mode_xxx.
16+
_Literal = br'.*{(?P<size>\d+)}$'
17+
_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'
18+
-
19+
+_control_chars = re.compile(b'[\x00-\x1F\x7F]')
20+
21+
22+
class IMAP4:
23+
@@ -1105,6 +1105,8 @@ def _command(self, name, *args):
24+
if arg is None: continue
25+
if isinstance(arg, str):
26+
arg = bytes(arg, self._encoding)
27+
+ if _control_chars.search(arg):
28+
+ raise ValueError("Control characters not allowed in commands")
29+
data = data + b' ' + arg
30+
31+
literal = self.literal

0 commit comments

Comments
 (0)