sync: auto-sync from HOWARD-HOME at 2026-07-06 08:29:31
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-06 08:29:31
This commit is contained in:
55
.claude/commands/tailscale.md
Normal file
55
.claude/commands/tailscale.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: tailscale
|
||||
description: Manage an ACG-administered Tailscale tailnet via the REST API v2 - list/inspect devices, delete a node, authorize a device, create/list/revoke tagged pre-auth keys. Reads free; writes gated --confirm.
|
||||
---
|
||||
|
||||
# /tailscale — Tailscale tailnet management (REST API v2)
|
||||
|
||||
Thin entry point to the `tailscale` skill. Engine:
|
||||
`.claude/skills/tailscale/scripts/tailscale-api.sh`. Full reference:
|
||||
`.claude/skills/tailscale/SKILL.md`. Doctrine (per-client tailnets, tagged pre-auth
|
||||
keys, offboarding): `wiki/patterns/tailscale-client-management.md`.
|
||||
|
||||
Read-only by default; every device delete/authorize and auth-key create/delete is
|
||||
**gated behind `--confirm`** and previews first. ADMIN rights: add + delete.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
TS="bash .claude/skills/tailscale/scripts/tailscale-api.sh"
|
||||
|
||||
# Reads (no confirm)
|
||||
$TS status # auth check + tailnet + device count
|
||||
$TS devices [--json] # list devices
|
||||
$TS device <name|id|100.x> [--json] # device detail
|
||||
$TS keys [--json] # list auth keys
|
||||
|
||||
# Writes (GATED — preview without --confirm, act with it)
|
||||
$TS create-key --tag tag:<client> --reusable --preauth [--ephemeral] \
|
||||
[--expiry-days N] [--desc "..."] --confirm
|
||||
$TS delete-key <keyId> --confirm # revoke an auth key
|
||||
$TS authorize <name|id|100.x> --confirm # approve a device
|
||||
$TS delete-device <name|id|100.x> --confirm # remove a node
|
||||
|
||||
# Point at a specific per-client tailnet's vault entry
|
||||
$TS devices --vault tailscale/roberts.sops.yaml
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Vault-first auth.** Credentials come from `tailscale/api-access.sops.yaml` (OAuth
|
||||
client preferred, `credentials.api_key` token fallback); tailnet defaults to `-`. Never
|
||||
hardcode a token. Provisioning commands are in the skill doc.
|
||||
2. **Read before write.** Device ops resolve a name/100.x to the stable id first; an
|
||||
**ambiguous match STOPS** (lists candidates, does nothing) — pass a specific id.
|
||||
3. **Gated writes.** No `--confirm` = preview + exit, no change. Deleting a node drops it
|
||||
off the tailnet; revoking a key blocks future enrollments with it.
|
||||
4. **Per-client isolation.** One tailnet per client, one vault entry per tailnet. Confirm
|
||||
the `--vault`/tailnet before any write.
|
||||
5. **Key secrets → vault, never chat/commit.** `create-key` prints the secret once; store
|
||||
it via `/vault` immediately.
|
||||
6. **Bot alert after every write.** Successful writes auto-post a `[TAILSCALE] ...` line to
|
||||
Discord (soft-fail); reads do not.
|
||||
|
||||
Use this to *drive* the tailnet; use `/rmm` to push the tagged pre-auth key onto client
|
||||
machines (`tailscale-client-enroll.ps1`) once a key is minted.
|
||||
@@ -9,6 +9,7 @@
|
||||
- [exchange-op = all-access Exchange tier](feedback_exchange_op_all_access.md) — STOP claiming "no tier can write mail." Exchange Operator app = Exchange Admin role + full_access_as_app + Exchange.ManageAsApp = full all-access (move mail, rules, config, EWS). Default to `exchange-op` for any Exchange write.
|
||||
- [Tedards tenant facts](reference_tedards_tenant_facts.md) — Bill Tedards law office; tenant `4fcbb1f4…`; bt@/y226@ mailboxes; matter-number filing; UAL ingestion OFF; 9 synced devices; botched-import DUPLICATE folder.
|
||||
- [Investigator EXO ManageAsApp gap](reference_investigator_exo_manageasapp_gap.md) — Security Investigator app lacks `Exchange.ManageAsApp` (only `full_access_as_app`) so `investigator-exo` 401s on EXO adminapi; use `exchange-op` tier for InvokeCommand.
|
||||
- [Windows edition upgrade via RMM](reference_windows_edition_upgrade_rmm.md) — Verified Home->Pro/Pro-for-Workstations fleet procedure: `changepk.exe /ProductKey <generic Pro>` (NOT DISM Set-Edition — Error 50) -> reboot with `Restart-Computer -Force` (`shutdown /r /t N` is silently dropped in SYSTEM session) -> `slmgr /ipk <MAK>` + /ato. The vault MAK is a Pro-for-WORKSTATIONS key (auto-upgrades edition past plain Pro).
|
||||
- [Tailscale subnet-route key expiry](reference_tailscale_subnet_key_expiry.md) — "internet OK but all of 172.16.3.x (Gitea .20, RMM/coord .30) dead" = Tailscale infra-node KEY EXPIRY (pfSense subnet router advertises 172.16.0.0/22), NOT a LAN outage; expiry now disabled on infra nodes (2026-06-25). Fallback: gururmm-server direct at tailnet 100.86.12.15:3001.
|
||||
- [GravityZone support center](reference_gravityzone_support.md) — Authoritative Bitdefender GravityZone product + Public API docs; use to confirm UNVERIFIED `bitdefender` skill methods/param shapes (push setPushEventSettings, assignPolicy, report/account writes, maintenancewindows/integrations names).
|
||||
- [GURU-5070 Rust toolchain](reference_guru5070_rust_toolchain.md) — toolchain installed BUT a WDAC/Smart App Control policy (since 2026-07-04) BLOCKS running rustc/cargo/sops/openssl locally (os error 4551) — build the Windows agent on Beast, not here. Toolchain paths + CI gates still valid for reference.
|
||||
|
||||
35
.claude/memory/reference_windows_edition_upgrade_rmm.md
Normal file
35
.claude/memory/reference_windows_edition_upgrade_rmm.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: reference_windows_edition_upgrade_rmm
|
||||
description: Verified fleet procedure to upgrade a Windows machine Home->Pro/Pro-for-Workstations via RMM, with the three gotchas (DISM error 50, shutdown /t drop, PfW MAK auto-upgrade).
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
Verified on ACG-Tech03L 2026-07-06 (Home 26200 -> Win 11 Pro for Workstations, permanently activated).
|
||||
|
||||
**Procedure (all steps as SYSTEM via `/rmm`, byte-exact via -EncodedCommand):**
|
||||
1. Probe: EditionID/LicenseStatus (`.rmm-key-probe.ps1` pattern), confirm no logged-in user
|
||||
(`query user`) so a reboot is safe.
|
||||
2. Flip edition: `changepk.exe /ProductKey VK7JG-NPHTM-C97JM-9MPGT-3V66T` (generic Pro key).
|
||||
Flips EditionID Core->Professional **live** (registry) + sets CBS RebootPending. exit 0.
|
||||
3. Reboot to finalize (see gotcha #2).
|
||||
4. Install MAK + activate: `slmgr /ipk <MAK>` then `slmgr /ato`; verify `slmgr /xpr` =
|
||||
"permanently activated" and WMI SoftwareLicensingProduct LicenseStatus=1.
|
||||
|
||||
**Gotcha 1 — DISM is a dead end for online edition change.** `DISM /online /Set-Edition:Professional`
|
||||
returns **Error 50 "Setting an edition is not supported with online images"** even though
|
||||
`/Get-TargetEditions` lists Professional. Use `changepk.exe`, not DISM, for online Home->Pro.
|
||||
|
||||
**Gotcha 2 — `shutdown /r /t N` is silently DROPPED in the SYSTEM service session.** The command
|
||||
returns exit 0 but the box never reboots (verified twice on tech03l). Also `shutdown /c "comment"`
|
||||
fails outright because the RMM->cmd.exe layer strips the quotes ([[feedback_windows_quote_stripping]]).
|
||||
Reboot reliably with `Restart-Computer -Force` (encoded PS) or `shutdown /r /t 0 /f` (no comment).
|
||||
NOTE: on a fast SSD the edition-upgrade reboot can be <30s of downtime — a 30s-interval last_seen
|
||||
monitor can miss the gap entirely; confirm the reboot by `LastBootUpTime` advancing, not by catching
|
||||
the offline window. `is_connected` is null fleet-wide — track `last_seen` age, never that flag.
|
||||
|
||||
**Gotcha 3 — the ACG MAK `infrastructure/windows-pro-mak` is a Pro-for-WORKSTATIONS MAK** (mislabeled
|
||||
"Pro"). `slmgr /ipk` of it on a Professional machine AUTO-UPGRADES the edition to ProfessionalWorkstation
|
||||
(a strict superset of Pro). If a client genuinely needs *plain* Pro, this key will over-shoot to PfW —
|
||||
there is no plain-Pro MAK in the vault. Billing: each activation consumes one MAK count = $99 to the
|
||||
customer (ACG-internal machines: no invoice, still burns a count).
|
||||
171
.claude/skills/tailscale/SKILL.md
Normal file
171
.claude/skills/tailscale/SKILL.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: tailscale
|
||||
description: "Manage an ACG-administered Tailscale tailnet via the Tailscale REST API v2: list/inspect devices, delete a device/node, authorize a device, list/create/revoke tagged pre-auth keys. ADMIN rights (add + delete). Reads run freely; every device delete/authorize and key create/delete is gated --confirm. Triggers: tailscale, tailnet, add device, delete device/node, revoke/create auth key, tailscale offboard."
|
||||
---
|
||||
|
||||
# Tailscale Skill
|
||||
|
||||
Standalone bash CLI for a Tailscale tailnet that ACG administers, via the **Tailscale
|
||||
REST API v2** (`https://api.tailscale.com/api/v2`). Read-only by default; every
|
||||
destructive or state-changing operation (delete a device, authorize a device, create an
|
||||
auth key, revoke an auth key) is **gated behind `--confirm`** and prints a preview first.
|
||||
|
||||
This skill implements the API side of ACG's **per-client tailnet doctrine** — see
|
||||
`wiki/patterns/tailscale-client-management.md`. One tailnet per client, ACG holds an
|
||||
Admin/Owner seat, devices enroll as **tagged nodes via reusable + pre-approved pre-auth
|
||||
keys** pushed from GuruRMM. This skill is how you drive that tailnet from the terminal:
|
||||
mint the tagged keys, watch devices land, authorize/prune nodes, and revoke keys at
|
||||
offboarding.
|
||||
|
||||
## Running the CLI
|
||||
|
||||
```bash
|
||||
TS="bash $CLAUDETOOLS_ROOT/.claude/skills/tailscale/scripts/tailscale-api.sh"
|
||||
$TS status # auth check + tailnet + device count
|
||||
$TS devices # list devices (table)
|
||||
$TS devices --json # machine output
|
||||
$TS device <name|id|100.x> # device detail
|
||||
$TS keys # list auth keys
|
||||
$TS create-key --tag tag:roberts --reusable --preauth --expiry-days 90 --desc "onboard" --confirm
|
||||
$TS delete-key <keyId> --confirm # revoke an auth key
|
||||
$TS delete-device <name|id|100.x> --confirm # remove a node
|
||||
$TS authorize <name|id|100.x> --confirm # approve a device
|
||||
$TS --help # full usage (runs with no creds)
|
||||
```
|
||||
|
||||
`--vault <path>` selects a different vault entry (per-client tailnets each have their own).
|
||||
`CLAUDETOOLS_ROOT` resolves from the env var, else the repo root via `git rev-parse`,
|
||||
else `C:/claudetools`.
|
||||
|
||||
## Credentials & auth (vault-first, OAuth preferred)
|
||||
|
||||
The bearer is NEVER hardcoded. At runtime it is resolved from the SOPS vault entry
|
||||
`tailscale/api-access.sops.yaml` (override with `--vault` or `TAILSCALE_VAULT`). Two
|
||||
credential shapes are supported, **OAuth preferred**:
|
||||
|
||||
- **OAuth client (preferred, non-expiring):** `credentials.client_id` +
|
||||
`credentials.client_secret`. The CLI POSTs
|
||||
`grant_type=client_credentials&client_id=...&client_secret=...` (secret fed over
|
||||
**stdin**, never argv) to `/oauth/token` and uses the returned short-lived
|
||||
`access_token` as the bearer. Required scopes: **`devices:core`** (list/delete/authorize
|
||||
devices) and **`auth_keys`** (create/list/delete keys).
|
||||
- **Raw API access token (fallback):** `credentials.api_key` (a `tskey-api-...` token)
|
||||
used directly as `Authorization: Bearer <token>`.
|
||||
|
||||
**Tailnet** is a path segment, resolved (first hit wins) from: env `TAILSCALE_TAILNET`,
|
||||
`credentials.tailnet`, top-level plaintext `tailnet:`, else default **`-`** (the default
|
||||
tailnet of the authenticated principal). Set a specific org name (e.g. `example.com`) only
|
||||
when the account administers more than one tailnet.
|
||||
|
||||
The bearer is written to a `0600` temp curl config (`-K`) and removed on exit, so it never
|
||||
appears in argv, the process list, or stdout.
|
||||
|
||||
### Provision the vault entry (admin, one-time — entry does NOT exist yet)
|
||||
|
||||
OAuth client (preferred):
|
||||
|
||||
```bash
|
||||
bash .claude/skills/vault/scripts/vault-helper.sh new tailscale/api-access.sops.yaml \
|
||||
--kind oauth-client --name "ACG Tailscale API (OAuth client)" \
|
||||
--url https://api.tailscale.com/api/v2 --tag tailscale \
|
||||
--set client_id='<OAUTH_CLIENT_ID>' \
|
||||
--set client_secret='<OAUTH_CLIENT_SECRET>' \
|
||||
--set tailnet='-'
|
||||
```
|
||||
|
||||
Raw API access token (fallback):
|
||||
|
||||
```bash
|
||||
bash .claude/skills/vault/scripts/vault-helper.sh new tailscale/api-access.sops.yaml \
|
||||
--kind api-key --name "ACG Tailscale API (access token)" \
|
||||
--url https://api.tailscale.com/api/v2 --tag tailscale \
|
||||
--set api_key='tskey-api-XXXXXXXXXXXXXXXX' \
|
||||
--set tailnet='-'
|
||||
```
|
||||
|
||||
`vault-helper.sh new` places every `--set` under the encrypted `credentials:` block, so
|
||||
`tailnet` reads back via `credentials.tailnet` (the CLI also honors a top-level plaintext
|
||||
`tailnet:` if an admin prefers it in cleartext). After creating: `vault-helper.sh verify
|
||||
tailscale/api-access.sops.yaml`, then publish with `bash .claude/scripts/sync.sh`. For a
|
||||
**per-client** tailnet, store its own entry (e.g. `tailscale/roberts.sops.yaml`) and pass
|
||||
`--vault tailscale/roberts.sops.yaml`.
|
||||
|
||||
## Endpoint map (Tailscale REST API v2)
|
||||
|
||||
| Subcommand | Method + path | Notes |
|
||||
|---|---|---|
|
||||
| `devices` | `GET /tailnet/{tailnet}/devices?fields=all` | full device inventory |
|
||||
| `device <q>` | `GET /device/{deviceId}?fields=all` | detail after name->id resolve |
|
||||
| `delete-device <q>` | `DELETE /device/{deviceId}` | **GATED** |
|
||||
| `authorize <q>` | `POST /device/{deviceId}/authorized` `{"authorized":true}` | **GATED** |
|
||||
| `keys` | `GET /tailnet/{tailnet}/keys` | list auth keys |
|
||||
| `create-key` | `POST /tailnet/{tailnet}/keys` | **GATED**; tagged reusable/preauth pattern |
|
||||
| `delete-key <keyId>` | `DELETE /tailnet/{tailnet}/keys/{keyId}` | **GATED** revoke |
|
||||
| `status` | `GET .../devices` | reachability + counts |
|
||||
|
||||
`create-key` body (matches the wiki's reusable + pre-approved + tagged pattern):
|
||||
|
||||
```json
|
||||
{"capabilities":{"devices":{"create":{"reusable":true,"ephemeral":false,"preauthorized":true,"tags":["tag:roberts"]}}},"expirySeconds":7776000,"description":"onboard roberts"}
|
||||
```
|
||||
|
||||
Built from `--tag` (repeatable), `--reusable`, `--preauth`, `--ephemeral`,
|
||||
`--expiry-days N` (default 90), `--desc`. The created key secret is returned **once** — the
|
||||
CLI prints a `[WARNING]` and the secret; store it in the vault immediately, never in
|
||||
chat/commit.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Read before write.** Device ops accept a NAME, the stable numeric/node id, or a 100.x
|
||||
address; the name is resolved to an id against the live device list before any action.
|
||||
- **Gated destructive ops.** `delete-device`, `authorize`, `create-key`, `delete-key`
|
||||
refuse to run without `--confirm` — they print the exact preview and exit 3. Never run a
|
||||
delete casually; deleting a node drops it off the tailnet.
|
||||
- **Ambiguous-match STOP.** If a device query matches >1 device, the CLI lists the
|
||||
candidates and refuses to act (exit 4). Re-run with a specific id or a unique name/address.
|
||||
- **Per-client isolation.** Never merge a client into ACG's own tailnet and never share one
|
||||
tailnet across clients — one tailnet per client, one vault entry per tailnet
|
||||
(`wiki/patterns/tailscale-client-management.md`). Confirm you are pointed at the right
|
||||
`--vault`/tailnet before a write.
|
||||
- **Secrets stay out of argv/stdout/logs.** Bearer via `-K` config file; OAuth secret via
|
||||
stdin. Do not paste key secrets into tickets/commits — vault the path.
|
||||
|
||||
## Bot alert (after every write)
|
||||
|
||||
Mirroring the `/rmm` and `screenconnect` skills, every successful **write** posts a
|
||||
one-line `[TAILSCALE] ...` alert via `.claude/scripts/post-bot-alert.sh` (soft-fail,
|
||||
ASCII only). Read operations (`status`, `devices`, `device`, `keys`) do NOT alert. Covered
|
||||
writes: `delete-device`, `authorize`, `create-key`, `delete-key`.
|
||||
|
||||
## Verified API response shapes
|
||||
|
||||
- **Device (list item / detail):** `id` (stable numeric string), `nodeId`, `name` (MagicDNS
|
||||
FQDN), `hostname`, `addresses` (`["100.x.y.z", "fd7a:..."]`), `os`, `user`, `tags`
|
||||
(`["tag:roberts"]`), `authorized` (bool), `lastSeen`, `clientVersion`, `updateAvailable`.
|
||||
Device ops use `id`.
|
||||
- **Auth key (list / create):** `id` (keyId), `key` (the secret — **create only, once**),
|
||||
`created`, `expires`, `capabilities.devices.create.{reusable,ephemeral,preauthorized,tags}`,
|
||||
`description`.
|
||||
- **Errors:** JSON `{"message":"..."}` with a non-2xx status.
|
||||
|
||||
## Error table
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `[BLOCKED] no Tailscale credential` | vault entry missing / wrong fields | provision per above; check `--vault` path |
|
||||
| `OAuth token exchange failed (HTTP 401)` | bad client_id/secret or missing scopes | verify OAuth client has `devices:core` + `auth_keys` |
|
||||
| `HTTP 403` on a write | token/OAuth scope lacks the capability | add `auth_keys` (keys) / `devices:core` (devices) scope |
|
||||
| `HTTP 404` on device/key | wrong id, or wrong tailnet | check `status` tailnet; re-resolve by name |
|
||||
| `AMBIGUOUS (>N matches)` | name matches several nodes | pass a specific id or a unique name/address |
|
||||
| `no device matches` | typo / device gone | run `devices` to see current inventory |
|
||||
|
||||
On a GENUINE functional error (auth failure, non-2xx API response) the CLI logs to
|
||||
`errorlog.md` via `log-skill-error.sh` (soft-fail). Expected/handled conditions (a `--confirm`
|
||||
gate blocking, an empty match, a preview) are NOT logged.
|
||||
|
||||
## Reference
|
||||
|
||||
- Doctrine: `wiki/patterns/tailscale-client-management.md` (per-client tailnets, tagged
|
||||
pre-auth keys, enrollment via GuruRMM, offboarding).
|
||||
- Enrollment script: `wiki/patterns/tailscale-client-enroll.ps1`.
|
||||
- Tailscale API docs: https://tailscale.com/api and https://tailscale.com/kb/1085/auth-keys
|
||||
293
.claude/skills/tailscale/scripts/tailscale-api.sh
Normal file
293
.claude/skills/tailscale/scripts/tailscale-api.sh
Normal file
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env bash
|
||||
# tailscale-api.sh — CLI for the ACG Tailscale tailnet via the Tailscale REST API v2
|
||||
# (base: https://api.tailscale.com/api/v2). Read-only by default; every device
|
||||
# delete/authorize and every auth-key create/delete is GATED behind --confirm.
|
||||
#
|
||||
# AUTH (resolved from the SOPS vault, OAuth preferred, token fallback):
|
||||
# vault path: tailscale/api-access.sops.yaml (override: --vault <path> or env TAILSCALE_VAULT)
|
||||
# credentials.client_id + credentials.client_secret -> OAuth client (preferred, non-expiring)
|
||||
# POST /oauth/token -> short-lived bearer
|
||||
# credentials.api_key -> raw API access token (tskey-api-...) fallback
|
||||
# tailnet (path segment): credentials.tailnet | top-level plaintext `tailnet:` | env
|
||||
# TAILSCALE_TAILNET | default `-` (the authenticated principal's default tailnet).
|
||||
#
|
||||
# Secrets never touch argv/stdout: the bearer is written to a 0600 curl config file
|
||||
# (-K) and the OAuth secret is fed to curl over stdin (--data @-).
|
||||
#
|
||||
# Subcommands:
|
||||
# status auth check + tailnet + device count
|
||||
# devices [--json] list devices (name/id/addr/os/user/lastSeen/tags/update/online)
|
||||
# device <name|id|100.x> [--json] device detail
|
||||
# delete-device <name|id|100.x> [--confirm] DELETE a device (GATED)
|
||||
# authorize <name|id|100.x> [--confirm] POST authorized=true (GATED)
|
||||
# keys [--json] list auth keys
|
||||
# create-key [--tag tag:x]... [--reusable] [--preauth] [--ephemeral]
|
||||
# [--expiry-days N] [--desc "..."] [--confirm] create an auth key (GATED)
|
||||
# delete-key <keyId> [--confirm] DELETE/revoke an auth key (GATED)
|
||||
#
|
||||
# Device args accept a NAME, the stable numeric/node id, or a 100.x address; the name is
|
||||
# resolved to an id against the live device list. On an AMBIGUOUS match (>1 device) the
|
||||
# candidates are printed and the op STOPS — it never acts on a guess.
|
||||
#
|
||||
# Usage: bash .claude/skills/tailscale/scripts/tailscale-api.sh <subcommand> [args] [--confirm]
|
||||
# Runs `--help` with no credentials.
|
||||
set -uo pipefail
|
||||
|
||||
API="https://api.tailscale.com/api/v2"
|
||||
OAUTH_URL="https://api.tailscale.com/api/v2/oauth/token"
|
||||
|
||||
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDETOOLS_ROOT:-C:/claudetools}")"
|
||||
VAULT_SH="$REPO/.claude/scripts/vault.sh"
|
||||
ALERT_SH="$REPO/.claude/scripts/post-bot-alert.sh"
|
||||
|
||||
logerr() { bash "$REPO/.claude/scripts/log-skill-error.sh" "tailscale" "$1" ${2:+--context "$2"} >/dev/null 2>&1 || true; }
|
||||
|
||||
usage() {
|
||||
sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
# ---- arg parse (global) -------------------------------------------------------
|
||||
VAULT_PATH="${TAILSCALE_VAULT:-tailscale/api-access.sops.yaml}"
|
||||
CONFIRM=0; JSON=0; TAGS=(); REUSABLE=false; PREAUTH=false; EPHEMERAL=false
|
||||
EXPIRY_DAYS=90; DESC=""; POS=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help|help) usage 0;;
|
||||
--vault) VAULT_PATH="${2:?--vault needs a path}"; shift 2;;
|
||||
--confirm) CONFIRM=1; shift;;
|
||||
--json) JSON=1; shift;;
|
||||
--tag) TAGS+=("${2:?--tag needs a value e.g. tag:roberts}"); shift 2;;
|
||||
--reusable) REUSABLE=true; shift;;
|
||||
--preauth) PREAUTH=true; shift;;
|
||||
--ephemeral) EPHEMERAL=true; shift;;
|
||||
--expiry-days) EXPIRY_DAYS="${2:?--expiry-days needs N}"; shift 2;;
|
||||
--desc) DESC="${2:-}"; shift 2;;
|
||||
*) POS+=("$1"); shift;;
|
||||
esac
|
||||
done
|
||||
SUB="${POS[0]:-}"; [ -n "$SUB" ] || usage 2
|
||||
|
||||
command -v jq >/dev/null 2>&1 || { echo "[ERROR] jq is required"; exit 2; }
|
||||
command -v curl >/dev/null 2>&1 || { echo "[ERROR] curl is required"; exit 2; }
|
||||
|
||||
# ---- credentials + bearer -----------------------------------------------------
|
||||
CURL_CFG="$(mktemp)"; chmod 600 "$CURL_CFG" 2>/dev/null || true
|
||||
trap 'rm -f "$CURL_CFG"' EXIT
|
||||
|
||||
vget() { bash "$VAULT_SH" get-field "$VAULT_PATH" "$1" 2>/dev/null; }
|
||||
|
||||
TAILNET="${TAILSCALE_TAILNET:-}"
|
||||
BEARER=""
|
||||
resolve_auth() {
|
||||
[ -n "$TAILNET" ] || TAILNET="$(vget credentials.tailnet)"
|
||||
[ -n "$TAILNET" ] || TAILNET="$(vget tailnet)"
|
||||
[ -n "$TAILNET" ] || TAILNET="-"
|
||||
|
||||
local cid csec key
|
||||
cid="$(vget credentials.client_id)"; csec="$(vget credentials.client_secret)"
|
||||
if [ -n "$cid" ] && [ -n "$csec" ]; then
|
||||
# OAuth: secret over stdin (not argv); scopes devices:core + auth_keys.
|
||||
local out code body
|
||||
out="$(printf 'grant_type=client_credentials&client_id=%s&client_secret=%s' "$cid" "$csec" \
|
||||
| curl -sS --data @- -w $'\n%{http_code}' "$OAUTH_URL" 2>/dev/null)"
|
||||
code="${out##*$'\n'}"; body="${out%$'\n'*}"
|
||||
if [ "$code" = "200" ]; then
|
||||
BEARER="$(printf '%s' "$body" | jq -r '.access_token // empty')"
|
||||
fi
|
||||
[ -n "$BEARER" ] || { echo "[ERROR] OAuth token exchange failed (HTTP $code) at vault:$VAULT_PATH"; logerr "OAuth token exchange failed" "http=$code vault=$VAULT_PATH"; exit 2; }
|
||||
return 0
|
||||
fi
|
||||
key="$(vget credentials.api_key)"
|
||||
if [ -n "$key" ]; then BEARER="$key"; return 0; fi
|
||||
|
||||
echo "[BLOCKED] no Tailscale credential at vault:$VAULT_PATH"
|
||||
echo " need credentials.client_id + credentials.client_secret (OAuth) OR credentials.api_key (token)."
|
||||
exit 2
|
||||
}
|
||||
resolve_auth
|
||||
printf 'header = "Authorization: Bearer %s"\n' "$BEARER" > "$CURL_CFG"
|
||||
|
||||
# ---- API wrapper (HTTP_CODE / HTTP_BODY) --------------------------------------
|
||||
HTTP_CODE=""; HTTP_BODY=""
|
||||
api() { # METHOD PATH [JSON_BODY]
|
||||
local method="$1" path="$2" data="${3:-}" out
|
||||
local args=(-sS -X "$method" -H "Accept: application/json" -K "$CURL_CFG" -w $'\n%{http_code}')
|
||||
if [ -n "$data" ]; then
|
||||
out="$(printf '%s' "$data" | curl "${args[@]}" -H "Content-Type: application/json" --data-binary @- "$API/$path" 2>/dev/null)"
|
||||
else
|
||||
out="$(curl "${args[@]}" "$API/$path" 2>/dev/null)"
|
||||
fi
|
||||
HTTP_CODE="${out##*$'\n'}"; HTTP_BODY="${out%$'\n'*}"
|
||||
}
|
||||
api_ok() { case "$HTTP_CODE" in 2*) return 0;; *) return 1;; esac; }
|
||||
api_fail() { # context
|
||||
echo "[ERROR] Tailscale API $1 -> HTTP $HTTP_CODE"
|
||||
printf '%s\n' "$HTTP_BODY" | jq -r '.message // .' 2>/dev/null || printf '%s\n' "$HTTP_BODY"
|
||||
logerr "API $1 HTTP $HTTP_CODE" "tailnet=$TAILNET vault=$VAULT_PATH"
|
||||
}
|
||||
|
||||
alert() { [ -f "$ALERT_SH" ] && bash "$ALERT_SH" "$1" >/dev/null 2>&1 || true; }
|
||||
|
||||
# ---- device list + name->id resolution ----------------------------------------
|
||||
DEVLIST_JSON=""
|
||||
load_devices() {
|
||||
[ -n "$DEVLIST_JSON" ] && return 0
|
||||
api GET "tailnet/$TAILNET/devices?fields=all"
|
||||
api_ok || { api_fail "list devices"; exit 2; }
|
||||
DEVLIST_JSON="$HTTP_BODY"
|
||||
}
|
||||
|
||||
# echoes the resolved device id on success; on none/ambiguous prints to stderr, non-zero.
|
||||
resolve_device() {
|
||||
local q="$1"; load_devices
|
||||
local filt='.devices[] | select(%s) | .id'
|
||||
local exact
|
||||
exact="$(printf '%s' "$DEVLIST_JSON" | jq -r --arg q "$q" \
|
||||
'.devices[] | select((.id==$q) or (.nodeId==$q) or ((.addresses//[])|index($q))
|
||||
or ((.hostname//""|ascii_downcase)==($q|ascii_downcase))
|
||||
or ((.name//""|ascii_downcase|split(".")[0])==($q|ascii_downcase))) | .id')"
|
||||
local n; n="$(printf '%s\n' "$exact" | grep -c . || true)"
|
||||
if [ "$n" -eq 1 ]; then printf '%s' "$exact"; return 0; fi
|
||||
if [ "$n" -eq 0 ]; then
|
||||
# substring fallback on name/hostname
|
||||
exact="$(printf '%s' "$DEVLIST_JSON" | jq -r --arg q "$q" \
|
||||
'.devices[] | select(((.name//""|ascii_downcase)|contains($q|ascii_downcase))
|
||||
or ((.hostname//""|ascii_downcase)|contains($q|ascii_downcase))) | .id')"
|
||||
n="$(printf '%s\n' "$exact" | grep -c . || true)"
|
||||
fi
|
||||
if [ "$n" -eq 1 ]; then printf '%s' "$exact"; return 0; fi
|
||||
if [ "$n" -eq 0 ]; then echo "[ERROR] no device matches '$q' in tailnet $TAILNET" >&2; return 3; fi
|
||||
echo "[BLOCKED] '$q' is AMBIGUOUS ($n matches) — refusing to act. Candidates:" >&2
|
||||
printf '%s' "$DEVLIST_JSON" | jq -r --arg q "$q" \
|
||||
'.devices[] | select(((.name//""|ascii_downcase)|contains($q|ascii_downcase))
|
||||
or ((.hostname//""|ascii_downcase)|contains($q|ascii_downcase)))
|
||||
| " " + .id + " " + (.name//"?") + " " + ((.addresses//[])|join(","))' >&2
|
||||
return 4
|
||||
}
|
||||
|
||||
dev_label() { # id -> "name (addr)"
|
||||
printf '%s' "$DEVLIST_JSON" | jq -r --arg id "$1" \
|
||||
'.devices[] | select(.id==$id) | (.name//"?") + " (" + ((.addresses//[])|join(","))+")"'
|
||||
}
|
||||
|
||||
# ---- subcommands --------------------------------------------------------------
|
||||
case "$SUB" in
|
||||
status)
|
||||
load_devices
|
||||
local_count="$(printf '%s' "$DEVLIST_JSON" | jq -r '.devices | length')"
|
||||
echo "[OK] Tailscale API reachable"
|
||||
echo " tailnet : $TAILNET"
|
||||
echo " auth : $([ -n "$(vget credentials.client_id)" ] && echo OAuth-client || echo api-token)"
|
||||
echo " devices : $local_count"
|
||||
;;
|
||||
|
||||
devices)
|
||||
load_devices
|
||||
if [ "$JSON" = 1 ]; then printf '%s\n' "$DEVLIST_JSON" | jq '.'; exit 0; fi
|
||||
printf '%s' "$DEVLIST_JSON" | jq -r '
|
||||
.devices | sort_by(.name) | .[]
|
||||
| [ (.name//"?"), .id, ((.addresses//[])|join(",")), (.os//"?"),
|
||||
(.user//"?"), (.lastSeen//"?"), ((.tags//[])|join(",")),
|
||||
(if .updateAvailable then "update" else "-" end) ] | @tsv' \
|
||||
| { echo -e "NAME\tID\tADDRESSES\tOS\tUSER\tLASTSEEN\tTAGS\tUPDATE"; cat; } | column -t -s $'\t'
|
||||
;;
|
||||
|
||||
device)
|
||||
Q="${POS[1]:?usage: device <name|id|100.x>}"
|
||||
ID="$(resolve_device "$Q")" || exit $?
|
||||
api GET "device/$ID?fields=all"; api_ok || { api_fail "device detail"; exit 2; }
|
||||
if [ "$JSON" = 1 ]; then printf '%s\n' "$HTTP_BODY" | jq '.'; else
|
||||
printf '%s\n' "$HTTP_BODY" | jq -r '
|
||||
"name : " + (.name//"?"),
|
||||
"id : " + (.id//"?"),
|
||||
"nodeId : " + (.nodeId//"?"),
|
||||
"addresses : " + ((.addresses//[])|join(", ")),
|
||||
"os : " + (.os//"?"),
|
||||
"user : " + (.user//"?"),
|
||||
"tags : " + ((.tags//[])|join(", ")),
|
||||
"authorized : " + ((.authorized//false)|tostring),
|
||||
"lastSeen : " + (.lastSeen//"?"),
|
||||
"clientVer : " + (.clientVersion//"?"),
|
||||
"update : " + ((.updateAvailable//false)|tostring)'
|
||||
fi
|
||||
;;
|
||||
|
||||
delete-device)
|
||||
Q="${POS[1]:?usage: delete-device <name|id|100.x> [--confirm]}"
|
||||
ID="$(resolve_device "$Q")" || exit $?
|
||||
LBL="$(dev_label "$ID")"
|
||||
if [ "$CONFIRM" != 1 ]; then
|
||||
echo "[BLOCKED] would DELETE device $ID $LBL from tailnet $TAILNET"
|
||||
echo " re-run with --confirm to delete."
|
||||
exit 3
|
||||
fi
|
||||
api DELETE "device/$ID"; api_ok || { api_fail "delete device"; exit 2; }
|
||||
echo "[OK] deleted device $ID $LBL"
|
||||
alert "[TAILSCALE] deleted device $LBL ($ID) from tailnet $TAILNET"
|
||||
;;
|
||||
|
||||
authorize)
|
||||
Q="${POS[1]:?usage: authorize <name|id|100.x> [--confirm]}"
|
||||
ID="$(resolve_device "$Q")" || exit $?
|
||||
LBL="$(dev_label "$ID")"
|
||||
if [ "$CONFIRM" != 1 ]; then
|
||||
echo "[BLOCKED] would AUTHORIZE device $ID $LBL on tailnet $TAILNET"
|
||||
echo " re-run with --confirm to authorize."
|
||||
exit 3
|
||||
fi
|
||||
api POST "device/$ID/authorized" '{"authorized":true}'; api_ok || { api_fail "authorize device"; exit 2; }
|
||||
echo "[OK] authorized device $ID $LBL"
|
||||
alert "[TAILSCALE] authorized device $LBL ($ID) on tailnet $TAILNET"
|
||||
;;
|
||||
|
||||
keys)
|
||||
api GET "tailnet/$TAILNET/keys"; api_ok || { api_fail "list keys"; exit 2; }
|
||||
if [ "$JSON" = 1 ]; then printf '%s\n' "$HTTP_BODY" | jq '.'; exit 0; fi
|
||||
printf '%s' "$HTTP_BODY" | jq -r '
|
||||
.keys // [] | .[]
|
||||
| [ .id, (.description//"-"),
|
||||
((.capabilities.devices.create.tags//[])|join(",") // "-"),
|
||||
((.capabilities.devices.create.reusable//false)|tostring),
|
||||
(.created//"?"), (.expires//"?") ] | @tsv' \
|
||||
| { echo -e "KEYID\tDESC\tTAGS\tREUSABLE\tCREATED\tEXPIRES"; cat; } | column -t -s $'\t'
|
||||
;;
|
||||
|
||||
create-key)
|
||||
TAGS_JSON="$(printf '%s\n' "${TAGS[@]:-}" | jq -R . | jq -s 'map(select(length>0))')"
|
||||
EXPIRY_SECONDS=$(( EXPIRY_DAYS * 86400 ))
|
||||
BODY="$(jq -n \
|
||||
--argjson reusable "$REUSABLE" --argjson ephemeral "$EPHEMERAL" --argjson preauth "$PREAUTH" \
|
||||
--argjson expiry "$EXPIRY_SECONDS" --arg desc "$DESC" --argjson tags "$TAGS_JSON" \
|
||||
'{capabilities:{devices:{create:{reusable:$reusable,ephemeral:$ephemeral,preauthorized:$preauth,tags:$tags}}},expirySeconds:$expiry,description:$desc}')"
|
||||
if [ "$CONFIRM" != 1 ]; then
|
||||
echo "[BLOCKED] would CREATE auth key on tailnet $TAILNET with:"
|
||||
printf '%s\n' "$BODY" | jq '.'
|
||||
echo " re-run with --confirm to create."
|
||||
exit 3
|
||||
fi
|
||||
api POST "tailnet/$TAILNET/keys" "$BODY"; api_ok || { api_fail "create key"; exit 2; }
|
||||
KID="$(printf '%s' "$HTTP_BODY" | jq -r '.id // "?"')"
|
||||
echo "[OK] created auth key $KID (tags: $(printf '%s' "$TAGS_JSON" | jq -r 'join(",")'))"
|
||||
echo "[WARNING] the key secret is shown ONCE below — store it in the vault, never in chat/commit:"
|
||||
printf '%s' "$HTTP_BODY" | jq -r '.key // "(no key field returned)"'
|
||||
alert "[TAILSCALE] created auth key $KID on tailnet $TAILNET (tags: $(printf '%s' "$TAGS_JSON" | jq -r 'join(",")'))"
|
||||
;;
|
||||
|
||||
delete-key)
|
||||
KID="${POS[1]:?usage: delete-key <keyId> [--confirm]}"
|
||||
if [ "$CONFIRM" != 1 ]; then
|
||||
echo "[BLOCKED] would DELETE/revoke auth key $KID on tailnet $TAILNET"
|
||||
echo " re-run with --confirm to revoke."
|
||||
exit 3
|
||||
fi
|
||||
api DELETE "tailnet/$TAILNET/keys/$KID"; api_ok || { api_fail "delete key"; exit 2; }
|
||||
echo "[OK] revoked auth key $KID"
|
||||
alert "[TAILSCALE] revoked auth key $KID on tailnet $TAILNET"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "[ERROR] unknown subcommand: $SUB"
|
||||
usage 2
|
||||
;;
|
||||
esac
|
||||
112
.oc.json
Normal file
112
.oc.json
Normal file
@@ -0,0 +1,112 @@
|
||||
[
|
||||
{
|
||||
"uid": "pavon",
|
||||
"display_name": "pavon",
|
||||
"email": "none@none.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 14187291259614,
|
||||
"used_gb": 14187.29,
|
||||
"used_source": "filecache",
|
||||
"quota": "none",
|
||||
"last_login": "2026-06-24 20:28:56"
|
||||
},
|
||||
{
|
||||
"uid": "sysadmin",
|
||||
"display_name": "sysadmin",
|
||||
"email": "mike@azcomputerguru.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 2893037227698,
|
||||
"used_gb": 2893.04,
|
||||
"used_source": "filecache",
|
||||
"quota": "default",
|
||||
"last_login": "2026-07-04 14:38:14"
|
||||
},
|
||||
{
|
||||
"uid": "anaise",
|
||||
"display_name": "anaise",
|
||||
"email": "none@none.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 604783498945,
|
||||
"used_gb": 604.78,
|
||||
"used_source": "du",
|
||||
"quota": "default",
|
||||
"last_login": "2026-07-06 12:58:42"
|
||||
},
|
||||
{
|
||||
"uid": "rohrbach",
|
||||
"display_name": "rohrbach",
|
||||
"email": "mike.rohrbach@cclac.net",
|
||||
"enabled": true,
|
||||
"used_bytes": 602561516857,
|
||||
"used_gb": 602.56,
|
||||
"used_source": "filecache",
|
||||
"quota": "default",
|
||||
"last_login": "2026-07-06 12:58:27"
|
||||
},
|
||||
{
|
||||
"uid": "mara",
|
||||
"display_name": "mara",
|
||||
"email": "none@none.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 307956123824,
|
||||
"used_gb": 307.96,
|
||||
"used_source": "filecache",
|
||||
"quota": "default",
|
||||
"last_login": "2026-06-29 18:02:25"
|
||||
},
|
||||
{
|
||||
"uid": "bst",
|
||||
"display_name": "bst",
|
||||
"email": "kathybussinessservicesoftucson@gmail.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 252587843651,
|
||||
"used_gb": 252.59,
|
||||
"used_source": "filecache",
|
||||
"quota": "default",
|
||||
"last_login": "2026-04-07 19:26:21"
|
||||
},
|
||||
{
|
||||
"uid": "Martell",
|
||||
"display_name": "Martell",
|
||||
"email": "none@none.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 185630639668,
|
||||
"used_gb": 185.63,
|
||||
"used_source": "filecache",
|
||||
"quota": "default",
|
||||
"last_login": "2026-01-16 16:15:33"
|
||||
},
|
||||
{
|
||||
"uid": "minrec",
|
||||
"display_name": "minrec",
|
||||
"email": "none@none.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 98272572822,
|
||||
"used_gb": 98.27,
|
||||
"used_source": "filecache",
|
||||
"quota": "default",
|
||||
"last_login": "2026-07-05 16:38:57"
|
||||
},
|
||||
{
|
||||
"uid": "themarcgroup",
|
||||
"display_name": "themarcgroup",
|
||||
"email": "aeichart@themarcgroup.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 28834826887,
|
||||
"used_gb": 28.83,
|
||||
"used_source": "filecache",
|
||||
"quota": "default",
|
||||
"last_login": "2026-07-06 10:31:04"
|
||||
},
|
||||
{
|
||||
"uid": "jburger",
|
||||
"display_name": "jburger",
|
||||
"email": "none@none.com",
|
||||
"enabled": true,
|
||||
"used_bytes": 10877098580,
|
||||
"used_gb": 10.88,
|
||||
"used_source": "filecache",
|
||||
"quota": "default",
|
||||
"last_login": "2026-07-06 00:47:06"
|
||||
}
|
||||
]
|
||||
20
.rmm-key-probe.ps1
Normal file
20
.rmm-key-probe.ps1
Normal file
@@ -0,0 +1,20 @@
|
||||
$os=Get-CimInstance Win32_OperatingSystem
|
||||
$cur=Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
|
||||
Write-Host "== CURRENT =="
|
||||
Write-Host ("Edition: {0} | EditionID: {1} | Build {2}" -f $os.Caption,$cur.EditionID,$os.BuildNumber)
|
||||
$lic=Get-CimInstance SoftwareLicensingProduct -Filter "ApplicationID='55c92734-d682-4d71-983e-d6ec3f16059f' AND PartialProductKey IS NOT NULL"
|
||||
foreach($l in $lic){Write-Host ("Lic: {0} | Partial {1} | Status {2}(1=OK)" -f $l.Name,$l.PartialProductKey,$l.LicenseStatus)}
|
||||
$oem=(Get-CimInstance SoftwareLicensingService).OA3xOriginalProductKey
|
||||
Write-Host ("FirmwareKey: {0}" -f $(if($oem){$oem}else{"(none)"}))
|
||||
Write-Host "== WINDOWS.OLD =="
|
||||
$hive="C:\Windows.old\Windows\System32\config\SOFTWARE"
|
||||
if(Test-Path $hive){
|
||||
reg load "HKLM\OLDSW" "$hive" | Out-Null
|
||||
try{
|
||||
$spp=Get-ItemProperty "HKLM:\OLDSW\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform"
|
||||
$old=Get-ItemProperty "HKLM:\OLDSW\Microsoft\Windows NT\CurrentVersion"
|
||||
Write-Host ("BackupProductKeyDefault: {0}" -f $spp.BackupProductKeyDefault)
|
||||
Write-Host ("Old: {0} | EditionID {1}" -f $old.ProductName,$old.EditionID)
|
||||
}catch{Write-Host ("hive err: {0}" -f $_.Exception.Message)}
|
||||
finally{[gc]::Collect();Start-Sleep -Milliseconds 300;reg unload "HKLM\OLDSW" | Out-Null}
|
||||
}else{Write-Host "no C:\Windows.old SOFTWARE hive"}
|
||||
15
.rmm-scan.ps1
Normal file
15
.rmm-scan.ps1
Normal file
@@ -0,0 +1,15 @@
|
||||
Write-Host "== drives =="
|
||||
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { Write-Host ("{0} free {1}GB / {2}GB" -f $_.DeviceID,[math]::Round($_.FreeSpace/1GB),[math]::Round($_.Size/1GB)) }
|
||||
Write-Host "== Windows.old* on all fixed drives =="
|
||||
$found=$false
|
||||
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
|
||||
$root="$($_.DeviceID)\"
|
||||
Get-ChildItem $root -Filter "Windows.old*" -Directory -Force -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$found=$true
|
||||
$sw=Join-Path $_.FullName "Windows\System32\config\SOFTWARE"
|
||||
Write-Host ("FOUND: {0} | SOFTWARE hive exists: {1}" -f $_.FullName,(Test-Path $sw))
|
||||
}
|
||||
}
|
||||
if(-not $found){Write-Host "none found on any drive"}
|
||||
Write-Host "== Pro digital license probe (dlv all) =="
|
||||
cscript //nologo "$env:windir\System32\slmgr.vbs" /dlv all 2>&1 | Select-String -Pattern "Name:|Description:|License Status:|Partial Product Key:" | ForEach-Object { $_.Line.Trim() }
|
||||
@@ -19,6 +19,7 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
|
||||
|
||||
<!-- Append entries below this line -->
|
||||
|
||||
<<<<<<< HEAD
|
||||
2026-07-06 | ACG-TECH03L | self-check/migrate-identity | [friction] migrate-identity.sh detected architecture=amd64 on a Windows-on-ARM (arm64) machine; had to patch identity.json manually. arch probe likely uses emulated uname [ctx: host=ACG-TECH03L arch=arm64]
|
||||
|
||||
2026-07-06 | ACG-Tech03L | vault | vault_path not set in identity.json; vault read failed [ctx: path=/c/claudetools/.claude/identity.json]
|
||||
@@ -26,6 +27,13 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
|
||||
2026-07-06 | ACG-Tech03L | vault | identity.json not found; vault read cannot resolve vault_path [ctx: path=/c/claudetools/.claude/identity.json] (x3)
|
||||
|
||||
2026-07-06 | DESKTOP-2U022SU | vault | identity.json not found; vault read cannot resolve vault_path [ctx: path=/c/claudetools/.claude/identity.json] (x4)
|
||||
=======
|
||||
2026-07-06 | Howard-Home | vault/windows-pro-mak | [correction] assumed vault key labeled 'Windows Pro MAK' was plain Pro; correct is Pro FOR WORKSTATIONS MAK - slmgr /ipk auto-upgrades edition to ProfessionalWorkstation [ctx: host=ACG-Tech03L partial=HMJ4K]
|
||||
|
||||
2026-07-06 | Howard-Home | rmm/reboot | [friction] shutdown /r /t 15 /c "comment" failed exit1 (usage dump) - quotes around /c comment stripped by RMM->cmd.exe layer; drop the /c comment or PS-encode [ctx: ref=feedback_windows_quote_stripping host=ACG-Tech03L]
|
||||
|
||||
2026-07-06 | Howard-Home | ps-encoded | encode produced empty output [ctx: src=.rmm-key-probe.ps1] (x2)
|
||||
>>>>>>> 3232dc1 (tailscale skill (admin add/delete) + verified Windows edition-upgrade procedure)
|
||||
|
||||
|
||||
2026-07-06 | Howard-Home | owncloud/testing | [friction] ran gated 'versions-cleanup --all-users --confirm' in a smoke test believing it would not execute; it DID (gating worked as designed) and a detached server-side occ versions:cleanup ran ~3min before I killed it. Fix: never pass --confirm to destructive all-users ops in tests; use refusal-path (no --confirm) only [ctx: skill=owncloud host=172.16.3.22 impact=partial-version-history-loss-user-mara]
|
||||
|
||||
202
projects/gps-rmm-audit/gps-clients.json
Normal file
202
projects/gps-rmm-audit/gps-clients.json
Normal file
@@ -0,0 +1,202 @@
|
||||
[
|
||||
{
|
||||
"client": "Instrumental Music Center",
|
||||
"cid": 7088508,
|
||||
"gps_devices": 20
|
||||
},
|
||||
{
|
||||
"client": "Safesite",
|
||||
"cid": 26563106,
|
||||
"gps_devices": 37
|
||||
},
|
||||
{
|
||||
"client": "Horseshoe Management",
|
||||
"cid": 625269,
|
||||
"gps_devices": 9
|
||||
},
|
||||
{
|
||||
"client": "Grabb & Durando Law Office",
|
||||
"cid": 14232794,
|
||||
"gps_devices": 12
|
||||
},
|
||||
{
|
||||
"client": "Quantum Wealth Management",
|
||||
"cid": 7088747,
|
||||
"gps_devices": 3
|
||||
},
|
||||
{
|
||||
"client": "Jimmy Company",
|
||||
"cid": 18560272,
|
||||
"gps_devices": 12
|
||||
},
|
||||
{
|
||||
"client": "Stamback Septic",
|
||||
"cid": 11513046,
|
||||
"gps_devices": 8
|
||||
},
|
||||
{
|
||||
"client": "Reliant Well Drilling and Pump Corporate",
|
||||
"cid": 10736261,
|
||||
"gps_devices": 9
|
||||
},
|
||||
{
|
||||
"client": "Zeus Nestora",
|
||||
"cid": 1196974,
|
||||
"gps_devices": 8
|
||||
},
|
||||
{
|
||||
"client": "Little Hearts Little Hands",
|
||||
"cid": 1144233,
|
||||
"gps_devices": 8
|
||||
},
|
||||
{
|
||||
"client": "PUTT LAND SURVEYING, INC.",
|
||||
"cid": 7180175,
|
||||
"gps_devices": 7
|
||||
},
|
||||
{
|
||||
"client": "Curtis Plumbing",
|
||||
"cid": 416585,
|
||||
"gps_devices": 6
|
||||
},
|
||||
{
|
||||
"client": "The Prairie Schooner",
|
||||
"cid": 3664974,
|
||||
"gps_devices": 5
|
||||
},
|
||||
{
|
||||
"client": "Mineralogical Record",
|
||||
"cid": 207770,
|
||||
"gps_devices": 5
|
||||
},
|
||||
{
|
||||
"client": "T & C Sorensen",
|
||||
"cid": 344886,
|
||||
"gps_devices": 4
|
||||
},
|
||||
{
|
||||
"client": "MVAN Enterprises Inc",
|
||||
"cid": 29462761,
|
||||
"gps_devices": 4
|
||||
},
|
||||
{
|
||||
"client": "Ridgetop Group",
|
||||
"cid": 9413367,
|
||||
"gps_devices": 3
|
||||
},
|
||||
{
|
||||
"client": "Multicultural Counseling Center",
|
||||
"cid": 35483539,
|
||||
"gps_devices": 3
|
||||
},
|
||||
{
|
||||
"client": "Brett Interiors",
|
||||
"cid": 15726057,
|
||||
"gps_devices": 3
|
||||
},
|
||||
{
|
||||
"client": "Heieck, Sheila",
|
||||
"cid": 12045942,
|
||||
"gps_devices": 3
|
||||
},
|
||||
{
|
||||
"client": "The Marc Group",
|
||||
"cid": 869073,
|
||||
"gps_devices": 2
|
||||
},
|
||||
{
|
||||
"client": "Residential and Renovation Engineering",
|
||||
"cid": 7088403,
|
||||
"gps_devices": 2
|
||||
},
|
||||
{
|
||||
"client": "Bill Tedards",
|
||||
"cid": 487887,
|
||||
"gps_devices": 2
|
||||
},
|
||||
{
|
||||
"client": "Janet Altschuler",
|
||||
"cid": 457710,
|
||||
"gps_devices": 2
|
||||
},
|
||||
{
|
||||
"client": "Business Services of Tucson LLC",
|
||||
"cid": 29338800,
|
||||
"gps_devices": 2
|
||||
},
|
||||
{
|
||||
"client": "Andy's Mobile Fuel",
|
||||
"cid": 27364453,
|
||||
"gps_devices": 2
|
||||
},
|
||||
{
|
||||
"client": "Design and Brand Envoys",
|
||||
"cid": 26747288,
|
||||
"gps_devices": 2
|
||||
},
|
||||
{
|
||||
"client": "Pro-Tech Services",
|
||||
"cid": 23702122,
|
||||
"gps_devices": 2
|
||||
},
|
||||
{
|
||||
"client": "Inside Track Productions",
|
||||
"cid": 3021358,
|
||||
"gps_devices": 1
|
||||
},
|
||||
{
|
||||
"client": "Gary A Hartman LLC",
|
||||
"cid": 29038261,
|
||||
"gps_devices": 1
|
||||
},
|
||||
{
|
||||
"client": "Robyn Pittman",
|
||||
"cid": 17031534,
|
||||
"gps_devices": 1
|
||||
},
|
||||
{
|
||||
"client": "Marty Ryan",
|
||||
"cid": 140717,
|
||||
"gps_devices": 1
|
||||
},
|
||||
{
|
||||
"client": "Dataforth Corp",
|
||||
"cid": 578095,
|
||||
"gps_devices": null
|
||||
},
|
||||
{
|
||||
"client": "Cascades of Tucson",
|
||||
"cid": 20149445,
|
||||
"gps_devices": null
|
||||
},
|
||||
{
|
||||
"client": "Valley Wide Plastering",
|
||||
"cid": 31694734,
|
||||
"gps_devices": null
|
||||
},
|
||||
{
|
||||
"client": "Len's Auto Brokerage",
|
||||
"cid": 3289131,
|
||||
"gps_devices": null
|
||||
},
|
||||
{
|
||||
"client": "Arizona Medical Transit",
|
||||
"cid": 7088349,
|
||||
"gps_devices": null
|
||||
},
|
||||
{
|
||||
"client": "AT Trebesch",
|
||||
"cid": 238740,
|
||||
"gps_devices": null
|
||||
},
|
||||
{
|
||||
"client": "Russo Law Firm",
|
||||
"cid": 23331699,
|
||||
"gps_devices": null
|
||||
},
|
||||
{
|
||||
"client": "Glaz-Tech Industries",
|
||||
"cid": 143932,
|
||||
"gps_devices": null
|
||||
}
|
||||
]
|
||||
42
projects/gps-rmm-audit/out.json
Normal file
42
projects/gps-rmm-audit/out.json
Normal file
@@ -0,0 +1,42 @@
|
||||
[
|
||||
{"client":"Instrumental Music Center","cid":7088508,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[365662],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Safesite","cid":26563106,"backup_seats":37,"backup_monthly":555,"backup_lines":["GPS Workstation"],"schedule_ids":[374386],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Horseshoe Management","cid":625269,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[323028,2379],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Grabb & Durando Law Office","cid":14232794,"backup_seats":1,"backup_monthly":49,"backup_lines":["Service - Data Backup"],"schedule_ids":[478096,126669],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Quantum Wealth Management","cid":7088747,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[456876,193901],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Jimmy Company","cid":18560272,"backup_seats":12,"backup_monthly":240,"backup_lines":["Service - Data Backup"],"schedule_ids":[402452,205345],"frequency":"Annually","note":"ok"},
|
||||
{"client":"Stamback Septic","cid":11513046,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[215079,154608],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Reliant Well Drilling and Pump Corporate","cid":10736261,"backup_seats":1,"backup_monthly":40,"backup_lines":["Service - Data Backup"],"schedule_ids":[144683,54149],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Zeus Nestora","cid":1196974,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[8332],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Little Hearts Little Hands","cid":1144233,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[333952,7547],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"PUTT LAND SURVEYING, INC.","cid":7180175,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[418370],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Curtis Plumbing","cid":416585,"backup_seats":2,"backup_monthly":50,"backup_lines":["Service - Data Backup"],"schedule_ids":[242342,9956,3399],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"The Prairie Schooner","cid":3664974,"backup_seats":1,"backup_monthly":10,"backup_lines":["Service - Data Backup"],"schedule_ids":[113429,46694],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Mineralogical Record","cid":207770,"backup_seats":1,"backup_monthly":10,"backup_lines":["Service - Data Backup"],"schedule_ids":[334414],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"T & C Sorensen","cid":344886,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[339403,269337,53177,53172,11392],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"MVAN Enterprises Inc","cid":29462761,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[379467],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Ridgetop Group","cid":9413367,"backup_seats":1,"backup_monthly":0,"backup_lines":["Service - Data Backup"],"schedule_ids":[155169],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Multicultural Counseling Center","cid":35483539,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[505004,498702],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"Brett Interiors","cid":15726057,"backup_seats":1,"backup_monthly":15,"backup_lines":["Service - Data Backup"],"schedule_ids":[101780],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Heieck, Sheila","cid":12045942,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[87583],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"The Marc Group","cid":869073,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[51489,5669,3330],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"Residential and Renovation Engineering","cid":7088403,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[336905,335126,66653],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Bill Tedards","cid":487887,"backup_seats":1,"backup_monthly":45,"backup_lines":["Service - Data Backup"],"schedule_ids":[2023],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Janet Altschuler","cid":457710,"backup_seats":2,"backup_monthly":0,"backup_lines":["Service - Data Backup"],"schedule_ids":[2666],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Business Services of Tucson LLC","cid":29338800,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[503850,373682],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"Andy's Mobile Fuel","cid":27364453,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[305466,305246],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Design and Brand Envoys","cid":26747288,"backup_seats":1,"backup_monthly":0,"backup_lines":["Service - Data Backup"],"schedule_ids":[332981,321139],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Pro-Tech Services","cid":23702122,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[389697],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Inside Track Productions","cid":3021358,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[30294,29759],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Gary A Hartman LLC","cid":29038261,"backup_seats":1,"backup_monthly":20,"backup_lines":["Service - Data Backup"],"schedule_ids":[351565],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Robyn Pittman","cid":17031534,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[114036],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Marty Ryan","cid":140717,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[336179,123459,713],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"Dataforth Corp","cid":578095,"backup_seats":50,"backup_monthly":665,"backup_lines":["GPS Workstation","GPS Server","Service - Data Backup","Service - Data Backup","Service - Data Backup"],"schedule_ids":[378819,362646],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Cascades of Tucson","cid":20149445,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[434199],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Valley Wide Plastering","cid":31694734,"backup_seats":4,"backup_monthly":60,"backup_lines":["Service - Data Backup"],"schedule_ids":[500734,500215],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Len's Auto Brokerage","cid":3289131,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[482424,11865],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Arizona Medical Transit","cid":7088349,"backup_seats":1,"backup_monthly":0,"backup_lines":["Service - Data Backup"],"schedule_ids":[151853,35028],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"AT Trebesch","cid":238740,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[2387],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Russo Law Firm","cid":23331699,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[513484,509659,499925],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Glaz-Tech Industries","cid":143932,"backup_seats":2,"backup_monthly":45,"backup_lines":["Service - Data Backup","Service - Data Backup"],"schedule_ids":[134087],"frequency":"Monthly","note":"ok"}
|
||||
]
|
||||
42
projects/gps-rmm-audit/out2.json
Normal file
42
projects/gps-rmm-audit/out2.json
Normal file
@@ -0,0 +1,42 @@
|
||||
[
|
||||
{"client":"Instrumental Music Center","cid":7088508,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[365662],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Safesite","cid":26563106,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[374386],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Horseshoe Management","cid":625269,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[323028,2379],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Grabb & Durando Law Office","cid":14232794,"backup_seats":1,"backup_monthly":49,"backup_lines":["Service - Data Backup"],"schedule_ids":[478096,126669],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Quantum Wealth Management","cid":7088747,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[456876,193901],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Jimmy Company","cid":18560272,"backup_seats":12,"backup_monthly":240,"backup_lines":["Service - Data Backup"],"schedule_ids":[402452,205345],"frequency":"Annually","note":"ok"},
|
||||
{"client":"Stamback Septic","cid":11513046,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[215079,154608],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Reliant Well Drilling and Pump Corporate","cid":10736261,"backup_seats":1,"backup_monthly":40,"backup_lines":["Service - Data Backup"],"schedule_ids":[144683,54149],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Zeus Nestora","cid":1196974,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[8332],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Little Hearts Little Hands","cid":1144233,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[333952,7547],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"PUTT LAND SURVEYING, INC.","cid":7180175,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[418370],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Curtis Plumbing","cid":416585,"backup_seats":2,"backup_monthly":50,"backup_lines":["Service - Data Backup"],"schedule_ids":[242342,9956,3399],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"The Prairie Schooner","cid":3664974,"backup_seats":1,"backup_monthly":10,"backup_lines":["Service - Data Backup"],"schedule_ids":[113429,46694],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Mineralogical Record","cid":207770,"backup_seats":1,"backup_monthly":10,"backup_lines":["Service - Data Backup"],"schedule_ids":[334414],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"T & C Sorensen","cid":344886,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[339403,269337,53177,53172,11392],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"MVAN Enterprises Inc","cid":29462761,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[379467],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Ridgetop Group","cid":9413367,"backup_seats":1,"backup_monthly":0,"backup_lines":["Service - Data Backup"],"schedule_ids":[155169],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Multicultural Counseling Center","cid":35483539,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[505004,498702],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"Brett Interiors","cid":15726057,"backup_seats":1,"backup_monthly":15,"backup_lines":["Service - Data Backup"],"schedule_ids":[101780],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Heieck, Sheila","cid":12045942,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[87583],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"The Marc Group","cid":869073,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[51489,5669,3330],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"Residential and Renovation Engineering","cid":7088403,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[336905,335126,66653],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Bill Tedards","cid":487887,"backup_seats":1,"backup_monthly":45,"backup_lines":["Service - Data Backup"],"schedule_ids":[2023],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Janet Altschuler","cid":457710,"backup_seats":2,"backup_monthly":0,"backup_lines":["Service - Data Backup"],"schedule_ids":[2666],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Business Services of Tucson LLC","cid":29338800,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[503850,373682],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"Andy's Mobile Fuel","cid":27364453,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[305466,305246],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Design and Brand Envoys","cid":26747288,"backup_seats":1,"backup_monthly":0,"backup_lines":["Service - Data Backup"],"schedule_ids":[332981,321139],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Pro-Tech Services","cid":23702122,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[389697],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Inside Track Productions","cid":3021358,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[30294,29759],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Gary A Hartman LLC","cid":29038261,"backup_seats":1,"backup_monthly":20,"backup_lines":["Service - Data Backup"],"schedule_ids":[351565],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Robyn Pittman","cid":17031534,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[114036],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Marty Ryan","cid":140717,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[336179,123459,713],"frequency":"Annually","note":"no backup line"},
|
||||
{"client":"Dataforth Corp","cid":578095,"backup_seats":7,"backup_monthly":95,"backup_lines":["Service - Data Backup","Service - Data Backup","Service - Data Backup"],"schedule_ids":[378819,362646],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Cascades of Tucson","cid":20149445,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[434199],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Valley Wide Plastering","cid":31694734,"backup_seats":4,"backup_monthly":60,"backup_lines":["Service - Data Backup"],"schedule_ids":[500734,500215],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"Len's Auto Brokerage","cid":3289131,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[482424,11865],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Arizona Medical Transit","cid":7088349,"backup_seats":1,"backup_monthly":0,"backup_lines":["Service - Data Backup"],"schedule_ids":[151853,35028],"frequency":"Monthly","note":"ok"},
|
||||
{"client":"AT Trebesch","cid":238740,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[2387],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Russo Law Firm","cid":23331699,"backup_seats":0,"backup_monthly":0,"backup_lines":[],"schedule_ids":[513484,509659,499925],"frequency":"Monthly","note":"no backup line"},
|
||||
{"client":"Glaz-Tech Industries","cid":143932,"backup_seats":2,"backup_monthly":45,"backup_lines":["Service - Data Backup","Service - Data Backup"],"schedule_ids":[134087],"frequency":"Monthly","note":"ok"}
|
||||
]
|
||||
73
projects/gps-rmm-audit/pull_backup.sh
Normal file
73
projects/gps-rmm-audit/pull_backup.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
BASE="https://computerguru.syncromsp.com/api/v1"
|
||||
KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18"
|
||||
CLIENTS="C:/claudetools/projects/gps-rmm-audit/gps-clients.json"
|
||||
|
||||
# iterate customers
|
||||
n=$(jq 'length' "$CLIENTS")
|
||||
echo "["
|
||||
first=1
|
||||
for i in $(seq 0 $((n-1))); do
|
||||
client=$(jq -r ".[$i].client" "$CLIENTS")
|
||||
cid=$(jq -r ".[$i].cid" "$CLIENTS")
|
||||
|
||||
# fetch schedules list
|
||||
slist=$(curl -s "$BASE/schedules?customer_id=$cid&api_key=$KEY" | tr -d '\000-\037')
|
||||
sids=$(echo "$slist" | jq -r '.schedules[]?.id' 2>/dev/null)
|
||||
|
||||
seats=0
|
||||
monthly=0
|
||||
lines_json="[]"
|
||||
sids_json="[]"
|
||||
freq="null"
|
||||
note="ok"
|
||||
|
||||
if [ -z "$sids" ]; then
|
||||
note="no recurring schedule"
|
||||
seats=0
|
||||
else
|
||||
# collect sids into json array
|
||||
sids_json=$(echo "$sids" | jq -R . | jq -s -c 'map(tonumber)')
|
||||
matched_lines="[]"
|
||||
any_backup=0
|
||||
for sid in $sids; do
|
||||
det=$(curl -s "$BASE/schedules/$sid?api_key=$KEY" | tr -d '\000-\037')
|
||||
# frequency (take first non-null)
|
||||
f=$(echo "$det" | jq -r '.schedule.frequency // empty' 2>/dev/null)
|
||||
if [ -n "$f" ] && [ "$freq" = "null" ]; then freq="$f"; fi
|
||||
# backup lines: name or description contains backup (case-insensitive)
|
||||
bl=$(echo "$det" | jq -c '[.schedule.lines[]? | select((.name // "") | ascii_downcase | test("backup"))]' 2>/dev/null)
|
||||
if [ -z "$bl" ]; then bl="[]"; fi
|
||||
cnt=$(echo "$bl" | jq 'length')
|
||||
if [ "$cnt" -gt 0 ]; then
|
||||
any_backup=1
|
||||
# sum quantity (quantity is a string like "18.0")
|
||||
q=$(echo "$bl" | jq '[.[].quantity // 0 | tonumber] | add')
|
||||
# sum quantity*price_retail
|
||||
m=$(echo "$bl" | jq '[.[] | ((.quantity // 0 | tonumber) * (.price_retail // 0 | tonumber))] | add')
|
||||
seats=$(awk "BEGIN{printf \"%g\", $seats + $q}")
|
||||
monthly=$(awk "BEGIN{printf \"%g\", $monthly + $m}")
|
||||
# append names
|
||||
nm=$(echo "$bl" | jq -c '[.[].name]')
|
||||
matched_lines=$(echo "$matched_lines $nm" | jq -s -c 'add')
|
||||
fi
|
||||
done
|
||||
lines_json="$matched_lines"
|
||||
if [ "$any_backup" -eq 0 ]; then
|
||||
note="no backup line"
|
||||
fi
|
||||
fi
|
||||
|
||||
# normalize monthly number
|
||||
if [ "$monthly" = "0" ]; then monthly="0"; fi
|
||||
if [ "$freq" != "null" ]; then freq="\"$freq\""; fi
|
||||
|
||||
# emit
|
||||
if [ $first -eq 0 ]; then echo ","; fi
|
||||
first=0
|
||||
printf '{"client":%s,"cid":%s,"backup_seats":%s,"backup_monthly":%s,"backup_lines":%s,"schedule_ids":%s,"frequency":%s,"note":%s}' \
|
||||
"$(echo "$client" | jq -R .)" "$cid" "$seats" "$monthly" "$lines_json" "$sids_json" "$freq" "$(echo "$note" | jq -R .)"
|
||||
done
|
||||
echo ""
|
||||
echo "]"
|
||||
Reference in New Issue
Block a user