diff --git a/PR_BODY_REPOSITORY_SANDBOX_EGRESS_10.md b/PR_BODY_REPOSITORY_SANDBOX_EGRESS_10.md new file mode 100644 index 00000000..94b1ea44 --- /dev/null +++ b/PR_BODY_REPOSITORY_SANDBOX_EGRESS_10.md @@ -0,0 +1,36 @@ +/claim #10 + +## Summary + +Adds `repository-sandbox-egress-guard/`, a focused Project Repository & Version Control guard for computation-aware reproducibility sandboxes. + +This is distinct from existing #10 slices for repository ledgers, branch protection, component-owner approval, semantic tags, Git LFS pointer integrity, notebook output diffs, fork provenance, release signatures, external-reference pinning, citation impact, and export bundle manifests. It focuses only on whether a repository reproducibility pipeline can run, merge, or support a DOI/tagged release without unsafe network egress, secret exposure, privileged execution, host mounts, or workspace-escaping outputs. + +## What is included + +- Dependency-free Node evaluator. +- Synthetic repository execution plans. +- Deterministic `RELEASE`, `REVIEW_MERGE`, and `HOLD_MERGE` decisions. +- Checks for pinned container images, command hashes, lockfiles, network egress, secrets, privileged/root/host namespace settings, unsafe mounts, resource caps, and output paths. +- Reviewer artifacts: + - `reports/summary.json` + - `reports/reviewer-packet.md` + - `reports/summary.svg` + - `reports/demo.mp4` + +## Validation + +Locally verified: + +```text +npm.cmd test +npm.cmd run demo +npm.cmd run check +npm.cmd run video +git diff --check +confirmed reports/demo.mp4 was generated as a non-empty local artifact +``` + +The MP4 demo is an FFmpeg-generated synthetic slate only. It is not a screen recording and contains no desktop capture, credentials, private files, or personal information. + +AI-assisted with OpenAI Codex; I reviewed and locally verified the implementation before submitting. diff --git a/repository-sandbox-egress-guard/README.md b/repository-sandbox-egress-guard/README.md new file mode 100644 index 00000000..a8e1c23f --- /dev/null +++ b/repository-sandbox-egress-guard/README.md @@ -0,0 +1,42 @@ +# Repository Sandbox Egress Guard + +This contribution adds a focused guard for computation-aware repository execution in SCIBASE's Project Repository & Version Control layer. It checks whether a repository reproducibility run can be executed, reviewed, merged, or tagged without leaking secrets, reaching unapproved network destinations, or mutating hosted artifacts outside the declared workspace. + +The slice is intentionally narrow. It does not replace broad repository versioning, file diffs, semantic tags, Git LFS integrity, branch protection, component-owner approvals, notebook replay, release signatures, citation badges, or export-bundle manifests. It covers the execution sandbox boundary for reproducibility pipelines. + +## What It Checks + +- Public release, DOI, or protected-branch merge is frozen when sandbox findings are unresolved. +- Reproducibility runs use pinned container images, locked dependency manifests, and hashed commands. +- Network egress is denied or limited to an explicit allowlist. +- Secret environment variables, host secret mounts, and credential-like runtime inputs are blocked. +- Privileged containers, root execution, host PID/IPC sharing, and writable Docker socket mounts are held. +- Resource caps for CPU, memory, runtime, and artifact size are present. +- Output paths stay inside declared `results/` or `artifacts/` locations. +- Generated reviewer packets explain deterministic `RELEASE`, `REVIEW_MERGE`, or `HOLD_MERGE` decisions. + +## Running Locally + +```bash +npm test +npm run demo +npm run check +npm run video +``` + +The demo uses only synthetic repository execution plans in `data/sample_sandbox_run_plans.json`. + +Generated files: + +- `reports/summary.json` +- `reports/reviewer-packet.md` +- `reports/summary.svg` +- `reports/demo.mp4` + +The MP4 is generated from an FFmpeg color source and drawtext slate. It is not a screen recording and contains no desktop capture or private information. + +## Why This Matters + +SCIBASE repository releases are credible only when reproducibility pipelines are repeatable and contained. A notebook or analysis script that can read host secrets, call arbitrary services, or write outside the repository workspace can undermine trust even when the version history looks clean. + +This guard gives reviewers a deterministic packet to hold risky execution plans, request sandbox hardening, and keep protected-branch merges or tagged DOI releases frozen until the computation boundary is safe. diff --git a/repository-sandbox-egress-guard/data/sample_sandbox_run_plans.json b/repository-sandbox-egress-guard/data/sample_sandbox_run_plans.json new file mode 100644 index 00000000..14bf63f6 --- /dev/null +++ b/repository-sandbox-egress-guard/data/sample_sandbox_run_plans.json @@ -0,0 +1,178 @@ +[ + { + "id": "repo-release-001", + "title": "Clean climate notebook reproduction", + "repository": { + "publicReleaseEnabled": true, + "protectedBranchMerge": true, + "doiTag": "v1.2.0" + }, + "execution": { + "pipeline": "notebooks/run_analysis.ipynb", + "commandHashPresent": true, + "lockfilePresent": true, + "runtime": "node18-python311" + }, + "container": { + "image": "ghcr.io/scibase/repro-runner", + "digest": "sha256:7b8a8f2f9d8a73c7aa11188e9b3188bde11e2f3b21fcf5a8b70c1f949820ed13", + "privileged": false, + "user": "1000:1000", + "hostPid": false, + "hostIpc": false, + "mounts": [ + { "source": "workspace", "target": "/workspace", "mode": "ro" }, + { "source": "results", "target": "/workspace/results", "mode": "rw" } + ] + }, + "network": { + "mode": "deny", + "allowedHosts": [] + }, + "secrets": { + "env": [], + "mounts": [] + }, + "resources": { + "cpu": "2", + "memoryMb": 4096, + "timeoutSeconds": 900, + "artifactLimitMb": 250 + }, + "outputs": { + "paths": ["results/figures/summary.png", "results/tables/model_metrics.csv"] + } + }, + { + "id": "repo-hold-002", + "title": "Model benchmark rerun with open egress and host secrets", + "repository": { + "publicReleaseEnabled": true, + "protectedBranchMerge": true, + "doiTag": "preprint-v3" + }, + "execution": { + "pipeline": "notebooks/benchmark.ipynb", + "commandHashPresent": false, + "lockfilePresent": false, + "runtime": "python311" + }, + "container": { + "image": "python:latest", + "digest": "", + "privileged": true, + "user": "root", + "hostPid": true, + "hostIpc": false, + "mounts": [ + { "source": "/var/run/docker.sock", "target": "/var/run/docker.sock", "mode": "rw" }, + { "source": "workspace", "target": "/workspace", "mode": "rw" } + ] + }, + "network": { + "mode": "open", + "allowedHosts": ["*"] + }, + "secrets": { + "env": ["AWS_SECRET_ACCESS_KEY", "SCIBASE_API_TOKEN"], + "mounts": ["/home/researcher/.ssh"] + }, + "resources": { + "cpu": "", + "memoryMb": 0, + "timeoutSeconds": 0, + "artifactLimitMb": 0 + }, + "outputs": { + "paths": ["../published/model.pkl", "/tmp/raw-dump.csv"] + } + }, + { + "id": "repo-review-003", + "title": "Materials simulation with approved package mirror", + "repository": { + "publicReleaseEnabled": false, + "protectedBranchMerge": false, + "doiTag": "" + }, + "execution": { + "pipeline": "code/run_simulation.py", + "commandHashPresent": true, + "lockfilePresent": false, + "runtime": "python311" + }, + "container": { + "image": "ghcr.io/scibase/materials-runner", + "digest": "sha256:2d9f0d60c20afaf0a724f911ba894c30df83f061f1da7c8119f79a26b985bf91", + "privileged": false, + "user": "1000:1000", + "hostPid": false, + "hostIpc": false, + "mounts": [ + { "source": "workspace", "target": "/workspace", "mode": "ro" }, + { "source": "results", "target": "/workspace/results", "mode": "rw" } + ] + }, + "network": { + "mode": "allowlist", + "allowedHosts": ["packages.scibase.example"] + }, + "secrets": { + "env": [], + "mounts": [] + }, + "resources": { + "cpu": "4", + "memoryMb": 8192, + "timeoutSeconds": 1800, + "artifactLimitMb": 500 + }, + "outputs": { + "paths": ["results/simulation/report.json"] + } + }, + { + "id": "repo-hold-004", + "title": "Private dataset release with writable home mount", + "repository": { + "publicReleaseEnabled": false, + "protectedBranchMerge": true, + "doiTag": "" + }, + "execution": { + "pipeline": "code/reproduce.sh", + "commandHashPresent": true, + "lockfilePresent": true, + "runtime": "conda" + }, + "container": { + "image": "ghcr.io/scibase/conda-runner", + "digest": "sha256:a14d0ea51f321418f1b7b4957677f0baaa18b6eab0e41a75032c5f0ee9913e58", + "privileged": false, + "user": "root", + "hostPid": false, + "hostIpc": true, + "mounts": [ + { "source": "/home/researcher", "target": "/host-home", "mode": "rw" }, + { "source": "workspace", "target": "/workspace", "mode": "rw" } + ] + }, + "network": { + "mode": "deny", + "allowedHosts": [] + }, + "secrets": { + "env": ["DATABASE_URL"], + "mounts": [] + }, + "resources": { + "cpu": "2", + "memoryMb": 4096, + "timeoutSeconds": 1200, + "artifactLimitMb": 200 + }, + "outputs": { + "paths": ["results/private-summary.csv"] + } + } +] diff --git a/repository-sandbox-egress-guard/package.json b/repository-sandbox-egress-guard/package.json new file mode 100644 index 00000000..4ec32d3d --- /dev/null +++ b/repository-sandbox-egress-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "repository-sandbox-egress-guard", + "version": "1.0.0", + "description": "Dependency-free guard for computation-aware repository execution sandboxes.", + "main": "src/index.js", + "scripts": { + "test": "node --test", + "demo": "node scripts/demo.js", + "video": "node scripts/render-demo-video.js", + "check": "node --check src/index.js && node --check scripts/demo.js && node --check scripts/render-demo-video.js" + }, + "license": "MIT" +} diff --git a/repository-sandbox-egress-guard/reports/demo.mp4 b/repository-sandbox-egress-guard/reports/demo.mp4 new file mode 100644 index 00000000..d0bfe2af Binary files /dev/null and b/repository-sandbox-egress-guard/reports/demo.mp4 differ diff --git a/repository-sandbox-egress-guard/reports/reviewer-packet.md b/repository-sandbox-egress-guard/reports/reviewer-packet.md new file mode 100644 index 00000000..8908497c --- /dev/null +++ b/repository-sandbox-egress-guard/reports/reviewer-packet.md @@ -0,0 +1,68 @@ +# Repository Sandbox Egress Guard Report + +Generated: 2026-08-16T14:57:25.834Z +Execution plans analyzed: 4 +Decision counts: RELEASE 1, REVIEW_MERGE 1, HOLD_MERGE 2 + +## Findings + +### repo-release-001: Clean climate notebook reproduction + +Decision: RELEASE +Pipeline: notebooks/run_analysis.ipynb + +- No sandbox egress or containment risk detected. + +Recommended actions: +- Allow repository execution, merge, and tagged release with normal provenance. + +### repo-hold-002: Model benchmark rerun with open egress and host secrets + +Decision: HOLD_MERGE +Pipeline: notebooks/benchmark.ipynb + +- MAJOR CONTAINER_IMAGE_NOT_PINNED: The execution container is not pinned by a content digest. +- MAJOR EXECUTION_PROVENANCE_INCOMPLETE: The command hash or dependency lockfile evidence is missing. +- CRITICAL SANDBOX_NETWORK_EGRESS_OPEN: The reproducibility run allows open or wildcard network egress. +- CRITICAL SECRET_ENVIRONMENT_EXPOSED: Secret-like environment variables are available to the repository execution sandbox. +- CRITICAL SECRET_MOUNT_EXPOSED: Host secret paths are mounted into the repository execution sandbox. +- CRITICAL PRIVILEGED_SANDBOX_RUNTIME: The sandbox uses privileged, root, host PID, or host IPC execution settings. +- CRITICAL UNSAFE_HOST_OR_WRITE_MOUNT: The sandbox has unsafe host mounts or writable paths outside declared artifact locations. +- MAJOR RESOURCE_CAPS_MISSING: The execution plan lacks one or more resource caps. +- CRITICAL OUTPUT_PATH_ESCAPES_WORKSPACE: One or more output paths escape declared results or artifacts directories. +- CRITICAL REPOSITORY_RELEASE_NOT_FROZEN_FOR_SANDBOX_RISK: Repository merge or public release remains enabled while sandbox findings are unresolved. + +Recommended actions: +- Freeze protected-branch merge, DOI tagging, and public reproducibility badges. +- Attach sandbox findings to the repository review packet. +- Re-run the exact reproducibility pipeline after remediation. +- Preserve reviewer evidence with the tagged release record. + +### repo-review-003: Materials simulation with approved package mirror + +Decision: REVIEW_MERGE +Pipeline: code/run_simulation.py + +- MAJOR EXECUTION_PROVENANCE_INCOMPLETE: The command hash or dependency lockfile evidence is missing. +- MAJOR SANDBOX_NETWORK_ALLOWLIST_REVIEW: The reproducibility run uses limited network egress and needs reviewer approval before merge. + +Recommended actions: +- Attach sandbox findings to the repository review packet. +- Re-run the exact reproducibility pipeline after remediation. +- Preserve reviewer evidence with the tagged release record. + +### repo-hold-004: Private dataset release with writable home mount + +Decision: HOLD_MERGE +Pipeline: code/reproduce.sh + +- CRITICAL SECRET_ENVIRONMENT_EXPOSED: Secret-like environment variables are available to the repository execution sandbox. +- CRITICAL PRIVILEGED_SANDBOX_RUNTIME: The sandbox uses privileged, root, host PID, or host IPC execution settings. +- CRITICAL UNSAFE_HOST_OR_WRITE_MOUNT: The sandbox has unsafe host mounts or writable paths outside declared artifact locations. +- CRITICAL REPOSITORY_RELEASE_NOT_FROZEN_FOR_SANDBOX_RISK: Repository merge or public release remains enabled while sandbox findings are unresolved. + +Recommended actions: +- Freeze protected-branch merge, DOI tagging, and public reproducibility badges. +- Attach sandbox findings to the repository review packet. +- Re-run the exact reproducibility pipeline after remediation. +- Preserve reviewer evidence with the tagged release record. diff --git a/repository-sandbox-egress-guard/reports/summary.json b/repository-sandbox-egress-guard/reports/summary.json new file mode 100644 index 00000000..41f8977c --- /dev/null +++ b/repository-sandbox-egress-guard/reports/summary.json @@ -0,0 +1,262 @@ +{ + "generatedAt": "2026-08-16T14:57:25.834Z", + "totalPlans": 4, + "counts": { + "RELEASE": 1, + "REVIEW_MERGE": 1, + "HOLD_MERGE": 2 + }, + "results": [ + { + "id": "repo-release-001", + "title": "Clean climate notebook reproduction", + "decision": "RELEASE", + "pipeline": "notebooks/run_analysis.ipynb", + "findings": [], + "recommendedActions": [ + "Allow repository execution, merge, and tagged release with normal provenance." + ] + }, + { + "id": "repo-hold-002", + "title": "Model benchmark rerun with open egress and host secrets", + "decision": "HOLD_MERGE", + "pipeline": "notebooks/benchmark.ipynb", + "findings": [ + { + "code": "CONTAINER_IMAGE_NOT_PINNED", + "severity": "major", + "message": "The execution container is not pinned by a content digest.", + "evidence": { + "image": "python:latest", + "digest": null + }, + "remediation": "Pin the runner image with a sha256 digest before publishing or merging the reproducibility run." + }, + { + "code": "EXECUTION_PROVENANCE_INCOMPLETE", + "severity": "major", + "message": "The command hash or dependency lockfile evidence is missing.", + "evidence": { + "commandHashPresent": false, + "lockfilePresent": false + }, + "remediation": "Attach hashed commands and dependency lock evidence for the exact pipeline revision." + }, + { + "code": "SANDBOX_NETWORK_EGRESS_OPEN", + "severity": "critical", + "message": "The reproducibility run allows open or wildcard network egress.", + "evidence": { + "mode": "open", + "allowedHosts": [ + "*" + ] + }, + "remediation": "Deny egress by default or replace it with a narrow host allowlist and cached inputs." + }, + { + "code": "SECRET_ENVIRONMENT_EXPOSED", + "severity": "critical", + "message": "Secret-like environment variables are available to the repository execution sandbox.", + "evidence": { + "env": [ + "AWS_SECRET_ACCESS_KEY", + "SCIBASE_API_TOKEN" + ] + }, + "remediation": "Remove secrets from reproducibility runs and replace live credentials with synthetic or read-only fixtures." + }, + { + "code": "SECRET_MOUNT_EXPOSED", + "severity": "critical", + "message": "Host secret paths are mounted into the repository execution sandbox.", + "evidence": { + "mounts": [ + "/home/researcher/.ssh" + ] + }, + "remediation": "Remove host secret mounts and use declared, non-sensitive repository fixtures." + }, + { + "code": "PRIVILEGED_SANDBOX_RUNTIME", + "severity": "critical", + "message": "The sandbox uses privileged, root, host PID, or host IPC execution settings.", + "evidence": { + "privileged": true, + "user": "root", + "hostPid": true, + "hostIpc": false + }, + "remediation": "Run the container as a non-root user without privileged mode or host namespace sharing." + }, + { + "code": "UNSAFE_HOST_OR_WRITE_MOUNT", + "severity": "critical", + "message": "The sandbox has unsafe host mounts or writable paths outside declared artifact locations.", + "evidence": { + "mounts": [ + { + "source": "/var/run/docker.sock", + "target": "/var/run/docker.sock", + "mode": "rw" + }, + { + "source": "workspace", + "target": "/workspace", + "mode": "rw" + } + ] + }, + "remediation": "Restrict mounts to read-only workspace inputs and writable results/artifacts directories." + }, + { + "code": "RESOURCE_CAPS_MISSING", + "severity": "major", + "message": "The execution plan lacks one or more resource caps.", + "evidence": { + "missingCaps": [ + "cpu", + "memoryMb", + "timeoutSeconds", + "artifactLimitMb" + ] + }, + "remediation": "Set CPU, memory, timeout, and artifact-size caps before queueing the run." + }, + { + "code": "OUTPUT_PATH_ESCAPES_WORKSPACE", + "severity": "critical", + "message": "One or more output paths escape declared results or artifacts directories.", + "evidence": { + "outputs": [ + "../published/model.pkl", + "/tmp/raw-dump.csv" + ] + }, + "remediation": "Write outputs only under repository-scoped results/ or artifacts/ paths." + }, + { + "code": "REPOSITORY_RELEASE_NOT_FROZEN_FOR_SANDBOX_RISK", + "severity": "critical", + "message": "Repository merge or public release remains enabled while sandbox findings are unresolved.", + "evidence": { + "publicReleaseEnabled": true, + "protectedBranchMerge": true, + "freezeRelease": false + }, + "remediation": "Freeze protected-branch merge, DOI tagging, and public reproducibility badges until sandbox findings are cleared." + } + ], + "recommendedActions": [ + "Freeze protected-branch merge, DOI tagging, and public reproducibility badges.", + "Attach sandbox findings to the repository review packet.", + "Re-run the exact reproducibility pipeline after remediation.", + "Preserve reviewer evidence with the tagged release record." + ] + }, + { + "id": "repo-review-003", + "title": "Materials simulation with approved package mirror", + "decision": "REVIEW_MERGE", + "pipeline": "code/run_simulation.py", + "findings": [ + { + "code": "EXECUTION_PROVENANCE_INCOMPLETE", + "severity": "major", + "message": "The command hash or dependency lockfile evidence is missing.", + "evidence": { + "commandHashPresent": true, + "lockfilePresent": false + }, + "remediation": "Attach hashed commands and dependency lock evidence for the exact pipeline revision." + }, + { + "code": "SANDBOX_NETWORK_ALLOWLIST_REVIEW", + "severity": "major", + "message": "The reproducibility run uses limited network egress and needs reviewer approval before merge.", + "evidence": { + "mode": "allowlist", + "allowedHosts": [ + "packages.scibase.example" + ] + }, + "remediation": "Confirm every allowed host is required for deterministic package retrieval, then mirror or pin where possible." + } + ], + "recommendedActions": [ + "Attach sandbox findings to the repository review packet.", + "Re-run the exact reproducibility pipeline after remediation.", + "Preserve reviewer evidence with the tagged release record." + ] + }, + { + "id": "repo-hold-004", + "title": "Private dataset release with writable home mount", + "decision": "HOLD_MERGE", + "pipeline": "code/reproduce.sh", + "findings": [ + { + "code": "SECRET_ENVIRONMENT_EXPOSED", + "severity": "critical", + "message": "Secret-like environment variables are available to the repository execution sandbox.", + "evidence": { + "env": [ + "DATABASE_URL" + ] + }, + "remediation": "Remove secrets from reproducibility runs and replace live credentials with synthetic or read-only fixtures." + }, + { + "code": "PRIVILEGED_SANDBOX_RUNTIME", + "severity": "critical", + "message": "The sandbox uses privileged, root, host PID, or host IPC execution settings.", + "evidence": { + "privileged": false, + "user": "root", + "hostPid": false, + "hostIpc": true + }, + "remediation": "Run the container as a non-root user without privileged mode or host namespace sharing." + }, + { + "code": "UNSAFE_HOST_OR_WRITE_MOUNT", + "severity": "critical", + "message": "The sandbox has unsafe host mounts or writable paths outside declared artifact locations.", + "evidence": { + "mounts": [ + { + "source": "/home/researcher", + "target": "/host-home", + "mode": "rw" + }, + { + "source": "workspace", + "target": "/workspace", + "mode": "rw" + } + ] + }, + "remediation": "Restrict mounts to read-only workspace inputs and writable results/artifacts directories." + }, + { + "code": "REPOSITORY_RELEASE_NOT_FROZEN_FOR_SANDBOX_RISK", + "severity": "critical", + "message": "Repository merge or public release remains enabled while sandbox findings are unresolved.", + "evidence": { + "publicReleaseEnabled": false, + "protectedBranchMerge": true, + "freezeRelease": false + }, + "remediation": "Freeze protected-branch merge, DOI tagging, and public reproducibility badges until sandbox findings are cleared." + } + ], + "recommendedActions": [ + "Freeze protected-branch merge, DOI tagging, and public reproducibility badges.", + "Attach sandbox findings to the repository review packet.", + "Re-run the exact reproducibility pipeline after remediation.", + "Preserve reviewer evidence with the tagged release record." + ] + } + ] +} diff --git a/repository-sandbox-egress-guard/reports/summary.svg b/repository-sandbox-egress-guard/reports/summary.svg new file mode 100644 index 00000000..a2554151 --- /dev/null +++ b/repository-sandbox-egress-guard/reports/summary.svg @@ -0,0 +1,17 @@ + + + Repository Sandbox Egress Guard + Synthetic reproducibility-run audit before protected merge or DOI release. + Decision distribution + HOLD + + 2 + REVIEW + + 1 + RELEASE + + 1 + Checks egress, secrets, image pins, host mounts, root/privileged mode, resource caps, and output paths. + Synthetic generated artifact only. No desktop capture or private data. + diff --git a/repository-sandbox-egress-guard/scripts/demo.js b/repository-sandbox-egress-guard/scripts/demo.js new file mode 100644 index 00000000..1f3f3924 --- /dev/null +++ b/repository-sandbox-egress-guard/scripts/demo.js @@ -0,0 +1,77 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { analyzeSandboxRunPlans } = require("../src"); + +const root = path.resolve(__dirname, ".."); +const dataPath = path.join(root, "data", "sample_sandbox_run_plans.json"); +const outDir = path.join(root, "reports"); +const plans = JSON.parse(fs.readFileSync(dataPath, "utf8")); +const report = analyzeSandboxRunPlans(plans); + +fs.mkdirSync(outDir, { recursive: true }); +fs.writeFileSync(path.join(outDir, "summary.json"), `${JSON.stringify(report, null, 2)}\n`); + +const markdown = [ + "# Repository Sandbox Egress Guard Report", + "", + `Generated: ${report.generatedAt}`, + `Execution plans analyzed: ${report.totalPlans}`, + `Decision counts: RELEASE ${report.counts.RELEASE}, REVIEW_MERGE ${report.counts.REVIEW_MERGE}, HOLD_MERGE ${report.counts.HOLD_MERGE}`, + "", + "## Findings", + "", + ...report.results.flatMap((item) => [ + `### ${item.id}: ${item.title}`, + "", + `Decision: ${item.decision}`, + `Pipeline: ${item.pipeline || "not declared"}`, + "", + item.findings.length === 0 + ? "- No sandbox egress or containment risk detected." + : item.findings.map((finding) => `- ${finding.severity.toUpperCase()} ${finding.code}: ${finding.message}`).join("\n"), + "", + "Recommended actions:", + ...item.recommendedActions.map((action) => `- ${action}`), + "" + ]) +].join("\n"); + +fs.writeFileSync(path.join(outDir, "reviewer-packet.md"), markdown); + +const total = Math.max(1, report.totalPlans); +const maxBar = 620; +const holdWidth = Math.round((report.counts.HOLD_MERGE / total) * maxBar); +const reviewWidth = Math.round((report.counts.REVIEW_MERGE / total) * maxBar); +const releaseWidth = Math.round((report.counts.RELEASE / total) * maxBar); +const svg = ` + + Repository Sandbox Egress Guard + Synthetic reproducibility-run audit before protected merge or DOI release. + Decision distribution + HOLD + + ${report.counts.HOLD_MERGE} + REVIEW + + ${report.counts.REVIEW_MERGE} + RELEASE + + ${report.counts.RELEASE} + Checks egress, secrets, image pins, host mounts, root/privileged mode, resource caps, and output paths. + Synthetic generated artifact only. No desktop capture or private data. + +`; + +fs.writeFileSync(path.join(outDir, "summary.svg"), svg); + +console.log(JSON.stringify({ + plans: report.totalPlans, + counts: report.counts, + outputs: [ + path.join(outDir, "summary.json"), + path.join(outDir, "reviewer-packet.md"), + path.join(outDir, "summary.svg") + ] +}, null, 2)); diff --git a/repository-sandbox-egress-guard/scripts/render-demo-video.js b/repository-sandbox-egress-guard/scripts/render-demo-video.js new file mode 100644 index 00000000..993e8621 --- /dev/null +++ b/repository-sandbox-egress-guard/scripts/render-demo-video.js @@ -0,0 +1,60 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const root = path.resolve(__dirname, ".."); +const ffmpeg = path.resolve( + root, + "..", + "..", + "tool_downloads", + "video_tools", + "node_modules", + "ffmpeg-static", + "ffmpeg.exe" +); +const outDir = path.join(root, "reports"); +const out = path.join(outDir, "demo.mp4"); +const font = "C\\:/Windows/Fonts/arial.ttf"; + +fs.mkdirSync(outDir, { recursive: true }); + +const draw = [ + `drawtext=fontfile=${font}:text='Repository Sandbox Egress Guard':x=64:y=56:fontsize=42:fontcolor=black`, + `drawtext=fontfile=${font}:text='Reproducibility run containment before merge or DOI release':x=64:y=122:fontsize=25:fontcolor=0x34405a`, + `drawtext=fontfile=${font}:text='Synthetic execution plans analyzed 4':x=64:y=214:fontsize=34:fontcolor=black`, + `drawtext=fontfile=${font}:text='HOLD 2 REVIEW 1 RELEASE 1':x=64:y=274:fontsize=40:fontcolor=0x225c9c`, + `drawtext=fontfile=${font}:text='Detects open egress secrets privileged mode unsafe mounts':x=64:y=366:fontsize=26:fontcolor=0x34405a`, + `drawtext=fontfile=${font}:text='Freezes protected merge and public release until sandbox risks clear':x=64:y=428:fontsize=26:fontcolor=0x34405a`, + `drawtext=fontfile=${font}:text='Synthetic generated slate only no desktop capture or private data':x=64:y=492:fontsize=26:fontcolor=0x34405a` +].join(","); + +if (!fs.existsSync(ffmpeg)) { + throw new Error(`ffmpeg binary not found at ${ffmpeg}`); +} + +const result = spawnSync(ffmpeg, [ + "-y", + "-f", + "lavfi", + "-i", + "color=c=0xf7fbff:s=1280x720:d=8:r=30", + "-vf", + draw, + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + out +], { encoding: "utf8" }); + +if (result.status !== 0) { + process.stderr.write(result.stderr || result.stdout); + process.exit(result.status || 1); +} + +console.log(JSON.stringify({ out, bytes: fs.statSync(out).size }, null, 2)); diff --git a/repository-sandbox-egress-guard/src/index.js b/repository-sandbox-egress-guard/src/index.js new file mode 100644 index 00000000..c725d69a --- /dev/null +++ b/repository-sandbox-egress-guard/src/index.js @@ -0,0 +1,273 @@ +"use strict"; + +function list(value) { + return Array.isArray(value) ? value : []; +} + +function normalizeText(value) { + return String(value || "").trim().toLowerCase(); +} + +function finding(code, severity, message, evidence, remediation) { + return { code, severity, message, evidence, remediation }; +} + +function normalizePlan(plan) { + return { + id: plan.id, + title: plan.title || "Untitled repository sandbox plan", + repository: plan.repository || {}, + execution: plan.execution || {}, + container: plan.container || {}, + network: plan.network || {}, + secrets: plan.secrets || {}, + resources: plan.resources || {}, + outputs: plan.outputs || {} + }; +} + +function hasPinnedImage(plan) { + return /^sha256:[a-f0-9]{64}$/i.test(String(plan.container.digest || "")); +} + +function hasLockedExecution(plan) { + return plan.execution.commandHashPresent === true && plan.execution.lockfilePresent === true; +} + +function networkFinding(plan) { + const mode = normalizeText(plan.network.mode); + const allowedHosts = list(plan.network.allowedHosts).map(normalizeText).filter(Boolean); + + if (mode === "deny") return null; + if (mode === "allowlist" && allowedHosts.length > 0 && !allowedHosts.includes("*")) { + return finding( + "SANDBOX_NETWORK_ALLOWLIST_REVIEW", + "major", + "The reproducibility run uses limited network egress and needs reviewer approval before merge.", + { mode, allowedHosts }, + "Confirm every allowed host is required for deterministic package retrieval, then mirror or pin where possible." + ); + } + + return finding( + "SANDBOX_NETWORK_EGRESS_OPEN", + "critical", + "The reproducibility run allows open or wildcard network egress.", + { mode: mode || "unspecified", allowedHosts }, + "Deny egress by default or replace it with a narrow host allowlist and cached inputs." + ); +} + +function secretFindings(plan) { + const env = list(plan.secrets.env).filter(Boolean); + const mounts = list(plan.secrets.mounts).filter(Boolean); + const findings = []; + + if (env.length > 0) { + findings.push(finding( + "SECRET_ENVIRONMENT_EXPOSED", + "critical", + "Secret-like environment variables are available to the repository execution sandbox.", + { env }, + "Remove secrets from reproducibility runs and replace live credentials with synthetic or read-only fixtures." + )); + } + + if (mounts.length > 0) { + findings.push(finding( + "SECRET_MOUNT_EXPOSED", + "critical", + "Host secret paths are mounted into the repository execution sandbox.", + { mounts }, + "Remove host secret mounts and use declared, non-sensitive repository fixtures." + )); + } + + return findings; +} + +function unsafeMounts(plan) { + return list(plan.container.mounts).filter((mount) => { + const source = normalizeText(mount.source); + const target = normalizeText(mount.target); + return source.includes("docker.sock") || + target.includes("docker.sock") || + source.startsWith("/") && !source.startsWith("/workspace") || + normalizeText(mount.mode) === "rw" && target !== "/workspace/results" && target !== "/workspace/artifacts"; + }); +} + +function missingResourceCaps(plan) { + const resources = plan.resources; + return ["cpu", "memoryMb", "timeoutSeconds", "artifactLimitMb"].filter((key) => { + const value = resources[key]; + if (typeof value === "string") return value.trim() === ""; + return !Number.isFinite(Number(value)) || Number(value) <= 0; + }); +} + +function unsafeOutputPaths(plan) { + return list(plan.outputs.paths).filter((rawPath) => { + const value = String(rawPath || "").replace(/\\/g, "/"); + return value.startsWith("/") || + value.includes("../") || + !(value.startsWith("results/") || value.startsWith("artifacts/")); + }); +} + +function analyzePlan(input) { + const plan = normalizePlan(input); + const findings = []; + const network = networkFinding(plan); + const mounts = unsafeMounts(plan); + const missingCaps = missingResourceCaps(plan); + const outputs = unsafeOutputPaths(plan); + + if (!hasPinnedImage(plan)) { + findings.push(finding( + "CONTAINER_IMAGE_NOT_PINNED", + "major", + "The execution container is not pinned by a content digest.", + { image: plan.container.image || null, digest: plan.container.digest || null }, + "Pin the runner image with a sha256 digest before publishing or merging the reproducibility run." + )); + } + + if (!hasLockedExecution(plan)) { + findings.push(finding( + "EXECUTION_PROVENANCE_INCOMPLETE", + "major", + "The command hash or dependency lockfile evidence is missing.", + { + commandHashPresent: plan.execution.commandHashPresent || false, + lockfilePresent: plan.execution.lockfilePresent || false + }, + "Attach hashed commands and dependency lock evidence for the exact pipeline revision." + )); + } + + if (network) findings.push(network); + findings.push(...secretFindings(plan)); + + if (plan.container.privileged === true || normalizeText(plan.container.user) === "root" || plan.container.hostPid === true || plan.container.hostIpc === true) { + findings.push(finding( + "PRIVILEGED_SANDBOX_RUNTIME", + "critical", + "The sandbox uses privileged, root, host PID, or host IPC execution settings.", + { + privileged: plan.container.privileged || false, + user: plan.container.user || null, + hostPid: plan.container.hostPid || false, + hostIpc: plan.container.hostIpc || false + }, + "Run the container as a non-root user without privileged mode or host namespace sharing." + )); + } + + if (mounts.length > 0) { + findings.push(finding( + "UNSAFE_HOST_OR_WRITE_MOUNT", + "critical", + "The sandbox has unsafe host mounts or writable paths outside declared artifact locations.", + { mounts }, + "Restrict mounts to read-only workspace inputs and writable results/artifacts directories." + )); + } + + if (missingCaps.length > 0) { + findings.push(finding( + "RESOURCE_CAPS_MISSING", + "major", + "The execution plan lacks one or more resource caps.", + { missingCaps }, + "Set CPU, memory, timeout, and artifact-size caps before queueing the run." + )); + } + + if (outputs.length > 0) { + findings.push(finding( + "OUTPUT_PATH_ESCAPES_WORKSPACE", + "critical", + "One or more output paths escape declared results or artifacts directories.", + { outputs }, + "Write outputs only under repository-scoped results/ or artifacts/ paths." + )); + } + + if ( + findings.length > 0 && + (plan.repository.publicReleaseEnabled === true || plan.repository.protectedBranchMerge === true) && + plan.repository.freezeRelease !== true + ) { + findings.push(finding( + "REPOSITORY_RELEASE_NOT_FROZEN_FOR_SANDBOX_RISK", + "critical", + "Repository merge or public release remains enabled while sandbox findings are unresolved.", + { + publicReleaseEnabled: plan.repository.publicReleaseEnabled || false, + protectedBranchMerge: plan.repository.protectedBranchMerge || false, + freezeRelease: plan.repository.freezeRelease || false + }, + "Freeze protected-branch merge, DOI tagging, and public reproducibility badges until sandbox findings are cleared." + )); + } + + const decision = decisionFor(findings); + return { + id: plan.id, + title: plan.title, + decision, + pipeline: plan.execution.pipeline || null, + findings, + recommendedActions: buildRecommendedActions(decision, findings) + }; +} + +function decisionFor(findings) { + if (findings.some((item) => item.severity === "critical")) return "HOLD_MERGE"; + if (findings.some((item) => item.severity === "major")) return "REVIEW_MERGE"; + return "RELEASE"; +} + +function buildRecommendedActions(decision, findings) { + if (decision === "RELEASE") { + return ["Allow repository execution, merge, and tagged release with normal provenance."]; + } + + const actions = [ + "Attach sandbox findings to the repository review packet.", + "Re-run the exact reproducibility pipeline after remediation.", + "Preserve reviewer evidence with the tagged release record." + ]; + + if (findings.some((item) => item.code === "REPOSITORY_RELEASE_NOT_FROZEN_FOR_SANDBOX_RISK")) { + actions.unshift("Freeze protected-branch merge, DOI tagging, and public reproducibility badges."); + } + + return actions; +} + +function analyzeSandboxRunPlans(plans) { + const results = list(plans).map(analyzePlan); + const counts = results.reduce((acc, item) => { + acc[item.decision] = (acc[item.decision] || 0) + 1; + return acc; + }, { RELEASE: 0, REVIEW_MERGE: 0, HOLD_MERGE: 0 }); + + return { + generatedAt: new Date().toISOString(), + totalPlans: results.length, + counts, + results + }; +} + +module.exports = { + analyzePlan, + analyzeSandboxRunPlans, + hasPinnedImage, + hasLockedExecution, + unsafeMounts, + unsafeOutputPaths, + missingResourceCaps +}; diff --git a/repository-sandbox-egress-guard/test/repository-sandbox-egress-guard.test.js b/repository-sandbox-egress-guard/test/repository-sandbox-egress-guard.test.js new file mode 100644 index 00000000..df26c400 --- /dev/null +++ b/repository-sandbox-egress-guard/test/repository-sandbox-egress-guard.test.js @@ -0,0 +1,65 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); +const { + analyzePlan, + analyzeSandboxRunPlans, + hasPinnedImage, + hasLockedExecution, + unsafeMounts, + unsafeOutputPaths, + missingResourceCaps +} = require("../src"); +const samples = require("../data/sample_sandbox_run_plans.json"); + +test("accepts a pinned, locked, egress-denied repository run", () => { + const plan = samples.find((item) => item.id === "repo-release-001"); + const result = analyzePlan(plan); + assert.equal(hasPinnedImage(plan), true); + assert.equal(hasLockedExecution(plan), true); + assert.equal(result.decision, "RELEASE"); + assert.equal(result.findings.length, 0); +}); + +test("holds open egress and host-secret exposure", () => { + const result = analyzePlan(samples.find((item) => item.id === "repo-hold-002")); + assert.equal(result.decision, "HOLD_MERGE"); + assert.ok(result.findings.some((item) => item.code === "SANDBOX_NETWORK_EGRESS_OPEN")); + assert.ok(result.findings.some((item) => item.code === "SECRET_ENVIRONMENT_EXPOSED")); + assert.ok(result.findings.some((item) => item.code === "SECRET_MOUNT_EXPOSED")); +}); + +test("detects unsafe mounts and workspace-escaping outputs", () => { + const plan = samples.find((item) => item.id === "repo-hold-002"); + assert.ok(unsafeMounts(plan).length >= 2); + assert.deepEqual(unsafeOutputPaths(plan), ["../published/model.pkl", "/tmp/raw-dump.csv"]); +}); + +test("routes allowlisted egress and missing lockfiles for review", () => { + const result = analyzePlan(samples.find((item) => item.id === "repo-review-003")); + assert.equal(result.decision, "REVIEW_MERGE"); + assert.ok(result.findings.some((item) => item.code === "SANDBOX_NETWORK_ALLOWLIST_REVIEW")); + assert.ok(result.findings.some((item) => item.code === "EXECUTION_PROVENANCE_INCOMPLETE")); +}); + +test("detects privileged runtime settings and missing resource caps", () => { + const plan = samples.find((item) => item.id === "repo-hold-002"); + const result = analyzePlan(plan); + assert.deepEqual(missingResourceCaps(plan), ["cpu", "memoryMb", "timeoutSeconds", "artifactLimitMb"]); + assert.ok(result.findings.some((item) => item.code === "PRIVILEGED_SANDBOX_RUNTIME")); + assert.ok(result.findings.some((item) => item.code === "RESOURCE_CAPS_MISSING")); +}); + +test("holds root or host namespace execution even when egress is denied", () => { + const result = analyzePlan(samples.find((item) => item.id === "repo-hold-004")); + assert.equal(result.decision, "HOLD_MERGE"); + assert.ok(result.findings.some((item) => item.code === "PRIVILEGED_SANDBOX_RUNTIME")); + assert.ok(result.findings.some((item) => item.code === "UNSAFE_HOST_OR_WRITE_MOUNT")); +}); + +test("aggregates sandbox run decisions", () => { + const report = analyzeSandboxRunPlans(samples); + assert.equal(report.totalPlans, 4); + assert.deepEqual(report.counts, { RELEASE: 1, REVIEW_MERGE: 1, HOLD_MERGE: 2 }); +});