Security hardening and release readiness
ober
310d6be2129134fb184788736118f31e87488dfe
new file mode 100644 --- /dev/null +++ b/.build.yml @@ -0,0 +1,15 @@ +image: ubuntu/latest +packages: + - git + - make + - gcc + - curl + - ca-certificates + - ripgrep +tasks: + - verify: | + cd jerboa-awk + make verify + - release-evidence: | + cd jerboa-awk + make release-evidence new file mode 100644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install build tools + run: sudo apt-get update && sudo apt-get install -y build-essential curl ca-certificates ripgrep + + - name: Install jerbuild + run: | + set -eux + curl -fsSL "https://github.com/jerboa-lang/jerboa/releases/download/${JERBOA_VERSION}/jerbuild-linux-x86_64" -o /usr/local/bin/jerbuild + chmod +x /usr/local/bin/jerbuild + env: + JERBOA_VERSION: v0.2.3 + + - name: Verify + run: make verify + + - name: Release evidence + run: make release-evidence new file mode 100644 --- /dev/null +++ b/.github/workflows/security-baseline.yml @@ -0,0 +1,22 @@ +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: Install scanner tools + run: sudo apt-get update && sudo apt-get install -y ripgrep + + - name: Security baseline + run: scripts/security-check.sh --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ *.so *.wpo +*.a +*.boot +*.o +/.build/ +/dist/ /jawk /jawk-debug @@ -13,4 +18,5 @@ /petite_boot.h /scheme_boot.h /jawk_program.h +.jawk-test-out .claude/ --- a/.jerboa/security.json +++ b/.jerboa/security.json @@ -2,20 +2,60 @@ "version": 1, "repo": "jerboa-awk", "extends": ["jerboa:cli", "jerboa:parser"], + "classification": "hostile-input parser and local CLI", + "license": "Apache-2.0", "paths": { - "production": ["*.ss", "*.sls", "lib/**/*.ss", "lib/**/*.sls", "src/**/*.{ss,sls,c,h,rs,sh}", "bin/**", "Makefile"], + "production": ["*.ss", "*.sls", "lib/**/*.ss", "lib/**/*.sls", "bin/**", "Makefile"], "tests": ["test/**", "tests/**", "**/*-test.ss", "bench/**", "benchmarks/**"], - "generated": ["build/**", "dist/**", "target/**", "*.so", "*.dylib", "*.wpo"], + "generated": ["build/**", "dist/**", "target/**", "jawk", "*.so", "*.dylib", "*.wpo"], "vendor": ["vendor/**", "third_party/**", "node_modules/**"], "docs": ["README.md", "docs/**", "*.md", "AGENTS.md"] }, + "capabilities": { + "network": "none", + "ffi": "none in repository source", + "shell": "disabled by default; JAWK_ALLOW_SYSTEM=1 enables AWK system() and pipe I/O", + "scriptFileIo": "disabled by default; JAWK_ALLOW_FILE_IO=1 enables AWK redirection/getline files", + "environment": "ENVIRON hidden by default; JAWK_EXPOSE_ENVIRON=1 exposes a small allowlist" + }, + "resourceLimits": { + "JAWK_MAX_PROGRAM_CHARS": "maximum AWK program text length; default 1048576", + "JAWK_MAX_RECORD_CHARS": "maximum input record length; default 1048576" + }, "policy": { "failOn": ["critical", "high"], "imports": { "directChezscheme": "deny" }, "ffi": { "allowed": false }, - "process": { "shellInterpolation": "deny", "restrictedModeExpected": true }, - "network": { "requireTimeouts": false }, + "process": { "allowedOnlyBehind": "JAWK_ALLOW_SYSTEM=1" }, + "network": { "allowed": false }, "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false } }, + "targetEvidence": { + "script": "scripts/target-evidence.sh", + "optionalProofFileEnv": "JAWK_TARGET_PROOF_FILE", + "requiredProofEnv": "JAWK_REQUIRE_TARGET_PROOF", + "maxProofBytes": 65536, + "targetProofStatus": "target_awk_proof_status", + "productionStatus": "production_awk_status", + "sensitiveArtifactPolicy": "no-input-record-output-captures-private-paths-or-host-data", + "requiredMarkers": [ + "long_awk_parity_corpus_status=reviewed", + "external_parser_review_status=reviewed", + "external_runtime_review_status=reviewed", + "sandbox_policy_review_status=reviewed", + "script_file_io_review_status=reviewed", + "environment_exposure_review_status=reviewed", + "sensitive_artifact_policy=no-input-record-output-captures-private-paths-or-host-data" + ] + }, + "releaseGates": [ + "make security", + "make test", + "make verify", + "make sbom", + "make reproducibility-report", + "make target-evidence", + "make release-evidence" + ], "suppressions": [] } 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,16 +1,26 @@ -# jerbuild bundles Chez Scheme + the jerboa stdlib under ~/.cache/jerbuild/, -# so building jawk needs only `jerbuild` + a C compiler — no jerboa source -# checkout and no separately-built Chez. -JERBUILD ?= jerbuild +JERBOA_VERSION ?= v0.2.3 +JERBOA_TOOL_DIR ?= ../jerboa/dist +ifeq ($(origin JERBUILD),undefined) +ifneq ($(wildcard $(JERBOA_TOOL_DIR)/jerbuild),) +JERBUILD := $(JERBOA_TOOL_DIR)/jerbuild +else +JERBUILD := jerbuild +endif +endif 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 LIBDIRS := --libdirs lib:$(JH)/lib +JEXEC := $(JERBUILD) exec $(LIBDIRS) JAWK_BIN := jawk +EVIDENCE_DIR ?= dist/release-evidence +SBOM_DIR ?= dist/sbom +REPRO_DIR ?= dist/reproducibility +TARGET_EVIDENCE_DIR ?= dist/target-evidence -.PHONY: all build binary test clean install +.PHONY: all build binary test test-parser parser-corpus test-cli-policy import-check clean-generated security audit verify sbom reproducibility-report target-evidence release-evidence clean install all: binary @@ -21,14 +31,31 @@ binary: build: binary clean: - find lib \( -name '*.so' -o -name '*.wpo' \) -delete 2>/dev/null || true - rm -f $(JAWK_BIN) + find lib \( -name '*.so' -o -name '*.wpo' -o -name '*.o' \) -delete 2>/dev/null || true + rm -rf dist + rm -f $(JAWK_BIN) .jawk-test-out install: binary install -d $(HOME)/.local/bin install -m 0755 $(JAWK_BIN) $(HOME)/.local/bin/$(JAWK_BIN) -test: binary +test-parser: + @$(JEXEC) tests/test-parser.ss + +parser-corpus: + @$(JEXEC) support/parser-corpus-evidence.ss + +test-cli-policy: binary + @echo "--- system disabled by default ---"; if ./$(JAWK_BIN) 'BEGIN{system("true")}' >/tmp/jawk-system.out 2>/tmp/jawk-system.err; then cat /tmp/jawk-system.out /tmp/jawk-system.err; exit 1; fi + @echo "--- pipe disabled by default ---"; if ./$(JAWK_BIN) 'BEGIN{print "x" | "cat"}' >/tmp/jawk-pipe.out 2>/tmp/jawk-pipe.err; then cat /tmp/jawk-pipe.out /tmp/jawk-pipe.err; exit 1; fi + @echo "--- file I/O disabled by default ---"; if ./$(JAWK_BIN) 'BEGIN{print "x" > ".jawk-test-out"}' >/tmp/jawk-file.out 2>/tmp/jawk-file.err; then cat /tmp/jawk-file.out /tmp/jawk-file.err; exit 1; fi + @echo "--- system opt-in ---"; JAWK_ALLOW_SYSTEM=1 ./$(JAWK_BIN) 'BEGIN{print system("true")}' | grep -qx 0 + @echo "--- pipe opt-in ---"; JAWK_ALLOW_SYSTEM=1 ./$(JAWK_BIN) 'BEGIN{print "ok" | "cat"}' + @echo "--- file I/O opt-in ---"; rm -f .jawk-test-out; JAWK_ALLOW_FILE_IO=1 ./$(JAWK_BIN) 'BEGIN{print "ok" > ".jawk-test-out"}'; grep -qx ok .jawk-test-out; rm -f .jawk-test-out + @echo "--- ENVIRON hidden by default ---"; ./$(JAWK_BIN) 'BEGIN{print ("PATH" in ENVIRON)}' | grep -qx 0 + @echo "--- ENVIRON allowlist opt-in ---"; JAWK_EXPOSE_ENVIRON=1 ./$(JAWK_BIN) 'BEGIN{print ("PATH" in ENVIRON)}' | grep -qx 1 + +test: test-parser binary @echo "--- print field ---"; echo "hello world" | ./$(JAWK_BIN) '{print $$1}' @echo "--- -F, ---"; echo "a,b,c" | ./$(JAWK_BIN) -F, '{print $$2}' @echo "--- BEGIN/END ---"; echo "" | ./$(JAWK_BIN) 'BEGIN{print "start"} END{print "end"}' @@ -36,4 +63,55 @@ test: binary @echo "--- sum ---"; printf "10\n20\n30\n" | ./$(JAWK_BIN) '{sum += $$1} END {print sum}' @echo "--- assoc arrays ---"; printf "apple\nbanana\napple\n" | ./$(JAWK_BIN) '{c[$$1]++} END {for (k in c) print k, c[k]}' @echo "--- gsub ---"; echo "foo bar foo" | ./$(JAWK_BIN) '{gsub(/foo/, "FOO"); print}' + $(MAKE) test-cli-policy @echo "All tests passed." + +import-check: + @$(JEXEC) support/import-check.ss + +clean-generated: + find lib \( -name '*.so' -o -name '*.wpo' -o -name '*.o' \) -delete 2>/dev/null || true + rm -f $(JAWK_BIN) .jawk-test-out + +security: clean-generated import-check scripts/security-check.sh + scripts/security-check.sh + +audit: security + @echo "jawk has no vendored native dependency manifest; security script covers source, generated artifacts, shell/file policy, and secrets." + +target-evidence: scripts/target-evidence.sh + @REPO_ROOT=. TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" sh scripts/target-evidence.sh + +verify: audit parser-corpus test target-evidence + +sbom: + REPO_ROOT="$(CURDIR)" SBOM_DIR="$(SBOM_DIR)" JERBUILD="$(JERBUILD)" JERBOA_VERSION="$(JERBOA_VERSION)" sh scripts/sbom.sh + +reproducibility-report: + REPO_ROOT="$(CURDIR)" REPRO_DIR="$(REPRO_DIR)" MAKE="$(MAKE)" sh scripts/reproducibility-report.sh + +release-evidence: verify + $(MAKE) reproducibility-report + $(MAKE) sbom + rm -rf $(EVIDENCE_DIR) + mkdir -p $(EVIDENCE_DIR) + rm -f $(JAWK_BIN) + { git rev-parse HEAD 2>/dev/null || true; } > $(EVIDENCE_DIR)/git-commit.txt + git status --short > $(EVIDENCE_DIR)/git-status.txt + git diff --stat > $(EVIDENCE_DIR)/diff-stat.txt + { printf 'JERBOA_VERSION=%s\n' '$(JERBOA_VERSION)'; "$(JERBUILD)" --version; if "$(JERBUILD)" --jerboa-home >/dev/null 2>&1; then echo "jerboa_home_status=present"; else echo "jerboa_home_status=missing"; fi; uname -srm; env | LC_ALL=C sort | grep -E '^(JAWK_ALLOW_FILE_IO|JAWK_ALLOW_SYSTEM|JAWK_EXPOSE_ENVIRON|JAWK_MAX_PROGRAM_CHARS|JAWK_MAX_RECORD_CHARS)=' || true; } > $(EVIDENCE_DIR)/build-env.txt + $(MAKE) security > $(EVIDENCE_DIR)/security.log 2>&1 + $(MAKE) parser-corpus > $(EVIDENCE_DIR)/parser-corpus.log 2>&1 + $(MAKE) test > $(EVIDENCE_DIR)/test.log 2>&1 + $(MAKE) target-evidence > $(EVIDENCE_DIR)/target-evidence.log 2>&1 + echo "hello world" | ./$(JAWK_BIN) '{print $$2}' > $(EVIDENCE_DIR)/smoke.txt + shasum -a 256 ./$(JAWK_BIN) > $(EVIDENCE_DIR)/binary-sha256.txt + { find . -type f -not -path './.git/*' -not -path './dist/*' -not -path './.build/*' -not -name 'jawk' -not -name '*.so' -not -name '*.wpo' -not -name '*.wp.so' -not -name '*-main.c' -not -name 'petite_boot.h' -not -name 'scheme_boot.h' -not -name 'jawk_program.h' | LC_ALL=C sort | while IFS= read -r f; do shasum -a 256 "$$f"; done; } > $(EVIDENCE_DIR)/source-sha256.txt + rm -rf $(EVIDENCE_DIR)/sbom $(EVIDENCE_DIR)/reproducibility $(EVIDENCE_DIR)/target-evidence + cp -R $(SBOM_DIR) $(EVIDENCE_DIR)/sbom + cp -R $(REPRO_DIR) $(EVIDENCE_DIR)/reproducibility + cp -R $(TARGET_EVIDENCE_DIR) $(EVIDENCE_DIR)/target-evidence + grep -q '^target_evidence_status=present$$' "$(EVIDENCE_DIR)/target-evidence/status.txt" + test "$$(grep '^status=' "$(EVIDENCE_DIR)/reproducibility/report.txt" | cut -d= -f2)" = "match" + sh scripts/sanitize-evidence.sh "$(EVIDENCE_DIR)" + rm -f $(JAWK_BIN) .jawk-test-out --- a/README.md +++ b/README.md @@ -1 +1,51 @@ # jerboa-awk + +`jawk` is an AWK interpreter implemented in Jerboa Scheme. + +## Build and Test + +```sh +make verify +make release-evidence +``` + +`make verify` runs the source security gate, import check, parser regression +tests, deterministic parser corpus evidence, native binary build, CLI smoke +tests, and secure-default policy tests. `make release-evidence` also records +SBOM/toolchain evidence and a two-pass clean-build reproducibility report for +the generated `jawk` binary, repeated parser corpus output, and target proof +status. + +## Secure Defaults + +AWK programs are treated as hostile input unless the caller explicitly opts into +legacy AWK capabilities: + +- `JAWK_ALLOW_SYSTEM=1` enables `system()` and pipe redirections. +- `JAWK_ALLOW_FILE_IO=1` enables script-driven `print > file` and + `getline < file` I/O. CLI input files and `-f` program files remain supported. +- `JAWK_EXPOSE_ENVIRON=1` exposes a small allowlist of process environment + variables through `ENVIRON`; by default `ENVIRON` is empty. +- `JAWK_MAX_PROGRAM_CHARS` caps program text, default 1 MiB. +- `JAWK_MAX_RECORD_CHARS` caps input records, default 1 MiB. + +Generated `jawk` binaries and native build artifacts are ignored and must not be +tracked. + +Production support also requires a reviewed `JAWK_TARGET_PROOF_FILE` with the +markers documented in `docs/release-evidence.md`. `JAWK_REQUIRE_TARGET_PROOF=1` +fails closed when that target proof is missing or incomplete. + +## Example + +```sh +echo "hello world" | ./jawk '{print $1}' +echo "a,b,c" | ./jawk -F, '{print $2}' +``` + +## Security Docs + +- `SECURITY.md` +- `docs/threat-model.md` +- `docs/parser-hardening.md` +- `docs/release-evidence.md` new file mode 100644 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,59 @@ +# Security Policy + +`jerboa-awk` is a local AWK interpreter. AWK program text, regexes, records, +filenames, and redirection targets are treated as hostile input. + +Production claims require the release-plan gates and the repository production +readiness tracker to be complete for this repository. + +## Supported Status + +No public production-support commitment exists yet. Security-sensitive releases +must be cut from a clean checkout after: + +- `make verify` passes. +- `make sbom` and `make reproducibility-report` pass. +- `make target-evidence` records local blocked status or a reviewed + marker-complete target proof. +- `make release-evidence` produces fresh evidence under `dist/release-evidence/`. +- `git status --short` shows only intentional source changes and ignored + generated artifacts. +- History has been reviewed for secrets or intentionally reset before first + public release. + +## Security Expectations + +- Do not add network, FFI, or daemon behavior without updating the threat model, + `.jerboa/security.json`, tests, and release gates. +- Keep generated `jawk` binaries, native artifacts, and WPO outputs out of + source control. +- Keep `system()` and pipe redirections disabled unless the operator sets + `JAWK_ALLOW_SYSTEM=1`. +- Keep script-driven file I/O disabled unless the operator sets + `JAWK_ALLOW_FILE_IO=1`. +- Keep `ENVIRON` hidden unless the operator sets `JAWK_EXPOSE_ENVIRON=1`. +- Keep program and record limits bounded for production. +- Keep target proof files free of raw AWK input records, output captures, + private checkout paths, hostnames, and customer/user data. +- Do not commit secrets, production `.env` files, operational hostnames, + customer/user data, or private infrastructure details. + +## Target Production Proof + +`JAWK_TARGET_PROOF_FILE` may point at a reviewed proof file for a production +target environment. `JAWK_REQUIRE_TARGET_PROOF=1` fails closed when that proof is +missing or marker-incomplete. The proof file must contain these exact markers: + +- `long_awk_parity_corpus_status=reviewed` +- `external_parser_review_status=reviewed` +- `external_runtime_review_status=reviewed` +- `sandbox_policy_review_status=reviewed` +- `script_file_io_review_status=reviewed` +- `environment_exposure_review_status=reviewed` +- `sensitive_artifact_policy=no-input-record-output-captures-private-paths-or-host-data` + +## 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/bin/jawk-program.ss +++ b/bin/jawk-program.ss @@ -5,7 +5,7 @@ ;;;; execute and (command-line-arguments) holds argv[1..]. (No scheme-start ;;;; hook fires under jerbuild, so call main directly.) -(import (chezscheme) (jerboa-awk main)) +(import (scheme) (jerboa-awk main)) (suppress-greeting #t) --- a/bin/jawk.ss +++ b/bin/jawk.ss @@ -1,6 +1,6 @@ #!/usr/bin/env scheme --libdirs lib --script ;;;; jawk — AWK interpreter in Jerboa Scheme -(import (chezscheme) (jerboa-awk main)) +(import (scheme) (jerboa-awk main)) (main (cdr (command-line))) new file mode 100644 --- /dev/null +++ b/docs/parser-hardening.md @@ -0,0 +1,50 @@ +# Parser Hardening + +`jawk` parses hostile AWK source and records. + +## Limits + +- `JAWK_MAX_PROGRAM_CHARS` caps inline and `-f` program text. Default: 1 MiB. +- `JAWK_MAX_RECORD_CHARS` caps input records. Default: 1 MiB. + +## Capability Gates + +- `JAWK_ALLOW_SYSTEM=1` enables `system()` and pipe redirections. +- `JAWK_ALLOW_FILE_IO=1` enables script-driven redirection and `getline` file + I/O. +- `JAWK_EXPOSE_ENVIRON=1` exposes a small allowlist of environment variables. + +## Regression Coverage + +`tests/test-parser.ss` covers normal parser forms, function/range/pipe AST +shapes, unterminated strings, unterminated regexes, malformed `for`, and +oversized direct parser input. + +`make parser-corpus` adds deterministic AWK parser corpus evidence: + +- `parser_corpus_status: documented` +- `parser_corpus_cases_minimum: 512` +- `parser_corpus_scope_status: functions-ranges-loops-io-malformed` + +The corpus covers generated function definitions, range patterns, loop forms, +getline syntax, large-but-bounded programs, and explicit malformed programs. +Malformed inputs must reject; accepted programs must produce a valid +`awk-program` with rule/function structures that satisfy AST predicates. + +`make test-cli-policy` covers disabled-by-default shell, pipe, file I/O, hidden +`ENVIRON`, and opt-in behavior for the compatibility paths. + +## Target Review Gates + +`make target-evidence` records production-only blockers that cannot be satisfied +by the local deterministic parser corpus alone: + +- `long_awk_parity_corpus_status` +- `external_parser_review_status` +- `external_runtime_review_status` +- `sandbox_policy_review_status` +- `script_file_io_review_status` +- `environment_exposure_review_status` + +Those markers default to `blocked-not-run` until a reviewed target proof file is +attached through `JAWK_TARGET_PROOF_FILE`. new file mode 100644 --- /dev/null +++ b/docs/release-evidence.md @@ -0,0 +1,64 @@ +# Release Evidence + +Run: + +```sh +make verify +make sbom +make reproducibility-report +make target-evidence +make release-evidence +``` + +`make release-evidence` writes `dist/release-evidence/` with: + +- git commit, status, and diff-stat; +- selected non-secret build environment values; +- security/import/parser-corpus/test logs; +- a CLI smoke output; +- SHA-256 hashes for the generated `jawk` binary and source inputs; +- `sbom/`, a dependency/toolchain manifest with source and release-input hashes; +- `reproducibility/report.txt`, comparing two clean local `jawk` builds after a + throwaway toolchain prewarm, plus the source manifest before/after the + measured comparison and repeated `parser_corpus_status`. +- `target-evidence/`, a local blocked-status record or a reviewed target proof + copied with a SHA-256 sidecar. + +Generated files under `dist/`, `.build/`, the root `jawk` binary, native object +files, and WPO outputs are excluded from source hashes and must remain untracked. + +Release evidence must remain host-neutral. Use status fields such as +`jerboa_home_status=present` instead of private toolchain paths, and `uname -srm` +instead of full host-identifying platform banners. + +## Target Production Proof + +`scripts/target-evidence.sh` writes `dist/target-evidence/status.txt`. +Without a proof file, local release evidence records: + +- `target_evidence_status: local-release-proof-recorded` +- `target_awk_proof_status: not-run` +- `long_awk_parity_corpus_status: blocked-not-run` +- `external_parser_review_status: blocked-not-run` +- `external_runtime_review_status: blocked-not-run` +- `sandbox_policy_review_status: blocked-not-run` +- `script_file_io_review_status: blocked-not-run` +- `environment_exposure_review_status: blocked-not-run` +- `production_awk_status: blocked-not-run` + +To attach reviewed target evidence, set `JAWK_TARGET_PROOF_FILE` to a file that +contains these exact markers: + +```text +long_awk_parity_corpus_status=reviewed +external_parser_review_status=reviewed +external_runtime_review_status=reviewed +sandbox_policy_review_status=reviewed +script_file_io_review_status=reviewed +environment_exposure_review_status=reviewed +sensitive_artifact_policy=no-input-record-output-captures-private-paths-or-host-data +``` + +Set `JAWK_REQUIRE_TARGET_PROOF=1` on production release hosts to fail closed +when the proof file is missing or incomplete. Proof files must not contain raw +input records, output captures, private paths, hostnames, or customer data. new file mode 100644 --- /dev/null +++ b/docs/threat-model.md @@ -0,0 +1,39 @@ +# jawk Threat Model + +`jawk` is a local AWK interpreter. It has no intended network behavior, daemon +mode, FFI, credential storage, or privileged operation. + +## Assets + +- Local filesystem integrity. +- Data passed to `jawk` on stdin or through input files. +- Release artifacts generated by `jerbuild`. +- The caller's environment variables. + +## Trust Boundaries + +- AWK program text is untrusted. +- Input records are untrusted. +- Regexes, field separators, redirection targets, and `getline` sources are + untrusted. +- CLI-selected input files and `-f` program files are operator-selected. +- Generated native binaries are build artifacts, not source. + +## Security Goals + +- No network access. +- No FFI in repository source. +- No broad direct Chez imports in production paths. +- `system()` and pipe redirections are disabled unless explicitly enabled. +- Script-driven file reads/writes are disabled unless explicitly enabled. +- `ENVIRON` is empty unless explicitly enabled. +- Program and record sizes are bounded. +- Release gates scan tracked and untracked source files and reject generated + artifacts. + +## Non-Goals + +- A sandbox around the host process. +- Full POSIX awk compatibility when secure defaults are active. +- Safe execution of arbitrary untrusted AWK programs with shell/file capabilities + enabled. --- a/lib/jerboa-awk/ast.sls +++ b/lib/jerboa-awk/ast.sls @@ -93,7 +93,7 @@ awk-func make-awk-func awk-func? awk-func-name awk-func-params awk-func-body) - (import (chezscheme) + (import (scheme) (std match2)) ;;; Program structure --- a/lib/jerboa-awk/builtins/io.sls +++ b/lib/jerboa-awk/builtins/io.sls @@ -7,7 +7,8 @@ awk-builtin-system awk-builtin-fflush) - (import (chezscheme) + (import (scheme) + (only (std security taint) check-untainted! safe-system) (jerboa-awk value) (jerboa-awk runtime) (jerboa-awk ast)) @@ -18,8 +19,10 @@ ;;; system(command) (define (awk-builtin-system env args) + (require-awk-shell-enabled! 'awk-builtin-system) (let* ((cmd (awk->string (car args))) - (status (system cmd))) + (_ (check-untainted! cmd 'awk-builtin-system)) + (status (safe-system cmd))) ;; system returns the exit status (make-awk-number (if (integer? status) status -1)))) --- a/lib/jerboa-awk/builtins/math.sls +++ b/lib/jerboa-awk/builtins/math.sls @@ -13,7 +13,7 @@ awk-builtin-rand awk-builtin-srand) - (import (chezscheme) + (import (scheme) (jerboa-awk value) (jerboa-awk runtime) (jerboa-awk ast)) --- a/lib/jerboa-awk/builtins/string.sls +++ b/lib/jerboa-awk/builtins/string.sls @@ -15,7 +15,7 @@ awk-builtin-toupper awk-sprintf) - (import (chezscheme) + (import (scheme) (only (std misc string) string-contains string-trim) (jerboa-awk value) (jerboa-awk runtime) --- a/lib/jerboa-awk/lexer.sls +++ b/lib/jerboa-awk/lexer.sls @@ -10,7 +10,7 @@ lexer-peeked-set! lexer-last-type-set! make-awk-lexer lex-next! lex-peek! lex-skip-newlines! lex-peek-ahead) - (import (chezscheme)) + (import (scheme)) ;;; Token (define-record-type tok (fields type value line column)) --- a/lib/jerboa-awk/main.sls +++ b/lib/jerboa-awk/main.sls @@ -4,8 +4,10 @@ (library (jerboa-awk main) (export main run-awk) - (import (chezscheme) + (import (scheme) (std match2) + (only (std os env) getenv) + (only (std security taint) check-untainted!) (jerboa-awk lexer) (jerboa-awk parser) (jerboa-awk ast) (jerboa-awk value) (jerboa-awk runtime) (jerboa-awk builtins string) (jerboa-awk builtins math) @@ -15,6 +17,33 @@ ;; Buffer for multi-char RS record splitting (define *record-buffer* '()) + (define default-max-program-chars (* 1024 1024)) + (define default-max-record-chars (* 1024 1024)) + + (define (configured-positive-int name default) + (let ((value (getenv name))) + (if value + (let ((n (string->number value))) + (if (and n (integer? n) (> n 0)) n default)) + default))) + + (define max-program-chars (configured-positive-int "JAWK_MAX_PROGRAM_CHARS" default-max-program-chars)) + (define max-record-chars (configured-positive-int "JAWK_MAX_RECORD_CHARS" default-max-record-chars)) + + (define (check-sized-string! who label value limit) + (when (> (string-length value) limit) + (error who (string-append label " exceeds configured limit") (string-length value) limit)) + value) + + (define (check-program-text! text) + (check-sized-string! 'jawk "program text" text max-program-chars)) + + (define (check-record-text! text) + (check-sized-string! 'jawk "input record" text max-record-chars)) + + (define (check-records! records) + (map check-record-text! records)) + ;;; CLI (define (main args) @@ -29,7 +58,8 @@ (define (run-awk args) (let-values (((program-text files var-assigns fs) (parse-args args))) - (let* ((prog (parse-awk-string program-text)) + (let* ((program-text (check-program-text! program-text)) + (prog (parse-awk-string program-text)) (env (make-initial-env))) ;; Apply -F (when fs (env-set! env 'FS (make-awk-string fs))) @@ -100,13 +130,17 @@ (env-set! env name (make-awk-string val)))))) (define (read-file-to-string filename) + (check-untainted! filename 'read-file-to-string) (call-with-input-file filename (lambda (port) - (let loop ((lines '())) + (let loop ((lines '()) (total 0)) (let ((line (get-line port))) (if (eof-object? line) - (string-join (reverse lines) "\n") - (loop (cons line lines)))))))) + (check-program-text! (string-join (reverse lines) "\n")) + (let ((next-total (+ total (string-length line) (if (null? lines) 0 1)))) + (when (> next-total max-program-chars) + (error 'read-file-to-string "program file exceeds JAWK_MAX_PROGRAM_CHARS" next-total max-program-chars)) + (loop (cons line lines) next-total)))))))) ;;; Rule execution @@ -134,9 +168,11 @@ (lambda (file) (if (string-contains file "=") (apply-var-assign! env file) - (call-with-input-file file - (lambda (port) - (process-stream env prog port file))))) + (begin + (check-untainted! file 'process-files) + (call-with-input-file file + (lambda (port) + (process-stream env prog port file)))))) files)) (define (process-stream env prog port filename) @@ -175,44 +211,50 @@ (cond ((string=? rs "\n") (let ((line (get-line port))) - (if (eof-object? line) #f line))) + (if (eof-object? line) #f (check-record-text! line)))) ((string=? rs "") - (let loop ((lines '()) (started? #f)) + (let loop ((lines '()) (started? #f) (total 0)) (let ((line (get-line port))) (cond ((eof-object? line) (if (null? lines) #f - (string-join (reverse lines) "\n"))) + (check-record-text! (string-join (reverse lines) "\n")))) ((= (string-length line) 0) (if started? - (string-join (reverse lines) "\n") - (loop lines #f))) + (check-record-text! (string-join (reverse lines) "\n")) + (loop lines #f total))) (else - (loop (cons line lines) #t)))))) + (let ((next-total (+ total (string-length line) (if (null? lines) 0 1)))) + (when (> next-total max-record-chars) + (error 'read-record "input record exceeds JAWK_MAX_RECORD_CHARS" next-total max-record-chars)) + (loop (cons line lines) #t next-total))))))) ((= (string-length rs) 1) (let ((sep (string-ref rs 0))) - (let loop ((chars '())) + (let loop ((chars '()) (count 0)) (let ((c (read-char port))) (cond ((eof-object? c) (if (null? chars) #f - (list->string (reverse chars)))) + (check-record-text! (list->string (reverse chars))))) ((char=? c sep) - (list->string (reverse chars))) + (check-record-text! (list->string (reverse chars)))) (else - (loop (cons c chars)))))))) + (let ((next-count (+ count 1))) + (when (> next-count max-record-chars) + (error 'read-record "input record exceeds JAWK_MAX_RECORD_CHARS" next-count max-record-chars)) + (loop (cons c chars) next-count)))))))) (else ;; Multi-char RS — regex split, buffered (if (pair? *record-buffer*) (let ((rec (car *record-buffer*))) (set! *record-buffer* (cdr *record-buffer*)) rec) - (let loop ((chars '())) + (let loop ((chars '()) (count 0)) (let ((c (read-char port))) (if (eof-object? c) (if (null? chars) #f (let* ((all (list->string (reverse chars))) - (records (pregexp-split rs all)) + (records (check-records! (pregexp-split rs all))) (records (if (and (pair? records) (string=? (last-element records) "")) (reverse (cdr (reverse records))) @@ -221,7 +263,10 @@