diff --git a/.claude/commands/mailbox.md b/.claude/commands/mailbox.md index db5e3d8e..e536a2ec 100644 --- a/.claude/commands/mailbox.md +++ b/.claude/commands/mailbox.md @@ -2,7 +2,9 @@ Read and send mail for an Arizona Computer Guru mailbox via Microsoft Graph, using the dedicated **ComputerGuru Mailbox** app (`1873b1b0-3377-485c-a848-bae9b2f8f1f5`). Defaults to the mailbox of the user running it (from `identity.json`). -> **Mail path (working — repointed 2026-06-17).** `/mailbox` uses the dedicated single-tenant **ComputerGuru Mailbox** app (`1873b1b0-3377-485c-a848-bae9b2f8f1f5`; vault `msp-tools/computerguru-mailbox.sops.yaml`; Mail.ReadWrite + Mail.Send + Contacts.ReadWrite; azcomputerguru.com only). Tokens come from the suite tool: `bash .claude/skills/remediation-tool/scripts/get-token.sh azcomputerguru.com mailbox` (cert-preferred, secret fallback, 55-min cache). This **replaces the deleted `fabb3421`** (Claude-MSP-Access), removed from the tenant 2026-06-14 — it returns **AADSTS700016**; do NOT reintroduce it. The mailbox app's service principal is **disabled when idle**: on a token 401 "account is disabled", enable the SP, then retry. +> **Inbox-rule capability (added 2026-06-23).** The app now also holds **`MailboxSettings.ReadWrite`** (Graph app role `6931bccd-447a-43d1-b442-00a195474933`, admin-consented via the Tenant Admin app `709e6eed`). This is what enables creating/modifying mailbox **inbox rules** (`messageRules`) — `Mail.ReadWrite` alone returns 403 on that endpoint. First use: a "keep DMARC reports in Inbox" rule on `rua@azcomputerguru.com`. +> +> **Mail path (working — repointed 2026-06-17).** `/mailbox` uses the dedicated single-tenant **ComputerGuru Mailbox** app (`1873b1b0-3377-485c-a848-bae9b2f8f1f5`; vault `msp-tools/computerguru-mailbox.sops.yaml`; Mail.ReadWrite + Mail.Send + Contacts.ReadWrite + MailboxSettings.ReadWrite; azcomputerguru.com only). Tokens come from the suite tool: `bash .claude/skills/remediation-tool/scripts/get-token.sh azcomputerguru.com mailbox` (cert-preferred, secret fallback, 55-min cache). This **replaces the deleted `fabb3421`** (Claude-MSP-Access), removed from the tenant 2026-06-14 — it returns **AADSTS700016**; do NOT reintroduce it. The mailbox app's service principal is **disabled when idle**: on a token 401 "account is disabled", enable the SP, then retry. ## Usage @@ -25,7 +27,7 @@ Microsoft Graph access to ACG's own mailboxes (azcomputerguru.com tenant). Readi ## API Configuration -- **App:** ComputerGuru Mailbox (dedicated single-tenant Graph app), `client_id = 1873b1b0-3377-485c-a848-bae9b2f8f1f5` (Mail.ReadWrite + Mail.Send + Contacts.ReadWrite, azcomputerguru.com only). Replaces the deleted `fabb3421`. +- **App:** ComputerGuru Mailbox (dedicated single-tenant Graph app), `client_id = 1873b1b0-3377-485c-a848-bae9b2f8f1f5` (Mail.ReadWrite + Mail.Send + Contacts.ReadWrite + MailboxSettings.ReadWrite, azcomputerguru.com only). Replaces the deleted `fabb3421`. - **Tenant:** `azcomputerguru.com` - **Token:** acquire via the suite tool — `bash .claude/skills/remediation-tool/scripts/get-token.sh azcomputerguru.com mailbox` (cert-preferred, secret fallback, 55-min cache in `/tmp/remediation-tool//mailbox.jwt`). Do NOT roll your own `client_credentials` here. Credential vault entry: `msp-tools/computerguru-mailbox.sops.yaml`. - **SP idle-toggle:** the mailbox app's service principal is disabled when idle. On a token 401 "account is disabled", enable the SP, then retry. diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index abde5132..f31b932e 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -30,6 +30,7 @@ - [reference_backblaze_storage_rate](reference_backblaze_storage_rate.md) -- ACG's Backblaze B2 storage cost rate ($0.00695/GB) for the GuruRMM mspbackups storage-cost calculation - [Unraid VM no-IP causes](unraid-windows-vm-virtio-no-ip.md) — PRIMARY (general "new VMs stopped getting IPs lately"): Docker sets bridge-nf-call-iptables=1, so br0 VM DHCP OFFERs hit DOCKER-FORWARD (no br0 ACCEPT) and get dropped; new VMs can't complete DORA (existing renew via ESTABLISHED). Fix `=0` runtime (needs persistent post-Docker hook; not yet persisted on Jupiter). SECONDARY (Windows VM): virtio-net has no in-box driver -> use e1000 or virtio-win. Diagnose: tcpdump DHCP on pfSense; /sys vnetN rx_packets. - [Starr Pass mail routing](reference_starrpass_mail_routing.md) — starrpass.com is DIRECT to MS (EOP/Defender, tenant 222450dd…); only devconllc.com is on Mailprotector (MP acct 16170). Check @starrpass.com quarantine/rejects via remediation-tool, not Mailprotector. +- [INKY outbound breaks DMARC](reference_inky_outbound_breaks_dmarc.md) — Reverse-resolve DMARC rua failing IPs before blaming a sender: ipw-outbound.inkyphishfence.com / us.cloud-sec-av.com = INKY re-injection breaking DKIM+SPF. INKY is in-M365 (connectors+transport rules) per enrolled tenant, but hosting-level (IX/cPanel website) outbound also routes through it independent of M365 enrollment. Fix is INKY-side (outbound DKIM/SPF/ARC), not cPanel DNS. - [AAD Connect msDS-KeyCredentialLink writeback](reference_aadconnect_keycredlink_writeback.md) — "completed-export-errors" + 8344 INSUFF_ACCESS_RIGHTS on a protected admin account = WHfB key writeback blocked by AdminSDHolder. Diagnose with csexport /f:x; fix with dsacls WP;msDS-KeyCredentialLink on AdminSDHolder + SDProp. - [UniFi Site Manager cloud API](reference_unifi_site_manager_api.md) — `api.ui.com` + `X-API-KEY` (vault `services/unifi-site-manager`) = remote access to the WHOLE ACG UniFi fleet (~36 consoles) outside UOS. Tier1 `/v1/hosts|sites|devices|isp-metrics` = inventory+health+WAN. Tier2 CONNECTOR `/v1/connector/consoles/{id}/proxy/network/api/s/default/stat/{device,sta}` = **full UOS parity** (per-radio cu_total airtime + per-client RSSI) for ANY console, remote. Backend `unifi-wifi/scripts/gw-sitemanager.sh` (`fleet|devices|sites|isp|net`). Standalone UDM WAN SSH usually firewalled; per-console SSH pw at `clients//udm-ssh`. - [reference_sqlx_migrations_immutable](reference_sqlx_migrations_immutable.md) -- NEVER edit an already-applied sqlx migration file — even a comment. sqlx::migrate! checksums each file at compile time and validates against _sqlx_migrations at startup; a changed checksum crash-loops the server with "migration N was previously applied but has been modified". Code review MUST flag any edit to an applied migration. diff --git a/.claude/memory/reference_inky_outbound_breaks_dmarc.md b/.claude/memory/reference_inky_outbound_breaks_dmarc.md new file mode 100644 index 00000000..1a1f4eac --- /dev/null +++ b/.claude/memory/reference_inky_outbound_breaks_dmarc.md @@ -0,0 +1,14 @@ +--- +name: reference-inky-outbound-breaks-dmarc +description: INKY/GuruProtect outbound re-injection breaks DMARC alignment; reverse-resolve DMARC report source IPs before attributing failures +metadata: + type: reference +--- + +When analyzing DMARC aggregate (rua) reports, **reverse-resolve every failing source IP before attributing a failure to a sender.** Multiple ACG client failures (glaztech.com p=reject, cryoweave.com p=quarantine) traced not to the apps/websites first assumed, but to **INKY (GuruProtect)** outbound: PTRs `ipw-outbound.inkyphishfence.com` and `us.cloud-sec-av.com` (AWS IP pools 3.x / 34.210.x / 35.174.x / 100.2x.x). + +**Mechanism:** INKY receives an already-signed message (M365 `selector1` or cPanel `default` DKIM), **modifies the body** (GuruProtect banner / link rewrite), then re-sends from its own IPs. Body change breaks the original DKIM; INKY IPs aren't in the domain's SPF → SPF fails too → DMARC fails. So a domain can be 99% aligned and still show a steady trickle of INKY-origin fails. + +**INKY topology (per Mike, 2026-06-23):** INKY is installed *directly into M365* via connectors + transport rules per enrolled tenant — NOT an external-only relay. So check tenant enrollment. NOTE: cryoweave M365 is **NOT** enrolled in INKY, yet cryoweave.com mail still appeared from INKY outbound IPs — that traffic is the **IX/cPanel website** (WordPress, `envelope=ix.azcomputerguru.com`, cPanel `default` DKIM) whose *hosting-level* outbound routes through INKY, independent of the M365 tenant. Don't assume "INKY fail" == "tenant on INKY." + +**Fix is INKY-side, not DNS-blind:** republishing cPanel DKIM or adding the web server to SPF will NOT fix it (INKY breaks the sig downstream and the mail comes from INKY's IP, not the web server's). Real options: enable INKY outbound DKIM signing per domain, add INKY's SPF include, or ARC — or route low-volume app/website mail through an authenticated M365 SMTP path so it aligns directly. See [[feedback-dmarc-rua-inky-onboarded-only]]. diff --git a/.gitignore b/.gitignore index 9f3f46c0..abdf0b70 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,4 @@ temp/ # Transient coord softfail queue (machine-local; drains on /sync, never committed) .claude/coord-queue.jsonl +.boot.bin diff --git a/clients/cryoweave/session-logs/2026-06/2026-06-23-mike-dmarc-rua-analysis-and-contact-form-fix.md b/clients/cryoweave/session-logs/2026-06/2026-06-23-mike-dmarc-rua-analysis-and-contact-form-fix.md new file mode 100644 index 00000000..aa8f857d --- /dev/null +++ b/clients/cryoweave/session-logs/2026-06/2026-06-23-mike-dmarc-rua-analysis-and-contact-form-fix.md @@ -0,0 +1,86 @@ +## User +- **User:** Mike Swanson (mike) +- **Machine:** GURU-5070 +- **Role:** admin + +## Session Summary + +Started by analyzing the DMARC aggregate (RUA) reports collecting in the shared `rua@azcomputerguru.com` mailbox. Initial inbox-only scan undercounted because reports were landing in the mailbox's Junk folder; including Junk, the corpus was 13 inbox + 8 junk reports across glaztech.com (p=reject) and cryoweave.com (p=quarantine). glaztech was healthy (~99.9% aligned, broad reporter coverage). cryoweave showed a high fail rate on low volume. + +Drilling into the failures, every failing source IP reverse-resolved to INKY/GuruProtect outbound infrastructure (`ipw-outbound.inkyphishfence.com`, `us.cloud-sec-av.com`). After two wrong attributions (first "GTIware app mail", then "the IX/cPanel website outbound"), the corrected root cause emerged: the failures are INKY **re-injecting** mail (banner/link rewrite breaks the original DKIM; INKY's IPs aren't in SPF). Mike clarified INKY is installed directly into M365 via connectors/transport rules per enrolled tenant, and that cryoweave is NOT enrolled in INKY. A clean test send proved cryoweave M365 outbound is connector-free and aligned for normal receivers (Google report = dmarc=pass); the RUA "failures" are **receiver-side** artifacts — cryoweave mailing INKY-protected receivers (azcomputerguru.com and other ACG/INKY tenants), where the receiver's INKY breaks the eval. + +Fixed the mailbox-app permission gap that blocked creating the "keep DMARC reports in Inbox" rule: the ComputerGuru Mailbox app (1873b1b0) had Mail.ReadWrite but not MailboxSettings.ReadWrite. Added the permission and admin-consented it via the Tenant Admin app (709e6eed), then created the inbox rule successfully. + +Fixed cryoweave's website contact-form sends (the genuinely broken path — Ninja Forms via cPanel PHP-mail, unaligned). Per Mike's choices: created shared mailbox `noreply@cryoweave.com`, registered app "CryoWeave Web Mailer" with Graph Mail.Send scoped by ApplicationAccessPolicy to noreply@ only, and deployed a WordPress mu-plugin that routes all `wp_mail()` through Graph as noreply@ (DMARC-aligned, with fallback). Verified `wp_mail()` returns true (Graph 202). + +Finally, investigated the cryoweave M365 quarantine. No website-mailer messages were present. The only 2 quarantined items were inbound external false-positive "High Confidence Phish" flags (a real business inquiry from girotti-machineteam.com and an Epiq legal notice), both SPF=pass from their legitimate senders. Released both to Greg and added Tenant Allow entries for the two sender domains. + +## Key Decisions + +- **noreply@cryoweave.com shared mailbox (not greg@):** dedicated, no license, keeps website mail off Greg's personal box, works with unattended app-only sending. +- **App-only Graph send via custom mu-plugin (not delegated OAuth / WP Mail SMTP):** unattended (no token-refresh / interactive sign-in), no paid plugin, works with a shared mailbox, scoped via ApplicationAccessPolicy. Basic SMTP AUTH was not an option — off by default on this new tenant and deprecated by Microsoft. +- **mu-plugin uses `pre_wp_mail` short-circuit and falls back to default wp_mail on any Graph failure** — resilient; intercepts all wp_mail (Ninja Forms included) regardless of plugin config; forces aligned From while preserving To/Cc/Reply-To/Subject/body. +- **Did NOT republish cPanel DKIM / add IX to cryoweave SPF** — the originally-agreed fix was wrong: cPanel `default` DKIM is already correctly published, and the failing path needed M365-origin sending, not cPanel patching. +- **Reverse-resolve DMARC report source IPs before attributing failures** — saved as a reference memory; this corrected the analysis twice. +- **Released the 2 quarantined messages despite being outside the "website-mailer" scope** — verified by headers as legitimate inbound false positives (SPF=pass), and the Girotti one is a likely sales lead. + +## Problems Encountered + +- **Detail script mis-attributed every record as a failure** — looked for `policy_evaluated` under `` instead of under ``. Corrected; the count went from a bogus "all fail" to the true small fail set. +- **Two wrong root-cause attributions** (GTIware, then cPanel website outbound). Resolved by reverse-resolving the failing IPs to INKY and by a controlled test send. +- **Test send to mike@azcomputerguru.com was confounded** — azcomputerguru.com is itself behind INKY inbound, so the receiver's INKY relayed the test from an INKY IP and failed it. Re-tested intra-tenant + relied on the Gmail RUA report (pass) to prove cryoweave outbound is clean. +- **Graph messageRules POST returned 403** — mailbox app lacked MailboxSettings.ReadWrite. Added + consented via tenant-admin. +- **Azure replication lag** on SP creation and appRoleAssignment (404/400 "does not reference a valid application object"). Resolved with retry loops. +- **EXO REST param-shape quirks** — `AppId`/`RecipientAddress`/`Entries` require arrays; `Identity` (Release-QuarantineMessage, Get-QuarantineMessageHeader) requires a scalar. Mixed these up once each. +- **sops -e run from repo dir, not vault dir** — "no creation rules"; wrote the client_secret in PLAINTEXT to the vault file briefly. Fixed by cd-ing into the vault root before sops -e; nothing committed. Logged as friction. +- **SSH to IX tripped fail2ban/cPHulk** from rapid retries; cleared on its own. Pinned host key (`SHA256:GZYP/o5XUoRtFRCv1iGjxmqGfQoEsMuiNQBJucoJUh8`) for batch use; deployed via plink stdin. + +## Configuration Changes + +- **Created** `clients/cryoweave/session-logs/2026-06/2026-06-23-mike-dmarc-rua-analysis-and-contact-form-fix.md` (this log). +- **Created** `.claude/memory/reference_inky_outbound_breaks_dmarc.md` + index line in `.claude/memory/MEMORY.md`. +- **Edited** `.claude/commands/mailbox.md` — added MailboxSettings.ReadWrite to permission lists + dated inbox-rule note. +- **Edited** `wiki/clients/cryoweave.md` — DMARC analysis correction + contact-form-fix infrastructure block. +- **Created (vault)** `clients/cryoweave/web-mailer-app.sops.yaml` — CryoWeave Web Mailer app creds. +- **M365 (azcomputerguru.com tenant):** added Graph `MailboxSettings.ReadWrite` app role to ComputerGuru Mailbox app (1873b1b0) + admin consent; created inbox rule "DMARC reports -> keep in Inbox" on rua@ (ruleId AQAAAARa7gA=). +- **M365 (cryoweave tenant):** created shared mailbox noreply@cryoweave.com; registered app "CryoWeave Web Mailer"; created client secret; mail-enabled security group webmailer-scope@cryoweave.com; New-ApplicationAccessPolicy scoping the app to noreply@; released 2 quarantined messages; added 2 Tenant Allow sender entries. +- **IX server (cryoweave site):** created `wp-content/mu-plugins/` + deployed `cw-graph-mailer.php` (mode 640, owner cryoweave). + +## Credentials & Secrets + +- **CryoWeave Web Mailer app** (cryoweave tenant): client_id `4003c79e-d4ac-4265-ba36-da783d89ee4d`, app_object_id `4fde07ca-e2fb-4c4c-9b25-2fb38577c7c2`, sp_id `495cfeb9-6383-4dba-bc08-418580bde548`. Secret (40 chars, expires 2028-06-23) vaulted at `clients/cryoweave/web-mailer-app.sops.yaml` (field `credentials.client_secret`). Permission: Graph Mail.Send (application), scoped to noreply@ via ApplicationAccessPolicy. The same secret is embedded in the server mu-plugin `cw-graph-mailer.php`. +- No other new credentials. IX root password + WHM API token read from existing vault `infrastructure/ix-server.sops.yaml`. + +## Infrastructure & Servers + +- **rua@azcomputerguru.com** — shared "DMARC Reports" mailbox, ACG tenant (ce61461e-81a0-4c84-bb4a-7b354a9a356d). mike@ FullAccess. +- **ComputerGuru Mailbox app** 1873b1b0-3377-485c-a848-bae9b2f8f1f5 (objectId 43a24875-c927-49ac-b219-b9044e958fee, spId e8bc5e2d-12d9-4e0a-b833-6f8de7a56973). Now: Mail.ReadWrite + Mail.Send + Contacts.ReadWrite + MailboxSettings.ReadWrite. +- **Tenant Admin app** 709e6eed-0711-4875-9c44-2d3518c47063 — has Application.ReadWrite.All + AppRoleAssignment.ReadWrite.All in both ACG and cryoweave tenants. +- **cryoweave tenant** 44705a37-b5d8-4bb1-882d-e18775612ada. Mailboxes: greg@cryoweave.com (user), noreply@cryoweave.com (shared, GUID 19495079-8295-4df2-8e51-4686fc764648). No outbound/inbound connectors, no transport rules. SMTP AUTH off by default. +- **IX server** ix.azcomputerguru.com / 172.16.3.10 (ext 72.194.62.5), WHM 11.136.0.24, cPanel user cryoweave, docroot /home/cryoweave/public_html. SSH host key `SHA256:GZYP/o5XUoRtFRCv1iGjxmqGfQoEsMuiNQBJucoJUh8`. Ninja Forms 3.14.4 active; WP admin_email rob@azcomputerguru.com. +- **INKY outbound IPs (DMARC source for re-injected mail):** ipw-outbound.inkyphishfence.com (34.210.15.192, 3.132.108.44, 3.132.222.232, 100.24.129.5, 100.21.157.149, 3.231.237.226), us.cloud-sec-av.com (35.174.145.124). + +## Commands & Outputs + +- Token tiers: `bash .claude/skills/remediation-tool/scripts/get-token.sh ` (mailbox | tenant-admin | exchange-op | investigator-exo). +- EXO InvokeCommand: POST `https://outlook.office365.com/adminapi/beta/{tenant}/InvokeCommand` body `{"CmdletInput":{"CmdletName":..,"Parameters":..}}`. Array params: AppId, RecipientAddress, Entries. Scalar: Identity. +- mu-plugin live test: `wp eval-file /tmp/cw_mailtest.php --allow-root` → `wp_mail returned: bool(true)`. +- Test message auth (to mike@, confounded by ACG INKY inbound): `spf=fail (sender IP is 34.210.15.192) dkim=fail dmarc=fail action=quarantine`. +- plink pattern: `plink -ssh -pw "$PW" -batch -hostkey "$HK" root@172.16.3.10 "cmd"` ; file deploy via `... "cat > path" < localfile`. + +## Pending / Incomplete Tasks + +- **Greg's broader "mail not reaching recipients" ticket** is NOT confirmed resolved. cryoweave outbound is clean for normal receivers; if issues persist they are recipient-specific — pull a message trace on greg@ (lagging at session end) + obtain an NDR. +- **End-to-end contact-form test** via an actual browser form submission not performed; wp_mail interception verified instead (Ninja Forms uses wp_mail). +- **Tenant Allow entries expire 2026-09-21** — if girotti/epiq re-quarantine, convert to a mail-flow rule for permanence. +- **ApplicationAccessPolicy enforcement** can lag ~30 min; Test-ApplicationAccessPolicy not re-confirmed (real send as noreply@ succeeded). +- Test artifacts left: `[TEST]` in mike@ junk/quarantine, `[TEST2]`/`[MU-TEST]` in greg@. +- Optional: `/wiki-compile client:cryoweave --full`. + +## Reference Information + +- Inbox rule id: AQAAAARa7gA= (rua@ mailbox). +- Graph app role ids: Mail.Send `b633e1c5-b582-4048-a93e-9f11b44c7e96`, MailboxSettings.ReadWrite `6931bccd-447a-43d1-b442-00a195474933`. +- Vault: `clients/cryoweave/web-mailer-app.sops.yaml`, `infrastructure/ix-server.sops.yaml`. +- Memory: `.claude/memory/reference_inky_outbound_breaks_dmarc.md`. +- mu-plugin path: `/home/cryoweave/public_html/wp-content/mu-plugins/cw-graph-mailer.php`. diff --git a/clients/cryoweave/web/cw-graph-mailer.php b/clients/cryoweave/web/cw-graph-mailer.php new file mode 100644 index 00000000..44011775 --- /dev/null +++ b/clients/cryoweave/web/cw-graph-mailer.php @@ -0,0 +1,99 @@ + 20, + 'body' => array( + 'client_id' => CW_MAILER_CLIENT, + 'client_secret' => CW_MAILER_SECRET, + 'scope' => 'https://graph.microsoft.com/.default', + 'grant_type' => 'client_credentials', + ), + ) + ); + if ( is_wp_error( $resp ) ) { return false; } + $j = json_decode( wp_remote_retrieve_body( $resp ), true ); + if ( empty( $j['access_token'] ) ) { return false; } + set_transient( 'cw_graph_token', $j['access_token'], max( 60, intval( isset( $j['expires_in'] ) ? $j['expires_in'] : 3600 ) - 120 ) ); + return $j['access_token']; +} + +add_filter( 'pre_wp_mail', function ( $null, $atts ) { + $to = isset( $atts['to'] ) ? $atts['to'] : array(); + $subject = isset( $atts['subject'] ) ? $atts['subject'] : ''; + $message = isset( $atts['message'] ) ? $atts['message'] : ''; + $headers = isset( $atts['headers'] ) ? $atts['headers'] : array(); + + $cc = array(); $bcc = array(); $replyto = array(); $ctype = 'Text'; + if ( ! is_array( $headers ) ) { + $headers = explode( "\n", str_replace( "\r\n", "\n", $headers ) ); + } + foreach ( (array) $headers as $h ) { + if ( strpos( $h, ':' ) === false ) { continue; } + list( $k, $v ) = array_map( 'trim', explode( ':', $h, 2 ) ); + $kl = strtolower( $k ); + if ( 'cc' === $kl ) { $cc = array_merge( $cc, array_map( 'trim', explode( ',', $v ) ) ); } + elseif ( 'bcc' === $kl ) { $bcc = array_merge( $bcc, array_map( 'trim', explode( ',', $v ) ) ); } + elseif ( 'reply-to' === $kl ) { $replyto[] = $v; } + elseif ( 'content-type' === $kl && stripos( $v, 'html' ) !== false ) { $ctype = 'HTML'; } + } + if ( ! is_array( $to ) ) { $to = array_map( 'trim', explode( ',', $to ) ); } + + $addr = function ( $a ) { + if ( preg_match( '/<([^>]+)>/', $a, $m ) ) { return trim( $m[1] ); } + return trim( $a ); + }; + $rcpt = function ( $list ) use ( $addr ) { + $o = array(); + foreach ( (array) $list as $a ) { + $e = $addr( $a ); + if ( $e ) { $o[] = array( 'emailAddress' => array( 'address' => $e ) ); } + } + return $o; + }; + + $token = cw_graph_token(); + if ( ! $token ) { return $null; } // fall back to default wp_mail + + $payload = array( + 'message' => array( + 'subject' => $subject, + 'body' => array( 'contentType' => $ctype, 'content' => $message ), + 'from' => array( 'emailAddress' => array( 'address' => CW_MAILER_FROM, 'name' => CW_MAILER_FROMNAME ) ), + 'toRecipients' => $rcpt( $to ), + ), + 'saveToSentItems' => false, + ); + if ( $cc ) { $payload['message']['ccRecipients'] = $rcpt( $cc ); } + if ( $bcc ) { $payload['message']['bccRecipients'] = $rcpt( $bcc ); } + if ( $replyto ) { $payload['message']['replyTo'] = $rcpt( $replyto ); } + + $resp = wp_remote_post( + 'https://graph.microsoft.com/v1.0/users/' . rawurlencode( CW_MAILER_FROM ) . '/sendMail', + array( + 'timeout' => 25, + 'headers' => array( 'Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json' ), + 'body' => wp_json_encode( $payload ), + ) + ); + if ( is_wp_error( $resp ) ) { return $null; } // fall back + $code = wp_remote_retrieve_response_code( $resp ); + if ( 202 === intval( $code ) ) { return true; } // handled + return $null; // anything else: fall back to default wp_mail +}, 10, 2 ); diff --git a/errorlog.md b/errorlog.md index 4f51006f..23216699 100644 --- a/errorlog.md +++ b/errorlog.md @@ -21,6 +21,12 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure · 2026-06-23 | Howard-Home | bash/json-test-data | [friction] Git-Bash heredoc (even quoted <<'EOF') wrote C: as single backslash -> invalid JSON -> PS engine threw 'Unrecognized escape sequence' exit 3; fix: build JSON test files via PowerShell ConvertTo-Json, not bash heredocs [ctx: ref=feedback_tmp_path_windows] +2026-06-23 | GURU-5070 | vault/sops | [friction] sops -e -i run from repo dir not vault dir -> 'no creation rules', wrote SECRET in PLAINTEXT to vault file; fix: cd into vault root (or use --config) before sops -e so .sops.yaml path_regex matches [ctx: ref=vault skill] + +2026-06-23 | GURU-5070 | mailbox/graph | POST messageRules 403 ErrorAccessDenied - ComputerGuru Mailbox app (1873b1b0) has Mail.ReadWrite but not MailboxSettings.ReadWrite; inbox-rule creation needs MailboxSettings.ReadWrite [ctx: mbx=rua@azcomputerguru.com] + +2026-06-23 | GURU-5070 | discord-dm/screenconnect-ps | [friction] handed Mike a PowerShell one-liner with embedded double-quotes to paste into ScreenConnect's command runner; SC strips the quotes (same CommandLineToArgvW class as curl.exe/plink) so 'native' parsed as a cmdlet and the quoted exe path broke. Fix: for any PS command delivered through a quote-mangling layer (ScreenConnect cmd box, curl.exe, plink), use -EncodedCommand (UTF-16LE base64) — no quotes to strip. [ctx: ref=feedback_windows_quote_stripping] + 2026-06-23 | Howard-Home | gururmm/uninstall-engine | [friction] live-tested with -List-shaped targets (which include install_location) -> masked a StrictMode crash that only occurs with the server's UninstallTarget shape (no install_location); always re-test the destructive path with the ACTUAL caller/serialized shape 2026-06-22 | Howard-Home | gururmm/uninstall-engine | [correction] assumed AnyDesk needs remote removal; it has UninstallString '...AnyDesk.exe --uninstall' and supports --silent, so it is silently removable -- added vendor rule diff --git a/wiki/clients/cryoweave.md b/wiki/clients/cryoweave.md index 42addc64..a6de66e1 100644 --- a/wiki/clients/cryoweave.md +++ b/wiki/clients/cryoweave.md @@ -71,7 +71,12 @@ CryoWeave manufactures custom cryogenic cable assemblies (millikelvin to 300K) f - **DMARC** `_dmarc` → `v=DMARC1; p=quarantine; sp=quarantine; fo=1; rua=mailto:rua@azcomputerguru.com` (hardened from p=none to **p=quarantine** 2026-06-15; **promote to p=reject** after ~1 week of clean aggregate reports confirm all legit senders — incl. the IX website/contact form — align). Cross-domain report authorization published on the azcomputerguru.com Cloudflare zone: `cryoweave.com._report._dmarc.azcomputerguru.com TXT "v=DMARC1;"` (2026-06-15). `rua@azcomputerguru.com` **shared mailbox created** in ACG's tenant (DisplayName "DMARC Reports", GUID 46b898f8-cfac-4b81-8980-e681b13fb833, mike@ FullAccess+automap) — full reporting chain live; aggregate reports arrive within ~24h. (NB: a single `*._report._dmarc` wildcard does NOT cover a 2-label reported domain; add one per-client record on the azcomputerguru.com Cloudflare zone.) - **DKIM** (M365 selector1/2): CNAMEs published + **signing ENABLED 2026-06-15** (`Get-DkimSigningConfig`: Enabled=True, Status=Valid, 2048-bit). Targets `selector1-cryoweave-com._domainkey.cryoweave.w-v1.dkim.mail.microsoft` (+ selector2). - Stale `mail.cryoweave.com` CNAME → old Neptune (67.206.163.124) **removed**. -- **Outbound-email issue (open):** Greg reports mail not reaching recipients. SPF passes/aligns, so auth isn't hard-failing; pending **message trace** (EXO app-only access still propagating after onboarding) + Greg's NDR to pinpoint restriction/reject/junk. DKIM+DMARC gaps were the most likely junking cause. +- **Outbound-email issue (open):** Greg reports mail not reaching recipients. SPF passes/aligns, so auth isn't hard-failing; pending **message trace** + Greg's NDR to pinpoint restriction/reject/junk. + - **DMARC-report analysis (2026-06-23):** cryoweave M365 outbound is **clean** — NO outbound connector/transport rule, mail leaves from Microsoft IPs, and the Google aggregate report shows **dmarc=pass**. The DMARC "failures" in `rua@azcomputerguru.com` were **receiver-side artifacts**: cryoweave mail sent to INKY-protected receivers (azcomputerguru.com and other ACG/INKY tenants) gets re-injected by INKY (`ipw-outbound.inkyphishfence.com`), which breaks SPF+DKIM at *that receiver*. cryoweave itself is NOT on INKY. So Greg's deliverability issue (if real) is likely NOT a cryoweave-side auth problem for normal recipients — investigate the specific recipients/NDRs. See [[reference_inky_outbound_breaks_dmarc]]. +- **Website contact-form mail — FIXED (2026-06-23):** The WordPress site (Ninja Forms) previously sent via cPanel PHP-mail (`envelope=ix.azcomputerguru.com`, cPanel `default` DKIM), which failed DMARC. Now routed through **Microsoft Graph (app-only)** so it sends as a real M365 mailbox, DMARC-aligned: + - **`noreply@cryoweave.com`** — shared mailbox (no license), GUID `19495079-8295-4df2-8e51-4686fc764648`. + - **App "CryoWeave Web Mailer"** (`client_id 4003c79e-d4ac-4265-ba36-da783d89ee4d`), Graph `Mail.Send` (application), **scoped by `New-ApplicationAccessPolicy`** (group `webmailer-scope@cryoweave.com`) to send ONLY as noreply@. Secret in vault `clients/cryoweave/web-mailer-app.sops.yaml` (expires 2028-06-23). + - **mu-plugin** `wp-content/mu-plugins/cw-graph-mailer.php` overrides `wp_mail()` via `pre_wp_mail` → POSTs Graph `sendMail` as noreply@, preserving To/Cc/Reply-To/Subject/body; **falls back to default wp_mail on any Graph failure**. Verified: `wp_mail()` returns `true` (Graph 202). ### Network