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) <noreply@anthropic.com>
This commit is contained in:
@@ -39,9 +39,9 @@ You are NOT an executor. You coordinate specialized agents and preserve your con
|
|||||||
## Key Rules
|
## Key Rules
|
||||||
|
|
||||||
- **NO EMOJIS** - Use ASCII markers: `[OK]`, `[ERROR]`, `[WARNING]`, `[SUCCESS]`, `[INFO]`
|
- **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 <path> <field>`) or 1Password as fallback
|
||||||
- **SSH:** Use system OpenSSH (on Windows: `C:\Windows\System32\OpenSSH\ssh.exe`, never Git for Windows SSH)
|
- **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)
|
- **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
|
## Context Recovery
|
||||||
|
|
||||||
When user references previous work, use `/context` command. Never ask user for info in:
|
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-logs/` - Daily work logs (also in `projects/*/session-logs/` and `clients/*/session-logs/`)
|
||||||
- `SESSION_STATE.md` - Project history
|
- `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:**
|
**Setup on new machines:**
|
||||||
1. Install 1Password CLI: https://developer.1password.com/docs/cli/get-started/
|
1. Install: `winget install Mozilla.sops FiloSottile.age MikeFarah.yq` (or brew/pacman)
|
||||||
2. Sign in: `op signin` (or use desktop app integration)
|
2. Generate key: `age-keygen -o ~/.config/sops/age/keys.txt`
|
||||||
3. For non-interactive use, add to shell config: `set -gx OP_SERVICE_ACCOUNT_TOKEN "token_value"`
|
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`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- [IX Server SSH Access](reference_ix_server_ssh.md) - SSH access notes, no key auth from CachyOS workstation yet
|
- [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
|
- [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
|
- [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
|
- [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
|
- [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
|
- [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) - Set permissions.defaultMode to bypassPermissions in settings.json on all machines
|
||||||
|
|
||||||
## Machine
|
## 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
|
## Project
|
||||||
- [Audio Processor Architecture](project_audio_processor_architecture.md) - Segment-first pipeline: detect breaks before transcription for complete content capture
|
- [Audio Processor Architecture](project_audio_processor_architecture.md) - Segment-first pipeline: detect breaks before transcription for complete content capture
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
---
|
---
|
||||||
name: CachyOS Workstation Setup
|
name: ACG-5070 Workstation Setup
|
||||||
description: Current workstation config - CachyOS on ASUS laptop, dual NVMe, autostart apps, old home btrfs subvolume location
|
description: Primary workstation ACG-5070 (Windows 11 Pro), clean install 2026-03-30. Replaced CachyOS.
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
## Workstation: acg-guru-5070
|
## Workstation: ACG-5070
|
||||||
|
|
||||||
- **OS:** CachyOS (Arch-based), kernel 6.19.x
|
- **OS:** Windows 11 Pro (clean install 2026-03-30)
|
||||||
- **DE:** KDE Plasma 6 (Wayland)
|
- **Previous OS:** CachyOS Linux (gone, replaced by Windows)
|
||||||
- **CPU/GPU:** Intel Arrow Lake-S + NVIDIA RTX 5070 Ti Mobile
|
- **Hardware:** ASUS laptop, Intel Arrow Lake-S + NVIDIA RTX 5070 Ti Mobile, dual NVMe
|
||||||
- **Tailscale IP:** 100.95.216.79
|
|
||||||
|
|
||||||
### Storage
|
### Installed Tools
|
||||||
- **nvme0n1:** 954GB btrfs - CachyOS install (OS, root)
|
- Node.js v24.14.1, npm 11.11.0
|
||||||
- **nvme1n1:** 954GB ext4 - `/home` (formatted from old Windows drive)
|
- Git 2.53.0, Python 3.14.3
|
||||||
- **Old home:** btrfs `@home` subvolume on nvme0n1, mount with: `sudo mount -o subvol=@home UUID=8a8b1d34-99fb-470f-82ca-b5d08e43ec32 /mnt/old-home`
|
- 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/)
|
### SOPS Vault
|
||||||
- `arch-update-tray.desktop` (pre-existing)
|
- age key: %APPDATA%\sops\age\keys.txt
|
||||||
- `cachyos-hello.desktop` (pre-existing)
|
- Vault repo: D:\vault (git.azcomputerguru.com/azcomputerguru/vault)
|
||||||
- `discord.desktop` (added, starts minimized)
|
- 1Password backup: "age Key - ACG-5070 (Windows)" in Infrastructure vault
|
||||||
- `tailscale-systray.desktop` (added)
|
|
||||||
- ScreenConnect: autostart removed (on-demand only via URI scheme handler from web UI)
|
|
||||||
|
|
||||||
### Known Issues
|
### Other Machines
|
||||||
- **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.
|
- GURU-BEAST-ROG (Windows 11) -- needs vault setup (sops, age, yq, clone repo, generate age key, rotate)
|
||||||
|
- Mikes-MacBook-Air (macOS) -- needs vault setup
|
||||||
### 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
|
|
||||||
|
|
||||||
**How to apply:** Reference when troubleshooting workstation issues or setting up additional services.
|
**How to apply:** Reference when troubleshooting workstation issues or setting up additional services.
|
||||||
|
|||||||
408
scripts/sync-sc-from-syncro.js
Normal file
408
scripts/sync-sc-from-syncro.js
Normal file
@@ -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<void>}
|
||||||
|
*/
|
||||||
|
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<Array<{scGuid: string, company: string, deviceName: string, customerId: number}>>}
|
||||||
|
*/
|
||||||
|
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<string[]|null>} 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);
|
||||||
|
});
|
||||||
60
scripts/syncro-deploy-sc.ps1
Normal file
60
scripts/syncro-deploy-sc.ps1
Normal file
@@ -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
|
||||||
439
scripts/syncro-kill-rogue-sc.ps1
Normal file
439
scripts/syncro-kill-rogue-sc.ps1
Normal file
@@ -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."
|
||||||
|
}
|
||||||
94
scripts/syncro-update-sc-properties.ps1
Normal file
94
scripts/syncro-update-sc-properties.ps1
Normal file
@@ -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
|
||||||
|
}
|
||||||
215
session-logs/2026-03-30-session.md
Normal file
215
session-logs/2026-03-30-session.md
Normal file
@@ -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: `["<guid>", ["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
|
||||||
Reference in New Issue
Block a user