Add LoRA SBOM and source reproducibility evidence
ober
273310ace08dc5fd781e0f30c638485a8b61022b
--- a/.jerboa/security.json +++ b/.jerboa/security.json @@ -18,10 +18,22 @@ "process": { "shellInterpolation": "deny" }, "network": { "requireTimeouts": true }, "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false }, - "secrets": { "scanGeneratedCorpora": true, "forbidTrainingSecrets": true } + "secrets": { "scanGeneratedCorpora": true, "forbidTrainingSecrets": true }, + "releaseEvidence": { + "sbom": "source-config-training-manifests", + "reproducibility": "source-and-manifest-only", + "modelArtifacts": "blocked-private-split" + }, + "targetEvidence": { + "splitProvenanceProof": "fail-closed", + "maxProofBytes": 65536, + "unsafeProofMaterial": "reject-before-copy" + } }, "required_gates": [ "make verify", + "make sbom", + "make reproducibility-report", "make release-evidence" ], "public_release_blockers": [ --- a/Makefile +++ b/Makefile @@ -3,8 +3,10 @@ SHELL := /bin/sh PYTHON ?= python3 EVIDENCE_DIR ?= dist/release-evidence PROVENANCE_DIR ?= dist/provenance +SBOM_DIR ?= dist/sbom +REPRO_DIR ?= dist/reproducibility -.PHONY: security test provenance-evidence verify release-evidence clean +.PHONY: security test provenance-evidence sbom reproducibility-report verify release-evidence clean security: sh scripts/security-check.sh @@ -19,14 +21,27 @@ provenance-evidence: JLORA_REQUIRE_SPLIT_PROVENANCE_PROOF="$(JLORA_REQUIRE_SPLIT_PROVENANCE_PROOF)" \ sh scripts/provenance-evidence.sh -verify: security test provenance-evidence +sbom: + JLORA_SBOM_DIR="$(SBOM_DIR)" \ + PYTHON="$(PYTHON)" \ + sh scripts/sbom.sh + +reproducibility-report: + JLORA_REPRO_DIR="$(REPRO_DIR)" \ + sh scripts/reproducibility-report.sh + +verify: security test provenance-evidence sbom reproducibility-report + grep -q '^status=match$$' "$(REPRO_DIR)/report.txt" release-evidence: - rm -rf $(EVIDENCE_DIR) + rm -rf $(EVIDENCE_DIR) $(PROVENANCE_DIR) $(SBOM_DIR) $(REPRO_DIR) mkdir -p $(EVIDENCE_DIR) $(MAKE) security >$(EVIDENCE_DIR)/security.log 2>&1 $(MAKE) test >$(EVIDENCE_DIR)/python-syntax.log 2>&1 $(MAKE) provenance-evidence >$(EVIDENCE_DIR)/provenance-evidence.log 2>&1 + $(MAKE) sbom >$(EVIDENCE_DIR)/sbom.log 2>&1 + $(MAKE) reproducibility-report >$(EVIDENCE_DIR)/reproducibility.log 2>&1 + grep -q '^status=match$$' "$(REPRO_DIR)/report.txt" git rev-parse HEAD >$(EVIDENCE_DIR)/git-commit.txt git status --short --ignored >$(EVIDENCE_DIR)/git-status-ignored.txt git diff --stat >$(EVIDENCE_DIR)/diff-stat.txt @@ -36,7 +51,12 @@ release-evidence: git ls-files --cached --others --exclude-standard | grep -E '(\.py|\.sh|\.yaml|\.toml|\.json|\.md|Modelfile|LICENSE|SECURITY.md)$$' | xargs shasum -a 256 >$(EVIDENCE_DIR)/source-sha256.txt rm -rf $(EVIDENCE_DIR)/provenance cp -R $(PROVENANCE_DIR) $(EVIDENCE_DIR)/provenance - { $(PYTHON) --version; uname -a; } >$(EVIDENCE_DIR)/build-env.txt + rm -rf $(EVIDENCE_DIR)/sbom + cp -R $(SBOM_DIR) $(EVIDENCE_DIR)/sbom + rm -rf $(EVIDENCE_DIR)/reproducibility + cp -R $(REPRO_DIR) $(EVIDENCE_DIR)/reproducibility + { echo "repo=jerboa-lora"; echo "platform=$$(uname -srm)"; $(PYTHON) --version; } >$(EVIDENCE_DIR)/build-env.txt + sh scripts/sanitize-evidence.sh "$(EVIDENCE_DIR)" clean: rm -rf dist --- a/docs/release-evidence.md +++ b/docs/release-evidence.md @@ -11,12 +11,18 @@ Evidence is written under `dist/release-evidence/` and records: - private security gate output; - tracked Python syntax-check output; - private model/data card and split-provenance evidence; +- SBOM-style source, config, training-data, docs, and release-control + manifests; +- source-only reproducibility evidence for the private split package; - git commit, status, and diff stat; - tracked plus untracked-but-not-ignored source/config hashes; - ignored artifact/state inventory. Passing evidence means this private workspace is controlled enough to continue internal work. It does not mean the repo, data, or weights are public-ready. +The reproducibility report intentionally records +`reproducibility_scope=source-and-manifest-only`; generated model weights remain +blocked private artifacts until a reviewed split package exists. The provenance evidence is expected to record `status=blocked-split-review-required` unless a separate reviewed split proof is attached. A required proof is supplied with: @@ -37,5 +43,7 @@ Required proof markers are: - `weight_publication_status=reviewed-or-blocked` - `sensitive_artifact_policy=no-tokens-private-endpoints-pod-state-or-private-training-records` -Missing, incomplete, or sensitive-looking required proof fails closed as -`status=blocked-split-provenance-proof`. +Missing, incomplete, oversized, host-private, or sensitive-looking required +proof fails closed as `status=blocked-split-provenance-proof`. The default +proof-size limit is 65,536 bytes and can be lowered with +`JLORA_SPLIT_PROVENANCE_PROOF_MAX_BYTES`. --- a/scripts/provenance-evidence.sh +++ b/scripts/provenance-evidence.sh @@ -7,10 +7,27 @@ cd "$ROOT" OUT=${JLORA_PROVENANCE_DIR:-${PROVENANCE_DIR:-dist/provenance}} PROOF_FILE=${JLORA_SPLIT_PROVENANCE_PROOF_FILE:-} REQUIRE_PROOF=${JLORA_REQUIRE_SPLIT_PROVENANCE_PROOF:-0} +MAX_PROOF_BYTES=${JLORA_SPLIT_PROVENANCE_PROOF_MAX_BYTES:-65536} +SANITIZER=${JLORA_EVIDENCE_SANITIZER:-scripts/sanitize-evidence.sh} rm -rf "$OUT" mkdir -p "$OUT" +if [ -x "$SANITIZER" ]; then + trap '"$SANITIZER" "$OUT"' EXIT +fi + +case "$MAX_PROOF_BYTES" in + ''|*[!0-9]*) + echo "provenance-evidence: JLORA_SPLIT_PROVENANCE_PROOF_MAX_BYTES must be a positive integer" >&2 + exit 1 + ;; +esac +if [ "$MAX_PROOF_BYTES" -lt 1 ]; then + echo "provenance-evidence: JLORA_SPLIT_PROVENANCE_PROOF_MAX_BYTES must be a positive integer" >&2 + exit 1 +fi + sha256_file() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" @@ -45,12 +62,21 @@ require_marker() { reject_sensitive_proof() { file=$1 + proof_size=$(wc -c < "$file" | tr -d ' ') + if [ "$proof_size" -gt "$MAX_PROOF_BYTES" ]; then + echo "provenance-evidence: proof larger than JLORA_SPLIT_PROVENANCE_PROOF_MAX_BYTES" >&2 + return 1 + fi if grep -E -i \ '(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|github_pat_|ghp_|sk-(ant-api03|proj|svcacct)-|AKIA[0-9A-Z]{16}|runpod[_-]?(api|token|key)|together[_-]?(api|token|key)|hf[_-]?(token|api)|endpoint[_-]?id|pod[_-]?id|ssh-rsa|ssh-ed25519)' \ "$file" >/dev/null 2>&1; then echo "provenance-evidence: proof contains sensitive-looking material" >&2 return 1 fi + if grep -E -i '(/Users/|~/mine|\$\(HOME\)/mine|git@|users-MacBook-Pro|uname[[:space:]]+-a)' "$file" >/dev/null 2>&1; then + echo "provenance-evidence: proof contains private local path or host material" >&2 + return 1 + fi } record_proof() { new file mode 100755 --- /dev/null +++ b/scripts/reproducibility-report.sh @@ -0,0 +1,69 @@ +#!/bin/sh +set -eu + +ROOT=${JLORA_REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)} +OUT=${JLORA_REPRO_DIR:-${REPRO_DIR:-"$ROOT/dist/reproducibility"}} +SANITIZER=${JLORA_EVIDENCE_SANITIZER:-scripts/sanitize-evidence.sh} + +rm -rf "$OUT" +mkdir -p "$OUT" +cd "$ROOT" + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" + else + shasum -a 256 "$1" + fi +} + +write_source_manifest() { + out=$1 + git ls-files | grep -E '(\.py$|\.sh$|\.ya?ml$|\.toml$|\.json$|\.jsonl$|\.md$|^Makefile$|^LICENSE$|^Modelfile|^\.github/|^\.jerboa/security\.json$)' \ + | LC_ALL=C sort | while IFS= read -r file; do + [ -f "$file" ] || continue + sha256_file "$file" + done > "$out" +} + +git ls-files | grep -E '(\.safetensors$|\.gguf$|\.npz$|\.bin$|\.pt$|\.pth$|^gguf/|^runpod-pipeline-final/|^mlx_adapters/|^jerboa-mlx-|^jerboa-v[0-9].*bf16/)' \ + | LC_ALL=C sort > "$OUT/tracked-model-artifacts.txt" || : > "$OUT/tracked-model-artifacts.txt" + +write_source_manifest "$OUT/source-manifest-first.sha256" +write_source_manifest "$OUT/source-manifest-second.sha256" + +first_manifest=$(sha256_file "$OUT/source-manifest-first.sha256" | awk '{print $1}') +second_manifest=$(sha256_file "$OUT/source-manifest-second.sha256" | awk '{print $1}') + +if [ "$first_manifest" = "$second_manifest" ]; then + source_manifest_status=match +else + source_manifest_status=mismatch +fi + +if [ -s "$OUT/tracked-model-artifacts.txt" ]; then + tracked_model_artifact_status=present-blocked +else + tracked_model_artifact_status=absent +fi + +case "$source_manifest_status:$tracked_model_artifact_status" in + match:absent) status=match ;; + *) status=blocked ;; +esac + +{ + echo "repo=jerboa-lora" + echo "lane=D/split" + echo "reproducibility_scope=source-and-manifest-only" + echo "source_manifest_status=$source_manifest_status" + echo "tracked_model_artifact_status=$tracked_model_artifact_status" + echo "model_artifact_publication_status=blocked-private-split" + echo "artifact_archive_status=hashes-only" + echo "scratch_build_logs_status=not-archived" + echo "status=$status" +} | tee "$OUT/report.txt" + +if [ -x "$SANITIZER" ]; then + "$SANITIZER" "$OUT" +fi new file mode 100755 --- /dev/null +++ b/scripts/sanitize-evidence.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -eu + +if [ "$#" -eq 0 ]; then + set -- dist/release-evidence +fi + +for evidence_dir in "$@"; do + if [ ! -d "$evidence_dir" ]; then + continue + fi + + find "$evidence_dir" -type f -print | while IFS= read -r file; do + perl -0pi -e ' + s#/Users/[^[:space:]\x22\x27()]+#<local-path>#g; + if (defined $ENV{HOME} && length $ENV{HOME}) { + my $home = quotemeta($ENV{HOME}); + s#$home#<home>#g; + } + s#~/mine#<local-path>#g; + s#\$\((HOME)\)/mine#<local-path>#g; + s#users-MacBook-Pro#<host>#g; + s#git\@#git-at-#g; + ' "$file" + done +done new file mode 100755 --- /dev/null +++ b/scripts/sbom.sh @@ -0,0 +1,98 @@ +#!/bin/sh +set -eu + +ROOT=${JLORA_REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)} +OUT=${JLORA_SBOM_DIR:-${SBOM_DIR:-"$ROOT/dist/sbom"}} +PYTHON_BIN=${PYTHON:-python3} +SANITIZER=${JLORA_EVIDENCE_SANITIZER:-scripts/sanitize-evidence.sh} + +rm -rf "$OUT" +mkdir -p "$OUT" +cd "$ROOT" + +if [ -x "$SANITIZER" ]; then + trap '"$SANITIZER" "$OUT"' EXIT +fi + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" + else + shasum -a 256 "$1" + fi +} + +tracked_files() { + git ls-files +} + +write_list() { + pattern=$1 + out=$2 + tracked_files | grep -E "$pattern" | LC_ALL=C sort > "$out" || : > "$out" +} + +hash_list() { + list=$1 + out=$2 + : > "$out" + while IFS= read -r file; do + [ -n "$file" ] || continue + [ -f "$file" ] || continue + sha256_file "$file" >> "$out" + done < "$list" +} + +write_list '\.py$' "$OUT/python-files.txt" +write_list '\.sh$' "$OUT/shell-files.txt" +write_list '(\.ya?ml$|\.toml$|\.json$|^Modelfile)' "$OUT/config-files.txt" +write_list '\.jsonl$' "$OUT/training-jsonl-files.txt" +write_list '(^README\.md$|^SECURITY\.md$|^TODO\.md$|^TRAINING_.*\.md$|^docs/.*\.md$|.*-findings\.md$)' "$OUT/doc-files.txt" +write_list '(^Makefile$|^LICENSE$|^\.github/|^\.jerboa/security\.json$)' "$OUT/release-control-files.txt" +write_list '(\.safetensors$|\.gguf$|\.npz$|\.bin$|\.pt$|\.pth$|^gguf/|^runpod-pipeline-final/|^mlx_adapters/|^jerboa-mlx-|^jerboa-v[0-9].*bf16/)' "$OUT/tracked-model-artifacts.txt" + +hash_list "$OUT/python-files.txt" "$OUT/python.sha256" +hash_list "$OUT/shell-files.txt" "$OUT/shell.sha256" +hash_list "$OUT/config-files.txt" "$OUT/config.sha256" +hash_list "$OUT/training-jsonl-files.txt" "$OUT/training-jsonl.sha256" +hash_list "$OUT/doc-files.txt" "$OUT/docs.sha256" +hash_list "$OUT/release-control-files.txt" "$OUT/release-control.sha256" + +if [ -f .jerboa/security.json ]; then + sha256_file .jerboa/security.json > "$OUT/jerboa-security.sha256" +else + : > "$OUT/jerboa-security.sha256" +fi + +{ + echo "repo=jerboa-lora" + echo "lane=D/split" + echo "generated_at_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + echo "git_head=$(git rev-parse HEAD 2>/dev/null || true)" + echo "platform=$(uname -srm)" + echo "python=$($PYTHON_BIN --version 2>&1 || true)" + if [ -f requirements.txt ] || [ -f pyproject.toml ] || [ -f setup.py ]; then + echo "python_dependency_manifest_status=present" + else + echo "python_dependency_manifest_status=not-present" + fi + echo "training_jsonl_files=$(wc -l < "$OUT/training-jsonl-files.txt" | tr -d ' ')" + echo "config_files=$(wc -l < "$OUT/config-files.txt" | tr -d ' ')" + echo "python_files=$(wc -l < "$OUT/python-files.txt" | tr -d ' ')" + echo "tracked_model_artifacts=$(wc -l < "$OUT/tracked-model-artifacts.txt" | tr -d ' ')" + echo "model_artifact_publication_status=blocked-private-split" + echo "source_split_status=blocked-review-required" + echo "sbom_status=present" +} > "$OUT/build-environment.txt" + +{ + echo "repo=jerboa-lora" + echo "publication_status=private-only" + echo "direct_publication_status=blocked" + echo "sbom_scope=source-config-training-manifests" + echo "model_weights_status=not-release-artifacts" + echo "split_provenance_proof=fail-closed" + echo "target_evidence_status=see-provenance" +} > "$OUT/dependency-posture.txt" + +echo "status=generated" --- a/scripts/security-check.sh +++ b/scripts/security-check.sh @@ -16,6 +16,9 @@ test -f docs/model-card.md || fail "missing docs/model-card.md" test -f docs/dataset-card.md || fail "missing docs/dataset-card.md" test -f docs/release-evidence.md || fail "missing docs/release-evidence.md" test -f scripts/provenance-evidence.sh || fail "missing scripts/provenance-evidence.sh" +test -f scripts/sanitize-evidence.sh || fail "missing scripts/sanitize-evidence.sh" +test -f scripts/sbom.sh || fail "missing scripts/sbom.sh" +test -f scripts/reproducibility-report.sh || fail "missing scripts/reproducibility-report.sh" grep -q 'D/split' SECURITY.md || fail "SECURITY.md must mark this repo D/split" grep -q 'not publishable as-is' SECURITY.md || fail "SECURITY.md must block direct publication" @@ -24,6 +27,9 @@ grep -q 'private D/split draft' docs/model-card.md || fail "model card must rema grep -q 'private D/split draft' docs/dataset-card.md || fail "dataset card must remain private D/split" grep -q 'blocked-split-provenance-proof' scripts/provenance-evidence.sh || fail "provenance evidence must fail closed on required proof" grep -q 'split_provenance_proof_status' scripts/provenance-evidence.sh || fail "provenance evidence must record proof status" +grep -q 'reproducibility_scope=source-and-manifest-only' scripts/reproducibility-report.sh || fail "reproducibility report must be source-only for private split repo" +grep -q 'sbom_status=present' scripts/sbom.sh || fail "SBOM script must record status" +grep -q 'JLORA_SPLIT_PROVENANCE_PROOF_MAX_BYTES' scripts/provenance-evidence.sh || fail "provenance proof must have a size limit" test ! -f PUBLIC_RELEASE_ALLOWED || fail "PUBLIC_RELEASE_ALLOWED must not exist in private D/split repo" for path in \