From 18cdea1f9ff4cdc210b83eadf7246631c381359c Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 13:17:09 +0800 Subject: [PATCH 01/15] Add narrow public-source release audit --- README.md | 11 +- bootstrap.sh | 1 + public-source-release-audit/SKILL.md | 79 ++++++ .../scripts/check_current_source.rb | 157 ++++++++++++ .../tests/test_check_current_source.rb | 224 ++++++++++++++++++ scripts/verify.sh | 37 +-- 6 files changed, 473 insertions(+), 36 deletions(-) create mode 100644 public-source-release-audit/SKILL.md create mode 100755 public-source-release-audit/scripts/check_current_source.rb create mode 100644 public-source-release-audit/tests/test_check_current_source.rb diff --git a/README.md b/README.md index e224c38..f1cc69b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 51Code Agent Skills -This repository is the source package for 15 skills maintained by 51Code and +This repository is the source package for 16 skills maintained by 51Code and the bootstrap for our reviewed machine-global skill baseline. The repository intentionally has no catalog, registry, generated lock, sync @@ -9,14 +9,15 @@ package directly. ## Global baseline -The global baseline supports Codex and Claude Code and contains these 38 skills: +The global baseline supports Codex and Claude Code and contains these 39 skills: - 51Code-owned: `code-review`, `gemini-files-api`, `harness-engineering`, `hint-overlay-visual-verification`, `ios-xcodegen`, `lifecycle-and-side-effects-correctness`, `local-model-serving`, `mechanism-audit`, - `meeting-transcription`, `silent-pushes-setup`, `spec-creation-updating`, - `swift-testing`, `swiftui-view-refactor`, `xcode-build`, and `xcode-cloud` + `meeting-transcription`, `public-source-release-audit`, + `silent-pushes-setup`, `spec-creation-updating`, `swift-testing`, + `swiftui-view-refactor`, `xcode-build`, and `xcode-cloud` - Third-party: `swift-concurrency` and the 22 `asc-*` App Store Connect CLI skills from `rorkai/app-store-connect-cli-skills` @@ -37,7 +38,7 @@ idempotent. The script uses `skills@1.5.14`, explicit Git tags or commits, and explicit skill names. In this CLI, `#ref` selects a Git branch or tag; `@name` selects a skill and must not be used as a version pin. Sources pinned to a raw commit are checked out and verified before being passed to the manager as a -local source. Before reporting success, the script verifies all 38 entrypoints +local source. Before reporting success, the script verifies all 39 entrypoints in the shared and Claude Code manager roots and bootstraps the copied `gemini-files-api` dependencies in both roots. The Claude Code root honors `CLAUDE_CONFIG_DIR` when it is set. The script also converts the pinned diff --git a/bootstrap.sh b/bootstrap.sh index 72780a0..8de31f2 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -39,6 +39,7 @@ owned_skills=( local-model-serving mechanism-audit meeting-transcription + public-source-release-audit silent-pushes-setup spec-creation-updating swift-testing diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md new file mode 100644 index 0000000..4b35627 --- /dev/null +++ b/public-source-release-audit/SKILL.md @@ -0,0 +1,79 @@ +--- +name: public-source-release-audit +description: Check a Git repository's current candidate for high-confidence credential, private-key, and machine-home leaks, then perform a bounded public-release review. Use before publishing source or opening a public PR; do not use it to claim complete history or workflow-execution safety. +--- + +# Public-Source Release Audit + +Use this skill to answer a narrow question honestly: does the current source +candidate contain a supported high-confidence leak, and have the separate +human and GitHub-native release checks been confirmed? + +## Automated Current-Source Check + +Run the repository's own verifier when it exists. Then run: + +```bash +ruby /scripts/check_current_source.rb +``` + +The checker reads exact Git-index blobs so an unstaged replacement cannot hide +the candidate. It also scans tracked and non-ignored untracked worktree files +for early feedback. It reports only paths and finding categories, never the +matched content. + +The supported categories are deliberately small: + +- private-key headers; +- GitHub, AWS, and OpenAI credential formats; +- explicit bearer credentials; and +- absolute macOS, Linux, and Windows user-home paths. + +Stage all intended release changes before relying on the candidate result. +Ignored files and submodule contents are outside this check. The checker does +not inspect Git history. + +## Semantic Review + +Automation cannot decide whether prose, filenames, images, or other assets +contain customer information, internal links, personal data, or company-only +context. Inspect the complete tree before first publication and the exact diff +for later releases. Mark this check unconfirmed unless a reviewer actually +performed it. + +## GitHub-Native Controls + +For an existing public GitHub repository, use read-only GitHub commands to +confirm the live state rather than recreating GitHub policy logic locally: + +```bash +gh repo view OWNER/REPO --json visibility,defaultBranchRef +gh api repos/OWNER/REPO --jq '{visibility,security_and_analysis}' +gh ruleset check --default --repo OWNER/REPO +gh ruleset list --repo OWNER/REPO --parents --limit 100 +gh ruleset view RULESET-ID --repo OWNER/REPO +gh api 'repos/OWNER/REPO/actions/runners?per_page=100' \ + --jq '{total_count,runners:[.runners[] | {name,status,busy}]}' +``` + +Inspect applicable rulesets when necessary to confirm the required hosted +check, update strictness, and bypass actors. If authorization cannot expose a +setting, report it as unconfirmed. Do not weaken or mutate settings unless the +user separately authorizes that action. + +## Result + +Report these fields separately: + +- repository verifier: `passed`, `failed`, or `not available`; +- automated current-source check: `passed` or `failed`; +- semantic context review: `confirmed` or `unconfirmed`; +- GitHub-native controls: `confirmed`, `unconfirmed`, or `not applicable`; +- history review: normally `not performed`; +- bounded verdict: `ready` only when every check required for this release is + passed or confirmed. + +Do not summarize the bounded verdict as “the repository is safe.” Full-history +investigation is a separate, exceptional workflow using established tooling. +Publishing, rewriting history, revoking credentials, and changing repository +settings remain separate actions requiring their own authorization. diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb new file mode 100755 index 0000000..fa9f776 --- /dev/null +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -0,0 +1,157 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "open3" +require "set" + +RULES = [ + [ + "machine-local home path", + %r{(?:\A|[\s"'`=:(,])/(?:Users|home)/[A-Za-z0-9._-]+(?:/|\b)|\b[A-Za-z]:[\\/]Users[\\/][^\\/\r\n]+(?:[\\/]|$)} + ], + ["AWS access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/], + ["bearer credential", /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._~+\/=:-]{16,}/i], + [ + "GitHub token", + /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/ + ], + ["OpenAI API key", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/], + [ + "private key", + /-----BEGIN (?:[A-Z0-9][A-Z0-9 ]* )?PRIVATE KEY-----/ + ] +].freeze + +INDEX_BLOB_MODES = %w[100644 100755 120000].freeze +GIT_ENVIRONMENT = { + "GIT_ALTERNATE_OBJECT_DIRECTORIES" => nil, + "GIT_COMMON_DIR" => nil, + "GIT_DIR" => nil, + "GIT_INDEX_FILE" => nil, + "GIT_NO_REPLACE_OBJECTS" => "1", + "GIT_OBJECT_DIRECTORY" => nil, + "GIT_WORK_TREE" => nil +}.freeze + +def usage(message = nil) + warn message if message + warn "Usage: check_current_source.rb [repository]" + exit 64 +end + +def git_capture(repo, *arguments) + Open3.capture3(GIT_ENVIRONMENT, "git", "-C", repo, *arguments) +end + +def scan_source(content, relative_path, findings, sensitive_paths = nil) + source = content.b + RULES.each do |label, pattern| + next unless pattern.match?(source) + + findings.add([relative_path, label]) + sensitive_paths&.add(relative_path) + end +end + +def display_path(relative_path, sensitive_paths) + sensitive_paths.include?(relative_path) ? "" : relative_path.dump +end + +usage("Too many arguments") if ARGV.length > 1 +repo = File.expand_path(ARGV.first || ".") +usage("Repository must be a directory") unless File.directory?(repo) + +index_output, _index_error, index_status = git_capture( + repo, + "ls-files", + "--stage", + "-z" +) +usage("Repository must be a Git worktree") unless index_status.success? + +errors = Set.new +findings = Set.new +sensitive_paths = Set.new +blob_cache = {} +index_entries = index_output.split("\0").reject(&:empty?).filter_map do |record| + metadata, relative_path = record.split("\t", 2) + mode, object_id, stage = metadata&.split(" ", 3) + unless mode && object_id && stage && relative_path + errors.add([relative_path || "", "unreadable Git index entry"]) + next + end + + if stage != "0" + errors.add([relative_path, "unresolved Git index entry"]) + next + end + + { mode: mode, object_id: object_id, path: relative_path } +end + +index_entries.each do |entry| + relative_path = entry.fetch(:path) + scan_source(relative_path, relative_path, findings, sensitive_paths) + next if entry.fetch(:mode) == "160000" + + unless INDEX_BLOB_MODES.include?(entry.fetch(:mode)) + errors.add([relative_path, "unsupported Git index mode"]) + next + end + + object_id = entry.fetch(:object_id) + content = blob_cache[object_id] + unless content + content, _blob_error, blob_status = git_capture( + repo, + "cat-file", + "blob", + object_id + ) + unless blob_status.success? + errors.add([relative_path, "unable to read Git index blob"]) + next + end + blob_cache[object_id] = content + end + scan_source(content, relative_path, findings) +end + +worktree_output, _worktree_error, worktree_status = git_capture( + repo, + "ls-files", + "-co", + "--exclude-standard", + "-z" +) +usage("Unable to enumerate repository worktree source") unless worktree_status.success? + +worktree_paths = worktree_output.split("\0").reject(&:empty?).uniq +worktree_paths.each do |relative_path| + absolute_path = File.join(repo, relative_path) + scan_source(relative_path, relative_path, findings, sensitive_paths) + begin + content = if File.symlink?(absolute_path) + File.readlink(absolute_path) + elsif File.file?(absolute_path) + File.binread(absolute_path) + end + scan_source(content, relative_path, findings) if content + rescue SystemCallError + errors.add([relative_path, "unable to read worktree source"]) + end +end + +unless errors.empty? && findings.empty? + warn "current public-source check failed:" + errors.to_a.sort.each do |relative_path, label| + warn "- #{display_path(relative_path, sensitive_paths)}: #{label}" + end + findings.to_a.sort.each do |relative_path, label| + warn "- #{display_path(relative_path, sensitive_paths)}: #{label}" + end + warn "Matched content is intentionally omitted." unless findings.empty? + exit 1 +end + +puts "current public-source check passed" diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb new file mode 100644 index 0000000..95017a9 --- /dev/null +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -0,0 +1,224 @@ +# frozen_string_literal: true + +require "fileutils" +require "minitest/autorun" +require "open3" +require "tmpdir" + +class CheckCurrentSourceTest < Minitest::Test + CHECKER = File.expand_path("../scripts/check_current_source.rb", __dir__) + + def with_repo + Dir.mktmpdir("public-source-check-") do |repo| + system("git", "-C", repo, "init", "--quiet", exception: true) + yield repo + end + end + + def write(repo, relative_path, content) + path = File.join(repo, relative_path) + FileUtils.mkdir_p(File.dirname(path)) + File.binwrite(path, content) + end + + def stage(repo, relative_path, content) + write(repo, relative_path, content) + system("git", "-C", repo, "add", "--", relative_path, exception: true) + end + + def run_checker(repo, environment = {}) + Open3.capture3(environment, "ruby", CHECKER, repo) + end + + def github_token + "gh" + "p_" + ("A" * 24) + end + + def test_safe_candidate_passes + with_repo do |repo| + stage(repo, "README.md", "Public documentation without credentials.\n") + + stdout, stderr, status = run_checker(repo) + + assert status.success?, stderr + assert_includes stdout, "current public-source check passed" + end + end + + def test_staged_credential_cannot_be_hidden_by_clean_worktree_replacement + with_repo do |repo| + token = github_token + stage(repo, "masked.txt", token) + write(repo, "masked.txt", "clean replacement\n") + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "masked.txt" + assert_includes stderr, "GitHub token" + refute_includes stderr, token + end + end + + def test_unstaged_credential_is_checked_before_it_can_be_added + with_repo do |repo| + token = github_token + stage(repo, "pending.txt", "safe candidate\n") + write(repo, "pending.txt", token) + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "pending.txt" + assert_includes stderr, "GitHub token" + refute_includes stderr, token + end + end + + def test_git_environment_cannot_redirect_the_candidate_index + with_repo do |repo| + token = github_token + stage(repo, "candidate.txt", token) + alternate_index = File.join(repo, "alternate-index") + environment = { "GIT_INDEX_FILE" => alternate_index } + _output, error, status = Open3.capture3( + environment, + "git", + "-C", + repo, + "read-tree", + "--empty" + ) + assert status.success?, error + + _stdout, stderr, checker_status = run_checker(repo, environment) + + refute checker_status.success? + assert_includes stderr, "candidate.txt" + assert_includes stderr, "GitHub token" + refute_includes stderr, token + end + end + + def test_sensitive_filename_is_detected_without_echoing_it + with_repo do |repo| + token = github_token + stage(repo, token, "safe content\n") + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "" + assert_includes stderr, "GitHub token" + refute_includes stderr, token + end + end + + def test_supported_high_confidence_formats_are_detected_and_redacted + with_repo do |repo| + fixtures = { + "aws.txt" => "AK" + "IA" + ("A" * 16), + "bearer.txt" => "Authorization: " + "Bearer " + ("b" * 24), + "fine-grained.txt" => "github_" + "pat_" + ("C" * 24), + "openai.txt" => "s" + "k-proj-" + ("D" * 24), + "private-key.txt" => "-----BEGIN " + "OPENSSH PRIVATE KEY-----", + "posix-home.txt" => "/" + "Users/example/private.txt", + "windows-home.txt" => "C:" + "\\Users\\example\\private.txt" + } + fixtures.each { |path, content| stage(repo, path, content) } + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + fixtures.each do |path, content| + assert_includes stderr, path + refute_includes stderr, content + end + [ + "AWS access key", + "bearer credential", + "GitHub token", + "OpenAI API key", + "private key", + "machine-local home path" + ].each { |label| assert_includes stderr, label } + end + end + + def test_symlink_target_is_scanned_without_following_it + with_repo do |repo| + target = "/" + "home/example/private.txt" + File.symlink(target, File.join(repo, "local-link")) + system("git", "-C", repo, "add", "local-link", exception: true) + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "local-link" + assert_includes stderr, "machine-local home path" + refute_includes stderr, target + end + end + + def test_ignored_local_files_are_outside_the_release_candidate + with_repo do |repo| + stage(repo, ".gitignore", "ignored.txt\n") + write(repo, "ignored.txt", github_token) + + stdout, stderr, status = run_checker(repo) + + assert status.success?, stderr + assert_includes stdout, "current public-source check passed" + end + end + + def test_unrelated_home_directory_and_web_route_do_not_match_user_homes + with_repo do |repo| + stage(repo, "paths.txt", [ + "/tmp/home/.local/bin/tool", + "https://example.invalid/home/user/profile" + ].join("\n")) + + stdout, stderr, status = run_checker(repo) + + assert status.success?, stderr + assert_includes stdout, "current public-source check passed" + end + end + + def test_unresolved_index_entries_fail_closed + with_repo do |repo| + object_ids = %w[base ours theirs].map do |content| + output, error, status = Open3.capture3( + "git", + "-C", + repo, + "hash-object", + "-w", + "--stdin", + stdin_data: content + ) + assert status.success?, error + output.strip + end + index_info = object_ids.each_with_index.map do |object_id, index| + "100644 #{object_id} #{index + 1}\tconflict.txt\n" + end.join + _output, error, status = Open3.capture3( + "git", + "-C", + repo, + "update-index", + "--index-info", + stdin_data: index_info + ) + assert status.success?, error + + _stdout, stderr, checker_status = run_checker(repo) + + refute checker_status.success? + assert_includes stderr, "conflict.txt" + assert_includes stderr, "unresolved Git index entry" + end + end +end diff --git a/scripts/verify.sh b/scripts/verify.sh index f4c15f2..7ac71bb 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -60,13 +60,13 @@ if (JSON.stringify(documentedOwned) !== JSON.stringify([...owned].sort())) { fail("README.md owned skill list does not match bootstrap.sh"); } -if (owned.length !== 15 || standalone.length !== 1 || asc.length !== 22) { - fail(`Expected 15 owned, 1 standalone, and 22 ASC skills; found ${owned.length}, ${standalone.length}, and ${asc.length}`); +if (owned.length !== 16 || standalone.length !== 1 || asc.length !== 22) { + fail(`Expected 16 owned, 1 standalone, and 22 ASC skills; found ${owned.length}, ${standalone.length}, and ${asc.length}`); } const managed = [...owned, ...standalone, ...asc]; -if (managed.length !== 38 || new Set(managed).size !== managed.length) { - fail("The 38-skill global baseline contains a missing or duplicate name"); +if (managed.length !== 39 || new Set(managed).size !== managed.length) { + fail("The 39-skill global baseline contains a missing or duplicate name"); } console.log("validated direct package contract"); @@ -98,33 +98,8 @@ end puts "validated skill front matter YAML" RUBY -ruby <<'RUBY' -patterns = { - "machine-local home path" => %r{/(?:Users|home)/[A-Za-z0-9._-]+(?:/|\b)}, - "AWS access key" => /AKIA[0-9A-Z]{16}/, - "bearer credential" => /Authorization:\s*Bearer\s+[A-Za-z0-9._~-]{16,}/i, - "GitHub token" => /gh[pousr]_[A-Za-z0-9]{20,}/, - "API secret" => /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/, - "private key" => /-----BEGIN (?:RSA )?PRIVATE KEY-----/ -} - -files = IO.popen(["git", "ls-files", "-co", "--exclude-standard", "-z"], &:read).split("\0") -failures = files.sort.filter_map do |file| - next unless File.file?(file) && !File.symlink?(file) - - content = File.binread(file) - next if content.include?("\0") - - text = content.force_encoding(Encoding::UTF_8) - next unless text.valid_encoding? - - labels = patterns.filter_map { |label, pattern| label if text.match?(pattern) } - "#{file}: #{labels.join(", ")}" unless labels.empty? -end - -abort "public-safety scan failed:\n#{failures.join("\n")}" unless failures.empty? -puts "public-safety scan passed" -RUBY +ruby public-source-release-audit/tests/test_check_current_source.rb +ruby public-source-release-audit/scripts/check_current_source.rb . while IFS= read -r -d '' script; do [[ "$script" == *.sh && -f "$script" && ! -L "$script" ]] || continue From 3dcd7b7c9fa16587032e94b2ac1f52276b0518ca Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 13:45:35 +0800 Subject: [PATCH 02/15] Address bounded release audit review findings Handle root homes, bounded blob caching, non-fetching partial clones, explicit LFS review, and classic branch protection without expanding into generalized parsing. --- public-source-release-audit/SKILL.md | 15 +++-- .../scripts/check_current_source.rb | 38 ++++++++--- .../tests/test_check_current_source.rb | 67 +++++++++++++++++++ 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index 4b35627..74d0d35 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -31,7 +31,9 @@ The supported categories are deliberately small: Stage all intended release changes before relying on the candidate result. Ignored files and submodule contents are outside this check. The checker does -not inspect Git history. +not inspect Git history. Git LFS pointers fail the check because their external +objects require separate review. Missing index blobs, including unavailable +partial-clone objects, fail without network fetching. ## Semantic Review @@ -52,14 +54,17 @@ gh api repos/OWNER/REPO --jq '{visibility,security_and_analysis}' gh ruleset check --default --repo OWNER/REPO gh ruleset list --repo OWNER/REPO --parents --limit 100 gh ruleset view RULESET-ID --repo OWNER/REPO +gh api repos/OWNER/REPO/branches/DEFAULT-BRANCH/protection gh api 'repos/OWNER/REPO/actions/runners?per_page=100' \ --jq '{total_count,runners:[.runners[] | {name,status,busy}]}' ``` -Inspect applicable rulesets when necessary to confirm the required hosted -check, update strictness, and bypass actors. If authorization cannot expose a -setting, report it as unconfirmed. Do not weaken or mutate settings unless the -user separately authorizes that action. +Inspect applicable rulesets and classic branch protection when necessary to +confirm the required hosted check, update strictness, and bypass actors. A 404 +from the classic endpoint means no classic rule is configured; it does not +invalidate an applicable ruleset. If authorization cannot expose a setting, +report it as unconfirmed. Do not weaken or mutate settings unless the user +separately authorizes that action. ## Result diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index fa9f776..75dbe5a 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -7,7 +7,7 @@ RULES = [ [ "machine-local home path", - %r{(?:\A|[\s"'`=:(,])/(?:Users|home)/[A-Za-z0-9._-]+(?:/|\b)|\b[A-Za-z]:[\\/]Users[\\/][^\\/\r\n]+(?:[\\/]|$)} + %r{(?:\A|[\s"'`=:(,])(?:/(?:Users|home)/[A-Za-z0-9._-]+(?:/|\b)|/root(?:/|(?![A-Za-z0-9._-])))|\b[A-Za-z]:[\\/]Users[\\/][^\\/\r\n]+(?:[\\/]|$)} ], ["AWS access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/], ["bearer credential", /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._~+\/=:-]{16,}/i], @@ -23,11 +23,13 @@ ].freeze INDEX_BLOB_MODES = %w[100644 100755 120000].freeze +LFS_POINTER = %r{\Aversion https://git-lfs\.github\.com/spec/v1(?:\r?\n|\z)}.freeze GIT_ENVIRONMENT = { "GIT_ALTERNATE_OBJECT_DIRECTORIES" => nil, "GIT_COMMON_DIR" => nil, "GIT_DIR" => nil, "GIT_INDEX_FILE" => nil, + "GIT_NO_LAZY_FETCH" => "1", "GIT_NO_REPLACE_OBJECTS" => "1", "GIT_OBJECT_DIRECTORY" => nil, "GIT_WORK_TREE" => nil @@ -43,16 +45,27 @@ def git_capture(repo, *arguments) Open3.capture3(GIT_ENVIRONMENT, "git", "-C", repo, *arguments) end -def scan_source(content, relative_path, findings, sensitive_paths = nil) +def matching_labels(content) source = content.b - RULES.each do |label, pattern| - next unless pattern.match?(source) + RULES.filter_map { |label, pattern| label if pattern.match?(source) } +end +def record_findings(labels, relative_path, findings, sensitive_paths = nil) + labels.each do |label| findings.add([relative_path, label]) sensitive_paths&.add(relative_path) end end +def scan_source(content, relative_path, findings, sensitive_paths = nil) + record_findings( + matching_labels(content), + relative_path, + findings, + sensitive_paths + ) +end + def display_path(relative_path, sensitive_paths) sensitive_paths.include?(relative_path) ? "" : relative_path.dump end @@ -72,7 +85,7 @@ def display_path(relative_path, sensitive_paths) errors = Set.new findings = Set.new sensitive_paths = Set.new -blob_cache = {} +blob_result_cache = {} index_entries = index_output.split("\0").reject(&:empty?).filter_map do |record| metadata, relative_path = record.split("\t", 2) mode, object_id, stage = metadata&.split(" ", 3) @@ -100,8 +113,8 @@ def display_path(relative_path, sensitive_paths) end object_id = entry.fetch(:object_id) - content = blob_cache[object_id] - unless content + result = blob_result_cache[object_id] + unless result content, _blob_error, blob_status = git_capture( repo, "cat-file", @@ -112,9 +125,16 @@ def display_path(relative_path, sensitive_paths) errors.add([relative_path, "unable to read Git index blob"]) next end - blob_cache[object_id] = content + result = { + labels: matching_labels(content), + lfs_pointer: LFS_POINTER.match?(content.b) + } + blob_result_cache[object_id] = result + end + if result.fetch(:lfs_pointer) + errors.add([relative_path, "Git LFS object requires separate review"]) end - scan_source(content, relative_path, findings) + record_findings(result.fetch(:labels), relative_path, findings) end worktree_output, _worktree_error, worktree_status = git_capture( diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 95017a9..c16c39e 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -123,6 +123,7 @@ def test_supported_high_confidence_formats_are_detected_and_redacted "openai.txt" => "s" + "k-proj-" + ("D" * 24), "private-key.txt" => "-----BEGIN " + "OPENSSH PRIVATE KEY-----", "posix-home.txt" => "/" + "Users/example/private.txt", + "root-home.txt" => "/" + "root/.ssh/id_ed25519", "windows-home.txt" => "C:" + "\\Users\\example\\private.txt" } fixtures.each { |path, content| stage(repo, path, content) } @@ -186,6 +187,72 @@ def test_unrelated_home_directory_and_web_route_do_not_match_user_homes end end + def test_duplicate_index_blobs_report_each_candidate_path + with_repo do |repo| + token = github_token + stage(repo, "first.txt", token) + stage(repo, "second.txt", token) + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "first.txt" + assert_includes stderr, "second.txt" + assert_includes stderr, "GitHub token" + refute_includes stderr, token + end + end + + def test_git_commands_disable_lazy_object_fetching + with_repo do |repo| + stage(repo, "README.md", "Public documentation without credentials.\n") + real_git = ENV.fetch("PATH").split(File::PATH_SEPARATOR).filter_map do |dir| + candidate = File.join(dir, "git") + candidate if File.executable?(candidate) + end.first + refute_nil real_git + + Dir.mktmpdir("public-source-check-bin-") do |bin_dir| + fake_git = File.join(bin_dir, "git") + File.write(fake_git, <<~RUBY) + #!/usr/bin/env ruby + abort "lazy fetching was not disabled" unless ENV["GIT_NO_LAZY_FETCH"] == "1" + exec #{real_git.dump}, *ARGV + RUBY + FileUtils.chmod(0o755, fake_git) + + stdout, stderr, status = run_checker( + repo, + { + "GIT_NO_LAZY_FETCH" => "0", + "PATH" => [bin_dir, ENV.fetch("PATH")].join(File::PATH_SEPARATOR) + } + ) + + assert status.success?, stderr + assert_includes stdout, "current public-source check passed" + end + end + end + + def test_git_lfs_pointer_requires_separate_review + with_repo do |repo| + pointer = [ + "version https://git-lfs.github.com/spec/v1", + "oid sha256:#{'a' * 64}", + "size 123" + ].join("\n") + "\n" + stage(repo, "large.dat", pointer) + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "large.dat" + assert_includes stderr, "Git LFS object requires separate review" + refute_includes stderr, "sha256:" + end + end + def test_unresolved_index_entries_fail_closed with_repo do |repo| object_ids = %w[base ours theirs].map do |content| From 0085174f017e2f734d2eceaded25e8ccb23a41f0 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 13:59:09 +0800 Subject: [PATCH 03/15] Close bounded path and format review gaps Reject symlinked parents, scan raw Git path bytes safely, and cover PGP headers plus serialized Windows home paths without expanding the audit architecture. --- public-source-release-audit/SKILL.md | 4 +- .../scripts/check_current_source.rb | 30 +++++--- .../tests/test_check_current_source.rb | 68 ++++++++++++++++++- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index 74d0d35..2e36d78 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -33,7 +33,9 @@ Stage all intended release changes before relying on the candidate result. Ignored files and submodule contents are outside this check. The checker does not inspect Git history. Git LFS pointers fail the check because their external objects require separate review. Missing index blobs, including unavailable -partial-clone objects, fail without network fetching. +partial-clone objects, fail without network fetching. A symlink's own target +text is scanned, while a symlinked parent component fails without reading +outside the repository. ## Semantic Review diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index 75dbe5a..aa20d34 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -7,7 +7,7 @@ RULES = [ [ "machine-local home path", - %r{(?:\A|[\s"'`=:(,])(?:/(?:Users|home)/[A-Za-z0-9._-]+(?:/|\b)|/root(?:/|(?![A-Za-z0-9._-])))|\b[A-Za-z]:[\\/]Users[\\/][^\\/\r\n]+(?:[\\/]|$)} + %r{(?:\A|[\s"'`=:(,])(?:/(?:Users|home)/[A-Za-z0-9._-]+(?:/|\b)|/root(?:/|(?![A-Za-z0-9._-])))|\b[A-Za-z]:[\\/]{1,2}Users[\\/]{1,2}[^\\/\r\n]+(?:[\\/]{1,2}|$)} ], ["AWS access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/], ["bearer credential", /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._~+\/=:-]{16,}/i], @@ -18,7 +18,7 @@ ["OpenAI API key", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/], [ "private key", - /-----BEGIN (?:[A-Z0-9][A-Z0-9 ]* )?PRIVATE KEY-----/ + /-----BEGIN (?:[A-Z0-9][A-Z0-9 ]* )?PRIVATE KEY(?: BLOCK)?-----/ ] ].freeze @@ -70,6 +70,14 @@ def display_path(relative_path, sensitive_paths) sensitive_paths.include?(relative_path) ? "" : relative_path.dump end +def symlinked_parent?(repo, relative_path) + current = repo.b + relative_path.b.split("/".b)[0...-1].any? do |component| + current = File.join(current, component) + File.symlink?(current) + end +end + usage("Too many arguments") if ARGV.length > 1 repo = File.expand_path(ARGV.first || ".") usage("Repository must be a directory") unless File.directory?(repo) @@ -86,9 +94,9 @@ def display_path(relative_path, sensitive_paths) findings = Set.new sensitive_paths = Set.new blob_result_cache = {} -index_entries = index_output.split("\0").reject(&:empty?).filter_map do |record| - metadata, relative_path = record.split("\t", 2) - mode, object_id, stage = metadata&.split(" ", 3) +index_entries = index_output.b.split("\0".b).reject(&:empty?).filter_map do |record| + metadata, relative_path = record.split("\t".b, 2) + mode, object_id, stage = metadata&.split(" ".b, 3) unless mode && object_id && stage && relative_path errors.add([relative_path || "", "unreadable Git index entry"]) next @@ -146,11 +154,15 @@ def display_path(relative_path, sensitive_paths) ) usage("Unable to enumerate repository worktree source") unless worktree_status.success? -worktree_paths = worktree_output.split("\0").reject(&:empty?).uniq +worktree_paths = worktree_output.b.split("\0".b).reject(&:empty?).uniq worktree_paths.each do |relative_path| - absolute_path = File.join(repo, relative_path) scan_source(relative_path, relative_path, findings, sensitive_paths) begin + if symlinked_parent?(repo, relative_path) + errors.add([relative_path, "symlinked parent component"]) + next + end + absolute_path = File.join(repo.b, relative_path) content = if File.symlink?(absolute_path) File.readlink(absolute_path) elsif File.file?(absolute_path) @@ -164,10 +176,10 @@ def display_path(relative_path, sensitive_paths) unless errors.empty? && findings.empty? warn "current public-source check failed:" - errors.to_a.sort.each do |relative_path, label| + errors.to_a.sort_by { |relative_path, label| [relative_path.b, label] }.each do |relative_path, label| warn "- #{display_path(relative_path, sensitive_paths)}: #{label}" end - findings.to_a.sort.each do |relative_path, label| + findings.to_a.sort_by { |relative_path, label| [relative_path.b, label] }.each do |relative_path, label| warn "- #{display_path(relative_path, sensitive_paths)}: #{label}" end warn "Matched content is intentionally omitted." unless findings.empty? diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index c16c39e..82931a9 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -122,9 +122,19 @@ def test_supported_high_confidence_formats_are_detected_and_redacted "fine-grained.txt" => "github_" + "pat_" + ("C" * 24), "openai.txt" => "s" + "k-proj-" + ("D" * 24), "private-key.txt" => "-----BEGIN " + "OPENSSH PRIVATE KEY-----", + "pgp-private-key.txt" => "-----BEGIN " + "PGP PRIVATE KEY BLOCK-----", "posix-home.txt" => "/" + "Users/example/private.txt", "root-home.txt" => "/" + "root/.ssh/id_ed25519", - "windows-home.txt" => "C:" + "\\Users\\example\\private.txt" + "windows-home.txt" => "C:" + "\\Users\\example\\private.txt", + "escaped-windows-home.txt" => [ + "C:", + "\\" * 2, + "Users", + "\\" * 2, + "example", + "\\" * 2, + "private.txt" + ].join } fixtures.each { |path, content| stage(repo, path, content) } @@ -161,6 +171,27 @@ def test_symlink_target_is_scanned_without_following_it end end + def test_symlinked_parent_is_rejected_without_reading_outside_repo + with_repo do |repo| + stage(repo, "dir/file.txt", "safe candidate\n") + FileUtils.mv(File.join(repo, "dir"), File.join(repo, "original-dir")) + + Dir.mktmpdir("public-source-check-outside-") do |outside| + token = github_token + write(outside, "file.txt", token) + File.symlink(outside, File.join(repo, "dir")) + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "dir/file.txt" + assert_includes stderr, "symlinked parent component" + refute_includes stderr, "GitHub token" + refute_includes stderr, token + end + end + end + def test_ignored_local_files_are_outside_the_release_candidate with_repo do |repo| stage(repo, ".gitignore", "ignored.txt\n") @@ -235,6 +266,41 @@ def test_git_commands_disable_lazy_object_fetching end end + def test_non_utf8_git_paths_are_scanned_without_crashing + with_repo do |repo| + token = github_token + Dir.mktmpdir("public-source-check-bin-") do |bin_dir| + fake_git = File.join(bin_dir, "git") + File.write(fake_git, <<~'RUBY') + #!/usr/bin/env ruby + STDOUT.binmode + path = "raw-\xFF.txt".b + if ARGV.include?("--stage") + STDOUT.write("100644 #{'a' * 40} 0\t".b + path + "\0".b) + elsif ARGV.include?("cat-file") + STDOUT.write("gh" + "p_" + ("A" * 24)) + elsif ARGV.include?("-co") + STDOUT.write(path + "\0".b) + else + abort "unexpected git invocation: #{ARGV.join(' ')}" + end + RUBY + FileUtils.chmod(0o755, fake_git) + + _stdout, stderr, status = run_checker( + repo, + { "PATH" => [bin_dir, ENV.fetch("PATH")].join(File::PATH_SEPARATOR) } + ) + + refute status.success? + assert_includes stderr, "GitHub token" + assert_includes stderr, "\\xFF" + refute_includes stderr, "invalid byte sequence" + refute_includes stderr, token + end + end + end + def test_git_lfs_pointer_requires_separate_review with_repo do |repo| pointer = [ From 044047eae843c8be13c83391346b1c48b80158e7 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 14:14:13 +0800 Subject: [PATCH 04/15] Enforce whole-worktree non-mutating audit Normalize subdirectory inputs, suppress repository fsmonitor hooks, and encode the classic-protection branch parameter without adding broader policy analysis. --- public-source-release-audit/SKILL.md | 6 +- .../scripts/check_current_source.rb | 21 ++++++- .../tests/test_check_current_source.rb | 63 ++++++++++++++++++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index 2e36d78..d1b4d79 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -56,7 +56,11 @@ gh api repos/OWNER/REPO --jq '{visibility,security_and_analysis}' gh ruleset check --default --repo OWNER/REPO gh ruleset list --repo OWNER/REPO --parents --limit 100 gh ruleset view RULESET-ID --repo OWNER/REPO -gh api repos/OWNER/REPO/branches/DEFAULT-BRANCH/protection +default_branch="$(gh repo view OWNER/REPO --json defaultBranchRef \ + --jq '.defaultBranchRef.name')" +encoded_branch="$(ruby -rerb -e \ + 'print ERB::Util.url_encode(ARGV.fetch(0))' "$default_branch")" +gh api "repos/OWNER/REPO/branches/${encoded_branch}/protection" gh api 'repos/OWNER/REPO/actions/runners?per_page=100' \ --jq '{total_count,runners:[.runners[] | {name,status,busy}]}' ``` diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index aa20d34..52a6bfc 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -42,7 +42,15 @@ def usage(message = nil) end def git_capture(repo, *arguments) - Open3.capture3(GIT_ENVIRONMENT, "git", "-C", repo, *arguments) + Open3.capture3( + GIT_ENVIRONMENT, + "git", + "-C", + repo, + "-c", + "core.fsmonitor=false", + *arguments + ) end def matching_labels(content) @@ -82,13 +90,22 @@ def symlinked_parent?(repo, relative_path) repo = File.expand_path(ARGV.first || ".") usage("Repository must be a directory") unless File.directory?(repo) +toplevel_output, _toplevel_error, toplevel_status = git_capture( + repo, + "rev-parse", + "--show-toplevel" +) +usage("Repository must be a Git worktree") unless toplevel_status.success? +repo = toplevel_output.b.sub(/\r?\n\z/, "".b) +usage("Unable to resolve Git worktree root") unless File.directory?(repo) + index_output, _index_error, index_status = git_capture( repo, "ls-files", "--stage", "-z" ) -usage("Repository must be a Git worktree") unless index_status.success? +usage("Unable to enumerate Git index") unless index_status.success? errors = Set.new findings = Set.new diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 82931a9..99c3940 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -60,6 +60,22 @@ def test_staged_credential_cannot_be_hidden_by_clean_worktree_replacement end end + def test_subdirectory_argument_still_scans_the_complete_worktree + with_repo do |repo| + token = github_token + stage(repo, "root-secret.txt", token) + child = File.join(repo, "child") + FileUtils.mkdir_p(child) + + _stdout, stderr, status = run_checker(child) + + refute status.success? + assert_includes stderr, "root-secret.txt" + assert_includes stderr, "GitHub token" + refute_includes stderr, token + end + end + def test_unstaged_credential_is_checked_before_it_can_be_added with_repo do |repo| token = github_token @@ -266,6 +282,48 @@ def test_git_commands_disable_lazy_object_fetching end end + def test_repository_fsmonitor_hook_is_disabled + with_repo do |repo| + stage(repo, "README.md", "Public documentation without credentials.\n") + hook = File.join(repo, "fsmonitor-hook") + marker = File.join(repo, "fsmonitor-ran") + File.write(hook, <<~'SH') + #!/bin/sh + printf 'ran\n' > "$FSMONITOR_MARKER" + SH + FileUtils.chmod(0o755, hook) + system( + "git", + "-C", + repo, + "config", + "core.fsmonitor", + hook, + exception: true + ) + + _output, error, status = Open3.capture3( + { "FSMONITOR_MARKER" => marker }, + "git", + "-C", + repo, + "ls-files" + ) + assert status.success?, error + assert File.exist?(marker), "fsmonitor fixture did not execute" + FileUtils.rm(marker) + + stdout, stderr, checker_status = run_checker( + repo, + { "FSMONITOR_MARKER" => marker } + ) + + assert checker_status.success?, stderr + assert_includes stdout, "current public-source check passed" + refute File.exist?(marker), "checker executed the repository fsmonitor hook" + end + end + def test_non_utf8_git_paths_are_scanned_without_crashing with_repo do |repo| token = github_token @@ -275,7 +333,10 @@ def test_non_utf8_git_paths_are_scanned_without_crashing #!/usr/bin/env ruby STDOUT.binmode path = "raw-\xFF.txt".b - if ARGV.include?("--stage") + if ARGV.include?("rev-parse") + repo_index = ARGV.index("-C") + 1 + STDOUT.write(ARGV.fetch(repo_index) + "\n") + elsif ARGV.include?("--stage") STDOUT.write("100644 #{'a' * 40} 0\t".b + path + "\0".b) elsif ARGV.include?("cat-file") STDOUT.write("gh" + "p_" + ("A" * 24)) From 85ea9275960648db99ec82d1e884914910b799de Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 14:32:02 +0800 Subject: [PATCH 05/15] Bound path matching and batch blob reads Support JSON-escaped POSIX paths, exclude URL query routes, and stream unique index blobs through one Git process without expanding detection categories. --- public-source-release-audit/SKILL.md | 3 + .../scripts/check_current_source.rb | 130 ++++++++++++++---- .../tests/test_check_current_source.rb | 71 +++++++++- 3 files changed, 177 insertions(+), 27 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index d1b4d79..558b4c9 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -29,6 +29,9 @@ The supported categories are deliberately small: - explicit bearer credentials; and - absolute macOS, Linux, and Windows user-home paths. +The home-path rule covers literal paths and common source escaping for POSIX +slashes and Windows backslashes. It does not decode URL-encoded content. + Stage all intended release changes before relying on the candidate result. Ignored files and submodule contents are outside this check. The checker does not inspect Git history. Git LFS pointers fail the check because their external diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index 52a6bfc..133a53f 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -7,7 +7,20 @@ RULES = [ [ "machine-local home path", - %r{(?:\A|[\s"'`=:(,])(?:/(?:Users|home)/[A-Za-z0-9._-]+(?:/|\b)|/root(?:/|(?![A-Za-z0-9._-])))|\b[A-Za-z]:[\\/]{1,2}Users[\\/]{1,2}[^\\/\r\n]+(?:[\\/]{1,2}|$)} + %r{ + (?:\A|[\s"'`:(,]) + (?:[A-Za-z_][A-Za-z0-9_.-]*\s*=\s*)? + (?: + (?:/|\\/) + (?: + (?:Users|home)(?:/|\\/)[A-Za-z0-9._-]+(?:(?:/|\\/)|\b) + | + root(?:(?:/|\\/)|(?![A-Za-z0-9._-])) + ) + | + [A-Za-z]:[\\/]{1,2}Users[\\/]{1,2}[^\\/\r\n]+(?:[\\/]{1,2}|$) + ) + }x ], ["AWS access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/], ["bearer credential", /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._~+\/=:-]{16,}/i], @@ -41,16 +54,87 @@ def usage(message = nil) exit 64 end -def git_capture(repo, *arguments) - Open3.capture3( - GIT_ENVIRONMENT, +def git_command(repo, *arguments) + [ "git", "-C", repo, "-c", "core.fsmonitor=false", *arguments - ) + ] +end + +def git_capture(repo, *arguments) + Open3.capture3(GIT_ENVIRONMENT, *git_command(repo, *arguments)) +end + +def read_blob_results(repo, object_ids) + return [{}, true] if object_ids.empty? + + results = {} + protocol_failed = false + Open3.popen3( + GIT_ENVIRONMENT, + *git_command(repo, "cat-file", "--batch") + ) do |input, output, error, wait_thread| + input.binmode + output.binmode + error_reader = Thread.new { error.read } + + begin + object_ids.each do |object_id| + input.write(object_id) + input.write("\n") + input.flush + + header = output.gets + unless header + protocol_failed = true + break + end + + fields = header.delete_suffix("\n".b).split(" ".b) + if fields.length == 2 && fields[0] == object_id && fields[1] == "missing".b + next + end + + returned_id, type, size_text = fields + size = begin + Integer(size_text, 10) + rescue ArgumentError, TypeError + nil + end + unless fields.length == 3 && returned_id == object_id && size && size >= 0 + protocol_failed = true + break + end + + content = output.read(size) + terminator = output.read(1) + unless content&.bytesize == size && terminator == "\n".b + protocol_failed = true + break + end + next unless type == "blob".b + + results[object_id] = { + labels: matching_labels(content), + lfs_pointer: LFS_POINTER.match?(content.b) + } + end + rescue IOError, SystemCallError + protocol_failed = true + ensure + input.close unless input.closed? + output.close if protocol_failed && !output.closed? + end + + error_reader.value + protocol_failed = true unless wait_thread.value.success? + end + + [results, !protocol_failed] end def matching_labels(content) @@ -110,7 +194,6 @@ def symlinked_parent?(repo, relative_path) errors = Set.new findings = Set.new sensitive_paths = Set.new -blob_result_cache = {} index_entries = index_output.b.split("\0".b).reject(&:empty?).filter_map do |record| metadata, relative_path = record.split("\t".b, 2) mode, object_id, stage = metadata&.split(" ".b, 3) @@ -127,6 +210,7 @@ def symlinked_parent?(repo, relative_path) { mode: mode, object_id: object_id, path: relative_path } end +blob_paths = Hash.new { |paths, object_id| paths[object_id] = [] } index_entries.each do |entry| relative_path = entry.fetch(:path) scan_source(relative_path, relative_path, findings, sensitive_paths) @@ -137,29 +221,25 @@ def symlinked_parent?(repo, relative_path) next end - object_id = entry.fetch(:object_id) - result = blob_result_cache[object_id] - unless result - content, _blob_error, blob_status = git_capture( - repo, - "cat-file", - "blob", - object_id - ) - unless blob_status.success? + blob_paths[entry.fetch(:object_id)] << relative_path +end + +blob_results, blob_reader_success = read_blob_results(repo, blob_paths.keys) +unless blob_reader_success + errors.add(["", "Git index blob reader failed"]) +end +blob_paths.each do |object_id, relative_paths| + result = blob_results[object_id] + relative_paths.each do |relative_path| + unless result errors.add([relative_path, "unable to read Git index blob"]) next end - result = { - labels: matching_labels(content), - lfs_pointer: LFS_POINTER.match?(content.b) - } - blob_result_cache[object_id] = result - end - if result.fetch(:lfs_pointer) - errors.add([relative_path, "Git LFS object requires separate review"]) + if result.fetch(:lfs_pointer) + errors.add([relative_path, "Git LFS object requires separate review"]) + end + record_findings(result.fetch(:labels), relative_path, findings) end - record_findings(result.fetch(:labels), relative_path, findings) end worktree_output, _worktree_error, worktree_status = git_capture( diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 99c3940..5dbdb44 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -132,6 +132,7 @@ def test_sensitive_filename_is_detected_without_echoing_it def test_supported_high_confidence_formats_are_detected_and_redacted with_repo do |repo| + escaped_slash = "\\" + "/" fixtures = { "aws.txt" => "AK" + "IA" + ("A" * 16), "bearer.txt" => "Authorization: " + "Bearer " + ("b" * 24), @@ -140,6 +141,14 @@ def test_supported_high_confidence_formats_are_detected_and_redacted "private-key.txt" => "-----BEGIN " + "OPENSSH PRIVATE KEY-----", "pgp-private-key.txt" => "-----BEGIN " + "PGP PRIVATE KEY BLOCK-----", "posix-home.txt" => "/" + "Users/example/private.txt", + "escaped-posix-home.txt" => [ + escaped_slash, + "home", + escaped_slash, + "example", + escaped_slash, + "private.txt" + ].join, "root-home.txt" => "/" + "root/.ssh/id_ed25519", "windows-home.txt" => "C:" + "\\Users\\example\\private.txt", "escaped-windows-home.txt" => [ @@ -224,7 +233,8 @@ def test_unrelated_home_directory_and_web_route_do_not_match_user_homes with_repo do |repo| stage(repo, "paths.txt", [ "/tmp/home/.local/bin/tool", - "https://example.invalid/home/user/profile" + "https://example.invalid/home/user/profile", + "https://example.invalid/login?next=/home/user/profile" ].join("\n")) stdout, stderr, status = run_checker(repo) @@ -234,6 +244,20 @@ def test_unrelated_home_directory_and_web_route_do_not_match_user_homes end end + def test_home_assignment_is_still_detected + with_repo do |repo| + home = "/" + "home/example/private.txt" + stage(repo, "settings.env", "HOME=#{home}\n") + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "settings.env" + assert_includes stderr, "machine-local home path" + refute_includes stderr, home + end + end + def test_duplicate_index_blobs_report_each_candidate_path with_repo do |repo| token = github_token @@ -324,6 +348,43 @@ def test_repository_fsmonitor_hook_is_disabled end end + def test_unique_blobs_use_one_cat_file_process + with_repo do |repo| + stage(repo, "first.txt", "first safe blob\n") + stage(repo, "second.txt", "second safe blob\n") + real_git = ENV.fetch("PATH").split(File::PATH_SEPARATOR).filter_map do |dir| + candidate = File.join(dir, "git") + candidate if File.executable?(candidate) + end.first + refute_nil real_git + + Dir.mktmpdir("public-source-check-bin-") do |bin_dir| + marker = File.join(bin_dir, "cat-file-processes") + fake_git = File.join(bin_dir, "git") + File.write(fake_git, <<~RUBY) + #!/usr/bin/env ruby + if ARGV.include?("cat-file") + File.open(ENV.fetch("CAT_FILE_MARKER"), "a") { |file| file.puts("started") } + end + exec #{real_git.dump}, *ARGV + RUBY + FileUtils.chmod(0o755, fake_git) + + stdout, stderr, status = run_checker( + repo, + { + "CAT_FILE_MARKER" => marker, + "PATH" => [bin_dir, ENV.fetch("PATH")].join(File::PATH_SEPARATOR) + } + ) + + assert status.success?, stderr + assert_includes stdout, "current public-source check passed" + assert_equal ["started\n"], File.readlines(marker) + end + end + end + def test_non_utf8_git_paths_are_scanned_without_crashing with_repo do |repo| token = github_token @@ -339,7 +400,13 @@ def test_non_utf8_git_paths_are_scanned_without_crashing elsif ARGV.include?("--stage") STDOUT.write("100644 #{'a' * 40} 0\t".b + path + "\0".b) elsif ARGV.include?("cat-file") - STDOUT.write("gh" + "p_" + ("A" * 24)) + STDOUT.sync = true + content = "gh" + "p_" + ("A" * 24) + while (object_id = STDIN.gets&.chomp) + STDOUT.write("#{object_id} blob #{content.bytesize}\n") + STDOUT.write(content) + STDOUT.write("\n") + end elsif ARGV.include?("-co") STDOUT.write(path + "\0".b) else From ef0254ab7f1153c523d331252d28662fbc45a0e6 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 14:43:51 +0800 Subject: [PATCH 06/15] Define and cover exact release-audit forms Handle quoted bearer values, command-option homes, PuTTY headers, and named AWS secrets while replacing broad format promises with an explicit lexical contract. --- public-source-release-audit/SKILL.md | 17 +++++++++++------ .../scripts/check_current_source.rb | 14 +++++++++++--- .../tests/test_check_current_source.rb | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index 558b4c9..ea37cef 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -22,12 +22,17 @@ the candidate. It also scans tracked and non-ignored untracked worktree files for early feedback. It reports only paths and finding categories, never the matched content. -The supported categories are deliberately small: - -- private-key headers; -- GitHub, AWS, and OpenAI credential formats; -- explicit bearer credentials; and -- absolute macOS, Linux, and Windows user-home paths. +The supported lexical forms are deliberately small: + +- PEM, OpenSSH, PGP, and PuTTY private-key headers; +- GitHub `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, and `github_pat_` token + prefixes; +- AWS `AKIA`/`ASIA` access-key IDs and canonical named 40-character secret + access-key values; +- OpenAI `sk-` and `sk-proj-` key prefixes; +- explicit `Authorization: Bearer` header or map values; and +- literal or commonly source-escaped absolute macOS, Linux, and Windows + user-home paths, including bounded environment and command-option values. The home-path rule covers literal paths and common source escaping for POSIX slashes and Windows backslashes. It does not decode URL-encoded content. diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index 133a53f..9f7b161 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -9,7 +9,7 @@ "machine-local home path", %r{ (?:\A|[\s"'`:(,]) - (?:[A-Za-z_][A-Za-z0-9_.-]*\s*=\s*)? + (?:(?:[A-Za-z_][A-Za-z0-9_.-]*|--?[A-Za-z0-9][A-Za-z0-9._-]*)\s*=\s*)? (?: (?:/|\\/) (?: @@ -23,7 +23,14 @@ }x ], ["AWS access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/], - ["bearer credential", /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._~+\/=:-]{16,}/i], + [ + "AWS secret access key", + /\b(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\b\s*(?:=|:)\s*["']?[A-Za-z0-9\/=+]{40}(?=["'\s\r\n,}\]]|\z)/i + ], + [ + "bearer credential", + /["']?Authorization["']?\s*(?::|=>)\s*["']?Bearer\s+[A-Za-z0-9._~+\/=:-]{16,}/i + ], [ "GitHub token", /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/ @@ -32,7 +39,8 @@ [ "private key", /-----BEGIN (?:[A-Z0-9][A-Z0-9 ]* )?PRIVATE KEY(?: BLOCK)?-----/ - ] + ], + ["private key", /\APuTTY-User-Key-File-[23]:/] ].freeze INDEX_BLOB_MODES = %w[100644 100755 120000].freeze diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 5dbdb44..f646937 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -135,11 +135,14 @@ def test_supported_high_confidence_formats_are_detected_and_redacted escaped_slash = "\\" + "/" fixtures = { "aws.txt" => "AK" + "IA" + ("A" * 16), + "aws-secret.txt" => "AWS_SECRET_ACCESS_KEY=" + ("S" * 40), "bearer.txt" => "Authorization: " + "Bearer " + ("b" * 24), + "bearer-json.txt" => "{\"Authorization\":\"" + "Bearer " + ("e" * 24) + "\"}", "fine-grained.txt" => "github_" + "pat_" + ("C" * 24), "openai.txt" => "s" + "k-proj-" + ("D" * 24), "private-key.txt" => "-----BEGIN " + "OPENSSH PRIVATE KEY-----", "pgp-private-key.txt" => "-----BEGIN " + "PGP PRIVATE KEY BLOCK-----", + "putty-private-key.txt" => "PuTTY" + "-User-Key-File-3: ssh-ed25519", "posix-home.txt" => "/" + "Users/example/private.txt", "escaped-posix-home.txt" => [ escaped_slash, @@ -172,6 +175,7 @@ def test_supported_high_confidence_formats_are_detected_and_redacted end [ "AWS access key", + "AWS secret access key", "bearer credential", "GitHub token", "OpenAI API key", @@ -258,6 +262,20 @@ def test_home_assignment_is_still_detected end end + def test_command_option_home_assignment_is_detected + with_repo do |repo| + home = "/" + "home/example/private.txt" + stage(repo, "command.txt", "tool --cache=#{home}\n") + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "command.txt" + assert_includes stderr, "machine-local home path" + refute_includes stderr, home + end + end + def test_duplicate_index_blobs_report_each_candidate_path with_repo do |repo| token = github_token From 5d766f9fab4f1aecd228111df56c2c1c26b6dd78 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 14:58:32 +0800 Subject: [PATCH 07/15] Close quoted and read-only audit gaps Cover quoted canonical AWS names and Windows casing, and disable optional Git locks so split-index metadata stays untouched during verification. --- public-source-release-audit/SKILL.md | 3 ++- public-source-release-audit/scripts/check_current_source.rb | 5 +++-- .../tests/test_check_current_source.rb | 6 +++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index ea37cef..d62e447 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -43,7 +43,8 @@ not inspect Git history. Git LFS pointers fail the check because their external objects require separate review. Missing index blobs, including unavailable partial-clone objects, fail without network fetching. A symlink's own target text is scanned, while a symlinked parent component fails without reading -outside the repository. +outside the repository. Git's optional locks are disabled so read-only index +operations do not freshen split-index metadata. ## Semantic Review diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index 9f7b161..733c5f5 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -18,14 +18,14 @@ root(?:(?:/|\\/)|(?![A-Za-z0-9._-])) ) | - [A-Za-z]:[\\/]{1,2}Users[\\/]{1,2}[^\\/\r\n]+(?:[\\/]{1,2}|$) + [A-Za-z]:[\\/]{1,2}(?i:Users)[\\/]{1,2}[^\\/\r\n]+(?:[\\/]{1,2}|$) ) }x ], ["AWS access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/], [ "AWS secret access key", - /\b(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\b\s*(?:=|:)\s*["']?[A-Za-z0-9\/=+]{40}(?=["'\s\r\n,}\]]|\z)/i + /(?["']?)aws_secret_access_key\k(?![A-Za-z0-9_])\s*(?:=|:)\s*["']?[A-Za-z0-9\/=+]{40}(?=["'\s\r\n,}\]]|\z)/i ], [ "bearer credential", @@ -53,6 +53,7 @@ "GIT_NO_LAZY_FETCH" => "1", "GIT_NO_REPLACE_OBJECTS" => "1", "GIT_OBJECT_DIRECTORY" => nil, + "GIT_OPTIONAL_LOCKS" => "0", "GIT_WORK_TREE" => nil }.freeze diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index f646937..ebbfdee 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -136,6 +136,7 @@ def test_supported_high_confidence_formats_are_detected_and_redacted fixtures = { "aws.txt" => "AK" + "IA" + ("A" * 16), "aws-secret.txt" => "AWS_SECRET_ACCESS_KEY=" + ("S" * 40), + "aws-secret-json.txt" => "{\"AWS_SECRET_ACCESS_KEY\":\"" + ("J" * 40) + "\"}", "bearer.txt" => "Authorization: " + "Bearer " + ("b" * 24), "bearer-json.txt" => "{\"Authorization\":\"" + "Bearer " + ("e" * 24) + "\"}", "fine-grained.txt" => "github_" + "pat_" + ("C" * 24), @@ -154,6 +155,7 @@ def test_supported_high_confidence_formats_are_detected_and_redacted ].join, "root-home.txt" => "/" + "root/.ssh/id_ed25519", "windows-home.txt" => "C:" + "\\Users\\example\\private.txt", + "lowercase-windows-home.txt" => "c:" + "\\users\\example\\private.txt", "escaped-windows-home.txt" => [ "C:", "\\" * 2, @@ -292,7 +294,7 @@ def test_duplicate_index_blobs_report_each_candidate_path end end - def test_git_commands_disable_lazy_object_fetching + def test_git_commands_enforce_read_only_environment with_repo do |repo| stage(repo, "README.md", "Public documentation without credentials.\n") real_git = ENV.fetch("PATH").split(File::PATH_SEPARATOR).filter_map do |dir| @@ -306,6 +308,7 @@ def test_git_commands_disable_lazy_object_fetching File.write(fake_git, <<~RUBY) #!/usr/bin/env ruby abort "lazy fetching was not disabled" unless ENV["GIT_NO_LAZY_FETCH"] == "1" + abort "optional Git locks were not disabled" unless ENV["GIT_OPTIONAL_LOCKS"] == "0" exec #{real_git.dump}, *ARGV RUBY FileUtils.chmod(0o755, fake_git) @@ -314,6 +317,7 @@ def test_git_commands_disable_lazy_object_fetching repo, { "GIT_NO_LAZY_FETCH" => "0", + "GIT_OPTIONAL_LOCKS" => "1", "PATH" => [bin_dir, ENV.fetch("PATH")].join(File::PATH_SEPARATOR) } ) From b2c5e9f9f23332452e0a2d71fe2fe8d5b8bde50f Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 15:12:02 +0800 Subject: [PATCH 08/15] Tighten audit match boundaries Require the exact Authorization key and a non-empty Windows username so generic map keys and profiles-directory prose remain public-safe. --- .../scripts/check_current_source.rb | 4 ++-- .../tests/test_check_current_source.rb | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index 733c5f5..4d4baea 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -18,7 +18,7 @@ root(?:(?:/|\\/)|(?![A-Za-z0-9._-])) ) | - [A-Za-z]:[\\/]{1,2}(?i:Users)[\\/]{1,2}[^\\/\r\n]+(?:[\\/]{1,2}|$) + [A-Za-z]:[\\/]{1,2}(?i:Users)[\\/]{1,2}[A-Za-z0-9._-]+(?:[\\/]{1,2}|(?=["'\s,;:)\]\}]|\z)) ) }x ], @@ -29,7 +29,7 @@ ], [ "bearer credential", - /["']?Authorization["']?\s*(?::|=>)\s*["']?Bearer\s+[A-Za-z0-9._~+\/=:-]{16,}/i + /(?["']?)Authorization\k(?![A-Za-z0-9_.-])\s*(?::|=>)\s*["']?Bearer\s+[A-Za-z0-9._~+\/=:-]{16,}/i ], [ "GitHub token", diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index ebbfdee..4f5737f 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -237,10 +237,12 @@ def test_ignored_local_files_are_outside_the_release_candidate def test_unrelated_home_directory_and_web_route_do_not_match_user_homes with_repo do |repo| + windows_profiles = "C:" + "\\Users\\" stage(repo, "paths.txt", [ "/tmp/home/.local/bin/tool", "https://example.invalid/home/user/profile", - "https://example.invalid/login?next=/home/user/profile" + "https://example.invalid/login?next=/home/user/profile", + "Windows stores profiles under #{windows_profiles} by default." ].join("\n")) stdout, stderr, status = run_checker(repo) @@ -250,6 +252,18 @@ def test_unrelated_home_directory_and_web_route_do_not_match_user_homes end end + def test_similar_bearer_map_key_is_not_authorization + with_repo do |repo| + value = "Bearer " + ("a" * 24) + stage(repo, "map.json", "{\"NotAuthorization\":\"#{value}\"}\n") + + stdout, stderr, status = run_checker(repo) + + assert status.success?, stderr + assert_includes stdout, "current public-source check passed" + end + end + def test_home_assignment_is_still_detected with_repo do |repo| home = "/" + "home/example/private.txt" From 7e819cca3051c725dd03baf6f3e3f7955506337b Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 15:24:24 +0800 Subject: [PATCH 09/15] Fail closed on BOM-marked UTF-16 source Require separate review for UTF-16 index and worktree content instead of adding transcoding or allowing encoded supported credentials to pass. --- public-source-release-audit/SKILL.md | 3 ++- .../scripts/check_current_source.rb | 19 +++++++++++++++++-- .../tests/test_check_current_source.rb | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index d62e447..f5ee8c7 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -44,7 +44,8 @@ objects require separate review. Missing index blobs, including unavailable partial-clone objects, fail without network fetching. A symlink's own target text is scanned, while a symlinked parent component fails without reading outside the repository. Git's optional locks are disabled so read-only index -operations do not freshen split-index metadata. +operations do not freshen split-index metadata. BOM-marked UTF-16 source also +requires separate review; the checker does not transcode or unpack content. ## Semantic Review diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index 4d4baea..afddca2 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -45,6 +45,7 @@ INDEX_BLOB_MODES = %w[100644 100755 120000].freeze LFS_POINTER = %r{\Aversion https://git-lfs\.github\.com/spec/v1(?:\r?\n|\z)}.freeze +UTF16_BOMS = ["\xFF\xFE".b, "\xFE\xFF".b].freeze GIT_ENVIRONMENT = { "GIT_ALTERNATE_OBJECT_DIRECTORIES" => nil, "GIT_COMMON_DIR" => nil, @@ -129,7 +130,8 @@ def read_blob_results(repo, object_ids) results[object_id] = { labels: matching_labels(content), - lfs_pointer: LFS_POINTER.match?(content.b) + lfs_pointer: LFS_POINTER.match?(content.b), + utf16_bom: utf16_bom?(content) } end rescue IOError, SystemCallError @@ -151,6 +153,11 @@ def matching_labels(content) RULES.filter_map { |label, pattern| label if pattern.match?(source) } end +def utf16_bom?(content) + source = content.b + UTF16_BOMS.any? { |bom| source.start_with?(bom) } +end + def record_findings(labels, relative_path, findings, sensitive_paths = nil) labels.each do |label| findings.add([relative_path, label]) @@ -247,6 +254,9 @@ def symlinked_parent?(repo, relative_path) if result.fetch(:lfs_pointer) errors.add([relative_path, "Git LFS object requires separate review"]) end + if result.fetch(:utf16_bom) + errors.add([relative_path, "BOM-marked UTF-16 source requires separate review"]) + end record_findings(result.fetch(:labels), relative_path, findings) end end @@ -274,7 +284,12 @@ def symlinked_parent?(repo, relative_path) elsif File.file?(absolute_path) File.binread(absolute_path) end - scan_source(content, relative_path, findings) if content + if content + if utf16_bom?(content) + errors.add([relative_path, "BOM-marked UTF-16 source requires separate review"]) + end + scan_source(content, relative_path, findings) + end rescue SystemCallError errors.add([relative_path, "unable to read worktree source"]) end diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 4f5737f..fd2c85d 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -483,6 +483,21 @@ def test_git_lfs_pointer_requires_separate_review end end + def test_bom_marked_utf16_source_requires_separate_review + with_repo do |repo| + token = github_token + content = "\xFF\xFE".b + token.encode("UTF-16LE").b + stage(repo, "script.ps1", content) + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "script.ps1" + assert_includes stderr, "BOM-marked UTF-16 source requires separate review" + refute_includes stderr, token + end + end + def test_unresolved_index_entries_fail_closed with_repo do |repo| object_ids = %w[base ours theirs].map do |content| From 8178d9b1caffdc45efa8217921291c55670f9c4a Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 15:35:31 +0800 Subject: [PATCH 10/15] Align worktree LFS and root-home boundaries Apply the existing LFS fail-closed rule to worktree content and cover the exact canonical macOS root-home families without broadening the path contract. --- public-source-release-audit/SKILL.md | 6 ++-- .../scripts/check_current_source.rb | 5 +++ .../tests/test_check_current_source.rb | 31 +++++++++++++++---- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index f5ee8c7..41aa03b 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -31,8 +31,10 @@ The supported lexical forms are deliberately small: access-key values; - OpenAI `sk-` and `sk-proj-` key prefixes; - explicit `Authorization: Bearer` header or map values; and -- literal or commonly source-escaped absolute macOS, Linux, and Windows - user-home paths, including bounded environment and command-option values. +- literal or commonly source-escaped rooted POSIX families `Users/`, + `var/root`, `private/var/root`, `home/`, and `root`, plus drive-letter + `Users\\` paths, including bounded environment and command-option + values. The home-path rule covers literal paths and common source escaping for POSIX slashes and Windows backslashes. It does not decode URL-encoded content. diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index afddca2..e6cb7ed 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -16,6 +16,8 @@ (?:Users|home)(?:/|\\/)[A-Za-z0-9._-]+(?:(?:/|\\/)|\b) | root(?:(?:/|\\/)|(?![A-Za-z0-9._-])) + | + (?:private(?:/|\\/))?var(?:/|\\/)root(?:(?:/|\\/)|(?![A-Za-z0-9._-])) ) | [A-Za-z]:[\\/]{1,2}(?i:Users)[\\/]{1,2}[A-Za-z0-9._-]+(?:[\\/]{1,2}|(?=["'\s,;:)\]\}]|\z)) @@ -285,6 +287,9 @@ def symlinked_parent?(repo, relative_path) File.binread(absolute_path) end if content + if LFS_POINTER.match?(content.b) + errors.add([relative_path, "Git LFS object requires separate review"]) + end if utf16_bom?(content) errors.add([relative_path, "BOM-marked UTF-16 source requires separate review"]) end diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index fd2c85d..1ef0984 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -34,6 +34,14 @@ def github_token "gh" + "p_" + ("A" * 24) end + def lfs_pointer + [ + "version https://git-lfs.github.com/spec/v1", + "oid sha256:#{'a' * 64}", + "size 123" + ].join("\n") + "\n" + end + def test_safe_candidate_passes with_repo do |repo| stage(repo, "README.md", "Public documentation without credentials.\n") @@ -154,6 +162,8 @@ def test_supported_high_confidence_formats_are_detected_and_redacted "private.txt" ].join, "root-home.txt" => "/" + "root/.ssh/id_ed25519", + "macos-root-home.txt" => "/" + "var/root/.ssh/id_ed25519", + "macos-private-root-home.txt" => "/" + "private/var/root/.ssh/id_ed25519", "windows-home.txt" => "C:" + "\\Users\\example\\private.txt", "lowercase-windows-home.txt" => "c:" + "\\users\\example\\private.txt", "escaped-windows-home.txt" => [ @@ -467,12 +477,21 @@ def test_non_utf8_git_paths_are_scanned_without_crashing def test_git_lfs_pointer_requires_separate_review with_repo do |repo| - pointer = [ - "version https://git-lfs.github.com/spec/v1", - "oid sha256:#{'a' * 64}", - "size 123" - ].join("\n") + "\n" - stage(repo, "large.dat", pointer) + stage(repo, "large.dat", lfs_pointer) + + _stdout, stderr, status = run_checker(repo) + + refute status.success? + assert_includes stderr, "large.dat" + assert_includes stderr, "Git LFS object requires separate review" + refute_includes stderr, "sha256:" + end + end + + def test_unstaged_git_lfs_pointer_requires_separate_review + with_repo do |repo| + stage(repo, "large.dat", "safe candidate\n") + write(repo, "large.dat", lfs_pointer) _stdout, stderr, status = run_checker(repo) From b423dab9fd6ea486aa69c5ca9c9dfc5a280a6cdd Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 15:45:10 +0800 Subject: [PATCH 11/15] Accept markup home-path boundaries Treat a closing markup delimiter as a bounded literal-path context and cover the supported plist/XML element form. --- public-source-release-audit/scripts/check_current_source.rb | 2 +- public-source-release-audit/tests/test_check_current_source.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index e6cb7ed..190c6b4 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -8,7 +8,7 @@ [ "machine-local home path", %r{ - (?:\A|[\s"'`:(,]) + (?:\A|[\s"'`>:(,]) (?:(?:[A-Za-z_][A-Za-z0-9_.-]*|--?[A-Za-z0-9][A-Za-z0-9._-]*)\s*=\s*)? (?: (?:/|\\/) diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 1ef0984..39629d5 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -152,6 +152,7 @@ def test_supported_high_confidence_formats_are_detected_and_redacted "private-key.txt" => "-----BEGIN " + "OPENSSH PRIVATE KEY-----", "pgp-private-key.txt" => "-----BEGIN " + "PGP PRIVATE KEY BLOCK-----", "putty-private-key.txt" => "PuTTY" + "-User-Key-File-3: ssh-ed25519", + "markup-home.txt" => "" + "/" + "Users/example/private.txt", "posix-home.txt" => "/" + "Users/example/private.txt", "escaped-posix-home.txt" => [ escaped_slash, From 6127a220f4d70d6af0641493d97125cbe024f804 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 16:00:04 +0800 Subject: [PATCH 12/15] Bound international and file-URI home paths Support UTF-8 profile components and a specific local file wrapper while explicitly excluding nested escaped-document decoding from the lexical audit. --- public-source-release-audit/SKILL.md | 12 +++++++----- .../scripts/check_current_source.rb | 8 ++++---- .../tests/test_check_current_source.rb | 3 +++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index 41aa03b..3548608 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -31,13 +31,15 @@ The supported lexical forms are deliberately small: access-key values; - OpenAI `sk-` and `sk-proj-` key prefixes; - explicit `Authorization: Bearer` header or map values; and -- literal or commonly source-escaped rooted POSIX families `Users/`, - `var/root`, `private/var/root`, `home/`, and `root`, plus drive-letter - `Users\\` paths, including bounded environment and command-option - values. +- literal, commonly source-escaped, or `file://`-wrapped rooted POSIX families + `Users/`, `var/root`, `private/var/root`, `home/`, and `root`, plus + drive-letter `Users\\` paths, including UTF-8 profile names and bounded + environment and command-option values. The home-path rule covers literal paths and common source escaping for POSIX -slashes and Windows backslashes. It does not decode URL-encoded content. +slashes and Windows backslashes. It does not decode URL-encoded content. The +credential map rules inspect direct source text; they do not decode nested +escaped documents. Stage all intended release changes before relying on the candidate result. Ignored files and submodule contents are outside this check. The checker does diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index 190c6b4..b803a43 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -8,21 +8,21 @@ [ "machine-local home path", %r{ - (?:\A|[\s"'`>:(,]) + (?:\A|[\s"'`>:(,]|(? "/" + "root/.ssh/id_ed25519", "macos-root-home.txt" => "/" + "var/root/.ssh/id_ed25519", "macos-private-root-home.txt" => "/" + "private/var/root/.ssh/id_ed25519", + "unicode-posix-home.txt" => "/" + "home/山田/private.txt", + "file-uri-home.txt" => "file://" + "/" + "home/example/private.txt", "windows-home.txt" => "C:" + "\\Users\\example\\private.txt", "lowercase-windows-home.txt" => "c:" + "\\users\\example\\private.txt", + "unicode-windows-home.txt" => "C:" + "\\Users\\山田\\private.txt", "escaped-windows-home.txt" => [ "C:", "\\" * 2, From 41c646c1da204db2688cf22c0412187d337b0668 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 16:12:51 +0800 Subject: [PATCH 13/15] Bound terminal UTF-8 profile paths Use an explicit source delimiter for terminal POSIX profile names so international names do not depend on ASCII word-boundary behavior. --- public-source-release-audit/scripts/check_current_source.rb | 2 +- public-source-release-audit/tests/test_check_current_source.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index b803a43..f2cc601 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -13,7 +13,7 @@ (?: (?:/|\\/) (?: - (?:Users|home)(?:/|\\/)[A-Za-z0-9._\x80-\xFF-]+(?:(?:/|\\/)|\b) + (?:Users|home)(?:/|\\/)[A-Za-z0-9._\x80-\xFF-]+(?:(?:/|\\/)|(?=[<>"'\s,;:)\]\}]|\z)) | root(?:(?:/|\\/)|(?![A-Za-z0-9._-])) | diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 1120322..91ebded 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -166,6 +166,7 @@ def test_supported_high_confidence_formats_are_detected_and_redacted "macos-root-home.txt" => "/" + "var/root/.ssh/id_ed25519", "macos-private-root-home.txt" => "/" + "private/var/root/.ssh/id_ed25519", "unicode-posix-home.txt" => "/" + "home/山田/private.txt", + "terminal-unicode-posix-home.txt" => "/" + "home/山田\n", "file-uri-home.txt" => "file://" + "/" + "home/example/private.txt", "windows-home.txt" => "C:" + "\\Users\\example\\private.txt", "lowercase-windows-home.txt" => "c:" + "\\users\\example\\private.txt", From 9386cb99d3c0fea3fa46ac625afe80aa430f9264 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 16:26:21 +0800 Subject: [PATCH 14/15] Fail closed on unreadable worktree source Use lstat for listed paths, clear inherited Git trace destinations, and align the terminal Windows markup boundary without expanding detection families. --- public-source-release-audit/SKILL.md | 5 ++-- .../scripts/check_current_source.rb | 22 +++++++++++++-- .../tests/test_check_current_source.rb | 28 +++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/public-source-release-audit/SKILL.md b/public-source-release-audit/SKILL.md index 3548608..e0b68f2 100644 --- a/public-source-release-audit/SKILL.md +++ b/public-source-release-audit/SKILL.md @@ -48,8 +48,9 @@ objects require separate review. Missing index blobs, including unavailable partial-clone objects, fail without network fetching. A symlink's own target text is scanned, while a symlinked parent component fails without reading outside the repository. Git's optional locks are disabled so read-only index -operations do not freshen split-index metadata. BOM-marked UTF-16 source also -requires separate review; the checker does not transcode or unpack content. +operations do not freshen split-index metadata, and inherited Git trace-file +destinations are cleared. BOM-marked UTF-16 source also requires separate +review; the checker does not transcode or unpack content. ## Semantic Review diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index f2cc601..7c9d14d 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -20,7 +20,7 @@ (?:private(?:/|\\/))?var(?:/|\\/)root(?:(?:/|\\/)|(?![A-Za-z0-9._-])) ) | - [A-Za-z]:[\\/]{1,2}(?i:Users)[\\/]{1,2}[A-Za-z0-9._\x80-\xFF-]+(?:[\\/]{1,2}|(?=["'\s,;:)\]\}]|\z)) + [A-Za-z]:[\\/]{1,2}(?i:Users)[\\/]{1,2}[A-Za-z0-9._\x80-\xFF-]+(?:[\\/]{1,2}|(?=[<>"'\s,;:)\]\}]|\z)) ) }xn ], @@ -57,6 +57,16 @@ "GIT_NO_REPLACE_OBJECTS" => "1", "GIT_OBJECT_DIRECTORY" => nil, "GIT_OPTIONAL_LOCKS" => "0", + "GIT_TRACE" => nil, + "GIT_TRACE2" => nil, + "GIT_TRACE2_EVENT" => nil, + "GIT_TRACE2_PERF" => nil, + "GIT_TRACE_CURL" => nil, + "GIT_TRACE_PACK_ACCESS" => nil, + "GIT_TRACE_PACKET" => nil, + "GIT_TRACE_PERFORMANCE" => nil, + "GIT_TRACE_SETUP" => nil, + "GIT_TRACE_SHALLOW" => nil, "GIT_WORK_TREE" => nil }.freeze @@ -281,10 +291,14 @@ def symlinked_parent?(repo, relative_path) next end absolute_path = File.join(repo.b, relative_path) - content = if File.symlink?(absolute_path) + stat = File.lstat(absolute_path) + content = if stat.symlink? File.readlink(absolute_path) - elsif File.file?(absolute_path) + elsif stat.file? File.binread(absolute_path) + else + errors.add([relative_path, "unsupported worktree source type"]) + next end if content if LFS_POINTER.match?(content.b) @@ -295,6 +309,8 @@ def symlinked_parent?(repo, relative_path) end scan_source(content, relative_path, findings) end + rescue Errno::ENOENT + next rescue SystemCallError errors.add([relative_path, "unable to read worktree source"]) end diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 91ebded..754e16b 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -171,6 +171,7 @@ def test_supported_high_confidence_formats_are_detected_and_redacted "windows-home.txt" => "C:" + "\\Users\\example\\private.txt", "lowercase-windows-home.txt" => "c:" + "\\users\\example\\private.txt", "unicode-windows-home.txt" => "C:" + "\\Users\\山田\\private.txt", + "terminal-markup-windows-home.txt" => "" + "C:" + "\\Users\\example" + "", "escaped-windows-home.txt" => [ "C:", "\\" * 2, @@ -333,11 +334,13 @@ def test_git_commands_enforce_read_only_environment refute_nil real_git Dir.mktmpdir("public-source-check-bin-") do |bin_dir| + trace_path = File.join(bin_dir, "git-trace.log") fake_git = File.join(bin_dir, "git") File.write(fake_git, <<~RUBY) #!/usr/bin/env ruby abort "lazy fetching was not disabled" unless ENV["GIT_NO_LAZY_FETCH"] == "1" abort "optional Git locks were not disabled" unless ENV["GIT_OPTIONAL_LOCKS"] == "0" + abort "Git tracing was not disabled" if ENV.key?("GIT_TRACE") exec #{real_git.dump}, *ARGV RUBY FileUtils.chmod(0o755, fake_git) @@ -347,16 +350,41 @@ def test_git_commands_enforce_read_only_environment { "GIT_NO_LAZY_FETCH" => "0", "GIT_OPTIONAL_LOCKS" => "1", + "GIT_TRACE" => trace_path, "PATH" => [bin_dir, ENV.fetch("PATH")].join(File::PATH_SEPARATOR) } ) assert status.success?, stderr assert_includes stdout, "current public-source check passed" + refute File.exist?(trace_path), "checker wrote inherited Git trace output" end end end + def test_inaccessible_worktree_source_fails_closed + skip "permission test requires a non-root user" if Process.uid.zero? + + with_repo do |repo| + token = github_token + stage(repo, "private/file.txt", "safe candidate\n") + write(repo, "private/file.txt", token) + directory = File.join(repo, "private") + FileUtils.chmod(0o000, directory) + + begin + _stdout, stderr, status = run_checker(repo) + ensure + FileUtils.chmod(0o755, directory) + end + + refute status.success? + assert_includes stderr, "private/file.txt" + assert_includes stderr, "unable to read worktree source" + refute_includes stderr, token + end + end + def test_repository_fsmonitor_hook_is_disabled with_repo do |repo| stage(repo, "README.md", "Public documentation without credentials.\n") From 5a6a4ad23afb9f12cb3ab443797da58191f11695 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2026 16:36:27 +0800 Subject: [PATCH 15/15] Honor the submodule audit boundary Track mode-160000 paths from the index and skip their initialized worktree directories while retaining path-name scanning and all other fail-closed checks. --- .../scripts/check_current_source.rb | 8 ++++- .../tests/test_check_current_source.rb | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/public-source-release-audit/scripts/check_current_source.rb b/public-source-release-audit/scripts/check_current_source.rb index 7c9d14d..19fb698 100755 --- a/public-source-release-audit/scripts/check_current_source.rb +++ b/public-source-release-audit/scripts/check_current_source.rb @@ -239,10 +239,14 @@ def symlinked_parent?(repo, relative_path) end blob_paths = Hash.new { |paths, object_id| paths[object_id] = [] } +submodule_paths = Set.new index_entries.each do |entry| relative_path = entry.fetch(:path) scan_source(relative_path, relative_path, findings, sensitive_paths) - next if entry.fetch(:mode) == "160000" + if entry.fetch(:mode) == "160000" + submodule_paths.add(relative_path) + next + end unless INDEX_BLOB_MODES.include?(entry.fetch(:mode)) errors.add([relative_path, "unsupported Git index mode"]) @@ -285,6 +289,8 @@ def symlinked_parent?(repo, relative_path) worktree_paths = worktree_output.b.split("\0".b).reject(&:empty?).uniq worktree_paths.each do |relative_path| scan_source(relative_path, relative_path, findings, sensitive_paths) + next if submodule_paths.include?(relative_path) + begin if symlinked_parent?(repo, relative_path) errors.add([relative_path, "symlinked parent component"]) diff --git a/public-source-release-audit/tests/test_check_current_source.rb b/public-source-release-audit/tests/test_check_current_source.rb index 754e16b..242e512 100644 --- a/public-source-release-audit/tests/test_check_current_source.rb +++ b/public-source-release-audit/tests/test_check_current_source.rb @@ -251,6 +251,37 @@ def test_ignored_local_files_are_outside_the_release_candidate end end + def test_initialized_submodule_directory_is_outside_the_check + with_repo do |repo| + object_id, error, status = Open3.capture3( + "git", + "-C", + repo, + "hash-object", + "-w", + "--stdin", + stdin_data: "placeholder" + ) + assert status.success?, error + system( + "git", + "-C", + repo, + "update-index", + "--add", + "--cacheinfo", + "160000,#{object_id.strip},module", + exception: true + ) + FileUtils.mkdir_p(File.join(repo, "module")) + + stdout, stderr, checker_status = run_checker(repo) + + assert checker_status.success?, stderr + assert_includes stdout, "current public-source check passed" + end + end + def test_unrelated_home_directory_and_web_route_do_not_match_user_homes with_repo do |repo| windows_profiles = "C:" + "\\Users\\"