diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 6c0583d..dcdccd9 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -30,7 +30,9 @@ - [Identity precedence](feedback_identity_precedence.md) — Trust `.claude/identity.json` over the system-reminder `userEmail` hint when they disagree (shared-login machines). - [1Password — always use service token](feedback_1password_service_token.md) — Source OP_SERVICE_ACCOUNT_TOKEN from SOPS for every `op` call. Desktop-app integration prompts are unacceptable in agent flows. - [/tmp path mismatch on Windows](feedback_tmp_path_windows.md) — Write tool and Git Bash resolve `/tmp` to DIFFERENT real dirs. Use heredoc or workspace path for JSON payloads handed to curl. Caused wrong-comment incident on Syncro #32225. -- [Syncro — never set contact on Cascades tickets](feedback_syncro_cascades_contact.md) — Cascades tickets must have `contact_id` blank; Syncro routes to the correct email distribution that way. Setting contact (often defaults to Meredith) overrides and breaks notifications. Never include the contact field in create or edit payloads for Cascades. +- [Syncro — leave contact blank by default](feedback_syncro_blank_contact.md) — Default to blank contact ("Not Assigned") on tickets and billing for ALL customers. Blank lets Syncro use company-level email defaults; setting a contact may route to a secondary email and bypass distribution. Generalizes the prior Cascades-only rule per Winter 2026-05-04. +- [Syncro — never set contact on Cascades tickets](feedback_syncro_cascades_contact.md) — Cascades-specific instance of the blank-contact rule above. Kept for the Meredith-defaulting incident detail. +- [Syncro — use a billable labor type, never "Prepaid project labor"](feedback_syncro_labor_type.md) — Time entries must use in-shop / onsite / remote / web labor. "Prepaid project labor" is exempt and won't decrement prepay blocks. Default is Remote labor for typical support tickets. Winter caught this 2026-05-04. - [Syncro — log time entries first, never bare add_line_item](feedback_syncro_timer_first.md) — All Syncro work-time billing MUST go through `timer_entry → charge_timer_entry`. Bare `add_line_item` leaves Syncro time tracking at 00:00:00 and breaks reporting. Mike caught this on 2026-04-30 across 31 tickets; I repeated the bug on 2026-05-01 across 3 more. ## Machine diff --git a/.claude/memory/feedback_syncro_blank_contact.md b/.claude/memory/feedback_syncro_blank_contact.md new file mode 100644 index 0000000..d3670d7 --- /dev/null +++ b/.claude/memory/feedback_syncro_blank_contact.md @@ -0,0 +1,19 @@ +--- +name: Syncro — leave contact blank by default on tickets and billing +description: When creating Syncro tickets or billing them out, leave the contact field blank ("Not Assigned") in most cases. Blank contact lets Syncro use the company-level defaults for notifications and email routing. Setting a specific contact can route to a secondary email and bypass the customer's intended distribution. +type: feedback +--- + +**Rule:** When creating or billing Syncro tickets, leave `contact_id` / `contact_name` / `contact_email` blank ("Not Assigned") by default for any customer. Only set a contact when there's an explicit, deliberate reason to (e.g., user explicitly says "set the contact to X"). + +**Why:** Winter clarified on 2026-05-04: blank contact lets Syncro apply the **company-level email defaults** for the account — those defaults route notifications to the right people. Setting a specific contact overrides that and may push notifications to a secondary email address belonging to that contact, bypassing the customer's intended distribution. This was originally flagged for Cascades of Tucson (where Meredith was being incorrectly auto-selected), but Winter generalized it: the rule applies to most customers. + +**How to apply:** + +- **Creating a ticket** (POST `/tickets`): Omit `contact_id` from the body entirely. Do not pull contacts via `GET /customers/{id}` and pick one — let Syncro use the company defaults. +- **Editing a ticket** (PUT `/tickets/{id}`): Send only the fields you're changing (`status`, `priority`, etc.). Never include `contact_id`, `contact_name`, or `contact_email` in the body, even matching the existing value. PUT can re-apply the record; safest is to never reference contact in any write payload. +- **Billing / invoices**: Same rule on the invoice creation side. If `contact_id` shows up in any payload, drop it. +- **When to set a contact anyway:** Only if the user explicitly directs you to ("set Mike as the contact on this one") OR there's a documented per-customer instruction that overrides the default. Default is always blank. +- **Verify after any write:** `GET /tickets/{id}` and confirm `.ticket.contact_id` is `null`. If you find it set, blank it explicitly: `PUT /tickets/{id}` with `{"contact_id": null}`. + +**Generalizes from:** the prior Cascades-specific guidance (originally `feedback_syncro_cascades_contact.md`). Winter's 2026-05-04 message broadened the scope from "Cascades only" to "most customers." diff --git a/.claude/memory/feedback_syncro_labor_type.md b/.claude/memory/feedback_syncro_labor_type.md new file mode 100644 index 0000000..6cecc71 --- /dev/null +++ b/.claude/memory/feedback_syncro_labor_type.md @@ -0,0 +1,24 @@ +--- +name: Syncro — use a billable labor type (in-shop / onsite / remote / web), never "Prepaid project labor" +description: When creating Syncro time entries, the labor type / product on the entry MUST be one of in-shop, onsite, remote, or web labor. "Prepaid project labor" is an exempt labor type and will NOT draw down a customer's prepay block — using it silently breaks block-hour accounting. +type: feedback +--- + +**Rule:** Time entries on Syncro tickets must use a billable labor product matching the work delivery channel: **in-shop**, **onsite**, **remote**, or **web labor**. Do NOT use **"Prepaid project labor"** as the labor type for normal work. + +**Why:** Winter caught me on 2026-05-04 using "Prepaid project labor" by default. That product is **exempt** — it does not consume hours from a customer's prepaid block. So even if the ticket is for a prepay customer and looks billed correctly on the invoice, the block balance never decrements. Block-hour accounting silently drifts. Only the four non-exempt labor types (in-shop / onsite / remote / web) burn block time as intended. + +**How to apply:** + +- **Picking labor type:** Match it to how the work was actually delivered: + - **Remote labor** — work done over remote tools (RDP, Splashtop, ScreenConnect, phone-only support, scripts). This will be the most common pick. + - **Onsite labor** — work done at the client's physical location. + - **In-shop labor** — hardware brought to ACG's office for repair/build. + - **Web labor** — purely cloud/portal work (Microsoft 365 admin center, Entra, Cloudflare, etc.) where there's no remote-into-a-machine component. (Confirm with Winter if this distinction matters in your situation — sometimes "remote" is the right pick even for cloud work.) +- **Resolving the product_id:** Use `GET /products?search=remote+labor` (etc.) to pull the right product_id for the labor type, then pass that as `product_id` on the `timer_entry` POST. +- **Never default to "Prepaid project labor"** unless explicitly directed. If you find an existing entry with that product on a normal billable ticket, flag it — Winter (or whoever) will need to retroactively switch the labor type so the block decrement actually posts. +- **Verifying:** After billing, check that the customer's prepay block balance dropped by the expected number of hours. If it didn't, the labor type was wrong. + +**Real-world incident — 2026-05-04:** Tickets I created on this date used "Prepaid project labor" as the auto-selected labor type. Winter is fixing them retroactively. Going forward, default to `Remote labor` for the typical remote-support ticket, then adjust per delivery channel. + +**Where this lands in skill code:** `.claude/commands/syncro.md` and the `syncro` skill workflow examples need to make labor-type selection an explicit step in the timer_entry workflow, not a silent default. diff --git a/.gitignore b/.gitignore index 8fd97db..f6f475a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ tmp-remediation/ *.tmp *.log *.bak +.claude/tmp/ # Live secrets / tokens — never commit .token diff --git a/clients/dataforth/PROJECT_STATE.md b/clients/dataforth/PROJECT_STATE.md index 2301bdc..5dd5659 100644 --- a/clients/dataforth/PROJECT_STATE.md +++ b/clients/dataforth/PROJECT_STATE.md @@ -47,6 +47,7 @@ Session manager was being developed for SAGE-SQL deployment — see `clients/dat | Date | By | Change | Status | |------|-----|--------|--------| +| 2026-05-04 | Howard | Lobby phone offline — D1-Server-Room port 1 reconfigured to VLAN 100. Syncro #32246. Phone registered after TFTP + SIP handshake. See `session-logs/2026-05-04-lobby-phone-vlan-fix.md`. | COMPLETE | | 2026-04-14 | Mike | Session log: follow-up work on datasheet pipeline | COMPLETE | | 2026-04-13 | Mike | Session log: pipeline verification | COMPLETE | | 2026-04-12 | Mike | SCMVAS/SCMHVAS pipeline deployed to production | DEPLOYED | diff --git a/clients/dataforth/reports/2026-05-03-account-status-check.md b/clients/dataforth/reports/2026-05-03-account-status-check.md new file mode 100644 index 0000000..c84128c --- /dev/null +++ b/clients/dataforth/reports/2026-05-03-account-status-check.md @@ -0,0 +1,69 @@ +# Dataforth — Account Status Check + +**Date:** 2026-05-03 (UTC) +**Tenant:** Dataforth Corporation (`dataforth.com`, `7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584`) +**Tool:** ComputerGuru Security Investigator (App ID `bfbc12a4-f0dd-4e12-b06d-997e7271e10c`) — Graph read-only +**Scope:** Status lookup only (enabled / licensed / last sign-in / object type). No remediation. +**Operator:** Howard Enos + +## Summary + +- 1 of 4 addresses is an **active licensed user** (`jantar@`). +- 1 of 4 is an **active distribution list** (`sales@`) routing to 3 employees. +- 2 of 4 (`dchapman@`, `dhenderson@`) **do not exist** anywhere in the directory — no user, no group, no contact, no alias, not in the 30-day soft-delete recycle bin. + +## Results + +| Address | Object type | Enabled | Licensed | Last sign-in (interactive) | Notes | +|---|---|---|---|---|---| +| `sales@dataforth.com` | Mail-enabled distribution list ("Sales") | Active | n/a | n/a | Members: ltobey@, ghaubner@, tdean@ | +| `jantar@dataforth.com` | User — Jacque Antar | Yes | 1 license | 2026-04-20 17:44 UTC (~13d ago) | Primary SMTP confirmed | +| `dchapman@dataforth.com` | **Does not exist** | — | — | — | Not a user, group, contact, or alias; not in soft-delete | +| `dhenderson@dataforth.com` | **Does not exist** | — | — | — | Not a user, group, contact, or alias; not in soft-delete | + +## What "does not exist" means here + +For `dchapman@` and `dhenderson@` we checked, against the live tenant: + +1. `/users` filtered on `mail` and `proxyAddresses/any(p: p eq 'smtp:')` — 0 results. Rules out the address being a primary SMTP, a UPN, or a secondary alias on any mailbox. +2. `/groups` same filter — 0 results. Rules out distribution lists and M365 groups. +3. `/directory/deletedItems/microsoft.graph.user` — 0 results. The user object was not soft-deleted in the past 30 days (so not recoverable via the standard restore path). +4. `/contacts` (beta, org-level mail contacts) — 0 results. + +If mail to these addresses is bouncing now, that is consistent — the tenant has no recipient with that smtp address. If mail to these addresses was being delivered historically and we need to know when they were removed, that requires a unified audit log search (`auditLogs/directoryAudits` with `activityDisplayName eq 'Delete user'`) over a longer window — say so and I can run it. + +## Per-address detail + +### sales@dataforth.com — Distribution List +- Group ID: `6dd5ec2b-c220-49bf-bd00-8d4d123914e7` +- mail-enabled, security-disabled, classic DL (no `groupTypes`) +- Proxy addresses: `SMTP:sales@dataforth.com`, `smtp:sales1@dataforthcom.onmicrosoft.com` +- Members (3): Logan Tobey (`ltobey@`), Georg Haubner (`ghaubner@`), Theresa Dean (`tdean@`) + +### jantar@dataforth.com — Jacque Antar +- `accountEnabled`: true +- 1 assigned license +- Last interactive sign-in: 2026-04-20 17:44:45 UTC +- Proxy addresses: `SMTP:jantar@dataforth.com`, `smtp:jantar@dataforthcom.onmicrosoft.com` + +### dchapman@dataforth.com — not found +- All four directory queries returned 0. + +### dhenderson@dataforth.com — not found +- All four directory queries returned 0. + +## Data artifacts + +Raw JSON in `/tmp/remediation-tool/7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584/account-active/`: +- `sales@dataforth.com-users.json`, `sales@dataforth.com-groups.json` +- `jantar@dataforth.com-users.json`, `jantar@dataforth.com-groups.json` +- `dchapman@dataforth.com-users.json`, `dchapman@dataforth.com-groups.json`, `dchapman@dataforth.com-deleted.json`, `dchapman@dataforth.com-orgcontact.json` +- `dhenderson@dataforth.com-users.json`, `dhenderson@dataforth.com-groups.json`, `dhenderson@dataforth.com-deleted.json`, `dhenderson@dataforth.com-orgcontact.json` + +## Operational note + +PyJWT + cryptography are not installed on HOWARD-HOME, so `get-token.sh` cert-auth (the new default) failed silently and the secret-auth fallback was used (`REMEDIATION_AUTH=secret`). Token issuance still works either way; cert-auth is preferred per the recent migration. Fix on this machine: + +```bash +py -m pip install PyJWT cryptography +``` diff --git a/clients/dataforth/reports/2026-05-03-jantar-account-check.md b/clients/dataforth/reports/2026-05-03-jantar-account-check.md new file mode 100644 index 0000000..71c92b1 --- /dev/null +++ b/clients/dataforth/reports/2026-05-03-jantar-account-check.md @@ -0,0 +1,139 @@ +# Dataforth — Account & Mailbox Check: jantar@dataforth.com + +**Date:** 2026-05-03 (UTC) +**Tenant:** Dataforth Corporation (`dataforth.com`, `7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584`) +**Subject:** Jacque Antar (UPN `jantar@dataforth.com`, object id `daa60027-be31-47a5-87af-d728499a9cc4`) +**Trigger:** Email surfaced on a paid dark-web ID monitoring report. +**Tool:** ComputerGuru Security Investigator (Graph read-only) — App ID `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` +**Operator:** Howard Enos +**Scope:** Read-only. No remediation taken. + +## Summary + +- **MFA is ENABLED and IS being enforced.** Per-user MFA state = `enforced`. Last 30 days of sign-ins all show `MFA requirement satisfied by claim in the token`. Non-interactive sign-ins (Outlook, Teams, etc.) all report `authenticationRequirement: multiFactorAuthentication`. +- **MFA method registered: SMS only** to `+1 520-245-6929`. No Authenticator app, no FIDO key. SMS is the weakest second factor (SIM-swap, SS7). +- **Mailbox is clean of obvious breach indicators.** No suspicious inbox rules, no auto-forwarding visible in Graph, no foreign sign-ins, no mass-mail patterns in sent items, no flagged risk detections. Sent items match her accounting role. +- **Posture gaps to fix (separate from breach response):** + 1. All 3 Conditional Access policies on this tenant are in **report-only** mode (`enabledForReportingButNotEnforced`) — including "Require MFA", "Block Legacy Authentication", and "Block Foreign Sign-Ins". The only thing enforcing MFA today is the deprecated per-user MFA toggle. Microsoft has been pushing tenants off per-user MFA for years. + 2. She has **OAuth grants for legacy email scopes** (IMAP, EWS, EAS) to "Apple Internet Accounts" and "eM Client". These are legitimate clients she uses, but they're protocol-level paths that the disabled "Block Legacy Auth" CA policy would close. + 3. **All 30d sign-ins originate from `67.206.163.122` (Salt Lake City, UT, CenturyLink residential).** Dataforth is Tucson. Either she's remote-working from SLC, uses a VPN exiting there, or this is persistent unauthorized access. **Confirm with her / Mike.** Same IP for 30 days = same workstation, not impersonation churn — but that workstation might or might not be hers. + +## Target details + +| Field | Value | +|---|---| +| UPN | jantar@dataforth.com | +| Object ID | daa60027-be31-47a5-87af-d728499a9cc4 | +| Display name | Jacque Antar | +| Account enabled | true | +| Created | 2023-12-07 | +| Last password change | **2026-03-09** (~55 days ago) | +| Assigned licenses | 1 | + +## MFA — enabled and enforced? + +**Enabled: YES.** Per-user MFA legacy endpoint returned `perUserMfaState: enforced`. Registration report: `isMfaCapable: true, isMfaRegistered: true`. + +**Enforced at sign-in: YES.** Evidence: + +- All 8 interactive sign-ins (last 30d) ended successfully with `additionalDetails: "MFA requirement satisfied by claim in the token"`. That string only appears when Entra evaluated MFA and it was satisfied (either by fresh challenge or by an MFA-claim in the cached refresh token). +- Non-interactive sign-ins (10 sampled from 2026-05-02 alone — Outlook, Edge, OfficeHome, WeveAgave, etc.) all show `authenticationRequirement: "multiFactorAuthentication"`. + +**Methods registered:** `mobilePhone` only (SMS to `+1 520-245-6929`). `defaultMfaMethod: null`, `userPreferredMethodForSecondaryAuthentication: sms`. + +**Caveat — what's enforcing the MFA:** +- It is the legacy **per-user MFA "enforced"** flag, not Conditional Access. All 3 CA policies on this tenant are in `enabledForReportingButNotEnforced`: + - `ACG - Require MFA for All Users` — report-only + - `ACG - Block Legacy Authentication` — report-only + - `ACG - Block Foreign Sign-Ins` — report-only +- Security Defaults: disabled. +- This works today, but Microsoft is sunsetting per-user MFA. The CA policies should be flipped to "On". + +**Recommendation for Jacque specifically:** +1. Have her register Microsoft Authenticator (push/TOTP) as her primary, demote SMS to fallback. Self-service: https://aka.ms/mfasetup +2. Treat SMS-only as a known posture gap until Authenticator is added. + +## Per-check findings + +### 1. Inbox rules (Graph v1.0) +- 1 rule, **disabled**. Moves messages whose header contains `X-Inky-Graymail: True` to a folder, then stops processing. This is a normal Inky-anti-phishing graymail filter. **Not suspicious.** + +### 2. Mailbox settings (Graph) +- Auto-reply: disabled. Time zone US Mountain. Locale en-US. **Nothing flagged.** + +### 3. Exchange REST (hidden rules / mailbox permissions / SendAs / Get-Mailbox) +- **NOT CHECKED.** Exchange admin endpoint returned **HTTP 401** for the Security Investigator SP on this tenant. The "Exchange Administrator" directory role is not assigned to that SP in Dataforth. This is a known gap from the per-tenant onboarding step. +- To enable: a tenant Global Admin assigns the Exchange Administrator role to the `ComputerGuru Security Investigator` service principal in this tenant's Entra Roles blade (or run `bash .claude/skills/remediation-tool/scripts/onboard-tenant.sh dataforth.com` if cert auth works on this machine). Without it we can't see hidden inbox rules, delegates, SendAs, or the canonical `ForwardingAddress / ForwardingSmtpAddress / DeliverToMailboxAndForward` mailbox flags. +- The Graph-side mailbox settings show no forwarding flag (`automaticRepliesSetting.status: disabled`) but Graph cannot see the Exchange-only forwarding fields. + +### 4. OAuth consents + app role assignments +- **2 user-consented OAuth grants** (both consented by her, scope = legacy email): + | Resource | Client ID | Scopes | + |---|---|---| + | Office 365 Exchange Online | `85e650f8-5eec-4523-a9ef-fc1a031fb1d6` | `openid offline_access EAS.AccessAsUser.All` (Apple Internet Accounts — EAS) | + | Office 365 Exchange Online | `25db1c08-f5a0-4f6c-bbdd-a738689b1587` | `IMAP.AccessAsUser.All EWS.AccessAsUser.All offline_access email openid` (eM Client) | +- **2 app role assignments** under her account: + - "Apple Internet Accounts" (assigned 2024-04-02) + - "eM Client" (assigned 2024-08-26) +- Both consistent with a Mac user running Apple Mail + a Windows/Mac user running eM Client. **Legitimate clients**, but they consume legacy auth scopes (IMAP / EWS / EAS) that bypass modern auth challenges. The disabled "Block Legacy Auth" CA policy would normally block these. + +### 5. Authentication methods +- 2 methods on record: + - `passwordAuthenticationMethod` (last set 2026-03-09) + - `phoneAuthenticationMethod` mobile, `+1 520-245-6929` +- No `microsoftAuthenticatorAuthenticationMethod`, no FIDO2, no Windows Hello, no software OATH token. + +### 6. Sign-ins (last 30 days, interactive) +- 8 successful sign-ins. **All 8 from `67.206.163.122` (Salt Lake City, UT, CenturyLink-issued residential).** No failures, no foreign-geo, no legacy-auth client app types in this set. +- App: mostly "Dime Client" (`a2760c41-63c9-42b5-8d58-bfa1fd9e2eb3` — Microsoft first-party app, used by some web client surfaces) + one "One Outlook Web". +- Risk level: `hidden` (Identity Protection not licensed). +- **Action:** confirm with Jacque or Mike that the SLC IP is hers (remote work, VPN, etc.). If not, treat as compromise. + +### 7. Directory audits (last 30 days, target = jantar) +- 5 events, all benign: + - 3 × "Update user" by Microsoft Substrate Management (Microsoft system process, automatic profile maintenance) + - 2 × "Add member to group" on 2026-04-06 by `dcenter@dataforth.com` (admin activity) +- **No password resets, no auth-method changes, no role grants, no app consents by anyone other than her.** + +### 8. Risky users / risk detections +- **HTTP 403 Forbidden** — `"Your tenant is not licensed for this feature."` Identity Protection requires Entra ID P2; Dataforth's SKUs (O365 Business Premium, Business Standard, Exchange Standard) include P1 only. **Not checkable on this tenant.** + +### 9. Sent items (last 25) +- Normal accounting/AP work: Patricia at `times-biz.com` (external bookkeeper), AMoreno + sabreu at `crestins.com` (insurance broker), Paychex contacts (`nknippel@`, `cknoll@`), internal Dataforth (`Kellynwackerly@`, `tdean@`, `dcenter@`, `ghaubner@`, `ofest@`, `ltobey@`, `shipping@`), various vendor reply-thread subjects ("Sales Invoice", "Statement", "JE to correct AP issue", "Commissions", "ACH", "Bank", "PER1 and PIN1"). +- **No blast patterns, no unusual external recipients, no obvious phishing or BEC payloads.** Subject lines and recipient mix consistent with her finance role. + +### 10. Deleted items (last 25 visible) +- Only 3 items: 1 promotional email (`info-az-specialists.com@shared1.ccsend.com`), 2 self-sent items (probably saved-then-discarded drafts). Low count likely indicates Deleted Items is being emptied regularly or auto-purged by retention. **Not flagged**, but anomalous low count means a mailbox-level audit log search would be needed if you want to see what was deleted earlier. + +## Suspicious items pulled from above + +- **All 30d sign-ins from a single Salt Lake City residential IP** (Dataforth is Tucson). Not a breach indicator on its own — the IP is consistent for 30 days, suggesting one persistent client. **Confirm with Jacque or Mike whether she works from SLC / uses a VPN there.** +- **Two OAuth grants to legacy-auth third-party email clients** (eM Client, Apple Mail). These are legitimate apps but they keep IMAP/EWS/EAS sessions alive that the dormant "Block Legacy Auth" CA policy would otherwise close. Ask whether she still uses both clients. + +## Gaps — checks not completed + +| Gap | Reason | Fix | +|---|---|---| +| Hidden inbox rules, delegates, SendAs, mailbox forwarding fields | Exchange Admin role not assigned to Security Investigator SP in this tenant (HTTP 401) | Tenant Global Admin: assign "Exchange Administrator" to SP `bfbc12a4-...` in Entra Roles. Or run `onboard-tenant.sh dataforth.com` after fixing PyJWT on operator workstation. | +| Identity Protection (riskyUsers, riskDetections) | Tenant not licensed for AAD/Entra ID P2 | Out of scope — would require license upgrade for ~$9/user/mo. | + +## Next actions + +1. **Confirm SLC sign-in IP with Mike or Jacque** — is `67.206.163.122` her? (single highest-value question) +2. **Have Jacque add Microsoft Authenticator** as MFA method, demote SMS to backup. Self-service: https://aka.ms/mfasetup. Could be done in 2 minutes during her next phone call with us. +3. **Force a password reset** as a precaution given the dark-web hit (separate `/remediation-tool remediate jantar@dataforth.com password-reset` would do it after explicit YES — currently NOT executed). +4. **Tenant-level posture (separate engagement, discuss with Mike before doing):** + - Flip the 3 ACG CA policies from report-only to On. + - Assign Exchange Administrator to the Security Investigator SP so we can see hidden rules / forwarding on future investigations. + - Decide whether eM Client / Apple Mail (legacy-auth scopes) are still needed — if yes, those users will need an exemption when "Block Legacy Auth" is enforced. + +## Data artifacts + +Raw JSON in `/tmp/remediation-tool/7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584/user-breach/jantar_dataforth_com/`: +- `00_user.json`, `01_inbox_rules_graph.json`, `02_mailbox_settings.json` +- `04a_oauth_grants.json`, `04b_app_role_assignments.json` +- `05_auth_methods.json`, `06_signins.json`, `07_dir_audits.json` +- `08a_risky_user.json` (403 — not licensed), `08b_risk_detections.json` (403) +- `09_sent.json`, `10_deleted.json` +- `mfa_perUserState.json`, `mfa_regDetails.json`, `ca_policies.json`, `secdef.json` +- `03a_InboxRule_hidden.json` / `03d_Mailbox.json` are EMPTY (Exchange 401) diff --git a/clients/dataforth/session-logs/2026-05-04-lobby-phone-vlan-fix.md b/clients/dataforth/session-logs/2026-05-04-lobby-phone-vlan-fix.md new file mode 100644 index 0000000..8e46623 --- /dev/null +++ b/clients/dataforth/session-logs/2026-05-04-lobby-phone-vlan-fix.md @@ -0,0 +1,68 @@ +# Dataforth — Lobby Phone Offline (VLAN/Switch Port Fix) + +**Date (UTC):** 2026-05-04 +**Tech:** Howard Enos +**Time onsite:** 0.5 hours +**Syncro ticket:** #32246 (`109836123`), invoice #67558 (`1650188916`) + +## User +- **User:** Howard Enos (howard) +- **Machine:** Howard-Home (driving the PBX remotely via Tailscale) +- **Role:** tech + +## Summary + +Lobby visitor phone (Cisco SPA502G, ext 201) had been offline — no dial tone, dialing extensions did nothing, displayed an incorrect date/time. Root cause: the lobby drop's switch port had been on the wrong VLAN, isolating the phone from the PBX. Fix was reconfiguring D1-Server-Room port 1 to VLAN 100. Phone immediately TFTP-pulled fresh provisioning and registered. + +## Diagnosis path + +1. **Phone state:** screen showed normal idle, but no dial tone. Dialing an extension just returned to home screen with no tone, ringback, or error. Wrong date/time on display — strong clue that the phone hadn't reached NTP for a while. +2. **PBX-side check** (driven from Howard-Home over Tailscale via SSH to `192.168.100.2` with vault creds): + - `pjsip show endpoint 201` → `Unavailable`, no contact, AOR but no registration. + - **Zero traffic from the phone's last known IP `192.168.100.235`** in the last 2 hours of TFTP/SIP logs. + - PBX could not ping `.235`; ARP "who-has" requests went unanswered. + - SIP secret in `pjsip.auth.conf` for ext 201 matched the secret in the per-MAC TFTP config `spa58bfea1158b4.xml` — so credentials were not the issue. +3. **VLAN test:** Howard plugged his laptop into the same lobby wall jack. Laptop received `192.168.0.53` (Unifi UDM main LAN). Meanwhile, the phone — after a factory reset to clear cached state — landed on `192.168.1.235` via LLDP-MED voice tagging onto Unifi's default voice VLAN (`192.168.1.0/24`). Neither matches the production voice/PBX VLAN, which is `192.168.100.0/24`. +4. **Cable trace:** Howard followed the lobby drop back to the **D1-Server-Room switch, port 1**. That port was not configured for VLAN 100. + +## Network topology learned + +| Subnet | Used for | +|---|---| +| `192.168.0.0/24` | Unifi main LAN (UDM is at `192.168.0.254`) | +| `192.168.1.0/24` | Unifi default voice VLAN (LLDP-MED) — NOT used for production phones in this office | +| `192.168.6.0/24` | OpenVPN management range (per UDM config) | +| `192.168.100.0/24` | **Production voice/PBX VLAN** — PBX on `.196` (and `.2` aliased), all production phones | +| `10.208.107.116/30` | PBX `ens224` secondary interface | + +Working office phones live on `192.168.100.x` directly. The Unifi-default voice VLAN (`192.168.1.x`) is not wired to anything that can reach the PBX. + +## Fix + +Reconfigured **D1-Server-Room port 1** to VLAN 100. After replug: + +- Phone DHCP'd `192.168.100.235`. +- TFTP fetched `/spa502G.cfg` (12:29:40 PDT) and per-MAC `/spa58bfea1158b4.xml` (12:30:40 PDT). +- SIP REGISTER → 401 Unauthorized → REGISTER (auth) → 200 OK at 12:31:42 PDT. +- `pjsip show endpoint 201` → `In use`, contact `201/sip:201@192.168.100.235:5060` Avail, RTT 22ms. +- NTP sync brought date/time current. + +## Recommendation for Mike / Dataforth IT + +- **Audit other Unifi-managed switch ports** for voice drops to ensure they all stay tagged on VLAN 100. A port that reverts to defaults will silently isolate any phone plugged into it (untagged main LAN for laptops, LLDP-MED voice tag onto `192.168.1.x` for phones — neither reaches the PBX). The wrong date/time is the canary; check that on phones that have been complained about. +- **D1-Server-Room port 1** should stay tagged on VLAN 100. If config drifts, the lobby phone goes silent again. + +## Tools / accounts touched + +- SSH to PBX (`sangoma@192.168.100.2`) via Tailscale + paramiko (vault creds). +- No production config changes on the PBX itself (read-only diagnostics there). +- Switch port config change: D1-Server-Room port 1 → VLAN 100 (changed from whatever it was before — not captured; assumed default Unifi profile). + +## Tools `not` touched + +- UDM controller (`192.168.0.254`) — has 2FA push enabled and was not accessed during this work. The switch port change was made by Howard via direct switch access. + +## Artifacts + +- TFTP config file confirmed correct: `/tftpboot/spa58bfea1158b4.xml` on PBX (mtime 2026-04-23 — was already current; no FreePBX-side change needed). +- pjsip auth password matches XML password (md5 hash form `4b57418f0a921fbce9d1bee10b6084e5`). diff --git a/clients/grabb-durando/reports/2026-05-04-leap-calendar-permission-investigation.md b/clients/grabb-durando/reports/2026-05-04-leap-calendar-permission-investigation.md new file mode 100644 index 0000000..e8bfbc9 --- /dev/null +++ b/clients/grabb-durando/reports/2026-05-04-leap-calendar-permission-investigation.md @@ -0,0 +1,115 @@ +# LEAP Calendar Sync Permission Investigation — slarionova@grabblaw.com + +**Date (UTC):** 2026-05-04 +**Tenant:** grabblaw.com (`032b383e-96e4-491b-880d-3fd3295672c3`) — Grabb & Durando, P.C. +**User:** Svetlana Larionova (`slarionova@grabblaw.com`, `affab40c-5535-4c1a-9a78-a2eda1a4a3b7`) +**Issue:** "Not able to add calendar event from Leap to M365 — no admin permissions" +**Investigator:** Howard Enos (read-only Graph via Security Investigator app) + +--- + +## TL;DR + +**She does NOT need an M365 admin role.** The error is a misnamed OAuth consent block — Leap's sign-in app asks for scopes (Mail.ReadWrite, Mail.Send, Files.ReadWrite.All) that the tenant's user-consent policy classifies as "high-risk" and reserves for admin approval. Five of her co-workers (4 with no admin roles at all, 1 with User Administrator) already have these grants because an admin previously approved them per-user. She just needs the same per-user approval — not an elevated role. + +**Recommended fix:** Have Svetlana run through Leap's M365 connect again and click "Request approval from your administrator." Then approve the request in Entra → Enterprise Applications → Admin consent requests, scoped to her user only. + +--- + +## Findings + +### 1. User account is healthy +- Account enabled: `true` +- License: `O365_BUSINESS_PREMIUM` (Exchange, SharePoint, Teams all enabled) +- Mailbox settings reachable (English-US, MST timezone, autoreply off) +- Group memberships: only `Grabb & Durando, P.C.` distribution group +- **Directory roles: NONE** (correctly, no admin role) +- Existing OAuth grants: **0** + +### 2. Two LEAP service principals are registered in this tenant + +| App ID | Display | Role | Consent state | +|---|---|---|---| +| `5602fc50-4c30-4faa-a595-e5a0f15d2cce` | LEAP (service) | App-only/daemon — runs as the app, not a user | Tenant-wide app-permission consent already granted (Calendars/Mail/Files via Application roles on Graph + EXO + SharePoint) | +| `a7d19842-33e2-457b-a399-d4e6ec010f0a` | LEAP (delegated) | User sign-in — runs as the signed-in user | Per-user (`consentType=Principal`) grants for 5 users only | + +### 3. The 5 users who already have working Leap calendar sync + +| User | Admin role? | +|---|---| +| jsosa@grabblaw.com (Jeannette Sosa) | None | +| rpesqueira@grabblaw.com (Reyna Pesqueira) | None | +| avazquez@grabblaw.com (Ana Vazquez) | None | +| yheredia@grabblaw.com (Yvette Heredia) | None | +| jwilliams@grabblaw.com (Jeff Williams) | User Administrator (incidental) | + +Each holds a `Principal`-type OAuth grant on the LEAP delegated app with the full scope set: + +``` +Calendars.Read Calendars.ReadWrite Mail.Read Mail.ReadWrite Mail.Send +Contacts.Read Tasks.Read Tasks.ReadWrite OnlineMeetings.ReadWrite +ChannelMessage.Send ChannelMessage.Edit Chat.Create Chat.ReadWrite +ChatMessage.Send Files.ReadWrite.All User.Read User.ReadBasic.All +offline_access email profile openid +``` + +The per-user grant is what makes Leap → calendar work. **No admin role is required to hold the grant.** + +### 4. Tenant user-consent policy + +`policies/authorizationPolicy` shows: +- `permissionGrantPoliciesAssigned`: + - `ManagePermissionGrantsForSelf.microsoft-user-default-recommended` + - `ManagePermissionGrantsForSelf.microsoft-user-default-allow-consent-apps` + +Under the **recommended** baseline, users may self-consent to apps from verified publishers, but only for "low-risk" delegated permissions. `Mail.ReadWrite`, `Mail.Send`, `Files.ReadWrite.All` are explicitly classified as **not** low-risk → admin approval required. This is what produces the "no admin permissions" message in Leap. + +### 5. Why she's the only one stuck + +She was hired/onboarded after the previous batch of 5 users was approved. The other consents were granted point-in-time (per-user), so a new user has to go through the same approval again. This is not a policy regression — it's the steady-state pattern in this tenant. + +--- + +## Recommended Fix (least-privilege) + +### Option A — Admin Consent Request (preferred, matches existing pattern) + +1. Have Svetlana sign in to Leap, click **Connect to Microsoft 365 / Enable calendar sync**. +2. When she hits the "approval required" page, she clicks **Request approval from your administrator** and submits a short justification. +3. The admin (`sysadmin@grabblaw.com` or the `guru@grabblaw.com` Global Admin) approves at: + `https://entra.microsoft.com → Identity → Applications → Enterprise applications → Admin consent requests` +4. Choose **Approve for this user only** (this matches what the other 5 employees have). +5. Leap calendar sync starts working immediately. + +Net effect: a single new `oauth2PermissionGrant` row tied to her object ID with `consentType=Principal`. No role change. No tenant-wide impact. + +### Option B — Switch to tenant-wide admin consent (broader, easier going forward) + +If new hires keep tripping over this, an admin can grant the LEAP delegated app **tenant-wide** consent once: + +`https://entra.microsoft.com → Enterprise applications → LEAP (appId a7d19842-33e2-457b-a399-d4e6ec010f0a) → Permissions → Grant admin consent for Grabb & Durando` + +Trade-off: ALL users get those scopes automatically (including new hires). The existing pattern in this tenant is per-user, so this is a deliberate change in posture — not a fix. Worth considering if Leap is the firm's standard practice management tool and everyone needs it. + +### What NOT to do + +- Do **not** assign her any directory role (Global Admin, Exchange Admin, etc.). It would not fix this — the error is OAuth consent, not RBAC. A user with no admin role can hold the grant, as 4 of the 5 working users prove. + +--- + +## Evidence Artifacts + +Raw JSON in `/tmp/remediation-tool/032b383e-96e4-491b-880d-3fd3295672c3/sla-check/`: +- `user.json` — slarionova profile + license +- `memberOf.json`, `transitive.json` — group/role membership (no admin roles) +- `grants.json` — her OAuth grants (empty) +- `sp-leap.json` — both LEAP SPs found in tenant +- `sp-{spid}-grants.json` (via direct query) — current consent state on each LEAP SP +- `skus.json` — license definitions +- `ga-role.json`, `exo-role.json` — directory role lookups + +## Tools used + +- App tier: `investigator` (Graph read-only) — `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` +- Auth: cert (client_assertion JWT) via `get-token.sh` +- No write actions performed. diff --git a/clients/grabb-durando/session-logs/2026-05-04-leap-m365-calendar-fix.md b/clients/grabb-durando/session-logs/2026-05-04-leap-m365-calendar-fix.md new file mode 100644 index 0000000..fc41df6 --- /dev/null +++ b/clients/grabb-durando/session-logs/2026-05-04-leap-m365-calendar-fix.md @@ -0,0 +1,72 @@ +# Grabb & Durando — Leap M365 Calendar Sync + Teams + Monitor + +**Date (UTC):** 2026-05-04 +**Tech:** Howard Enos +**User helped:** Svetlana Larionova (`slarionova@grabblaw.com`) +**Tenant:** grabblaw.com (`032b383e-96e4-491b-880d-3fd3295672c3`) + +## User +- **User:** Howard Enos (howard) +- **Machine:** Howard-Home +- **Role:** tech + +## Summary + +Multiple support items for Svetlana at Grabb & Durando in one session: got Leap's M365 calendar sync working for her account (admin-consent + token-rebind cleanup), added a second monitor to her workstation, and resolved a Teams file-send issue. + +## 1. Leap M365 calendar sync — root cause and fix + +### Problem +Svetlana could not add calendar events from Leap to M365. Leap displayed "no admin permissions" during the connect-to-M365 flow. + +### Root cause +The error message is misleading — it's an OAuth consent block, not an RBAC issue. Leap's user-facing app (appId `a7d19842-33e2-457b-a399-d4e6ec010f0a`, registered in tenant as service principal `7dd62220-081d-4c6b-8184-dc3187e81578`) requests scopes including `Mail.ReadWrite`, `Mail.Send`, `Files.ReadWrite.All` along with `Calendars.ReadWrite`. The grabblaw.com tenant's user-consent policy (`microsoft-user-default-recommended` + `microsoft-user-default-allow-consent-apps`) classifies those as high-risk and blocks user-level self-consent. New users have to be admin-approved for Leap to connect — at the time of investigation, five other users (jsosa, rpesqueira, avazquez, jwilliams, yheredia) already had per-user `Principal` grants from a prior approval round; Svetlana didn't because she was onboarded later. + +### Investigation +Used the ComputerGuru Security Investigator (Graph read-only) app to: +- Pull Svetlana's user record — confirmed `accountEnabled=true`, license `O365_BUSINESS_PREMIUM`, no admin roles, no group memberships beyond the company DL. +- Enumerate the LEAP service principals — found two: a daemon/app-only app (5602fc50…, has `Calendars.ReadWrite` Application permission) and the user-facing delegated app (a7d19842…, requires per-user delegated consent). +- Confirm the 5 working users had `Principal`-type OAuth grants on the delegated app and 4 of them had no admin roles. Conclusion: she did not need an admin role — just admin approval for the Leap app for her account. + +Full investigation report: `clients/grabb-durando/reports/2026-05-04-leap-calendar-permission-investigation.md`. + +### Fix +Mike granted **tenant-wide** consent (`consentType=AllPrincipals`) on the LEAP delegated app at 16:26 UTC via sysadmin@grabblaw.com. Verified via `oauth2PermissionGrants` endpoint — grant count on LEAP2 went from 5 (per-user Principals) → 6 (5 Principals + 1 AllPrincipals). + +This is broader than my original recommendation (per-user via Admin Consent Request), but it has a useful side-effect: any future new hire at Grabb & Durando won't hit the consent block — Leap will just work. + +### Second issue — Leap bound to sysadmin's identity +After consent, Leap stopped asking for admin permission but threw "unable to subscribe to notifications. leap cannot complete this action" instead. On further inspection, Leap was syncing **sysadmin's** mailbox/calendar instead of Svetlana's — because when Mike signed in to Leap on Svetlana's PC to grant consent, Leap stored sysadmin's user token as the calendar identity. + +### Resolution steps Howard performed +1. Ran Leap repair (leap.us) — did not fully fix. +2. Revoked the M365 OAuth grant for Leap on sysadmin's account. +3. Deleted the Leap Outlook integration cache from `%LOCALAPPDATA%\Microsoft Corporation\` on Svetlana's PC. +4. Re-enabled the M365 connection in Leap, signing in as Svetlana. +5. Calendar sync now works against Svetlana's mailbox. + +### Going forward +For future new hires at Grabb & Durando, do NOT have an admin sign in to Leap on the user's machine to grant consent — the admin's identity gets bound to that Leap install. Either: +- Have the user run the connect flow themselves (tenant-wide consent now exists, so they should sail through), OR +- If broader consent is ever needed, grant it via the Entra portal admin-consent flow, not by signing in to the third-party app. + +## 2. Teams not sending files — "outside of their domain" + +Svetlana was getting blocked sending files in Teams. Root cause: the recipient was outside the grabblaw.com domain. Teams correctly enforces the firm's external-collaboration policy; this isn't a bug. Walked through the limitation with her so she knows when to use email/secure-share for external recipients vs. Teams for internal. + +(No tenant-side change needed — this is policy working as intended.) + +## 3. Second monitor + +Added a second monitor to her workstation. Plug-and-play; configured display arrangement. + +## Tools / accounts touched + +- ComputerGuru Security Investigator (Graph read-only) — investigation only +- sysadmin@grabblaw.com — granted tenant-wide consent for LEAP delegated app +- Svetlana's PC — Leap repair, deleted `%LOCALAPPDATA%\Microsoft Corporation\` Leap cache, re-signed in to Leap + +## Follow-up + +- Syncro ticket to create + bill (this session). +- No further M365 / Leap work needed unless calendar sync regresses. diff --git a/clients/instrumental-music-center/session-logs/2026-05-04-station2-printer-and-manda-vpn.md b/clients/instrumental-music-center/session-logs/2026-05-04-station2-printer-and-manda-vpn.md new file mode 100644 index 0000000..8aa0d55 --- /dev/null +++ b/clients/instrumental-music-center/session-logs/2026-05-04-station2-printer-and-manda-vpn.md @@ -0,0 +1,20 @@ +# IMC — Station 2 receipt printer reconnect + VPN install on Manda's machine + +**Date (UTC):** 2026-05-04 +**Tech:** Howard Enos +**Time onsite:** 0.5 hours +**Syncro ticket:** #32247 (`109838539`), invoice #67559 (`1650189692`) + +## User +- **User:** Howard Enos (howard) +- **Machine:** Howard-Home +- **Role:** tech + +## Summary + +Two quick onsite items at Instrumental Music Center on Speedway: + +1. **Station 2 receipt printer.** Stopped printing. Removed the shared printer from Station 2 and re-added it from `\\imc1`. Test print succeeded; printer back to normal operation. Classic Windows print spooler / shared-printer drift — re-adding the share refreshes the local print queue binding. +2. **VPN install on Manda's machine.** Installed and configured the VPN client; verified connection. + +No other findings. No follow-up needed.