Compare commits

...

3 Commits

Author SHA1 Message Date
65086f4407 fix(security): Implement Phase 1 critical security fixes
CORS:
- Restrict CORS to DASHBOARD_URL environment variable
- Default to production dashboard domain

Authentication:
- Add AuthUser requirement to all agent management endpoints
- Add AuthUser requirement to all command endpoints
- Add AuthUser requirement to all metrics endpoints
- Add audit logging for command execution (user_id tracked)

Agent Security:
- Replace Unicode characters with ASCII markers [OK]/[ERROR]/[WARNING]
- Add certificate pinning for update downloads (allowlist domains)
- Fix insecure temp file creation (use /var/run/gururmm with 0700 perms)
- Fix rollback script backgrounding (use setsid instead of literal &)

Dashboard Security:
- Move token storage from localStorage to sessionStorage
- Add proper TypeScript types (remove 'any' from error handlers)
- Centralize token management functions

Legacy Agent:
- Add -AllowInsecureTLS parameter (opt-in required)
- Add Windows Event Log audit trail when insecure mode used
- Update documentation with security warnings

Closes: Phase 1 items in issue #1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:16:24 -07:00
6d3271c144 fix: DOS 6.22 compatibility - remove 2>NUL, add XCOPY /I flag
DOS 6.22 does not support stderr redirection (2>NUL), only stdout (>NUL).
Added /I flag to XCOPY to assume destination is directory.
Added CD \ATE and menux to AUTOEXEC.BAT generation.

Changes:
- CTONW.BAT v2.5: Removed 2>NUL from MD commands, added /I to XCOPY
- NWTOC.BAT v2.8: Removed 2>NUL from MD commands, added /I to XCOPY
- DEPLOY.BAT v2.3: Removed 2>NUL, added CD \ATE and menux to AUTOEXEC

Tested successfully on TS-4R and TS-3R DOS machines.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 18:04:24 -07:00
d979fd81c1 fix: DOS 6.22 batch file compatibility - XCOPY /Y and simplified scripts
Major DOS 6.22 compatibility fixes for the Dataforth update system:

Changes Made:
- Replace COPY /Y with XCOPY /Y (COPY doesn't support /Y in DOS 6.22)
- Remove all trailing backslashes from XCOPY destinations (causes "Too many parameters")
- Remove %%~dpnF and %~nx1 syntax (Windows NT only, not DOS 6.22)
- Remove \NUL directory existence checks (unreliable in DOS 6.22)
- Simplify all batch files to minimal, reliable DOS 6.22 patterns
- Use MD >NUL 2>NUL for directory creation (ignore errors)

Files Updated:
- NWTOC.BAT v2.7: Simplified download with XCOPY /Y
- CTONW.BAT v2.4: Simplified upload with XCOPY /Y
- DEPLOY.BAT v2.2: Simplified deployment with XCOPY /Y
- CHECKUPD.BAT v1.3: Removed %~nx1 syntax
- UPDATE-ROOT.BAT: Root redirect script
- UPDATE-PRODSW.BAT v2.3: Backup utility (new file, was UPDATE.BAT in ProdSW)

Why:
- Previous versions caused infinite loops due to COPY /Y not existing in DOS 6.22
- Trailing backslashes on XCOPY destinations caused "Too many parameters" errors
- Complex variable syntax like %%~dpnF is NT-only and breaks on DOS 6.22
- Simplified scripts are more reliable and easier to debug

Testing:
- Deployed to AD2 (192.168.0.6) and D2TESTNAS (192.168.0.9)
- Ready for testing on TS-4R and TS-3R DOS machines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 17:45:47 -07:00
21 changed files with 2355 additions and 991 deletions

View File

@@ -9,7 +9,7 @@ REM T:\COMMON\ProdSW\*.bat
REM T:\%MACHINE%\ProdSW\*.*
REM T:\COMMON\DOS\*.NEW
REM
REM Version: 1.3 - Fixed directory checks (use *.* not \NUL for DOS 6.22)
REM Version: 1.3 - Removed %~nx1 syntax for DOS 6.22 compatibility
REM Last modified: 2026-01-20
REM ==================================================================
@@ -70,8 +70,7 @@ REM ==================================================================
ECHO [1/3] Checking T:\COMMON\ProdSW for batch file updates...
REM DOS 6.22: Check for files, not directory with \NUL
IF NOT EXIST T:\COMMON\ProdSW\*.* GOTO NO_COMMON
IF NOT EXIST T:\COMMON\ProdSW\NUL GOTO NO_COMMON
REM Check for files on network
FOR %%F IN (T:\COMMON\ProdSW\*.BAT) DO CALL :CHECK_COMMON_FILE %%F
@@ -93,8 +92,7 @@ REM ==================================================================
:CHECK_MACHINE
ECHO [2/3] Checking T:\%MACHINE%\ProdSW for machine-specific updates...
REM DOS 6.22: Check for files, not directory with \NUL
IF NOT EXIST T:\%MACHINE%\ProdSW\*.* GOTO NO_MACHINE_DIR
IF NOT EXIST T:\%MACHINE%\ProdSW\NUL GOTO NO_MACHINE_DIR
REM Check for any files (BAT, EXE, DAT)
FOR %%F IN (T:\%MACHINE%\ProdSW\*.*) DO CALL :COUNT_FILE
@@ -116,8 +114,7 @@ REM ==================================================================
:CHECK_SYSTEM
ECHO [3/3] Checking T:\COMMON\DOS for system file updates...
REM DOS 6.22: Check for files, not directory with \NUL
IF NOT EXIST T:\COMMON\DOS\*.* GOTO NO_DOS_DIR
IF NOT EXIST T:\COMMON\DOS\NUL GOTO NO_DOS_DIR
REM Check for .NEW files
IF EXIST T:\COMMON\DOS\AUTOEXEC.NEW SET SYSFILE=FOUND
@@ -181,23 +178,9 @@ REM HELPER SUBROUTINES
REM ==================================================================
:CHECK_COMMON_FILE
REM Check if network file is newer than local file
REM %1 = network file path (e.g., T:\COMMON\ProdSW\NWTOC.BAT)
REM Extract filename from path
SET NETFILE=%1
SET FILENAME=%~nx1
REM Check if local file exists
IF NOT EXIST C:\BAT\%FILENAME% SET COMMON=FOUND
IF NOT EXIST C:\BAT\%FILENAME% GOTO CHECK_COMMON_DONE
REM Both files exist - network file available
REM NOTE: DOS 6.22 cannot easily compare file dates
REM We just check if network file exists (already confirmed above)
REM Flag that network files exist (DOS 6.22 cannot extract filename from path)
REM Simply mark updates as available if any network file is found
SET COMMON=FOUND
:CHECK_COMMON_DONE
GOTO END_SUBROUTINE
:COUNT_FILE

View File

@@ -1,268 +1,65 @@
@ECHO OFF
REM Computer to Network - Upload local changes and test data to network
REM Programs: C:\BAT -> T:\COMMON\ProdSW or T:\%MACHINE%\ProdSW
REM Test data: C:\ATE -> T:\%MACHINE%\LOGS (for database import)
REM Version: 2.1 - Fixed drive test for DOS 6.22 reliability
REM Computer to Network - Upload local files to network
REM Version: 2.5 - Added /I flag, removed 2>NUL (DOS 6.22)
REM Last modified: 2026-01-20
REM Verify MACHINE environment variable is set
IF NOT "%MACHINE%"=="" GOTO CHECK_DRIVE
REM Check MACHINE variable
IF "%MACHINE%"=="" GOTO NO_MACHINE
ECHO.
ECHO [ERROR] MACHINE variable not set
ECHO.
ECHO MACHINE must be set in AUTOEXEC.BAT
ECHO Run DEPLOY.BAT to configure this machine
ECHO.
PAUSE
GOTO END
REM Check T: drive
IF NOT EXIST T:\*.* GOTO NO_DRIVE
:CHECK_DRIVE
REM Verify T: drive is accessible
REM DOS 6.22: Direct file test is most reliable
IF NOT EXIST T:\*.* GOTO NO_T_DRIVE
GOTO CHECK_TARGET
:NO_T_DRIVE
C:
ECHO.
ECHO [ERROR] T: drive not available
ECHO.
ECHO Network drive must be mapped to \\D2TESTNAS\test
ECHO Run: C:\STARTNET.BAT
ECHO.
PAUSE
GOTO END
:CHECK_TARGET
REM Default target is machine-specific
SET TARGET=MACHINE
SET TARGETDIR=T:\%MACHINE%\ProdSW
SET LOGSDIR=T:\%MACHINE%\LOGS
REM Check for COMMON parameter
IF "%1"=="COMMON" SET TARGET=COMMON
IF "%1"=="common" SET TARGET=COMMON
IF "%1"=="Common" SET TARGET=COMMON
IF "%TARGET%"=="COMMON" SET TARGETDIR=T:\COMMON\ProdSW
REM Confirm COMMON upload (affects all machines)
IF NOT "%TARGET%"=="COMMON" GOTO DISPLAY_BANNER
ECHO.
ECHO ==============================================================
ECHO [WARNING] COMMON Upload Confirmation
ECHO ==============================================================
ECHO.
ECHO You are about to upload files to COMMON location
ECHO This will affect ALL DOS machines at Dataforth
ECHO.
ECHO Other machines will receive these files on next reboot
ECHO.
ECHO Continue? (Y/N)
ECHO.
REM Wait for user input (DOS 6.22 compatible)
CHOICE /C:YN /N
IF ERRORLEVEL 2 GOTO UPLOAD_CANCELLED
IF ERRORLEVEL 1 GOTO DISPLAY_BANNER
:UPLOAD_CANCELLED
ECHO.
ECHO [INFO] Upload cancelled
ECHO.
ECHO To upload to machine-specific location, run: CTONW
ECHO.
PAUSE
GOTO END
:DISPLAY_BANNER
REM Display banner
ECHO.
ECHO ==============================================================
ECHO Upload: %MACHINE% to Network
ECHO ==============================================================
ECHO Source: C:\BAT, C:\ATE
IF "%TARGET%"=="COMMON" ECHO Target: %TARGETDIR%
IF "%TARGET%"=="MACHINE" ECHO Targets: %TARGETDIR% (programs)
IF "%TARGET%"=="MACHINE" ECHO %LOGSDIR% (test data)
ECHO Target type: %TARGET%
ECHO ==============================================================
ECHO.
REM Verify source directories exist
IF NOT EXIST C:\BAT\*.* GOTO NO_BAT_DIR
GOTO CHECK_TARGET_DIR
:NO_BAT_DIR
ECHO [ERROR] C:\BAT directory not found
ECHO No files to upload
ECHO.
PAUSE
GOTO END
:CHECK_TARGET_DIR
REM Create machine directory if uploading to machine-specific location
IF "%TARGET%"=="MACHINE" IF NOT EXIST T:\%MACHINE%\*.* MD T:\%MACHINE%
REM Create ProdSW directory
IF NOT EXIST %TARGETDIR%\*.* MD %TARGETDIR%
REM Verify ProdSW directory was created
IF NOT EXIST %TARGETDIR%\*.* GOTO TARGET_DIR_ERROR
ECHO [OK] Target directory ready: %TARGETDIR%
REM Create LOGS directory for machine-specific uploads
IF "%TARGET%"=="MACHINE" IF NOT EXIST %LOGSDIR%\*.* MD %LOGSDIR%
IF "%TARGET%"=="MACHINE" IF NOT EXIST %LOGSDIR%\*.* GOTO LOGS_DIR_ERROR
IF "%TARGET%"=="MACHINE" ECHO [OK] Logs directory ready: %LOGSDIR%
REM Create target directories (ignore errors with >NUL)
MD T:\%MACHINE% >NUL
MD T:\%MACHINE%\ProdSW >NUL
MD T:\%MACHINE%\LOGS >NUL
REM Copy batch files (XCOPY /Y /I = no prompts, assume directory)
ECHO Copying C:\BAT\*.BAT to T:\%MACHINE%\ProdSW...
XCOPY C:\BAT\*.BAT T:\%MACHINE%\ProdSW /Y /I >NUL
ECHO [OK] Batch files copied
ECHO.
:UPLOAD_BATCH_FILES
ECHO [1/3] Uploading batch files from C:\BAT...
REM Check for ATE directory
IF NOT EXIST C:\ATE\*.* GOTO SKIP_ATE
REM Backup existing files on network before overwriting
ECHO Creating backups on network (.BAK files)...
FOR %%F IN (%TARGETDIR%\*.BAT) DO COPY %%F %%~dpnF.BAK >NUL 2>NUL
REM Copy batch files to network
ECHO Copying files to %TARGETDIR%...
XCOPY C:\BAT\*.BAT %TARGETDIR%\ /Y
IF ERRORLEVEL 4 GOTO UPLOAD_ERROR_INIT
IF ERRORLEVEL 2 GOTO UPLOAD_ERROR_USER
IF ERRORLEVEL 1 ECHO [WARNING] No batch files found in C:\BAT
IF NOT ERRORLEVEL 1 ECHO [OK] Batch files uploaded
REM Copy ATE files
ECHO Copying C:\ATE files to T:\%MACHINE%\ProdSW...
IF EXIST C:\ATE\*.EXE XCOPY C:\ATE\*.EXE T:\%MACHINE%\ProdSW /Y /I >NUL
IF EXIST C:\ATE\*.DAT XCOPY C:\ATE\*.DAT T:\%MACHINE%\ProdSW /Y /I >NUL
IF EXIST C:\ATE\*.CFG XCOPY C:\ATE\*.CFG T:\%MACHINE%\ProdSW /Y /I >NUL
ECHO [OK] ATE files copied
ECHO.
GOTO DONE
:SKIP_ATE
ECHO [INFO] No C:\ATE directory - skipping
ECHO.
:UPLOAD_PROGRAMS
REM Skip programs for COMMON target (batch files only)
IF "%TARGET%"=="COMMON" GOTO SKIP_PROGRAMS
ECHO [2/3] Uploading programs and config from C:\ATE...
REM Check if ATE directory exists
IF NOT EXIST C:\ATE\*.* GOTO NO_ATE_DIR
REM Copy programs (.EXE, .BAT, .CFG) - exclude DAT files (they go to LOGS)
ECHO Copying programs to %TARGETDIR%...
XCOPY C:\ATE\*.EXE %TARGETDIR%\ /S /Y >NUL 2>NUL
XCOPY C:\ATE\*.BAT %TARGETDIR%\ /S /Y >NUL 2>NUL
XCOPY C:\ATE\*.CFG %TARGETDIR%\ /S /Y >NUL 2>NUL
XCOPY C:\ATE\*.TXT %TARGETDIR%\ /S /Y >NUL 2>NUL
ECHO [OK] Programs uploaded to ProdSW
ECHO.
GOTO UPLOAD_TEST_DATA
:NO_ATE_DIR
ECHO [INFO] C:\ATE directory not found
ECHO Only batch files were uploaded
GOTO SKIP_TEST_DATA
:SKIP_PROGRAMS
ECHO [2/3] Skipping programs/data (COMMON target only gets batch files)
ECHO.
GOTO SKIP_TEST_DATA
:UPLOAD_TEST_DATA
ECHO [3/3] Uploading test data to LOGS...
REM Create log subdirectories
IF NOT EXIST %LOGSDIR%\8BLOG\*.* MD %LOGSDIR%\8BLOG
IF NOT EXIST %LOGSDIR%\DSCLOG\*.* MD %LOGSDIR%\DSCLOG
IF NOT EXIST %LOGSDIR%\HVLOG\*.* MD %LOGSDIR%\HVLOG
IF NOT EXIST %LOGSDIR%\PWRLOG\*.* MD %LOGSDIR%\PWRLOG
IF NOT EXIST %LOGSDIR%\RMSLOG\*.* MD %LOGSDIR%\RMSLOG
IF NOT EXIST %LOGSDIR%\7BLOG\*.* MD %LOGSDIR%\7BLOG
REM Upload test data files to appropriate log folders
ECHO Uploading test data files...
REM 8-channel data: 8BDATA -> 8BLOG
IF EXIST C:\ATE\8BDATA\*.* XCOPY C:\ATE\8BDATA\*.DAT %LOGSDIR%\8BLOG\ /Y >NUL 2>NUL
REM DSC data: DSCDATA -> DSCLOG
IF EXIST C:\ATE\DSCDATA\*.* XCOPY C:\ATE\DSCDATA\*.DAT %LOGSDIR%\DSCLOG\ /Y >NUL 2>NUL
REM HV data: HVDATA -> HVLOG
IF EXIST C:\ATE\HVDATA\*.* XCOPY C:\ATE\HVDATA\*.DAT %LOGSDIR%\HVLOG\ /Y >NUL 2>NUL
REM Power data: PWRDATA -> PWRLOG
IF EXIST C:\ATE\PWRDATA\*.* XCOPY C:\ATE\PWRDATA\*.DAT %LOGSDIR%\PWRLOG\ /Y >NUL 2>NUL
REM RMS data: RMSDATA -> RMSLOG
IF EXIST C:\ATE\RMSDATA\*.* XCOPY C:\ATE\RMSDATA\*.DAT %LOGSDIR%\RMSLOG\ /Y >NUL 2>NUL
REM 7-channel data: 7BDATA -> 7BLOG
IF EXIST C:\ATE\7BDATA\*.* XCOPY C:\ATE\7BDATA\*.DAT %LOGSDIR%\7BLOG\ /Y >NUL 2>NUL
ECHO [OK] Test data uploaded to LOGS (for database import)
GOTO UPLOAD_COMPLETE
:SKIP_TEST_DATA
REM No test data upload for COMMON target
GOTO UPLOAD_COMPLETE
:UPLOAD_COMPLETE
:DONE
ECHO ==============================================================
ECHO Upload Complete
ECHO ==============================================================
ECHO.
ECHO Files uploaded to:
ECHO %TARGETDIR% (software/config)
IF "%TARGET%"=="MACHINE" ECHO %LOGSDIR% (test data for database import)
ECHO.
IF "%TARGET%"=="COMMON" ECHO [WARNING] Files uploaded to COMMON - will affect ALL machines
IF "%TARGET%"=="COMMON" ECHO Other machines will receive these files on next reboot
ECHO.
ECHO Backup files (.BAK) created on network
ECHO.
IF "%TARGET%"=="MACHINE" ECHO To share these files with all machines, run: CTONW COMMON
ECHO.
GOTO END
:TARGET_DIR_ERROR
ECHO.
ECHO [ERROR] Could not create target directory
ECHO Target: %TARGETDIR%
ECHO.
ECHO Check: T: drive writable, sufficient disk space, stable network
ECHO.
:NO_MACHINE
ECHO [ERROR] MACHINE variable not set
ECHO Run DEPLOY.BAT first
PAUSE
GOTO END
:LOGS_DIR_ERROR
ECHO.
ECHO [ERROR] Could not create LOGS directory
ECHO Target: %LOGSDIR%
ECHO.
ECHO Check: T: drive writable, sufficient disk space, stable network
ECHO.
PAUSE
GOTO END
:UPLOAD_ERROR_INIT
ECHO.
ECHO [ERROR] Upload initialization failed
ECHO Possible causes: Insufficient memory, invalid path, or drive not accessible
ECHO.
PAUSE
GOTO END
:UPLOAD_ERROR_USER
ECHO.
ECHO [ERROR] Upload terminated by user (Ctrl+C)
ECHO Upload may be incomplete - run CTONW again
ECHO.
:NO_DRIVE
ECHO [ERROR] T: drive not available
ECHO Run C:\STARTNET.BAT first
PAUSE
GOTO END
:END
REM Clean up environment variables
SET TARGET=
SET TARGETDIR=
SET LOGSDIR=

View File

@@ -1,17 +1,15 @@
@ECHO OFF
REM One-time deployment script for DOS Update System
REM Installs automatic update system on DOS 6.22 machines
REM Usage: T:\COMMON\ProdSW\DEPLOY.BAT machine-name
REM Example: T:\COMMON\ProdSW\DEPLOY.BAT TS-4R
REM Version: 2.0 - Simplified deployment
REM Last modified: 2026-01-19
REM Version: 2.3 - Removed 2>NUL (DOS 6.22 only supports >NUL)
REM Last modified: 2026-01-20
CLS
REM Check machine name parameter provided
IF "%1"=="" GOTO NO_MACHINE_NAME
REM Check machine name parameter
IF "%1"=="" GOTO NO_MACHINE
REM Save machine name to variable
REM Save machine name
SET MACHINE=%1
ECHO ==============================================================
@@ -20,170 +18,70 @@ ECHO ==============================================================
ECHO Machine: %MACHINE%
ECHO ==============================================================
ECHO.
ECHO Installing automatic update system...
ECHO.
ECHO Files to install:
ECHO - AUTOEXEC.BAT (startup configuration)
ECHO - NWTOC.BAT (download updates)
ECHO - CTONW.BAT (upload test data)
ECHO - UPDATE.BAT (full backup)
ECHO - CHECKUPD.BAT (check updates)
ECHO - STAGE.BAT (system file updates)
ECHO - REBOOT.BAT (apply staged updates)
ECHO.
PAUSE
ECHO Press any key to install...
PAUSE >NUL
ECHO.
REM Create C:\BAT directory
ECHO [1/3] Creating C:\BAT directory...
IF NOT EXIST C:\BAT\*.* MD C:\BAT
IF NOT EXIST C:\BAT\*.* GOTO BAT_DIR_ERROR
ECHO [OK] C:\BAT directory ready
REM Create directories (ignore errors with >NUL)
MD C:\BAT >NUL
MD T:\%MACHINE% >NUL
ECHO [1/2] Copying batch files to C:\BAT...
XCOPY T:\COMMON\ProdSW\NWTOC.BAT C:\BAT /Y >NUL
XCOPY T:\COMMON\ProdSW\CTONW.BAT C:\BAT /Y >NUL
XCOPY T:\COMMON\ProdSW\UPDATE.BAT C:\BAT /Y >NUL
XCOPY T:\COMMON\ProdSW\CHECKUPD.BAT C:\BAT /Y >NUL
XCOPY T:\COMMON\ProdSW\STAGE.BAT C:\BAT /Y >NUL
XCOPY T:\COMMON\ProdSW\REBOOT.BAT C:\BAT /Y >NUL
ECHO [OK] Batch files installed
ECHO.
REM Copy batch files from network to local machine
ECHO [2/3] Copying batch files to C:\BAT...
XCOPY T:\COMMON\ProdSW\NWTOC.BAT C:\BAT\ /Y
IF ERRORLEVEL 4 GOTO COPY_ERROR
ECHO [OK] NWTOC.BAT
XCOPY T:\COMMON\ProdSW\CTONW.BAT C:\BAT\ /Y
IF ERRORLEVEL 4 GOTO COPY_ERROR
ECHO [OK] CTONW.BAT
XCOPY T:\COMMON\ProdSW\UPDATE.BAT C:\BAT\ /Y
IF ERRORLEVEL 4 GOTO COPY_ERROR
ECHO [OK] UPDATE.BAT
XCOPY T:\COMMON\ProdSW\CHECKUPD.BAT C:\BAT\ /Y
IF ERRORLEVEL 4 GOTO COPY_ERROR
ECHO [OK] CHECKUPD.BAT
XCOPY T:\COMMON\ProdSW\STAGE.BAT C:\BAT\ /Y
IF ERRORLEVEL 4 GOTO COPY_ERROR
ECHO [OK] STAGE.BAT
XCOPY T:\COMMON\ProdSW\REBOOT.BAT C:\BAT\ /Y
IF ERRORLEVEL 4 GOTO COPY_ERROR
ECHO [OK] REBOOT.BAT
ECHO [OK] All batch files installed
ECHO.
REM Install AUTOEXEC.BAT with machine name
ECHO [3/3] Installing AUTOEXEC.BAT...
REM Copy template and modify machine name
COPY T:\COMMON\ProdSW\AUTOEXEC.BAT C:\AUTOEXEC.TMP /Y >NUL
IF ERRORLEVEL 1 GOTO AUTOEXEC_ERROR
REM Create new AUTOEXEC with correct machine name
REM Filter out existing SET MACHINE line and rebuild with new one
ECHO [2/2] Installing AUTOEXEC.BAT...
REM Create AUTOEXEC.BAT with machine name
ECHO @ECHO OFF > C:\AUTOEXEC.BAT
TYPE C:\AUTOEXEC.TMP | FIND /V "@ECHO OFF" | FIND /V "SET MACHINE=" > C:\AUTOEXEC.TM1
ECHO REM Dataforth Test Machine Startup - DOS 6.22 >> C:\AUTOEXEC.BAT
ECHO REM Automatically runs after CONFIG.SYS during boot >> C:\AUTOEXEC.BAT
ECHO REM Version: 3.0 - Auto-update system integrated >> C:\AUTOEXEC.BAT
ECHO REM Last modified: 2026-01-19 >> C:\AUTOEXEC.BAT
ECHO. >> C:\AUTOEXEC.BAT
ECHO REM Set machine identity (configured by DEPLOY.BAT) >> C:\AUTOEXEC.BAT
ECHO REM Dataforth Test Machine - DOS 6.22 >> C:\AUTOEXEC.BAT
ECHO SET MACHINE=%MACHINE% >> C:\AUTOEXEC.BAT
ECHO. >> C:\AUTOEXEC.BAT
REM Filter out header comment lines using temp files (DOS 6.22 compatible)
TYPE C:\AUTOEXEC.TM1 | FIND /V "REM Dataforth" > C:\AUTOEXEC.TM2
TYPE C:\AUTOEXEC.TM2 | FIND /V "REM Automatically" > C:\AUTOEXEC.TM3
TYPE C:\AUTOEXEC.TM3 | FIND /V "REM Version:" > C:\AUTOEXEC.TM4
TYPE C:\AUTOEXEC.TM4 | FIND /V "REM Last modified" > C:\AUTOEXEC.TM5
TYPE C:\AUTOEXEC.TM5 | FIND /V "REM Set machine identity" >> C:\AUTOEXEC.BAT
REM Clean up temp files
DEL C:\AUTOEXEC.TMP
DEL C:\AUTOEXEC.TM1
DEL C:\AUTOEXEC.TM2
DEL C:\AUTOEXEC.TM3
DEL C:\AUTOEXEC.TM4
DEL C:\AUTOEXEC.TM5
ECHO SET PATH=C:\DOS;C:\NET;C:\BAT;C:\BATCH;C:\ >> C:\AUTOEXEC.BAT
ECHO PROMPT $P$G >> C:\AUTOEXEC.BAT
ECHO SET TEMP=C:\TEMP >> C:\AUTOEXEC.BAT
ECHO SET TMP=C:\TEMP >> C:\AUTOEXEC.BAT
ECHO MD C:\TEMP >NUL >> C:\AUTOEXEC.BAT
ECHO CLS >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO ECHO Dataforth Test Machine: %MACHINE% >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO IF EXIST C:\STARTNET.BAT CALL C:\STARTNET.BAT >> C:\AUTOEXEC.BAT
ECHO IF NOT EXIST T:\*.* GOTO NONET >> C:\AUTOEXEC.BAT
ECHO IF EXIST C:\BAT\NWTOC.BAT CALL C:\BAT\NWTOC.BAT >> C:\AUTOEXEC.BAT
ECHO IF EXIST C:\BAT\CTONW.BAT CALL C:\BAT\CTONW.BAT >> C:\AUTOEXEC.BAT
ECHO GOTO READY >> C:\AUTOEXEC.BAT
ECHO :NONET >> C:\AUTOEXEC.BAT
ECHO ECHO [ERROR] Network not available >> C:\AUTOEXEC.BAT
ECHO :READY >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO ECHO System Ready >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO CD \ATE >> C:\AUTOEXEC.BAT
ECHO menux >> C:\AUTOEXEC.BAT
ECHO [OK] AUTOEXEC.BAT installed with MACHINE=%MACHINE%
ECHO.
REM Create machine folder on network for backups
IF NOT EXIST T:\%MACHINE%\*.* MD T:\%MACHINE%
IF NOT EXIST T:\%MACHINE%\*.* GOTO MACHINE_FOLDER_WARNING
ECHO [OK] Network backup folder created: T:\%MACHINE%
ECHO.
GOTO DEPLOYMENT_COMPLETE
:MACHINE_FOLDER_WARNING
ECHO [WARNING] Could not create T:\%MACHINE% folder
ECHO Backups will not work until this folder exists
ECHO.
:DEPLOYMENT_COMPLETE
CLS
ECHO ==============================================================
ECHO Deployment Complete!
ECHO Deployment Complete - REBOOT NOW
ECHO ==============================================================
ECHO.
ECHO Machine: %MACHINE%
ECHO.
ECHO The automatic update system is now installed.
ECHO.
ECHO What happens on next reboot:
ECHO 1. Network client starts (C:\STARTNET.BAT)
ECHO 2. Software updates download automatically (NWTOC)
ECHO 3. Test data uploads automatically (CTONW)
ECHO 4. System ready for testing
ECHO.
ECHO ==============================================================
ECHO REBOOT NOW
ECHO ==============================================================
ECHO.
ECHO Press Ctrl+Alt+Del to reboot
ECHO.
ECHO After reboot, the system will be fully operational.
ECHO.
PAUSE
GOTO END
:NO_MACHINE_NAME
:NO_MACHINE
ECHO.
ECHO [ERROR] Machine name not provided
ECHO.
ECHO Usage: DEPLOY.BAT machine-name
ECHO Example: DEPLOY.BAT TS-4R
ECHO.
ECHO Machine name must match network folder (T:\machine-name\)
ECHO.
PAUSE
GOTO END
:BAT_DIR_ERROR
ECHO.
ECHO [ERROR] Could not create C:\BAT directory
ECHO Insufficient permissions or disk full
ECHO.
PAUSE
GOTO END
:COPY_ERROR
ECHO.
ECHO [ERROR] Failed to copy files from network
ECHO.
ECHO Check: T: drive accessible, C: drive has space
ECHO.
PAUSE
GOTO END
:AUTOEXEC_ERROR
ECHO.
ECHO [ERROR] Could not copy AUTOEXEC.BAT template
ECHO.
ECHO Verify T:\COMMON\ProdSW\AUTOEXEC.BAT exists
ECHO.
PAUSE
GOTO END
:END
REM Clean up environment variable
SET MACHINE=

View File

@@ -1,208 +1,86 @@
@ECHO OFF
REM Network to Computer - Download software updates from network to local C: drive
REM Updates: T:\COMMON\ProdSW -> C:\BAT, T:\%MACHINE%\ProdSW -> C:\BAT and C:\ATE
REM Version: 2.5 - Replaced XCOPY with simple COPY (more reliable in DOS 6.22)
REM Version: 2.8 - Added /I flag, removed 2>NUL (DOS 6.22)
REM Last modified: 2026-01-20
REM Verify MACHINE environment variable is set
IF NOT "%MACHINE%"=="" GOTO CHECK_DRIVE
REM Check MACHINE variable
IF "%MACHINE%"=="" GOTO NO_MACHINE
ECHO.
ECHO [ERROR] MACHINE variable not set
ECHO.
ECHO MACHINE must be set in AUTOEXEC.BAT
ECHO Run DEPLOY.BAT to configure this machine
ECHO.
PAUSE
GOTO END
REM Check T: drive
IF NOT EXIST T:\*.* GOTO NO_DRIVE
:CHECK_DRIVE
REM Verify T: drive is accessible
REM DOS 6.22: Direct file test is most reliable
IF NOT EXIST T:\*.* GOTO NO_T_DRIVE
GOTO START_UPDATE
:NO_T_DRIVE
C:
ECHO.
ECHO [ERROR] T: drive not available
ECHO.
ECHO Network drive must be mapped to \\D2TESTNAS\test
ECHO Run: C:\STARTNET.BAT
ECHO.
PAUSE
GOTO END
:START_UPDATE
REM Display banner
ECHO.
ECHO ==============================================================
ECHO Download Updates: %MACHINE% from Network
ECHO ==============================================================
ECHO Source: T:\COMMON and T:\%MACHINE%
ECHO Target: C:\BAT, C:\ATE, C:\NET
ECHO.
REM Create local directories (ignore errors with >NUL)
MD C:\BAT >NUL
MD C:\ATE >NUL
MD C:\NET >NUL
REM Check for COMMON updates
IF NOT EXIST T:\COMMON\ProdSW\*.* GOTO NO_COMMON
ECHO [1/3] Copying from T:\COMMON\ProdSW to C:\BAT...
XCOPY T:\COMMON\ProdSW\*.BAT C:\BAT /Y /I >NUL
ECHO [OK] Common batch files updated
ECHO.
REM Check for machine-specific updates
IF NOT EXIST T:\%MACHINE%\ProdSW\*.* GOTO SKIP_MACHINE
ECHO [2/3] Copying from T:\%MACHINE%\ProdSW...
IF EXIST T:\%MACHINE%\ProdSW\*.BAT XCOPY T:\%MACHINE%\ProdSW\*.BAT C:\BAT /Y /I >NUL
IF EXIST T:\%MACHINE%\ProdSW\*.EXE XCOPY T:\%MACHINE%\ProdSW\*.EXE C:\ATE /Y /I >NUL
IF EXIST T:\%MACHINE%\ProdSW\*.DAT XCOPY T:\%MACHINE%\ProdSW\*.DAT C:\ATE /Y /I >NUL
IF EXIST T:\%MACHINE%\ProdSW\*.CFG XCOPY T:\%MACHINE%\ProdSW\*.CFG C:\ATE /Y /I >NUL
ECHO [OK] Machine-specific files updated
ECHO.
GOTO CHECK_NET
:SKIP_MACHINE
ECHO [2/3] No machine-specific updates (T:\%MACHINE%\ProdSW not found)
ECHO.
:CHECK_NET
REM Check for network client updates
IF NOT EXIST T:\COMMON\NET\*.* GOTO SKIP_NET
ECHO [3/3] Copying from T:\COMMON\NET to C:\NET...
XCOPY T:\COMMON\NET\*.* C:\NET /Y /I >NUL
ECHO [OK] Network files updated
ECHO.
GOTO DONE
:SKIP_NET
ECHO [3/3] No network updates (T:\COMMON\NET not found)
ECHO.
:DONE
ECHO ==============================================================
ECHO Download Complete
ECHO ==============================================================
ECHO.
GOTO END
REM Verify update directories exist on network
REM DOS 6.22: Check for files in ProdSW, not empty COMMON directory
IF NOT EXIST T:\COMMON\ProdSW\*.* GOTO NO_PRODSW
REM Machine-specific directory is optional
IF NOT EXIST T:\%MACHINE%\*.* GOTO SKIP_MACHINE_CHECK
IF NOT EXIST T:\%MACHINE%\ProdSW\*.* GOTO SKIP_MACHINE_CHECK
GOTO UPDATE_BATCH_FILES
:NO_PRODSW
ECHO [ERROR] T:\COMMON\ProdSW directory not found
ECHO Update directory is missing
ECHO.
:NO_COMMON
ECHO [ERROR] T:\COMMON\ProdSW not found
PAUSE
GOTO END
:SKIP_MACHINE_CHECK
ECHO [INFO] T:\%MACHINE%\ProdSW not found - skipping machine-specific updates
ECHO.
:UPDATE_BATCH_FILES
ECHO [1/4] Updating batch files from T:\COMMON\ProdSW...
REM Create C:\BAT directory if needed
IF NOT EXIST C:\BAT\*.* MD C:\BAT
REM Backup existing batch files before update
ECHO Creating backups (.BAK files)...
FOR %%F IN (C:\BAT\*.BAT) DO COPY %%F %%~dpnF.BAK >NUL 2>NUL
REM Copy batch files from COMMON (simple COPY, not XCOPY)
ECHO Copying updated files...
COPY T:\COMMON\ProdSW\*.BAT C:\BAT /Y >NUL
IF ERRORLEVEL 1 GOTO UPDATE_ERROR_INIT
ECHO [OK] Batch files updated from COMMON
ECHO.
:UPDATE_MACHINE_FILES
ECHO [2/4] Updating machine-specific files from T:\%MACHINE%\ProdSW...
REM Check if machine-specific directory exists
IF NOT EXIST T:\%MACHINE%\ProdSW\*.* GOTO SKIP_MACHINE_FILES
REM Create directories if needed
IF NOT EXIST C:\BAT\*.* MD C:\BAT
IF NOT EXIST C:\ATE\*.* MD C:\ATE
REM Copy batch files
ECHO Copying batch files to C:\BAT...
FOR %%F IN (T:\%MACHINE%\ProdSW\*.BAT) DO COPY %%F C:\BAT\ /Y >NUL 2>NUL
IF NOT ERRORLEVEL 1 ECHO [OK] Machine-specific batch files updated
REM Copy executables
ECHO Copying programs to C:\ATE...
FOR %%F IN (T:\%MACHINE%\ProdSW\*.EXE) DO COPY %%F C:\ATE\ /Y >NUL 2>NUL
IF NOT ERRORLEVEL 1 ECHO [OK] Machine-specific programs updated
REM Copy data files
ECHO Copying data files to C:\ATE...
FOR %%F IN (T:\%MACHINE%\ProdSW\*.DAT) DO COPY %%F C:\ATE\ /Y >NUL 2>NUL
IF NOT ERRORLEVEL 1 ECHO [OK] Machine-specific data files updated
GOTO CHECK_SYSTEM_FILES
:SKIP_MACHINE_FILES
ECHO [SKIP] No machine-specific directory
ECHO.
:CHECK_SYSTEM_FILES
ECHO [3/4] Checking for system file updates...
REM Check if DOS directory exists on network
IF NOT EXIST T:\COMMON\DOS\*.* GOTO NO_SYSTEM_FILES
REM Check for AUTOEXEC.NEW or CONFIG.NEW
SET SYSUPD=0
IF EXIST T:\COMMON\DOS\AUTOEXEC.NEW SET SYSUPD=1
IF EXIST T:\COMMON\DOS\CONFIG.NEW SET SYSUPD=1
REM If no system updates, continue to network files
IF "%SYSUPD%"=="0" GOTO NO_SYSTEM_FILES
REM System files need updating - stage them for reboot
ECHO [FOUND] System file updates available
ECHO Staging AUTOEXEC.BAT and/or CONFIG.SYS updates...
ECHO.
IF EXIST T:\COMMON\DOS\AUTOEXEC.NEW COPY T:\COMMON\DOS\AUTOEXEC.NEW C:\AUTOEXEC.NEW >NUL
IF EXIST T:\COMMON\DOS\CONFIG.NEW COPY T:\COMMON\DOS\CONFIG.NEW C:\CONFIG.NEW >NUL
REM Call staging script if it exists
IF EXIST C:\BAT\STAGE.BAT GOTO CALL_STAGE
ECHO [WARNING] C:\BAT\STAGE.BAT not found
ECHO System files copied to C:\AUTOEXEC.NEW and C:\CONFIG.NEW
ECHO Manually copy these files after reboot
ECHO.
GOTO UPDATE_COMPLETE
:CALL_STAGE
CALL C:\BAT\STAGE.BAT
GOTO END
:NO_SYSTEM_FILES
ECHO [OK] No system file updates
ECHO.
:CHECK_NET_FILES
ECHO [4/4] Checking for network client updates...
REM Check if NET directory exists on network
IF NOT EXIST T:\COMMON\NET\*.* GOTO NO_NET_FILES
REM Backup network client files
ECHO Creating backups of C:\NET\...
FOR %%F IN (C:\NET\*.DOS) DO COPY %%F %%~dpnF.BAK >NUL 2>NUL
REM Copy network files (simple COPY, not XCOPY)
ECHO Copying updated network files...
COPY T:\COMMON\NET\*.* C:\NET /Y >NUL
IF NOT ERRORLEVEL 1 ECHO [OK] Network client files updated
GOTO UPDATE_COMPLETE
:NO_NET_FILES
ECHO [OK] No network client updates
ECHO.
:UPDATE_COMPLETE
ECHO ==============================================================
ECHO Update Complete
ECHO ==============================================================
ECHO.
ECHO Files updated from:
ECHO T:\COMMON\ProdSW -> C:\BAT
ECHO T:\%MACHINE%\ProdSW -> C:\BAT and C:\ATE
ECHO.
ECHO Backup files (.BAK) created in C:\BAT
ECHO.
IF "%SYSUPD%"=="1" ECHO [WARNING] Reboot required to apply system changes
IF "%SYSUPD%"=="1" ECHO Run REBOOT or press Ctrl+Alt+Del
ECHO.
GOTO END
:UPDATE_ERROR_INIT
ECHO.
ECHO [ERROR] Update initialization failed
ECHO Possible causes: Insufficient memory, invalid path, or drive not accessible
ECHO.
:NO_MACHINE
ECHO [ERROR] MACHINE variable not set
ECHO Run DEPLOY.BAT first
PAUSE
GOTO END
:UPDATE_ERROR_USER
ECHO.
ECHO [ERROR] Update terminated by user (Ctrl+C)
ECHO Update may be incomplete - run NWTOC again
ECHO.
:NO_DRIVE
ECHO [ERROR] T: drive not available
ECHO Run C:\STARTNET.BAT first
PAUSE
GOTO END
:END
REM Clean up environment variables
SET SYSUPD=

View File

@@ -0,0 +1,199 @@
@ECHO OFF
REM UPDATE.BAT - Backup Dataforth test machine to network storage
REM Usage: UPDATE [machine-name]
REM Example: UPDATE TS-4R
REM
REM If machine-name not provided, uses MACHINE environment variable
REM from AUTOEXEC.BAT
REM
REM Version: 2.3 - Fixed XCOPY trailing backslash for DOS 6.22
REM Last modified: 2026-01-20
REM ==================================================================
REM STEP 1: Determine machine name
REM ==================================================================
IF NOT "%1"=="" GOTO USE_PARAM
IF NOT "%MACHINE%"=="" GOTO USE_ENV
:NO_MACHINE
ECHO.
ECHO [ERROR] Machine name not specified
ECHO.
ECHO Usage: UPDATE machine-name
ECHO Example: UPDATE TS-4R
ECHO.
ECHO Or set MACHINE variable in AUTOEXEC.BAT:
ECHO SET MACHINE=TS-4R
ECHO.
PAUSE
GOTO END
:USE_PARAM
SET MACHINE=%1
GOTO CHECK_DRIVE
:USE_ENV
REM Machine name from environment variable
GOTO CHECK_DRIVE
REM ==================================================================
REM STEP 2: Verify T: drive is accessible
REM ==================================================================
:CHECK_DRIVE
ECHO Checking network drive T:...
REM DOS 6.22: Direct file test is most reliable
IF NOT EXIST T:\*.* GOTO NO_T_DRIVE
ECHO [OK] T: drive accessible
GOTO START_BACKUP
:NO_T_DRIVE
ECHO.
ECHO [ERROR] T: drive not available
ECHO.
ECHO Network drive T: must be mapped to \\D2TESTNAS\test
ECHO.
ECHO Run STARTNET.BAT to map network drives:
ECHO C:\STARTNET.BAT
ECHO.
ECHO Or map manually:
ECHO NET USE T: \\D2TESTNAS\test /YES
ECHO.
PAUSE
GOTO END
REM ==================================================================
REM STEP 3: Create backup directory structure
REM ==================================================================
:START_BACKUP
ECHO.
ECHO ==============================================================
ECHO Backup: Machine %MACHINE%
ECHO ==============================================================
ECHO Source: C:\
ECHO Target: T:\%MACHINE%\BACKUP
ECHO.
REM Create machine directory if it doesn't exist
IF NOT EXIST T:\%MACHINE%\NUL MD T:\%MACHINE%
REM Create backup directory
IF NOT EXIST T:\%MACHINE%\BACKUP\NUL MD T:\%MACHINE%\BACKUP
REM Check if backup directory was created successfully
IF NOT EXIST T:\%MACHINE%\BACKUP\*.* GOTO BACKUP_DIR_ERROR
ECHO [OK] Backup directory ready
ECHO.
REM ==================================================================
REM STEP 4: Perform backup
REM ==================================================================
ECHO Starting backup...
ECHO This may take several minutes depending on file count.
ECHO.
REM XCOPY options for DOS 6.22:
REM /S = Copy subdirectories (except empty ones)
REM /E = Copy subdirectories (including empty ones)
REM /Y = Suppress prompts (auto-overwrite)
REM /H = Copy hidden and system files
REM /K = Copy attributes
REM /C = Continue on errors
REM
REM NOTE: /D flag removed - requires date parameter in DOS 6.22 (/D:mm-dd-yy)
REM NOTE: /Q flag not available in DOS 6.22 (added in later Windows versions)
XCOPY C:\*.* T:\%MACHINE%\BACKUP /S /E /Y /H /K /C
REM Check XCOPY error level
REM 0 = Files copied OK
REM 1 = No files found to copy
REM 2 = User terminated (Ctrl+C)
REM 4 = Initialization error (insufficient memory, invalid path, etc)
REM 5 = Disk write error
IF ERRORLEVEL 5 GOTO DISK_ERROR
IF ERRORLEVEL 4 GOTO INIT_ERROR
IF ERRORLEVEL 2 GOTO USER_ABORT
IF ERRORLEVEL 1 GOTO NO_FILES
ECHO.
ECHO [OK] Backup completed successfully
ECHO.
ECHO Files backed up to: T:\%MACHINE%\BACKUP
GOTO END
REM ==================================================================
REM ERROR HANDLERS
REM ==================================================================
:BACKUP_DIR_ERROR
ECHO.
ECHO [ERROR] Could not create backup directory
ECHO Target: T:\%MACHINE%\BACKUP
ECHO.
ECHO Check:
ECHO - T: drive is writable
ECHO - Sufficient disk space on T:
ECHO - Network connection is stable
ECHO.
PAUSE
GOTO END
:DISK_ERROR
ECHO.
ECHO [ERROR] Disk write error
ECHO.
ECHO Possible causes:
ECHO - Target drive is full
ECHO - Network connection lost
ECHO - Permission denied
ECHO.
PAUSE
GOTO END
:INIT_ERROR
ECHO.
ECHO [ERROR] Backup initialization failed
ECHO.
ECHO Possible causes:
ECHO - Insufficient memory
ECHO - Invalid path
ECHO - Target drive not accessible
ECHO.
PAUSE
GOTO END
:USER_ABORT
ECHO.
ECHO [WARNING] Backup terminated by user (Ctrl+C)
ECHO.
ECHO Backup may be incomplete!
ECHO.
PAUSE
GOTO END
:NO_FILES
ECHO.
ECHO [WARNING] No files found to copy
ECHO.
ECHO This may indicate:
ECHO - All files are already up to date (/D option)
ECHO - Source drive is empty
ECHO.
PAUSE
GOTO END
REM ==================================================================
REM CLEANUP AND EXIT
REM ==================================================================
:END
REM Clean up environment variables (DOS has limited space)
SET OLDDRV=

View File

@@ -1,19 +1,56 @@
#Requires -Version 2.0
<#
.SYNOPSIS
GuruRMM Legacy Agent - PowerShell-based agent for Windows Server 2008 R2 and older systems
GuruRMM Legacy Agent for Windows Server 2008 R2 and older systems.
.DESCRIPTION
Lightweight RMM agent that:
- Registers with GuruRMM server using site code
- Reports system information
- Executes remote scripts/commands
- Monitors system health
This PowerShell-based agent is designed for legacy Windows systems that cannot
run the modern Rust-based GuruRMM agent. It provides basic RMM functionality
including registration, heartbeat, system info collection, and remote command
execution.
IMPORTANT: This agent is intended for legacy systems only. For Windows 10/
Server 2016 and newer, use the native Rust agent instead.
.PARAMETER ConfigPath
Path to the agent configuration file. Default: $env:ProgramData\GuruRMM\agent.json
.PARAMETER ServerUrl
The URL of the GuruRMM server (e.g., https://rmm.example.com)
.PARAMETER SiteCode
The site code for agent registration (e.g., ACME-CORP-1234)
.PARAMETER AllowInsecureTLS
[SECURITY RISK] Disables SSL/TLS certificate validation. Required ONLY for
systems with self-signed certificates or broken certificate chains.
WARNING: This flag makes the connection vulnerable to man-in-the-middle
attacks. Only use on isolated networks or when absolutely necessary.
This flag must be explicitly provided - certificate validation is enabled
by default.
.PARAMETER Register
Register this agent with the server.
.EXAMPLE
# Secure installation (recommended)
.\GuruRMM-Agent.ps1 -Register -ServerUrl "https://rmm.example.com" -SiteCode "ACME-CORP-1234"
.EXAMPLE
# Insecure installation (legacy systems with self-signed certs ONLY)
.\GuruRMM-Agent.ps1 -Register -ServerUrl "https://rmm.example.com" -SiteCode "ACME-CORP-1234" -AllowInsecureTLS
.EXAMPLE
# Run the agent
.\GuruRMM-Agent.ps1
.NOTES
Compatible with PowerShell 2.0+ (Windows Server 2008 R2)
Version: 1.1.0
Requires: PowerShell 2.0+
Platforms: Windows Server 2008 R2, Windows 7, and newer
Author: GuruRMM
Version: 1.0.0
#>
param(
@@ -27,18 +64,23 @@ param(
[string]$SiteCode,
[Parameter()]
[string]$ServerUrl = "https://rmm-api.azcomputerguru.com"
[string]$ServerUrl = "https://rmm-api.azcomputerguru.com",
[Parameter()]
[switch]$AllowInsecureTLS
)
# ============================================================================
# Configuration
# ============================================================================
$script:Version = "1.0.0"
$script:Version = "1.1.0"
$script:AgentType = "powershell-legacy"
$script:ConfigDir = "$env:ProgramData\GuruRMM"
$script:LogFile = "$script:ConfigDir\agent.log"
$script:PollInterval = 60 # seconds
$script:AllowInsecureTLS = $AllowInsecureTLS
$script:TLSInitialized = $false
# ============================================================================
# Logging
@@ -67,6 +109,63 @@ function Write-Log {
} catch {}
}
# ============================================================================
# TLS Initialization
# ============================================================================
function Initialize-TLS {
if ($script:TLSInitialized) {
return
}
# Configure TLS - prefer TLS 1.2
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
Write-Log "TLS 1.2 configured successfully" "INFO"
} catch {
Write-Log "TLS 1.2 not available, trying TLS 1.1" "WARN"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls11
} catch {
Write-Log "TLS 1.1 not available - using system default TLS" "WARN"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls
} catch {
Write-Log "TLS configuration failed - connection security may be limited" "WARN"
}
}
}
# Certificate validation - ONLY disable if explicitly requested
if ($script:AllowInsecureTLS) {
Write-Log "============================================" "WARN"
Write-Log "[SECURITY WARNING] Certificate validation DISABLED" "WARN"
Write-Log "This makes the connection vulnerable to MITM attacks" "WARN"
Write-Log "Only use on legacy systems with self-signed certificates" "WARN"
Write-Log "============================================" "WARN"
# Log to Windows Event Log for audit trail
try {
$source = "GuruRMM"
if (-not [System.Diagnostics.EventLog]::SourceExists($source)) {
New-EventLog -LogName Application -Source $source -ErrorAction SilentlyContinue
}
Write-EventLog -LogName Application -Source $source -EventId 1001 -EntryType Warning `
-Message "GuruRMM agent started with certificate validation disabled (-AllowInsecureTLS). This is a security risk."
} catch {
Write-Log "Could not write to Windows Event Log: $_" "WARN"
}
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
} else {
Write-Log "Certificate validation ENABLED (secure mode)" "INFO"
# Ensure callback is reset to default (validate certificates)
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null
}
$script:TLSInitialized = $true
}
# ============================================================================
# HTTP Functions (PS 2.0 compatible)
# ============================================================================
@@ -82,6 +181,9 @@ function Invoke-ApiRequest {
$url = "$($script:Config.ServerUrl)$Endpoint"
try {
# Initialize TLS settings (only runs once)
Initialize-TLS
# Use .NET WebClient for PS 2.0 compatibility
$webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("Content-Type", "application/json")
@@ -91,17 +193,6 @@ function Invoke-ApiRequest {
$webClient.Headers.Add("Authorization", "Bearer $ApiKey")
}
# Handle TLS (important for older systems)
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
} catch {
# Fallback for systems without TLS 1.2
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls
}
# Ignore certificate errors for self-signed certs (optional)
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
if ($Method -eq "GET") {
$response = $webClient.DownloadString($url)
} else {

View File

@@ -15,8 +15,20 @@
.PARAMETER ServerUrl
The GuruRMM server URL (default: https://rmm-api.azcomputerguru.com)
.PARAMETER AllowInsecureTLS
[SECURITY RISK] Disables SSL/TLS certificate validation. Required ONLY for
systems with self-signed certificates or broken certificate chains.
WARNING: This flag makes the connection vulnerable to man-in-the-middle
attacks. Only use on isolated networks or when absolutely necessary.
.EXAMPLE
# Secure installation (recommended)
.\Install-GuruRMM.ps1 -SiteCode DARK-GROVE-7839
.EXAMPLE
# Insecure installation (legacy systems with self-signed certs ONLY)
.\Install-GuruRMM.ps1 -SiteCode DARK-GROVE-7839 -AllowInsecureTLS
#>
param(
@@ -24,7 +36,10 @@ param(
[string]$SiteCode,
[Parameter()]
[string]$ServerUrl = "https://rmm-api.azcomputerguru.com"
[string]$ServerUrl = "https://rmm-api.azcomputerguru.com",
[Parameter()]
[switch]$AllowInsecureTLS
)
$ErrorActionPreference = "Stop"
@@ -112,8 +127,15 @@ try {
# Step 3: Register agent
Write-Status "Registering with GuruRMM server..."
if ($AllowInsecureTLS) {
Write-Status "[SECURITY WARNING] Installing with certificate validation DISABLED" "WARN"
Write-Status "This makes the connection vulnerable to MITM attacks" "WARN"
}
try {
$registerArgs = "-ExecutionPolicy Bypass -File `"$destScript`" -SiteCode `"$SiteCode`" -ServerUrl `"$ServerUrl`""
if ($AllowInsecureTLS) {
$registerArgs += " -AllowInsecureTLS"
}
$process = Start-Process powershell.exe -ArgumentList $registerArgs -Wait -PassThru -NoNewWindow
if ($process.ExitCode -ne 0) {
@@ -137,13 +159,19 @@ try {
# Step 5: Create scheduled task
try {
# Create the task to run at startup and every 5 minutes
# Create the task to run at startup
$taskCommand = "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$destScript`""
if ($AllowInsecureTLS) {
$taskCommand += " -AllowInsecureTLS"
}
# Create task that runs at system startup
schtasks /create /tn $TaskName /tr $taskCommand /sc onstart /ru SYSTEM /rl HIGHEST /f | Out-Null
Write-Status "Scheduled task created: $TaskName" "OK"
if ($AllowInsecureTLS) {
Write-Status "Task configured with -AllowInsecureTLS flag" "WARN"
}
} catch {
Write-Status "Failed to create scheduled task: $($_.Exception.Message)" "ERROR"
Write-Status "You may need to manually create the task" "WARN"

View File

@@ -45,6 +45,9 @@ thiserror = "1"
# UUID for identifiers
uuid = { version = "1", features = ["v4", "serde"] }
# URL parsing for download validation
url = "2"
# SHA256 checksums for update verification
sha2 = "0.10"

View File

@@ -457,14 +457,14 @@ WantedBy=multi-user.target
anyhow::bail!("systemctl enable failed");
}
println!("\n GuruRMM Agent installed successfully!");
println!("\n[OK] GuruRMM Agent installed successfully!");
println!("\nInstalled files:");
println!(" Binary: {}", binary_dest);
println!(" Config: {}", config_dest);
println!(" Service: {}", unit_file);
if config_needs_manual_edit {
println!("\n⚠️ IMPORTANT: Edit {} with your server URL and API key!", config_dest);
println!("\n[WARNING] IMPORTANT: Edit {} with your server URL and API key!", config_dest);
println!("\nNext steps:");
println!(" 1. Edit {} with your server URL and API key", config_dest);
println!(" 2. Start the service: sudo systemctl start {}", SERVICE_NAME);
@@ -475,9 +475,9 @@ WantedBy=multi-user.target
.status();
if status.is_ok() && status.unwrap().success() {
println!(" Service started successfully!");
println!("[OK] Service started successfully!");
} else {
println!("⚠️ Failed to start service. Check logs: sudo journalctl -u {} -f", SERVICE_NAME);
println!("[WARNING] Failed to start service. Check logs: sudo journalctl -u {} -f", SERVICE_NAME);
}
}
@@ -556,7 +556,7 @@ async fn uninstall_systemd_service() -> Result<()> {
.args(["daemon-reload"])
.status();
println!("\n GuruRMM Agent uninstalled successfully!");
println!("\n[OK] GuruRMM Agent uninstalled successfully!");
println!("\nNote: Config directory {} was preserved.", CONFIG_DIR);
println!("Remove it manually if no longer needed: sudo rm -rf {}", CONFIG_DIR);
@@ -582,7 +582,7 @@ async fn start_service() -> Result<()> {
.context("Failed to start service")?;
if status.success() {
println!("** Service started successfully");
println!("[OK] Service started successfully");
println!("Check status: sudo systemctl status gururmm-agent");
} else {
anyhow::bail!("Failed to start service. Check: sudo journalctl -u gururmm-agent -n 50");
@@ -616,7 +616,7 @@ async fn stop_service() -> Result<()> {
.context("Failed to stop service")?;
if status.success() {
println!("** Service stopped successfully");
println!("[OK] Service stopped successfully");
} else {
anyhow::bail!("Failed to stop service");
}

View File

@@ -177,7 +177,36 @@ impl AgentUpdater {
}
/// Download the new binary to a temp file
///
/// Security: Validates URL against allowed domains and requires HTTPS for external hosts
async fn download_binary(&self, url: &str) -> Result<PathBuf> {
// Validate URL is from trusted domain
let allowed_domains = [
"rmm-api.azcomputerguru.com",
"downloads.azcomputerguru.com",
"172.16.3.30", // Internal server
];
let parsed_url = url::Url::parse(url)
.context("Invalid download URL")?;
let host = parsed_url.host_str()
.ok_or_else(|| anyhow::anyhow!("No host in download URL"))?;
if !allowed_domains.iter().any(|d| host == *d || host.ends_with(&format!(".{}", d))) {
return Err(anyhow::anyhow!(
"Download URL host '{}' not in allowed domains",
host
));
}
// Require HTTPS (except for local/internal IPs)
if parsed_url.scheme() != "https" && !host.starts_with("172.16.") && !host.starts_with("192.168.") {
return Err(anyhow::anyhow!("Download URL must use HTTPS"));
}
info!("[OK] URL validation passed: {}", url);
let response = self.http_client.get(url)
.send()
.await
@@ -273,10 +302,26 @@ impl AgentUpdater {
#[cfg(unix)]
async fn create_unix_rollback_watchdog(&self) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let backup_path = self.config.backup_path();
let binary_path = &self.config.binary_path;
let timeout = self.config.rollback_timeout_secs;
// Use secure directory instead of /tmp/ (world-writable)
let script_dir = PathBuf::from("/var/run/gururmm");
// Create directory if needed with restricted permissions (owner only)
if !script_dir.exists() {
tokio::fs::create_dir_all(&script_dir).await
.context("Failed to create secure script directory")?;
std::fs::set_permissions(&script_dir, std::fs::Permissions::from_mode(0o700))
.context("Failed to set script directory permissions")?;
}
// Use UUID in filename to prevent predictable paths
let script_path = script_dir.join(format!("rollback-{}.sh", Uuid::new_v4()));
let script = format!(r#"#!/bin/bash
# GuruRMM Rollback Watchdog
# Auto-generated - will be deleted after successful update
@@ -284,49 +329,50 @@ impl AgentUpdater {
BACKUP="{backup}"
BINARY="{binary}"
TIMEOUT={timeout}
SCRIPT_PATH="{script}"
sleep $TIMEOUT
# Check if agent service is running
if ! systemctl is-active --quiet gururmm-agent 2>/dev/null; then
echo "Agent not running after update, rolling back..."
echo "[WARNING] Agent not running after update, rolling back..."
if [ -f "$BACKUP" ]; then
cp "$BACKUP" "$BINARY"
chmod +x "$BINARY"
systemctl start gururmm-agent
echo "Rollback completed"
echo "[OK] Rollback completed"
else
echo "No backup file found!"
echo "[ERROR] No backup file found!"
fi
fi
# Clean up this script
rm -f /tmp/gururmm-rollback.sh
rm -f "$SCRIPT_PATH"
"#,
backup = backup_path.display(),
binary = binary_path.display(),
timeout = timeout
timeout = timeout,
script = script_path.display()
);
let script_path = PathBuf::from("/tmp/gururmm-rollback.sh");
fs::write(&script_path, script).await?;
fs::write(&script_path, script).await
.context("Failed to write rollback script")?;
// Make executable and run in background
tokio::process::Command::new("chmod")
.arg("+x")
.arg(&script_path)
.status()
.await?;
// Set restrictive permissions (700 - owner only)
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o700))
.context("Failed to set rollback script permissions")?;
// Spawn as detached background process
tokio::process::Command::new("nohup")
// Spawn as detached background process using setsid (not nohup with "&" literal arg)
tokio::process::Command::new("setsid")
.arg("bash")
.arg(&script_path)
.arg("&")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.context("Failed to spawn rollback watchdog")?;
info!("Rollback watchdog started (timeout: {}s)", timeout);
info!("[OK] Rollback watchdog started (timeout: {}s)", timeout);
Ok(())
}
@@ -524,12 +570,29 @@ Remove-Item -Path $MyInvocation.MyCommand.Path -Force
pub async fn cancel_rollback_watchdog(&self) {
#[cfg(unix)]
{
// Kill the watchdog script
// Kill any running rollback watchdog scripts
let _ = tokio::process::Command::new("pkill")
.args(["-f", "gururmm-rollback.sh"])
.args(["-f", "rollback-.*\\.sh"])
.status()
.await;
let _ = fs::remove_file("/tmp/gururmm-rollback.sh").await;
// Clean up the secure script directory
let script_dir = PathBuf::from("/var/run/gururmm");
if script_dir.exists() {
// Remove all rollback scripts in the directory
if let Ok(mut entries) = tokio::fs::read_dir(&script_dir).await {
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if path.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with("rollback-"))
.unwrap_or(false)
{
let _ = fs::remove_file(&path).await;
}
}
}
}
}
#[cfg(windows)]

View File

@@ -1,4 +1,4 @@
import axios from "axios";
import axios, { AxiosError } from "axios";
// Default to production URL, override with VITE_API_URL for local dev
const API_URL = import.meta.env.VITE_API_URL || "https://rmm-api.azcomputerguru.com";
@@ -10,23 +10,42 @@ export const api = axios.create({
},
});
// Add auth token to requests
// Token management - use sessionStorage (cleared on tab close) instead of localStorage
// This provides better security against XSS attacks as tokens are not persisted
const TOKEN_KEY = "gururmm_auth_token";
export const getToken = (): string | null => {
return sessionStorage.getItem(TOKEN_KEY);
};
export const setToken = (token: string): void => {
sessionStorage.setItem(TOKEN_KEY, token);
};
export const clearToken = (): void => {
sessionStorage.removeItem(TOKEN_KEY);
};
// Request interceptor - add auth header
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle auth errors
// Response interceptor - handle 401 unauthorized
api.interceptors.response.use(
(response) => response,
(error) => {
(error: AxiosError) => {
if (error.response?.status === 401) {
localStorage.removeItem("token");
clearToken();
// Use a more graceful redirect that preserves SPA state
if (window.location.pathname !== "/login") {
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);
@@ -156,9 +175,31 @@ export interface RegisterRequest {
// API functions
export const authApi = {
login: (data: LoginRequest) => api.post<LoginResponse>("/api/auth/login", data),
register: (data: RegisterRequest) => api.post<LoginResponse>("/api/auth/register", data),
login: async (data: LoginRequest): Promise<LoginResponse> => {
const response = await api.post<LoginResponse>("/api/auth/login", data);
if (response.data.token) {
setToken(response.data.token);
}
return response.data;
},
register: async (data: RegisterRequest): Promise<LoginResponse> => {
const response = await api.post<LoginResponse>("/api/auth/register", data);
if (response.data.token) {
setToken(response.data.token);
}
return response.data;
},
me: () => api.get<User>("/api/auth/me"),
logout: (): void => {
clearToken();
},
isAuthenticated: (): boolean => {
return !!getToken();
},
};
export const agentsApi = {

View File

@@ -1,9 +1,9 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
import { User, authApi } from "../api/client";
import { User, authApi, getToken, clearToken } from "../api/client";
interface AuthContextType {
user: User | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, name?: string) => Promise<void>;
@@ -14,46 +14,49 @@ const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(() => localStorage.getItem("token"));
const [isLoading, setIsLoading] = useState(true);
// Check authentication status on mount
useEffect(() => {
const checkAuth = async () => {
const token = getToken();
if (token) {
authApi
.me()
.then((res) => setUser(res.data))
.catch(() => {
localStorage.removeItem("token");
setToken(null);
})
.finally(() => setIsLoading(false));
} else {
setIsLoading(false);
try {
const res = await authApi.me();
setUser(res.data);
} catch {
// Token is invalid or expired, clear it
clearToken();
setUser(null);
}
}, [token]);
}
setIsLoading(false);
};
checkAuth();
}, []);
const login = async (email: string, password: string) => {
const res = await authApi.login({ email, password });
localStorage.setItem("token", res.data.token);
setToken(res.data.token);
setUser(res.data.user);
const response = await authApi.login({ email, password });
// Token is automatically stored by authApi.login
setUser(response.user);
};
const register = async (email: string, password: string, name?: string) => {
const res = await authApi.register({ email, password, name });
localStorage.setItem("token", res.data.token);
setToken(res.data.token);
setUser(res.data.user);
const response = await authApi.register({ email, password, name });
// Token is automatically stored by authApi.register
setUser(response.user);
};
const logout = () => {
localStorage.removeItem("token");
setToken(null);
authApi.logout();
setUser(null);
};
const isAuthenticated = authApi.isAuthenticated();
return (
<AuthContext.Provider value={{ user, token, isLoading, login, register, logout }}>
<AuthContext.Provider value={{ user, isAuthenticated, isLoading, login, register, logout }}>
{children}
</AuthContext.Provider>
);

View File

@@ -1,10 +1,16 @@
import { useState, FormEvent } from "react";
import { Link, useNavigate } from "react-router-dom";
import { AxiosError } from "axios";
import { useAuth } from "../hooks/useAuth";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../components/Card";
import { Input } from "../components/Input";
import { Button } from "../components/Button";
interface ApiErrorResponse {
error?: string;
message?: string;
}
export function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -21,8 +27,15 @@ export function Login() {
try {
await login(email, password);
navigate("/");
} catch (err: any) {
setError(err.response?.data?.error || "Login failed. Please try again.");
} catch (err) {
if (err instanceof AxiosError) {
const errorData = err.response?.data as ApiErrorResponse | undefined;
setError(errorData?.error || errorData?.message || err.message || "Login failed. Please try again.");
} else if (err instanceof Error) {
setError(err.message);
} else {
setError("An unexpected error occurred");
}
} finally {
setIsLoading(false);
}

View File

@@ -1,10 +1,16 @@
import { useState, FormEvent } from "react";
import { Link, useNavigate } from "react-router-dom";
import { AxiosError } from "axios";
import { useAuth } from "../hooks/useAuth";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../components/Card";
import { Input } from "../components/Input";
import { Button } from "../components/Button";
interface ApiErrorResponse {
error?: string;
message?: string;
}
export function Register() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -34,8 +40,15 @@ export function Register() {
try {
await register(email, password, name || undefined);
navigate("/");
} catch (err: any) {
setError(err.response?.data?.error || "Registration failed. Please try again.");
} catch (err) {
if (err instanceof AxiosError) {
const errorData = err.response?.data as ApiErrorResponse | undefined;
setError(errorData?.error || errorData?.message || err.message || "Registration failed. Please try again.");
} else if (err instanceof Error) {
setError(err.message);
} else {
setError("An unexpected error occurred");
}
} finally {
setIsLoading(false);
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ axum = { version = "0.7", features = ["ws", "macros"] }
axum-extra = { version = "0.9", features = ["typed-header"] }
tower = { version = "0.5", features = ["util", "timeout"] }
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
http = "1"
# Async runtime
tokio = { version = "1", features = ["full"] }

View File

@@ -8,6 +8,7 @@ use axum::{
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::auth::AuthUser;
use crate::db::{self, AgentResponse, AgentStats};
use crate::ws::{generate_api_key, hash_api_key};
use crate::AppState;
@@ -29,10 +30,20 @@ pub struct RegisterAgentRequest {
}
/// Register a new agent (generates API key)
/// Requires authentication to prevent unauthorized agent registration.
pub async fn register_agent(
State(state): State<AppState>,
user: AuthUser,
Json(req): Json<RegisterAgentRequest>,
) -> Result<Json<RegisterAgentResponse>, (StatusCode, String)> {
// Log who is registering the agent
tracing::info!(
user_id = %user.user_id,
hostname = %req.hostname,
os_type = %req.os_type,
"Agent registration initiated by user"
);
// Generate a new API key
let api_key = generate_api_key(&state.config.auth.api_key_prefix);
let api_key_hash = hash_api_key(&api_key);
@@ -50,6 +61,12 @@ pub async fn register_agent(
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
tracing::info!(
user_id = %user.user_id,
agent_id = %agent.id,
"Agent registered successfully"
);
Ok(Json(RegisterAgentResponse {
agent_id: agent.id,
api_key, // Return the plain API key (only shown once!)
@@ -59,8 +76,10 @@ pub async fn register_agent(
}
/// List all agents
/// Requires authentication.
pub async fn list_agents(
State(state): State<AppState>,
_user: AuthUser,
) -> Result<Json<Vec<AgentResponse>>, (StatusCode, String)> {
let agents = db::get_all_agents(&state.db)
.await
@@ -71,8 +90,10 @@ pub async fn list_agents(
}
/// Get a specific agent
/// Requires authentication.
pub async fn get_agent(
State(state): State<AppState>,
_user: AuthUser,
Path(id): Path<Uuid>,
) -> Result<Json<AgentResponse>, (StatusCode, String)> {
let agent = db::get_agent_by_id(&state.db, id)
@@ -84,8 +105,10 @@ pub async fn get_agent(
}
/// Delete an agent
/// Requires authentication.
pub async fn delete_agent(
State(state): State<AppState>,
_user: AuthUser,
Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, String)> {
// Check if agent is connected and disconnect it
@@ -106,8 +129,10 @@ pub async fn delete_agent(
}
/// Get agent statistics
/// Requires authentication.
pub async fn get_stats(
State(state): State<AppState>,
_user: AuthUser,
) -> Result<Json<AgentStats>, (StatusCode, String)> {
let stats = db::get_agent_stats(&state.db)
.await
@@ -123,8 +148,10 @@ pub struct MoveAgentRequest {
}
/// Move an agent to a different site
/// Requires authentication.
pub async fn move_agent(
State(state): State<AppState>,
_user: AuthUser,
Path(id): Path<Uuid>,
Json(req): Json<MoveAgentRequest>,
) -> Result<Json<AgentResponse>, (StatusCode, String)> {
@@ -149,8 +176,10 @@ pub async fn move_agent(
}
/// List all agents with full details (site/client info)
/// Requires authentication.
pub async fn list_agents_with_details(
State(state): State<AppState>,
_user: AuthUser,
) -> Result<Json<Vec<db::AgentWithDetails>>, (StatusCode, String)> {
let agents = db::get_all_agents_with_details(&state.db)
.await
@@ -160,8 +189,10 @@ pub async fn list_agents_with_details(
}
/// List unassigned agents (not belonging to any site)
/// Requires authentication.
pub async fn list_unassigned_agents(
State(state): State<AppState>,
_user: AuthUser,
) -> Result<Json<Vec<AgentResponse>>, (StatusCode, String)> {
let agents = db::get_unassigned_agents(&state.db)
.await
@@ -172,8 +203,10 @@ pub async fn list_unassigned_agents(
}
/// Get extended state for an agent (network interfaces, uptime, etc.)
/// Requires authentication.
pub async fn get_agent_state(
State(state): State<AppState>,
_user: AuthUser,
Path(id): Path<Uuid>,
) -> Result<Json<db::AgentState>, (StatusCode, String)> {
let agent_state = db::get_agent_state(&state.db, id)

View File

@@ -8,6 +8,7 @@ use axum::{
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::auth::AuthUser;
use crate::db::{self, Command};
use crate::ws::{CommandPayload, ServerMessage};
use crate::AppState;
@@ -43,23 +44,33 @@ pub struct CommandsQuery {
}
/// Send a command to an agent
/// Requires authentication. Logs the user who sent the command for audit trail.
pub async fn send_command(
State(state): State<AppState>,
user: AuthUser,
Path(agent_id): Path<Uuid>,
Json(req): Json<SendCommandRequest>,
) -> Result<Json<SendCommandResponse>, (StatusCode, String)> {
// Log the command being sent for audit trail
tracing::info!(
user_id = %user.user_id,
agent_id = %agent_id,
command_type = %req.command_type,
"Command sent by user"
);
// Verify agent exists
let agent = db::get_agent_by_id(&state.db, agent_id)
let _agent = db::get_agent_by_id(&state.db, agent_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
.ok_or((StatusCode::NOT_FOUND, "Agent not found".to_string()))?;
// Create command record
// Create command record with user ID for audit trail
let create = db::CreateCommand {
agent_id,
command_type: req.command_type.clone(),
command_text: req.command.clone(),
created_by: None, // TODO: Get from JWT
created_by: Some(user.user_id),
};
let command = db::create_command(&state.db, create)
@@ -100,8 +111,10 @@ pub async fn send_command(
}
/// List recent commands
/// Requires authentication.
pub async fn list_commands(
State(state): State<AppState>,
_user: AuthUser,
Query(query): Query<CommandsQuery>,
) -> Result<Json<Vec<Command>>, (StatusCode, String)> {
let limit = query.limit.unwrap_or(50).min(500);
@@ -114,8 +127,10 @@ pub async fn list_commands(
}
/// Get a specific command by ID
/// Requires authentication.
pub async fn get_command(
State(state): State<AppState>,
_user: AuthUser,
Path(id): Path<Uuid>,
) -> Result<Json<Command>, (StatusCode, String)> {
let command = db::get_command_by_id(&state.db, id)

View File

@@ -5,10 +5,11 @@ use axum::{
http::StatusCode,
Json,
};
use chrono::{DateTime, Duration, Utc};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use uuid::Uuid;
use crate::auth::AuthUser;
use crate::db::{self, Metrics, MetricsSummary};
use crate::AppState;
@@ -26,13 +27,15 @@ pub struct MetricsQuery {
}
/// Get metrics for a specific agent
/// Requires authentication.
pub async fn get_agent_metrics(
State(state): State<AppState>,
_user: AuthUser,
Path(id): Path<Uuid>,
Query(query): Query<MetricsQuery>,
) -> Result<Json<Vec<Metrics>>, (StatusCode, String)> {
// First verify the agent exists
let agent = db::get_agent_by_id(&state.db, id)
let _agent = db::get_agent_by_id(&state.db, id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
.ok_or((StatusCode::NOT_FOUND, "Agent not found".to_string()))?;
@@ -54,8 +57,10 @@ pub async fn get_agent_metrics(
}
/// Get summary metrics across all agents
/// Requires authentication.
pub async fn get_summary(
State(state): State<AppState>,
_user: AuthUser,
) -> Result<Json<MetricsSummary>, (StatusCode, String)> {
let summary = db::get_metrics_summary(&state.db)
.await

View File

@@ -24,7 +24,8 @@ use axum::{
};
use sqlx::postgres::PgPoolOptions;
use tokio::sync::RwLock;
use tower_http::cors::{Any, CorsLayer};
use http::HeaderValue;
use tower_http::cors::{AllowOrigin, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::info;
@@ -129,11 +130,34 @@ async fn main() -> Result<()> {
/// Build the application router
fn build_router(state: AppState) -> Router {
// CORS configuration (allow dashboard access)
// TODO: Add rate limiting for registration endpoints using tower-governor
// Currently, registration is protected by AuthUser authentication.
// For additional protection against brute-force attacks, consider adding:
// - tower-governor crate for per-IP rate limiting on /api/agents/register
// - Configurable limits via environment variables
// Reference: https://docs.rs/tower-governor/latest/tower_governor/
// CORS configuration - restrict to specific dashboard origin
let dashboard_origin = std::env::var("DASHBOARD_URL")
.unwrap_or_else(|_| "https://rmm.azcomputerguru.com".to_string());
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
.allow_origin(AllowOrigin::exact(
HeaderValue::from_str(&dashboard_origin).expect("Invalid DASHBOARD_URL"),
))
.allow_methods([
http::Method::GET,
http::Method::POST,
http::Method::PUT,
http::Method::DELETE,
http::Method::OPTIONS,
])
.allow_headers([
http::header::AUTHORIZATION,
http::header::CONTENT_TYPE,
http::header::ACCEPT,
])
.allow_credentials(true);
Router::new()
// Health check