Skip to content

fix: changed_files resolved to zero files in the Docker container action (git dubious ownership) - #105

Open
David Larsen (dc-larsen) wants to merge 3 commits into
mainfrom
dc-larsen/fix-changed-files-safe-directory
Open

fix: changed_files resolved to zero files in the Docker container action (git dubious ownership)#105
David Larsen (dc-larsen) wants to merge 3 commits into
mainfrom
dc-larsen/fix-changed-files-safe-directory

Conversation

@dc-larsen

@dc-larsen David Larsen (dc-larsen) commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Problem

changed_files diff-only mode resolves to zero files on every PR when Socket Basics runs as the pre-built Docker container action. Each run logs No scan targets to analyze (scoped scan matched no existing files); skipping OpenGrep and exits green with zero alerts. The scan looks healthy while scanning nothing. Full-repo mode on the same checkout works. A customer hit this rolling the action out org-wide on v2.2.1 (eng-support thread: https://socketdev.slack.com/archives/C05TT2Q3FEZ/p1786394320795579), and it reproduces identically on 3.0.0.

Root cause

GitHub runs Docker container actions as root, while the checkout mounted at /github/workspace is owned by the runner user (uid 1001). Git 2.35.2+ refuses to read a repository owned by a different user. Every git subprocess in _detect_git_changed_files fails, the helper catches the CalledProcessError and returns [], and the empty scope makes every scanner skip.

actions/checkout does not cover this case: its safe.directory entry is written to /home/runner/.gitconfig, but container actions run with HOME=/github/home. A prior workflow step running git config --global --add safe.directory misses the container for the same reason.

The same mismatch breaks the git fallbacks in _discover_repository, _discover_branch, _discover_commit_hash, and _discover_is_default_branch. That surface shows up in local Docker runs, where no GITHUB_* env vars mask it.

Fix

A _git_env() helper injects safe.directory=<workspace> into the environment of each git subprocess via command-scope GIT_CONFIG_COUNT / GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n entries. Git honors command-scope safe.directory since 2.38, and the image ships 2.47.

  • No git config files are written, and nothing outside these subprocesses changes.
  • Caller-provided GIT_CONFIG_* entries are preserved, with ours appended after them, so users who already deployed the env-var workaround are unaffected.
  • The workspace is an explicit scan target, not an incidentally discovered repository, so marking it safe matches what the user asked the tool to do.

Testing

Unit (tests/test_changed_files_scope.py)

  • TestDubiousOwnership drives the real git ownership check end to end using GIT_TEST_ASSUME_DIFFERENT_OWNER, git's own test knob for this code path. A control probe skips the tests on a git build without the knob. Both tests fail on the unpatched code and pass with the fix. I confirmed that by disabling the injection and re-running.
  • TestGitEnv covers appending after caller entries, a malformed GIT_CONFIG_COUNT, and the GITHUB_WORKSPACE default.
  • Full suite: 222 passed.

Published container images

Repo owned by uid 1001, process as root, GITHUB_BASE_REF=main, comment-only Go change:

Case Image Result
Unpatched, flags given 3.0.0 skip, Total alerts: 0 (bug)
Unpatched, no --repo/--branch 3.0.0 repository discovery fails (bug, second surface)
Patched 3.0.0 diff resolves, seeded finding reported, Total alerts: 1
Patched, no --repo/--branch 3.0.0 git discovery works, Total alerts: 1
Patched 2.2.1 Total alerts: 1
Patched + user GIT_CONFIG_* block already set 3.0.0 Total alerts: 1 (workaround coexists)
Patched, root-owned repo (ownership matches) 3.0.0 Total alerts: 1 (no regression)
Patched, delete-only PR 3.0.0 skip, Total alerts: 0 (empty-diff semantics preserved)

Real GitHub Actions A/B

One workflow, two jobs on a comment-only Go-file PR in a scratch repo. Each job builds this action from source at a different ref. The rig branches on my fork swap image: to Dockerfile and the trivy base to the public upstream, and are otherwise identical to base and fix.

  • baseline (unpatched main): No scan targets to analyze (scoped scan matched no existing files); skipping OpenGrep, Total alerts: 0, job passed green. That is the customer symptom on a real runner.
  • fix (this branch): the diff resolved to svc/main.go, OpenGrep ran on it, and the job failed the check with Total alerts: 1 and Found 1 high/critical severity issues. Failing the check is the intended blocking behavior for a seeded critical finding.

Run: https://github.com/dc-larsen/sb-changed-files-test/actions/runs/31432924728 (private scratch repo, log lines quoted verbatim above). The rig branches test-ownership-baseline and test-ownership-fix on dc-larsen/socket-basics reproduce this against any scratch repo.

Notes for review

  • fix/changed-files-scope-observability is complementary: it makes this failure loud instead of silent. One interaction: its troubleshooting doc recommends git config --global --add safe.directory in a prior step, which cannot reach the container action. After this lands, that row can be dropped or swapped for the env-var form.
  • CHANGELOG entry added under Unreleased.

Note

Medium Risk
Changes CI scan scope and fail-open behavior (full-repo fallback on git errors), which can increase scan volume and findings versus the previous silent skip; git env injection is localized and well-tested.

Overview
Fixes changed_files diff-only mode resolving to zero files in the pre-built Docker GitHub Action (root container vs runner-owned checkout → git “dubious ownership”). Adds _git_env() to inject command-scope safe.directory via GIT_CONFIG_* on git subprocesses (no config files; preserves existing GIT_CONFIG_* workarounds), and applies it to changed-file detection plus repo/branch/commit/default-branch discovery.

_detect_git_changed_files now captures stderr, returns None on hard failures vs [] for a real empty diff, and avoids falling back to staged diff in PR CI when the base ref is missing. Config resolution via _apply_scoped_changed_files logs scope at INFO/DEBUG, and on None falls back to a full-repo scan with warnings instead of a green skip. Trivy/TruffleHog treat None from detection as empty via or [] for staged fallbacks. Tests cover dubious ownership, _git_env, and failure vs empty-diff behavior.

Reviewed by Cursor Bugbot for commit 3e836bd. Configure here.

…sses

The pre-built Docker action runs as root while the checkout at
GITHUB_WORKSPACE is owned by the runner user, so git's ownership check
(git 2.35.2+) refused the repository. changed_files diff-only mode then
resolved to zero files on every PR and the scanners silently skipped with
a green run. Git-based repository/branch/commit and default-branch
discovery failed the same way in local Docker runs. actions/checkout's
own safe.directory entry cannot help: it lands in the runner's global
config, which is not mounted into container actions.

Inject safe.directory for the scan workspace into the environment of
each git subprocess via command-scope GIT_CONFIG_* entries. No config
files are touched, and caller-provided GIT_CONFIG_* entries (including
the previously documented env-block workaround) are appended after, not
clobbered.

Tested:
- unit: new TestDubiousOwnership tests drive the real git ownership
  check via GIT_TEST_ASSUME_DIFFERENT_OWNER; they fail on the unpatched
  code and pass with the fix. TestGitEnv covers append-after-caller,
  garbage GIT_CONFIG_COUNT, and the GITHUB_WORKSPACE default. Full
  suite: 222 passed.
- container: on the published 3.0.0 and 2.2.1 images with a uid-1001
  checkout and a root process, unpatched runs skip with zero targets;
  patched runs resolve the PR diff and report the seeded finding, with
  and without a pre-existing user GIT_CONFIG_* block. Same-owner and
  delete-only-PR behavior unchanged.
@dc-larsen
David Larsen (dc-larsen) requested a review from a team as a code owner August 10, 2026 21:18
@lelia lelia self-assigned this Aug 10, 2026
@lelia

lelia commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

bugbot run

Comment thread socket_basics/core/config.py Outdated
Git ignores relative safe.directory values, so a relative --workspace
under an ownership mismatch still failed the check and the diff kept
resolving to empty. Resolve the path before writing the entry.

Regression tests: a relative --workspace now passes the end-to-end
ownership test, and TestGitEnv asserts the injected value is absolute.
Both fail without this change. Full suite: 224 passed. Re-ran the
container check on the published 3.0.0 image: unchanged, finding still
reported.
A git failure during scope resolution previously collapsed into the same
empty list as a genuinely empty diff, so any future breakage (beyond the
safe.directory fix) would again skip every scanner and report green.

- _detect_git_changed_files now captures git stderr (instead of DEVNULL),
  logs the failure reason, and returns None on failure vs [] for a truly
  empty diff. Ref-not-found is classified separately so the base-ref
  candidate loop still falls through, while unreadable-repo errors fail fast.
- On failed resolution the config layer falls back to a full-repo scan with
  a prominent warning, never a silent zero-file skip. Delete-only diffs keep
  the empty-scope skip (existing test still guards this).
- An unresolvable base ref in a PR context (e.g. shallow fetch) is now a
  failure rather than a quiet fall-through to the usually-empty staged diff.
- Resolved scope is logged: file count at INFO, full list at DEBUG (the
  customer ask from the report).
- Connector-internal staged-diff callers get 'or []' for the new contract.

Full suite: 231 passed.

Signed-off-by: lelia <2418071+lelia@users.noreply.github.com>
@lelia

lelia commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

bugbot run

@lelia

lelia commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

David Larsen (@dc-larsen) Heads up — I pushed 3e836bd onto your branch so the observability half from the support report ships in this same PR/release. Built directly on your work — the root-cause fix and the GIT_TEST_ASSUME_DIFFERENT_OWNER test approach are great.

What it adds: detection now distinguishes failed resolution (None) from a genuinely empty diff ([]); git stderr is captured and logged instead of discarded; ref-not-found stays a soft miss (your candidate-loop and delete-only behavior are preserved, tests still green) while unreadable-repo errors fail fast. On failure, the config layer falls back to a full-repo scan with a prominent warning instead of silently skipping every scanner — that also now applies to an unresolvable base ref in a PR context (e.g. shallow fetch), which previously fell through to the usually-empty staged diff. Resolved scope is logged every run (count at INFO, list at DEBUG) — the direct ask. Four connector-internal staged callers got or [] for the new contract. 7 new tests, full suite 231 passing, CHANGELOG extended.

One thing worth your eyes specifically: the full-scan-fallback and shallow-fetch-base cases are deliberate semantic changes from "skip silently" — flag if you know a customer scenario where that's the wrong call.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 3e836bd. Configure here.

@dc-larsen

Copy link
Copy Markdown
Contributor Author

Verified 3e836bd end to end on the container rig: the ownership case still resolves and alerts, delete-only PRs still skip, and the missing-base case warns with the real git error before full-scanning. 231 tests pass locally, and the stderr capture with the dubious-ownership hint is exactly what support needed.

On your question, two customer scenarios worth weighing, both from the account that hit the original bug:

  1. Very large repos choose diff-only mode partly because full-repo is the documented OOM path. Their main monorepo is 3.8 GB. A missing fetch-depth: 0 is deterministic, so with the fallback every PR in a misconfigured repo runs a full scan, and on a repo that size that's an OOM or timeout red check rather than a clean signal about the real problem.

  2. Their full-repo run reports 851 findings, 28 high/critical. In PR context the fallback exits non-zero on pre-existing findings, so one org-wide workflow-template mistake blocks every PR with a very large comment, and the red check reads as "you have hundreds of findings" rather than "add fetch-depth: 0".

Both are loud and discoverable, which beats the silent skip. If you want to blunt them, the deterministic base-unresolvable case could fail fast with a config error naming the one-line fix, keeping the full-scan fallback for the non-deterministic failures. Fail-toward-scanning is a defensible default for a security tool and the warning names the cause, so I'm fine shipping it as is.

The shallow-fetch change itself looks strictly better. The staged fallthrough in CI was almost always an empty diff, and local pre-commit runs have no GITHUB_BASE_REF so they keep the staged path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants