Security hardening and release readiness
ober
6fc0d2b4192922870e10ccc6bc2be8b674529d34
new file mode 100644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +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 system tools + run: | + set -eu + sudo apt-get update + sudo apt-get install -y --no-install-recommends build-essential ca-certificates curl git ripgrep + + - name: Install cargo-audit + run: | + set -eu + cargo install cargo-audit --locked + + - name: Verify + run: make verify 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,}|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 @@ -7,6 +7,13 @@ /jsecmon-analyze /jsecmon-collector /jsecmon-agent +/jsecmon-telemetry + +# Release evidence is generated per candidate. +/dist/ + +# Local CI/bootstrap cache. +/.jerboa/bin/ # Stray Chez compile artifacts (build-binary.ss cleans these; ignore as backstop). *.so new file mode 100644 --- /dev/null +++ b/.jerboa/security.json @@ -0,0 +1,97 @@ +{ + "version": 1, + "repo": "jerboa-secmon", + "extends": [ + "jerboa:daemon", + "jerboa:security-monitor", + "jerboa:endpoint-agent", + "jerboa:network-service", + "jerboa:static-binary", + "jerboa:ffi-boundary", + "jerboa:crypto-boundary", + "jerboa:sqlite-store" + ], + "paths": { + "production": [ + "Makefile", + "build-binary.ss", + "bin/**/*.ss", + "jsecmon/**/*.ss", + "typed/**/*.ss", + "tests/**/*.rs" + ], + "tests": [ + "examples/**", + "tests/**" + ], + "generated": [ + "build/**", + "dist/**", + "vendor/**", + "jsecmon-keygen", + "jsecmon-analyze", + "jsecmon-collector", + "jsecmon-agent", + "jsecmon-telemetry" + ], + "docs": [ + "README.md", + "SECURITY.md", + "docs/**", + "*.md" + ] + }, + "policy": { + "failOn": ["critical", "high"], + "crypto": { + "customCrypto": "deny", + "requireAuditedDependencies": true, + "requireKeySeparation": true, + "requireNonceRandomizationTests": true + }, + "network": { + "requireAuthenticatedTransport": true, + "requireLoopbackTelemetryTests": true, + "requireConnectionLimits": true, + "requireDeterministicShutdown": true + }, + "daemon": { + "requirePrivilegeDrop": true, + "requireSandboxPlan": true, + "requireReleaseEvidence": true, + "requireNoEmbeddedSecrets": true + }, + "ffi": { + "requireSafeSharedObjectLoading": true, + "requireCollectSafeBlockingCalls": true, + "requireOwnershipDocumentation": true + }, + "storage": { + "requireParameterizedSql": true, + "requireEncryptedLocalStore": true, + "requireTempCleanup": true + }, + "releaseEvidence": { + "targetLoadProof": { + "proofFileEnv": "JSECMON_TARGET_LOAD_PROOF_FILE", + "requireEnv": "JSECMON_REQUIRE_TARGET_LOAD_PROOF", + "requiredMarkers": [ + "production_load_status=current-run-recorded", + "agent_load_status=release-host-sustained-recorded", + "telemetry_load_status=release-host-sustained-recorded", + "collector_load_status=release-host-sustained-recorded" + ] + }, + "targetTelemetryProof": { + "proofFileEnv": "JSECMON_TARGET_TELEMETRY_PROOF_FILE", + "requireEnv": "JSECMON_REQUIRE_TARGET_TELEMETRY_PROOF", + "requiredMarkers": [ + "target_telemetry_status=target-evidence-recorded", + "privileged_monitor_status=target-evidence-recorded", + "listener_confinement_status=target-evidence-recorded" + ] + } + } + }, + "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 @@ -13,8 +13,15 @@ JERBOA_NATIVE_FEATURES ?= tls,crypto SCHEME ?= $(JERBOA)/.chez/bin/scheme BUILD ?= build/rust TYPED := $(wildcard typed/*.ss) +DIST_DIR ?= dist/release-evidence +REPRO_DIR ?= dist/reproducibility +SOAK_DIR ?= dist/soak +SOAK_TIMEOUT_SECONDS ?= 300 +SOAK_ITERATIONS ?= 8 +BINARY_SMOKE_TIMEOUT_SECONDS ?= 10 .PHONY: vendor-deps ensure-jerboa ensure-jsqlite rust test ffi-demo kernels-check triage-check triage-store-check analytics-check detect-check storage-check entity-check threats-check geoip-check sigma-check yaml-rules-check buffer-check dns-sniffer-check suspicious-check netconn-check kernmod-check selinux-check container-check dns-servers-check sensitive-path-check dtrace-parse-check dtrace-runtime-check stealth-check ebpf-events-check ebpf-runtime-check proc-linux-check freebsd-parse-check event-meta-check config-check privdrop-check event-danger-check persistence-check file-change-check webshell-check platform-mounts-check analyze-cli-check collector-cli-check event-summary-check ioc-check frame-check correlate-check revshell-check cron-check logtamper-check detection-rules-check daemon-telemetry-check mux-telemetry-check ipaddr-check auth-check lolbin-check dga-check calendar-check monitor-process-check monitor-network-check monitor-files-check monitor-auth-check monitor-kernel-check monitor-cron-check monitor-container-check monitor-rootkit-check monitor-podman-check monitor-selinux-check monitor-lateral-check monitor-webshell-check monitor-revshell-check monitor-persistence-check monitor-logtamper-check monitor-dns-check monitor-manager-check event-json-check collector-check protocol-check event-codec-check local-store-check collector-pull-check agent-server-check checks native-runtime keygen analyze collector agent telemetry binaries clean +.PHONY: security audit verify reproducibility-report release-evidence soak-check soak-evidence binary-smoke # Combined libdir path so sibling libraries `(jsecmon ...)` resolve to ./jsecmon # (a second --libdirs would replace, not append, the jerboa one). LIBDIRS := "$(CURDIR):$(JSQLITE_SRC):$(JERBOA)/lib" @@ -624,6 +631,62 @@ checks: kernels-check ensure-jsqlite $(SCHEME) --libdirs $(LIBDIRS) --script examples/event_json_check.ss $(LOADER_ENV) $(SCHEME) --libdirs $(LIBDIRS) --script examples/collector_check.ss +# ── Release/security gates ─────────────────────────────────────────────────── + +security: + scripts/security-check.sh + +audit: rust + @if ! cargo audit --version >/dev/null 2>&1; then \ + echo "cargo audit is required. Install with: cargo install cargo-audit --locked"; \ + exit 1; \ + fi + cargo audit --file "$(BUILD)/Cargo.lock" -D warnings + +soak-check: rust ensure-jsqlite + cd $(BUILD) && cargo build --release + @i=1; while [ "$$i" -le "$(SOAK_ITERATIONS)" ]; do \ + echo "soak iteration $$i: agent poll server"; \ + $(LOADER_ENV) $(SCHEME) --libdirs $(LIBDIRS) --script examples/agent_server_check.ss; \ + echo "soak iteration $$i: mux telemetry listener"; \ + $(LOADER_ENV) $(SCHEME) --libdirs $(LIBDIRS) --script examples/mux_telemetry_check.ss; \ + echo "soak iteration $$i: collector persistence loop"; \ + $(LOADER_ENV) $(SCHEME) --libdirs $(LIBDIRS) --script examples/collector_check.ss; \ + i=$$((i + 1)); \ + done + +soak-evidence: + SOAK_DIR="$(SOAK_DIR)" SOAK_ITERATIONS="$(SOAK_ITERATIONS)" SOAK_TIMEOUT_SECONDS="$(SOAK_TIMEOUT_SECONDS)" MAKE="$(MAKE)" bash scripts/soak-evidence.sh + +verify: security test checks audit + +reproducibility-report: + REPO_ROOT="$(CURDIR)" REPRO_DIR="$(REPRO_DIR)" MAKE="$(MAKE)" JERBOA="$(JERBOA)" JSQLITE_REPO="$(JSQLITE_REPO)" JSQLITE_SRC="$(JSQLITE_SRC)" SCHEME="$(SCHEME)" sh scripts/reproducibility-report.sh + +binary-smoke: + REPO_ROOT="$(CURDIR)" BINARY_SMOKE_TIMEOUT_SECONDS="$(BINARY_SMOKE_TIMEOUT_SECONDS)" sh scripts/binary-smoke.sh + +release-evidence: verify reproducibility-report + rm -rf "$(DIST_DIR)" + mkdir -p "$(DIST_DIR)" + $(MAKE) --no-print-directory soak-evidence + REPO_ROOT="$(CURDIR)" BINARY_SMOKE_TIMEOUT_SECONDS="$(BINARY_SMOKE_TIMEOUT_SECONDS)" sh scripts/binary-smoke.sh > "$(DIST_DIR)/binary-smoke.txt" + rm -rf "$(DIST_DIR)/soak" + cp -R "$(SOAK_DIR)" "$(DIST_DIR)/soak" + cp "$(SOAK_DIR)/soak-check.txt" "$(DIST_DIR)/soak-check.txt" + git rev-parse HEAD > "$(DIST_DIR)/git-commit.txt" + git status --short > "$(DIST_DIR)/git-status.txt" + uname -a > "$(DIST_DIR)/build-environment.txt" + "$(SCHEME)" --version >> "$(DIST_DIR)/build-environment.txt" + cargo metadata --manifest-path "$(BUILD)/Cargo.toml" --locked --format-version 1 > "$(DIST_DIR)/cargo-metadata-typed.json" + cargo audit --file "$(BUILD)/Cargo.lock" -D warnings > "$(DIST_DIR)/rustsec-typed.txt" + shasum -a 256 jsecmon-keygen jsecmon-analyze jsecmon-collector jsecmon-agent jsecmon-telemetry > "$(DIST_DIR)/binaries-sha256.txt" + shasum -a 256 Makefile build-binary.ss scripts/build-binaries.sh scripts/binary-smoke.sh scripts/security-check.sh scripts/reproducibility-report.sh scripts/soak-evidence.sh .jerboa/security.json SECURITY.md docs/threat-model.md docs/deployment-security.md docs/release-evidence.md > "$(DIST_DIR)/release-inputs-sha256.txt" + { for bin in jsecmon-keygen jsecmon-analyze jsecmon-collector jsecmon-agent jsecmon-telemetry; do echo "== $$bin =="; if command -v otool >/dev/null 2>&1; then otool -L "$$bin"; elif command -v ldd >/dev/null 2>&1; then ldd "$$bin"; else echo "no dynamic-linkage inspector found"; fi; done; } > "$(DIST_DIR)/native-linkage.txt" + rm -rf "$(DIST_DIR)/reproducibility" + cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility" + test "$$(grep '^status=' "$(DIST_DIR)/reproducibility/report.txt" | cut -d= -f2)" = "match" + # ── Native binaries ─────────────────────────────────────────────────────────── # The shippable artifacts are compiled, self-contained executables — never # `scheme --script` launchers. build-binary.ss whole-program-compiles a bin/*.ss @@ -637,14 +700,14 @@ native-runtime: ensure-jerboa keygen: rust ensure-jsqlite cd $(BUILD) && cargo build --release - JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/keygen.ss jsecmon-keygen + JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" JSECMON_BINARY_WPO=0 $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/keygen.ss jsecmon-keygen # secmon-analyze: query/detect/triage/risk over the SQLite store. Statically # links the Rust kernels (lolbin/dga scoring, calendar) like keygen; SQLite is # provided by vendored jsqlite. analyze: rust native-runtime ensure-jsqlite cd $(BUILD) && cargo build --release - JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/analyze.ss jsecmon-analyze + JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/analyze.ss jsecmon-analyze # secmon-collector: pull/status/watch over the PSK-encrypted protocol; decrypts # ECIES events and prints (human/NDJSON) and/or stores them. Statically links the @@ -652,17 +715,17 @@ analyze: rust native-runtime ensure-jsqlite # SQLite is available through vendored jsqlite when --db is used. collector: rust native-runtime ensure-jsqlite cd $(BUILD) && cargo build --release - JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/collector.ss jsecmon-collector + JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" JSECMON_BINARY_WPO=0 $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/collector.ss jsecmon-collector # secmon-agent: Linux polling monitors + encrypted event buffer + poll server. # It loads the collector public key and PSK at runtime, never embeds secrets. agent: rust native-runtime ensure-jsqlite cd $(BUILD) && cargo build --release - JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/agent.ss jsecmon-agent + JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/agent.ss jsecmon-agent telemetry: rust native-runtime ensure-jsqlite cd $(BUILD) && cargo build --release - JERBOA_HOME="$(JERBOA)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/telemetry.ss jsecmon-telemetry + JERBOA_HOME="$(JERBOA)" JSQLITE_SRC="$(JSQLITE_SRC)" $(SCHEME) --libdirs $(LIBDIRS) --script build-binary.ss bin/telemetry.ss jsecmon-telemetry # ── Experimental: Typed Jerboa → LLVM IR native backend ────────────────────── # @@ -710,7 +773,8 @@ llvmir-clean: rm -rf $(LLVMIR_DIR) # All shippable binaries. -binaries: keygen analyze collector agent telemetry +binaries: + REPO_ROOT="$(CURDIR)" JERBOA="$(JERBOA)" JSQLITE_REPO="$(JSQLITE_REPO)" JSQLITE_SRC="$(JSQLITE_SRC)" SCHEME="$(SCHEME)" BUILD="$(BUILD)" MAKE="$(MAKE)" sh scripts/build-binaries.sh clean: rm -rf $(BUILD) $(LLVMIR_DIR) new file mode 100644 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy + +This repository is part of the Jerboa ecosystem. Treat it as experimental unless the README and release notes explicitly state a stronger support level. + +Production claims require the release gates tracked in `~/Release-plan.md` and `~/mine/jerboa-production-readiness.md` to be complete for this repository. + +Secmon-specific security documentation: + +- `docs/threat-model.md` +- `docs/deployment-security.md` +- `docs/release-evidence.md` + +## Supported Status + +No public production-support commitment exists yet. Security-sensitive releases must be cut from a clean checkout after: + +- `git status --short` shows only intentional release changes. +- A secret scan is clean for the working tree, and history has been reviewed or intentionally reset before first public release. +- The documented build and test commands pass. +- Dependency and native-code audits are clean, or accepted risks are documented. +- Any FFI, filesystem, network, shell, or credential-handling behavior is documented. +- Release evidence includes explicit local soak and target-OS production load + status. `production_load_status=blocked-not-run` is a production blocker. + Reviewed target proof may be attached with `JSECMON_TARGET_LOAD_PROOF_FILE` + and `JSECMON_TARGET_TELEMETRY_PROOF_FILE`, but required proof must be paired + with `JSECMON_REQUIRE_TARGET_LOAD_PROOF=1` or + `JSECMON_REQUIRE_TARGET_TELEMETRY_PROOF=1` and fail closed as + `local_loopback_status=blocked-target-proof` if missing or marker-incomplete. +- Release evidence includes `binary-smoke.txt`; every shipped `jsecmon-*` + executable must pass its no-secret runtime smoke path, and macOS executables + must pass code-signature verification. +- Agent and telemetry listener sockets use Secmon's TCP runtime wrapper, which + verifies nonblocking mode and sets close-on-exec before background accept + loops start. + +## Security Expectations + +- Do not commit secrets, private keys, tokens, production `.env` files, operational hostnames, customer/user data, or private infrastructure details. +- Do not implement cryptography directly. Use audited libraries or delegated components. +- Prefer Rust for byte-level parsing, FFI boundaries, crypto-adjacent integration, and sandbox setup. +- Prefer Jerboa for policy, orchestration, configuration, routing, tests, and high-level behavior. +- Document file I/O, shell execution, network access, FFI ownership/lifetime rules, and credential storage before publication. +- Daemons and hostile-input parsers require threat models, fuzz harnesses, regression corpora, resource limits, sandboxing documentation, and review before public production use. +- For `jsecmon-agent` and `jsecmon-telemetry`, never expose default listeners directly to the public Internet. Bind to a private management network or loopback/VPN path and enforce host firewall rules. +- Treat SQLite stores, WAL/SHM sidecars, release evidence, and analyzer output as sensitive operational records. + +## 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/agent.ss +++ b/bin/agent.ss @@ -1,7 +1,7 @@ #!chezscheme ;;; jsecmon-agent — monitor locally, ECIES-buffer events, serve collector pulls. -(import (except (chezscheme) +(import (except (scheme) make-hash-table hash-table? sort sort! printf fprintf --- a/bin/analyze.ss +++ b/bin/analyze.ss @@ -21,7 +21,7 @@ ;;; in via (command-line-arguments) = (command db remaining…), i.e. secmon's ;;; args[1..]. SQLite persistence resolves through vendored jsqlite. -(import (except (chezscheme) +(import (except (scheme) make-hash-table hash-table? sort sort! printf fprintf @@ -66,7 +66,9 @@ (only (jsecmon analyze-cli) parse-duration-ms parse-alert-sink parse-alert-sinks parse-flag-value has-flag is-json-format) - (only (jsecmon lolbin) score-json-cmdline lol-label-summary)) + (only (jsecmon lolbin) score-json-cmdline lol-label-summary) + (only (std pkg util) mkdir-p) + (only (std os aproc) aproc-run/status*)) ;; ── tiny output helpers ─────────────────────────────────────────────────────── (def (println . parts) (for-each display parts) (newline)) @@ -74,6 +76,9 @@ (let ((p (current-error-port))) (for-each (lambda (x) (display x p)) parts) (newline p))) (def (die . parts) (apply eprintln parts) (exit 1)) +(def (write-text-file/replace path writer) + (call-with-port (open-file-output-port path (file-options no-fail) (buffer-mode block)) + writer)) (def (sev-marker s) (cond ((string=? s "critical") "!!!") ((string=? s "high") "!! ") @@ -299,31 +304,30 @@ (let ((ids (compute-triaged-ids db filter))) (values (filter-set filter "exclude_event_ids" ids) (length ids))))) -;; ── alert sinks (stdout / file / webhook / syslog) ──────────────────────────── -(def (shell-quote s) (string-append "'" (string-join (string-split s #\') "'\\''") "'")) - (def (sink-dispatch sink line) (case (car sink) ((stdout) (println line) (ok #t)) ((file) (let* ((path (cadr sink)) (old (if (file-exists? path) (read-file-string path) ""))) - (try (begin (call-with-output-file path - (lambda (o) (display old o) (display line o) (newline o)) 'replace) + (try (begin (write-text-file/replace path + (lambda (o) (display old o) (display line o) (newline o))) (ok #t)) (catch (e) (err (str "write " path ": " e)))))) ((webhook syslog) - (let ((tmp (str "/tmp/jsecmon-alert-" (now-ms) "-" (random 100000)))) - (call-with-output-file tmp (lambda (o) (display line o)) 'replace) - (let ((rc (system + (let-values (((out err rc) (if (eq? (car sink) 'webhook) - (str "curl -sS --max-time 10 -X POST " - "-H 'Content-Type: application/json' --data-binary @" - tmp " " (shell-quote (cadr sink)) " >/dev/null") - (str "logger -t " (shell-quote (cadr sink)) - " -p auth.warning -f " tmp))))) - (when (file-exists? tmp) (delete-file tmp)) - (if (= rc 0) (ok #t) (err (str (car sink) " exited " rc)))))) + (aproc-run/status* + (list "curl" "-sS" "--max-time" "10" "-X" "POST" + "-H" "Content-Type: application/json" + "--data-binary" "@-" (cadr sink)) + 'stdin: line + 'timeout-ms: 15000) + (aproc-run/status* + (list "logger" "-t" (cadr sink) "-p" "auth.warning") + 'stdin: line + 'timeout-ms: 15000)))) + (if (= rc 0) (ok #t) (err (str (car sink) " exited " rc))))) (else (err "unknown sink")))) (def (dispatch-anomalies sinks anomalies) @@ -1009,7 +1013,7 @@ (let ((input (car args)) (out-dir (or (parse-flag-value args "--out") (die "--out <yaml-dir> is required"))) (dry (has-flag args "--dry-run"))) - (unless dry (system (str "mkdir -p " (shell-quote out-dir)))) + (unless dry (mkdir-p out-dir)) (let* ((files (if (file-directory? input) (sigma-files input) (list input))) (results (map (lambda (f) (cons f (let ((txt (try (read-file-string f) (catch (e) #f)))) @@ -1025,8 +1029,8 @@ (if dry (begin (println "# " path " -> " (imported-rule-name rule) ".yml") (println (imported-rule-yaml rule))) - (call-with-output-file (path-join out-dir (str (imported-rule-name rule) ".yml")) - (lambda (o) (display (imported-rule-yaml rule) o)) 'replace))) + (write-text-file/replace (path-join out-dir (str (imported-rule-name rule) ".yml")) + (lambda (o) (display (imported-rule-yaml rule) o))))) (begin (set! skip (+ skip 1)) (eprintln "skip " path ": " (unwrap-err res)))))) results) (eprintln ok-n "/" total " rules imported, " skip " skipped" (if dry " (dry-run)" ""))))) --- a/bin/collector.ss +++ b/bin/collector.ss @@ -29,7 +29,7 @@ ;;; observable behaviour without a fiber runtime. Linux/live-only; the codecs it ;;; rests on are vector-tested, this orchestration is exercised by hand. -(import (except (chezscheme) +(import (except (scheme) make-hash-table hash-table? sort sort! printf fprintf @@ -74,6 +74,10 @@ (let ((p (current-error-port))) (for-each (lambda (x) (display x p)) parts) (newline p))) (def (die . parts) (apply eprintln parts) (exit 1)) +(def (raise-message who msg) + (raise (condition + (make-who-condition who) + (make-message-condition msg)))) ;; ── key loading (secmon load_key, minus the compile-time embedded fallback) ─── ;; env var first; a short value (<64) starting with / or . is a key-file path, @@ -112,9 +116,9 @@ (if (>= off n) buf (let ((chunk (get-bytevector-n in (- n off)))) (when (or (eof-object? chunk) (not (bytevector? chunk))) - (error 'recv-message "connection closed")) + (raise-message 'recv-message "connection closed")) (let ((k (bytevector-length chunk))) - (when (= k 0) (error 'recv-message "connection closed")) + (when (= k 0) (raise-message 'recv-message "connection closed")) (bytevector-copy! chunk 0 buf off k) (loop (+ off k)))))))) @@ -125,7 +129,7 @@ ;; → (ok message) | (err string); throws only on a closed/oversize socket. (def (recv-message in transport-key) (let ((len (frame-read-length (read-exact in 4)))) - (when (> len *max-msg*) (error 'recv-message "message too large")) + (when (> len *max-msg*) (raise-message 'recv-message "message too large")) (let ((dec (transport-decrypt transport-key (read-exact in len)))) (if (not dec) (err "transport decryption failed") (message-from-bytes dec))))) @@ -140,9 +144,11 @@ (transport-key (derive-transport-key psk))) (let-values (((in out) (tcp-connect-binary addr port))) (let ((r (recv-message in transport-key))) - (when (err? r) (error 'connect (unwrap-err r))) + (when (err? r) + (raise-message 'connect (unwrap-err r))) (let ((msg (unwrap r))) - (unless (eq? (car msg) 'challenge) (error 'connect "expected challenge")) + (unless (eq? (car msg) 'challenge) + (raise-message 'connect "expected challenge")) (send-message out transport-key (list 'challenge-response (respond-to-challenge auth-key (cadr msg)))) (make-client in out transport-key))))))) @@ -154,25 +160,25 @@ (def (client-request c msg) ;; send one request, await one reply (send-message (client-out c) (client-tk c) msg) (let ((r (recv-message (client-in c) (client-tk c)))) - (if (err? r) (error 'request (unwrap-err r)) (unwrap r)))) + (if (err? r) (raise-message 'request (unwrap-err r)) (unwrap r)))) ;; GetEventsAfter → the SerializedEvent list (or signal auth/error). (def (client-get-events-after c seq) (let ((msg (client-request c (list 'request (list 'get-events-after seq))))) (cond ((and (eq? (car msg) 'response) (eq? (car (cadr msg)) 'events)) (cdr (cadr msg))) - ((eq? (car msg) 'auth-failed) (error 'request "authentication failed")) - ((eq? (car msg) 'error) (error 'request (cadr msg))) - (#t (error 'request "unexpected response"))))) + ((eq? (car msg) 'auth-failed) (raise-message 'request "authentication failed")) + ((eq? (car msg) 'error) (raise-message 'request (cadr msg))) + (#t (raise-message 'request "unexpected response"))))) ;; Status → (buffered latest-seq uptime db-count db-latest). (def (client-get-status c) (let ((msg (client-request c (list 'request (list 'status))))) (cond ((and (eq? (car msg) 'response) (eq? (car (cadr msg)) 'status)) (cdr (cadr msg))) - ((eq? (car msg) 'auth-failed) (error 'request "authentication failed")) - ((eq? (car msg) 'error) (error 'request (cadr msg))) - (#t (error 'request "unexpected response"))))) + ((eq? (car msg) 'auth-failed) (raise-message 'request "authentication failed")) + ((eq? (car msg) 'error) (raise-message 'request (cadr msg))) + (#t (raise-message 'request "unexpected response"))))) ;; ── per-event decrypt → the monitor row-hash (or err) ───────────────────────── (def (decrypt-event recipient-secret sev) --- a/bin/keygen.ss +++ b/bin/keygen.ss @@ -12,7 +12,7 @@ ;;; loads its public key + PSK from config paths/env at runtime (see config.ss), ;;; it does not embed them at compile time the way the Rust agent did. -(import (except (chezscheme) +(import (except (scheme) make-hash-table hash-table? sort sort! printf fprintf --- a/bin/telemetry.ss +++ b/bin/telemetry.ss @@ -5,7 +5,7 @@ ;;; telemetry, stores accepted rows in the jsecmon events database, and returns ;;; encrypted ACK/ERROR mux frames. -(import (except (chezscheme) +(import (except (scheme) make-hash-table hash-table? sort sort! printf fprintf @@ -23,7 +23,7 @@ mux-telemetry-server-start! mux-telemetry-server-port)) -(def *default-listen* "0.0.0.0:31338") +(def *default-bind* "0.0.0.0:31338") (def (println . parts) (for-each display parts) (newline)) (def (eprintln . parts) @@ -40,7 +40,7 @@ (eprintln " SECMON_PSK Pre-shared key (hex) or path to key file") (eprintln " PSK Alias accepted from keygen output") (eprintln " SECMON_PSK_FILE Path to PSK hex file") - (eprintln " SECMON_TELEMETRY_LISTEN Listen address, default " *default-listen*) + (eprintln " SECMON_TELEMETRY_LISTEN Listen address, default " *default-bind*) (eprintln " SECMON_DB_PATH Events DB path")) (def (arg? flag argv) (and (member flag argv) #t)) @@ -119,7 +119,7 @@ (exit 0)) (let* ((listen (or (flag-value argv "--listen") (getenv "SECMON_TELEMETRY_LISTEN") - *default-listen*)) + *default-bind*)) (db-path (or (flag-value argv "--db") (local-db-path getenv (runtime-platform)))) (psk (decode-32 "PSK" (load-psk-hex))) --- a/build-binary.ss +++ b/build-binary.ss @@ -23,7 +23,8 @@ ;;; (e.g. (jerboa core)) come back in compile-whole-program's "missing" list and ;;; are bundled into the boot file instead. -(import (chezscheme)) +(import (scheme) + (only (std security taint) safe-delete-file)) ;; ── args ───────────────────────────────────────────────────────────────────── (define args (cdr (command-line))) @@ -36,13 +37,181 @@ entry)) (define prog-so (string-append output "-all.so")) +(define (write-file/replace path writer) + (call-with-port (open-file-output-port path (file-options no-fail) + (buffer-mode block) + (native-transcoder)) + writer)) + +(define (try-load-shared-object name) + (guard (e [(condition? e) #f]) + (load-shared-object name) + #t)) + +(define tracked-foreign-alloc foreign-alloc) + +(define *process-ffi-loaded?* #f) +(define c-posix-spawnp #f) +(define c-waitpid #f) + +(define (resolve-process-ffi!) + (unless *process-ffi-loaded?* + (or (try-load-shared-object #f) + (try-load-shared-object "/usr/lib/libSystem.B.dylib") + (try-load-shared-object "libc.so.6") + (try-load-shared-object "libc.so.7") + (try-load-shared-object "libc.so")) + (set! c-posix-spawnp + (guard (e [(condition? e) #f]) + (foreign-procedure "posix_spawnp" + (void* string void* void* void* void*) + int))) + (set! c-waitpid + (guard (e [(condition? e) #f]) + (foreign-procedure __collect_safe + "waitpid" (int void* int) int))) + (set! *process-ffi-loaded?* + (and c-posix-spawnp c-waitpid))) + *process-ffi-loaded?*) + +(define (alloc-cstring s) + ;; The returned pointer is owned by the argv bundle and released by + ;; free-c-argv! after posix_spawnp returns. + (let* ([bv (string->utf8 s)] + [len (bytevector-length bv)] + [ptr (tracked-foreign-alloc (+ len 1))]) + (let loop ([i 0]) + (when (< i len) + (foreign-set! 'unsigned-8 ptr i (bytevector-u8-ref bv i)) + (loop (+ i 1)))) + (foreign-set! 'unsigned-8 ptr len 0) + ptr)) + +(define (alloc-c-argv argv) + (let* ([argc (length argv)] + [ptr-size (foreign-sizeof 'void*)] + [argv-ptr (tracked-foreign-alloc (* (+ argc 1) ptr-size))]) + (let loop ([xs argv] [i 0] [allocated '()]) + (if (null? xs) + (begin + (foreign-set! 'void* argv-ptr (* i ptr-size) 0) + (cons argv-ptr (reverse allocated))) + (let ([arg-ptr (alloc-cstring (car xs))]) + (foreign-set! 'void* argv-ptr (* i ptr-size) arg-ptr) + (loop (cdr xs) (+ i 1) (cons arg-ptr allocated))))))) + +(define (free-c-argv! bundle) + (for-each foreign-free (cdr bundle)) + (foreign-free (car bundle))) + +(define child-environment-variable-names + '("HOME" + "PATH" + "TMPDIR" + "JERBOA_HOME" + "JSQLITE_SRC" + "JSQLITE_DIR" + "CHEZ_DIR" + "SCHEME" + "DYLD_LIBRARY_PATH" + "LD_LIBRARY_PATH" + "SDKROOT" + "DEVELOPER_DIR" + "MACOSX_DEPLOYMENT_TARGET" + "CARGO_HOME" + "RUSTUP_HOME" + "SSL_CERT_FILE" + "SSL_CERT_DIR")) + +(define (child-environment-assignments) + (let loop ([names child-environment-variable-names] [out '()]) + (if (null? names) + (reverse out) + (let ([value (getenv (car names))]) + (loop (cdr names) + (if value + (cons (string-append (car names) "=" value) out) + out)))))) + +(define (alloc-c-envp) + (alloc-c-argv (child-environment-assignments))) + +(define (wait-status->exit-code status) + (let ([signal (bitwise-and status #x7f)]) + (cond + [(= signal 0) (bitwise-arithmetic-shift-right status 8)] + [(= signal #x7f) 1] + [else (+ 128 signal)]))) + +(define (run-command/status argv) + (if (null? argv) + 1 + (if (not (resolve-process-ffi!)) + 255 + (let ([bundle (alloc-c-argv argv)] + [env-bundle (alloc-c-envp)] + [pid-ptr (tracked-foreign-alloc (foreign-sizeof 'int))] + [status-ptr (tracked-foreign-alloc (foreign-sizeof 'int))]) + (let ([spawn-rc + (c-posix-spawnp pid-ptr + (car argv) + 0 + 0 + (car bundle) + (car env-bundle))]) + (let ([rc + (if (= spawn-rc 0) + (let ([pid (foreign-ref 'int pid-ptr 0)]) + (let ([waited (c-waitpid pid status-ptr 0)]) + (if (= waited pid) + (wait-status->exit-code + (foreign-ref 'int status-ptr 0)) + 255))) + 255)]) + (free-c-argv! bundle) + (free-c-argv! env-bundle) + (foreign-free pid-ptr) + (foreign-free status-ptr) + rc)))))) + +(define (run-command label argv) + (let ([rc (run-command/status argv)]) + (unless (= rc 0) + (printf "Error: ~a failed (exit ~a)\n" label rc) + (exit 1)))) + +(define (bounded-positive-integer value default) + (if value + (let ([n (string->number value)]) + (if (and (integer? n) (> n 0) (< n 10)) n default)) + default)) + +(define (wpo-helper-attempts) + (bounded-positive-integer (getenv "JSECMON_WPO_HELPER_ATTEMPTS") 2)) + +(define (run-wpo-helper argv) + (let ([max-attempts (wpo-helper-attempts)]) + (let loop ([attempt 1]) + (let ([rc (run-command/status argv)]) + (cond + [(= rc 0) #t] + [(< attempt max-attempts) + (printf " WPO helper failed (exit ~a); retrying (~a/~a)\n" + rc (+ attempt 1) max-attempts) + (when (file-exists? prog-so) (safe-delete-file prog-so)) + (when (file-exists? wpo-missing-file) (safe-delete-file wpo-missing-file)) + (loop (+ attempt 1))] + [else + (printf "Error: WPO helper failed (exit ~a)\n" rc) + (exit 1)]))))) + ;; ── helper: binary file → C unsigned-char array header ──────────────────────── (define (file->c-header input-path output-path array-name size-name) (let* ([port (open-file-input-port input-path)] [data (get-bytevector-all port)] [size (bytevector-length data)]) (close-port port) - (call-with-output-file output-path + (write-file/replace output-path