From 61081f70c22c816c8408ca3615ca88e543aafa42 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Tue, 2 Jun 2026 10:44:27 -0700 Subject: [PATCH] sync: auto-sync from GURU-BEAST-ROG at 2026-06-02 10:44:23 Author: Mike Swanson Machine: GURU-BEAST-ROG Timestamp: 2026-06-02 10:44:23 --- .claude/memory/MEMORY.md | 1 + .claude/memory/policy_pricing_verification.md | 30 +++++ .../session-logs/2026-06-02-session.md | 117 ++++++++++++++++++ projects/discord-bot/DISCORD_CLAUDE.md | 47 ++++++- .../bot/handlers/message_handler.py | 22 ++-- projects/discord-bot/scripts/delete-thread.sh | 56 +++++++++ wiki/clients/glaztech.md | 48 ++++++- wiki/index.md | 2 +- 8 files changed, 309 insertions(+), 14 deletions(-) create mode 100644 .claude/memory/policy_pricing_verification.md create mode 100644 clients/glaztech/session-logs/2026-06-02-session.md create mode 100644 projects/discord-bot/scripts/delete-thread.sh diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index f391486..f7ee16e 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -48,6 +48,7 @@ - [Paste-safe command formatting (Howard)](feedback_command_formatting.md) — Two clauses, one root cause: (a) multi-line scripts not semicolon one-liners (wrap breaks paste), (b) all code at column 0 inside fences (indentation breaks PowerShell paste). - [Autonomous infra/build setup](feedback_autonomous_infra_setup.md) — During infra/build/CI/dev setup, just install prerequisites and push through routine steps; reserve check-ins for genuine decisions (forks, destructive/outward, client/prod). - [Check patterns before asking](feedback_check_patterns_before_asking.md) — Before asking how to do something repeat-style (sync, save, sweep, billing), study existing artifacts and workflow docs first; reach for similar past artifacts as the template. +- [Pricing verification — no guessing](policy_pricing_verification.md) — ANY cost presented to the team or a client MUST be verified via live web lookup (WebFetch/WebSearch, fallback to headless Chrome). Never estimate from training data. Cite source + date inline. If unreachable, say so — do NOT substitute a guess. - [Client communication tone](feedback_client_tone.md) — How to write client-facing Syncro comments — expert partner, not intake questionnaire. - [Add Mike as owner on all Entra apps](feedback_entra_app_owner.md) — Apps created via management SP have no user owner — must add Mike manually or publisher verification fails. - [No TOML/config file approach for endpoints](feedback_no_toml_config_endpoints.md) — User explicitly prohibits TOML or config-file-based endpoint configuration — this will never be approved. diff --git a/.claude/memory/policy_pricing_verification.md b/.claude/memory/policy_pricing_verification.md new file mode 100644 index 0000000..250152d --- /dev/null +++ b/.claude/memory/policy_pricing_verification.md @@ -0,0 +1,30 @@ +# Policy: Pricing Verification — No Guessing + +**Set by:** Mike Swanson, 2026-06-02 + +Any time a cost or price is presented to the team or a client, it MUST be verified via a +live web lookup before being stated. Never use training-data recollections or estimates. + +## Rules + +- Hardware, parts, software licensing, SaaS pricing, labor rates — all require real-time verification. +- Use WebFetch / WebSearch first. Fall back to headless Chrome (`web-fetch-chrome.py`) if bot-blocked. +- Always cite source and date inline: `[$X.XX — Amazon, 2026-06-02]` +- If no pricing source is reachable, state that explicitly. Do NOT substitute a guess or a range + "from memory." +- Applies in all contexts: Discord bot, main session, any Claude session in this repo. + +## Scope + +Applies to ALL estimate types: +- Computer hardware (drives, RAM, CPUs, etc.) +- Replacement parts and peripherals +- Software licensing and subscriptions +- Third-party service quotes +- Repair labor estimates (when citing market rates) + +## Why + +Hardware prices fluctuate significantly. A "budget" vs "mid-range" vs "quality" drive can +vary by 2-3x depending on current market. Presenting stale or fabricated numbers to clients +damages trust and can result in inaccurate quotes. diff --git a/clients/glaztech/session-logs/2026-06-02-session.md b/clients/glaztech/session-logs/2026-06-02-session.md new file mode 100644 index 0000000..08544aa --- /dev/null +++ b/clients/glaztech/session-logs/2026-06-02-session.md @@ -0,0 +1,117 @@ +# Session Log — 2026-06-02 — Glaz-Tech Industries + +## User +- **User:** Mike Swanson (mike) +- **Machine:** GURU-BEAST-ROG +- **Role:** admin + +--- + +## Session Summary + +Mike requested a transport rule in the Glaztech Exchange Online tenant to allow messages from MailProtector as `noreply@azcomputerguru.com` through spam filtering. These are MailProtector quarantine digest notifications sent to Glaztech users on behalf of ACG's no-reply address. + +Before creating the rule, a message trace was pulled (via `Get-MessageTraceV2`) for `noreply@azcomputerguru.com` over the past 10 days to verify that messages were in fact being filtered by Microsoft. The trace confirmed the issue: the vast majority of digest messages delivered successfully, but some recipients were hitting `FilteredAsSpam` status (e.g., `tshaw@glaztech.com` on 2026-06-02 at 3:07 PM). The `gtimail@glaztech.com` address showed `Failed` status on every daily send — this is caused by the existing "GTIMail No-Reply - Reject Inbound" transport rule (Priority 1, `SentToPredicate` → `RejectMessageAction`) and is a separate, pre-existing issue noted for follow-up. + +Authentication to Exchange Online used the ComputerGuru Exchange Operator multi-tenant app (`b43e7342`) with certificate-based credentials from the vault. The token was acquired via `get-token.sh` for the `exchange-op` tier against the Glaztech tenant (`82931e3c-de7a-4f74-87f7-fe714be1f160`) and passed to `Connect-ExchangeOnline -AccessToken` with EXO PowerShell V3 (3.9.2). + +A new transport rule was created: **"SCL Bypass - noreply@azcomputerguru.com (MailProtector digests)"** at Priority 4, condition `From: noreply@azcomputerguru.com`, action `SetSCL -1`. This bypasses all spam and junk folder filtering for these digests. The rule was verified active immediately after creation. + +--- + +## Key Decisions + +- **SCL = -1 rather than domain-level bypass:** The sender address `noreply@azcomputerguru.com` is specific enough that setting SCL=-1 on it carries minimal risk. A domain-level bypass (`azcomputerguru.com`) was considered but rejected — too broad, would cover all ACG-origin mail. +- **Priority 4:** Placed below the existing SCL bypass rules (Priority 2–3) since no conflict exists; priority ordering doesn't matter for non-overlapping senders. Placed above any catch-all rules that might exist in the future. +- **Did not restrict by connector:** The "Inbound Spam Filter" connector has no SenderIPAddresses restriction (per prior decision — avoids blocking calendar invites from external M365 tenants). Adding a connector-based condition to the rule was avoided for the same reason. +- **gtimail@glaztech.com not addressed:** The daily `Failed` delivery to `gtimail@glaztech.com` is caused by the pre-existing "GTIMail No-Reply - Reject Inbound" rule. Mike did not request any change to that rule; flagged for separate review. + +--- + +## Problems Encountered + +- **`Get-MessageTrace` deprecated:** Initial call to `Get-MessageTrace` returned a deprecation warning and failed. Switched to `Get-MessageTraceV2`. Note: `Get-MessageTraceV2` does not accept `-PageSize` — that parameter does not exist on the V2 cmdlet. +- **`New-TransportRule -SenderAddresses` not valid:** First attempt used `-SenderAddresses` which is not a valid parameter. Correct parameter is `-From` for explicit sender address matching. +- **Cert not in Windows cert store:** Exchange Operator cert (`A615823DE1CAF15229027DEC075AFE32B900D82C`) is not installed in LocalMachine\My or CurrentUser\My on BEAST. Used `get-token.sh` cert-based JWT flow instead, passing the resulting bearer token to `Connect-ExchangeOnline -AccessToken`. + +--- + +## Configuration Changes + +- **Exchange Online transport rule created** in `glaztechindustries.onmicrosoft.com`: + - Name: `SCL Bypass - noreply@azcomputerguru.com (MailProtector digests)` + - Condition: `From = noreply@azcomputerguru.com` + - Action: `SetSCL -1` + - Priority: 4 + - State: Enabled + - Comments: "Bypass spam filtering for MailProtector quarantine digest emails sent as noreply@azcomputerguru.com. Created 2026-06-02 by ACG." + +--- + +## Credentials & Secrets + +- **Vault path used:** `msp-tools/computerguru-exchange-operator.sops.yaml` + - App: ComputerGuru - Exchange Operator + - Client ID: `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` + - Cert thumbprint: `A615823DE1CAF15229027DEC075AFE32B900D82C` + - Token acquired via: `bash .claude/skills/remediation-tool/scripts/get-token.sh exchange-op` + +--- + +## Infrastructure & Servers + +- **Glaztech tenant:** `glaztechindustries.onmicrosoft.com` +- **Tenant ID:** `82931e3c-de7a-4f74-87f7-fe714be1f160` +- **Inbound mail filter:** MailProtector — `glaztech-com.inbound.emailservice.io` +- **Inbound connector:** "Inbound Spam Filter" — Partner type, RequireTls=True, no IP restriction (intentional — preserves calendar invite delivery) +- **EXO PowerShell module:** ExchangeOnlineManagement 3.9.2 + +--- + +## Commands & Outputs + +```powershell +# Connect to Glaztech EXO with app-only token +$token = bash .claude/skills/remediation-tool/scripts/get-token.sh 82931e3c-de7a-4f74-87f7-fe714be1f160 exchange-op +Connect-ExchangeOnline -AccessToken $token -Organization 'glaztechindustries.onmicrosoft.com' -ShowBanner:$false + +# Message trace (last 10 days) — confirmed FilteredAsSpam occurrences +Get-MessageTraceV2 -SenderAddress 'noreply@azcomputerguru.com' -StartDate (Get-Date).AddDays(-10) -EndDate (Get-Date) +# Key finding: tshaw@glaztech.com → FilteredAsSpam (2026-06-02 3:07 PM) +# Key finding: gtimail@glaztech.com → Failed daily (pre-existing rule, separate issue) + +# Create rule +New-TransportRule ` + -Name 'SCL Bypass - noreply@azcomputerguru.com (MailProtector digests)' ` + -From 'noreply@azcomputerguru.com' ` + -SetSCL -1 ` + -Priority 4 ` + -Comments 'Bypass spam filtering for MailProtector quarantine digest emails sent as noreply@azcomputerguru.com. Created 2026-06-02 by ACG.' ` + -Enabled $true +``` + +**Final transport rule list (Glaztech):** +``` +Priority 0 Pensky Allow Enabled +Priority 1 GTIMail No-Reply - Reject Inbound Enabled +Priority 2 SCL Bypass - hartsglass + olemons (SHVSALES) Enabled +Priority 3 SCL Bypass - aaaglassinc.com (SHVSALES) Enabled +Priority 4 SCL Bypass - noreply@azcomputerguru.com (MailProtector digests) Enabled +``` + +--- + +## Pending / Incomplete Tasks + +- **gtimail@glaztech.com failing daily:** The "GTIMail No-Reply - Reject Inbound" rule (Priority 1) rejects all inbound mail to `gtimail@glaztech.com`. This causes the daily MailProtector digest to fail for that address. Confirm with Steve Eastman whether `gtimail@glaztech.com` should receive digests (i.e., whether the reject rule should have an exception or be modified). +- **Exchange Operator cert not in BEAST cert store:** If cert-based PowerShell connections are needed without `get-token.sh` (e.g., for interactive EXO sessions), the cert will need to be imported to the machine store. Not urgent — token flow works fine for bot-driven operations. + +--- + +## Reference Information + +- **Syncro customer ID:** 143932 +- **EXO rule created:** `SCL Bypass - noreply@azcomputerguru.com (MailProtector digests)` — Priority 4 +- **EXO PowerShell V2 deprecation note:** `Get-MessageTrace` deprecated Sept 1 2025; use `Get-MessageTraceV2` (no `-PageSize` parameter) +- **Vault:** `msp-tools/computerguru-exchange-operator.sops.yaml` +- **Token cache:** `/tmp/remediation-tool/82931e3c-de7a-4f74-87f7-fe714be1f160/exchange-op.jwt` diff --git a/projects/discord-bot/DISCORD_CLAUDE.md b/projects/discord-bot/DISCORD_CLAUDE.md index d6895a8..54b5583 100644 --- a/projects/discord-bot/DISCORD_CLAUDE.md +++ b/projects/discord-bot/DISCORD_CLAUDE.md @@ -73,6 +73,23 @@ drive the human's interactive session. --- +## Pricing — Always Verify, Never Guess + +**MANDATORY:** Before presenting any cost to Mike, Howard, or a client, verify current pricing +via live web lookup. Never estimate or recall costs from training data — hardware prices, +software licensing, and labor rates change constantly. + +Rules: +- Any dollar amount for hardware, parts, software, or services requires a real-time lookup + before being stated. +- Use WebFetch / WebSearch first. Fall back to headless Chrome if bot-blocked. +- Cite the source and date: `[$X.XX — Amazon, 2026-06-02]` +- If you cannot reach a pricing source, say so explicitly — do NOT substitute a guess. +- Applies to all estimate types: parts, repair quotes, hardware refreshes, software licensing, + labor, and third-party services. + +--- + ## Task Loop For every request, work this loop: @@ -86,13 +103,41 @@ For every request, work this loop: 4. **Offer Syncro** — once they have nothing else, ask whether to log the work in Syncro ("Want me to log this in Syncro?"). If yes, invoke `/syncro` to create or update the ticket. 5. **Save** — after the loop closes, run `/save` to write the session log and sync the repo. +6. **Kill the thread** — after `/save` completes successfully, delete the thread: + ```bash + bash C:/Users/guru/ClaudeTools/projects/discord-bot/scripts/delete-thread.sh + ``` + The Thread ID is in every `[DISCORD_CONTEXT]` block as `Thread ID: `. Do not delete + if `/save` failed or errored. Do not post a closing message — the deletion is immediate. + +--- + +## Thread Lifecycle — Auto-Delete on Completion + +Every `[DISCORD_CONTEXT]` block now includes `Thread ID: ` (injected by the bot after +the thread is resolved or created). This ID is what you pass to the delete script. + +**When to delete:** Only after step 6 of the Task Loop — `/save` succeeded, session log +committed and pushed. The thread is the conversation record; don't kill it before the log lands. + +**When NOT to delete:** +- `/save` failed or sync errored +- The user said "yes" to continuing (open follow-up items remain) +- The thread is an ongoing informational channel, not a single-task session + +**Script:** +```bash +bash C:/Users/guru/ClaudeTools/projects/discord-bot/scripts/delete-thread.sh +``` +Source: `projects/discord-bot/scripts/delete-thread.sh` — reads bot token from `.env`, calls +`DELETE /channels/{id}` on the Discord API. Exits 0 on HTTP 200/204, 1 on error. --- ## Who Is Asking: Discord User Identity Every message is prefixed with a `[DISCORD_CONTEXT]` block containing the sender's Discord -username, display name, and user ID. Always read this block to determine who is asking. +username, display name, user ID, and Thread ID. Always read this block to determine who is asking. ### Known Team Members — Full Access diff --git a/projects/discord-bot/bot/handlers/message_handler.py b/projects/discord-bot/bot/handlers/message_handler.py index 3e20102..6ebbd26 100644 --- a/projects/discord-bot/bot/handlers/message_handler.py +++ b/projects/discord-bot/bot/handlers/message_handler.py @@ -41,7 +41,18 @@ class MessageHandler: await message.reply("Hey! How can I help?") return + # Resolve or create the thread first so we can include its ID in context. + if isinstance(message.channel, discord.Thread): + thread = message.channel + else: + name = self._thread_name(user_text) if user_text else "Attachment" + thread = await message.create_thread( + name=name, + auto_archive_duration=1440, + ) + # Build caller-identity header so the agent always knows who is asking. + # Thread ID is included so the agent can delete the thread on completion. author = message.author display = getattr(author, "display_name", author.name) guild_name = message.guild.name if message.guild else "DM" @@ -51,20 +62,11 @@ class MessageHandler: f"User: @{author.name}" + (f" (display: {display})" if display != author.name else "") + f" | ID: {author.id}\n" - f"Channel: #{channel_name} | Guild: {guild_name}\n" + f"Channel: #{channel_name} | Thread ID: {thread.id} | Guild: {guild_name}\n" "[/DISCORD_CONTEXT]\n\n" ) content = (discord_ctx + user_text).strip() - if isinstance(message.channel, discord.Thread): - thread = message.channel - else: - name = self._thread_name(user_text) if user_text else "Attachment" - thread = await message.create_thread( - name=name, - auto_archive_duration=1440, - ) - attachment_paths = await self._download_attachments(message, thread.id) if attachment_paths: lines = "\n".join(f"- {p}" for p in attachment_paths) diff --git a/projects/discord-bot/scripts/delete-thread.sh b/projects/discord-bot/scripts/delete-thread.sh new file mode 100644 index 0000000..301fee9 --- /dev/null +++ b/projects/discord-bot/scripts/delete-thread.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# delete-thread.sh +# +# Deletes a Discord thread (channel) via the Discord REST API. +# Reads the bot token from projects/discord-bot/.env (DISCORD_TOKEN=...). +# +# Exit codes: +# 0 — deleted (HTTP 200 or 204) +# 1 — bad args, token not found, or API error + +set -euo pipefail + +THREAD_ID="${1:-}" +if [ -z "$THREAD_ID" ]; then + echo "[ERROR] Usage: delete-thread.sh " >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/../.env" + +if [ ! -f "$ENV_FILE" ]; then + echo "[ERROR] .env not found at $ENV_FILE" >&2 + exit 1 +fi + +# Extract token — handles quoted and unquoted values, ignores inline comments +DISCORD_TOKEN=$(grep '^DISCORD_TOKEN=' "$ENV_FILE" \ + | head -1 \ + | sed 's/^DISCORD_TOKEN=//' \ + | sed "s/[\"']//g" \ + | sed 's/#.*//' \ + | tr -d '[:space:]') + +if [ -z "$DISCORD_TOKEN" ]; then + echo "[ERROR] DISCORD_TOKEN not found or empty in $ENV_FILE" >&2 + exit 1 +fi + +RESP=$(curl -s -o /tmp/discord_delete_resp.txt -w "%{http_code}" \ + -X DELETE \ + "https://discord.com/api/v10/channels/${THREAD_ID}" \ + -H "Authorization: Bot ${DISCORD_TOKEN}" \ + -H "Content-Type: application/json") + +HTTP_CODE="$RESP" +BODY=$(cat /tmp/discord_delete_resp.txt 2>/dev/null || echo "") +rm -f /tmp/discord_delete_resp.txt + +if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then + echo "[OK] Thread ${THREAD_ID} deleted (HTTP ${HTTP_CODE})" + exit 0 +else + echo "[ERROR] Delete failed: HTTP ${HTTP_CODE} — ${BODY}" >&2 + exit 1 +fi diff --git a/wiki/clients/glaztech.md b/wiki/clients/glaztech.md index 7b4bfcc..be66fed 100644 --- a/wiki/clients/glaztech.md +++ b/wiki/clients/glaztech.md @@ -2,11 +2,13 @@ type: client name: glaztech display_name: Glaz-Tech Industries -last_compiled: 2026-05-24 +last_compiled: 2026-06-02 compiled_by: DESKTOP-0O8A1RL/claude-main sources: - clients/glaztech/session-logs/2026-04-20-session.md - clients/glaztech/session-logs/2026-04-21-session.md + - clients/glaztech/session-logs/2026-05-28-session.md + - clients/glaztech/session-logs/2026-06-02-session.md - clients/glaztech/reports/2026-04-17-phishing-incident-report.md - clients/glaztech/PROJECT_STATE.md - clients/glaztech/README.md @@ -43,12 +45,39 @@ No dedicated on-premises server infrastructure documented. Multi-site Windows en - **Tenant ID:** 82931e3c-de7a-4f74-87f7-fe714be1f160 - **Primary domain:** glaztech.com - **Inbound mail filter:** MailProtector — `glaztech-com.inbound.emailservice.io` (MX 5, sole MX as of 2026-04-17) +- **MailProtector IPs (EFSkipIPs on inbound connector):** 162.248.93.233, 162.248.93.81, 65.113.52.82 - **DMARC:** p=reject; sp=reject (hardened 2026-04-17, was p=none) - **DKIM:** CNAME records exist for selector1/selector2 — active status unverified [WARNING: confirm DKIM is active in M365] - **MFA status:** [WARNING] DISABLED as of 2026-04-21. Security Defaults off. No Conditional Access (requires Entra P1, not licensed). ~160 users with password-only sign-in. MFA rollout is open work item — do not enable Security Defaults until service account audit is complete (see Active Work). - **Licensing:** Basic M365 (no Entra P1 / Business Premium). Per-user MFA or Security Defaults are the available free options. - **Mailbox forwarding (internal, low risk):** Payroll@glaztech.com → carmen@glaztech.com; TUCCSR@glaztech.com → bryce@glaztech.com - **OAuth consent grants:** 38 grants — not audited as of last session +- **EXO PowerShell:** ExchangeOnlineManagement 3.9.2. `Get-MessageTrace` deprecated Sept 2025 — use `Get-MessageTraceV2` (no `-PageSize` parameter). + +### Exchange Online Transport Rules + +Full transport rule list as of 2026-06-02: + +| Priority | Name | Condition | Action | State | +|---|---|---|---|---| +| 0 | Pensky Allow | [unknown] | [unknown] | Enabled | +| 1 | GTIMail No-Reply - Reject Inbound | SentTo: gtimail@glaztech.com | RejectMessageAction | Enabled | +| 2 | SCL Bypass - hartsglass + olemons (SHVSALES) | From: hartsglass@centurytel.net, olemons@eastexglass.com, SSales@arkglass.com, bossier@glassservices.com | SetSCL -1 | Enabled | +| 3 | SCL Bypass - aaaglassinc.com (SHVSALES) | SenderDomainIs: aaaglassinc.com | SetSCL -1 | Enabled | +| 4 | SCL Bypass - noreply@azcomputerguru.com (MailProtector digests) | From: noreply@azcomputerguru.com | SetSCL -1 | Enabled | + +Rule GUIDs: Priority 2 = 482c714a-8780-4c62-ae0a-0b6da9ca9d52; Priority 3 = 7e0c01a8-ec22-43fe-b600-796c0f295aa5. GUIDs for Priority 0, 1, 4 not recorded. + +Note on Priority 1: The "GTIMail No-Reply - Reject Inbound" rule rejects ALL inbound mail to gtimail@glaztech.com, which causes the daily MailProtector digest for that address to fail. This is a pre-existing rule — review with Steve is pending (see Active Work). + +### Inbound Connector + +- **Name:** "Inbound Spam Filter" +- **Type:** Partner +- **RequireTls:** True +- **EFSkipIPs:** 162.248.93.233, 162.248.93.81, 65.113.52.82 (MailProtector IPs) +- **SCLMinusOne:** null (EOP re-evaluates all mail; do NOT change to true — too broad) +- **SenderIPAddresses restriction:** None (intentional — avoids blocking calendar invites from external M365 tenants) ### Network @@ -61,11 +90,13 @@ No dedicated on-premises server infrastructure documented. Multi-site Windows en - **Remediation tool:** ComputerGuru apps consented in tenant (Exchange Operator, Security Investigator, Tenant Admin, Defender Add-on) - **Exchange Operator App ID:** b43e7342-5b4b-492f-890f-bb5a4f7f40e9 +- **Exchange Operator cert thumbprint:** A615823DE1CAF15229027DEC075AFE32B900D82C (not in Windows cert store on BEAST — use `get-token.sh` bearer token flow) - **Remediation tool app (AI):** fabb3421-8b34-484b-bc17-e46de9703418 - **Exchange Admin role:** Assigned to ACG service principal in Entra - **Global Admin account:** admin@glaztechindustries.onmicrosoft.com (ACG admin only — external GA from tomakkglass.com removed 2026-04-21) - **Vault path:** `clients/glaztech/` [no SOPS credential file documented — remediation tool uses MSP-wide app credentials] - **Exchange Operator vault:** `msp-tools/computerguru-exchange-operator.sops.yaml` +- **Token acquisition:** `bash .claude/skills/remediation-tool/scripts/get-token.sh exchange-op` → `Connect-ExchangeOnline -AccessToken $token -Organization 'glaztechindustries.onmicrosoft.com'` - **DNS access:** `root@172.16.3.10` (IX server) - **Deploy (endpoints):** ScreenConnect or GuruRMM @@ -73,9 +104,14 @@ No dedicated on-premises server infrastructure documented. Multi-site Windows en - **Phishing via direct-to-M365 MX bypass:** Two phishing campaigns in April 2026 succeeded because DNS had a secondary MX record (`glaztech-com.mail.protection.outlook.com` at priority 10) that bypassed MailProtector. Hardened: MX 10 removed, DMARC to p=reject, Enhanced Filtering for Connectors enabled. Do not re-add a secondary MX record. - **Inbound connector IP restriction:** Do NOT restrict `SenderIPAddresses` on the "Inbound Spam Filter" connector — blocks legitimate calendar invites from external M365 tenants (learned from Dataforth incident). EFSkipIPs are set to MailProtector IPs instead. +- **Do NOT set SCLMinusOne=true on connector:** This would trust MailProtector's verdict for all inbound mail — too broad. Use targeted transport rules for specific senders instead. +- **DMARC-rejecting vendor senders:** With Enhanced Filtering enabled, EOP looks past MailProtector to the original sender's SPF/DKIM/DMARC. Vendors with `p=reject` domains (e.g., centurytel.net, eastexglass.com) get hard 550 5.7.509 NDR rejections. Fix: SCL=-1 transport rule scoped to the specific sender address or domain. Transport rules evaluate before DMARC enforcement in EOP. +- **EXO transport rule name limit:** 64-character maximum. Plan names accordingly. +- **EXO REST API:** Direct `/TransportRule` REST endpoints 404 in this tenant. Use `InvokeCommand` pattern: `POST /adminapi/beta/{tenant}/InvokeCommand` with `{"CmdletInput": {"CmdletName": "New-TransportRule", "Parameters": {...}}}`. - **Service accounts need audit before MFA rollout:** Shoretel, mitel, Gti-FaxFinder, GTIMail, GTIQUOTE, CAS1944, clerk — all need SMTP/auth method confirmation before Security Defaults can be enabled. - **PDF preview broken (MOTW):** Windows KB5066791/KB5066835 broke PDF preview on network shares via Mark of the Web. Fix scripts are ready in `clients/glaztech/` — deployment is pending (as of 2026-03-30). - **clearcutglass.com DMARC history:** Corena Spottsville (clearcutglass.com) emails to seastman and zulema were rejected. Temporary transport rule (SCL=-1) was set and removed on 2026-04-21. SPF ~all weakness noted to Team Logic IT (Jordan Fox, jfox@tlit60302.com); recommend they harden to -all and confirm DKIM. +- **glassservices.com SPF broken:** `bossier@glassservices.com` publishes `v=spf1 -all` — rejected by all mail providers. SCL=-1 rule covers this as a workaround. Steve should notify vendor to fix SPF. - **Client tone:** ACG has managed GlazTech ~15 years. Steve Eastman is a trusted internal IT partner. Comments and communication should lead with what we know, state findings and actions taken, ask only one targeted question if needed — not open-ended discovery. - **Unlicensed accounts (pending Steve confirmation):** Chauntelle@glaztech.com, Denouser1@glaztech.com, Gti-FaxFinder@glaztech.com. @@ -103,6 +139,10 @@ Waiting on Steve's reply to: MFA rollout plan: Phase 1 — user communication (install Authenticator); Phase 2 — enable enforcement; Phase 3 — follow-up stragglers; Phase 4 (future/P1) — Conditional Access with trusted IPs for office locations. +### gtimail@glaztech.com Daily Digest Failure (Pending — review with Steve) + +The "GTIMail No-Reply - Reject Inbound" transport rule (Priority 1) rejects all inbound mail to `gtimail@glaztech.com`, causing the daily MailProtector digest for that address to fail every day. This is a pre-existing rule and was not modified during the 2026-06-02 session. Confirm with Steve Eastman whether `gtimail@glaztech.com` should receive MailProtector digests — if so, the rule needs an exception or the recipient needs to be removed from the MailProtector digest list. + ### Pending follow-ups - Audit 38 OAuth consent grants (not done as of 2026-04-21) @@ -110,7 +150,9 @@ MFA rollout plan: Phase 1 — user communication (install Authenticator); Phase - Monitor DMARC aggregate reports (rua=noreply@glaztech.com — should be a monitored mailbox or reporting service) - Security awareness training for staff (multiple employees forwarded and replied to obvious phishing in April 2026) - Review whether any user clicked phishing links (check sign-in logs for suspicious auth attempts post-April 17) -- Confirm test email clean delivery from clearcutglass.com after DMARC fix +- Notify Steve: glassservices.com vendor needs to fix their SPF record (`v=spf1 -all`) +- Harts Glass original rejected emails need to be resent by sender — our SCL bypass is live but NDR'd messages do not auto-retry +- Consider creating retroactive Syncro ticket for 2026-05-28 SHVSALES email delivery work ## History Highlights @@ -119,6 +161,8 @@ MFA rollout plan: Phase 1 — user communication (install Authenticator); Phase - **2026-04-17** — Two phishing campaigns bypassed MailProtector via direct-to-M365 MX bypass. 32 messages purged across 8 users. Hardened: MX 10 removed, DMARC p=reject, Enhanced Filtering Connectors enabled. Remediation tool onboarded (admin consent, Exchange Admin role). Forensic evidence preserved in `clients/glaztech/reports/`. - **2026-04-20** — Exchange transport rule created to allow clearcutglass.com mail (DMARC bypass, SCL=-1) while Team Logic IT fixed their DNS. Ticket #32176 created. - **2026-04-21** — clearcutglass.com DNS fixed by Team Logic IT (Jordan Fox). Transport rule removed. External Global Admin (glaztechadmin from tomakkglass.com / Team Logic IT) removed from tenant. M365 security review surfaced: no MFA, 38 OAuth grants, unlicensed accounts, service account audit needed. Ticket #32186 opened for MFA implementation. Feedback: use expert-partner tone with Steve, not open-ended discovery questions. +- **2026-05-28** — SHVSALES@glaztech.com vendor email delivery failure. Root cause: vendors (centurytel.net, eastexglass.com) publish DMARC p=reject; Enhanced Filtering re-evaluates past MailProtector relay, producing 550 5.7.509 NDR. Fix: two SCL=-1 transport rules created (Priority 2: specific addresses for hartsglass, olemons, SSales, bossier; Priority 3: aaaglassinc.com domain). glassservices.com SPF broken (`-all`) — workaround only, vendor must fix. +- **2026-06-02** — MailProtector quarantine digest messages from `noreply@azcomputerguru.com` confirmed hitting `FilteredAsSpam` for some recipients (e.g., tshaw@glaztech.com). Transport rule created: "SCL Bypass - noreply@azcomputerguru.com (MailProtector digests)" at Priority 4 (From=noreply@azcomputerguru.com, SetSCL=-1). Message trace via `Get-MessageTraceV2` also revealed `gtimail@glaztech.com` failing daily due to pre-existing Priority 1 reject rule — flagged for Steve review. ## Backlinks diff --git a/wiki/index.md b/wiki/index.md index 02d62ec..f46c696 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -25,7 +25,7 @@ Run `/wiki-lint` to check for stale entries and broken backlinks. | [ACG Internal Infrastructure](clients/internal-infrastructure.md) | ACG's own hosting infra — Neptune Exchange (cert expires 2026-05-31, DkimSigner disabled), IX server, Cloudflare tunnel workaround, ACG M365 tenant gaps | 2026-05-24 | | [BirthBiologic](clients/birth-biologic.md) | Bio/healthcare; BB-SERVER (WS2016) GuruRMM enrolled; Datto→SharePoint migration incomplete; M365 apps partially consented | 2026-05-24 | | [CryoWeave](clients/cryoweave.md) | Custom cryogenic cable assemblies; cPanel on IX; website redesign + SEO project in progress; Syncro ID not documented | 2026-05-24 | -| [Glaz-Tech Industries](clients/glaztech.md) | ~200 users, 9 locations; M365; two phishing campaigns bypassed MailProtector via secondary MX (removed); no MFA enforcement yet | 2026-05-24 | +| [Glaz-Tech Industries](clients/glaztech.md) | ~200 users, 9 locations; M365; two phishing campaigns bypassed MailProtector via secondary MX (removed); no MFA enforcement yet; SCL bypass rules for vendor DMARC failures + MailProtector digests | 2026-06-02 | | [Grabb & Durando Law Office](clients/grabb-durando.md) | Personal injury law firm; GND-SERVER GuruRMM enrolled; AI demand review app scoped ($4K–$7K); website migration pending; plaintext DB password in README needs vaulting | 2026-05-24 | | [Pavon](clients/pavon.md) | Former/archive client; GeoVision NVR surveillance; OwnCloud at 172.16.3.22 backed by Uranus; cron stacking fixed; Nextcloud migration deferred 3–6 months | 2026-05-24 | | [Rednour Law Offices](clients/rednour.md) | Law firm; M365 rednourlaw.com (tenant 4a4ca18a) fully onboarded 2026-05-31; all 5 ComputerGuru SPs consented; no MDE license; 3 workstations GuruRMM enrolled (FRONTDESKRECEPT/LEGALASST/REDNOURCARRIEVI); Carla Skinner renamed from Emma; prior MSP agents (ScreenConnect/Splashtop/Datto) still present; shared-drive access for Nick Pafford deferred | 2026-06-02 |