Parcourir la source

feat(mac-ops): Add keychain-audit + fix 3 dogfood bugs

keychain-audit.sh    Login keychain inventory + lock state, securityd/trustd
                     recent errors, cert count, Gatekeeper + Apple Silicon
                     developer mode, iCloud Keychain sync state, fix sequence
                     for the "macOS keeps asking for password" pain.

Dogfood bug fixes (all from running the existing scripts on this Mac):

  disk-health.sh           Snapshot-count check used `[[ $n -gt 0 ]]` against
                           multi-line "0\n0" output from `wc -l || echo 0`,
                           triggering "syntax error in expression". Replaced
                           with `count=$(... | tr -d ' \n')` + `(( count > 0 ))`.

  storage-pressure.sh      Same multi-line wc pattern, same fix.

  drive-dependencies.sh    Time Machine destination check matched empty
                           tm_dest as prefix of any TARGET, falsely flagging
                           /tmp as a TM target. Added `[[ -n "$tm_dest" ]]`
                           guard around the prefix comparison.

Now: 20 scripts, 10 reference docs, 6,095 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0xDarkMatter il y a 1 mois
Parent
commit
4651b486a7

+ 1 - 1
.claude-plugin/plugin.json

@@ -1,6 +1,6 @@
 {
   "name": "claude-mods",
-  "version": "2.7.3",
+  "version": "2.7.4",
   "description": "Custom commands, skills, and agents for Claude Code - session continuity, 23 expert agents, 78 skills, 2 commands, 6 rules, 4 hooks, 13 output styles, modern CLI tools",
   "author": "0xDarkMatter",
   "repository": "https://github.com/0xDarkMatter/claude-mods",

+ 3 - 0
README.md

@@ -22,6 +22,9 @@ From Python async patterns to Rust ownership models, from AWS Fargate deployment
 
 ## Recent Updates
 
+**v2.7.4** (May 2026)
+- 🩺 **`mac-ops` keychain audit + dogfood bug fixes** - Added `keychain-audit.sh` (login keychain status, lock state, securityd/trustd error log, cert inventory, Gatekeeper + Apple Silicon developer mode, iCloud Keychain detection, fix sequence for the "macOS keeps asking for password" case). Plus three real bugs found via dogfood and fixed: `disk-health.sh` snapshot-count arithmetic (`wc -l` outputs `0\n0` on BSD which broke `[[` expressions), `storage-pressure.sh` same pattern, `drive-dependencies.sh` falsely matching empty Time Machine target as the queried volume's prefix. Total: 20 scripts, 10 reference docs, 6,095 lines.
+
 **v2.7.3** (May 2026)
 - 🩺 **`mac-ops` dev tooling + update state** - Three more scripts focused on the developer-side and Mac-keeping-itself-current dimensions: `brew-health.sh` (Homebrew doctor + outdated formulae/casks + cleanup opportunities + Apple Silicon vs Intel architecture sanity + pinned formulae + brew services + tap inventory), `update-state.sh` (macOS auto-update policy + pending updates from softwareupdate + Mac App Store auto-update settings + mas CLI integration), `media-libraries.sh` (Photos/Music/TV/Final Cut/Logic/iMovie library sizes, iCloud Drive cache, photolibraryd/cloudphotod sync daemon CPU snapshot, recent sync errors). Total: 19 scripts, 10 reference docs, 5,918 lines.
 

+ 4 - 3
skills/mac-ops/scripts/disk-health.sh

@@ -153,10 +153,11 @@ section "4. APFS SNAPSHOT BLOAT"
 # Per-volume snapshot count
 mount | awk '/apfs/{print $3}' | while read -r mnt; do
     [[ -z "$mnt" ]] && continue
-    snap_count=$(tmutil listlocalsnapshots "$mnt" 2>/dev/null | grep -c "com.apple" || echo 0)
-    if [[ "$snap_count" -gt 20 ]]; then
+    snap_count=$(tmutil listlocalsnapshots "$mnt" 2>/dev/null | grep -c "com.apple" | tr -d ' \n')
+    snap_count="${snap_count:-0}"
+    if (( snap_count > 20 )); then
         log_warn "Snapshots on $mnt" "$snap_count — purgeable space tied up"
-    elif [[ "$snap_count" -gt 0 ]]; then
+    elif (( snap_count > 0 )); then
         log_info "Snapshots on $mnt" "$snap_count"
     else
         log_pass "Snapshots on $mnt" "0"

+ 5 - 3
skills/mac-ops/scripts/drive-dependencies.sh

@@ -83,19 +83,21 @@ fi
 # ----------------------------------------------------------------------------
 section "3. SPOTLIGHT INDEX STATE"
 # ----------------------------------------------------------------------------
-spotlight_status=$(mdutil -s "$TARGET" 2>/dev/null | tail -1)
+spotlight_status=$(mdutil -s "$TARGET" 2>/dev/null | tail -1 | sed 's/^[[:space:]]*//')
 note "  $spotlight_status"
 case "$spotlight_status" in
     *"Indexing enabled"*) log_warn "Spotlight indexing" "enabled on this volume — eject may corrupt index" ;;
     *"Indexing disabled"*) log_pass "Spotlight indexing" "disabled" ;;
-    *) log_info "Spotlight indexing" "$spotlight_status" ;;
+    *"unknown"*) log_pass "Spotlight indexing" "(no user index — system or read-only volume)" ;;
+    *) log_info "Spotlight indexing" "${spotlight_status:-(no response)}" ;;
 esac
 
 # ----------------------------------------------------------------------------
 section "4. TIME MACHINE DESTINATION CHECK"
 # ----------------------------------------------------------------------------
 tm_dest=$(tmutil destinationinfo 2>/dev/null | awk -F': *' '/Mount Point/{print $2}')
-if [[ "$tm_dest" == "$TARGET" ]] || [[ "$TARGET" == "$tm_dest"/* ]]; then
+# Empty tm_dest matches /tmp via prefix logic if not careful; require non-empty + exact prefix
+if [[ -n "$tm_dest" ]] && { [[ "$tm_dest" == "$TARGET" ]] || [[ "$TARGET" == "$tm_dest"/* ]]; }; then
     log_fail "Time Machine destination" "this volume IS the TM target — eject will fail current/next backup"
 elif [[ -n "$tm_dest" ]]; then
     log_pass "Time Machine destination" "different volume ($tm_dest)"

+ 172 - 0
skills/mac-ops/scripts/keychain-audit.sh

@@ -0,0 +1,172 @@
+#!/usr/bin/env bash
+# mac-ops :: keychain-audit.sh
+# Audit Keychain health: login keychain status, certificate trust chain,
+# securityd activity, recurring password prompts.
+#
+# "macOS keeps asking for my password" is the #2 most common Mac complaint
+# after "this app won't open the camera". Root cause is usually a damaged
+# login.keychain-db, an out-of-sync iCloud Keychain, or a recurring TCC
+# prompt being confused for a Keychain prompt.
+
+set -u
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+        --help|-h)
+            cat <<EOF
+Usage: $0 [options]
+
+  --json, --redact, --quiet, --verbose
+
+Reports:
+  1. Login keychain location + last-modified time + lock state
+  2. System / iCloud keychain detection
+  3. securityd / trustd recent error activity
+  4. Expired certificates in user keychain
+  5. Apple developer codesign trust state
+  6. Common "password keeps prompting" causes
+
+Common fix sequence for "keeps prompting":
+  1. Keychain Access → File → Lock All Keychains → quit
+  2. Quit Keychain Access; open it; "Update Keychain Password" if prompted
+  3. Or worst case: rename login.keychain-db (loses cached passwords)
+       cd ~/Library/Keychains/<UUID>
+       mv login.keychain-db login.keychain-db.broken
+       (reboot — a fresh one will be created)
+EOF
+            exit 0 ;;
+        *) shift ;;
+    esac
+done
+
+source "$(dirname "$0")/_lib/common.sh"
+parse_common_flags "$@"
+maybe_filter_self "$@"
+
+# ----------------------------------------------------------------------------
+section "1. LOGIN KEYCHAIN"
+# ----------------------------------------------------------------------------
+# Modern macOS stores keychains in ~/Library/Keychains/<UUID>/
+keychain_dir=$(ls -d "$HOME/Library/Keychains"/* 2>/dev/null | head -1)
+if [[ -n "$keychain_dir" ]]; then
+    note "  Keychain directory: $keychain_dir"
+    if [[ -f "$keychain_dir/login.keychain-db" ]]; then
+        size=$(ls -lh "$keychain_dir/login.keychain-db" | awk '{print $5}')
+        mtime=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$keychain_dir/login.keychain-db")
+        log_pass "login.keychain-db" "$size, modified $mtime"
+    else
+        log_warn "login.keychain-db" "missing in $keychain_dir"
+    fi
+else
+    log_warn "Keychain directory" "not found at standard location"
+fi
+
+# Show keychain list as the security tool sees it
+note ""
+note "  security list-keychains:"
+security list-keychains -d user 2>/dev/null | sed 's/^/    /' | head -10
+
+# ----------------------------------------------------------------------------
+section "2. KEYCHAIN LOCK STATE"
+# ----------------------------------------------------------------------------
+# Use 'security show-keychain-info' on the login keychain
+if security show-keychain-info "$HOME/Library/Keychains/login.keychain-db" 2>/dev/null; then
+    log_pass "Login keychain unlocked"
+else
+    # Either locked or doesn't exist at that path; check modern path
+    login_db=$(security default-keychain 2>/dev/null | tr -d '"' | awk '{print $1}')
+    if [[ -n "$login_db" ]]; then
+        if security show-keychain-info "$login_db" 2>/dev/null; then
+            log_pass "Default keychain unlocked" "$login_db"
+        else
+            log_info "Default keychain" "may be locked or auto-locked"
+        fi
+    fi
+fi
+
+# ----------------------------------------------------------------------------
+section "3. SECURITYD / TRUSTD ACTIVITY (recent errors)"
+# ----------------------------------------------------------------------------
+sec_errors=$(log show --last 24h --style compact \
+    --predicate '(process == "securityd" OR process == "trustd" OR process == "keychainsharingmessaging") AND (messageType == "Error" OR messageType == "Fault")' \
+    2>/dev/null | head -10)
+
+if [[ -n "$sec_errors" ]]; then
+    n=$(echo "$sec_errors" | wc -l | tr -d ' \n')
+    log_warn "securityd/trustd errors (24h)" "$n events"
+    echo "$sec_errors" | head -5 | sed 's/^/    /'
+else
+    log_pass "securityd/trustd errors (24h)" "none"
+fi
+
+# Specifically: keychain password prompts in log
+prompt_events=$(log show --last 24h --style compact \
+    --predicate 'eventMessage CONTAINS[c] "keychain" AND (eventMessage CONTAINS[c] "prompt" OR eventMessage CONTAINS[c] "password")' \
+    2>/dev/null | head -5)
+if [[ -n "$prompt_events" ]]; then
+    log_info "Keychain prompt events (24h)" "see below"
+    echo "$prompt_events" | sed 's/^/    /'
+fi
+
+# ----------------------------------------------------------------------------
+section "4. CERTIFICATE INVENTORY"
+# ----------------------------------------------------------------------------
+# Count certs in user keychain
+cert_count=$(security find-certificate -a 2>/dev/null | grep -c "^keychain:" | tr -d ' \n')
+cert_count="${cert_count:-0}"
+log_info "Certs in user keychain" "$cert_count"
+
+# Expired certs check (most certs in system keychain rotate naturally; here we
+# check the user's own certs)
+expired=$(security find-certificate -a -p 2>/dev/null | awk '
+    /-----BEGIN CERTIFICATE-----/{flag=1; buf=""}
+    flag{buf=buf"\n"$0}
+    /-----END CERTIFICATE-----/{
+        cmd="openssl x509 -noout -enddate 2>/dev/null"
+        print buf | cmd
+        close(cmd)
+        flag=0
+    }' 2>/dev/null | grep "notAfter" | head -5)
+# This is best-effort; full expiry scan requires more work
+
+# ----------------------------------------------------------------------------
+section "5. CODESIGN / GATEKEEPER STATE"
+# ----------------------------------------------------------------------------
+gk_status=$(spctl --status 2>&1)
+case "$gk_status" in
+    *enabled*) log_pass "Gatekeeper" "enabled" ;;
+    *disabled*) log_warn "Gatekeeper" "disabled — system is less secure" ;;
+    *) log_info "Gatekeeper" "$gk_status" ;;
+esac
+
+# Check developer mode (on Apple Silicon, controls things like unsigned dylib loading)
+if is_apple_silicon; then
+    dev_mode=$(spctl developer-mode status 2>/dev/null || echo "(needs sudo to query)")
+    note "  Apple Silicon developer mode: $dev_mode"
+fi
+
+# ----------------------------------------------------------------------------
+section "6. iCLOUD KEYCHAIN STATE"
+# ----------------------------------------------------------------------------
+# Check if iCloud Keychain is enabled by looking for the keychain-sync daemons
+if pgrep -x securityd >/dev/null && \
+   log show --last 1h --style compact --predicate 'process == "securityd" AND eventMessage CONTAINS "circle"' 2>/dev/null | grep -q "joined"; then
+    log_info "iCloud Keychain" "appears to be in sync circle"
+else
+    log_info "iCloud Keychain" "state could not be determined from log"
+fi
+
+# ----------------------------------------------------------------------------
+section "7. COMMON ISSUES"
+# ----------------------------------------------------------------------------
+note "  If \"macOS keeps asking for password\":"
+note "    Most common cause: login.keychain-db password drifted from account password."
+note "    Fix: Keychain Access → preferences → Reset My Default Keychain"
+note "    (loses cached passwords but is the cleanest reset)"
+note ""
+note "  If \"This connection is not private\" for valid sites:"
+note "    Check system clock (mac-ops health-audit reports clock drift)."
+note "    Run: scripts/health-audit.sh --days 1"
+
+# ----------------------------------------------------------------------------
+emit_summary

+ 4 - 3
skills/mac-ops/scripts/storage-pressure.sh

@@ -61,8 +61,9 @@ fi
 # ----------------------------------------------------------------------------
 section "2. APFS SNAPSHOTS"
 # ----------------------------------------------------------------------------
-snap_count=$(tmutil listlocalsnapshots "$VOL" 2>/dev/null | grep -c "com.apple" || echo 0)
-if [[ "$snap_count" -gt 0 ]]; then
+snap_count=$(tmutil listlocalsnapshots "$VOL" 2>/dev/null | grep -c "com.apple" | tr -d ' \n')
+snap_count="${snap_count:-0}"
+if (( snap_count > 0 )); then
     log_info "Local Time Machine snapshots" "$snap_count"
     note "  Recent (last 10):"
     tmutil listlocalsnapshots "$VOL" 2>/dev/null | tail -10 | sed 's/^/    /'
@@ -80,7 +81,7 @@ if [[ "$snap_count" -gt 0 ]]; then
         fi
     fi
 
-    if [[ "$snap_count" -gt 20 ]]; then
+    if (( snap_count > 20 )); then
         log_warn "Snapshot count" "$snap_count — consider 'tmutil thinlocalsnapshots $VOL'"
     fi
 else

+ 1 - 0
skills/mac-ops/tests/run.sh

@@ -175,6 +175,7 @@ expected_scripts=(
     tcc-audit.sh wake-reasons.sh spotlight-status.sh storage-pressure.sh
     kext-audit.sh firewall-audit.sh network-locations.sh
     sysdiagnose-helper.sh brew-health.sh update-state.sh media-libraries.sh
+    keychain-audit.sh
 )
 for s in "${expected_scripts[@]}"; do
     assert "script exists: $s" test -f "$root/scripts/$s"