Security hardening and release readiness
ober
a6231fc11475b67caa8d528d7a70f4ea058c4713
new file mode 100644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + JERBOA_VERSION: v0.2.3 + JERBUILD: ${{ github.workspace }}/.jerboa/bin/jerbuild + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install system tools + run: | + set -eu + sudo apt-get update + sudo apt-get install -y --no-install-recommends build-essential git curl ca-certificates + + - name: Install Jerboa toolchain + run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin + + - name: Release evidence + run: make release-evidence new file mode 100644 --- /dev/null +++ b/.github/workflows/security-baseline.yml @@ -0,0 +1,35 @@ +name: Security Baseline + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + baseline: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Required release files + run: | + set -eu + test -f LICENSE + test -f SECURITY.md + test -f .gitignore + find . -maxdepth 1 -iname "README*" -type f | grep -q . + + - name: High-confidence secret scan + run: | + set -eu + pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|sk-(ant-api03|proj|svcacct)-[A-Za-z0-9_-]{30,}|AKIA[0-9A-Z]{16})" + matches="$(git grep -n -I -E "$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)" + if [ -n "$matches" ]; then + echo "$matches" + echo "High-confidence secret pattern found." + exit 1 + fi --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,12 @@ jgl-*-main.c # Cross-build isolated object dir /.build/ +# Release evidence +/dist/ +.jerboa/* +!.jerboa/ +!.jerboa/security.json + # Default export output /issues/ new file mode 100644 --- /dev/null +++ b/.jerboa/security.json @@ -0,0 +1,49 @@ +{ + "version": 1, + "repo": "jerboa-gitlab", + "extends": ["jerboa:cli", "jerboa:network-client", "jerboa:credentialed-client"], + "paths": { + "production": ["*.ss", "*.sls", "lib/**/*.ss", "lib/**/*.sls", "Makefile"], + "tests": ["test/**", "tests/**", "**/*-test.ss"], + "generated": ["build/**", "dist/**", "target/**", "*.so", "*.dylib", "*.wpo", "*-main.c", "*_boot.h", "*_program.h"], + "vendor": ["vendor/**", "third_party/**"], + "docs": ["README.md", "docs/**", "*.md"] + }, + "policy": { + "failOn": ["critical", "high"], + "imports": { "directChezscheme": "deny" }, + "ffi": { "allowed": false }, + "process": { "shellInterpolation": "deny" }, + "network": { "requireTimeouts": true, "rawServiceOutputEvidence": "deny" }, + "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false }, + "secrets": { "forbidLogging": true, "leastPrivilegeOAuthScopes": true }, + "credentials": { + "plaintextCache": "deny", + "rawCredentialEvidence": "deny", + "preferReadApiScope": true + }, + "targetEvidence": { + "gitlabProof": "fail-closed", + "requiredMarkers": [ + "gitlab_public_project_smoke_status=target-evidence-recorded", + "gitlab_private_project_smoke_status=target-evidence-recorded", + "least_privilege_token_scope_status=confirmed", + "token_source_hygiene_status=confirmed", + "api_error_redaction_status=target-evidence-recorded", + "export_attachment_redaction_status=target-evidence-recorded", + "credential_redaction_status=target-evidence-recorded", + "target_platform_smoke_status=target-evidence-recorded", + "production_credential_review_status=current-run-recorded", + "raw_api_output_status=not-preserved", + "sensitive_artifact_policy=no-gitlab-tokens-private-project-paths-issue-exports-attachments-or-raw-api-responses-preserved" + ] + } + }, + "suppressions": [ + { + "rule": "missing-taint-check-at-sink", + "path": "build-jgl-cross.ss", + "reason": "The cross-release build script invokes a configured compiler and only after shell-quoting path inputs." + } + ] +} new file mode 100644 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. --- a/Makefile +++ b/Makefile @@ -1,17 +1,25 @@ # jerbuild bundles Chez Scheme + the jerboa stdlib + the jerboa-native Rust -# crate, so building jerboa-gitlab needs only jerbuild + a C compiler + cargo — -# no jerboa source checkout. jerboa-native provides Rustls HTTPS (tls,crypto). -JERBUILD ?= jerbuild -JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null) -ifeq ($(JH),) -$(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild) -endif - -NATIVE_DIR := $(JH)/jerboa-native-rs/target/release -LIBDIRS := --libdirs lib:$(JH)/lib -JEXEC := $(JERBUILD) exec $(LIBDIRS) +# crate, so building jerboa-gitlab needs only jerbuild + a C compiler + cargo. +# If no jerbuild is found, build/test targets fetch the release toolchain into +# .jerboa/bin using support/ensure-jerboa.sh. +JERBOA_VERSION ?= v0.2.3 +JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin +JERBUILD ?= $(shell if [ -x ./jerbuild ]; then echo ./jerbuild; \ + elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ]; then echo "$(JERBOA_TOOL_DIR)/jerbuild"; \ + elif [ -x ../jerboa/dist/jerbuild ]; then echo ../jerboa/dist/jerbuild; \ + elif [ -x ../jerboa/jerbuild ]; then echo ../jerboa/jerbuild; \ + elif command -v jerbuild >/dev/null 2>&1; then command -v jerbuild; \ + else echo "$(JERBOA_TOOL_DIR)/jerbuild"; fi) +JH = $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null) + +NATIVE_DIR = $(JH)/jerboa-native-rs/target/release +LIBDIRS = --libdirs lib:$(JH)/lib +JEXEC = "$(JERBUILD)" exec $(LIBDIRS) BIN := jgl BIN_DIR := $(HOME)/.local/bin +DIST_DIR ?= $(CURDIR)/dist +RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence +TARGET_EVIDENCE_DIR ?= $(DIST_DIR)/target-evidence # The std net modules dlopen libjerboa_native in interpreter mode; point the # loader at jerbuild's cache for `make run`/`make test`. DYLD := DYLD_FALLBACK_LIBRARY_PATH="$(NATIVE_DIR)" LD_LIBRARY_PATH="$(NATIVE_DIR):$$LD_LIBRARY_PATH" @@ -30,28 +38,117 @@ MUSL_TARGET := x86_64-unknown-linux-musl MUSL_CC ?= x86_64-linux-musl-gcc NATIVE_MUSL_A := $(JH)/jerboa-native-rs/target/$(MUSL_TARGET)/release/libjerboa_native.a -.PHONY: all build binary run test install clean help cross-linux +.PHONY: all build binary run test native-runtime security sbom reproducibility-report target-evidence target-evidence-selftest release-evidence install clean help cross-linux ensure-jerboa-tools .DEFAULT_GOAL := help all: binary +ensure-jerboa-tools: + @if "$(JERBUILD)" --jerboa-home >/dev/null 2>&1; then \ + echo "=== Using Jerboa toolchain: $(JERBUILD) ==="; \ + elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && "$(JERBOA_TOOL_DIR)/jerbuild" --jerboa-home >/dev/null 2>&1; then \ + echo "=== Using downloaded Jerboa toolchain: $(JERBOA_TOOL_DIR) ==="; \ + elif command -v jerbuild >/dev/null 2>&1 && jerbuild --jerboa-home >/dev/null 2>&1; then \ + echo "=== Using Jerboa toolchain from PATH: $$(command -v jerbuild) ==="; \ + else \ + echo "=== Fetching Jerboa $(JERBOA_VERSION) release tools into $(JERBOA_TOOL_DIR) ==="; \ + sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \ + fi + @"$(JERBUILD)" --jerboa-home >/dev/null || { \ + echo "ERROR: Jerboa toolchain is unavailable; set JERBUILD=/path/to/jerbuild or JERBOA_VERSION=<tag>" >&2; \ + exit 1; \ + } + +native-runtime: ensure-jerboa-tools + @command -v cargo >/dev/null 2>&1 || { echo "ERROR: cargo not found on PATH."; exit 1; } + cargo build --release --manifest-path "$(JH)/jerboa-native-rs/Cargo.toml" --no-default-features --features tls,crypto + # Standalone native binary via .jerbuild (entry jgl.ss -> jgl). # Two passes: pass 1 cargo-builds jerboa-native into jerbuild's cache; we then # regenerate the FFI symbol list (platform-correct) and relink. -binary: +binary: ensure-jerboa-tools @touch support/ffi-symbols.gen - $(JERBUILD) build - sh support/gen-ffi-symbols.sh - $(JERBUILD) build + "$(JERBUILD)" build + JERBUILD="$(JERBUILD)" sh support/gen-ffi-symbols.sh + "$(JERBUILD)" build build: binary run: binary $(DYLD) ./$(BIN) $(ARGS) -test: +test: native-runtime $(DYLD) $(JEXEC) test/test-jgl.ss +security: + @set -eu; \ + pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|gldt-[A-Za-z0-9_-]{20,}|glrt-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16})"; \ + matches="$$(git grep -n -I -E "$$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)"; \ + if [ -n "$$matches" ]; then \ + echo "$$matches"; \ + echo "High-confidence secret pattern found."; \ + exit 1; \ + fi; \ + echo "secret_scan_status=pass" + +sbom: ensure-jerboa-tools + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + FFI_SYMBOLS="support/ffi-symbols.gen" \ + BIN="$(BIN)" \ + sh scripts/sbom.sh + +reproducibility-report: ensure-jerboa-tools + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + MAKE="$(MAKE)" \ + BIN="$(BIN)" \ + sh scripts/reproducibility-report.sh + +target-evidence: + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" \ + sh scripts/target-evidence.sh + +target-evidence-selftest: + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + sh scripts/test-target-evidence.sh + +release-evidence: + @rm -rf "$(RELEASE_EVIDENCE_DIR)" "$(TARGET_EVIDENCE_DIR)" "$(DIST_DIR)/reproducibility" "$(DIST_DIR)/sbom" + @mkdir -p "$(RELEASE_EVIDENCE_DIR)" + @echo "==> Collecting git status" + @git status --short > "$(RELEASE_EVIDENCE_DIR)/git-status.txt" + @git rev-parse HEAD > "$(RELEASE_EVIDENCE_DIR)/git-head.txt" 2>/dev/null || true + @echo "==> Running high-confidence secret scan" + @$(MAKE) security > "$(RELEASE_EVIDENCE_DIR)/security.log" 2>&1 + @echo "==> Running tests" + @$(MAKE) test > "$(RELEASE_EVIDENCE_DIR)/test.log" 2>&1 + @echo "==> Building binary" + @$(MAKE) binary > "$(RELEASE_EVIDENCE_DIR)/binary.log" 2>&1 + @echo "==> Running binary help smoke" + @./$(BIN) --help > "$(RELEASE_EVIDENCE_DIR)/binary-help.log" 2>&1 + @echo "==> Running target evidence selftest" + @$(MAKE) target-evidence-selftest > "$(RELEASE_EVIDENCE_DIR)/target-evidence-selftest.log" 2>&1 + @echo "==> Recording target GitLab evidence" + @$(MAKE) target-evidence > "$(RELEASE_EVIDENCE_DIR)/target-evidence.log" 2>&1 + @rm -rf "$(RELEASE_EVIDENCE_DIR)/target-evidence" + @cp -R "$(TARGET_EVIDENCE_DIR)" "$(RELEASE_EVIDENCE_DIR)/target-evidence" + @echo "==> Running reproducibility report" + @$(MAKE) reproducibility-report > "$(RELEASE_EVIDENCE_DIR)/reproducibility.log" 2>&1 + @rm -rf "$(RELEASE_EVIDENCE_DIR)/reproducibility" + @cp -R "$(DIST_DIR)/reproducibility" "$(RELEASE_EVIDENCE_DIR)/reproducibility" + @echo "==> Generating SBOM" + @$(MAKE) sbom > "$(RELEASE_EVIDENCE_DIR)/sbom.log" 2>&1 + @rm -rf "$(RELEASE_EVIDENCE_DIR)/sbom" + @cp -R "$(DIST_DIR)/sbom" "$(RELEASE_EVIDENCE_DIR)/sbom" + @find jgl.ss build*.ss lib test support docs scripts .github .jerboa/security.json -type f -print | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/source-hashes.sha256" + @echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)" + install: binary mkdir -p $(BIN_DIR) install -m 0755 $(BIN) $(BIN_DIR)/$(BIN) @@ -59,7 +156,7 @@ install: binary # Cross-compile macOS -> Linux x86_64 musl (static). Builds the rustls native # crate for musl, lists its FFI symbols, then cross-links jgl-linux-amd64. -cross-linux: +cross-linux: ensure-jerboa-tools cd $(JH)/jerboa-native-rs && \ RUSTC="$$(rustup which --toolchain $(RUSTUP_TC) rustc)" \ CC_x86_64_unknown_linux_musl=$(MUSL_CC) \ @@ -76,6 +173,7 @@ cross-linux: clean: rm -f $(BIN) jgl-linux-amd64 jgl-*main.c *.h *.boot *.wp.so find . \( -name '*.so' -o -name '*.wpo' -o -name '*.tarm64osx' -o -name '*.ta6le' \) -delete 2>/dev/null || true + rm -rf dist help: @echo "jerboa-gitlab -- the jgl GitLab CLI (jerbuild + cargo + cc)" @@ -83,6 +181,7 @@ help: @echo " make binary Build the standalone ./jgl" @echo " make run ARGS='...' Build + run ./jgl" @echo " make test Run the test suite" + @echo " make release-evidence Run test/build/security/SBOM/reproducibility gate" @echo " make install Build + install to ~/.local/bin/jgl" @echo " make cross-linux Cross-build jgl-linux-amd64 (musl static, rustls)" @echo " make clean Remove build artifacts" --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ HTTPS). You need `jerbuild`, a C compiler, and `cargo`. make binary # -> ./jgl make install # -> ~/.local/bin/jgl make test # run the unit tests +make release-evidence ``` The same `make binary` works natively on macOS, Linux, and FreeBSD. To @@ -69,6 +70,17 @@ jgl <command> [options] A token with `read_api` scope is needed for private projects; public projects read without one. +## Security + +`jgl` can read private GitLab project metadata, issues, comments, CI status, and +attachments. Prefer a `read_api` token, avoid passing tokens in argv on shared +systems, and treat `jgl export` output as sensitive. Credential and export +handling are documented in [`docs/credential-handling.md`](docs/credential-handling.md). +Native dependency provenance is documented in +[`docs/dependency-provenance.md`](docs/dependency-provenance.md), and local +release evidence is documented in +[`docs/release-evidence.md`](docs/release-evidence.md). + ### Configuration `jgl` resolves each setting with the precedence **flag › environment › @@ -123,7 +135,9 @@ Each `README.md` has the title, metadata (state, author, assignees, labels, dates, URL), the description, and non-system comments. When the project is given as a path (`group/proj`), referenced `/uploads/` attachments are downloaded into `attachments/` and the links are rewritten to point at the local copies; if a -download fails the original link is left intact. +download fails the original link is left intact. Issue directory names and +attachment filenames derived from API responses are sanitized before local +filesystem writes. ## Cross-compilation @@ -166,4 +180,4 @@ test/test-jgl.ss unit tests (pure logic) ## License -MIT. +Apache-2.0. new file mode 100644 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,48 @@ +# Security Policy + +`jerboa-gitlab` is a GitLab API CLI and issue exporter. It handles GitLab access +tokens and can write private issue data to disk, so public release requires a +clean security gate even though it is not a daemon. + +## Supported Status + +No public production-support commitment exists yet. Treat current builds as +experimental until the release checklist in `~/Release-plan.md` and +`~/mine/jerboa-production-readiness.md` is complete for this repository. + +## Security Expectations + +- Use the narrowest GitLab token scope possible. `read_api` is expected for + private read-only project usage. +- Prefer `GITLAB_TOKEN` from a secret manager or a mode-0600 config file over + passing tokens in argv. +- Do not commit `~/.gitlab.yaml`, GitLab tokens, exported issue archives, + downloaded attachments, private project paths, or private hostnames. +- Treat `jgl export` output as sensitive. It may contain issue bodies, comments, + labels, usernames, project URLs, and attachments. +- Keep raw GitLab API response bodies out of logs and errors. Runtime errors + redact response bodies and strip query strings from failed API paths. +- Production release requires marker-complete live GitLab target evidence + through `JERBOA_GITLAB_TARGET_PROOF_FILE`; setting + `JERBOA_GITLAB_REQUIRE_TARGET_PROOF=1` makes missing or incomplete proof fail + closed. +- Target proof files must contain only status markers and sanitized reviewer + notes. Do not preserve GitLab tokens, `PRIVATE-TOKEN` headers, bearer + headers, private project paths, exported issue archives, attachments, private + hostnames, or raw API response bodies. +- Release builds must pass `make release-evidence`, which runs tests, binary + build and help smoke, high-confidence secret scan, SBOM/dependency capture, + cargo audit when installed, and repeated binary/FFI-symbol reproducibility + checks. + +Credential and export details are documented in +[`docs/credential-handling.md`](docs/credential-handling.md). +Native dependency and local release-evidence details are documented in +[`docs/dependency-provenance.md`](docs/dependency-provenance.md) and +[`docs/release-evidence.md`](docs/release-evidence.md). + +## Reporting + +Before public release, report issues privately to the repository owner. After +public release, replace this section with a dedicated advisory contact, +supported versions, and disclosure window. --- a/build-jgl-cross.ss +++ b/build-jgl-cross.ss @@ -29,8 +29,6 @@ ;;; ;;; Produces: jgl-linux-amd64 (static Linux x86_64 ELF). -(import (chezscheme)) - ;; ── Params ────────────────────────────────────────────────────────────────── (define jerboa-home (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa")) (define cross-prefix (format "~a/.chez-cross-ta6le" jerboa-home)) @@ -110,6 +108,32 @@ (printf " cross csv-dir: ~a~n native .a: ~a~n symbols: ~a jerboa_* + ~a libc~n output: ~a~n~n" cross-csv-dir native-a (length jerboa-symbols) (length libc-symbols) output) +(define (shell-quote s) + (let ([out (open-output-string)]) + (write-char #\' out) + (let loop ([chars (string->list s)]) + (unless (null? chars) + (let ([ch (car chars)]) + (if (char=? ch #\') + (display "'\\''" out) + (write-char ch out))) + (loop (cdr chars)))) + (write-char #\' out) + (get-output-string out))) + +(define (run-safe-system! who safe-cmd) + (let ([status (system safe-cmd)]) ; jerboa-security: suppress missing-taint-check-at-sink -- trusted cross-release build command after path shell-quoting + (unless (zero? status) + (error who "command failed" safe-cmd status)))) + +(define (remove-build-dir! dir) + (run-safe-system! 'build-jgl-cross + (string-append "rm -rf " (shell-quote dir)))) + +(define (reset-build-dir! dir) + (run-safe-system! 'build-jgl-cross + (string-append "rm -rf " (shell-quote dir) " && mkdir -p " (shell-quote dir)))) + ;; ── Stage 0.5: isolate compiled objects ──────────────────────────────────── ;; Never patch source libraries in place. Chez maps each library source dir to ;; a writable object dir; point them ALL at obj-dir so the ta6le .so/.wpo land @@ -117,7 +141,7 @@ (define obj-dir (format "~a/.build/~a-obj" jgl-repo output)) (define entry-so (format "~a/program.so" obj-dir)) (define entry-wpo (format "~a/program.wpo" obj-dir)) -(system (format "rm -rf '~a' && mkdir -p '~a'" obj-dir obj-dir)) +(reset-build-dir! obj-dir) ;; ── Stage 1: load xpatch (target=ta6le emit mode) ────────────────────────── (define orig-libdirs (library-directories)) @@ -130,13 +154,13 @@ ;; ── Stage 2: compile-program ──────────────────────────────────────────────── (printf "==> [2/6] compile-program ~a -> ~a~n" entry-script entry-so) -(guard (e (#t (system (format "rm -rf '~a'" obj-dir)) (raise e))) +(guard (e (#t (remove-build-dir! obj-dir) (raise e))) (compile-program entry-script entry-so)) ;; ── Stage 3: whole-program optimize → single .so ──────────────────────────── (define wpo-output (string-append output ".wp.so")) (printf "==> [3/6] compile-whole-program ~a -> ~a~n" entry-wpo wpo-output) -(guard (e (#t (system (format "rm -rf '~a'" obj-dir)) (raise e))) +(guard (e (#t (remove-build-dir! obj-dir) (raise e))) (compile-whole-program entry-wpo wpo-output #t)) ;; ── Stage 4: embed boots + program as C arrays ────────────────────────────── @@ -222,18 +246,18 @@ ;; ── Stage 6: compile + link with cross-cc ────────────────────────────────── (printf "==> [6/6] compile + link with ~a~n" cross-cc) (define link-cmd - (format - (string-append - "~a -static -O2 -I~a ~a " - "-Wl,--start-group " - "~a ~a/libkernel.a ~a/libz.a ~a/liblz4.a " - "-lpthread -lm -ldl " - "-Wl,--end-group -o ~a") - cross-cc cross-csv-dir main-c-path - native-a cross-csv-dir cross-csv-dir cross-csv-dir - output)) + (string-append + (shell-quote cross-cc) + " -static -O2 -I " (shell-quote cross-csv-dir) + " " (shell-quote main-c-path) + " -Wl,--start-group " + (shell-quote native-a) " " + (shell-quote (format "~a/libkernel.a" cross-csv-dir)) " " + (shell-quote (format "~a/libz.a" cross-csv-dir)) " " + (shell-quote (format "~a/liblz4.a" cross-csv-dir)) " " + "-lpthread -lm -ldl " + "-Wl,--end-group -o " (shell-quote output))) (printf " ~a~n" link-cmd) -(unless (zero? (system link-cmd)) - (error 'build-jgl-cross "link failed")) -(system (format "rm -rf '~a'" obj-dir)) ;; drop isolated objects +(run-safe-system! 'build-jgl-cross link-cmd) +(remove-build-dir! obj-dir) ;; drop isolated objects (printf "~n==> done: ~a~n" output) new file mode 100644 --- /dev/null +++ b/docs/credential-handling.md @@ -0,0 +1,65 @@ +# Credential Handling + +`jgl` is a GitLab API client. Treat every token, project path, issue body, +comment, attachment, and exported archive as sensitive unless the project is +known public. + +## Token Sources + +`jgl` resolves credentials in this order: + +1. `--token TOKEN` +2. `GITLAB_TOKEN` +3. `token:` in `~/.gitlab.yaml` or the path supplied by `--config` or + `GITLAB_CONFIG` + +Use a GitLab token with the narrowest scope that works. For private read-only +usage, `read_api` is the expected scope. Do not use admin, write, registry, or +sudo-capable tokens for routine issue and CI queries. + +## Local Storage + +`jgl` does not persist tokens by itself. If a config file stores a token, the +operator is responsible for filesystem permissions. Recommended mode: + +```sh +chmod 0600 ~/.gitlab.yaml +``` + +Avoid passing tokens on a shared shell command line because process listings and +shell history can expose argv. Prefer `GITLAB_TOKEN` from a secret manager or a +mode-0600 config file. + +## Error Output + +GitLab API error responses can echo request data or private project content. +`jgl` redacts response bodies in raised error messages and keeps only the +response byte count. Failed API paths are reported without query strings. + +## Exports And Attachments + +`jgl export` writes issue markdown under `--output` and may download referenced +`/uploads/` attachments. Export output can contain issue descriptions, comments, +usernames, labels, private project URLs, and binary attachments. Store exports +outside public repos unless intentionally publishing sanitized project data. +Issue directory names and downloaded attachment filenames derived from API +responses are sanitized before local filesystem writes. + +## Release Requirements + +Before a production release: + +- Run `make release-evidence` from a clean checkout. +- Run release evidence with `JERBOA_GITLAB_REQUIRE_TARGET_PROOF=1` and a + marker-complete `JERBOA_GITLAB_TARGET_PROOF_FILE` from a least-privilege live + GitLab token. +- The target proof must confirm public and private project smokes, + least-privilege token scope, token source hygiene, API error redaction, + export/attachment redaction, credential redaction, target-platform smoke, + production credential review, no raw API output retention, and the + sensitive-artifact policy. +- Run the baseline secret scan and a history scan for GitLab token patterns. +- Confirm release artifacts do not include local exports, generated stale C + files, tokens, private project paths, or private GitLab hostnames. +- Review `jerboa-native` dependency versions and produce SBOM/reproducibility + notes. new file mode 100644 --- /dev/null +++ b/docs/dependency-provenance.md @@ -0,0 +1,17 @@ +# Dependency Provenance + +`jgl` uses Jerboa's `(std net request)` module for HTTPS requests to the GitLab +REST API. TLS is provided by the bundled `jerboa-native-rs` crate built with +`tls,crypto` features. + +The release-evidence SBOM records: + +- selected Jerboa toolchain and cached Jerboa home; +- `jerboa-native-rs` `Cargo.toml` and `Cargo.lock`; +- `cargo tree` for the `tls,crypto` native feature set; +- `cargo audit` output when the tool is installed; +- binary linkage/load-command data and source/workflow hashes. + +No OpenSSL CLI subprocess is used for GitLab API requests. Release review +should still check the native Rust dependency lockfile, hosted CI output, and +GitLab token handling before public production support. new file mode 100644 --- /dev/null +++ b/docs/release-evidence.md @@ -0,0 +1,64 @@ +# Release Evidence + +`make release-evidence` writes ignored local evidence under +`dist/release-evidence/`. + +The evidence bundle contains: + +- high-confidence tracked-file secret-scan output; +- unit-test output; +- standalone binary build and `--help` smoke output; +- SBOM-style source, workflow, binary, toolchain, and native dependency + manifests; +- Jerboa native Cargo metadata, dependency tree, and `cargo audit` status when + `cargo audit` is installed; +- target GitLab proof status and proof-gate self-test output; +- repeated binary and generated FFI-symbol reproducibility checks. + +GitLab API access uses `(std net request)` over Jerboa's bundled +`jerboa-native-rs` TLS provider with `tls,crypto` features. The client sends +the token in the `PRIVATE-TOKEN` header and redacts non-2xx API response bodies +from raised errors. + +This evidence does not replace GitLab-specific release blockers: hosted CI on +the release platform, live least-privilege token smoke tests against public and +private projects, export/attachment redaction review, and external review of +credentialed client behavior are still required before production claims. + +## Target GitLab Proof + +Local `make release-evidence` always writes +`dist/release-evidence/target-evidence/status.txt`. Without a proof file, the +target-only GitLab checks are recorded as `blocked-not-run` so local evidence is +not mistaken for live token evidence. + +To attach production target evidence, create a sanitized marker-only proof file +and run: + +```sh +JERBOA_GITLAB_TARGET_PROOF_FILE=/path/to/gitlab-target-proof.txt \ +JERBOA_GITLAB_REQUIRE_TARGET_PROOF=1 \ +make release-evidence +``` + +The proof file must contain every line below exactly: + +```text +gitlab_public_project_smoke_status=target-evidence-recorded +gitlab_private_project_smoke_status=target-evidence-recorded +least_privilege_token_scope_status=confirmed +token_source_hygiene_status=confirmed +api_error_redaction_status=target-evidence-recorded +export_attachment_redaction_status=target-evidence-recorded +credential_redaction_status=target-evidence-recorded +target_platform_smoke_status=target-evidence-recorded +production_credential_review_status=current-run-recorded +raw_api_output_status=not-preserved +sensitive_artifact_policy=no-gitlab-tokens-private-project-paths-issue-exports-attachments-or-raw-api-responses-preserved +``` + +The proof gate rejects missing, empty, marker-incomplete, or high-confidence +sensitive proof files. The proof must not preserve GitLab tokens, +`PRIVATE-TOKEN` headers, bearer headers, private project paths, exported issue +archives, attachments, private hostnames, raw API responses, or downloaded +content. --- a/jgl.ss +++ b/jgl.ss @@ -6,7 +6,7 @@ ;;; argv through <PREFIX>_ARGC / <PREFIX>_ARGn environment variables. We try the ;;; expected prefixes and fall back to (command-line) for interpreted mode. -(import (chezscheme) +(import (jerboa core) (jgl cli)) (define (env-args) --- a/lib/jgl/client.ss +++ b/lib/jgl/client.ss @@ -8,7 +8,7 @@ (library (jgl client) (export make-client client? client-base-url client-token gl-get gl-get-all gl-get-text gl-try-get gl-download - pct-encode build-query api-path) + pct-encode build-query api-path gitlab-error-response-summary) (import (except (chezscheme) make-hash-table hash-table? sort sort! @@ -74,16 +74,31 @@ (list (cons "PRIVATE-TOKEN" (client-token c)) (cons "Accept" "application/json"))) - (def (truncate-str s n) - (if (> (string-length s) n) (string-append (substring s 0 n) "...") s)) + (def (string-index-char s ch) + (let loop ([i 0]) + (cond + [(>= i (string-length s)) #f] + [(char=? (string-ref s i) ch) i] + [else (loop (+ i 1))]))) + + (def (path-without-query path) + (let ([idx (string-index-char path #\?)]) + (if idx (substring path 0 idx) path))) + + (def (gitlab-error-response-summary body) + ;; GitLab errors may echo request data or private issue/project content. + ;; Preserve only the byte count so CLI/log callers do not persist secrets. + (if (and (string? body) (> (string-length body) 0)) + (string-append " -- response redacted (" + (number->string (string-length body)) + " bytes)") + "")) (def (gl-error status body path) (error 'gitlab (string-append "GitLab API returned " (number->string status) - " for /" path - (if (> (string-length body) 0) - (string-append " -- " (truncate-str body 400)) - "")))) + " for /" (path-without-query path) + (gitlab-error-response-summary body)))) ;; Raw authenticated GET. Returns (values status body-text). (def (gl-request c path) --- a/lib/jgl/issues.ss +++ b/lib/jgl/issues.ss @@ -7,7 +7,8 @@ (library (jgl issues) (export list-issues get-issue get-notes - issue->markdown export-issues) + issue->markdown export-issues + safe-export-file-name safe-issue-dir-name) (import (except (chezscheme) make-hash-table hash-table? sort sort! @@ -112,6 +113,31 @@ (let ([idx (last-index path #\/)]) (if idx (substring path (+ idx 1) (string-length path)) path))) + (def (safe-name-char? ch) + (or (char-alphabetic? ch) + (char-numeric? ch) + (memv ch '(#\. #\_ #\-)))) + + (def (safe-export-file-name raw fallback) + (let* ([source (if (and (string? raw) (not (string=? raw ""))) + raw + fallback)] + [out (open-output-string)]) + (for-each + (lambda (ch) + (write-char (if (safe-name-char? ch) ch #\_) out)) + (string->list source)) + (let ([name (get-output-string out)]) + (cond + [(or (string=? name "") (string=? name ".") (string=? name "..")) + fallback] + [(char=? (string-ref name 0) #\.) + (string-append "_" name)] + [else name])))) + + (def (safe-issue-dir-name iid) + (string-append "issue-" (safe-export-file-name iid "unknown"))) + ;; Collect distinct "/uploads/..." paths referenced in `text`. (def (extract-upload-paths text) (let ([n (string-length text)] [needle "/uploads/"]) @@ -158,16 +184,20 @@ content (let ([attach-dir (string-append issue-dir "/attachments")]) (ensure-dir attach-dir) - (let loop ([us uploads] [text content]) + (let loop ([us uploads] [text content] [index 1]) (if (null? us) text (let* ([up (car us)] - [fname (basename up)] + [fname (string-append + (number->string index) + "-" + (safe-export-file-name (basename up) "attachment"))] [url (string-append project-web up)] [dest (string-append attach-dir "/" fname)] [ok (guard (e (#t #f)) (gl-download c url dest))]) (loop (cdr us) - (if ok (replace-all text up (string-append "attachments/" fname)) text))))))))) + (if ok (replace-all text up (string-append "attachments/" fname)) text) + (+ index 1))))))))) ;; Export every issue to <outdir>/issue-<iid>/README.md. Returns the count. (def (export-issues c project outdir) @@ -181,7 +211,7 @@ (when (pair? is)