sync: auto-sync from HOWARD-HOME at 2026-07-07 20:31:16
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-07 20:31:16
This commit is contained in:
@@ -613,6 +613,27 @@ COMMENT_ID=$(echo "$COMMENT_RESP" | jq -r '.comment.id')
|
|||||||
- Do NOT wrap the payload in `{"comment": {...}}` — returns 422.
|
- Do NOT wrap the payload in `{"comment": {...}}` — returns 422.
|
||||||
- **If `COMMENT_ID` is null:** GET `/tickets/{id}` and check `.ticket.comments[]` by subject before doing anything else. Comments cannot be deleted via API — duplicates require manual GUI removal.
|
- **If `COMMENT_ID` is null:** GET `/tickets/{id}` and check `.ticket.comments[]` by subject before doing anything else. Comments cannot be deleted via API — duplicates require manual GUI removal.
|
||||||
|
|
||||||
|
#### Customer Assets (retire / archive — API GAP)
|
||||||
|
|
||||||
|
**Retiring a device in Syncro = "Archive" (UI ONLY — no REST API).** Verified against
|
||||||
|
Syncro's own docs 2026-07-08 (`docs.syncrosecure.com/assets-rmm/archive-assets`): archiving
|
||||||
|
is done in the web UI only — asset Details page → **Actions → Archive**, or bulk via the
|
||||||
|
Assets table checkboxes → **Bulk Actions → Archive**. There is **no** archive endpoint and
|
||||||
|
**no** `archived` field on `PUT /customer_assets/{id}`. Do NOT try to archive via API, and
|
||||||
|
do NOT substitute `DELETE /customer_assets/{id}` — delete is destructive/irreversible and
|
||||||
|
loses history + breaks ticket linkages (Archive keeps the asset visible on its tickets/alerts).
|
||||||
|
|
||||||
|
- **To retire assets: hand the user the asset list + the Bulk-Actions-→-Archive steps.** We
|
||||||
|
cannot do it for them via API.
|
||||||
|
- Asset READS are fine: `GET /customer_assets?customer_id=N[&query=<name>]` (list, 100/page,
|
||||||
|
`.assets[]`) and `GET /customer_assets/{id}` (detail; `.asset` — RMM data lives under
|
||||||
|
`.asset.properties.kabuto_information`). `updated_at` is NOT a reliable "last online" (bulk
|
||||||
|
account touches move it) — use the ScreenConnect `GuestInfoUpdateTime` or kabuto data for
|
||||||
|
real last-seen.
|
||||||
|
- **[RESEARCH]** Re-check if Syncro ships an archive API (watch release notes). If/when it
|
||||||
|
does, wire it in here and add `--confirm` gating. Until then this is a "cannot be performed
|
||||||
|
via skill" function. (First hit: IMC retirement pass 2026-07-08.)
|
||||||
|
|
||||||
#### Customers
|
#### Customers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ if [ "${1:-}" = "--read" ]; then
|
|||||||
THREAD="${2:-}"
|
THREAD="${2:-}"
|
||||||
LIMIT="${3:-25}"
|
LIMIT="${3:-25}"
|
||||||
[ -z "$THREAD" ] && { echo "[ERROR] usage: ask-forum.sh --read <thread_id> [limit]" >&2; exit 1; }
|
[ -z "$THREAD" ] && { echo "[ERROR] usage: ask-forum.sh --read <thread_id> [limit]" >&2; exit 1; }
|
||||||
|
printf '%s' "$LIMIT" | grep -qE '^[0-9]+$' || LIMIT=25 # Discord caps at 100
|
||||||
|
[ "$LIMIT" -lt 1 ] && LIMIT=1; [ "$LIMIT" -gt 100 ] && LIMIT=100
|
||||||
TOKEN="$(resolve_token)"
|
TOKEN="$(resolve_token)"
|
||||||
[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { echo "[ERROR] no Discord bot token" >&2; exit 2; }
|
[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { echo "[ERROR] no Discord bot token" >&2; exit 2; }
|
||||||
auth=(-H "Authorization: Bot ${TOKEN}" -H "User-Agent: ${UA}")
|
auth=(-H "Authorization: Bot ${TOKEN}" -H "User-Agent: ${UA}")
|
||||||
@@ -86,15 +88,20 @@ if [ "${1:-}" = "--wait" ]; then
|
|||||||
TOKEN="$(resolve_token)"
|
TOKEN="$(resolve_token)"
|
||||||
[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { echo "[ERROR] no Discord bot token" >&2; exit 2; }
|
[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { echo "[ERROR] no Discord bot token" >&2; exit 2; }
|
||||||
auth=(-H "Authorization: Bot ${TOKEN}" -H "User-Agent: ${UA}")
|
auth=(-H "Authorization: Bot ${TOKEN}" -H "User-Agent: ${UA}")
|
||||||
# baseline = id of newest message present right now
|
# baseline: for a forum post the starter message id == the thread id, so
|
||||||
AFTER="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?limit=1" | jq -r '.[0].id // "0"' 2>/dev/null)"
|
# after=<thread> returns every human reply after the starter -- this catches a
|
||||||
[ -z "$AFTER" ] && AFTER="0"
|
# reply that already landed BEFORE this --wait began (the old "newest message
|
||||||
|
# now" baseline missed it and hung to timeout).
|
||||||
|
AFTER="${THREAD}"
|
||||||
echo "[INFO] waiting on thread ${THREAD} for a human reply (up to ${TIMEOUT}s)..." >&2
|
echo "[INFO] waiting on thread ${THREAD} for a human reply (up to ${TIMEOUT}s)..." >&2
|
||||||
DEADLINE=$(( $(date +%s) + TIMEOUT ))
|
DEADLINE=$(( $(date +%s) + TIMEOUT ))
|
||||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||||
sleep "$POLL"
|
|
||||||
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?after=${AFTER}")"
|
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?after=${AFTER}")"
|
||||||
printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1 || continue
|
if ! printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
||||||
|
# thread gone -> bail fast; otherwise (transient/429) keep waiting
|
||||||
|
printf '%s' "$MSGS" | jq -e '.code==10003' >/dev/null 2>&1 && { echo "[ERROR] thread ${THREAD} no longer exists" >&2; exit 3; }
|
||||||
|
sleep "$POLL"; continue
|
||||||
|
fi
|
||||||
HUMAN="$(printf '%s' "$MSGS" | jq -c 'reverse[] | select(.author.bot|not) | {u:.author.username, c:.content}')"
|
HUMAN="$(printf '%s' "$MSGS" | jq -c 'reverse[] | select(.author.bot|not) | {u:.author.username, c:.content}')"
|
||||||
if [ -n "$HUMAN" ]; then
|
if [ -n "$HUMAN" ]; then
|
||||||
echo "[OK] answer received in thread ${THREAD}:"
|
echo "[OK] answer received in thread ${THREAD}:"
|
||||||
@@ -102,6 +109,7 @@ if [ "${1:-}" = "--wait" ]; then
|
|||||||
echo "THREAD_ID=${THREAD}"
|
echo "THREAD_ID=${THREAD}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
sleep "$POLL"
|
||||||
done
|
done
|
||||||
echo "[TIMEOUT] no human reply on thread ${THREAD} within ${TIMEOUT}s" >&2
|
echo "[TIMEOUT] no human reply on thread ${THREAD} within ${TIMEOUT}s" >&2
|
||||||
echo "THREAD_ID=${THREAD}" >&2
|
echo "THREAD_ID=${THREAD}" >&2
|
||||||
@@ -149,7 +157,18 @@ fi
|
|||||||
auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}")
|
auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}")
|
||||||
|
|
||||||
# --- create the forum post (thread) with the question ---
|
# --- create the forum post (thread) with the question ---
|
||||||
BODY="$(jq -nc --arg name "$TITLE" --arg c "$CONTENT" '{name:$name, message:{content:$c}}')"
|
# Discord caps message content at 2000 chars. Keep the starter <=1900 and send any
|
||||||
|
# overflow as follow-up messages in the created thread (rare, but don't hard-fail).
|
||||||
|
LIMIT_CHARS=1900
|
||||||
|
STARTER_CONTENT="$CONTENT"
|
||||||
|
OVERFLOW=""
|
||||||
|
if [ "${#CONTENT}" -gt "$LIMIT_CHARS" ]; then
|
||||||
|
STARTER_CONTENT="${CONTENT:0:$LIMIT_CHARS}"
|
||||||
|
nl="${STARTER_CONTENT%$'\n'*}"
|
||||||
|
[ "${#nl}" -lt "${#STARTER_CONTENT}" ] && [ "${#nl}" -gt 0 ] && STARTER_CONTENT="$nl"
|
||||||
|
OVERFLOW="${CONTENT:${#STARTER_CONTENT}}"; OVERFLOW="${OVERFLOW#$'\n'}"
|
||||||
|
fi
|
||||||
|
BODY="$(jq -nc --arg name "$TITLE" --arg c "$STARTER_CONTENT" '{name:$name, message:{content:$c}}')"
|
||||||
RESP="$(printf '%s' "$BODY" | curl -s -m 20 -w $'\n%{http_code}' "${auth[@]}" \
|
RESP="$(printf '%s' "$BODY" | curl -s -m 20 -w $'\n%{http_code}' "${auth[@]}" \
|
||||||
-X POST "$API/channels/${FORUM_ID}/threads" --data-binary @-)"
|
-X POST "$API/channels/${FORUM_ID}/threads" --data-binary @-)"
|
||||||
HTTP="$(printf '%s' "$RESP" | tail -n1)"
|
HTTP="$(printf '%s' "$RESP" | tail -n1)"
|
||||||
@@ -165,17 +184,29 @@ if [ -z "$THREAD" ]; then
|
|||||||
echo "[ERROR] posted but no thread id in response: $(printf '%s' "$JSON" | head -c 200)" >&2
|
echo "[ERROR] posted but no thread id in response: $(printf '%s' "$JSON" | head -c 200)" >&2
|
||||||
exit 3
|
exit 3
|
||||||
fi
|
fi
|
||||||
|
# --- post any overflow chunks as follow-up messages in the new thread ---
|
||||||
|
rest="$OVERFLOW"
|
||||||
|
while [ -n "$rest" ]; do
|
||||||
|
piece="${rest:0:$LIMIT_CHARS}"
|
||||||
|
nl="${piece%$'\n'*}"; [ "${#nl}" -lt "${#piece}" ] && [ "${#nl}" -gt 0 ] && piece="$nl"
|
||||||
|
printf '%s' "$(jq -nc --arg c "$piece" '{content:$c}')" | \
|
||||||
|
curl -s -m 15 "${auth[@]}" -X POST "$API/channels/${THREAD}/messages" --data-binary @- >/dev/null 2>&1
|
||||||
|
rest="${rest:${#piece}}"; rest="${rest#$'\n'}"
|
||||||
|
done
|
||||||
|
|
||||||
echo "[INFO] asked in #ct-forum (thread=${THREAD}) — waiting up to ${TIMEOUT}s for a human reply..." >&2
|
echo "[INFO] asked in #ct-forum (thread=${THREAD}) — waiting up to ${TIMEOUT}s for a human reply..." >&2
|
||||||
|
|
||||||
# --- block-poll the thread for the first NON-BOT reply ---
|
# --- block-poll the thread for the first NON-BOT reply ---
|
||||||
AFTER="${STARTER:-0}"
|
# starter message id == thread id for a forum post, so after=<starter> = all replies.
|
||||||
|
AFTER="${STARTER:-$THREAD}"
|
||||||
DEADLINE=$(( $(date +%s) + TIMEOUT ))
|
DEADLINE=$(( $(date +%s) + TIMEOUT ))
|
||||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||||
sleep "$POLL"
|
sleep "$POLL"
|
||||||
Q="after=${AFTER}"
|
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?after=${AFTER}")"
|
||||||
[ "$AFTER" = "0" ] && Q="limit=50"
|
if ! printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
||||||
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?${Q}")"
|
printf '%s' "$MSGS" | jq -e '.code==10003' >/dev/null 2>&1 && { echo "[ERROR] thread ${THREAD} disappeared" >&2; exit 3; }
|
||||||
printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1 || continue
|
continue
|
||||||
|
fi
|
||||||
# oldest-first; keep only human (non-bot) messages
|
# oldest-first; keep only human (non-bot) messages
|
||||||
HUMAN="$(printf '%s' "$MSGS" | jq -c 'reverse[] | select(.author.bot|not) | {u:.author.username, c:.content, id:.id}')"
|
HUMAN="$(printf '%s' "$MSGS" | jq -c 'reverse[] | select(.author.bot|not) | {u:.author.username, c:.content, id:.id}')"
|
||||||
if [ -n "$HUMAN" ]; then
|
if [ -n "$HUMAN" ]; then
|
||||||
|
|||||||
@@ -77,8 +77,25 @@ replies — then you surface the answer to the user **unprompted**. See
|
|||||||
the bot explicitly allowed; Mike sees it as owner. To let someone else answer, they need
|
the bot explicitly allowed; Mike sees it as owner. To let someone else answer, they need
|
||||||
**View Channel** on #ct-forum (grant a role like Techs, or the person). Winter/Rob are not
|
**View Channel** on #ct-forum (grant a role like Techs, or the person). Winter/Rob are not
|
||||||
in by default.
|
in by default.
|
||||||
- **Deleting threads** needs the bot to have **Manage Threads** on #ct-forum (owner grants
|
|
||||||
it). Without it, `DELETE /channels/<thread>` returns 403.
|
### Bot capability map — the limit of control (tested 2026-07-08)
|
||||||
|
What the bot can do in #ct-forum with its current permissions, and where the wall is:
|
||||||
|
|
||||||
|
| Bot CAN (no grant needed) | Bot CANNOT (needs a permission grant) |
|
||||||
|
|---|---|
|
||||||
|
| Read messages / thread / members / guild channels | **Pin** a message → 403 (needs *Manage Messages*) |
|
||||||
|
| Create a forum post (thread), post follow-up messages | **Delete** a thread → 403 (needs *Manage Threads*) |
|
||||||
|
| Edit its OWN messages; add/remove reactions; typing | **Archive** a thread → 403 (needs *Manage Threads*) |
|
||||||
|
| Rename / lock a thread it owns (see trap below) | **Unlock** a thread once locked → 403 (needs *Manage Threads*) |
|
||||||
|
|
||||||
|
- **One-way trap:** as thread owner the bot can *lock* a thread (200) but then **cannot
|
||||||
|
unlock or rename it** (403) — locking without *Manage Threads* is irreversible. So the
|
||||||
|
skill does NOT lock/rename threads; treat thread lifecycle as off-limits until granted.
|
||||||
|
- **To enable cleanup:** owner grants the ClaudeTools bot **Manage Threads** on #ct-forum
|
||||||
|
(Edit Channel → Permissions → bot → Manage Threads → Allow). Then delete/archive/unlock
|
||||||
|
work. **Manage Messages** additionally enables pin.
|
||||||
|
- Reactions + own-message edits DO work with no grant — usable for lightweight
|
||||||
|
acknowledgement (react to a reply, or edit the question to "[answered]") if ever wanted.
|
||||||
|
|
||||||
## Exit codes
|
## Exit codes
|
||||||
`0` answered · `1` usage · `2` no token · `3` Discord API error · `4` timeout (thread stays
|
`0` answered · `1` usage · `2` no token · `3` Discord API error · `4` timeout (thread stays
|
||||||
|
|||||||
@@ -130,6 +130,16 @@ erroring `a value is required for '--url'`. Push via **GuruRMM** (`/rmm`) or any
|
|||||||
remote-exec channel. `RegKey` auto-approves + adds to the key's target group; without
|
remote-exec channel. `RegKey` auto-approves + adds to the key's target group; without
|
||||||
it the agent registers but awaits manual approval.
|
it the agent registers but awaits manual approval.
|
||||||
|
|
||||||
|
**Offline endpoint whose GuruRMM agent is down but ScreenConnect is up:** the one-liner
|
||||||
|
is a single self-contained command, so queue it via the `screenconnect` skill's
|
||||||
|
`send-command --session <id> --command "powershell -NoProfile -ExecutionPolicy Bypass
|
||||||
|
-Command \"...Install-EDR...\"" --confirm` — SC holds it in the session's one command slot
|
||||||
|
and runs it on reconnect (verify by `agents --org` / `sweep` afterward; SC returns no
|
||||||
|
output). Used 2026-07-08 to stage REPAIRADMIN (IMC) while it was offline overnight.
|
||||||
|
**AV-swap ordering:** when replacing another AV (e.g. Bitdefender), install + verify EDR
|
||||||
|
live FIRST, then remove the old AV — never leave an unprotected window. Note Bitdefender's
|
||||||
|
API has NO uninstall (console-only); pull it via its local uninstall tool over `/rmm`.
|
||||||
|
|
||||||
## Safety gating
|
## Safety gating
|
||||||
|
|
||||||
- Reads never mutate and run without confirmation.
|
- Reads never mutate and run without confirmation.
|
||||||
|
|||||||
@@ -58,6 +58,37 @@ This is the headline use case - set a device for SC and have it land correctly:
|
|||||||
VERIFIED end-to-end on RMM-TEST-MACHINE 2026-06-22 (installed, self-tagged
|
VERIFIED end-to-end on RMM-TEST-MACHINE 2026-06-22 (installed, self-tagged
|
||||||
Company/Site/Tag, ran a command, re-tagged via set-properties).
|
Company/Site/Tag, ran a command, re-tagged via set-properties).
|
||||||
|
|
||||||
|
### Deploying the GuruRMM agent via SC (verified 2026-07-08)
|
||||||
|
|
||||||
|
Different from the SC-installer push above: to enroll a box that is in SC but NOT in GuruRMM,
|
||||||
|
`send-command` the server's own site-preconfigured one-liner. The site code is baked into the
|
||||||
|
downloaded signed binary (GRMM_CFG trailer) so it self-enrolls straight into that site — no
|
||||||
|
Staging, no reassign:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# site_code from GET /api/sites/<id>/install-info (e.g. Main = INNER-BRIDGE-8354)
|
||||||
|
CMD='powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; iex (irm '\''https://rmm.azcomputerguru.com/install/<SITE_CODE>/windows'\'')"'
|
||||||
|
$SC send-command --session <sessionID> --command "$CMD" --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
The install script auto-relaunches into native 64-bit PowerShell (handles SC's 32-bit command
|
||||||
|
runner via Sysnative). Verified: DESKTOP-NFU17AJ enrolled into IMC → Main this way 2026-07-08.
|
||||||
|
|
||||||
|
- **`send-command` returns `{}`** — SC gives no command output via the API (documented below).
|
||||||
|
It is queued, not confirmed. **Verify by enrollment**, not by the send response: poll
|
||||||
|
`rmm-search.sh -c <client>` for the new agent (pipe `--json` with `2>/dev/null` — the `[OK]
|
||||||
|
Authenticated` banner is on stderr; `2>&1 | jq` breaks jq).
|
||||||
|
- **Offline targets:** the one-liner is a single self-contained command, so it queues in the
|
||||||
|
session's one event slot and runs on reconnect. It will NOT run until the box is online.
|
||||||
|
- **Gotcha:** the install script calls `Get-CimInstance Win32_Processor`; a box with corrupt
|
||||||
|
WMI dies there (bit CP-QB). A box that black-holes outbound TLS installs but never connects
|
||||||
|
(bit IMC-PRINTSERVER) — SC still works because its relay is not TLS.
|
||||||
|
- **CP values are under `.CustomProperties.CustomPropertyN` / `.CustomPropertyValues[]`** on the
|
||||||
|
raw session object (CP1=Company, CP2=Site, ...). `--company "X"` matches CP1 **exactly**, so
|
||||||
|
pass the full tagged value (IMC's CP1 is `IMC - Instrumental Music Center`, not `Instrumental
|
||||||
|
Music Center`) or enumerate with `--like` / a raw `CustomProperty1 = '...'` filter first.
|
||||||
|
SC truncates session Name to 15 chars — `IMC-M-EDSERVICE` (SC) is `IMC-M-EdServices1` (RMM).
|
||||||
|
|
||||||
## Method surface (probed live 2026-06-22)
|
## Method surface (probed live 2026-06-22)
|
||||||
|
|
||||||
**Available (CLI-exposed):**
|
**Available (CLI-exposed):**
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
|
|||||||
|
|
||||||
<!-- Append entries below this line -->
|
<!-- Append entries below this line -->
|
||||||
|
|
||||||
|
2026-07-08 | Howard-Home | bash/background-task | [friction] added shell '& ... disown' on top of run_in_background:true -> shell forks the long-running wait and exits 0 immediately, orphaning it; the blocking read never notifies. Twice this session. Fix: with run_in_background:true, run the command in the FOREGROUND of that shell (no trailing &, no disown) - the harness does the backgrounding. [ctx: ref=discord ask-forum.sh --wait]
|
||||||
|
|
||||||
|
2026-07-08 | Howard-Home | rmm/deploy-beast | [friction] cannot git-fetch Gitea submodule as SYSTEM via RMM: credential.helper=manager (GCM) fails with 'Must specify at least one OAuthAuthenticationModes' when no guru desktop session is logged on; new commit not fetchable, so remote SYSTEM deploy of the discord-bot is blocked. Restart-Service works as SYSTEM, but pull must run in guru context (interactive /sync on BEAST) first. [ctx: host=GURU-BEAST-ROG repo=acg-discord-bot]
|
||||||
|
|
||||||
|
2026-07-08 | Howard-Home | syncro/assets | [correction] assumed 'retire in Syncro' meant DELETE /customer_assets; correct is Syncro's Archive feature (UI-only, no API) and never delete — delete loses history + breaks ticket links [ctx: client=IMC ref=syncro.md#customer-assets]
|
||||||
|
|
||||||
2026-07-07 | Howard-Home | ps-encoded | encode produced empty output [ctx: src=scratch-recon.ps1] (x2)
|
2026-07-07 | Howard-Home | ps-encoded | encode produced empty output [ctx: src=scratch-recon.ps1] (x2)
|
||||||
|
|
||||||
2026-07-07 | Howard-Home | windows/scheduled-task | [friction] EDR Isolation Watcher task launched bash.exe directly (Hidden=False, every 10min) -> console window flashed on desktop; recurring class also hit GPS-RMM-Progress today. FIX: wscript VBS wrapper (style 0) for bash, pythonw+-Hidden for python; fixed both installers [ctx: ref=feedback_scheduled_task_no_console_flash host=Howard-Home task='ClaudeTools - EDR Isolation Watcher']
|
2026-07-07 | Howard-Home | windows/scheduled-task | [friction] EDR Isolation Watcher task launched bash.exe directly (Hidden=False, every 10min) -> console window flashed on desktop; recurring class also hit GPS-RMM-Progress today. FIX: wscript VBS wrapper (style 0) for bash, pythonw+-Hidden for python; fixed both installers [ctx: ref=feedback_scheduled_task_no_console_flash host=Howard-Home task='ClaudeTools - EDR Isolation Watcher']
|
||||||
|
|||||||
30
scratch-qbcopy.ps1
Normal file
30
scratch-qbcopy.ps1
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
$ErrorActionPreference='Continue'
|
||||||
|
$src='C:\Share\Quickbooks'
|
||||||
|
$dst='\\TPS-SVR\Share\Quickbooks'
|
||||||
|
$qbw='schoonerQB2025.QBW'
|
||||||
|
$tlg='schoonerQB2025.QBW.TLG'
|
||||||
|
|
||||||
|
"== stop QB hosting services (prevents re-lock during copy) =="
|
||||||
|
foreach($s in 'QuickBooksDB34','QBCFMonitorService'){ Stop-Service $s -Force -ErrorAction SilentlyContinue; Start-Sleep 1; "$s -> $((Get-Service $s -ErrorAction SilentlyContinue).Status)" }
|
||||||
|
Start-Sleep 3
|
||||||
|
|
||||||
|
try {
|
||||||
|
"== confirm company file is unlocked =="
|
||||||
|
try { $fs=[System.IO.File]::Open((Join-Path $src $qbw),'Open','Read','None'); $fs.Close(); "UNLOCKED - copying" }
|
||||||
|
catch { "STILL LOCKED: $($_.Exception.Message)" }
|
||||||
|
|
||||||
|
"== copy clean company file + transaction log =="
|
||||||
|
$out = robocopy $src $dst $qbw $tlg /COPY:DAT /R:2 /W:3 /NP /NJH
|
||||||
|
($out | Select-String 'schoonerQB2025|Bytes :|ERROR|New File') | Out-String
|
||||||
|
"robocopy exit code: $LASTEXITCODE (0-7 = success)"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
"== restart QB services (users resume on OLD server) =="
|
||||||
|
foreach($s in 'QBCFMonitorService','QuickBooksDB34'){ Start-Service $s -ErrorAction SilentlyContinue; Start-Sleep 1; "$s -> $((Get-Service $s -ErrorAction SilentlyContinue).Status)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
"== verify copied file matches source =="
|
||||||
|
$s=(Get-Item (Join-Path $src $qbw) -ErrorAction SilentlyContinue).Length
|
||||||
|
$d=(Get-Item (Join-Path $dst $qbw) -ErrorAction SilentlyContinue).Length
|
||||||
|
"SRC: $([math]::Round($s/1MB,1)) MB DST: $([math]::Round($d/1MB,1)) MB MATCH: $($s -eq $d -and $s -gt 0)"
|
||||||
|
"== DONE =="
|
||||||
41
scratch-qbwc.ps1
Normal file
41
scratch-qbwc.ps1
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
$ErrorActionPreference='SilentlyContinue'
|
||||||
|
|
||||||
|
"===== .QWC application files (define the connected app) ====="
|
||||||
|
$roots = @('C:\Share\Quickbooks','C:\ProgramData\Intuit','C:\Program Files\Common Files\Intuit','C:\Program Files (x86)\Common Files\Intuit','C:\Program Files (x86)\Intuit','C:\Program Files\Intuit')
|
||||||
|
$qwc = @()
|
||||||
|
# top-level of the QB data folder (where apps usually drop the .qwc)
|
||||||
|
$qwc += Get-ChildItem 'C:\Share\Quickbooks' -Filter *.qwc -ErrorAction SilentlyContinue
|
||||||
|
# recursive in the (small) profile + program locations
|
||||||
|
foreach($r in @('C:\Users','C:\ProgramData\Intuit','C:\Program Files\Common Files\Intuit','C:\Program Files (x86)\Common Files\Intuit')){
|
||||||
|
if(Test-Path $r){ $qwc += Get-ChildItem $r -Recurse -Filter *.qwc -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
$qwc = $qwc | Sort-Object FullName -Unique
|
||||||
|
if(-not $qwc){ "(no .qwc files in searched locations)" }
|
||||||
|
foreach($f in $qwc){
|
||||||
|
"### $($f.FullName) (modified $($f.LastWriteTime))"
|
||||||
|
try { [xml]$x = Get-Content $f.FullName -Raw
|
||||||
|
" AppName : $($x.QBWCXML.AppName)"
|
||||||
|
" AppURL : $($x.QBWCXML.AppURL)"
|
||||||
|
" AppSupport : $($x.QBWCXML.AppSupport)"
|
||||||
|
" AppDescription: $($x.QBWCXML.AppDescription)"
|
||||||
|
" UserName : $($x.QBWCXML.UserName)"
|
||||||
|
} catch { " raw:"; (Get-Content $f.FullName -Raw) }
|
||||||
|
}
|
||||||
|
|
||||||
|
"`n===== Web Connector data dirs (per-user) ====="
|
||||||
|
Get-ChildItem 'C:\Users\*\AppData\Local\Intuit\QBWebConnector' -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
"-- $($_.FullName) --"
|
||||||
|
Get-ChildItem $_.FullName -Recurse -ErrorAction SilentlyContinue | Select-Object @{n='KB';e={[math]::Round($_.Length/1KB,1)}},FullName,LastWriteTime | Format-Table -Auto -Wrap | Out-String
|
||||||
|
}
|
||||||
|
|
||||||
|
"`n===== Web Connector LOG - app names / URLs it talks to ====="
|
||||||
|
$logs = Get-ChildItem 'C:\Users\*\AppData\Local\Intuit\QBWebConnector','C:\ProgramData\Intuit\QBWebConnector' -Recurse -Include *.txt,*.log -ErrorAction SilentlyContinue
|
||||||
|
foreach($l in $logs){
|
||||||
|
"### $($l.FullName)"
|
||||||
|
Select-String -Path $l.FullName -Pattern 'AppName|App Name|AppURL|Connecting|http' -ErrorAction SilentlyContinue | ForEach-Object { $_.Line.Trim() } | Sort-Object -Unique | Select-Object -Last 25 | Out-String
|
||||||
|
}
|
||||||
|
|
||||||
|
"`n===== registry-registered Web Connector apps ====="
|
||||||
|
reg query "HKLM\SOFTWARE\Intuit\QBWebConnector" /s 2>$null
|
||||||
|
reg query "HKLM\SOFTWARE\WOW6432Node\Intuit\QBWebConnector" /s 2>$null
|
||||||
|
"===== DONE ====="
|
||||||
Reference in New Issue
Block a user