From e34f51fe5d430e3207e32c219b7b72987fb29124 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Mon, 30 Mar 2026 19:38:38 -0700 Subject: [PATCH] Session 2026-03-30: SOPS vault, SC-Syncro sync, Syncro scripts - SOPS+age credential vault created (59 encrypted files, separate repo) - Updated CLAUDE.md credential access to reference SOPS vault - Updated memory for ACG-5070 (Windows 11, replaces CachyOS) - SC-Syncro sync script: enriched 410 SC sessions with company/device data - Syncro scripts: SC property updater, SC deployer, rogue SC killer - Session log with full details Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/CLAUDE.md | 44 +- .claude/memory/MEMORY.md | 4 +- .claude/memory/reference_workstation_setup.md | 47 +- scripts/sync-sc-from-syncro.js | 408 ++++++++++++++++ scripts/syncro-deploy-sc.ps1 | 60 +++ scripts/syncro-kill-rogue-sc.ps1 | 439 ++++++++++++++++++ scripts/syncro-update-sc-properties.ps1 | 94 ++++ session-logs/2026-03-30-session.md | 215 +++++++++ 8 files changed, 1274 insertions(+), 37 deletions(-) create mode 100644 scripts/sync-sc-from-syncro.js create mode 100644 scripts/syncro-deploy-sc.ps1 create mode 100644 scripts/syncro-kill-rogue-sc.ps1 create mode 100644 scripts/syncro-update-sc-properties.ps1 create mode 100644 session-logs/2026-03-30-session.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 78e79cb..9b31c24 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -39,9 +39,9 @@ You are NOT an executor. You coordinate specialized agents and preserve your con ## Key Rules - **NO EMOJIS** - Use ASCII markers: `[OK]`, `[ERROR]`, `[WARNING]`, `[SUCCESS]`, `[INFO]` -- **No hardcoded credentials** - Use 1Password (`op read "op://Vault/Item/field"`) or encrypted storage +- **No hardcoded credentials** - Use SOPS vault (`vault get-field `) or 1Password as fallback - **SSH:** Use system OpenSSH (on Windows: `C:\Windows\System32\OpenSSH\ssh.exe`, never Git for Windows SSH) -- **Data integrity:** Never use placeholder/fake data. Check credentials.md (op:// refs) or 1Password or ask user. +- **Data integrity:** Never use placeholder/fake data. Check SOPS vault, credentials.md, or ask user. - **Full coding standards:** `.claude/CODING_GUIDELINES.md` (agents read on-demand, not every session) --- @@ -57,22 +57,46 @@ You are NOT an executor. You coordinate specialized agents and preserve your con ## Context Recovery When user references previous work, use `/context` command. Never ask user for info in: -- `credentials.md` - Infrastructure reference with `op://` paths (secrets in 1Password) +- `credentials.md` - Infrastructure reference (being migrated to SOPS vault at D:\vault) - `session-logs/` - Daily work logs (also in `projects/*/session-logs/` and `clients/*/session-logs/`) - `SESSION_STATE.md` - Project history -### 1Password Credential Access +### Credential Access (SOPS Vault - Primary) -Credentials are stored in 1Password across 4 vaults: **Infrastructure**, **Clients**, **Projects**, **MSP Tools**. +Credentials are stored in SOPS+age encrypted YAML files in a dedicated Gitea repo. -**To read a secret:** `op read "op://VaultName/ItemTitle/field_name"` +**Vault repo:** `D:\vault` (git.azcomputerguru.com/azcomputerguru/vault, private) +**Structure:** infrastructure/, clients/, services/, projects/, msp-tools/ -**Service account (non-interactive):** Set `OP_SERVICE_ACCOUNT_TOKEN` env var. Token stored in `op://Infrastructure/Service Account Auth Token: Agentic_Cli/credential`. The service account has Read & Write on all 4 vaults (except Projects which is read-only -- use desktop app auth for Projects writes). +**To read credentials:** +```bash +# Search by keyword (no decryption needed - metadata is plaintext) +bash D:/vault/scripts/vault.sh search "172.16.3.30" + +# Get a specific field +bash D:/vault/scripts/vault.sh get-field infrastructure/gururmm-server.sops.yaml credentials.password + +# Decrypt full entry +bash D:/vault/scripts/vault.sh get infrastructure/gururmm-server.sops.yaml + +# List all entries +bash D:/vault/scripts/vault.sh list +``` + +**Encryption:** AES-256 via age. Metadata (kind, name, host, tags) stays plaintext for searchability. Only `credentials`, `notes`, and secret fields are encrypted. + +**age key location:** `%APPDATA%\sops\age\keys.txt` (Windows) / `~/.config/sops/age/keys.txt` (Linux/Mac) **Setup on new machines:** -1. Install 1Password CLI: https://developer.1password.com/docs/cli/get-started/ -2. Sign in: `op signin` (or use desktop app integration) -3. For non-interactive use, add to shell config: `set -gx OP_SERVICE_ACCOUNT_TOKEN "token_value"` +1. Install: `winget install Mozilla.sops FiloSottile.age MikeFarah.yq` (or brew/pacman) +2. Generate key: `age-keygen -o ~/.config/sops/age/keys.txt` +3. Clone: `git clone git@git.azcomputerguru.com:azcomputerguru/vault.git` +4. Add public key to `keys/recipients.txt`, push, then run `vault rotate` from existing machine + +### 1Password (Fallback) + +1Password remains available for browser autofill and as fallback. Service account token is in the vault: +`bash D:/vault/scripts/vault.sh get-field infrastructure/1password-service-account.sops.yaml credentials.token` --- diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 1e48742..951675c 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -6,7 +6,7 @@ - [IX Server SSH Access](reference_ix_server_ssh.md) - SSH access notes, no key auth from CachyOS workstation yet - [IX Access via Tailscale](reference_ix_access_tailscale.md) - IX server accessible with Tailscale on, no VPN needed - [Neptune Access via D2TESTNAS](reference_neptune_access_d2testnas.md) - Neptune must be routed through D2TESTNAS -- [CachyOS Workstation Setup](reference_workstation_setup.md) - Dual NVMe, autostart apps, key fixes applied, old home location +- [ACG-5070 Workstation](reference_workstation_setup.md) - Windows 11, replaced CachyOS. SOPS vault, Ollama, all dev tools. - [Matomo Analytics](reference_matomo_analytics.md) - Self-hosted analytics at analytics.azcomputerguru.com, site IDs, tracking for all 3 sites - [Dataforth Contact - AJ](reference_dataforth_contact.md) - AJ at Dataforth, dataforthgit@ email forwarding to him @@ -15,7 +15,7 @@ - [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) - Set permissions.defaultMode to bypassPermissions in settings.json on all machines ## Machine -- [Windows GURU-BEAST-ROG Setup](machine_windows_guru_setup_status.md) - Fully configured: Node.js, Ollama (qwen3:14b, nomic-embed-text), GrepAI, MCP servers. Pending: codestral:22b pull +- [ACG-5070 Workstation Setup](reference_workstation_setup.md) - Windows 11 Pro clean install 2026-03-30, replaced CachyOS. All tools installed. ## Project - [Audio Processor Architecture](project_audio_processor_architecture.md) - Segment-first pipeline: detect breaks before transcription for complete content capture diff --git a/.claude/memory/reference_workstation_setup.md b/.claude/memory/reference_workstation_setup.md index 5e270fe..a2a9342 100644 --- a/.claude/memory/reference_workstation_setup.md +++ b/.claude/memory/reference_workstation_setup.md @@ -1,35 +1,32 @@ --- -name: CachyOS Workstation Setup -description: Current workstation config - CachyOS on ASUS laptop, dual NVMe, autostart apps, old home btrfs subvolume location +name: ACG-5070 Workstation Setup +description: Primary workstation ACG-5070 (Windows 11 Pro), clean install 2026-03-30. Replaced CachyOS. type: reference --- -## Workstation: acg-guru-5070 +## Workstation: ACG-5070 -- **OS:** CachyOS (Arch-based), kernel 6.19.x -- **DE:** KDE Plasma 6 (Wayland) -- **CPU/GPU:** Intel Arrow Lake-S + NVIDIA RTX 5070 Ti Mobile -- **Tailscale IP:** 100.95.216.79 +- **OS:** Windows 11 Pro (clean install 2026-03-30) +- **Previous OS:** CachyOS Linux (gone, replaced by Windows) +- **Hardware:** ASUS laptop, Intel Arrow Lake-S + NVIDIA RTX 5070 Ti Mobile, dual NVMe -### Storage -- **nvme0n1:** 954GB btrfs - CachyOS install (OS, root) -- **nvme1n1:** 954GB ext4 - `/home` (formatted from old Windows drive) -- **Old home:** btrfs `@home` subvolume on nvme0n1, mount with: `sudo mount -o subvol=@home UUID=8a8b1d34-99fb-470f-82ca-b5d08e43ec32 /mnt/old-home` +### Installed Tools +- Node.js v24.14.1, npm 11.11.0 +- Git 2.53.0, Python 3.14.3 +- 1Password CLI 2.33.1 (desktop app integration) +- Ollama 0.18.3 (models on D:\OllamaModels: qwen3:14b, codestral:22b, nomic-embed-text) +- Claude Code 2.1.87 +- sops 3.7.3, age 1.3.1, yq 4.52.5 +- jq, curl, Windows OpenSSH +- Missing: gh (GitHub CLI) -### Autostart Apps (~/.config/autostart/) -- `arch-update-tray.desktop` (pre-existing) -- `cachyos-hello.desktop` (pre-existing) -- `discord.desktop` (added, starts minimized) -- `tailscale-systray.desktop` (added) -- ScreenConnect: autostart removed (on-demand only via URI scheme handler from web UI) +### SOPS Vault +- age key: %APPDATA%\sops\age\keys.txt +- Vault repo: D:\vault (git.azcomputerguru.com/azcomputerguru/vault) +- 1Password backup: "age Key - ACG-5070 (Windows)" in Infrastructure vault -### Known Issues -- **Warm reboot hangs:** Rebooting (e.g. for GPU issues) causes system to hang with spinning symbol — requires hard power-off. Observed multiple times. Likely NVIDIA driver not unloading cleanly during shutdown. - -### Key Fixes Applied -- **Tailscale:** `--accept-routes`, systemd-resolved + NetworkManager DNS config -- **Brightness:** Hide nvidia_0 backlight via udev rule, KDE controls intel_backlight only -- **ScreenConnect:** dpkg + full JRE + Wayland patch (GDK_BACKEND=x11) -- **Sudo:** NOPASSWD for guru user +### Other Machines +- GURU-BEAST-ROG (Windows 11) -- needs vault setup (sops, age, yq, clone repo, generate age key, rotate) +- Mikes-MacBook-Air (macOS) -- needs vault setup **How to apply:** Reference when troubleshooting workstation issues or setting up additional services. diff --git a/scripts/sync-sc-from-syncro.js b/scripts/sync-sc-from-syncro.js new file mode 100644 index 0000000..181d54b --- /dev/null +++ b/scripts/sync-sc-from-syncro.js @@ -0,0 +1,408 @@ +#!/usr/bin/env node +'use strict'; + +const https = require('https'); +const { execSync } = require('child_process'); +const { URL } = require('url'); + +// --------------------------------------------------------------------------- +// CLI argument parsing +// --------------------------------------------------------------------------- + +const args = process.argv.slice(2); +const FLAG_FORCE = args.includes('--force'); +const FLAG_DRY_RUN = args.includes('--dry-run') || !FLAG_FORCE; +const FLAG_VERBOSE = args.includes('--verbose'); + +if (args.includes('--help') || args.includes('-h')) { + console.log(`Usage: node sync-sc-from-syncro.js [--dry-run] [--force] [--verbose] + +Options: + --dry-run List what would be updated without making SC API calls (default) + --force Actually perform the ScreenConnect updates + --verbose Show detailed output for each asset + --help Show this help message + +Without --force, the script runs in dry-run mode.`); + process.exit(0); +} + +// --------------------------------------------------------------------------- +// Credential loading via vault +// --------------------------------------------------------------------------- + +const VAULT_PATH = process.env.VAULT_PATH || 'D:/vault'; + +function vaultGet(file, field) { + try { + const result = execSync( + `bash "${VAULT_PATH}/scripts/vault.sh" get-field "${file}" "${field}"`, + { encoding: 'utf8', timeout: 30000 } + ); + return result.trim(); + } catch (err) { + console.error(`[ERROR] Failed to read vault field "${field}" from "${file}": ${err.message}`); + process.exit(1); + } +} + +let SYNCRO_API_KEY; +let SC_SECRET; + +function loadCredentials() { + console.log('Loading credentials from vault...'); + SYNCRO_API_KEY = vaultGet('msp-tools/syncro.sops.yaml', 'credentials.credential'); + SC_SECRET = vaultGet('msp-tools/screenconnect.sops.yaml', 'credentials.api_secret'); + + if (!SYNCRO_API_KEY) { + console.error('[ERROR] Syncro API key is empty.'); + process.exit(1); + } + if (!SC_SECRET) { + console.error('[ERROR] ScreenConnect API secret is empty.'); + process.exit(1); + } + console.log('Credentials loaded successfully.'); +} + +// --------------------------------------------------------------------------- +// HTTP helpers (built-in https module) +// --------------------------------------------------------------------------- + +/** + * Make an HTTPS request and return the parsed JSON response. + * @param {object} options + * @param {string} options.url - Full URL + * @param {string} [options.method='GET'] - HTTP method + * @param {object} [options.headers={}] - Request headers + * @param {string|null} [options.body=null] - Request body (string) + * @returns {Promise<{statusCode: number, data: any}>} + */ +function httpsRequest({ url, method = 'GET', headers = {}, body = null }) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const reqOptions = { + hostname: parsed.hostname, + port: parsed.port || 443, + path: parsed.pathname + parsed.search, + method, + headers, + }; + + const req = https.request(reqOptions, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const raw = Buffer.concat(chunks).toString('utf8'); + let data; + try { + data = JSON.parse(raw); + } catch (_) { + data = raw; + } + resolve({ statusCode: res.statusCode, data }); + }); + }); + + req.on('error', (err) => reject(err)); + req.setTimeout(30000, () => { + req.destroy(new Error('Request timed out')); + }); + + if (body !== null) { + const buf = Buffer.from(body, 'utf8'); + req.setHeader('Content-Length', buf.length); + req.write(buf); + } + req.end(); + }); +} + +/** + * Sleep for a given number of milliseconds. + * @param {number} ms + * @returns {Promise} + */ +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// --------------------------------------------------------------------------- +// Syncro API - paginate all assets +// --------------------------------------------------------------------------- + +/** + * Fetch all customer assets from Syncro, paginating until no more results. + * @returns {Promise>} + */ +async function fetchAllSyncroAssets() { + const baseUrl = 'https://computerguru.syncromsp.com/api/v1/customer_assets'; + const perPage = 100; + let page = 1; + const results = []; + let totalRaw = 0; + + console.log('Fetching Syncro assets...'); + + while (true) { + const url = `${baseUrl}?page=${page}&per_page=${perPage}`; + if (FLAG_VERBOSE) { + console.log(` Fetching page ${page}...`); + } + + let response; + try { + response = await httpsRequest({ + url, + method: 'GET', + headers: { + 'Authorization': SYNCRO_API_KEY, + 'Accept': 'application/json', + }, + }); + } catch (err) { + console.error(`[ERROR] Syncro API request failed on page ${page}: ${err.message}`); + process.exit(1); + } + + if (response.statusCode !== 200) { + console.error(`[ERROR] Syncro API returned status ${response.statusCode} on page ${page}.`); + if (typeof response.data === 'string') { + console.error(` Response: ${response.data.substring(0, 500)}`); + } + process.exit(1); + } + + const assets = response.data.assets || response.data; + + if (!Array.isArray(assets) || assets.length === 0) { + if (FLAG_VERBOSE) { + console.log(` Page ${page} returned 0 assets, done paginating.`); + } + break; + } + + totalRaw += assets.length; + + for (const asset of assets) { + const props = asset.properties || {}; + const scGuid = props['ScreenConnect GUID'] || ''; + if (!scGuid) { + continue; + } + + const customer = asset.customer || {}; + results.push({ + scGuid, + company: customer.business_then_name || '', + deviceName: asset.name || 'Unknown', + customerId: customer.id || 0, + deviceType: (props.form_factor || '').replace(/^Physical /, ''), + }); + } + + if (FLAG_VERBOSE) { + console.log(` Page ${page}: ${assets.length} assets (${results.length} with SC GUID so far)`); + } + + page++; + } + + console.log(`Fetched ${totalRaw} total assets, ${results.length} have ScreenConnect GUIDs.`); + return { totalRaw, assets: results }; +} + +// --------------------------------------------------------------------------- +// ScreenConnect API - read session custom properties +// --------------------------------------------------------------------------- + +/** + * Read current session custom properties from ScreenConnect to check tagging. + * Uses the same extension endpoint with GetSessionDetails or similar. + * + * Note: ScreenConnect does not have a clean REST API for reading session + * properties by GUID through the extension endpoint. We attempt to read + * session details. If reading fails or is unsupported, we return null + * and let the caller decide whether to update. + * + * @param {string} scGuid + * @returns {Promise} Array of custom property values, or null if unreadable + */ +async function readScSessionProperties(scGuid) { + const url = 'https://computerguru.screenconnect.com/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx/GetSessionDetails'; + + try { + const response = await httpsRequest({ + url, + method: 'POST', + headers: { + 'CTRLAuthHeader': SC_SECRET, + 'Content-Type': 'application/json', + 'Origin': 'https://computerguru.screenconnect.com', + }, + body: JSON.stringify([scGuid]), + }); + + if (response.statusCode === 200 && response.data) { + // The response structure may vary; attempt to extract custom properties + const session = response.data; + if (Array.isArray(session.CustomPropertyValues)) { + return session.CustomPropertyValues; + } + if (Array.isArray(session)) { + const first = session[0]; + if (first && Array.isArray(first.CustomPropertyValues)) { + return first.CustomPropertyValues; + } + } + } + } catch (_) { + // Reading failed - will attempt update anyway (unless --force is off) + } + + return null; +} + +// --------------------------------------------------------------------------- +// ScreenConnect API - update session custom properties +// --------------------------------------------------------------------------- + +/** + * Update ScreenConnect session custom properties for a given GUID. + * @param {string} scGuid + * @param {string} company + * @param {string} site + * @returns {Promise<{success: boolean, error?: string}>} + */ +async function updateScSession(scGuid, company, deviceType) { + const url = 'https://computerguru.screenconnect.com/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx/UpdateSessionCustomProperties'; + + // CP1=Company, CP2=Site (blank), CP3=Department (blank), CP4=Device Type, CP5=Tag, CP6-8=blank + const bodyPayload = [ + scGuid, + [company, '', '', deviceType, 'Syncro-Matched', '', '', ''], + ]; + + try { + const response = await httpsRequest({ + url, + method: 'POST', + headers: { + 'CTRLAuthHeader': SC_SECRET, + 'Content-Type': 'application/json', + 'Origin': 'https://computerguru.screenconnect.com', + }, + body: JSON.stringify(bodyPayload), + }); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return { success: true }; + } + + const detail = typeof response.data === 'string' + ? response.data.substring(0, 200) + : JSON.stringify(response.data).substring(0, 200); + return { success: false, error: `HTTP ${response.statusCode}: ${detail}` }; + } catch (err) { + return { success: false, error: err.message }; + } +} + +// --------------------------------------------------------------------------- +// Main processing +// --------------------------------------------------------------------------- + +async function main() { + console.log('=== SC-Syncro Session Sync ==='); + console.log(`Mode: ${FLAG_FORCE ? 'LIVE (--force)' : 'DRY-RUN'}`); + console.log(''); + + loadCredentials(); + console.log(''); + + const { totalRaw, assets } = await fetchAllSyncroAssets(); + console.log(''); + + const stats = { + totalSyncroAssets: totalRaw, + assetsWithScGuid: assets.length, + alreadyTagged: 0, + updated: 0, + errors: 0, + wouldUpdate: 0, + }; + + for (let i = 0; i < assets.length; i++) { + const asset = assets[i]; + const shortGuid = asset.scGuid.length > 12 + ? asset.scGuid.substring(0, 12) + '...' + : asset.scGuid; + + // Check if already tagged + const currentProps = await readScSessionProperties(asset.scGuid); + if (currentProps !== null && currentProps.length >= 5 && currentProps[4] === 'Syncro-Matched') { + stats.alreadyTagged++; + if (FLAG_VERBOSE) { + console.log(`[SKIP] ${asset.deviceName} (${shortGuid}) - already tagged`); + } + await sleep(100); + continue; + } + + if (FLAG_DRY_RUN && !FLAG_FORCE) { + stats.wouldUpdate++; + if (FLAG_VERBOSE) { + console.log(`[DRY-RUN] ${asset.deviceName} (${shortGuid}) -> Company: "${asset.company}", Type: "${asset.deviceType}"`); + } + await sleep(100); + continue; + } + + // Perform the update + const result = await updateScSession(asset.scGuid, asset.company, asset.deviceType); + + if (result.success) { + stats.updated++; + if (FLAG_VERBOSE) { + console.log(`[UPDATE] ${asset.deviceName} (${shortGuid}) -> Company: "${asset.company}", Type: "${asset.deviceType}"`); + } + } else { + stats.errors++; + if (FLAG_VERBOSE) { + console.log(`[ERROR] ${asset.deviceName} (${shortGuid}): ${result.error}`); + } + } + + // Rate limiting: 200ms between SC API calls + await sleep(200); + + // Progress indicator for non-verbose mode + if (!FLAG_VERBOSE && (i + 1) % 50 === 0) { + console.log(` Progress: ${i + 1}/${assets.length} processed...`); + } + } + + // Print summary + console.log(''); + console.log('SC-Syncro Sync Results:'); + console.log(` Total Syncro assets: ${stats.totalSyncroAssets}`); + console.log(` Assets with SC GUID: ${stats.assetsWithScGuid}`); + console.log(` Already tagged (skipped): ${stats.alreadyTagged}`); + + if (FLAG_DRY_RUN && !FLAG_FORCE) { + console.log(` Would update: ${stats.wouldUpdate}`); + console.log(''); + console.log('Run with --force to apply updates.'); + } else { + console.log(` Updated: ${stats.updated}`); + console.log(` Errors: ${stats.errors}`); + } +} + +main().catch((err) => { + console.error(`[FATAL] Unhandled error: ${err.message}`); + if (err.stack) { + console.error(err.stack); + } + process.exit(1); +}); diff --git a/scripts/syncro-deploy-sc.ps1 b/scripts/syncro-deploy-sc.ps1 new file mode 100644 index 0000000..5ebf6d0 --- /dev/null +++ b/scripts/syncro-deploy-sc.ps1 @@ -0,0 +1,60 @@ +# Syncro Script: Install ScreenConnect with Company Properties +# +# Syncro Platform Variables (set in script editor): +# $MachineName - platform - {{asset_name}} +# $OrgName - platform - {{customer_business_name_or_customer_name}} +# +# Required File: +# ScreenConnect.ClientSetup.msi -> c:\screenconnectinstaller.msi +# (Download from SC Admin > Build Installer > Access > Windows MSI) +# +# File Type: PowerShell | Run as: System | Max Run Time: 10 minutes + +Import-Module $env:SyncroModule + +$InstallerLocation = 'C:\screenconnectinstaller.msi' + +# URL-encode spaces in names for SERVICE_ARGUMENTS +$MachineName = $MachineName -replace '\s','%20' +$OrgName = $OrgName -replace '\s','%20' + +# Detect device type from chassis +$DeviceType = "Desktop" +try { + $chassis = (Get-CimInstance -ClassName Win32_SystemEnclosure).ChassisTypes + if ($chassis | Where-Object { $_ -in @(8,9,10,11,12,14,18,21,31,32) }) { $DeviceType = "Laptop" } + if ($chassis | Where-Object { $_ -in @(17,23) }) { $DeviceType = "Server" } + $model = (Get-CimInstance -ClassName Win32_ComputerSystem).Model + if ($model -match "Virtual|VMware|VirtualBox|Hyper-V|KVM|Xen") { $DeviceType = "Virtual%20$DeviceType" } +} catch {} + +# Check if already installed +$SCService = Get-Service -Name "ScreenConnect Client*" -ErrorAction SilentlyContinue +if ($SCService) { + Write-Host "ScreenConnect already installed: $($SCService.Name)" + Log-Activity -Message "SC Deploy: Already installed ($($SCService.Name))" -EventName "ScreenConnect" + exit 0 +} + +# Build SERVICE_ARGUMENTS with custom properties +# c=Company&c=Site&c=Department&c=DeviceType&c=Tag&c=&c=&c= +$ServiceArgs = "SERVICE_ARGUMENTS=""?e=Access&y=Guest&h=computerguru.screenconnect.com&p=443&k=BgIAAACkAABSU0ExAAgAAAEAAQCZmJAsjX2QoAvu/VF8SV7ggAxARlSj/TwgdqA8NcZgw9q+6G/FWwABU2WOeGPvRu6rA+sECP+u11d1BOp16iWA+KbkJPT93TIctreTy/BegdplEL5Bq0L3ZJcim++PLZjwYLDaIotdnOl+24JqkV75DxC1MV9dKNkz5DqS1+jNVMBvpOLY8UgPc9Io71pNIMo/rakJNlT4ofNeJiKIfuwRtgNNYKb51vSHGyFPYtHVNjDNYlJeu320yNJdN0zWwSQst/2GR3hAX8SnzJcZeROZ3HJuJc63uT0KS4ie4+4ExKaUimtfl8oAqIp4vBwiEXhm8T5RKhx9hLiJj/5shza8&c=$OrgName&c=&c=&c=$DeviceType&c=Syncro-Deploy&c=&c=&c=""" + +$installparams = '/i', $InstallerLocation, $ServiceArgs +$uninstallparams = '/uninstall', $InstallerLocation, '/qb' + +# Install +Write-Host "Installing ScreenConnect for $OrgName ($DeviceType)..." +$exitCode = (Start-Process -FilePath msiexec.exe -ArgumentList $installparams -Wait -Passthru).ExitCode + +if ($exitCode -eq 0 -or $exitCode -eq 3010) { + Write-Host "ScreenConnect installed successfully (exit code: $exitCode)" + Log-Activity -Message "SC Deploy: Installed for '$OrgName' ($DeviceType)" -EventName "ScreenConnect" +} else { + Write-Host "Installation failed with exit code: $exitCode" + Log-Activity -Message "SC Deploy FAILED: exit code $exitCode" -EventName "ScreenConnect" + exit 1 +} + +# Cleanup +Remove-Item -Path $InstallerLocation -Force -ErrorAction SilentlyContinue diff --git a/scripts/syncro-kill-rogue-sc.ps1 b/scripts/syncro-kill-rogue-sc.ps1 new file mode 100644 index 0000000..a4a3cb6 --- /dev/null +++ b/scripts/syncro-kill-rogue-sc.ps1 @@ -0,0 +1,439 @@ +# Syncro Script: Find and Remove Rogue ScreenConnect Instances +# +# Detects ScreenConnect/ConnectWise Control services that do NOT connect +# to our instance (computerguru.screenconnect.com). Collects forensic +# evidence, creates a security ticket with attachments, then removes +# the unauthorized instance. +# +# Syncro Platform Variables (set in script editor): +# $OrgName - platform - {{customer_business_name_or_customer_name}} +# +# File Type: PowerShell | Run as: System | Max Run Time: 10 minutes + +Import-Module $env:SyncroModule + +$OurHost = "computerguru.screenconnect.com" +$Found = 0 +$Killed = 0 +$EvidenceDir = "$env:TEMP\RogueSC_$(Get-Date -Format 'yyyyMMdd_HHmmss')" +$RogueInstances = @() + +Write-Host "=== Rogue ScreenConnect Detection ===" +Write-Host "Legitimate host: $OurHost" +Write-Host "" + +# ============================================================================ +# Find all ScreenConnect services +# ============================================================================ + +$scServices = Get-Service -Name "ScreenConnect Client*" -ErrorAction SilentlyContinue + +if (-not $scServices) { + Write-Host "No ScreenConnect services found." + exit 0 +} + +foreach ($svc in $scServices) { + $serviceName = $svc.Name + Write-Host "Found service: $serviceName (Status: $($svc.Status))" + + # Get the ImagePath from registry to find the install location + $regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName" + $imagePath = (Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue).ImagePath + + if (-not $imagePath) { + Write-Host " [WARNING] Could not read ImagePath - skipping" + continue + } + + # Clean up the image path (remove quotes, get directory) + $exePath = ($imagePath -replace '"', '').Split(' ')[0] + $installDir = Split-Path -Parent $exePath + + # Check the service launch parameters for the host + $connectedHost = "unknown" + + # Method 1: Check registry ImagePath for host parameter + if ($imagePath -match 'h=([^&\s"]+)') { + $connectedHost = $Matches[1] + } + + # Method 2: Check config/xml files in install directory + if ($connectedHost -eq "unknown" -and (Test-Path $installDir)) { + foreach ($pattern in @("*.xml", "*.config")) { + Get-ChildItem -Path $installDir -Filter $pattern -ErrorAction SilentlyContinue | ForEach-Object { + $content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue + if ($content -match 'h=([^&\s<"]+)') { + $connectedHost = $Matches[1] + } + } + if ($connectedHost -ne "unknown") { break } + } + } + + # Method 3: Check binary strings + if ($connectedHost -eq "unknown" -and (Test-Path $exePath)) { + try { + $content = [System.IO.File]::ReadAllText($exePath) + if ($content -match 'h=([a-zA-Z0-9\.\-]+\.screenconnect\.com)') { + $connectedHost = $Matches[1] + } elseif ($content -match 'h=([a-zA-Z0-9\.\-]+)') { + $connectedHost = $Matches[1] + } + } catch {} + } + + Write-Host " Connected to: $connectedHost" + + # Determine if this is ours + if ($connectedHost -like "*$OurHost*" -or $connectedHost -eq $OurHost) { + Write-Host " [OK] This is our instance - keeping." + continue + } + + # ====================================================================== + # ROGUE INSTANCE DETECTED - Collect forensic evidence before removal + # ====================================================================== + + $Found++ + Write-Host " [ROGUE] Unauthorized ScreenConnect instance!" + Write-Host " Collecting forensic evidence..." + + # Create evidence directory + if (-not (Test-Path $EvidenceDir)) { + New-Item -ItemType Directory -Path $EvidenceDir -Force | Out-Null + } + + $instanceDir = Join-Path $EvidenceDir "instance_$Found" + New-Item -ItemType Directory -Path $instanceDir -Force | Out-Null + + # --- Evidence Collection --- + + $evidence = [ordered]@{ + Timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC) + ComputerName = $env:COMPUTERNAME + ServiceName = $serviceName + ServiceStatus = $svc.Status.ToString() + ServiceStartType = $svc.StartType.ToString() + ConnectedHost = $connectedHost + ImagePath = $imagePath + InstallDirectory = $installDir + ExePath = $exePath + } + + # Service account info + $svcWmi = Get-CimInstance Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue + if ($svcWmi) { + $evidence["ServiceAccount"] = $svcWmi.StartName + $evidence["ServicePID"] = $svcWmi.ProcessId + $evidence["ServicePath"] = $svcWmi.PathName + } + + # File details + if (Test-Path $exePath) { + $fileInfo = Get-Item $exePath -ErrorAction SilentlyContinue + $evidence["ExeSize"] = "$([math]::Round($fileInfo.Length/1KB, 2)) KB" + $evidence["ExeCreated"] = $fileInfo.CreationTimeUtc.ToString("yyyy-MM-dd HH:mm:ss UTC") + $evidence["ExeModified"] = $fileInfo.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss UTC") + + # Digital signature + $sig = Get-AuthenticodeSignature $exePath -ErrorAction SilentlyContinue + if ($sig) { + $evidence["ExeSignatureStatus"] = $sig.Status.ToString() + $evidence["ExeSignerSubject"] = $sig.SignerCertificate.Subject + $evidence["ExeSignerIssuer"] = $sig.SignerCertificate.Issuer + $evidence["ExeSignerThumbprint"] = $sig.SignerCertificate.Thumbprint + } + + # File hash + $hash = Get-FileHash $exePath -Algorithm SHA256 -ErrorAction SilentlyContinue + if ($hash) { + $evidence["ExeSHA256"] = $hash.Hash + } + } + + # Install date from registry + $uninstallPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + ) + foreach ($uPath in $uninstallPaths) { + if (-not (Test-Path $uPath)) { continue } + Get-ChildItem $uPath -ErrorAction SilentlyContinue | ForEach-Object { + $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue + if ($props.DisplayName -match "ScreenConnect|ConnectWise Control" -and + $props.UninstallString -notmatch [regex]::Escape($OurHost)) { + $evidence["InstallerDisplayName"] = $props.DisplayName + $evidence["InstallerVersion"] = $props.DisplayVersion + $evidence["InstallerPublisher"] = $props.Publisher + $evidence["InstallerInstallDate"] = $props.InstallDate + $evidence["InstallerProductCode"] = $_.PSChildName + $evidence["InstallerUninstallCmd"] = $props.UninstallString + } + } + } + + # Save evidence summary + $evidenceTxt = ($evidence.GetEnumerator() | ForEach-Object { "$($_.Key): $($_.Value)" }) -join "`r`n" + $evidenceTxt | Out-File (Join-Path $instanceDir "evidence-summary.txt") -Encoding UTF8 + + # Copy install directory contents (configs, logs -- not the binary itself to save space) + if (Test-Path $installDir) { + $artifactDir = Join-Path $instanceDir "install_files" + New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null + Get-ChildItem -Path $installDir -ErrorAction SilentlyContinue | ForEach-Object { + if ($_.Length -lt 5MB) { + Copy-Item $_.FullName -Destination $artifactDir -Force -ErrorAction SilentlyContinue + } else { + # For large files, just log metadata + "SKIPPED (too large): $($_.Name) - $([math]::Round($_.Length/1MB,2)) MB" | + Out-File (Join-Path $artifactDir "_skipped_files.txt") -Append -Encoding UTF8 + } + } + } + + # Export relevant Windows Event Logs + Write-Host " Collecting event logs..." + + # Application log - SC install/service events + try { + $appEvents = Get-WinEvent -FilterHashtable @{ + LogName = 'Application' + StartTime = (Get-Date).AddDays(-30) + } -ErrorAction SilentlyContinue | Where-Object { + $_.Message -match "ScreenConnect|ConnectWise Control" -or + $_.ProviderName -match "MsiInstaller" + } | Select-Object TimeCreated, Id, ProviderName, LevelDisplayName, Message -First 100 + if ($appEvents) { + $appEvents | Export-Csv (Join-Path $instanceDir "eventlog-application.csv") -NoTypeInformation -Encoding UTF8 + } + } catch {} + + # System log - service start/stop events + try { + $sysEvents = Get-WinEvent -FilterHashtable @{ + LogName = 'System' + Id = @(7034, 7035, 7036, 7040, 7045) + StartTime = (Get-Date).AddDays(-30) + } -ErrorAction SilentlyContinue | Where-Object { + $_.Message -match "ScreenConnect" + } | Select-Object TimeCreated, Id, ProviderName, LevelDisplayName, Message -First 100 + if ($sysEvents) { + $sysEvents | Export-Csv (Join-Path $instanceDir "eventlog-system.csv") -NoTypeInformation -Encoding UTF8 + } + } catch {} + + # Security log - logon events around install time + if ($evidence["InstallerInstallDate"]) { + try { + $installDate = [DateTime]::ParseExact($evidence["InstallerInstallDate"], "yyyyMMdd", $null) + $secEvents = Get-WinEvent -FilterHashtable @{ + LogName = 'Security' + Id = @(4624, 4625, 4648, 4672) + StartTime = $installDate.AddHours(-2) + EndTime = $installDate.AddHours(2) + } -ErrorAction SilentlyContinue | + Select-Object TimeCreated, Id, LevelDisplayName, Message -First 200 + if ($secEvents) { + $secEvents | Export-Csv (Join-Path $instanceDir "eventlog-security-around-install.csv") -NoTypeInformation -Encoding UTF8 + } + } catch {} + } + + # Active network connections at time of detection + try { + $netConns = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | + Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, OwningProcess, + @{N='ProcessName';E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} + $netConns | Export-Csv (Join-Path $instanceDir "active-connections.csv") -NoTypeInformation -Encoding UTF8 + } catch {} + + # Running processes at time of detection + try { + Get-Process | Select-Object Id, ProcessName, Path, StartTime, Company | + Export-Csv (Join-Path $instanceDir "running-processes.csv") -NoTypeInformation -Encoding UTF8 + } catch {} + + # Recent user logins + try { + $loginEvents = Get-WinEvent -FilterHashtable @{ + LogName = 'Security' + Id = @(4624) + StartTime = (Get-Date).AddDays(-7) + } -MaxEvents 50 -ErrorAction SilentlyContinue | + Select-Object TimeCreated, @{N='LogonType';E={$_.Properties[8].Value}}, + @{N='User';E={"$($_.Properties[6].Value)\$($_.Properties[5].Value)"}}, + @{N='SourceIP';E={$_.Properties[18].Value}} + $loginEvents | Export-Csv (Join-Path $instanceDir "recent-logins.csv") -NoTypeInformation -Encoding UTF8 + } catch {} + + # Scheduled tasks (attackers sometimes add persistence) + try { + Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { + $_.Actions.Execute -match "ScreenConnect|ConnectWise" -or + $_.TaskPath -match "ScreenConnect|ConnectWise" + } | Select-Object TaskName, TaskPath, State, + @{N='Action';E={$_.Actions.Execute}}, + @{N='Arguments';E={$_.Actions.Arguments}} | + Export-Csv (Join-Path $instanceDir "scheduled-tasks.csv") -NoTypeInformation -Encoding UTF8 + } catch {} + + # Collect the full service registry export + try { + reg export "HKLM\SYSTEM\CurrentControlSet\Services\$serviceName" (Join-Path $instanceDir "service-registry.reg") /y 2>&1 | Out-Null + } catch {} + + # Add to rogue list for ticket + $RogueInstances += $evidence + + # ====================================================================== + # REMOVE the rogue instance + # ====================================================================== + + Write-Host " Stopping and removing rogue instance..." + + # Stop the service + try { + Stop-Service -Name $serviceName -Force -ErrorAction Stop + } catch { + Get-Process -Name "ScreenConnect.ClientService" -ErrorAction SilentlyContinue | + Stop-Process -Force -ErrorAction SilentlyContinue + } + + # Disable it + Set-Service -Name $serviceName -StartupType Disabled -ErrorAction SilentlyContinue + + # Uninstall via MSI + $uninstalled = $false + foreach ($uPath in $uninstallPaths) { + if (-not (Test-Path $uPath)) { continue } + Get-ChildItem $uPath -ErrorAction SilentlyContinue | ForEach-Object { + $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue + if ($props.DisplayName -match "ScreenConnect|ConnectWise Control" -and + $props.UninstallString -and + $props.UninstallString -notmatch [regex]::Escape($OurHost)) { + if ($props.UninstallString -match "\{[0-9A-Fa-f\-]+\}") { + $productCode = $Matches[0] + Start-Process msiexec.exe -ArgumentList "/x $productCode /qn /norestart" -Wait | Out-Null + $uninstalled = $true + } + } + } + } + + # Fallback: nuke install directory + if (-not $uninstalled -and (Test-Path $installDir)) { + Remove-Item -Path $installDir -Recurse -Force -ErrorAction SilentlyContinue + } + + # Remove service registration + sc.exe delete $serviceName 2>&1 | Out-Null + + $Killed++ + Write-Host " [REMOVED] Rogue instance cleaned up." + Write-Host "" +} + +# ============================================================================ +# Package evidence and create ticket if rogue instances found +# ============================================================================ + +if ($Found -gt 0) { + Write-Host "=== Packaging Evidence ===" + + # Build ticket body + $ticketBody = @" +## SECURITY INCIDENT: Rogue ScreenConnect Detected + +**Computer:** $env:COMPUTERNAME +**Detection Time:** $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") +**Customer:** $OrgName +**Instances Found:** $Found +**Instances Removed:** $Killed + +### Rogue Instance Details + +"@ + + foreach ($inst in $RogueInstances) { + $ticketBody += @" + +--- +**Service:** $($inst.ServiceName) +**Connected To:** $($inst.ConnectedHost) +**Install Directory:** $($inst.InstallDirectory) +**Exe Created:** $($inst.ExeCreated) +**Exe SHA256:** $($inst.ExeSHA256) +**Signature:** $($inst.ExeSignatureStatus) ($($inst.ExeSignerSubject)) +**Installer Date:** $($inst.InstallerInstallDate) +**Installer Product:** $($inst.InstallerDisplayName) v$($inst.InstallerVersion) +**Service Account:** $($inst.ServiceAccount) + +"@ + } + + $ticketBody += @" + +### Actions Taken +- Forensic evidence collected (event logs, network connections, processes, install files) +- Service stopped and disabled +- Software uninstalled / install directory removed +- Service registration deleted +- Evidence package attached to this ticket + +### Recommended Follow-Up +1. Review the attached evidence package for indicators of compromise +2. Check the security event logs around the install date for unauthorized access +3. Verify no other persistence mechanisms remain (scheduled tasks, startup items) +4. Consider password resets for accounts on this machine +5. Check other machines at this customer for similar rogue instances +6. Determine if this was an authorized install by another vendor or an actual breach +"@ + + # Create zip of evidence + $zipPath = "$EvidenceDir.zip" + try { + Compress-Archive -Path "$EvidenceDir\*" -DestinationPath $zipPath -Force + Write-Host "Evidence packaged: $zipPath" + } catch { + Write-Host "[WARNING] Could not create zip: $_" + } + + # Create Syncro ticket + Write-Host "Creating security ticket..." + $ticketResult = Create-Syncro-Ticket -Subject "SECURITY: Rogue ScreenConnect on $env:COMPUTERNAME" -IssueType "Security" -Status "New" -Body $ticketBody + + # Upload evidence zip to the ticket + if ($ticketResult -and (Test-Path $zipPath)) { + try { + Upload-File -FilePath $zipPath -TicketIdOrNumber $ticketResult.ticket.id + Write-Host "Evidence attached to ticket." + } catch { + Write-Host "[WARNING] Could not attach evidence to ticket: $_" + # Fallback: upload to asset files + try { + Upload-File -FilePath $zipPath + Write-Host "Evidence uploaded to asset files instead." + } catch { + Write-Host "[WARNING] Could not upload evidence at all. Local copy at: $zipPath" + } + } + } + + # RMM Alert + Rmm-Alert -Category "Security" -Body "ROGUE SCREENCONNECT on $env:COMPUTERNAME - $Found instance(s) connecting to: $(($RogueInstances | ForEach-Object { $_.ConnectedHost }) -join ', '). Evidence collected. Ticket created. Instances removed." + + # Activity log + Log-Activity -Message "SECURITY: Removed $Killed rogue ScreenConnect instance(s). Ticket created with forensic evidence." -EventName "Security" + + Write-Host "" + Write-Host "[ALERT] Security ticket created with full evidence package." + + # Cleanup local evidence (zip is uploaded, raw files no longer needed) + Remove-Item -Path $EvidenceDir -Recurse -Force -ErrorAction SilentlyContinue +} else { + Write-Host "" + Write-Host "=== Summary ===" + Write-Host "Total SC services found: $($scServices.Count)" + Write-Host "All clear - no rogue instances." +} diff --git a/scripts/syncro-update-sc-properties.ps1 b/scripts/syncro-update-sc-properties.ps1 new file mode 100644 index 0000000..4123f4e --- /dev/null +++ b/scripts/syncro-update-sc-properties.ps1 @@ -0,0 +1,94 @@ +# Syncro Script: Update ScreenConnect Session Properties via RESTful API +# +# Runs on each machine to self-report its company/device info to ScreenConnect. +# Designed to run on a schedule (e.g., daily) alongside other maintenance scripts. +# +# Syncro Platform Variables (set in script editor): +# $OrgName - platform - {{customer_business_name_or_customer_name}} +# +# File Type: PowerShell | Run as: System | Max Run Time: 5 minutes + +Import-Module $env:SyncroModule + +# ============================================================================ +# Configuration +# ============================================================================ + +$SCBaseUrl = "https://computerguru.screenconnect.com" +$SCExtGuid = "2d558935-686a-4bd0-9991-07539f5fe749" +$SCAuthSecret = "FTnl15dK1uaKCOeFzkO1UnjGqpgtqCA5vRExWeXT38LjAV4vF9W/mYf8GpCyqlAv" + +# ============================================================================ +# Check if ScreenConnect is installed +# ============================================================================ + +$SCService = Get-Service -Name "ScreenConnect Client*" -ErrorAction SilentlyContinue +if (-not $SCService) { + Write-Host "ScreenConnect not installed - skipping." + exit 0 +} + +# ============================================================================ +# Extract session GUID from service name +# ============================================================================ + +# Service name format: "ScreenConnect Client (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)" +$guidMatch = $SCService.Name | Select-String -Pattern '\(([0-9a-f\-]{36})\)' -AllMatches +if (-not $guidMatch -or -not $guidMatch.Matches) { + Write-Host "Could not extract session GUID from service: $($SCService.Name)" + exit 1 +} + +$SessionGuid = $guidMatch.Matches[0].Groups[1].Value +Write-Host "SC Session GUID: $SessionGuid" + +# ============================================================================ +# Determine device type +# ============================================================================ + +$DeviceType = "Desktop" +try { + $chassis = (Get-CimInstance -ClassName Win32_SystemEnclosure).ChassisTypes + if ($chassis | Where-Object { $_ -in @(8,9,10,11,12,14,18,21,31,32) }) { $DeviceType = "Laptop" } + if ($chassis | Where-Object { $_ -in @(17,23) }) { $DeviceType = "Server" } + $model = (Get-CimInstance -ClassName Win32_ComputerSystem).Model + if ($model -match "Virtual|VMware|VirtualBox|Hyper-V|KVM|Xen") { $DeviceType = "Virtual $DeviceType" } +} catch { + Write-Host "Could not detect device type, defaulting to Desktop" +} + +# ============================================================================ +# Build and send API request +# ============================================================================ + +$Company = if ([string]::IsNullOrWhiteSpace($OrgName)) { "Unassigned" } else { $OrgName } + +Write-Host "Updating SC: Company='$Company', DeviceType='$DeviceType', Tag='Syncro-Matched'" + +# CP1=Company, CP2=Site, CP3=Department, CP4=DeviceType, CP5=Tag, CP6-8=blank +$body = ConvertTo-Json @( + $SessionGuid, + @($Company, "", "", $DeviceType, "Syncro-Matched", "", "", "") +) -Compress + +$url = "$SCBaseUrl/App_Extensions/$SCExtGuid/Service.ashx/UpdateSessionCustomProperties" + +try { + $response = Invoke-WebRequest -Uri $url -Method POST -Body $body -ContentType "application/json" -Headers @{ + "CTRLAuthHeader" = $SCAuthSecret + "Origin" = $SCBaseUrl + } -UseBasicParsing -ErrorAction Stop + + if ($response.StatusCode -eq 200) { + Write-Host "Success - SC session updated." + Log-Activity -Message "SC Properties: $Company / $DeviceType" -EventName "ScreenConnect" + } else { + Write-Host "Unexpected status: $($response.StatusCode)" + Write-Host $response.Content + } +} catch { + $err = $_.Exception.Message + Write-Host "API call failed: $err" + Log-Activity -Message "SC Properties FAILED: $err" -EventName "ScreenConnect" + exit 1 +} diff --git a/session-logs/2026-03-30-session.md b/session-logs/2026-03-30-session.md new file mode 100644 index 0000000..77eec93 --- /dev/null +++ b/session-logs/2026-03-30-session.md @@ -0,0 +1,215 @@ +# Session Log: 2026-03-30 + +## Session Summary + +Major infrastructure session on a fresh Windows 11 install (ACG-5070, formerly CachyOS). Three major accomplishments: + +1. **Machine Setup** - Verified and installed all required tools on clean Windows install +2. **SOPS+age Credential Vault** - Built a complete local encrypted credential store, migrated all 1Password credentials, synced to Gitea +3. **ScreenConnect-Syncro Sync** - Built and ran a script to enrich 410 ScreenConnect sessions with company names and device types from Syncro data + +--- + +## 1. Machine Setup (ACG-5070 - Windows 11 Pro) + +### Pre-existing +- Node.js v24.14.1, npm 11.11.0 +- Git 2.53.0 +- Python 3.14.3 +- 1Password CLI 2.33.1 +- Ollama 0.18.3 +- Claude Code 2.1.87 +- jq, curl, Windows OpenSSH + +### Installed This Session +- **sops** 3.7.3 (`winget install Mozilla.sops`) +- **age** 1.3.1 (`winget install FiloSottile.age`) +- **yq** 4.52.5 (`winget install MikeFarah.yq`) + +### Ollama Models Pulled to D:\OllamaModels +- qwen3:14b (9.3 GB) +- codestral:22b (12 GB) +- nomic-embed-text (274 MB) + +Environment variable `OLLAMA_MODELS=D:\OllamaModels` was already set. + +### Still Missing +- gh (GitHub CLI) +- Global git config (only set in vault repo: Mike Swanson / mike@azcomputerguru.com) +- Hostname not yet set (will be ACG-5070) + +### Machine Context +- CachyOS is gone -- this machine (ASUS laptop, Arrow Lake-S + RTX 5070 Ti) is now Windows 11 only +- Other machines: GURU-BEAST-ROG (Windows), Mikes-MacBook-Air (macOS) -- both need vault setup + +--- + +## 2. SOPS+age Credential Vault + +### Architecture +- **Dedicated Gitea repo**: git.azcomputerguru.com/azcomputerguru/vault (private) +- **Local path**: D:\vault +- **Encryption**: SOPS + age (AES-256), metadata stays plaintext for searchability +- **Selective encryption**: Only `credentials`, `notes`, `password`, `secret`, `api_key`, `token`, `pre_shared_key`, `content` fields are encrypted (via `encrypted_regex` in .sops.yaml) + +### age Key +- **Public key**: age1qz7ct84m50u06h97artqddkj3c8se2yu4nxu59clq8rhj945jc0s5excpr +- **Private key location (Windows)**: %APPDATA%\sops\age\keys.txt AND ~/.config/sops/age/keys.txt +- **1Password backup**: "age Key - ACG-5070 (Windows)" in Infrastructure vault + +### Credentials +- age private key: AGE-SECRET-KEY-1DE3V6V0ZLLZ45A7GA77M79CTN4LZQMTRCURP8VRGNLV6T2FSZEEQXUW2EU + +### Vault Structure (59 encrypted files) +``` +vault/ + .sops.yaml # Encryption config + .gitignore + .githooks/pre-commit # Blocks unencrypted commits + keys/recipients.txt # Public keys (ACG-5070 active, Beast+Mac pending) + scripts/vault.sh # CLI wrapper (search, get, get-field, edit, add, list, rotate) + infrastructure/ # 12 files (servers, network, OpenClaw) + clients/ # 25 files (Dataforth 10, VWP 4, Khalsa 3, etc.) + services/ # 5 files (Gitea, NPM, Cloudflare, Seafile, Matomo) + projects/ # 10 files (ClaudeTools 3, GuruRMM 6, GuruConnect 1) + msp-tools/ # 6 files (Syncro, Autotask, CIPP, Graph API, Google, ScreenConnect) +``` + +### Key Commands +```bash +# Search (no decryption needed) +bash D:/vault/scripts/vault.sh search "172.16.3.30" + +# Get specific field +bash D:/vault/scripts/vault.sh get-field infrastructure/gururmm-server.sops.yaml credentials.password + +# Full decrypt +bash D:/vault/scripts/vault.sh get services/gitea.sops.yaml + +# List all entries +bash D:/vault/scripts/vault.sh list +``` + +### Migration Process +1. Exported all 1Password data via .1pux export (manual from 1Password app) +2. Agent parsed export.data JSON, created YAML files per item, encrypted with SOPS +3. Skipped Sorting vault (1776 duplicate items) and decommissioned items +4. All plaintext temp files deleted after migration + +### CLAUDE.md Updated +- Credential access section now references SOPS vault as primary, 1Password as fallback +- New machine setup instructions for vault (install sops+age+yq, generate key, clone, rotate) + +### Git +- Repo created on Gitea: azcomputerguru/vault (private) +- Git identity set (vault repo only): Mike Swanson / mike@azcomputerguru.com +- Two commits pushed: + 1. Initial vault: 59 SOPS+age encrypted credential files + 2. Add pre-commit hook to block unencrypted credential files + +--- + +## 3. ScreenConnect-Syncro Sync + +### Goal +Enrich generic ScreenConnect sessions (installed via Syncro's prebuilt installer) with proper company names, device types from Syncro asset data. + +### ScreenConnect RESTful API Setup +- **URL**: https://computerguru.screenconnect.com +- **Extension GUID**: 2d558935-686a-4bd0-9991-07539f5fe749 +- **Auth**: CTRLAuthHeader + Origin header required +- **API Secret**: FTnl15dK1uaKCOeFzkO1UnjGqpgtqCA5vRExWeXT38LjAV4vF9W/mYf8GpCyqlAv +- **API User**: acg-sc-api +- **Stored in vault**: msp-tools/screenconnect.sops.yaml + +### SC Custom Property Mapping +| SC Field | CP# | What we populate | +|----------|-----|-----------------| +| Company | CP1 | Syncro customer.business_then_name | +| Site | CP2 | (blank - no site data in Syncro) | +| Department | CP3 | (blank) | +| Device Type | CP4 | Syncro form_factor (Laptop/Desktop/Virtual Server) | +| Tag | CP5 | "Syncro-Matched" or "Syncro-Deploy" or "Manual" | +| CP6-8 | | (blank) | + +### SC API Endpoints Used +- `GetSessionDetailsBySessionID` (GET) - read session +- `GetSessionsByName` (GET) - search by name +- `UpdateSessionCustomProperties` (POST) - update custom fields + - Body format: `["", ["CP1","CP2","CP3","CP4","CP5","CP6","CP7","CP8"]]` + +### Key Discovery: Direct GUID Link +Syncro assets have `properties["ScreenConnect GUID"]` which maps directly to SC session GUIDs. No hostname matching needed. + +### Sync Script +- **Path**: D:\claudetools\scripts\sync-sc-from-syncro.js +- **Language**: Node.js (zero npm dependencies) +- **CLI**: `node sync-sc-from-syncro.js [--dry-run] [--force] [--verbose]` +- **Credentials**: Loaded from SOPS vault via vault.sh + +### Bug Fix During Run +Node.js `https` module wasn't sending `Content-Length` header, causing SC API to return NullReferenceException. Fixed by adding explicit `Content-Length` via `Buffer.byteLength()`. + +### Results +``` +Total Syncro assets: 4636 +Assets with SC GUID: 690 +Already tagged (skipped): 0 +Updated: 410 +Errors: 280 (stale GUIDs - sessions no longer exist in SC) +``` + +### Manual Updates +- DF-GAGETRAK (501340ab-7145-428e-a2c0-c86cb3860a53) -> Dataforth Corporation, Tag: "Manual" (not in Syncro) + +### SC Deployment Script for Syncro +- **Path**: D:\claudetools\scripts\syncro-deploy-sc.ps1 +- **Purpose**: PowerShell script to deploy in Syncro as a policy script +- **What it does**: Downloads SC MSI with company name baked into installer URL, installs silently +- **Checks**: Skips if SC already installed, auto-detects device type from chassis +- **Tags with**: "Syncro-Deploy" in CP5 + +--- + +## 4. 1Password Observations + +### Rate Limiting +Service account token got rate-limited from an agent making too many parallel requests. Rate limit persisted for 30+ minutes. Desktop app integration worked as fallback but requires biometric per-call. + +### Service Account Details +- **Item name**: "Service Account Auth Token: Agentic-RW" (in Infrastructure vault) +- **Token**: ops_eyJzaWduSW5BZGRyZXNzIjoibXkuMXBhc3N3b3JkLmNvbSIs... (stored in vault at infrastructure/1password-service-account.sops.yaml) + +### Duplicate Analysis (Started, Not Completed) +- Sorting vault: 1776 items, 258 titles with duplicates +- Worst: microsoftonline.com (76 copies), acghosting.com (58 copies) +- This cleanup is a separate project + +--- + +## 5. Files Created/Modified + +### New Files +- D:\vault/ (entire repo - 62+ files) +- D:\claudetools\scripts\sync-sc-from-syncro.js +- D:\claudetools\scripts\syncro-deploy-sc.ps1 +- D:\claudetools\.claude\memory\reference_workstation_setup.md (updated from CachyOS to Windows) + +### Modified Files +- D:\claudetools\.claude\CLAUDE.md (credential access section updated for SOPS vault) +- D:\claudetools\.claude\memory\MEMORY.md (updated machine reference) + +--- + +## 6. Pending/Next Steps + +1. **Set hostname** to ACG-5070 +2. **Install gh** (GitHub CLI): `winget install GitHub.cli` +3. **Set global git config** (currently only in vault repo) +4. **Vault setup on GURU-BEAST-ROG**: install sops+age+yq, generate age key, clone vault, add key to recipients.txt, run rotate +5. **Vault setup on Mac**: same as above +6. **1Password Sorting vault cleanup**: dedup 1776 items (separate project) +7. **Commit SC sync scripts** to ClaudeTools repo +8. **Deploy syncro-deploy-sc.ps1** via Syncro policy to cover ~3946 assets without SC +9. **SC sessions with no Syncro match**: ~280 stale GUIDs to clean up in Syncro +10. **Consider scheduled sync**: run sync-sc-from-syncro.js periodically to catch new assets