diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb31541..a0dd636 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,21 @@ Instead use placeholders: ``, ``, ``, ``, If a step can't be written without a real particular, it doesn't belong here — split the particular out to the private tier and reference it as a placeholder. +### Naming + +Runbooks go in a domain directory and drop the prefix — `od/db-backup.md`, not +`od-db-backup.md`. The directory *is* the prefix, which keeps the fetch command +exactly as short as it was. Existing domains are `od/` and `sec/`; add `win/`, +`m365/`, or `net/` when the first file needs one. + +Scripts stay flat in `scripts/` and keep their domain prefix. Everything +executable lives in one directory on purpose — that is the set worth reading +before it runs. Non-interactive scripts (Intune remediations) get their own +sibling directory, not `scripts/`. + +Filenames stay short: they are typed by hand on a client keyboard, often +mid-incident. That constraint outranks descriptiveness. + ## Before every commit 1. Re-read the diff. Would a stranger learn *who* the client is, or *how to diff --git a/README.md b/README.md index cb1be3c..58127cc 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ client workstations during on-site work with short, hand-typeable commands. Fetch and read on the target workstation: ```powershell -irm rb.godwinsystems.com/ | more +irm rb.godwinsystems.com/od/db-backup.md | more ``` Run an executable runbook script directly (scripts live under `scripts/`): ```powershell -irm rb.godwinsystems.com/scripts/.ps1 | iex +irm rb.godwinsystems.com/scripts/od-db-backup.ps1 | iex ``` Scripts prompt for anything client-specific via `Read-Host` — nothing to edit @@ -44,7 +44,7 @@ mid-incident — the canonical URL is 63 characters before the filename. The canonical form still works and is what the redirect targets: ``` -https://gitea.ivangodwin.com/godwinsystems/rb/raw/branch/main/ +https://gitea.ivangodwin.com/godwinsystems/rb/raw/branch/main//.md ``` The redirect is a backendless Gateway API `HTTPRoute` defined in @@ -55,21 +55,37 @@ runbook here documents a dead URL. ## Layout & naming -- **Runbooks** (`.md`) live flat at the repo root with short, hand-typeable - filenames and light category prefixes. -- **Scripts** (`.ps1`) live under [`scripts/`](scripts/). `_template.ps1` - sorts first and is the convention reference, not a runnable runbook. -- **Meta** (`README.md`, `CONTRIBUTING.md`) stays at the root. +``` +od/ Open Dental +sec/ Security / incident response +scripts/ Executable .ps1 — one flat namespace, prefixed by domain +``` -Runbook prefixes: +- **Runbooks** (`.md`) live in a domain directory, named without a prefix — + the directory is the prefix. `od/db-backup.md`, not `od-db-backup.md`. +- **Scripts** (`.ps1`) stay flat in [`scripts/`](scripts/) and keep their + domain prefix. One directory holds everything executable, which is the set + worth auditing before a change; `_template.ps1` sorts first as the + convention reference, not a runnable runbook. +- **Meta** (`README.md`, `CONTRIBUTING.md`, `LICENSE`) stays at the root. -| Prefix | Domain | +The split is deliberate: the fetch command's length is the constraint this +repo is organized around, and `od/db-backup.md` is exactly as long to type as +`od-db-backup.md` was — the hyphen became a slash. Nesting scripts by domain +too would add characters without adding clarity to a five-file directory. + +Domain directories, as they are needed: + +| Directory | Domain | |---|---| -| `win-` | Windows workstation / server | -| `m365-` | Microsoft 365 / Entra | -| `od-` | Open Dental | -| `net-` | Networking | -| `sec-` | Security / incident response | +| `win/` | Windows workstation / server | +| `m365/` | Microsoft 365 / Entra | +| `od/` | Open Dental | +| `net/` | Networking | +| `sec/` | Security / incident response | + +Non-interactive scripts (Intune remediations) get their own sibling directory +when they arrive — not `scripts/`, which is for `iex`-safe interactive ones. ## Placeholder conventions @@ -87,16 +103,28 @@ run time — never commit filled-in values. ## Contents +### Open Dental — `od/` + +| File | Purpose | +|---|---| +| [`od/smb-cred.md`](od/smb-cred.md) | Open Dental SMB share — stored-credential fix | +| [`od/cfg-persist.md`](od/cfg-persist.md) | Open Dental — persist "Do not show this window on startup" (writable FreeDentalConfig.xml) | +| [`od/scan-duplex.md`](od/scan-duplex.md) | Open Dental — duplex ADF scanner captures only one side (TWAIN, Show TWAIN UI branches) | +| [`od/db-backup.md`](od/db-backup.md) | Open Dental — rock-solid cold backup of the database + images (stop/copy/start MySQL/MariaDB) | +| [`od/backup-verify.md`](od/backup-verify.md) | Open Dental — verify a backup by test-restoring into an isolated Hyper-V VM (health checklist) | +| [`od/backup-schedule.md`](od/backup-schedule.md) | Open Dental — schedule the backup + off-site upload and monitor it (dead-man's-switch heartbeat) | + +### Security / incident response — `sec/` + +| File | Purpose | +|---|---| +| [`sec/google-compromise.md`](sec/google-compromise.md) | Incident response — suspected compromise of a **consumer** Google/Gmail account (AiTM session theft; no Workspace admin console) | +| [`sec/google-evidence.md`](sec/google-evidence.md) | Google/Gmail account — evidence capture before changes + re-entry check after a password reset (numbered, field-usable) | + +### Scripts — `scripts/` + | File | Purpose | |---|---| -| [`od-smb-cred.md`](od-smb-cred.md) | Open Dental SMB share — stored-credential fix | -| [`od-cfg-persist.md`](od-cfg-persist.md) | Open Dental — persist "Do not show this window on startup" (writable FreeDentalConfig.xml) | -| [`od-scan-duplex.md`](od-scan-duplex.md) | Open Dental — duplex ADF scanner captures only one side (TWAIN, Show TWAIN UI branches) | -| [`od-db-backup.md`](od-db-backup.md) | Open Dental — rock-solid cold backup of the database + images (stop/copy/start MySQL/MariaDB) | -| [`od-backup-verify.md`](od-backup-verify.md) | Open Dental — verify a backup by test-restoring into an isolated Hyper-V VM (health checklist) | -| [`od-backup-schedule.md`](od-backup-schedule.md) | Open Dental — schedule the backup + off-site upload and monitor it (dead-man's-switch heartbeat) | -| [`sec-google-compromise.md`](sec-google-compromise.md) | Incident response — suspected compromise of a **consumer** Google/Gmail account (AiTM session theft; no Workspace admin console) | -| [`sec-google-evidence.md`](sec-google-evidence.md) | Google/Gmail account — evidence capture before changes + re-entry check after a password reset (numbered, field-usable) | | [`scripts/cg-disable.ps1`](scripts/cg-disable.ps1) | Disable Credential Guard, then reboot (prompts to confirm) | | [`scripts/od-cfg-acl.ps1`](scripts/od-cfg-acl.ps1) | Grant Users Modify on FreeDentalConfig.xml (Option B of od-cfg-persist) | | [`scripts/od-db-backup.ps1`](scripts/od-db-backup.ps1) | Cold backup: stop MySQL/MariaDB, copy whole data dir + OpenDentImages, always restart (od-db-backup) | diff --git a/od-backup-schedule.md b/od/backup-schedule.md similarity index 90% rename from od-backup-schedule.md rename to od/backup-schedule.md index acab298..c471863 100644 --- a/od-backup-schedule.md +++ b/od/backup-schedule.md @@ -3,7 +3,7 @@ > For qualified IT professionals, on systems they are authorized to administer. > Provided as-is, without warranty — verify it fits your environment. See LICENSE. -**Applies to:** The Open Dental database server, once [`od-db-backup.md`](od-db-backup.md) is proven to run by hand. +**Applies to:** The Open Dental database server, once [`od/db-backup.md`](db-backup.md) is proven to run by hand. **Goal:** Make the backup run **automatically every day**, replicate it **off-site**, and **monitor** it so a silent failure gets noticed within a day — not the day you need a restore. Open Dental's floor is **at least one backup per day**, with a combination of methods and at least one **automated**. ([Open Dental — Backups](https://opendental.com/manual/backups.html)) This runbook automates and watches the cold backup. @@ -26,10 +26,10 @@ Open Dental's floor is **at least one backup per day**, with a combination of me | Job | What | When | Runbook | |---|---|---|---| -| **1. Cold backup** | Stop DB, copy data + images, restart | Nightly, off-hours | [`od-db-backup.md`](od-db-backup.md) | -| **2. Off-site upload** | Replicate `` to cloud/immutable | After job 1 finishes | [`od-db-backup.md` §6](od-db-backup.md) | -| **3. Health check** | Verify newest backup + heartbeat ping | After job 1 finishes | this runbook + [`od-backup-check.ps1`](scripts/od-backup-check.ps1) | -| **4. Test-restore** | Full restore into isolated VM | Monthly | [`od-backup-verify.md`](od-backup-verify.md) | +| **1. Cold backup** | Stop DB, copy data + images, restart | Nightly, off-hours | [`od/db-backup.md`](db-backup.md) | +| **2. Off-site upload** | Replicate `` to cloud/immutable | After job 1 finishes | [`od/db-backup.md` §6](db-backup.md) | +| **3. Health check** | Verify newest backup + heartbeat ping | After job 1 finishes | this runbook + [`od-backup-check.ps1`](../scripts/od-backup-check.ps1) | +| **4. Test-restore** | Full restore into isolated VM | Monthly | [`od/backup-verify.md`](backup-verify.md) | Stagger them so nothing reads a half-written folder, e.g. **backup 23:30 → check 00:45 → off-site upload 01:00**. @@ -55,7 +55,7 @@ schtasks /Create /TN "OD Nightly Cold Backup" ^ ## 2. Schedule the off-site upload (after the backup) -Point your off-site tool (Duplicati → Backblaze B2, `rclone`, Veeam, etc.) at **``** — it uploads *this backup*, it does **not** re-run the stop/copy against the live database. Schedule it **after** job 1 completes and stagger the start. Enable that tool's **own** email/report and **object-lock/immutability** for the ransomware-resistant off-site copy. Details: [`od-db-backup.md` §6](od-db-backup.md). +Point your off-site tool (Duplicati → Backblaze B2, `rclone`, Veeam, etc.) at **``** — it uploads *this backup*, it does **not** re-run the stop/copy against the live database. Schedule it **after** job 1 completes and stagger the start. Enable that tool's **own** email/report and **object-lock/immutability** for the ransomware-resistant off-site copy. Details: [`od/db-backup.md` §6](db-backup.md). ## 3. Schedule the health check (and heartbeat) @@ -120,7 +120,7 @@ When Layer 2/3 flags a problem, in rough order: 1. **Is the database up?** `Get-Service ` — if the backup died mid-run, confirm the service **restarted** (the script's `finally` should have; verify). The practice being able to work comes first. 2. **Destination full / offline?** Free space on ``; is the disk/UNC reachable? Prune old `od-backup-` generations if space-bound. -3. **Partial/most-recent folder incomplete?** Check that run's `backup.log` for the `ERROR`/`CRITICAL` line; re-run the backup by hand ([`od-db-backup.md`](od-db-backup.md)). +3. **Partial/most-recent folder incomplete?** Check that run's `backup.log` for the `ERROR`/`CRITICAL` line; re-run the backup by hand ([`od/db-backup.md`](db-backup.md)). 4. **Version drift?** A recent Open Dental/MySQL update can change paths — reconcile against `MANIFEST.txt`. 5. **Off-site not uploading?** Check the cloud tool's log and that it sources `` (not the live datadir). @@ -128,7 +128,7 @@ Then re-run the health check and confirm the heartbeat goes green. ## Records / evidence -Keep the `\backup-check.log`, the monitor's uptime history, and the monthly **test-restore** results ([`od-backup-verify.md`](od-backup-verify.md)) together. That trail is your DR evidence for E&O / cyber insurance and HIPAA contingency-plan testing. +Keep the `\backup-check.log`, the monitor's uptime history, and the monthly **test-restore** results ([`od/backup-verify.md`](backup-verify.md)) together. That trail is your DR evidence for E&O / cyber insurance and HIPAA contingency-plan testing. --- @@ -141,4 +141,4 @@ Keep the `\backup-check.log`, the monitor's uptime history, and the monthl - Open Dental manual — Backups (daily minimum; automated + combined methods): - Microsoft — `schtasks` / Scheduled Tasks: - healthchecks.io — dead-man's-switch cron monitoring: -- Companion runbooks — [`od-db-backup.md`](od-db-backup.md) (produce the backup), [`od-backup-verify.md`](od-backup-verify.md) (test-restore) +- Companion runbooks — [`od/db-backup.md`](db-backup.md) (produce the backup), [`od/backup-verify.md`](backup-verify.md) (test-restore) diff --git a/od-backup-verify.md b/od/backup-verify.md similarity index 93% rename from od-backup-verify.md rename to od/backup-verify.md index eb42a93..8f8526a 100644 --- a/od-backup-verify.md +++ b/od/backup-verify.md @@ -3,7 +3,7 @@ > For qualified IT professionals, on systems they are authorized to administer. > Provided as-is, without warranty — verify it fits your environment. See LICENSE. -**Applies to:** Any Open Dental cold backup produced by [`od-db-backup.md`](od-db-backup.md) (an `od-backup-` folder holding `data\`, `OpenDentImages\`, `FreeDentalConfig.xml`, `MANIFEST.txt`). +**Applies to:** Any Open Dental cold backup produced by [`od/db-backup.md`](db-backup.md) (an `od-backup-` folder holding `data\`, `OpenDentImages\`, `FreeDentalConfig.xml`, `MANIFEST.txt`). **Goal:** Prove a backup is actually restorable and complete by restoring it into a **throwaway, network-isolated Hyper-V virtual machine** and running a short health checklist. A backup you have never restored is a guess. > [!IMPORTANT] @@ -66,7 +66,7 @@ Run these **inside the VM**, elevated. This mirrors Open Dental's documented res ``` robocopy "\data" "" /E /COPY:DAT /R:2 /W:5 ``` - > For **InnoDB** you must restore the shared tablespace and logs (`ibdata1`, `ib_logfile*`) that live at the datadir root alongside the `opendental` folder — that's why we restore the whole `data\`, matching how [`od-db-backup.md`](od-db-backup.md) captured it. Restoring only the `opendental` subfolder yields a database that won't start. + > For **InnoDB** you must restore the shared tablespace and logs (`ibdata1`, `ib_logfile*`) that live at the datadir root alongside the `opendental` folder — that's why we restore the whole `data\`, matching how [`od/db-backup.md`](db-backup.md) captured it. Restoring only the `opendental` subfolder yields a database that won't start. 4. **Restore the images:** ``` robocopy "\OpenDentImages" "" /E /COPY:DAT /R:2 /W:5 @@ -108,7 +108,7 @@ Log, outside the VM: **date verified**, `` timestamp, ` - Open Dental manual — Database Maintenance (Check/Fix health tool): - Open Dental manual — Backup Tool: -- Companion runbook — producing the backup: [`od-db-backup.md`](od-db-backup.md) +- Companion runbook — producing the backup: [`od/db-backup.md`](db-backup.md) diff --git a/od-cfg-persist.md b/od/cfg-persist.md similarity index 100% rename from od-cfg-persist.md rename to od/cfg-persist.md diff --git a/od-db-backup.md b/od/db-backup.md similarity index 97% rename from od-db-backup.md rename to od/db-backup.md index df22d67..be6acef 100644 --- a/od-db-backup.md +++ b/od/db-backup.md @@ -109,11 +109,11 @@ Register the cold backup as a nightly **Task Scheduler** job (runs off-hours, as schtasks /Create /TN "OD Nightly Cold Backup" /TR "powershell -NoProfile -ExecutionPolicy Bypass -File C:\ops\od-db-backup-local.ps1" /SC DAILY /ST 23:30 /RU SYSTEM /RL HIGHEST ``` -Full scheduling **and monitoring** (staggering the off-site upload, a daily health check, and a dead-man's-switch heartbeat so a silent failure gets caught): **[`od-backup-schedule.md`](od-backup-schedule.md)**. +Full scheduling **and monitoring** (staggering the off-site upload, a daily health check, and a dead-man's-switch heartbeat so a silent failure gets caught): **[`od/backup-schedule.md`](backup-schedule.md)**. ## 5. Verify — a backup you haven't restored is a guess -- **Test-restore** to an **isolated** machine periodically (matching Open Dental + MySQL/MariaDB versions): `net stop`, rename the existing `opendental` folder aside, drop in the backup's `data\` contents, `net start`, launch Open Dental, spot-check patients/images. Full step-by-step with a health checklist: **[`od-backup-verify.md`](od-backup-verify.md)** (isolated Hyper-V test-restore). +- **Test-restore** to an **isolated** machine periodically (matching Open Dental + MySQL/MariaDB versions): `net stop`, rename the existing `opendental` folder aside, drop in the backup's `data\` contents, `net start`, launch Open Dental, spot-check patients/images. Full step-by-step with a health checklist: **[`od/backup-verify.md`](backup-verify.md)** (isolated Hyper-V test-restore). - **Never restore over a live production database** — data loss is irreversible. - Confirm the nightly job is actually producing dated folders and that they leave the building (off-site / immutable copy) — ransomware that reaches the server will reach on-line backups too. @@ -138,7 +138,7 @@ Any off-site/cloud replication (Duplicati → Backblaze B2, Veeam, `rclone`, Was Backup media and dumps hold the **entire practice's PHI**. Treat them as the crown jewels: encrypt the destination, lock down share permissions, and keep at least one copy **off-line/immutable** so ransomware can't encrypt your backups along with production. Don't store `` in the scheduled command line — use a MySQL option file / limited account. -Note that the captured `FreeDentalConfig.xml` stores the MySQL password **obfuscated but reversible** on **direct-connection** setups. The backup already contains all PHI so this doesn't change the requirement — but it's one more reason the destination must be encrypted and access-controlled. (Middle Tier setups don't put DB credentials in that file — see `od-cfg-persist.md`.) +Note that the captured `FreeDentalConfig.xml` stores the MySQL password **obfuscated but reversible** on **direct-connection** setups. The backup already contains all PHI so this doesn't change the requirement — but it's one more reason the destination must be encrypted and access-controlled. (Middle Tier setups don't put DB credentials in that file — see `od/cfg-persist.md`.) ## References diff --git a/od-scan-duplex.md b/od/scan-duplex.md similarity index 100% rename from od-scan-duplex.md rename to od/scan-duplex.md diff --git a/od-smb-cred.md b/od/smb-cred.md similarity index 100% rename from od-smb-cred.md rename to od/smb-cred.md diff --git a/scripts/cg-disable.ps1 b/scripts/cg-disable.ps1 index c972c13..40c0185 100644 --- a/scripts/cg-disable.ps1 +++ b/scripts/cg-disable.ps1 @@ -12,7 +12,7 @@ .NOTES Run via: irm rb.godwinsystems.com/scripts/cg-disable.ps1 | iex - Referenced by od-smb-cred.md, Step 3. + Referenced by od/smb-cred.md, Step 3. If Credential Guard is still running after reboot, it was enabled with a UEFI lock (needs the bcdedit / physical-presence removal), or MDM policy is diff --git a/scripts/od-backup-check.ps1 b/scripts/od-backup-check.ps1 index 2b0e180..4cea251 100644 --- a/scripts/od-backup-check.ps1 +++ b/scripts/od-backup-check.ps1 @@ -26,7 +26,7 @@ For unattended monitoring, schedule a filled-in LOCAL copy (with and the heartbeat URL baked in) — keep that copy in the private tier, not here. See - od-backup-schedule.md. + od/backup-schedule.md. iex-safe: uses `return`, never `exit` (which would close an interactive session). PUBLIC REPO: no client specifics hard-coded; prompt at run time. @@ -148,4 +148,4 @@ if ($heartbeat) { } } -if (-not $healthy) { Write-Warning 'Investigate now — see od-backup-schedule.md triage.' } +if (-not $healthy) { Write-Warning 'Investigate now — see od/backup-schedule.md triage.' } diff --git a/scripts/od-cfg-acl.ps1 b/scripts/od-cfg-acl.ps1 index b9d1f8d..f914e8b 100644 --- a/scripts/od-cfg-acl.ps1 +++ b/scripts/od-cfg-acl.ps1 @@ -16,7 +16,7 @@ .NOTES Run via: irm rb.godwinsystems.com/scripts/od-cfg-acl.ps1 | iex - Referenced by od-cfg-persist.md, Option B. + Referenced by od/cfg-persist.md, Option B. SECURITY: on direct-connection setups FreeDentalConfig.xml also holds the MySQL password (obfuscated but reversible). Widening write access here does diff --git a/scripts/od-db-backup.ps1 b/scripts/od-db-backup.ps1 index f9f37b3..17c2acb 100644 --- a/scripts/od-db-backup.ps1 +++ b/scripts/od-db-backup.ps1 @@ -24,7 +24,7 @@ This is the primary "gold" backup. Pair it with an off-hours mysqldump for a portable logical copy and with off-site/immutable retention — see - od-db-backup.md. + od/db-backup.md. .NOTES Run ON THE DATABASE SERVER, elevated, when no one is using Open Dental: diff --git a/sec-google-compromise.md b/sec/google-compromise.md similarity index 99% rename from sec-google-compromise.md rename to sec/google-compromise.md index 97336a0..ed05713 100644 --- a/sec-google-compromise.md +++ b/sec/google-compromise.md @@ -12,7 +12,7 @@ > Nothing here decides whether a reportable breach occurred, whether notification is required, or what any deadline is. Those are legal determinations. Where this runbook reaches one, it tells you to **stop, document, and escalate** to the practice's compliance contact or counsel. Work the technical steps; do not opine on the legal ones — not in this document, not in email, not verbally to the client. > [!WARNING] -> **This repo is public-read.** This file is a **template with placeholders only**. Do not fill it in here and do not commit incident particulars, real account names, phishing domains, hostnames, or timestamps to this repo. Clone it to the **private tier** (private repo or Bitwarden secure note) and work the filled-in copy there. See [CONTRIBUTING.md](CONTRIBUTING.md). +> **This repo is public-read.** This file is a **template with placeholders only**. Do not fill it in here and do not commit incident particulars, real account names, phishing domains, hostnames, or timestamps to this repo. Clone it to the **private tier** (private repo or Bitwarden secure note) and work the filled-in copy there. See [CONTRIBUTING.md](../CONTRIBUTING.md). --- @@ -656,7 +656,7 @@ Deliver **in writing**, factually, with no legal characterization: - [ ] Deploy a password manager; eliminate reuse. - [ ] Add DNS filtering and configure UniFi **syslog export** so the next incident has logs. - [ ] Security awareness training covering AiTM phishing specifically — including that a correct-looking Google page and a working 2FA prompt prove nothing. -- [ ] Verify Duplicati backups are intact, versioned, and **not reachable with any credential exposed in this incident**; confirm the Open Dental/MariaDB backup chain per [`od-backup-verify.md`](od-backup-verify.md). +- [ ] Verify Duplicati backups are intact, versioned, and **not reachable with any credential exposed in this incident**; confirm the Open Dental/MariaDB backup chain per [`od/backup-verify.md`](../od/backup-verify.md). --- @@ -755,4 +755,4 @@ If a path doesn't match what you see: navigate from **[myaccount.google.com](htt - Microsoft — Microsoft Safety Scanner download: - Sysinternals — Autoruns: - Sysinternals — Process Explorer: -- Companion runbook — backup integrity after an incident: [`od-backup-verify.md`](od-backup-verify.md) +- Companion runbook — backup integrity after an incident: [`od/backup-verify.md`](../od/backup-verify.md) diff --git a/sec-google-evidence.md b/sec/google-evidence.md similarity index 97% rename from sec-google-evidence.md rename to sec/google-evidence.md index 84722dd..9a2540b 100644 --- a/sec-google-evidence.md +++ b/sec/google-evidence.md @@ -5,7 +5,7 @@ **Applies to:** A consumer Google/Gmail account, before or after containment. No Workspace admin console required. **Use it for:** (a) capturing account state **before** you change anything, and (b) checking whether anyone got back in **after** a password reset. -**Companion:** [`sec-google-compromise.md`](sec-google-compromise.md) — full containment procedure. +**Companion:** [`sec/google-compromise.md`](google-compromise.md) — full containment procedure. **Placeholders:**