diff --git a/.claude/commands/save.md b/.claude/commands/save.md index 7e93a9b..e41c43e 100644 --- a/.claude/commands/save.md +++ b/.claude/commands/save.md @@ -72,9 +72,32 @@ Format credentials as: ## After Saving +Before committing, emit a **Change Summary** block for the user to review: + +``` +## Change Summary (this session) +User: (from .claude/identity.json) +Machine: + +Files changed: + + +Stats: + +``` + +Then: 1. Commit with message: "Session log: [brief description of work done]" 2. Push to gitea remote (if configured) -3. Confirm push was successful +3. After push, emit a **Post-commit Summary**: + - New commit SHA + message + - Author (from `git log -1 --format='%an <%ae>'`) + - Files in the commit (from `git show --stat HEAD`) +4. Confirm push was successful + +### Why the summary + +In the multi-user setup, commits can land in `main` from either team member. Always attributing author + files makes it obvious who made what change when someone else pulls the repo. Saves re-reading diffs to figure out "wait, when did that happen?" ## Purpose diff --git a/.claude/commands/sync.md b/.claude/commands/sync.md index d673ebf..0678d8c 100644 --- a/.claude/commands/sync.md +++ b/.claude/commands/sync.md @@ -7,23 +7,33 @@ bash .claude/scripts/sync.sh ``` The script automatically: -1. Stages and commits local changes (if any) -2. Fetches and pulls remote changes -3. Pushes local changes -4. Reports sync status +1. Stages and commits local changes (attributed to the current user from `.claude/identity.json`) +2. Fetches remote and shows **incoming commits with authors** before pulling +3. Shows **outgoing commits with authors** before pushing +4. Pulls (rebase), then pushes +5. Prints a final change summary (who committed what, on which side) -After the script completes, report the 3 most recent session logs: +## After the script completes + +The script emits a "Sync Summary" block. Relay the key bits to the user: + +- **Incoming from remote:** N commits. If N > 0, list commits as ` ` so the user immediately sees what Howard / Mike / other teammates pushed since their last sync. +- **Outgoing to remote:** M commits by the current user (this is what they're publishing). +- **Net file changes in this sync:** output of `git diff --stat ..HEAD -- . ':(exclude)session-logs'` (or similar scoping) so the user sees the meaningful edits, not noise. + +Then report the 3 most recent session logs: ```bash ls -t session-logs/*.md projects/*/session-logs/*.md clients/*/session-logs/*.md 2>/dev/null | head -3 ``` -## Conflict Resolution +## Conflict resolution -- **Session logs:** Keep both, rename with machine suffix -- **credentials.md:** Do NOT auto-merge, report to user -- **Other files:** Standard git conflict resolution +- **Session logs:** Keep both, rename with machine suffix. Note which user authored each conflicting side. +- **credentials.md:** Do NOT auto-merge, report to user. +- **Other files:** Standard git conflict resolution. When presenting a conflict, include the author of the conflicting commits on each side so the user can coordinate (e.g., "Howard changed this in commit abc123 on 2026-04-15"). -## Error Handling +## Error handling -If push fails with auth error, retry once (transient Gitea auth issue). -If pull fails with conflicts, report affected files and ask for guidance. +- **Auth failure on push:** retry once (transient Gitea auth issue). +- **Pull conflicts:** report affected files + author of each conflicting side, then ask for guidance. +- **No identity.json yet:** follow the onboarding flow in CLAUDE.md before syncing. diff --git a/.claude/scripts/sync.sh b/.claude/scripts/sync.sh index cca09df..4bb655f 100755 --- a/.claude/scripts/sync.sh +++ b/.claude/scripts/sync.sh @@ -1,118 +1,158 @@ #!/bin/bash # ClaudeTools Bidirectional Sync Script # Ensures proper pull BEFORE push on all machines +# Prints incoming/outgoing change summary with author attribution -set -e # Exit on error +set -e -# Colors for output +# Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' -NC='\033[0m' # No Color +CYAN='\033[0;36m' +NC='\033[0m' -# Detect machine name +# Machine + timestamp if [ -n "$COMPUTERNAME" ]; then MACHINE="$COMPUTERNAME" else MACHINE=$(hostname) fi - -# Timestamp TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") echo -e "${GREEN}[OK]${NC} Starting ClaudeTools sync from $MACHINE at $TIMESTAMP" -# Navigate to ClaudeTools directory -if [ -d "$HOME/ClaudeTools" ]; then - cd "$HOME/ClaudeTools" -elif [ -d "/d/ClaudeTools" ]; then - cd "/d/ClaudeTools" -elif [ -d "D:/ClaudeTools" ]; then - cd "D:/ClaudeTools" -else - echo -e "${RED}[ERROR]${NC} ClaudeTools directory not found" +# Navigate to ClaudeTools directory (check common locations) +for candidate in "$HOME/ClaudeTools" "/d/ClaudeTools" "D:/ClaudeTools" "/d/claudetools" "D:/claudetools"; do + if [ -d "$candidate" ]; then + cd "$candidate" + break + fi +done + +if [ ! -d ".git" ]; then + echo -e "${RED}[ERROR]${NC} Not in a git working tree" exit 1 fi echo -e "${GREEN}[OK]${NC} Working directory: $(pwd)" -# Phase 1: Check and commit local changes +# Load user identity +USER_DISPLAY="unknown" +USER_GITEA="" +if [ -f ".claude/identity.json" ]; then + USER_DISPLAY=$(python -c "import json,sys; d=json.load(open('.claude/identity.json')); print(d.get('full_name', d.get('user','unknown')))" 2>/dev/null || echo "unknown") + USER_GITEA=$(python -c "import json,sys; d=json.load(open('.claude/identity.json')); print(d.get('user',''))" 2>/dev/null || echo "") +fi +echo -e "${GREEN}[OK]${NC} Syncing as: $USER_DISPLAY (machine: $MACHINE)" + +# Phase 1: Local changes echo "" -echo "=== Phase 1: Local Changes ===" +echo "=== Phase 1: Local changes ===" if ! git diff-index --quiet HEAD -- 2>/dev/null; then - echo -e "${YELLOW}[INFO]${NC} Local changes detected" - - # Show status + echo -e "${YELLOW}[INFO]${NC} Local changes detected:" git status --short + echo "" - # Stage all changes echo -e "${GREEN}[OK]${NC} Staging all changes..." git add -A - # Commit with timestamp - COMMIT_MSG="sync: Auto-sync from $MACHINE at $TIMESTAMP - -Synced files: -- Session logs updated -- Latest context and credentials -- Command/directive updates + # Commit message (Co-Authored-By uses local git user if configured) + COMMIT_MSG="sync: auto-sync from $MACHINE at $TIMESTAMP +Author: $USER_DISPLAY Machine: $MACHINE -Timestamp: $TIMESTAMP - -Co-Authored-By: Claude Sonnet 4.5 " +Timestamp: $TIMESTAMP" git commit -m "$COMMIT_MSG" - echo -e "${GREEN}[OK]${NC} Changes committed" + echo -e "${GREEN}[OK]${NC} Committed." else - echo -e "${GREEN}[OK]${NC} No local changes to commit" + echo -e "${GREEN}[OK]${NC} No local changes to commit." fi -# Phase 2: Sync with remote (CRITICAL: Pull BEFORE Push) +# Phase 2: Remote sync echo "" -echo "=== Phase 2: Remote Sync (Pull + Push) ===" +echo "=== Phase 2: Fetch + inspect ===" -# Fetch to see what's available -echo -e "${GREEN}[OK]${NC} Fetching from remote..." -git fetch origin +LOCAL_BEFORE=$(git rev-parse HEAD) -# Check if remote has updates -LOCAL=$(git rev-parse main) -REMOTE=$(git rev-parse origin/main) +echo -e "${GREEN}[OK]${NC} Fetching from origin..." +git fetch origin --quiet -if [ "$LOCAL" != "$REMOTE" ]; then - echo -e "${YELLOW}[INFO]${NC} Remote has updates, pulling..." +LOCAL=$(git rev-parse HEAD) +REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "$LOCAL") +REMOTE_BRANCH="origin/main" +if ! git rev-parse origin/main >/dev/null 2>&1; then + REMOTE_BRANCH="origin/master" +fi - # Pull with rebase +# Count and show incoming +INCOMING_COUNT=$(git rev-list --count HEAD..$REMOTE_BRANCH 2>/dev/null || echo 0) +OUTGOING_COUNT=$(git rev-list --count $REMOTE_BRANCH..HEAD 2>/dev/null || echo 0) + +if [ "$INCOMING_COUNT" -gt 0 ]; then + echo "" + echo -e "${CYAN}--- Incoming: $INCOMING_COUNT commits from remote ---${NC}" + git log --oneline --format=' %C(yellow)%h%Creset %C(cyan)%an%Creset %s %C(dim)(%ar)%Creset' HEAD..$REMOTE_BRANCH | head -30 + echo "" + echo -e "${CYAN}--- Files touched by incoming commits ---${NC}" + git diff --stat HEAD..$REMOTE_BRANCH | tail -20 +else + echo -e "${GREEN}[OK]${NC} No incoming changes." +fi + +if [ "$OUTGOING_COUNT" -gt 0 ]; then + echo "" + echo -e "${CYAN}--- Outgoing: $OUTGOING_COUNT commits to remote ---${NC}" + git log --oneline --format=' %C(yellow)%h%Creset %C(cyan)%an%Creset %s %C(dim)(%ar)%Creset' $REMOTE_BRANCH..HEAD | head -30 +fi + +# Phase 3: Pull (if needed) +if [ "$INCOMING_COUNT" -gt 0 ]; then + echo "" + echo "=== Phase 3: Pull (rebase) ===" if git pull origin main --rebase; then - echo -e "${GREEN}[OK]${NC} Successfully pulled remote changes" - git log --oneline "$LOCAL..origin/main" + echo -e "${GREEN}[OK]${NC} Pulled successfully." else - echo -e "${RED}[ERROR]${NC} Pull failed - may have conflicts" - echo -e "${YELLOW}[INFO]${NC} Resolve conflicts and run sync again" + echo -e "${RED}[ERROR]${NC} Pull failed (likely conflicts). Resolve and re-run sync." + exit 1 + fi +fi + +# Phase 4: Push (if needed) +OUTGOING_AFTER_PULL=$(git rev-list --count $REMOTE_BRANCH..HEAD 2>/dev/null || echo 0) +if [ "$OUTGOING_AFTER_PULL" -gt 0 ]; then + echo "" + echo "=== Phase 4: Push ===" + if git push origin main; then + echo -e "${GREEN}[OK]${NC} Pushed successfully." + else + echo -e "${RED}[ERROR]${NC} Push failed. Check auth / network." exit 1 fi else - echo -e "${GREEN}[OK]${NC} Already up to date with remote" + echo -e "${GREEN}[OK]${NC} Nothing to push." fi -# Push local changes +# Phase 5: Summary echo "" -echo -e "${GREEN}[OK]${NC} Pushing local changes to remote..." -if git push origin main; then - echo -e "${GREEN}[OK]${NC} Successfully pushed to remote" -else - echo -e "${RED}[ERROR]${NC} Push failed" - exit 1 +echo "=== Sync Summary ===" + +if [ "$INCOMING_COUNT" -gt 0 ]; then + # Count commits by author + INCOMING_AUTHORS=$(git log --format='%an' $LOCAL_BEFORE..HEAD 2>/dev/null | sort | uniq -c | sort -rn | awk '{printf "%s (%s), ", substr($0, index($0,$2)), $1}' | sed 's/, $//') + echo -e "${CYAN}Pulled in:${NC} $INCOMING_COUNT commit(s) — authors: ${INCOMING_AUTHORS:-unknown}" +fi +if [ "$OUTGOING_AFTER_PULL" -gt 0 ]; then + echo -e "${CYAN}Pushed out:${NC} $OUTGOING_AFTER_PULL commit(s) by $USER_DISPLAY" +fi +if [ "$INCOMING_COUNT" -eq 0 ] && [ "$OUTGOING_AFTER_PULL" -eq 0 ]; then + echo -e "${GREEN}Already in sync — no commits moved in either direction.${NC}" fi -# Phase 3: Report final status -echo "" -echo "=== Sync Complete ===" -echo -e "${GREEN}[OK]${NC} Local branch: $(git rev-parse --abbrev-ref HEAD)" -echo -e "${GREEN}[OK]${NC} Current commit: $(git log -1 --oneline)" -echo -e "${GREEN}[OK]${NC} Remote status: $(git status -sb | head -1)" +echo -e "${GREEN}[OK]${NC} HEAD: $(git log -1 --oneline)" +echo -e "${GREEN}[OK]${NC} Status: $(git status -sb | head -1)" echo "" -echo -e "${GREEN}[SUCCESS]${NC} All machines in sync. Ready to continue work." +echo -e "${GREEN}[SUCCESS]${NC} Sync complete." diff --git a/session-logs/2026-04-16-session.md b/session-logs/2026-04-16-session.md index 6829ab9..06adbe5 100644 --- a/session-logs/2026-04-16-session.md +++ b/session-logs/2026-04-16-session.md @@ -690,3 +690,284 @@ Claude: [Already has context, proceeds immediately with work] **Status:** Automatic context loading system complete and deployed ✅ **Impact:** Eliminates recurring problem of Claude not knowing previous work **Validation:** To be tested in next session with fresh Claude instance + +--- + +## Update: 17:30 UTC — MSP tooling + incident response + remediation skill + +### User +- **User:** Mike Swanson (mike) +- **Machine:** DESKTOP-0O8A1RL +- **Role:** admin + +### Session summary + +Separate session later the same day (different machine/Claude instance from the context-loading work above). Five interleaved threads: + +1. **Cascades Tucson breach investigation** — John Trozzi reported as possible credential-stuffing victim. Check found John clean; tenant-wide sweep discovered **Megan Hiatt under active credential-stuffing attack** RIGHT NOW (bursts from Belfast GB, Hamburg DE). +2. **Built `/remediation-tool` skill + slash command** codifying the M365 investigation workflow. +3. **Fixed SOPS `vault.sh` on Windows** — Device Guard (WDAC) blocks unsigned `yq.exe`; added Python + PyYAML fallback. +4. **Valleywide RemoteApp-over-VPN troubleshooting** — walked through `0x3000008` -> NXDOMAIN -> RDS licensing in sequence. +5. **Howard Enos breach check** — clean, but actively targeted on cloud-admin paths (Azure CLI/LU, AAD PowerShell/DE+JP). + +### Thread 1: Cascades Tucson breach investigation + +**John Trozzi (`john.trozzi@cascadestucson.com`, `a638f4b9-6936-4401-a9b7-015b9900e49e`)** — tenant `207fa277-e9d8-4eb7-ada1-1064d2221498`. + +Verdict: **NO BREACH.** All 10 breach checks clean. +- No Graph inbox rules; one Exchange hidden rule (`Junk E-mail Rule` — default) +- No forwarding, no delegates, no non-SELF SendAs +- 2 OAuth grants (both BlueMail, consented 2022) +- 5 auth methods all pre-dating attack window (MS Authenticator on Samsung SM-F731U + FIDO2 passkey, both 2026-02-12) +- 30d sign-ins: 11, 100% from `184.191.143.62` Phoenix AZ (Cox) +- Directory audits show the legit IR sequence by sysadmin (disable -> password reset -> enable), then John self-changed at 16:04:46 UTC + +**Tenant-wide sweep flagged PRIORITY 1: Megan Hiatt (`megan.hiatt@cascadestucson.com`) under active credential-stuffing:** +- **126 failed sign-ins in 30 days** across 8 IPs / 6 countries (CH, DE, GB, LT, NL, US) +- **Today (2026-04-16 15:58–16:01 UTC):** 23 failures from `80.94.92.102` (Belfast, GB) via Authenticated SMTP +- Earlier: 2026-04-15 Hamburg DE (`158.94.211.16`), 2026-04-13 Belfast GB (`80.94.92.123`) +- Password last changed 2026-02-18 (~2 months stale) +- Only 1 MFA method (MS Authenticator iPhone 13) +- Mailbox clean. NOT breached — MFA + IP reputation + account lockout holding. +- **Action items:** reset Megan's password, disable SMTP AUTH on her mailbox, keep monitoring. + +Other notable: external guest `dunedolly21@gmail.com` invited 2026-04-14 by `lauren.hasselman@cascadestucson.com` from her mobile. Lauren's activity is clean. Mike to confirm with Lauren what the invite is for. No meaningful access granted yet. + +Gaps encountered and addressed during investigation: +- Exchange Admin role was not assigned to `ComputerGuru - AI Remediation` SP in Cascades. Mike assigned it via Entra UI. ~15 min to propagate. Unlocked hidden-rule / delegate / SendAs checks. +- IdentityRiskyUser scope still NOT consented in Cascades. Consent URL opened multiple times but `/servicePrincipals/{id}/appRoleAssignments` shows no new grants today — permission may not be in the app manifest. Mike to verify home-tenant app registration. + +**Report:** `clients/cascades-tucson/reports/2026-04-16-john-breach-check.md` + +### Thread 2: Built `/remediation-tool` skill + +Codified the Cascades workflow into a reusable skill. Files: + +``` +.claude/commands/remediation-tool.md +.claude/skills/remediation-tool/ +├── SKILL.md # auto-invocation triggers +├── scripts/ +│ ├── resolve-tenant.sh # domain -> tenant GUID via OpenID discovery +│ ├── get-token.sh # Graph + Exchange tokens, 55-min cache +│ ├── user-breach-check.sh # 10-point user check +│ └── tenant-sweep.sh # tenant-wide signals +├── references/ +│ ├── gotchas.md # role prereqs, consent URLs, display name quirk +│ ├── graph-endpoints.md # Graph + Exchange REST cheatsheet +│ └── checklist.md # breach-check rubric +└── templates/breach-report.md # report skeleton +``` + +Subcommands: +``` +/remediation-tool check +/remediation-tool sweep +/remediation-tool signins [--user upn] [--failed-only] [--days N] +/remediation-tool consent-url +/remediation-tool remediate # gated — requires YES in chat +``` + +Auth flow: resolve tenant ID from domain via OpenID discovery -> pull secret from SOPS vault -> acquire client-credentials tokens -> run checks -> dump raw JSON to `/tmp/remediation-tool/{tenant}/{check}/` -> write report to `clients/{slug}/reports/YYYY-MM-DD-{action}.md`. + +Updated: +- `.claude/CLAUDE.md` — added `/remediation-tool` row to Commands & Skills table +- `.claude/memory/feedback_365_remediation_tool.md` — cross-reference to the skill + +Smoke-tested end-to-end against Cascades (token acquired, Graph /organization call returned correct tenant) and Howard (full 10-point check in ~5 seconds). + +**App identity gotchas captured in references/gotchas.md:** +- App ID: `fabb3421-8b34-484b-bc17-e46de9703418` +- Home-tenant name: Claude-MSP-Access +- **Customer-tenant display name: ComputerGuru - AI Remediation** (important when searching role assignment dialogs) +- Client secret: vault `msp-tools/claude-msp-access-graph-api.sops.yaml`, field `credentials.credential` + +### Thread 3: Vault.sh Device Guard fix + +**Root cause:** `yq.exe` on this Windows box is blocked by corporate Device Guard / WDAC policy (unsigned binary). Both the WinGet `Links` shim and the real binary at `C:/Users/guru/AppData/Local/Microsoft/WinGet/Packages/MikeFarah.yq_.../yq.exe` return "Permission denied" / "blocked by your organization's Device Guard policy". + +**Fix:** Replaced yq dependency with Python + PyYAML fallback. + +Files: +- **New:** `D:/vault/scripts/yaml-query.py` — minimal yq replacement, two commands (`path `, `flatten-env `) +- **Modified:** `D:/vault/scripts/vault.sh` — added `_detect_yq_mode`, `_yaml_field`, `_yaml_flatten_env` helpers; replaced two `yq eval` call sites. Prefers yq if it works, falls back to Python. + +Verified: +- `vault.sh get-field msp-tools/claude-msp-access-graph-api.sops.yaml credentials.credential` -> returns `~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO` ✅ +- `vault.sh export-env ...` -> `CREDENTIAL=~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO` ✅ +- `vault.sh search`, `vault.sh list` unchanged + +PyYAML 6.0.3 already installed at `/c/Program Files/Python314/python`. + +**Defender alerts fired** during the fix (rapid SOPS decryption + JWT base64-decoding + client-credentials OAuth looked like credential-dumping heuristics). All false positives. Mike left exclusions unchanged; future runs will hit the 55-min token cache and quiet down. + +### Thread 4: Valleywide RemoteApp over VPN (three sequential problems) + +**Problem 1 — `0x3000008` (RD Gateway unreachable):** Public WAN :443 port-forward to VWP-QBS was removed during 2026-04-13 brute-force IR. RDP manifest still routed through external FQDN `remote.valleywideplastering.com` -> WAN IP `4.18.160.106` (firewalled). + +Fix: Mike removed RD Gateway from RDS deployment on VWP-QBS (Server Manager -> RDS -> Edit Deployment Properties -> RD Gateway -> "Do not use"). New RDP files have `gatewayusagemethod:i:0` and `full address:s:VWP-QBS.VWP.US`. + +**Problem 2 — NXDOMAIN for `VWP-QBS.VWP.US`:** After gateway removed, client tried to resolve the session host hostname directly. UDM's static DNS had a typo: `qwp-qbs.vwp.us` (Q not V). `vwp.us` is a real registered domain (website lives publicly), so external DNS doesn't help; internal override needed. + +Fix: Mike edited UniFi UI (Settings -> Routing -> DNS -> Static DNS Records), changed `qwp-qbs.vwp.us` -> `vwp-qbs.vwp.us`, still pointing at `172.16.9.169`. + +**Problem 3 — "No Remote Desktop License Servers available" (0x3, 0x101):** Once DNS resolved and client reached session host, RDS-Licensing role was installed + activated locally on VWP-QBS but the RDSH was never configured to use it. + +Fix (applied remotely via WinRM over VPN from Mike's box): +```powershell +$ts = Get-CimInstance -Namespace root\cimv2\TerminalServices -ClassName Win32_TerminalServiceSetting +Invoke-CimMethod -InputObject $ts -MethodName ChangeMode -Arguments @{LicensingType = 4} # Per User +Invoke-CimMethod -InputObject $ts -MethodName SetSpecifiedLicenseServerList -Arguments @{SpecifiedLSList = @('vwp-qbs.vwp.us')} +``` +Both returned `ReturnValue=0`. Mike confirmed "it works". + +**Outstanding VWP issue:** License server has only the Windows 2000-era `Built-in TS Per Device CAL` placeholder — **no real CALs**. Grace period is consumed. Purchase needed: **Windows Server 2022 RDS Per User CAL pack** sized to active user count; install via `licmgr.msc` on VWP-QBS. + +### VWP UDM access + +- Host: `172.16.9.1` — UniFi Dream Machine Pro, firmware 5.0.16 +- Access: SSH as `root` via ed25519 key (added during this session via PuTTY after UI-based add didn't land) +- Public key added: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINXR2BOcFAlOPuB7OYOKfOZDNd3u1tCt/IINRH9beFyB guru@DESKTOP-0O8A1RL` +- Fingerprint: `SHA256:ZVbowRHhxPX47eKy9FyMwjvIKPzTf3Dwx3BCsBrP4ds` +- Vault entry password `Gptf*77ttb123!@#-vwp` does NOT work — needs rotation + vault update + +### VWP network topology (discovered) + +- LAN: `172.16.9.0/24` (br0 — VWP-QBS at `.169`), `192.168.0.0/24` (br2 — legacy), `192.168.3.0/24` (br99 — iDRAC) +- WAN: `eth8` = `4.18.160.106/30` +- OpenVPN server on `tun1` — clients land on `192.168.4.0/24`, DNS pushed = `192.168.4.1` (UDM), routes pushed for all three LAN subnets +- WireGuard site-to-site peers: `wgsts1001` (192.168.5.2), `wgsts1003` (192.168.5.6), `wgsts1005` (192.168.5.11) — learn OSPF routes for 192.168.1.0/24 and 192.168.10.0/24 +- VPN -> LAN firewall: `UBIOS_VPN_LAN_USER` = ACCEPT all +- Active port forwards: NONE (DNAT hook empty after 2026-04-13 removal) + +`clients/valleywide/README.md` appended with `## 2026-04-16` section documenting all three fixes, topology, and CAL-purchase action item. + +### Thread 5: Howard Enos breach check (own tenant) + +Invoked `/remediation-tool check howard@azcomputerguru.com`. + +- **Tenant:** azcomputerguru.com, `ce61461e-81a0-4c84-bb4a-7b354a9a356d` +- **UPN:** howard@azcomputerguru.com, object id `c99de3bd-ddc1-43f1-907f-e84b91273660` +- **Password last changed:** 2024-09-24 (18 months ago) + +Verdict: **CLEAN, but actively targeted on cloud-admin paths.** + +- **174 of 200 sign-ins non-US in 30d — 100% FAILED, zero successful foreign sign-ins** +- Top attackers: CN(32), IN(32), KR(28), LU(15 via **Microsoft Azure CLI**), BR(14), DE(8 via **Azure AD PowerShell**), JP(8 via **AAD PowerShell**), plus 19 other countries. +- Attacker is specifically probing admin-grade endpoints, not just random Exchange. +- 3 inbox rules — all legit user filters (Telnyx, Atlas_LNP whitelabel, Facebook) +- 4 OAuth grants — standard Microsoft Graph + Teams +- 8 app role assignments — all MSP-relevant (Syncro v1+v2, ASUS, Tailscale, Perfect Wiki, KaseyaSSO, Graph Explorer, Uizard) +- 6 auth methods — password + SMS + OATH + 3x MS Authenticator (phone upgrades) + +**Gaps on our own tenant:** +- Exchange Admin role NOT assigned to ComputerGuru-AI-Remediation SP in azcomputerguru -> blocks hidden-rule / delegate / SendAs checks +- IdentityRiskyUser NOT consented in azcomputerguru + +**Report:** `clients/internal-infrastructure/reports/2026-04-16-howard-breach-check.md` + +### Credentials & secrets + +**Claude-MSP-Access Graph API app ("ComputerGuru - AI Remediation"):** +- App ID: `fabb3421-8b34-484b-bc17-e46de9703418` +- Client Secret: `~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO` +- Vault: `msp-tools/claude-msp-access-graph-api.sops.yaml`, field `credentials.credential` +- Admin consent URL: `https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=fabb3421-8b34-484b-bc17-e46de9703418&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient` + +**VWP (Valleywide):** +- Domain admin: `vwp\sysadmin` / `r3tr0gradE99#` +- Vault: `clients/vwp/{adsrvr,dc1,udm,xenserver,quickbooks-server-idrac}.sops.yaml` +- UDM root (vault says, but broken): `Gptf*77ttb123!@#-vwp` -> ROTATE + update vault +- UDM SSH: `ssh root@172.16.9.1` via ed25519 key +- VWP_ADSRVR SSH: `ssh vwp\guru@192.168.0.25` (key from 2026-04-13) + +**Cascades Tucson:** Tenant `207fa277-e9d8-4eb7-ada1-1064d2221498`, admin `sysadmin@cascadestucson.com` + +**AZ Computer Guru:** Tenant `ce61461e-81a0-4c84-bb4a-7b354a9a356d` + +### Files created / modified in this update block + +**New:** +- `.claude/commands/remediation-tool.md` +- `.claude/skills/remediation-tool/SKILL.md` +- `.claude/skills/remediation-tool/scripts/{resolve-tenant.sh,get-token.sh,user-breach-check.sh,tenant-sweep.sh}` +- `.claude/skills/remediation-tool/references/{gotchas.md,graph-endpoints.md,checklist.md}` +- `.claude/skills/remediation-tool/templates/breach-report.md` +- `D:/vault/scripts/yaml-query.py` +- `clients/cascades-tucson/reports/2026-04-16-john-breach-check.md` +- `clients/internal-infrastructure/reports/2026-04-16-howard-breach-check.md` + +**Modified:** +- `.claude/CLAUDE.md` — added `/remediation-tool` row +- `.claude/memory/feedback_365_remediation_tool.md` — cross-reference +- `clients/valleywide/README.md` — 2026-04-16 section (RemoteApp + RDS licensing + CAL TODO) +- `D:/vault/scripts/vault.sh` — Python fallback for yq + +### Pending / incomplete + +1. Cascades — **reset Megan's password + disable SMTP AUTH** on her mailbox +2. Cascades — confirm `dunedolly21@gmail.com` invite with Lauren Hasselman +3. Cascades — verify IdentityRiskyUser.ReadWrite.All actually in the app manifest; re-run consent URL +4. Howard — password rotation (18 months old); consider passwordless/FIDO2 primary +5. Own tenant (azcomputerguru) — assign Exchange Admin role + consent IdentityRiskyUser on ComputerGuru-AI-Remediation SP (oversight) +6. Own tenant — verify CA policies block legacy auth (attacker hitting basic auth + AAD PowerShell paths) +7. VWP — purchase Server 2022 RDS Per User CAL pack, install via licmgr.msc +8. VWP — rotate UDM root password, update vault +9. VWP — UPnP audit on UDM (carried from 2026-04-13) +10. VWP — rotate `scanner` AD account password (carried from 2026-04-13) + +### Key references + +- Skill invocation: `/remediation-tool {check|sweep|signins|consent-url|remediate} [flags]` +- Raw JSON artifacts: `/tmp/remediation-tool/{tenant-id}/{check}/` +- Report directory pattern: `clients/{slug}/reports/YYYY-MM-DD-{action}.md` +- Gotchas: `.claude/skills/remediation-tool/references/gotchas.md` +- Graph endpoints: `.claude/skills/remediation-tool/references/graph-endpoints.md` +- Memory: `.claude/memory/feedback_365_remediation_tool.md` + +**Update end:** 2026-04-16 ~17:45 UTC +**Outcome:** Cascades incident triaged (John clean, Megan actively attacked but holding); `/remediation-tool` skill live and tested; vault working on Windows; Valleywide RemoteApp restored; Howard clean but targeted. + +--- + +## Update: 19:00 UTC — /save + /sync multi-user change summaries + +### Motivation + +The repo is now shared between Mike and Howard (per CLAUDE.md's new multi-user section). When either person pulls `main`, they want to know **what changed and who did it** without re-reading diffs. Mike asked `/save` and `/sync` to surface that automatically. + +### Changes + +**`.claude/commands/sync.md`** — rewrote to describe the new behavior: pre-pull incoming summary (sha / author / subject / age + `git diff --stat`), pre-push outgoing summary, post-sync totals by author. Conflict-resolution guidance now includes author attribution of each conflicting side. + +**`.claude/scripts/sync.sh`** — rewrote. Now: +- Loads `.claude/identity.json` to pick up current user's full name +- Commit message replaces the old "Claude Sonnet 4.5 co-author" boilerplate with user + machine attribution +- Before pulling: prints incoming commits as `sha author subject (ago)` plus a `git diff --stat` +- Before pushing: prints outgoing commits the same way +- End-of-run "Sync Summary" counts commits by author on each side +- Also added the `D:/claudetools` / `/d/claudetools` lowercase variants to the directory-search list (was hitting only TitleCase) + +**`.claude/commands/save.md`** — added a pre-commit **Change Summary** block (user + machine + `git status --short` + diff stats) and a post-commit summary (SHA + author + files in commit), with a "why" paragraph about multi-user attribution. + +### Design notes + +- Author attribution is `%an` from git (the person who made the commit), not the shared push account. Since each user has their own `user.email` + `user.name` set from identity.json during onboarding, `%an` carries the real person. +- For incoming commits viewed before pull, `%an` works because fetch pulls the commit objects with their original author metadata. +- Summaries are emitted by the bash script (sync) or by Claude following the command spec (save), not by a git hook. Keeps the behavior visible in normal terminal output when a user runs sync by hand. + +### Syntax-checked + +`bash -n .claude/scripts/sync.sh` — OK. + +### Files touched in this micro-update + +- `.claude/commands/sync.md` — rewritten +- `.claude/scripts/sync.sh` — rewritten +- `.claude/commands/save.md` — edited (added "After Saving" section) + +### Pending from this block + +- Actually commit + push everything accumulated in today's session (skill directory, reports, README updates, command updates, this log). Delegated to Gitea agent next. + +**Update end:** 2026-04-16 ~19:00 UTC