Skip to content

fix(audit): stop misclassifying manual installs as brew and surface removal failures - #133

Merged
CybotTM merged 1 commit into
mainfrom
fix/reconcile-manual-install-classification
Jul 30, 2026
Merged

fix(audit): stop misclassifying manual installs as brew and surface removal failures#133
CybotTM merged 1 commit into
mainfrom
fix/reconcile-manual-install-classification

Conversation

@CybotTM

@CybotTM CybotTM commented Jul 30, 2026

Copy link
Copy Markdown
Member

Problem

make reconcile-all prompted for removals, the user confirmed with y, yet the run ended with Conflicts resolved: 0 and exit 1 — with no indication of what went wrong. Nothing was actually removed.

Root cause chain:

  1. On machines without Homebrew, the path heuristic in _classify_via_path labeled any /usr/local/bin (or /opt/homebrew) binary as brew, so removal ran a nonexistent brew uninstall and failed with FileNotFoundError.
  2. Even correctly-classified manual binaries in /usr/local/bin are typically root-owned, and os.remove fails with a bare Permission denied giving no path forward.
  3. All per-tool removal errors were vlog-only, and the --reconcile --all --apply non-JSON path printed just the summary — so every failure after a confirmed prompt was silent.

Fix

  • Path heuristic returns brew for /usr/local/bin / /opt/homebrew only when brew is installed; otherwise manual. (The shell-side scripts/lib/capability.sh and reconcile.sh already gate on command -v brew; only the Python side was affected.)
  • PermissionError during manual removal now surfaces actionable guidance: Permission denied — remove manually: sudo rm <path>, matching the existing apt/dnf behavior.
  • --reconcile --all --apply prints per-tool outcomes to stderr before the summary: ✗ tool: <error> for failures, skipped (declined), skipped (protected system tool).

Test plan

  • 4 new tests (written first, watched fail): brew-present vs brew-absent classification for /usr/local/bin and /opt/homebrew, sudo guidance on PermissionError, and per-tool stderr output for the --all --apply path.
  • Full suite: 776 passed, 1 skipped. Pre-commit (flake8, isort, black) green; ./scripts/test_smoke.sh passes.
  • Real-world check on an affected machine: audit.py --reconcile yq --apply --yes now reports Failed to remove /usr/local/bin/yq: Permission denied — remove manually: sudo rm /usr/local/bin/yq instead of failing invisibly.

https://claude.ai/code/session_01MH3EaniXCnJdwqNvrMB4Ym

…emoval failures

`make reconcile-all` confirmed removals then reported "Conflicts resolved: 0"
with no explanation. Three defects:

- Path heuristic labeled any /usr/local/bin or /opt/homebrew binary as
  'brew' even when brew is not installed, so removal ran a nonexistent
  `brew uninstall` and always failed. Classify as 'manual' when brew is
  absent (the shell-side capability.sh already gated on `command -v brew`).
- Manual removal of root-owned binaries failed with a bare "Permission
  denied"; now surfaces actionable guidance (`sudo rm <path>`), matching
  the apt/dnf branch.
- The `--reconcile --all --apply` non-JSON path printed only the summary;
  per-tool failures, declines, and protected skips were vlog-only. Print
  them to stderr unconditionally so a confirmed-but-failed removal is
  never silent.

Claude-Session: https://claude.ai/code/session_01MH3EaniXCnJdwqNvrMB4Ym
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Copilot AI review requested due to automatic review settings July 30, 2026 09:46
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CybotTM
CybotTM merged commit 1e9cb11 into main Jul 30, 2026
24 of 25 checks passed
@CybotTM
CybotTM deleted the fix/reconcile-manual-install-classification branch July 30, 2026 10:07
CybotTM added a commit that referenced this pull request Jul 30, 2026
…alls (#134)

## Problem

Follow-up to #133. With failures now visible, a full `make
reconcile-all` run surfaced three remaining issues:

1. Dozens of individual sudo hints — the user has to collect `sudo apt
remove X` / `sudo rm <path>` commands one by one from the output.
2. `~/go/bin` binaries (golangci-lint, shfmt) fell through the generic
`/bin` heuristic to `system`, producing nonsense guidance: `sudo system
remove golangci-lint`. They are user-owned `go install` binaries —
removal is deleting the file, no sudo needed.
3. `cargo uninstall delta` / `watchexec` failed with "package ID
specification did not match any packages": the owning crates are
`git-delta` and `watchexec-cli`.

## Fix

- `--reconcile --all --apply` now ends with one copy-paste command per
remedy: a single `sudo apt remove <pkg…>` per system package manager and
a single `sudo rm -f <path…>` for unmanaged binaries (deduped, sorted;
only for conflicts the user confirmed).
- Path heuristic classifies `/go/bin` as `go` (before the generic `/bin`
fallback); `go` binaries are removed like manual installs and rank tier
2 alongside cargo/pip/npm. Shell-side `capability.sh` already handled
this.
- Cargo uninstall maps binary → owning crate via `cargo install --list`
(fallback: tool name).
- `system`-classified binaries get `sudo rm <path>` guidance instead of
the nonexistent `sudo system remove <tool>`.

## Test plan

- 5 new tests (written first, watched fail): GOPATH classification,
go-method file removal, system-method rm guidance, cargo crate mapping,
and the aggregated command lines in `--all --apply` output.
- Full suite: 781 passed, 1 skipped; smoke test OK; pre-commit (flake8,
isort, black) green.
- Real-machine spot checks:
`classify_install_method('/home/sme/go/bin/shfmt') == 'go'`;
`_cargo_package_for('delta') == 'git-delta'`;
`_cargo_package_for('watchexec') == 'watchexec-cli'`.

https://claude.ai/code/session_01MH3EaniXCnJdwqNvrMB4Ym
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