From 8b6f0bcc9634f76bd30a03df9a8953c06abd27d5 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Wed, 18 Feb 2026 16:16:18 -0700 Subject: [PATCH] sync: Multi-project updates - SolverBot, GuruRMM, Dataforth SolverBot: - Inject active project path into agent system prompts so agents know which directory to scope file operations to GuruRMM: - Bump agent version to 0.6.0 - Add serde aliases for PowerShell/ClaudeTask command types - Add typed CommandType enum on server for proper serialization - Support claude_task command type in send_command API Dataforth: - Fix SCP space-escaping in Sync-FromNAS.ps1 Co-Authored-By: Claude Opus 4.6 --- projects/dataforth-dos/Sync-FromNAS.ps1 | 12 +- .../dataforth-dos/Sync-FromNAS.ps1.original | 0 .../guru-rmm/GURURMM_DOCUMENTATION.md | 606 ++++++++++++++++++ projects/msp-tools/guru-rmm/agent/Cargo.toml | 2 +- .../guru-rmm/agent/src/transport/mod.rs | 4 + .../msp-tools/guru-rmm/deploy_agent_chunks.py | 67 ++ .../guru-rmm/deploy_via_textchunks.py | 88 +++ projects/msp-tools/guru-rmm/download_agent.py | 35 + projects/msp-tools/guru-rmm/scp_agent.py | 46 ++ .../guru-rmm/server/src/api/commands.rs | 32 +- .../msp-tools/guru-rmm/server/src/ws/mod.rs | 66 +- projects/msp-tools/guru-rmm/swap_agent.py | 80 +++ projects/solverbot | 2 +- tmp_backup_ad2.ps1 | 20 + tmp_bulksync.ps1 | 56 ++ tmp_bulksync2.ps1 | 29 + tmp_check2.ps1 | 13 + tmp_check3.ps1 | 18 + tmp_check4.ps1 | 18 + tmp_check_proc.ps1 | 18 + tmp_connect_ad2.ps1 | 21 + tmp_fix2.ps1 | 37 ++ tmp_fix_script.ps1 | 58 ++ tmp_grep.ps1 | 7 + tmp_grep2.ps1 | 14 + tmp_inspect.ps1 | 24 + tmp_lines.ps1 | 20 + tmp_readlog.ps1 | 9 + tmp_readlog2.ps1 | 0 tmp_readtail.ps1 | 18 + tmp_summary.ps1 | 10 + tmp_syntax.ps1 | 29 + tmp_syntax2.ps1 | 11 + tmp_tail.ps1 | 6 + tmp_test.ps1 | 18 + tmp_verify.ps1 | 33 + tmp_verify2.ps1 | 32 + 37 files changed, 1544 insertions(+), 15 deletions(-) create mode 100644 projects/dataforth-dos/Sync-FromNAS.ps1.original create mode 100644 projects/msp-tools/guru-rmm/GURURMM_DOCUMENTATION.md create mode 100644 projects/msp-tools/guru-rmm/deploy_agent_chunks.py create mode 100644 projects/msp-tools/guru-rmm/deploy_via_textchunks.py create mode 100644 projects/msp-tools/guru-rmm/download_agent.py create mode 100644 projects/msp-tools/guru-rmm/scp_agent.py create mode 100644 projects/msp-tools/guru-rmm/swap_agent.py create mode 100644 tmp_backup_ad2.ps1 create mode 100644 tmp_bulksync.ps1 create mode 100644 tmp_bulksync2.ps1 create mode 100644 tmp_check2.ps1 create mode 100644 tmp_check3.ps1 create mode 100644 tmp_check4.ps1 create mode 100644 tmp_check_proc.ps1 create mode 100644 tmp_connect_ad2.ps1 create mode 100644 tmp_fix2.ps1 create mode 100644 tmp_fix_script.ps1 create mode 100644 tmp_grep.ps1 create mode 100644 tmp_grep2.ps1 create mode 100644 tmp_inspect.ps1 create mode 100644 tmp_lines.ps1 create mode 100644 tmp_readlog.ps1 create mode 100644 tmp_readlog2.ps1 create mode 100644 tmp_readtail.ps1 create mode 100644 tmp_summary.ps1 create mode 100644 tmp_syntax.ps1 create mode 100644 tmp_syntax2.ps1 create mode 100644 tmp_tail.ps1 create mode 100644 tmp_test.ps1 create mode 100644 tmp_verify.ps1 create mode 100644 tmp_verify2.ps1 diff --git a/projects/dataforth-dos/Sync-FromNAS.ps1 b/projects/dataforth-dos/Sync-FromNAS.ps1 index 63abcbd..8bbf9cf 100644 --- a/projects/dataforth-dos/Sync-FromNAS.ps1 +++ b/projects/dataforth-dos/Sync-FromNAS.ps1 @@ -68,9 +68,9 @@ function Copy-FromNAS { New-Item -ItemType Directory -Path $localDir -Force | Out-Null } - # FIX: Use -i for key auth, remove embedded quotes around remote path - # DOS 8.3 filenames have no spaces - embedded quotes caused "ambiguous target" - $result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${RemotePath}" "$LocalPath" 2>&1 + # Escape spaces in remote path for SCP (unescaped spaces cause "ambiguous target") + $escapedRemote = $RemotePath -replace ' ', '\ ' + $result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${escapedRemote}" "$LocalPath" 2>&1 if ($LASTEXITCODE -ne 0) { $errorMsg = $result | Out-String Write-Log " SCP PULL ERROR (exit $LASTEXITCODE): $errorMsg" @@ -93,9 +93,9 @@ function Copy-ToNAS { $remoteDir = ($RemotePath -replace '[^/]+$', '').TrimEnd('/') Invoke-NASCommand "mkdir -p '$remoteDir'" | Out-Null - # FIX: Use -i for key auth, remove embedded quotes around remote path - # DOS 8.3 filenames have no spaces - embedded quotes caused "ambiguous target" - $result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:${RemotePath}" 2>&1 + # Escape spaces in remote path for SCP (unescaped spaces cause "ambiguous target") + $escapedRemote = $RemotePath -replace ' ', '\ ' + $result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:${escapedRemote}" 2>&1 if ($LASTEXITCODE -ne 0) { $errorMsg = $result | Out-String Write-Log " SCP PUSH ERROR (exit $LASTEXITCODE): $errorMsg" diff --git a/projects/dataforth-dos/Sync-FromNAS.ps1.original b/projects/dataforth-dos/Sync-FromNAS.ps1.original new file mode 100644 index 0000000..e69de29 diff --git a/projects/msp-tools/guru-rmm/GURURMM_DOCUMENTATION.md b/projects/msp-tools/guru-rmm/GURURMM_DOCUMENTATION.md new file mode 100644 index 0000000..9b18296 --- /dev/null +++ b/projects/msp-tools/guru-rmm/GURURMM_DOCUMENTATION.md @@ -0,0 +1,606 @@ +# GuruRMM - Complete Reference Documentation + +**Project:** GuruRMM - Remote Monitoring and Management Platform +**Version:** Server 0.2.0 / Agent 0.3.5 (deployed as 0.5.1) +**Last Updated:** 2026-02-17 + +--- + +## Table of Contents + +1. [Project Overview](#project-overview) +2. [Architecture](#architecture) +3. [API Endpoints](#api-endpoints) +4. [WebSocket Protocol](#websocket-protocol) +5. [Command Execution](#command-execution) +6. [Claude Code Integration](#claude-code-integration) +7. [Agent Configuration](#agent-configuration) +8. [Deployed Agents](#deployed-agents) +9. [Database](#database) +10. [Authentication](#authentication) +11. [Auto-Update System](#auto-update-system) +12. [Known Issues](#known-issues) +13. [Development](#development) +14. [File Structure](#file-structure) + +--- + +## Project Overview + +GuruRMM is a Remote Monitoring and Management (RMM) platform built entirely in Rust. It provides real-time agent monitoring, remote command execution, system metrics collection, and service watchdog capabilities for managed IT environments. + +### Technology Stack + +| Component | Technology | Version | +|------------|-----------------------------------------|---------| +| Server | Rust (Axum 0.7, SQLx 0.8, PostgreSQL) | 0.2.0 | +| Agent | Rust (cross-platform, native service) | 0.3.5 (deployed as 0.5.1) | +| Dashboard | React + TypeScript + Vite | -- | +| Real-time | WebSocket (tokio-tungstenite) | -- | +| Database | PostgreSQL | -- | + +--- + +## Architecture + +### Server + +- **Internal Address:** 172.16.3.30:3001 +- **Production URL:** https://rmm-api.azcomputerguru.com +- **WebSocket Endpoint:** wss://rmm-api.azcomputerguru.com/ws +- **Database:** PostgreSQL (same server) +- **Service:** systemd unit `gururmm-server` +- **Source:** `D:\ClaudeTools\projects\msp-tools\guru-rmm\server\` + +### Agent + +- **Windows Service Name:** GuruRMM (uses native-service feature) +- **Legacy Mode:** NSSM wrapper for Windows 7 / Server 2008 R2 +- **Config Path:** `C:\ProgramData\GuruRMM\agent.toml` +- **Binary Path:** `C:\Program Files\GuruRMM\gururmm-agent.exe` +- **Source:** `D:\ClaudeTools\projects\msp-tools\guru-rmm\agent\` + +### Communication Model + +``` ++-------------------+ WebSocket (persistent, bidirectional) +-------------------+ +| GuruRMM Agent | <-----------------------------------------------> | GuruRMM Server | +| (Windows/Linux) | | (Axum + Tokio) | ++-------------------+ +-------------------+ + | + | REST API (JWT) + v + +-------------------+ + | Dashboard | + | (React + TS) | + +-------------------+ +``` + +- **Primary:** WebSocket -- persistent bidirectional connection between agent and server +- **Legacy Fallback:** REST heartbeat polling -- [WARNING] NOT FULLY IMPLEMENTED +- **Auth:** API key sent in initial WebSocket authentication message +- **Site-Based Auth:** WORD-WORD-NUMBER format site codes combined with device_id + +--- + +## API Endpoints + +### Authentication + +| Method | Path | Description | Auth Required | +|--------|--------------------|----------------------------|---------------| +| POST | /api/auth/login | User login (email/password -> JWT) | No | +| POST | /api/auth/register | User registration | No (disabled) | +| GET | /api/auth/me | Get current user info | Yes | + +### Clients + +| Method | Path | Description | Auth Required | +|--------|-------------------------|-------------------------|---------------| +| GET | /api/clients | List all clients | Yes | +| POST | /api/clients | Create client | Yes | +| GET | /api/clients/:id | Get client by ID | Yes | +| PUT | /api/clients/:id | Update client | Yes | +| DELETE | /api/clients/:id | Delete client | Yes | +| GET | /api/clients/:id/sites | List client's sites | Yes | + +### Sites + +| Method | Path | Description | Auth Required | +|--------|--------------------------------|--------------------------|---------------| +| GET | /api/sites | List all sites | Yes | +| POST | /api/sites | Create site | Yes | +| GET | /api/sites/:id | Get site by ID | Yes | +| PUT | /api/sites/:id | Update site | Yes | +| DELETE | /api/sites/:id | Delete site | Yes | +| POST | /api/sites/:id/regenerate-key | Regenerate site API key | Yes | + +### Agents + +| Method | Path | Description | Auth Required | +|--------|--------------------------|--------------------------------------|---------------| +| GET | /api/agents | List all agents | Yes | +| POST | /api/agents | Register agent (authenticated) | Yes | +| GET | /api/agents/stats | Agent statistics | Yes | +| GET | /api/agents/unassigned | List unassigned agents | Yes | +| GET | /api/agents/:id | Get agent details | Yes | +| DELETE | /api/agents/:id | Delete agent | Yes | +| POST | /api/agents/:id/move | Move agent to different site | Yes | +| GET | /api/agents/:id/state | Get agent state (network, metrics) | Yes | + +### Commands + +| Method | Path | Description | Auth Required | +|--------|----------------------------|----------------------------|---------------| +| POST | /api/agents/:id/command | Send command to agent | Yes | +| GET | /api/commands | List recent commands | Yes | +| GET | /api/commands/:id | Get command status/result | Yes | + +### Metrics + +| Method | Path | Description | Auth Required | +|--------|----------------------------|---------------------------|---------------| +| GET | /api/agents/:id/metrics | Get agent metrics history | Yes | +| GET | /api/metrics/summary | Metrics summary | Yes | + +### Legacy Agent Endpoints + +These endpoints do **not** require JWT authentication. They are used by agents in legacy polling mode. + +| Method | Path | Description | Auth Required | +|--------|-------------------------------|------------------------------|---------------| +| POST | /api/agent/register-legacy | Register with site code | No | +| POST | /api/agent/heartbeat | Agent heartbeat | No | +| POST | /api/agent/command-result | Submit command result | No | + +[WARNING] Legacy heartbeat returns empty `pending_commands` -- not implemented (agents.rs line 334). +[WARNING] Legacy command-result endpoint does not store results (agents.rs lines 354-360). + +### WebSocket + +| Method | Path | Description | Auth Required | +|--------|------|------------------------|---------------------| +| GET | /ws | WebSocket upgrade | API key in auth msg | + +--- + +## WebSocket Protocol + +### Connection Flow + +1. Client initiates WebSocket upgrade to `wss://rmm-api.azcomputerguru.com/ws` +2. Agent sends authentication message with API key and device info +3. Server validates API key (SHA256 hash match or site code lookup) +4. On success, server registers the WebSocket connection for the agent +5. Bidirectional message exchange begins + +### Message Types + +**Agent -> Server:** + +- `Auth` -- Initial authentication payload (api_key, hostname, os_info, version) +- `Heartbeat` -- Periodic keepalive +- `MetricsReport` -- System metrics (CPU, memory, disk, network) +- `NetworkState` -- Network configuration snapshot (hash-based change detection) +- `CommandResult` -- Result of executed command (exit_code, stdout, stderr, duration) +- `WatchdogEvent` -- Service monitoring event + +**Server -> Agent:** + +- `AuthResponse` -- Success/failure of authentication +- `Command` -- Command to execute (CommandPayload) +- `Update` -- Auto-update instruction (download_url, checksum) +- `Ping` -- Keepalive ping + +--- + +## Command Execution + +### Command Types + +| Type | Description | Shell Used | +|--------------|----------------------------------------------|---------------------------------------------| +| shell | System shell command | cmd.exe (Windows), /bin/sh (Unix) | +| powershell | PowerShell command | powershell -NoProfile -NonInteractive -Command | +| python | Python inline code | python -c | +| script | Custom interpreter | Configurable | +| claude_task | Claude Code task execution (special handler) | Claude Code CLI | + +### Command Flow + +``` +1. Dashboard sends POST /api/agents/:id/command + Body: { command_type, command, timeout_seconds, elevated } + +2. Server creates command record in database (status = pending) + +3. If agent is connected via WebSocket: + -> Server sends command via WebSocket + -> Status updated to "running" + +4. If agent is offline: + -> Command stays as "pending" (queued) + +5. Agent receives command and executes it + +6. Agent sends CommandResult back via WebSocket + -> { id, exit_code, stdout, stderr, duration_ms } + +7. Server updates database with result +``` + +### Command States + +| State | Description | +|-----------|------------------------------------------------| +| pending | Created, agent offline or not yet sent | +| running | Sent to agent via WebSocket, awaiting result | +| completed | Agent reported exit_code = 0 | +| failed | Agent reported exit_code != 0 | + +### [BUG] Server-Agent Command Type Mismatch + +This is a **critical** known bug that prevents all remote command execution. + +**Root Cause:** + +The server's `CommandPayload` serializes `command_type` as a plain JSON string: + +```json +{"command_type": "powershell", "command": "Get-Process", ...} +``` + +The agent's `CommandPayload` expects `command_type` as a Rust enum (`CommandType::PowerShell`), which serde deserializes from an object or tagged format, not a bare string. + +**Result:** Serde deserialization fails silently on the agent side. Commands are never executed. All commands remain in "running" state permanently because no `CommandResult` is ever sent back. + +**Fix Required:** Either: +- Change the server to serialize `command_type` in the enum format the agent expects, OR +- Change the agent to accept plain string values for `command_type` + +--- + +## Claude Code Integration + +### Architecture + +The agent includes a built-in Claude Code executor for running AI-assisted tasks. + +- **Singleton:** Global `ClaudeExecutor` via `once_cell::Lazy` +- **Working Directory:** Restricted to `C:\Shares\test\` only +- **Rate Limit:** 10 tasks per hour (sliding window) +- **Max Concurrent:** 2 simultaneous tasks +- **Default Timeout:** 300 seconds (max 600) +- **Input Sanitization:** Blocks `& | ; $ ( ) < > \` \n \r` + +### Claude Task Command Format + +The server sends: + +```json +{ + "type": "command", + "payload": { + "id": "uuid", + "command_type": "claude_task", + "command": "task description", + "timeout_seconds": 300, + "elevated": false + } +} +``` + +[WARNING] This also suffers from the command type mismatch bug. The agent expects `command_type` to be an object for ClaudeTask: + +```json +{ + "claude_task": { + "task": "...", + "working_directory": "...", + "context_files": [...] + } +} +``` + +### Exit Codes + +| Code | Meaning | +|------|------------------------------------------| +| 0 | Task completed successfully | +| 1 | Task failed | +| 124 | Task timed out | +| -1 | Executor error (rate limit, validation) | + +--- + +## Agent Configuration + +### agent.toml Format + +```toml +[server] +url = "wss://rmm-api.azcomputerguru.com/ws" +api_key = "SITE-CODE-1234" # or grmm_xxxxx API key + +[metrics] +interval_seconds = 60 # Range: 10-3600, default: 60 +collect_cpu = true +collect_memory = true +collect_disk = true +collect_network = true + +[watchdog] +enabled = true +check_interval_seconds = 30 + +[[watchdog.services]] +name = "ServiceName" +action = "restart" +max_restarts = 3 +restart_cooldown_seconds = 60 +``` + +### Hardcoded Intervals + +These values are currently not configurable via `agent.toml`: + +| Interval | Value | Notes | +|----------------------------|-------------|--------------------------------| +| Heartbeat | 30 seconds | | +| Network state check | 30 seconds | Uses hash-based change detection | +| Connection idle timeout | 90 seconds | | +| Auth timeout | 10 seconds | | +| Reconnect delay | 10 seconds | | +| Command execution timeout | 300 seconds | Configurable per command | + +--- + +## Deployed Agents + +| Hostname | Agent ID (prefix) | Version | OS | Status | +|-------------|--------------------|---------|-----------------------------|---------| +| ACG-M-L5090 | 97f63c3b-... | 0.5.1 | Windows 11 (26200) | online | +| AD2 | d28a1c90-... | 0.5.1 | Windows Server 2016 (14393) | online | +| gururmm | 8cd0440f-... | 0.5.1 | Ubuntu 22.04 | offline | +| SL-SERVER | 2585f6d5-... | 0.5.1 | unknown | offline | +| SL-SERVER | dff818e6-... | 0.5.1 | unknown | online | + +--- + +## Database + +### Connection Details + +| Parameter | Value | +|-----------|------------------------------------| +| Host | 172.16.3.30 | +| Port | 5432 | +| Database | gururmm | +| User | gururmm | +| Password | 43617ebf7eb242e814ca9988cc4df5ad | + +### Key Tables + +| Table | Description | +|---------------------|------------------------------------------------| +| users | User accounts (JWT auth, Argon2id hashing) | +| clients | Client organizations | +| sites | Physical locations with API keys | +| agents | RMM agent instances | +| agent_state | Latest agent state (network, metrics snapshot) | +| agent_updates | Agent update tracking | +| alerts | System alerts | +| commands | Remote command execution log | +| metrics | Performance metrics time series | +| policies | Configuration policies | +| registration_tokens | Agent registration tokens | +| watchdog_events | Service monitoring events | + +--- + +## Authentication + +### API Authentication (JWT) + +1. Send `POST /api/auth/login` with `{ email, password }` +2. Server validates credentials (Argon2id password hash) +3. Returns JWT token (24-hour expiry) +4. Include token in subsequent requests: `Authorization: Bearer ` + +**Admin Credentials:** + +| Field | Value | +|----------|------------------------------------| +| Email | claude-api@azcomputerguru.com | +| Password | ClaudeAPI2026!@# | + +### Agent Authentication (API Key) + +Two authentication modes: + +1. **Direct API Key** -- Agent sends `grmm_xxxxx` format key, server matches against `api_key_hash` (SHA256) in agents table +2. **Site-Based** -- Agent sends site code (WORD-WORD-NUMBER format, e.g., `DARK-GROVE-7839`) combined with `device_id`, server looks up site and registers/matches agent + +### SSO (Optional) + +- **Provider:** Microsoft Entra ID +- **Client ID:** 18a15f5d-7ab8-46f4-8566-d7b5436b84b6 + +--- + +## Auto-Update System + +### Update Flow + +``` +1. Agent connects via WebSocket and sends its version in the auth payload + +2. Server checks if a newer version is available for the agent's OS/architecture + +3. If update needed: + -> Server sends Update message with download_url and SHA256 checksum + +4. Agent downloads the new binary from the download URL + +5. Agent verifies the SHA256 checksum + +6. Agent replaces its own binary and restarts + +7. On reconnection, agent reports previous_version in auth payload + +8. Server marks the update as completed +``` + +### Download Location + +- **Server Path:** `/var/www/gururmm/downloads/` +- **Public URL:** `https://rmm-api.azcomputerguru.com/downloads/` + +--- + +## Known Issues + +### CRITICAL + +| ID | Type | Description | +|----|------------|--------------------------------------------------------------------------------------------| +| 1 | [BUG] | Command type mismatch between server (String) and agent (Enum) -- commands never execute | +| 2 | [TODO] | Legacy heartbeat returns empty pending_commands (agents.rs line 334) | +| 3 | [TODO] | Legacy command-result endpoint does not store results (agents.rs lines 354-360) | +| 4 | [SECURITY] | CORS configured with AllowOrigin::Any -- should be restricted to known origins | + +### MAJOR + +| ID | Description | +|----|--------------------------------------------------------------------------------| +| 1 | No command timeout enforcement on server side | +| 2 | No retry logic for failed WebSocket sends | +| 3 | Database inconsistency: agent shows "online" but command sends fail silently | +| 4 | Missing database indexes on frequently queried columns | +| 5 | No rate limiting on command submissions | + +### MINOR + +| ID | Description | +|----|--------------------------------------------------------------------------| +| 1 | Hardcoded intervals (heartbeat, network check) not configurable | +| 2 | Watchdog events logged but not stored in database | +| 3 | No log rotation configured | +| 4 | Unicode characters in agent output (should use ASCII per coding guidelines) | + +--- + +## Development + +### Building + +```bash +# Server +cd server && cargo build --release + +# Agent (Windows, native service mode) +cd agent && cargo build --release + +# Agent (Legacy mode for Windows 7 / Server 2008 R2) +cd agent && cargo build --release --features legacy --no-default-features +``` + +### Testing + +```bash +cargo test # Run unit tests +cargo clippy # Run linter +cargo fmt --check # Check formatting +``` + +### Deploying the Server + +```bash +# On gururmm server (172.16.3.30) +systemctl stop gururmm-server +cp target/release/gururmm-server /opt/gururmm/ +systemctl start gururmm-server +journalctl -u gururmm-server -f +``` + +### Deploying the Agent + +```cmd +REM On target Windows machine +sc stop GuruRMM +copy gururmm-agent.exe "C:\Program Files\GuruRMM\gururmm-agent.exe" +sc start GuruRMM +``` + +--- + +## File Structure + +``` +D:\ClaudeTools\projects\msp-tools\guru-rmm\ +| ++-- agent/ +| +-- src/ +| | +-- main.rs # Entry point, CLI parsing, service install +| | +-- config.rs # TOML config loading and validation +| | +-- claude.rs # Claude Code executor (rate-limited singleton) +| | +-- service.rs # Windows service handler (native-service feature) +| | +-- device_id.rs # Hardware-based device ID generation +| | +-- transport/ +| | | +-- mod.rs # Message types (AgentMessage, ServerMessage, CommandType enum) +| | | +-- websocket.rs # WebSocket client, reconnection, command execution +| | +-- metrics/ +| | | +-- mod.rs # System metrics collection, network state hashing +| | +-- updater/ +| | +-- mod.rs # Self-update logic (download, verify, replace) +| +-- deploy/ # Deployment configs per site +| +-- Cargo.toml # v0.3.5, features: native-service, legacy +| ++-- server/ +| +-- src/ +| | +-- main.rs # Axum server setup, router, middleware +| | +-- api/ +| | | +-- mod.rs # Route definitions and grouping +| | | +-- agents.rs # Agent management + legacy polling endpoints +| | | +-- commands.rs # Command dispatch and status tracking +| | | +-- auth.rs # JWT login, registration, user info +| | | +-- clients.rs # Client CRUD operations +| | | +-- sites.rs # Site management and API key regeneration +| | | +-- metrics.rs # Metrics query endpoints +| | +-- ws/ +| | | +-- mod.rs # WebSocket handler, ServerMessage types, CommandPayload (String type) +| | +-- db/ +| | | +-- agents.rs # Agent database operations +| | | +-- commands.rs # Command database operations +| | +-- auth/ +| | +-- mod.rs # JWT middleware and token validation +| +-- Cargo.toml # v0.2.0 +| ++-- dashboard/ # React frontend (if present) +| ++-- docs/ + +-- FEATURE_ROADMAP.md # Complete feature plan (654 lines) + +-- REMEDIATION_PLAN.md # Security and code review (1277 lines) +``` + +--- + +## Quick Reference + +| Item | Value | +|--------------------|---------------------------------------------| +| Server URL | https://rmm-api.azcomputerguru.com | +| WebSocket URL | wss://rmm-api.azcomputerguru.com/ws | +| Internal Address | 172.16.3.30:3001 | +| Database | PostgreSQL @ 172.16.3.30:5432/gururmm | +| Service Name | gururmm-server (systemd) | +| Agent Service | GuruRMM (Windows SCM) | +| Agent Config | C:\ProgramData\GuruRMM\agent.toml | +| Agent Binary | C:\Program Files\GuruRMM\gururmm-agent.exe | +| Downloads | https://rmm-api.azcomputerguru.com/downloads/ | +| Admin Email | claude-api@azcomputerguru.com | +| SSO Client ID | 18a15f5d-7ab8-46f4-8566-d7b5436b84b6 | + +--- + +*Document generated 2026-02-17. Source of truth for GuruRMM project reference.* diff --git a/projects/msp-tools/guru-rmm/agent/Cargo.toml b/projects/msp-tools/guru-rmm/agent/Cargo.toml index 2f5bc0e..5cca4c7 100644 --- a/projects/msp-tools/guru-rmm/agent/Cargo.toml +++ b/projects/msp-tools/guru-rmm/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gururmm-agent" -version = "0.3.5" +version = "0.6.0" edition = "2021" description = "GuruRMM Agent - Cross-platform RMM agent" authors = ["GuruRMM"] diff --git a/projects/msp-tools/guru-rmm/agent/src/transport/mod.rs b/projects/msp-tools/guru-rmm/agent/src/transport/mod.rs index bf2decf..3f65b57 100644 --- a/projects/msp-tools/guru-rmm/agent/src/transport/mod.rs +++ b/projects/msp-tools/guru-rmm/agent/src/transport/mod.rs @@ -199,6 +199,9 @@ pub enum CommandType { Shell, /// PowerShell command (Windows) + /// Alias "powershell" for backwards compatibility with servers that send + /// the command type as a plain string instead of snake_case enum format. + #[serde(alias = "powershell")] PowerShell, /// Python script @@ -208,6 +211,7 @@ pub enum CommandType { Script { interpreter: String }, /// Claude Code task execution + #[serde(alias = "claude_task")] ClaudeTask { /// Task description for Claude Code task: String, diff --git a/projects/msp-tools/guru-rmm/deploy_agent_chunks.py b/projects/msp-tools/guru-rmm/deploy_agent_chunks.py new file mode 100644 index 0000000..cf3457f --- /dev/null +++ b/projects/msp-tools/guru-rmm/deploy_agent_chunks.py @@ -0,0 +1,67 @@ +import requests, json, time, sys + +with open("/tmp/agent_b64.txt") as f: + b64 = f.read().strip() +print("Base64 length:", len(b64)) + +token_r = requests.post("http://localhost:3001/api/auth/login", json={"email": "claude-api@azcomputerguru.com", "password": "ClaudeAPI2026!@#"}) +token = token_r.json()["token"] +headers = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} +agent_id = "d28a1c90-47d7-448f-a287-197bc8892234" + +chunk_size = 200000 +chunks = [b64[i:i+chunk_size] for i in range(0, len(b64), chunk_size)] +print("Chunks:", len(chunks)) + +def send_cmd(cmd, timeout=60, wait=15): + r = requests.post( + "http://localhost:3001/api/agents/" + agent_id + "/command", + headers=headers, + json={"command_type": "powershell", "command": cmd, "timeout_seconds": timeout} + ) + cmd_id = r.json()["command_id"] + time.sleep(wait) + r2 = requests.get("http://localhost:3001/api/commands/" + cmd_id, headers=headers) + return r2.json() + +# Chunk 1: create file +cmd = "$b = [Convert]::FromBase64String('" + chunks[0] + "'); New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; [System.IO.File]::WriteAllBytes('C:/Temp/agent_chunk.bin', $b); Write-Output ('Chunk 1: ' + $b.Length.ToString() + ' bytes')" +d = send_cmd(cmd) +print("Chunk 1:", d["status"], d.get("stdout","")) +if d["status"] != "completed": + print("ERROR:", (d.get("stderr","") or "")[:300]) + sys.exit(1) + +# Append remaining chunks +for i, chunk in enumerate(chunks[1:], 2): + cmd = "$b = [Convert]::FromBase64String('" + chunk + "'); $f = [System.IO.File]::Open('C:/Temp/agent_chunk.bin', [System.IO.FileMode]::Append); $f.Write($b, 0, $b.Length); $f.Close(); Write-Output ('Chunk " + str(i) + ": ' + $b.Length.ToString() + ' bytes')" + d = send_cmd(cmd, wait=10) + print("Chunk", i, ":", d["status"], d.get("stdout","")) + if d["status"] != "completed": + print("ERROR:", (d.get("stderr","") or "")[:300]) + sys.exit(1) + +# Verify final size +d = send_cmd("(Get-Item C:/Temp/agent_chunk.bin).Length", wait=5) +print("Final size:", d.get("stdout","").strip(), "(expected 3577856)") + +# Now create update script that stops service, replaces binary, starts service +update_cmd = """ +$src = 'C:/Temp/agent_chunk.bin' +$dst = 'C:/Program Files/GuruRMM/gururmm-agent.exe' +$bak = 'C:/Program Files/GuruRMM/gururmm-agent.exe.bak' +Copy-Item $dst $bak -Force +Stop-Service GuruRMMAgent -Force +Start-Sleep -Seconds 2 +Copy-Item $src $dst -Force +Start-Service GuruRMMAgent +Write-Output 'Agent updated and restarted' +""" +print("\nNow creating scheduled task to perform the update...") +sched_cmd = 'schtasks /create /tn AgentUpdate /tr "powershell.exe -ExecutionPolicy Bypass -Command \\"' + update_cmd.replace('\n', '; ').strip() + '\\"" /sc ONCE /st 00:00 /sd 01/01/2030 /ru SYSTEM /f' +d = send_cmd(sched_cmd, wait=5) +print("Sched task create:", d["status"], d.get("stdout",""), d.get("stderr","")[:200] if d.get("stderr") else "") + +d = send_cmd("schtasks /run /tn AgentUpdate", wait=5) +print("Sched task run:", d["status"], d.get("stdout","")) +print("\nAgent will restart momentarily. Wait 30s then check connection.") diff --git a/projects/msp-tools/guru-rmm/deploy_via_textchunks.py b/projects/msp-tools/guru-rmm/deploy_via_textchunks.py new file mode 100644 index 0000000..f4f303a --- /dev/null +++ b/projects/msp-tools/guru-rmm/deploy_via_textchunks.py @@ -0,0 +1,88 @@ +"""Deploy agent binary to AD2 by writing base64 text chunks to a file, then decoding.""" +import requests, json, time, sys, base64 + +# Read and encode the binary +with open("agent/target/release/gururmm-agent.exe", "rb") as f: + binary = f.read() +b64 = base64.b64encode(binary).decode('ascii') +print(f"Binary: {len(binary)} bytes, Base64: {len(b64)} chars") + +# Auth +token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={ + 'email': 'claude-api@azcomputerguru.com', + 'password': 'ClaudeAPI2026!@#' +}) +token = token_r.json()['token'] +headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'} +agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234' + +def send_cmd(cmd, timeout=60, wait=10): + r = requests.post( + 'http://172.16.3.30:3001/api/agents/' + agent_id + '/command', + headers=headers, + json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': timeout} + ) + data = r.json() + cmd_id = data['command_id'] + time.sleep(wait) + # Poll until complete + for attempt in range(10): + r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers) + d = r2.json() + if d['status'] != 'running': + return d + time.sleep(5) + return d + +# Step 1: Delete old file and create fresh one +print("Step 1: Preparing temp file...") +d = send_cmd("Remove-Item C:/Temp/agent.b64 -Force -ErrorAction SilentlyContinue; " + "New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; " + "'' | Set-Content C:/Temp/agent.b64 -NoNewline; " + "Write-Output 'Ready'", wait=8) +print(f" {d['status']}: {d.get('stdout','').strip()}") +if d['status'] != 'completed': + print(f" ERROR: {(d.get('stderr','') or '')[:300]}") + sys.exit(1) + +# Step 2: Write base64 text in chunks +# Windows command line limit is ~32KB, keep chunks under 20KB to be safe +chunk_size = 20000 +chunks = [b64[i:i+chunk_size] for i in range(0, len(b64), chunk_size)] +print(f"Step 2: Writing {len(chunks)} chunks of ~{chunk_size} chars each...") + +for i, chunk in enumerate(chunks): + # Use Add-Content to append text (no base64 decode here, just text) + # Escape single quotes in base64 (shouldn't have any, but just in case) + safe_chunk = chunk.replace("'", "''") + cmd = f"Add-Content -Path C:/Temp/agent.b64 -Value '{safe_chunk}' -NoNewline; Write-Output 'chunk{i+1}ok'" + d = send_cmd(cmd, wait=5) + status = d['status'] + stdout = d.get('stdout', '').strip() + if status != 'completed' or f'chunk{i+1}ok' not in stdout: + print(f" Chunk {i+1}/{len(chunks)} FAILED: {status} - {stdout}") + print(f" stderr: {(d.get('stderr','') or '')[:300]}") + sys.exit(1) + if (i+1) % 10 == 0 or i == 0 or i == len(chunks)-1: + print(f" Chunk {i+1}/{len(chunks)}: OK") + +# Step 3: Verify base64 file size +print("Step 3: Verifying base64 file...") +d = send_cmd(f"$f = Get-Item C:/Temp/agent.b64; Write-Output $f.Length", wait=5) +remote_size = d.get('stdout', '').strip() +print(f" Remote b64 size: {remote_size} (expected: {len(b64)})") + +# Step 4: Decode base64 file to binary +print("Step 4: Decoding base64 to binary...") +cmd = ("$b64 = Get-Content C:/Temp/agent.b64 -Raw; " + "$bytes = [Convert]::FromBase64String($b64); " + "[System.IO.File]::WriteAllBytes('C:/Temp/gururmm-agent-new.exe', $bytes); " + "$f = Get-Item C:/Temp/gururmm-agent-new.exe; " + "Write-Output ('Decoded: ' + $f.Length.ToString() + ' bytes')") +d = send_cmd(cmd, timeout=120, wait=15) +print(f" {d['status']}: {d.get('stdout','').strip()}") +if d.get('stderr'): + print(f" stderr: {(d.get('stderr','') or '')[:300]}") + +print(f"\nExpected binary size: {len(binary)} bytes") +print("Done!") diff --git a/projects/msp-tools/guru-rmm/download_agent.py b/projects/msp-tools/guru-rmm/download_agent.py new file mode 100644 index 0000000..a57199b --- /dev/null +++ b/projects/msp-tools/guru-rmm/download_agent.py @@ -0,0 +1,35 @@ +import requests, json, time, sys + +# Auth +token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={ + 'email': 'claude-api@azcomputerguru.com', + 'password': 'ClaudeAPI2026!@#' +}) +token = token_r.json()['token'] +headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'} +agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234' + +# Send download command via PowerShell +cmd = ( + "New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; " + "Invoke-WebRequest -Uri 'http://172.16.3.30/gururmm-agent-new.exe' " + "-OutFile 'C:/Temp/gururmm-agent-new.exe' -UseBasicParsing; " + "$f = Get-Item 'C:/Temp/gururmm-agent-new.exe'; " + "Write-Output ('Downloaded: ' + $f.Length.ToString() + ' bytes')" +) + +r = requests.post( + 'http://172.16.3.30:3001/api/agents/' + agent_id + '/command', + headers=headers, + json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': 120} +) +print('Send:', r.json()) +cmd_id = r.json()['command_id'] + +# Wait and check +time.sleep(20) +r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers) +d = r2.json() +print('Status:', d['status']) +print('stdout:', d.get('stdout', '')) +print('stderr:', (d.get('stderr', '') or '')[:500]) diff --git a/projects/msp-tools/guru-rmm/scp_agent.py b/projects/msp-tools/guru-rmm/scp_agent.py new file mode 100644 index 0000000..03bfe15 --- /dev/null +++ b/projects/msp-tools/guru-rmm/scp_agent.py @@ -0,0 +1,46 @@ +import requests, json, time, sys + +# Auth +token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={ + 'email': 'claude-api@azcomputerguru.com', + 'password': 'ClaudeAPI2026!@#' +}) +token = token_r.json()['token'] +headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'} +agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234' + +def send_cmd(cmd, timeout=120, wait=20): + r = requests.post( + 'http://172.16.3.30:3001/api/agents/' + agent_id + '/command', + headers=headers, + json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': timeout} + ) + data = r.json() + print('Sent:', data.get('status', 'error'), data.get('message', '')) + cmd_id = data['command_id'] + time.sleep(wait) + r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers) + d = r2.json() + print('Status:', d['status']) + print('stdout:', d.get('stdout', '')) + stderr = d.get('stderr', '') or '' + if stderr: + print('stderr:', stderr[:500]) + print('exit_code:', d.get('exit_code')) + return d + +# First, check what transfer tools are available on AD2 +print("=== Checking available tools ===") +d = send_cmd("Get-Command scp,ssh,curl -ErrorAction SilentlyContinue | Select-Object Name,Source | Format-Table -AutoSize", wait=10) + +print("\n=== Trying SCP from AD2 to RMM server ===") +# Use scp with StrictHostKeyChecking=no for automated transfer +cmd = ( + "scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL " + "guru@172.16.3.30:/tmp/gururmm-agent-new.exe C:/Temp/gururmm-agent-new.exe 2>&1; " + "if (Test-Path C:/Temp/gururmm-agent-new.exe) { " + " $f = Get-Item C:/Temp/gururmm-agent-new.exe; " + " Write-Output ('File size: ' + $f.Length.ToString() + ' bytes') " + "} else { Write-Output 'File not found after SCP' }" +) +d = send_cmd(cmd, wait=30) diff --git a/projects/msp-tools/guru-rmm/server/src/api/commands.rs b/projects/msp-tools/guru-rmm/server/src/api/commands.rs index b4aa7da..35fd6f5 100644 --- a/projects/msp-tools/guru-rmm/server/src/api/commands.rs +++ b/projects/msp-tools/guru-rmm/server/src/api/commands.rs @@ -10,16 +10,16 @@ use uuid::Uuid; use crate::auth::AuthUser; use crate::db::{self, Command}; -use crate::ws::{CommandPayload, ServerMessage}; +use crate::ws::{CommandPayload, CommandType, ServerMessage}; use crate::AppState; /// Request to send a command to an agent #[derive(Debug, Deserialize)] pub struct SendCommandRequest { - /// Command type (shell, powershell, python, script) + /// Command type (shell, powershell, python, script, claude_task) pub command_type: String, - /// Command text to execute + /// Command text to execute (also used as task description for claude_task) pub command: String, /// Timeout in seconds (optional, default 300) @@ -27,6 +27,12 @@ pub struct SendCommandRequest { /// Run as elevated/admin (optional, default false) pub elevated: Option, + + /// Working directory for claude_task (optional, default C:\Shares\test) + pub working_directory: Option, + + /// Context files for claude_task (optional) + pub context_files: Option>, } /// Response after sending a command @@ -59,6 +65,18 @@ pub async fn send_command( "Command sent by user" ); + // Validate and build command type + let command_type = if CommandType::is_claude_task(&req.command_type) { + CommandType::new_claude_task( + req.command.clone(), + req.working_directory.clone(), + req.context_files.clone(), + ) + } else { + CommandType::from_api_string(&req.command_type) + .map_err(|e| (StatusCode::BAD_REQUEST, e))? + }; + // Verify agent exists let _agent = db::get_agent_by_id(&state.db, agent_id) .await @@ -66,9 +84,10 @@ pub async fn send_command( .ok_or((StatusCode::NOT_FOUND, "Agent not found".to_string()))?; // Create command record with user ID for audit trail + // Store the canonical db string format for consistency let create = db::CreateCommand { agent_id, - command_type: req.command_type.clone(), + command_type: command_type.as_db_string().to_string(), command_text: req.command.clone(), created_by: Some(user.user_id), }; @@ -80,10 +99,11 @@ pub async fn send_command( // Check if agent is connected let agents = state.agents.read().await; if agents.is_connected(&agent_id) { - // Send command via WebSocket + // Send command via WebSocket using the proper enum type + // This serializes as snake_case to match the agent's expected format let cmd_msg = ServerMessage::Command(CommandPayload { id: command.id, - command_type: req.command_type, + command_type, command: req.command, timeout_seconds: req.timeout_seconds, elevated: req.elevated.unwrap_or(false), diff --git a/projects/msp-tools/guru-rmm/server/src/ws/mod.rs b/projects/msp-tools/guru-rmm/server/src/ws/mod.rs index e5abdd8..45a1524 100644 --- a/projects/msp-tools/guru-rmm/server/src/ws/mod.rs +++ b/projects/msp-tools/guru-rmm/server/src/ws/mod.rs @@ -193,10 +193,74 @@ pub struct NetworkStatePayload { pub state_hash: String, } +/// Types of commands that can be sent to agents. +/// Must match the agent's CommandType enum serialization format. +/// Uses snake_case to match the agent's #[serde(rename_all = "snake_case")]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CommandType { + /// Shell command (cmd on Windows, sh on Unix) + Shell, + /// PowerShell command (Windows) + PowerShell, + /// Python script + Python, + /// Raw script execution + Script, + /// Claude Code task execution + ClaudeTask { + /// Task description for Claude Code + task: String, + /// Optional working directory + working_directory: Option, + /// Optional context files + context_files: Option>, + }, +} + +impl CommandType { + /// Parse a command type string from the API into the enum. + /// Accepts both snake_case ("power_shell") and common formats ("powershell"). + /// Note: ClaudeTask requires additional fields - use `new_claude_task()` instead. + pub fn from_api_string(s: &str) -> Result { + match s.to_lowercase().as_str() { + "shell" => Ok(Self::Shell), + "powershell" | "power_shell" => Ok(Self::PowerShell), + "python" => Ok(Self::Python), + "script" => Ok(Self::Script), + "claude_task" | "claudetask" => Err( + "claude_task type requires task field - use the claude_task-specific API fields".to_string() + ), + _ => Err(format!("Unknown command type: '{}'. Valid types: shell, powershell, python, script, claude_task", s)), + } + } + + /// Check if a command type string represents a claude_task. + pub fn is_claude_task(s: &str) -> bool { + matches!(s.to_lowercase().as_str(), "claude_task" | "claudetask") + } + + /// Create a ClaudeTask command type with the required fields. + pub fn new_claude_task(task: String, working_directory: Option, context_files: Option>) -> Self { + Self::ClaudeTask { task, working_directory, context_files } + } + + /// Convert back to the string format stored in the database. + pub fn as_db_string(&self) -> &'static str { + match self { + Self::Shell => "shell", + Self::PowerShell => "powershell", + Self::Python => "python", + Self::Script => "script", + Self::ClaudeTask { .. } => "claude_task", + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommandPayload { pub id: Uuid, - pub command_type: String, + pub command_type: CommandType, pub command: String, pub timeout_seconds: Option, pub elevated: bool, diff --git a/projects/msp-tools/guru-rmm/swap_agent.py b/projects/msp-tools/guru-rmm/swap_agent.py new file mode 100644 index 0000000..1d0a3f7 --- /dev/null +++ b/projects/msp-tools/guru-rmm/swap_agent.py @@ -0,0 +1,80 @@ +"""Create scheduled task on AD2 to swap agent binary and restart service.""" +import requests, time + +# Auth +token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={ + 'email': 'claude-api@azcomputerguru.com', + 'password': 'ClaudeAPI2026!@#' +}) +token = token_r.json()['token'] +headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'} +agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234' + +# Create a scheduled task to swap the binary and restart +# Uses schtasks for simplicity - runs a PowerShell script that: +# 1. Stops the agent service +# 2. Backs up old binary +# 3. Copies new binary in place +# 4. Starts the service +# 5. Writes result to file +cmd = ( + "$script = @'\n" + "Stop-Service GuruRMMAgent -Force\n" + "Start-Sleep -Seconds 3\n" + "Copy-Item \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe\" \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe.bak\" -Force\n" + "Copy-Item \"C:\\Temp\\gururmm-agent-new.exe\" \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe\" -Force\n" + "Start-Sleep -Seconds 1\n" + "Start-Service GuruRMMAgent\n" + "\"Agent updated at $(Get-Date)\" | Out-File C:\\Temp\\update_result.txt\n" + "'@\n" + "$script | Out-File C:/Temp/update_agent.ps1 -Encoding UTF8\n" + "Write-Output ('Script written: ' + (Get-Item C:/Temp/update_agent.ps1).Length.ToString() + ' bytes')" +) + +print("Step 1: Writing update script to AD2...") +r = requests.post( + 'http://172.16.3.30:3001/api/agents/' + agent_id + '/command', + headers=headers, + json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': 30} +) +print('Send:', r.json()) +cmd_id = r.json()['command_id'] +time.sleep(10) +r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers) +d = r2.json() +print('Status:', d['status']) +print('stdout:', d.get('stdout', '')) +if d.get('stderr'): + print('stderr:', (d.get('stderr', '') or '')[:300]) + +if d['status'] != 'completed': + print("FAILED to write script") + exit(1) + +# Step 2: Create and run scheduled task +print("\nStep 2: Creating scheduled task to run update...") +cmd2 = ( + "schtasks /create /tn AgentBinaryUpdate /tr " + "\"powershell.exe -ExecutionPolicy Bypass -File C:\\Temp\\update_agent.ps1\" " + "/sc ONCE /st 00:00 /sd 01/01/2030 /ru SYSTEM /rl HIGHEST /f; " + "schtasks /run /tn AgentBinaryUpdate; " + "Write-Output 'Update task started - agent will restart momentarily'" +) + +r = requests.post( + 'http://172.16.3.30:3001/api/agents/' + agent_id + '/command', + headers=headers, + json={'command_type': 'powershell', 'command': cmd2, 'timeout_seconds': 30} +) +print('Send:', r.json()) +cmd_id = r.json()['command_id'] +time.sleep(10) +r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers) +d = r2.json() +print('Status:', d['status']) +print('stdout:', d.get('stdout', '')) +if d.get('stderr'): + print('stderr:', (d.get('stderr', '') or '')[:300]) + +print("\nAgent will stop, binary will be swapped, then service restarts.") +print("Wait ~30 seconds then check if agent reconnects.") diff --git a/projects/solverbot b/projects/solverbot index 342fe0f..60b2617 160000 --- a/projects/solverbot +++ b/projects/solverbot @@ -1 +1 @@ -Subproject commit 342fe0fdf4e68bbd8b3524a6c7530e566cc1e193 +Subproject commit 60b2617651d1f6a7786a673fca8a6ef8e41662c0 diff --git a/tmp_backup_ad2.ps1 b/tmp_backup_ad2.ps1 new file mode 100644 index 0000000..6f42eb7 --- /dev/null +++ b/tmp_backup_ad2.ps1 @@ -0,0 +1,20 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + Write-Output "=== DRIVE MAPPED ===" + + # Step 3: Back up the current script + $src = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + $bak = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1.bak.2026-02-17" + Copy-Item -Path $src -Destination $bak -Force + Write-Output "=== BACKUP CREATED: $bak ===" + + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" +} diff --git a/tmp_bulksync.ps1 b/tmp_bulksync.ps1 new file mode 100644 index 0000000..7f55145 --- /dev/null +++ b/tmp_bulksync.ps1 @@ -0,0 +1,56 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" +New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + +# Write a one-time bulk sync runner script +$bulkScript = @" +# One-time bulk sync with 86400 minute window (60 days) +# This catches all stranded files from the last ~35 days +# Auto-deletes itself after running +Set-Location "C:\Shares\test\scripts" +& powershell -NoProfile -ExecutionPolicy Bypass -File "C:\Shares\test\scripts\Sync-FromNAS.ps1" -MaxAgeMinutes 86400 -Verbose +# Log completion +Add-Content -Path "C:\Shares\test\scripts\sync-from-nas.log" -Value "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) : BULK SYNC COMPLETE - one-time catchup finished" +# Clean up this trigger script +Remove-Item -Path "C:\Shares\test\scripts\BulkSync-OneTime.ps1" -Force -ErrorAction SilentlyContinue +"@ + +Set-Content -Path "AD2:\Shares\test\scripts\BulkSync-OneTime.ps1" -Value $bulkScript +Write-Output "[OK] Bulk sync script written to AD2:\Shares\test\scripts\BulkSync-OneTime.ps1" + +# Now create a scheduled task on AD2 to run it immediately +# We can do this by writing a schtasks command file +$taskScript = @" +schtasks /create /tn "BulkSync-OneTime" /tr "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\BulkSync-OneTime.ps1" /sc once /st 00:00 /ru INTRANET\sysadmin /rp "Paper123\!@#" /f +schtasks /run /tn "BulkSync-OneTime" +"@ + +Set-Content -Path "AD2:\Shares\test\scripts\run-bulk.cmd" -Value $taskScript +Write-Output "[OK] Task creation script written" +Write-Output "" +Write-Output "NOTE: The scheduled task needs to be triggered on AD2." +Write-Output "Attempting to use WMI to create process on AD2..." + +# Try WMI process creation (may work even if WinRM doesnt) +try { + $proc = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\BulkSync-OneTime.ps1" + if ($proc.ReturnValue -eq 0) { + Write-Output "[OK] WMI process started on AD2\! PID: $($proc.ProcessId)" + } else { + Write-Output "[ERROR] WMI process creation failed with return value: $($proc.ReturnValue)" + } +} catch { + Write-Output "[ERROR] WMI failed: $_" + Write-Output "Trying schtasks approach..." + try { + $schResult = schtasks /create /s 192.168.0.6 /u INTRANET\sysadmin /p "Paper123\!@#" /tn "BulkSync-OneTime" /tr "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\BulkSync-OneTime.ps1" /sc once /st 00:00 /ru INTRANET\sysadmin /rp "Paper123\!@#" /f 2>&1 + Write-Output "schtasks create: $schResult" + $runResult = schtasks /run /s 192.168.0.6 /u INTRANET\sysadmin /p "Paper123\!@#" /tn "BulkSync-OneTime" 2>&1 + Write-Output "schtasks run: $runResult" + } catch { + Write-Output "[ERROR] schtasks also failed: $_" + } +} + +Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue diff --git a/tmp_bulksync2.ps1 b/tmp_bulksync2.ps1 new file mode 100644 index 0000000..c4535ac --- /dev/null +++ b/tmp_bulksync2.ps1 @@ -0,0 +1,29 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncPath = "\\192.168.0.6\C$\Shares\test\scripts" + +# Map net use +net use "\\192.168.0.6\C$" /user:INTRANET\sysadmin ("Paper123" + [char]33 + "@#") 2>&1 | Out-Null + +# Write the bulk sync script using direct UNC path +$bulkContent = @" +Set-Location "C:\Shares\test\scripts" +& powershell -NoProfile -ExecutionPolicy Bypass -File "C:\Shares\test\scripts\Sync-FromNAS.ps1" -MaxAgeMinutes 86400 -Verbose +"@ + +[System.IO.File]::WriteAllText("\\192.168.0.6\C$\Shares\test\scripts\BulkSync-OneTime.ps1", $bulkContent) +Write-Output "[OK] BulkSync script written via System.IO" + +# Verify it exists +$exists = Test-Path "\\192.168.0.6\C$\Shares\test\scripts\BulkSync-OneTime.ps1" +Write-Output "File exists: $exists" + +# Now trigger it via WMI +$proc = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\BulkSync-OneTime.ps1" +if ($proc.ReturnValue -eq 0) { + Write-Output "[OK] Bulk sync started on AD2\! PID: $($proc.ProcessId)" +} else { + Write-Output "[ERROR] Failed to start: return value $($proc.ReturnValue)" +} + +net use "\\192.168.0.6\C$" /delete 2>&1 | Out-Null diff --git a/tmp_check2.ps1 b/tmp_check2.ps1 new file mode 100644 index 0000000..ace3160 --- /dev/null +++ b/tmp_check2.ps1 @@ -0,0 +1,13 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +# Check if still running +$procs = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "Name='powershell.exe'" 2>$null +$syncRunning = $false +foreach ($p in $procs) { + if ($p.CommandLine -match "BulkSync|86400") { + Write-Output "Still running: PID=$($p.ProcessId)" + $syncRunning = $true + } +} +if (-not $syncRunning) { Write-Output "Bulk sync has completed\!" } diff --git a/tmp_check3.ps1 b/tmp_check3.ps1 new file mode 100644 index 0000000..f9ddc37 --- /dev/null +++ b/tmp_check3.ps1 @@ -0,0 +1,18 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +# Read the last 40 lines of the sync log +$logContent = [System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-from-nas.log") +$lines = $logContent -split "`n" +$lastLines = $lines | Select-Object -Last 40 +Write-Output "=== LAST 40 LOG LINES ===" +$lastLines | ForEach-Object { Write-Output $_ } + +# Check process status +$procs = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "Name='powershell.exe'" 2>$null +$syncRunning = $false +foreach ($p in $procs) { + if ($p.CommandLine -match "BulkSync|86400") { $syncRunning = $true } +} +Write-Output "" +if ($syncRunning) { Write-Output "STATUS: Still running..." } else { Write-Output "STATUS: COMPLETED" } diff --git a/tmp_check4.ps1 b/tmp_check4.ps1 new file mode 100644 index 0000000..fb13a84 --- /dev/null +++ b/tmp_check4.ps1 @@ -0,0 +1,18 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +# Read the last 50 lines of the sync log +$logContent = [System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-from-nas.log") +$lines = $logContent -split "`n" +$lastLines = $lines | Select-Object -Last 50 +Write-Output "=== LAST 50 LOG LINES ===" +$lastLines | ForEach-Object { Write-Output $_ } + +# Check process status +$procs = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "Name='powershell.exe'" 2>$null +$syncRunning = $false +foreach ($p in $procs) { + if ($p.CommandLine -match "BulkSync|86400") { $syncRunning = $true } +} +Write-Output "" +if ($syncRunning) { Write-Output "[...] STATUS: Still running..." } else { Write-Output "[OK] STATUS: COMPLETED" } diff --git a/tmp_check_proc.ps1 b/tmp_check_proc.ps1 new file mode 100644 index 0000000..1449063 --- /dev/null +++ b/tmp_check_proc.ps1 @@ -0,0 +1,18 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +# Check if the process is still running +$proc = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "ProcessId=15856" 2>$null +if ($proc) { + Write-Output "Process 15856 still running: $($proc.CommandLine)" +} else { + Write-Output "Process 15856 has completed" +} + +# Check for any powershell processes running our script +$procs = Get-WmiObject -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Filter "Name='powershell.exe'" 2>$null +foreach ($p in $procs) { + if ($p.CommandLine -match "Sync-FromNAS|BulkSync") { + Write-Output "Found sync process: PID=$($p.ProcessId) CMD=$($p.CommandLine)" + } +} diff --git a/tmp_connect_ad2.ps1 b/tmp_connect_ad2.ps1 new file mode 100644 index 0000000..939e7fb --- /dev/null +++ b/tmp_connect_ad2.ps1 @@ -0,0 +1,21 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop + Write-Output "=== DRIVE MAPPED ===" + $scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + if (Test-Path $scriptPath) { + Write-Output "=== SCRIPT CONTENT START ===" + Get-Content $scriptPath -Raw + Write-Output "=== SCRIPT CONTENT END ===" + } else { + Write-Output "Script not found" + } + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" +} diff --git a/tmp_fix2.ps1 b/tmp_fix2.ps1 new file mode 100644 index 0000000..83e2299 --- /dev/null +++ b/tmp_fix2.ps1 @@ -0,0 +1,37 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + $scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + $lines = Get-Content $scriptPath + + # Fix line 70 (0-indexed: 69) - the Copy-FromNAS SCP line + $oldLine = $lines[69] + Write-Output "OLD LINE 70: $oldLine" + + # The correct line should have backtick-quoted remote path and quoted local path + $lines[69] = ' $result = & $SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:`"${RemotePath}`"" "$LocalPath" 2>&1' + + Write-Output "NEW LINE 70: $($lines[69])" + + # Write back + Set-Content -Path $scriptPath -Value $lines + Write-Output "=== LINE 70 FIXED ===" + + # Verify both SCP lines are now correct + $verify = Get-Content $scriptPath + Write-Output "=== FINAL SCP LINES ===" + for ($i = 0; $i -lt $verify.Count; $i++) { + if ($verify[$i] -match "result.*SCP") { + Write-Output ("{0,3}: {1}" -f ($i+1), $verify[$i].TrimEnd()) + } + } + + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" + Write-Output $_.ScriptStackTrace +} diff --git a/tmp_fix_script.ps1 b/tmp_fix_script.ps1 new file mode 100644 index 0000000..f45866d --- /dev/null +++ b/tmp_fix_script.ps1 @@ -0,0 +1,58 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + Write-Output "=== DRIVE MAPPED ===" + + $scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + $content = Get-Content $scriptPath -Raw + + # Fix 1: Invoke-NASCommand - add user@host target + $old1 = '$result = & $SSH -i "C:\Users\sysadmin\.ssh\id_ed25519" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new $Command 2>&1' + $new1 = '$result = & $SSH -i "C:\Users\sysadmin\.ssh\id_ed25519" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" $Command 2>&1' + $content = $content.Replace($old1, $new1) + + # Fix 2: Copy-FromNAS - fix the SCP line (it has the if statement on same line + unquoted paths) + $old2 = '$result = & $SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:$RemotePath" $LocalPath 2>&1 if ($LASTEXITCODE -ne 0) {' + $new2 = @" +`$result = & `$SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "`${NAS_USER}@`${NAS_IP}:`"`${RemotePath}`"" "`$LocalPath" 2>&1 + if (`$LASTEXITCODE -ne 0) { +"@ + $content = $content.Replace($old2, $new2) + + # Fix 3: Fix the error message in Copy-FromNAS (PUSH -> PULL) + $old3 = 'Write-Log " SCP PUSH ERROR (exit $LASTEXITCODE): $errorMsg"' + # Only fix the FIRST occurrence (in Copy-FromNAS). Use a targeted approach. + # Since we already fixed the structure, just do a simple replace of the first occurrence + $idx = $content.IndexOf($old3) + if ($idx -ge 0) { + $new3 = 'Write-Log " SCP PULL ERROR (exit $LASTEXITCODE): $errorMsg"' + $content = $content.Substring(0, $idx) + $new3 + $content.Substring($idx + $old3.Length) + } + + # Fix 4: Copy-ToNAS - quote both paths + $old4 = '$result = & $SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" $LocalPath "${NAS_USER}@${NAS_IP}:$RemotePath" 2>&1' + $new4 = '$result = & $SCP -O -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:`"${RemotePath}`"" 2>&1' + $content = $content.Replace($old4, $new4) + + # Write the fixed script + Set-Content -Path $scriptPath -Value $content -NoNewline + Write-Output "=== SCRIPT UPDATED ===" + + # Verify the fixes + $verify = Get-Content $scriptPath -Raw + Write-Output "=== VERIFY: SCP LINES ===" + $verify -split "`n" | Where-Object { $_ -match "result = .* SCP" } | ForEach-Object { Write-Output (" " + $_.Trim()) } + Write-Output "=== VERIFY: SSH LINE ===" + $verify -split "`n" | Where-Object { $_ -match "result = .* SSH" } | ForEach-Object { Write-Output (" " + $_.Trim()) } + Write-Output "=== VERIFY: ERROR MESSAGES ===" + $verify -split "`n" | Where-Object { $_ -match "SCP (PULL|PUSH) ERROR" } | ForEach-Object { Write-Output (" " + $_.Trim()) } + + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" + Write-Output $_.ScriptStackTrace +} diff --git a/tmp_grep.ps1 b/tmp_grep.ps1 new file mode 100644 index 0000000..fa1dc4a --- /dev/null +++ b/tmp_grep.ps1 @@ -0,0 +1,7 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$cmd = "cmd /c powershell -NoProfile -Command \"Get-Content C:\Shares\test\scripts\sync-from-nas.log | Select-String -Pattern 'Starting sync|Max age: 86400|Sync complete|No new DAT|Found.*DAT|PULL=|ambiguous' | Select-Object -Last 30 | ForEach-Object { $_.Line } > C:\Shares\test\scripts\tail-grep.txt 2>&1\"" +$r = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList $cmd +Write-Output "PID: $($r.ProcessId)" +Start-Sleep -Seconds 15 +Write-Output ([System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\tail-grep.txt")) diff --git a/tmp_grep2.ps1 b/tmp_grep2.ps1 new file mode 100644 index 0000000..a3281e2 --- /dev/null +++ b/tmp_grep2.ps1 @@ -0,0 +1,14 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +$filterScript = 'Get-Content C:\Shares\test\scripts\sync-from-nas.log | Select-String -Pattern "Starting sync|Max age: 86400|Sync complete|No new DAT|Found.*DAT|PULL=|ambiguous|Copied to station|Copied report" | Select-Object -Last 40 | ForEach-Object { $_.Line } | Out-File C:\Shares\test\scripts\tail-grep.txt -Encoding utf8' + +[System.IO.File]::WriteAllText("\\192.168.0.6\C$\Shares\test\scripts\filter-log.ps1", $filterScript) +$r = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\filter-log.ps1" +Write-Output "PID: $($r.ProcessId)" +Start-Sleep -Seconds 20 +try { + Write-Output ([System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\tail-grep.txt")) +} catch { + Write-Output "File not ready: $_" +} diff --git a/tmp_inspect.ps1 b/tmp_inspect.ps1 new file mode 100644 index 0000000..b88f06d --- /dev/null +++ b/tmp_inspect.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + Write-Output "=== DRIVE MAPPED ===" + + $scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + $content = Get-Content $scriptPath -Raw + + # Show the original problematic lines + Write-Output "=== ORIGINAL Copy-FromNAS SCP LINE ===" + $content -split "`n" | Where-Object { $_ -match "SCP.*RemotePath.*LocalPath|SCP.*LocalPath.*RemotePath" } | ForEach-Object { Write-Output $_.Trim() } + + Write-Output "=== ORIGINAL Invoke-NASCommand LINE ===" + $content -split "`n" | Where-Object { $_ -match "result = .* \" } | ForEach-Object { Write-Output $_.Trim() } + + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" +} diff --git a/tmp_lines.ps1 b/tmp_lines.ps1 new file mode 100644 index 0000000..2e14861 --- /dev/null +++ b/tmp_lines.ps1 @@ -0,0 +1,20 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + $scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + $lines = Get-Content $scriptPath + + # Show lines 70-95 (around Copy-FromNAS SCP area) + Write-Output "=== Lines 60-95 ===" + for ($i = 59; $i -lt 95 -and $i -lt $lines.Count; $i++) { + Write-Output ("{0,3}: {1}" -f ($i+1), $lines[$i]) + } + + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" +} diff --git a/tmp_readlog.ps1 b/tmp_readlog.ps1 new file mode 100644 index 0000000..1066e96 --- /dev/null +++ b/tmp_readlog.ps1 @@ -0,0 +1,9 @@ +# Read sync log - last 60 lines +try { + $log = [System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-from-nas.log") + $logLines = $log -split "`n" + Write-Output "=== SYNC LOG (last 60 lines) ===" + $logLines | Select-Object -Last 60 | ForEach-Object { Write-Output $_ } +} catch { + Write-Output "ERROR reading log: $_" +} diff --git a/tmp_readlog2.ps1 b/tmp_readlog2.ps1 new file mode 100644 index 0000000..e69de29 diff --git a/tmp_readtail.ps1 b/tmp_readtail.ps1 new file mode 100644 index 0000000..15a6a01 --- /dev/null +++ b/tmp_readtail.ps1 @@ -0,0 +1,18 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +# Write a small script on AD2 to create the tail file +$tailScript = "Get-Content C:\Shares\test\scripts\sync-from-nas.log -Tail 80 | Out-File C:\Shares\test\scripts\sync-tail.txt -Encoding utf8; Get-Content C:\Shares\test\_SYNC_STATUS.txt | Out-File C:\Shares\test\scripts\sync-tail.txt -Append -Encoding utf8" +[System.IO.File]::WriteAllText("\\192.168.0.6\C$\Shares\test\scripts\read-tail.ps1", $tailScript) + +# Run it via WMI +$cmd = "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\read-tail.ps1" +$result = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList $cmd +Write-Output "WMI result: $($result.ReturnValue) PID: $($result.ProcessId)" + +# Wait for it to finish +Start-Sleep -Seconds 5 + +# Read the tail file +$tailContent = [System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-tail.txt") +Write-Output $tailContent diff --git a/tmp_summary.ps1 b/tmp_summary.ps1 new file mode 100644 index 0000000..0ce1312 --- /dev/null +++ b/tmp_summary.ps1 @@ -0,0 +1,10 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) + +$script = '$log = Get-Content C:\Shares\test\scripts\sync-from-nas.log; $bulk = $log | Where-Object { $_ -match "2026-02-17 16:(4|5)" }; $out = @(); $out += "=== FIRST 30 LINES ==="; $out += ($bulk | Select-Object -First 30); $out += ""; $out += "=== LAST 20 LINES ==="; $out += ($bulk | Select-Object -Last 20); $out += ""; $out += "=== SUMMARY ==="; $out += "Total lines: $($bulk.Count)"; $out += "Pushed: $(($bulk | Where-Object { $_ -match "Pushed:" }).Count)"; $out += "Errors: $(($bulk | Where-Object { $_ -match "ERROR" }).Count)"; $out += "Ambiguous: $(($bulk | Where-Object { $_ -match "ambiguous" }).Count)"; $out += "No such file: $(($bulk | Where-Object { $_ -match "No such file" }).Count)"; $out += "Copied station: $(($bulk | Where-Object { $_ -match "Copied to station" }).Count)"; $out += "Copied report: $(($bulk | Where-Object { $_ -match "Copied report" }).Count)"; $out | Out-File C:\Shares\test\scripts\sync-summary.txt -Encoding utf8' + +[System.IO.File]::WriteAllText("\\192.168.0.6\C$\Shares\test\scripts\make-summary.ps1", $script) +$r = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\test\scripts\make-summary.ps1" +Write-Output "PID: $($r.ProcessId)" +Start-Sleep -Seconds 10 +Write-Output ([System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\sync-summary.txt")) diff --git a/tmp_syntax.ps1 b/tmp_syntax.ps1 new file mode 100644 index 0000000..754906f --- /dev/null +++ b/tmp_syntax.ps1 @@ -0,0 +1,29 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + $scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + $content = Get-Content $scriptPath -Raw + + # Syntax check via parsing the content string + $parseErrors = $null + [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$parseErrors) + if ($parseErrors.Count -eq 0) { + Write-Output "[OK] Script parses OK - no syntax errors" + } else { + foreach ($err in $parseErrors) { + Write-Output "[ERROR] PARSE ERROR: $($err.Message) at line $($err.Extent.StartLineNumber)" + } + } + + # Count total lines + $lineCount = ($content -split "`n").Count + Write-Output "Total lines: $lineCount" + + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" +} diff --git a/tmp_syntax2.ps1 b/tmp_syntax2.ps1 new file mode 100644 index 0000000..b8abf95 --- /dev/null +++ b/tmp_syntax2.ps1 @@ -0,0 +1,11 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" +New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null +$content = Get-Content "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" -Raw +$tokens = $null; $parseErrors = $null +[void][System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$tokens, [ref]$parseErrors) +Write-Output "Parse errors: $($parseErrors.Count)" +if ($parseErrors.Count -gt 0) { $parseErrors | ForEach-Object { Write-Output " $_" } } +else { Write-Output "[OK] No syntax errors detected" } +Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue diff --git a/tmp_tail.ps1 b/tmp_tail.ps1 new file mode 100644 index 0000000..99141bb --- /dev/null +++ b/tmp_tail.ps1 @@ -0,0 +1,6 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$r = Invoke-WmiMethod -ComputerName 192.168.0.6 -Credential $cred -Class Win32_Process -Name Create -ArgumentList "cmd /c powershell -NoProfile -Command Get-Content C:\Shares\test\scripts\sync-from-nas.log -Tail 40 > C:\Shares\test\scripts\tail40.txt 2>&1" +Write-Output "PID: $($r.ProcessId) Return: $($r.ReturnValue)" +Start-Sleep -Seconds 8 +Write-Output ([System.IO.File]::ReadAllText("\\192.168.0.6\C$\Shares\test\scripts\tail40.txt")) diff --git a/tmp_test.ps1 b/tmp_test.ps1 new file mode 100644 index 0000000..b6be0aa --- /dev/null +++ b/tmp_test.ps1 @@ -0,0 +1,18 @@ +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" +New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + +# First, do a dry run to see what files are stranded on the NAS +Write-Output "=== DRY RUN: Bulk sync with 86400 minute window (60 days) ===" +$dryRunOutput = & powershell -NoProfile -ExecutionPolicy Bypass -Command { + # We cannot run the script directly on AD2 from here, so we will invoke it remotely +} + +# Actually we need to run this ON AD2. Lets check if we can invoke it via the mapped drive. +# The script needs to run locally on AD2 (it calls SCP/SSH). We need WinRM or SSH for that. +# Since WinRM is blocked from this machine, lets try another approach. + +# Check: can we SSH to AD2 with the key from this machine? +Write-Output "=== Testing SSH to NAS directly from this machine ===" +Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue diff --git a/tmp_verify.ps1 b/tmp_verify.ps1 new file mode 100644 index 0000000..09ace83 --- /dev/null +++ b/tmp_verify.ps1 @@ -0,0 +1,33 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + $scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + $content = Get-Content $scriptPath -Raw + + # Show the Copy-FromNAS function + $lines = $content -split "`n" + $inFunc = $false + $funcName = "" + foreach ($line in $lines) { + if ($line -match "^function (Copy-FromNAS|Copy-ToNAS|Invoke-NASCommand)") { + $inFunc = $true + $funcName = $Matches[1] + Write-Output "=== $funcName ===" + } + if ($inFunc) { + Write-Output $line.TrimEnd() + if ($line.Trim() -eq "}" -and $funcName) { + $inFunc = $false + Write-Output "" + } + } + } + + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" +} diff --git a/tmp_verify2.ps1 b/tmp_verify2.ps1 new file mode 100644 index 0000000..12f612c --- /dev/null +++ b/tmp_verify2.ps1 @@ -0,0 +1,32 @@ +$ErrorActionPreference = "Stop" +$pass = ConvertTo-SecureString ("Paper123" + [char]33 + "@#") -AsPlainText -Force +$cred = New-Object PSCredential("INTRANET\sysadmin", $pass) +$uncRoot = "\\192.168.0.6\C$" + +try { + New-PSDrive -Name AD2 -PSProvider FileSystem -Root $uncRoot -Credential $cred -ErrorAction Stop | Out-Null + $scriptPath = "AD2:\Shares\test\scripts\Sync-FromNAS.ps1" + $lines = Get-Content $scriptPath + + Write-Output "=== FULL VERIFICATION: Lines 48-100 ===" + for ($i = 47; $i -lt 100 -and $i -lt $lines.Count; $i++) { + Write-Output ("{0,3}: {1}" -f ($i+1), $lines[$i]) + } + + # Also check the script parses without errors + Write-Output "" + Write-Output "=== SYNTAX CHECK ===" + $parseErrors = $null + [System.Management.Automation.Language.Parser]::ParseFile("AD2:\Shares\test\scripts\Sync-FromNAS.ps1", [ref]$null, [ref]$parseErrors) + if ($parseErrors.Count -eq 0) { + Write-Output "Script parses OK - no syntax errors" + } else { + foreach ($err in $parseErrors) { + Write-Output "PARSE ERROR: $($err.Message) at line $($err.Extent.StartLineNumber)" + } + } + + Remove-PSDrive -Name AD2 -ErrorAction SilentlyContinue +} catch { + Write-Output "ERROR: $_" +}