From c0e3589da72116d4a5d6c4e6e4f1c55a9b0ac4da Mon Sep 17 00:00:00 2001 From: AndreasS Date: Thu, 1 Oct 2026 11:50:28 +0200 Subject: [PATCH] Redact home paths from this public repo, and add a test so it cannot recur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repository has been PUBLIC since it was created on 2026-06-17. How it happened, from the history: - 2026-07-25 — a launch-readiness change ran a scan for credential prefixes, /Users/ and 192.168., recorded "no matches", and closed the question. That scan was TRUE when it ran. - 2026-07-31 — cross_repo_test.go was added with real absolute paths as test fixtures. Six days after the scan. Test fixtures read as code, so it is exactly the content a scan-then-stop misses. - 2026-09-17 — an archived proposal carried two real worktree paths in its illustrative JSON. - Nothing re-checked for the following two months. Redacted (5 occurrences, 3 files): - cross_repo_test.go: the two fixtures now use /home/user/dev/..., matching the generic style already on the next line. The test exercises path-to-repo-name extraction, not these particular directories, so its intent is unchanged. - archive/2026-09-17-agent-topology-command/proposal.md: two illustrative paths in the work-item JSON. - archive/2026-07-25-launch-readiness/proposal.md: the scan record keeps what it searched for but writes the shape as /Users/. Also adds TestNoPrivateDataInPublicRepo, run by `go test ./...` — which ci.yml already invokes, so it cannot be skipped by forgetting a step, and it fails locally too. It flags /Users/, RFC1918 dotted quads, and credential prefixes. Placeholders like /Users/ and prose such as "192.168." are not matched, so documentation can still describe the shape. The point is not the redaction, it is that a manual scan recorded as complete drifted within a week. A check that only runs when someone remembers is not a check. Confirmed the test fails against a re-introduced home path and against an injected ghp_ token. Full suite: 852 passed. --- cross_repo_test.go | 4 +- .../2026-07-25-launch-readiness/proposal.md | 2 +- .../proposal.md | 4 +- private_data_test.go | 96 +++++++++++++++++++ 4 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 private_data_test.go diff --git a/cross_repo_test.go b/cross_repo_test.go index 6f8b3fd..473ccd5 100644 --- a/cross_repo_test.go +++ b/cross_repo_test.go @@ -9,8 +9,8 @@ func TestExtractRepoFromDir(t *testing.T) { dir string want string }{ - {"/Users/andreas/dev/brick-now/openspec", "brick-now"}, - {"/Users/andreas/dev/tengil/openspec/changes/foo", "tengil"}, + {"/home/user/dev/brick-now/openspec", "brick-now"}, + {"/home/user/dev/tengil/openspec/changes/foo", "tengil"}, {"/home/user/project/openspec", "project"}, {"/openspec", ""}, // edge case: no parent {"openspec", "openspec"}, // bare openspec — no parent, returns full dir diff --git a/openspec/changes/archive/2026-07-25-launch-readiness/proposal.md b/openspec/changes/archive/2026-07-25-launch-readiness/proposal.md index 0c183a7..b132931 100644 --- a/openspec/changes/archive/2026-07-25-launch-readiness/proposal.md +++ b/openspec/changes/archive/2026-07-25-launch-readiness/proposal.md @@ -69,7 +69,7 @@ owner-only decisions. - Grep for `/Users/`, `/home/`, `192.168.`, `10.0.`, personal emails — no matches in tracked files. - History spot checks (`git log --all -S`) for `ghp_`, `github_pat_`, - `sk-ant`, `/Users/andreas`, `192.168.` — no matches. + `sk-ant`, `/Users/`, `192.168.` — no matches. - Commit author metadata contains the owner's work and personal email addresses. This is ordinary git metadata, visible on any public repo, and not a secret; noted only so the owner can consciously accept it. diff --git a/openspec/changes/archive/2026-09-17-agent-topology-command/proposal.md b/openspec/changes/archive/2026-09-17-agent-topology-command/proposal.md index e11fca2..eb0c07e 100644 --- a/openspec/changes/archive/2026-09-17-agent-topology-command/proposal.md +++ b/openspec/changes/archive/2026-09-17-agent-topology-command/proposal.md @@ -44,13 +44,13 @@ JSON shape, one entry per change: "slug": "add-field-forms", "title": "Auto-render field forms", "stage": "active", - "store": "/Users/andreas/exopen/portal", + "store": "/home/user/projects/portal", "repo": "ExopenGitHub/portal", "issue": {"provider": "github:ExopenGitHub/portal", "id": "4231", "url": "https://github.com/ExopenGitHub/portal/issues/4231", "state": "open"}, "branch": "feat/4231-add-field-forms", - "worktree": "/Users/andreas/exopen/portal-worktrees/add-field-forms", + "worktree": "/home/user/projects/portal-worktrees/add-field-forms", "epic": "https://github.com/ExopenGitHub/planning/issues/88", "linked": ["add-field-forms-api"] } diff --git a/private_data_test.go b/private_data_test.go new file mode 100644 index 0000000..fe86044 --- /dev/null +++ b/private_data_test.go @@ -0,0 +1,96 @@ +package specsync + +// This repository is PUBLIC. A check that only runs when someone remembers to +// run it will drift — and did. A scan on 2026-07-25 found no home paths and was +// recorded as done; six days later cross_repo_test.go added real absolute paths +// as test fixtures, and nothing re-checked for two months. +// +// So the check runs in `go test ./...`, which ci.yml already invokes. It +// cannot be skipped by forgetting a CI step, and it fails locally too. + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +var ( + // /Users/ — a real macOS home directory. Placeholders + // like /Users/ are deliberately excluded by requiring lowercase + // letters, so documentation can still describe the shape of a path. + homePath = regexp.MustCompile(`/Users/[a-z][a-z0-9._-]*`) + + // RFC1918 address literals. A dotted quad only; prose mentioning + // "192.168." as a thing to scan for is not a match. + privateIP = regexp.MustCompile( + `\b(?:10\.[0-9]{1,3}|192\.168|172\.(?:1[6-9]|2[0-9]|3[01]))\.[0-9]{1,3}\.[0-9]{1,3}\b`) + + // Credential prefixes. Belt and braces: the check that matters most is + // the one nobody should ever need. + credential = regexp.MustCompile( + `\b(?:ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|` + + `ABSK[A-Z0-9]{10,}|AKIA[0-9A-Z]{16}|sk-ant-[A-Za-z0-9_-]{20,})\b`) +) + +// skippedNever: this test is meaningless if the repository stops being public, +// but it is also not harmful then, so it runs unconditionally and says so. +func TestNoPrivateDataInPublicRepo(t *testing.T) { + root := "." + if _, err := os.Stat("go.mod"); err != nil { + root = "." // running from a subpackage; paths still resolve upward + } + + var offenders []string + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return nil // unreadable path: not a privacy failure + } + name := d.Name() + if d.IsDir() { + // Skip VCS, build and dependency trees. They are not published + // as repository content and would drown the signal. + switch name { + case ".git", "node_modules", "site", "dist", "vendor": + return filepath.SkipDir + } + return nil + } + switch filepath.Ext(name) { + case ".go", ".md", ".yml", ".yaml", ".json", ".sh", ".txt", ".toml": + default: + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return nil + } + for i, line := range strings.Split(string(data), "\n") { + for _, re := range []struct { + what string + re *regexp.Regexp + }{ + {"home path", homePath}, + {"private IP", privateIP}, + {"credential", credential}, + } { + if re.re.FindString(line) != "" { + offenders = append(offenders, fmt.Sprintf("%s:%d: %s: %s", + path, i+1, re.what, strings.TrimSpace(line))) + } + } + } + return nil + }) + if err != nil { + t.Fatalf("walk: %v", err) + } + + if len(offenders) > 0 { + t.Errorf("this repository is PUBLIC but contains private data "+ + "(%d occurrence(s)). Redact before pushing:\n %s", + len(offenders), strings.Join(offenders, "\n ")) + } +}