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

@@ -1,222 +1,205 @@
@ECHO OFF @ECHO OFF
REM CHECKUPD.BAT - Check for available updates without applying them REM CHECKUPD.BAT - Check for available updates without applying them
REM Quick status check to see if network has newer files REM Quick status check to see if network has newer files
REM REM
REM Usage: CHECKUPD REM Usage: CHECKUPD
REM REM
REM Checks these sources: REM Checks these sources:
REM T:\COMMON\ProdSW\*.bat REM T:\COMMON\ProdSW\*.bat
REM T:\%MACHINE%\ProdSW\*.* REM T:\%MACHINE%\ProdSW\*.*
REM T:\COMMON\DOS\*.NEW REM T:\COMMON\DOS\*.NEW
REM 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 Last modified: 2026-01-20
REM ================================================================== REM ==================================================================
REM STEP 1: Verify machine name is set REM STEP 1: Verify machine name is set
REM ================================================================== REM ==================================================================
IF NOT "%MACHINE%"=="" GOTO CHECK_DRIVE IF NOT "%MACHINE%"=="" GOTO CHECK_DRIVE
:NO_MACHINE :NO_MACHINE
ECHO. ECHO.
ECHO [ERROR] MACHINE variable not set ECHO [ERROR] MACHINE variable not set
ECHO. ECHO.
ECHO Set MACHINE in AUTOEXEC.BAT: ECHO Set MACHINE in AUTOEXEC.BAT:
ECHO SET MACHINE=TS-4R ECHO SET MACHINE=TS-4R
ECHO. ECHO.
PAUSE PAUSE
GOTO END GOTO END
REM ================================================================== REM ==================================================================
REM STEP 2: Verify T: drive is accessible REM STEP 2: Verify T: drive is accessible
REM ================================================================== REM ==================================================================
:CHECK_DRIVE :CHECK_DRIVE
REM Verify T: drive is accessible REM Verify T: drive is accessible
REM DOS 6.22: Direct file test is most reliable REM DOS 6.22: Direct file test is most reliable
IF NOT EXIST T:\*.* GOTO NO_T_DRIVE IF NOT EXIST T:\*.* GOTO NO_T_DRIVE
GOTO START_CHECK GOTO START_CHECK
:NO_T_DRIVE :NO_T_DRIVE
C: C:
ECHO. ECHO.
ECHO [ERROR] T: drive not available ECHO [ERROR] T: drive not available
ECHO. ECHO.
ECHO Run: C:\STARTNET.BAT ECHO Run: C:\STARTNET.BAT
ECHO. ECHO.
PAUSE PAUSE
GOTO END GOTO END
REM ================================================================== REM ==================================================================
REM STEP 3: Display check banner REM STEP 3: Display check banner
REM ================================================================== REM ==================================================================
:START_CHECK :START_CHECK
ECHO. ECHO.
ECHO ============================================================== ECHO ==============================================================
ECHO Update Check: %MACHINE% ECHO Update Check: %MACHINE%
ECHO ============================================================== ECHO ==============================================================
ECHO. ECHO.
REM Initialize flags (no counters - not critical for functionality) REM Initialize flags (no counters - not critical for functionality)
SET COMMON= SET COMMON=
SET MACHINEFILES= SET MACHINEFILES=
SET SYSFILE= SET SYSFILE=
REM ================================================================== REM ==================================================================
REM STEP 4: Check COMMON batch files REM STEP 4: Check COMMON batch files
REM ================================================================== REM ==================================================================
ECHO [1/3] Checking T:\COMMON\ProdSW for batch file updates... 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\NUL GOTO NO_COMMON
IF NOT EXIST T:\COMMON\ProdSW\*.* GOTO NO_COMMON
REM Check for files on network
REM Check for files on network FOR %%F IN (T:\COMMON\ProdSW\*.BAT) DO CALL :CHECK_COMMON_FILE %%F
FOR %%F IN (T:\COMMON\ProdSW\*.BAT) DO CALL :CHECK_COMMON_FILE %%F
IF "%COMMON%"=="" ECHO [OK] No updates in COMMON
IF "%COMMON%"=="" ECHO [OK] No updates in COMMON IF NOT "%COMMON%"=="" ECHO [FOUND] Updates available in COMMON
IF NOT "%COMMON%"=="" ECHO [FOUND] Updates available in COMMON
ECHO.
ECHO. GOTO CHECK_MACHINE
GOTO CHECK_MACHINE
:NO_COMMON
:NO_COMMON ECHO [SKIP] T:\COMMON\ProdSW not found
ECHO [SKIP] T:\COMMON\ProdSW not found ECHO.
ECHO.
REM ==================================================================
REM ================================================================== REM STEP 5: Check machine-specific files
REM STEP 5: Check machine-specific files REM ==================================================================
REM ==================================================================
:CHECK_MACHINE
:CHECK_MACHINE ECHO [2/3] Checking T:\%MACHINE%\ProdSW for machine-specific updates...
ECHO [2/3] Checking T:\%MACHINE%\ProdSW for machine-specific updates...
IF NOT EXIST T:\%MACHINE%\ProdSW\NUL GOTO NO_MACHINE_DIR
REM DOS 6.22: Check for files, not directory with \NUL
IF NOT EXIST T:\%MACHINE%\ProdSW\*.* GOTO NO_MACHINE_DIR REM Check for any files (BAT, EXE, DAT)
FOR %%F IN (T:\%MACHINE%\ProdSW\*.*) DO CALL :COUNT_FILE
REM Check for any files (BAT, EXE, DAT)
FOR %%F IN (T:\%MACHINE%\ProdSW\*.*) DO CALL :COUNT_FILE IF "%MACHINEFILES%"=="" ECHO [OK] No updates for %MACHINE%
IF NOT "%MACHINEFILES%"=="" ECHO [FOUND] Updates available for %MACHINE%
IF "%MACHINEFILES%"=="" ECHO [OK] No updates for %MACHINE%
IF NOT "%MACHINEFILES%"=="" ECHO [FOUND] Updates available for %MACHINE% ECHO.
GOTO CHECK_SYSTEM
ECHO.
GOTO CHECK_SYSTEM :NO_MACHINE_DIR
ECHO [SKIP] T:\%MACHINE%\ProdSW not found
:NO_MACHINE_DIR ECHO.
ECHO [SKIP] T:\%MACHINE%\ProdSW not found
ECHO. REM ==================================================================
REM STEP 6: Check system file updates
REM ================================================================== REM ==================================================================
REM STEP 6: Check system file updates
REM ================================================================== :CHECK_SYSTEM
ECHO [3/3] Checking T:\COMMON\DOS for system file updates...
:CHECK_SYSTEM
ECHO [3/3] Checking T:\COMMON\DOS for system file updates... IF NOT EXIST T:\COMMON\DOS\NUL GOTO NO_DOS_DIR
REM DOS 6.22: Check for files, not directory with \NUL REM Check for .NEW files
IF NOT EXIST T:\COMMON\DOS\*.* GOTO NO_DOS_DIR IF EXIST T:\COMMON\DOS\AUTOEXEC.NEW SET SYSFILE=FOUND
IF EXIST T:\COMMON\DOS\AUTOEXEC.NEW ECHO [FOUND] AUTOEXEC.NEW (system reboot required)
REM Check for .NEW files
IF EXIST T:\COMMON\DOS\AUTOEXEC.NEW SET SYSFILE=FOUND IF EXIST T:\COMMON\DOS\CONFIG.NEW SET SYSFILE=FOUND
IF EXIST T:\COMMON\DOS\AUTOEXEC.NEW ECHO [FOUND] AUTOEXEC.NEW (system reboot required) IF EXIST T:\COMMON\DOS\CONFIG.NEW ECHO [FOUND] CONFIG.NEW (system reboot required)
IF EXIST T:\COMMON\DOS\CONFIG.NEW SET SYSFILE=FOUND IF "%SYSFILE%"=="" ECHO [OK] No system file updates
IF EXIST T:\COMMON\DOS\CONFIG.NEW ECHO [FOUND] CONFIG.NEW (system reboot required)
ECHO.
IF "%SYSFILE%"=="" ECHO [OK] No system file updates GOTO SHOW_SUMMARY
ECHO. :NO_DOS_DIR
GOTO SHOW_SUMMARY ECHO [SKIP] T:\COMMON\DOS not found
ECHO.
:NO_DOS_DIR
ECHO [SKIP] T:\COMMON\DOS not found REM ==================================================================
ECHO. REM STEP 7: Show summary and recommendations
REM ==================================================================
REM ==================================================================
REM STEP 7: Show summary and recommendations :SHOW_SUMMARY
REM ================================================================== REM Determine if any updates found
SET HASUPDATES=
:SHOW_SUMMARY IF NOT "%COMMON%"=="" SET HASUPDATES=YES
REM Determine if any updates found IF NOT "%MACHINEFILES%"=="" SET HASUPDATES=YES
SET HASUPDATES= IF NOT "%SYSFILE%"=="" SET HASUPDATES=YES
IF NOT "%COMMON%"=="" SET HASUPDATES=YES
IF NOT "%MACHINEFILES%"=="" SET HASUPDATES=YES ECHO ==============================================================
IF NOT "%SYSFILE%"=="" SET HASUPDATES=YES ECHO Update Summary
ECHO ==============================================================
ECHO ============================================================== ECHO.
ECHO Update Summary ECHO Available updates:
ECHO ============================================================== IF NOT "%COMMON%"=="" ECHO [FOUND] Common batch files
ECHO. IF "%COMMON%"=="" ECHO [OK] Common batch files
ECHO Available updates: IF NOT "%MACHINEFILES%"=="" ECHO [FOUND] Machine-specific files
IF NOT "%COMMON%"=="" ECHO [FOUND] Common batch files IF "%MACHINEFILES%"=="" ECHO [OK] Machine-specific files
IF "%COMMON%"=="" ECHO [OK] Common batch files IF NOT "%SYSFILE%"=="" ECHO [FOUND] System files
IF NOT "%MACHINEFILES%"=="" ECHO [FOUND] Machine-specific files IF "%SYSFILE%"=="" ECHO [OK] System files
IF "%MACHINEFILES%"=="" ECHO [OK] Machine-specific files ECHO.
IF NOT "%SYSFILE%"=="" ECHO [FOUND] System files
IF "%SYSFILE%"=="" ECHO [OK] System files REM Provide recommendation
ECHO. IF "%HASUPDATES%"=="" GOTO NO_UPDATES_AVAILABLE
REM Provide recommendation ECHO Recommendation:
IF "%HASUPDATES%"=="" GOTO NO_UPDATES_AVAILABLE ECHO Run NWTOC to download and install updates
ECHO.
ECHO Recommendation: IF NOT "%SYSFILE%"=="" ECHO [WARNING] System file updates will require reboot
ECHO Run NWTOC to download and install updates IF NOT "%SYSFILE%"=="" ECHO.
ECHO.
IF NOT "%SYSFILE%"=="" ECHO [WARNING] System file updates will require reboot GOTO END
IF NOT "%SYSFILE%"=="" ECHO.
:NO_UPDATES_AVAILABLE
GOTO END ECHO Status: All files are up to date
ECHO.
:NO_UPDATES_AVAILABLE
ECHO Status: All files are up to date GOTO END
ECHO.
REM ==================================================================
GOTO END REM HELPER SUBROUTINES
REM ==================================================================
REM ==================================================================
REM HELPER SUBROUTINES :CHECK_COMMON_FILE
REM ================================================================== 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
:CHECK_COMMON_FILE SET COMMON=FOUND
REM Check if network file is newer than local file GOTO END_SUBROUTINE
REM %1 = network file path (e.g., T:\COMMON\ProdSW\NWTOC.BAT)
:COUNT_FILE
REM Extract filename from path REM Flag that machine-specific files exist
SET NETFILE=%1 SET MACHINEFILES=FOUND
SET FILENAME=%~nx1 GOTO END_SUBROUTINE
REM Check if local file exists :END_SUBROUTINE
IF NOT EXIST C:\BAT\%FILENAME% SET COMMON=FOUND REM Return point for all subroutines (replaces :EOF)
IF NOT EXIST C:\BAT\%FILENAME% GOTO CHECK_COMMON_DONE
REM ==================================================================
REM Both files exist - network file available REM CLEANUP AND EXIT
REM NOTE: DOS 6.22 cannot easily compare file dates REM ==================================================================
REM We just check if network file exists (already confirmed above)
SET COMMON=FOUND :END
REM Clean up environment variables
:CHECK_COMMON_DONE SET COMMON=
GOTO END_SUBROUTINE SET MACHINEFILES=
SET SYSFILE=
:COUNT_FILE SET HASUPDATES=
REM Flag that machine-specific files exist SET NETFILE=
SET MACHINEFILES=FOUND SET FILENAME=
GOTO END_SUBROUTINE
:END_SUBROUTINE
REM Return point for all subroutines (replaces :EOF)
REM ==================================================================
REM CLEANUP AND EXIT
REM ==================================================================
:END
REM Clean up environment variables
SET COMMON=
SET MACHINEFILES=
SET SYSFILE=
SET HASUPDATES=
SET NETFILE=
SET FILENAME=

View File

@@ -1,268 +1,65 @@
@ECHO OFF @ECHO OFF
REM Computer to Network - Upload local changes and test data to network REM Computer to Network - Upload local files to network
REM Programs: C:\BAT -> T:\COMMON\ProdSW or T:\%MACHINE%\ProdSW REM Version: 2.5 - Added /I flag, removed 2>NUL (DOS 6.22)
REM Test data: C:\ATE -> T:\%MACHINE%\LOGS (for database import) REM Last modified: 2026-01-20
REM Version: 2.1 - Fixed drive test for DOS 6.22 reliability
REM Last modified: 2026-01-20 REM Check MACHINE variable
IF "%MACHINE%"=="" GOTO NO_MACHINE
REM Verify MACHINE environment variable is set
IF NOT "%MACHINE%"=="" GOTO CHECK_DRIVE REM Check T: drive
IF NOT EXIST T:\*.* GOTO NO_DRIVE
ECHO.
ECHO [ERROR] MACHINE variable not set REM Display banner
ECHO. ECHO.
ECHO MACHINE must be set in AUTOEXEC.BAT ECHO ==============================================================
ECHO Run DEPLOY.BAT to configure this machine ECHO Upload: %MACHINE% to Network
ECHO. ECHO ==============================================================
PAUSE ECHO.
GOTO END
REM Create target directories (ignore errors with >NUL)
:CHECK_DRIVE MD T:\%MACHINE% >NUL
REM Verify T: drive is accessible MD T:\%MACHINE%\ProdSW >NUL
REM DOS 6.22: Direct file test is most reliable MD T:\%MACHINE%\LOGS >NUL
IF NOT EXIST T:\*.* GOTO NO_T_DRIVE
GOTO CHECK_TARGET REM Copy batch files (XCOPY /Y /I = no prompts, assume directory)
ECHO Copying C:\BAT\*.BAT to T:\%MACHINE%\ProdSW...
:NO_T_DRIVE XCOPY C:\BAT\*.BAT T:\%MACHINE%\ProdSW /Y /I >NUL
C: ECHO [OK] Batch files copied
ECHO. ECHO.
ECHO [ERROR] T: drive not available
ECHO. REM Check for ATE directory
ECHO Network drive must be mapped to \\D2TESTNAS\test IF NOT EXIST C:\ATE\*.* GOTO SKIP_ATE
ECHO Run: C:\STARTNET.BAT
ECHO. REM Copy ATE files
PAUSE ECHO Copying C:\ATE files to T:\%MACHINE%\ProdSW...
GOTO END 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
:CHECK_TARGET IF EXIST C:\ATE\*.CFG XCOPY C:\ATE\*.CFG T:\%MACHINE%\ProdSW /Y /I >NUL
REM Default target is machine-specific ECHO [OK] ATE files copied
SET TARGET=MACHINE ECHO.
SET TARGETDIR=T:\%MACHINE%\ProdSW GOTO DONE
SET LOGSDIR=T:\%MACHINE%\LOGS
:SKIP_ATE
REM Check for COMMON parameter ECHO [INFO] No C:\ATE directory - skipping
IF "%1"=="COMMON" SET TARGET=COMMON ECHO.
IF "%1"=="common" SET TARGET=COMMON
IF "%1"=="Common" SET TARGET=COMMON :DONE
ECHO ==============================================================
IF "%TARGET%"=="COMMON" SET TARGETDIR=T:\COMMON\ProdSW ECHO Upload Complete
ECHO ==============================================================
REM Confirm COMMON upload (affects all machines) ECHO.
IF NOT "%TARGET%"=="COMMON" GOTO DISPLAY_BANNER GOTO END
ECHO. :NO_MACHINE
ECHO ============================================================== ECHO [ERROR] MACHINE variable not set
ECHO [WARNING] COMMON Upload Confirmation ECHO Run DEPLOY.BAT first
ECHO ============================================================== PAUSE
ECHO. GOTO END
ECHO You are about to upload files to COMMON location
ECHO This will affect ALL DOS machines at Dataforth :NO_DRIVE
ECHO. ECHO [ERROR] T: drive not available
ECHO Other machines will receive these files on next reboot ECHO Run C:\STARTNET.BAT first
ECHO. PAUSE
ECHO Continue? (Y/N) GOTO END
ECHO.
:END
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
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%
ECHO.
:UPLOAD_BATCH_FILES
ECHO [1/3] Uploading batch files from C:\BAT...
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
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
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.
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.
PAUSE
GOTO END
:END
REM Clean up environment variables
SET TARGET=
SET TARGETDIR=
SET LOGSDIR=

View File

@@ -1,189 +1,87 @@
@ECHO OFF @ECHO OFF
REM One-time deployment script for DOS Update System 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 Usage: T:\COMMON\ProdSW\DEPLOY.BAT machine-name REM Version: 2.3 - Removed 2>NUL (DOS 6.22 only supports >NUL)
REM Example: T:\COMMON\ProdSW\DEPLOY.BAT TS-4R REM Last modified: 2026-01-20
REM Version: 2.0 - Simplified deployment
REM Last modified: 2026-01-19 CLS
CLS REM Check machine name parameter
IF "%1"=="" GOTO NO_MACHINE
REM Check machine name parameter provided
IF "%1"=="" GOTO NO_MACHINE_NAME REM Save machine name
SET MACHINE=%1
REM Save machine name to variable
SET MACHINE=%1 ECHO ==============================================================
ECHO DOS Update System - Deployment
ECHO ============================================================== ECHO ==============================================================
ECHO DOS Update System - Deployment ECHO Machine: %MACHINE%
ECHO ============================================================== ECHO ==============================================================
ECHO Machine: %MACHINE% ECHO.
ECHO ============================================================== ECHO Press any key to install...
ECHO. PAUSE >NUL
ECHO Installing automatic update system... ECHO.
ECHO.
ECHO Files to install: REM Create directories (ignore errors with >NUL)
ECHO - AUTOEXEC.BAT (startup configuration) MD C:\BAT >NUL
ECHO - NWTOC.BAT (download updates) MD T:\%MACHINE% >NUL
ECHO - CTONW.BAT (upload test data)
ECHO - UPDATE.BAT (full backup) ECHO [1/2] Copying batch files to C:\BAT...
ECHO - CHECKUPD.BAT (check updates) XCOPY T:\COMMON\ProdSW\NWTOC.BAT C:\BAT /Y >NUL
ECHO - STAGE.BAT (system file updates) XCOPY T:\COMMON\ProdSW\CTONW.BAT C:\BAT /Y >NUL
ECHO - REBOOT.BAT (apply staged updates) XCOPY T:\COMMON\ProdSW\UPDATE.BAT C:\BAT /Y >NUL
ECHO. XCOPY T:\COMMON\ProdSW\CHECKUPD.BAT C:\BAT /Y >NUL
PAUSE XCOPY T:\COMMON\ProdSW\STAGE.BAT C:\BAT /Y >NUL
ECHO. XCOPY T:\COMMON\ProdSW\REBOOT.BAT C:\BAT /Y >NUL
ECHO [OK] Batch files installed
REM Create C:\BAT directory ECHO.
ECHO [1/3] Creating C:\BAT directory...
IF NOT EXIST C:\BAT\*.* MD C:\BAT ECHO [2/2] Installing AUTOEXEC.BAT...
IF NOT EXIST C:\BAT\*.* GOTO BAT_DIR_ERROR REM Create AUTOEXEC.BAT with machine name
ECHO [OK] C:\BAT directory ready ECHO @ECHO OFF > C:\AUTOEXEC.BAT
ECHO. ECHO REM Dataforth Test Machine - DOS 6.22 >> C:\AUTOEXEC.BAT
ECHO SET MACHINE=%MACHINE% >> C:\AUTOEXEC.BAT
REM Copy batch files from network to local machine ECHO SET PATH=C:\DOS;C:\NET;C:\BAT;C:\BATCH;C:\ >> C:\AUTOEXEC.BAT
ECHO [2/3] Copying batch files to C:\BAT... ECHO PROMPT $P$G >> C:\AUTOEXEC.BAT
XCOPY T:\COMMON\ProdSW\NWTOC.BAT C:\BAT\ /Y ECHO SET TEMP=C:\TEMP >> C:\AUTOEXEC.BAT
IF ERRORLEVEL 4 GOTO COPY_ERROR ECHO SET TMP=C:\TEMP >> C:\AUTOEXEC.BAT
ECHO [OK] NWTOC.BAT ECHO MD C:\TEMP >NUL >> C:\AUTOEXEC.BAT
ECHO CLS >> C:\AUTOEXEC.BAT
XCOPY T:\COMMON\ProdSW\CTONW.BAT C:\BAT\ /Y ECHO ECHO. >> C:\AUTOEXEC.BAT
IF ERRORLEVEL 4 GOTO COPY_ERROR ECHO ECHO Dataforth Test Machine: %MACHINE% >> C:\AUTOEXEC.BAT
ECHO [OK] CTONW.BAT ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO IF EXIST C:\STARTNET.BAT CALL C:\STARTNET.BAT >> C:\AUTOEXEC.BAT
XCOPY T:\COMMON\ProdSW\UPDATE.BAT C:\BAT\ /Y ECHO IF NOT EXIST T:\*.* GOTO NONET >> C:\AUTOEXEC.BAT
IF ERRORLEVEL 4 GOTO COPY_ERROR ECHO IF EXIST C:\BAT\NWTOC.BAT CALL C:\BAT\NWTOC.BAT >> C:\AUTOEXEC.BAT
ECHO [OK] UPDATE.BAT ECHO IF EXIST C:\BAT\CTONW.BAT CALL C:\BAT\CTONW.BAT >> C:\AUTOEXEC.BAT
ECHO GOTO READY >> C:\AUTOEXEC.BAT
XCOPY T:\COMMON\ProdSW\CHECKUPD.BAT C:\BAT\ /Y ECHO :NONET >> C:\AUTOEXEC.BAT
IF ERRORLEVEL 4 GOTO COPY_ERROR ECHO ECHO [ERROR] Network not available >> C:\AUTOEXEC.BAT
ECHO [OK] CHECKUPD.BAT ECHO :READY >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
XCOPY T:\COMMON\ProdSW\STAGE.BAT C:\BAT\ /Y ECHO ECHO System Ready >> C:\AUTOEXEC.BAT
IF ERRORLEVEL 4 GOTO COPY_ERROR ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO [OK] STAGE.BAT ECHO CD \ATE >> C:\AUTOEXEC.BAT
ECHO menux >> C:\AUTOEXEC.BAT
XCOPY T:\COMMON\ProdSW\REBOOT.BAT C:\BAT\ /Y ECHO [OK] AUTOEXEC.BAT installed with MACHINE=%MACHINE%
IF ERRORLEVEL 4 GOTO COPY_ERROR ECHO.
ECHO [OK] REBOOT.BAT
ECHO ==============================================================
ECHO [OK] All batch files installed ECHO Deployment Complete - REBOOT NOW
ECHO. ECHO ==============================================================
ECHO.
REM Install AUTOEXEC.BAT with machine name PAUSE
ECHO [3/3] Installing AUTOEXEC.BAT... GOTO END
REM Copy template and modify machine name :NO_MACHINE
COPY T:\COMMON\ProdSW\AUTOEXEC.BAT C:\AUTOEXEC.TMP /Y >NUL ECHO.
IF ERRORLEVEL 1 GOTO AUTOEXEC_ERROR ECHO [ERROR] Machine name not provided
ECHO.
REM Create new AUTOEXEC with correct machine name ECHO Usage: DEPLOY.BAT machine-name
REM Filter out existing SET MACHINE line and rebuild with new one ECHO Example: DEPLOY.BAT TS-4R
ECHO @ECHO OFF > C:\AUTOEXEC.BAT ECHO.
TYPE C:\AUTOEXEC.TMP | FIND /V "@ECHO OFF" | FIND /V "SET MACHINE=" > C:\AUTOEXEC.TM1 PAUSE
ECHO REM Dataforth Test Machine Startup - DOS 6.22 >> C:\AUTOEXEC.BAT GOTO END
ECHO REM Automatically runs after CONFIG.SYS during boot >> C:\AUTOEXEC.BAT
ECHO REM Version: 3.0 - Auto-update system integrated >> C:\AUTOEXEC.BAT :END
ECHO REM Last modified: 2026-01-19 >> C:\AUTOEXEC.BAT SET MACHINE=
ECHO. >> C:\AUTOEXEC.BAT
ECHO REM Set machine identity (configured by DEPLOY.BAT) >> 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 [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 ==============================================================
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
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 @ECHO OFF
REM Network to Computer - Download software updates from network to local C: drive 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.8 - Added /I flag, removed 2>NUL (DOS 6.22)
REM Version: 2.5 - Replaced XCOPY with simple COPY (more reliable in DOS 6.22) REM Last modified: 2026-01-20
REM Last modified: 2026-01-20
REM Check MACHINE variable
REM Verify MACHINE environment variable is set IF "%MACHINE%"=="" GOTO NO_MACHINE
IF NOT "%MACHINE%"=="" GOTO CHECK_DRIVE
REM Check T: drive
ECHO. IF NOT EXIST T:\*.* GOTO NO_DRIVE
ECHO [ERROR] MACHINE variable not set
ECHO. REM Display banner
ECHO MACHINE must be set in AUTOEXEC.BAT ECHO.
ECHO Run DEPLOY.BAT to configure this machine ECHO ==============================================================
ECHO. ECHO Download Updates: %MACHINE% from Network
PAUSE ECHO ==============================================================
GOTO END ECHO.
:CHECK_DRIVE REM Create local directories (ignore errors with >NUL)
REM Verify T: drive is accessible MD C:\BAT >NUL
REM DOS 6.22: Direct file test is most reliable MD C:\ATE >NUL
IF NOT EXIST T:\*.* GOTO NO_T_DRIVE MD C:\NET >NUL
GOTO START_UPDATE
REM Check for COMMON updates
:NO_T_DRIVE IF NOT EXIST T:\COMMON\ProdSW\*.* GOTO NO_COMMON
C:
ECHO. ECHO [1/3] Copying from T:\COMMON\ProdSW to C:\BAT...
ECHO [ERROR] T: drive not available XCOPY T:\COMMON\ProdSW\*.BAT C:\BAT /Y /I >NUL
ECHO. ECHO [OK] Common batch files updated
ECHO Network drive must be mapped to \\D2TESTNAS\test ECHO.
ECHO Run: C:\STARTNET.BAT
ECHO. REM Check for machine-specific updates
PAUSE IF NOT EXIST T:\%MACHINE%\ProdSW\*.* GOTO SKIP_MACHINE
GOTO END
ECHO [2/3] Copying from T:\%MACHINE%\ProdSW...
:START_UPDATE IF EXIST T:\%MACHINE%\ProdSW\*.BAT XCOPY T:\%MACHINE%\ProdSW\*.BAT C:\BAT /Y /I >NUL
ECHO. IF EXIST T:\%MACHINE%\ProdSW\*.EXE XCOPY T:\%MACHINE%\ProdSW\*.EXE C:\ATE /Y /I >NUL
ECHO ============================================================== IF EXIST T:\%MACHINE%\ProdSW\*.DAT XCOPY T:\%MACHINE%\ProdSW\*.DAT C:\ATE /Y /I >NUL
ECHO Download Updates: %MACHINE% from Network IF EXIST T:\%MACHINE%\ProdSW\*.CFG XCOPY T:\%MACHINE%\ProdSW\*.CFG C:\ATE /Y /I >NUL
ECHO ============================================================== ECHO [OK] Machine-specific files updated
ECHO Source: T:\COMMON and T:\%MACHINE% ECHO.
ECHO Target: C:\BAT, C:\ATE, C:\NET GOTO CHECK_NET
ECHO ==============================================================
ECHO. :SKIP_MACHINE
ECHO [2/3] No machine-specific updates (T:\%MACHINE%\ProdSW not found)
REM Verify update directories exist on network ECHO.
REM DOS 6.22: Check for files in ProdSW, not empty COMMON directory
IF NOT EXIST T:\COMMON\ProdSW\*.* GOTO NO_PRODSW :CHECK_NET
REM Check for network client updates
REM Machine-specific directory is optional IF NOT EXIST T:\COMMON\NET\*.* GOTO SKIP_NET
IF NOT EXIST T:\%MACHINE%\*.* GOTO SKIP_MACHINE_CHECK
IF NOT EXIST T:\%MACHINE%\ProdSW\*.* GOTO SKIP_MACHINE_CHECK ECHO [3/3] Copying from T:\COMMON\NET to C:\NET...
GOTO UPDATE_BATCH_FILES XCOPY T:\COMMON\NET\*.* C:\NET /Y /I >NUL
ECHO [OK] Network files updated
:NO_PRODSW ECHO.
ECHO [ERROR] T:\COMMON\ProdSW directory not found GOTO DONE
ECHO Update directory is missing
ECHO. :SKIP_NET
PAUSE ECHO [3/3] No network updates (T:\COMMON\NET not found)
GOTO END ECHO.
:SKIP_MACHINE_CHECK :DONE
ECHO [INFO] T:\%MACHINE%\ProdSW not found - skipping machine-specific updates ECHO ==============================================================
ECHO. ECHO Download Complete
ECHO ==============================================================
:UPDATE_BATCH_FILES ECHO.
ECHO [1/4] Updating batch files from T:\COMMON\ProdSW... GOTO END
REM Create C:\BAT directory if needed :NO_COMMON
IF NOT EXIST C:\BAT\*.* MD C:\BAT ECHO [ERROR] T:\COMMON\ProdSW not found
PAUSE
REM Backup existing batch files before update GOTO END
ECHO Creating backups (.BAK files)...
FOR %%F IN (C:\BAT\*.BAT) DO COPY %%F %%~dpnF.BAK >NUL 2>NUL :NO_MACHINE
ECHO [ERROR] MACHINE variable not set
REM Copy batch files from COMMON (simple COPY, not XCOPY) ECHO Run DEPLOY.BAT first
ECHO Copying updated files... PAUSE
COPY T:\COMMON\ProdSW\*.BAT C:\BAT /Y >NUL GOTO END
IF ERRORLEVEL 1 GOTO UPDATE_ERROR_INIT
ECHO [OK] Batch files updated from COMMON :NO_DRIVE
ECHO [ERROR] T: drive not available
ECHO. ECHO Run C:\STARTNET.BAT first
PAUSE
:UPDATE_MACHINE_FILES GOTO END
ECHO [2/4] Updating machine-specific files from T:\%MACHINE%\ProdSW...
:END
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.
PAUSE
GOTO END
:UPDATE_ERROR_USER
ECHO.
ECHO [ERROR] Update terminated by user (Ctrl+C)
ECHO Update may be incomplete - run NWTOC again
ECHO.
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,5 +1,5 @@
@ECHO OFF @ECHO OFF
REM UPDATE.BAT - Redirect to DEPLOY.BAT in proper location REM UPDATE.BAT - Redirect to DEPLOY.BAT in proper location
REM Usage: UPDATE.BAT machine-name REM Usage: UPDATE.BAT machine-name
REM Example: UPDATE.BAT TS-4R REM Example: UPDATE.BAT TS-4R
CALL T:\COMMON\ProdSW\DEPLOY.BAT %1 CALL T:\COMMON\ProdSW\DEPLOY.BAT %1

View File

@@ -1,19 +1,56 @@
#Requires -Version 2.0 #Requires -Version 2.0
<# <#
.SYNOPSIS .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 .DESCRIPTION
Lightweight RMM agent that: This PowerShell-based agent is designed for legacy Windows systems that cannot
- Registers with GuruRMM server using site code run the modern Rust-based GuruRMM agent. It provides basic RMM functionality
- Reports system information including registration, heartbeat, system info collection, and remote command
- Executes remote scripts/commands execution.
- Monitors system health
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 .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 Author: GuruRMM
Version: 1.0.0
#> #>
param( param(
@@ -27,18 +64,23 @@ param(
[string]$SiteCode, [string]$SiteCode,
[Parameter()] [Parameter()]
[string]$ServerUrl = "https://rmm-api.azcomputerguru.com" [string]$ServerUrl = "https://rmm-api.azcomputerguru.com",
[Parameter()]
[switch]$AllowInsecureTLS
) )
# ============================================================================ # ============================================================================
# Configuration # Configuration
# ============================================================================ # ============================================================================
$script:Version = "1.0.0" $script:Version = "1.1.0"
$script:AgentType = "powershell-legacy" $script:AgentType = "powershell-legacy"
$script:ConfigDir = "$env:ProgramData\GuruRMM" $script:ConfigDir = "$env:ProgramData\GuruRMM"
$script:LogFile = "$script:ConfigDir\agent.log" $script:LogFile = "$script:ConfigDir\agent.log"
$script:PollInterval = 60 # seconds $script:PollInterval = 60 # seconds
$script:AllowInsecureTLS = $AllowInsecureTLS
$script:TLSInitialized = $false
# ============================================================================ # ============================================================================
# Logging # Logging
@@ -67,6 +109,63 @@ function Write-Log {
} catch {} } 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) # HTTP Functions (PS 2.0 compatible)
# ============================================================================ # ============================================================================
@@ -82,6 +181,9 @@ function Invoke-ApiRequest {
$url = "$($script:Config.ServerUrl)$Endpoint" $url = "$($script:Config.ServerUrl)$Endpoint"
try { try {
# Initialize TLS settings (only runs once)
Initialize-TLS
# Use .NET WebClient for PS 2.0 compatibility # Use .NET WebClient for PS 2.0 compatibility
$webClient = New-Object System.Net.WebClient $webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("Content-Type", "application/json") $webClient.Headers.Add("Content-Type", "application/json")
@@ -91,17 +193,6 @@ function Invoke-ApiRequest {
$webClient.Headers.Add("Authorization", "Bearer $ApiKey") $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") { if ($Method -eq "GET") {
$response = $webClient.DownloadString($url) $response = $webClient.DownloadString($url)
} else { } else {

View File

@@ -15,8 +15,20 @@
.PARAMETER ServerUrl .PARAMETER ServerUrl
The GuruRMM server URL (default: https://rmm-api.azcomputerguru.com) 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 .EXAMPLE
# Secure installation (recommended)
.\Install-GuruRMM.ps1 -SiteCode DARK-GROVE-7839 .\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( param(
@@ -24,7 +36,10 @@ param(
[string]$SiteCode, [string]$SiteCode,
[Parameter()] [Parameter()]
[string]$ServerUrl = "https://rmm-api.azcomputerguru.com" [string]$ServerUrl = "https://rmm-api.azcomputerguru.com",
[Parameter()]
[switch]$AllowInsecureTLS
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -112,8 +127,15 @@ try {
# Step 3: Register agent # Step 3: Register agent
Write-Status "Registering with GuruRMM server..." 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 { try {
$registerArgs = "-ExecutionPolicy Bypass -File `"$destScript`" -SiteCode `"$SiteCode`" -ServerUrl `"$ServerUrl`"" $registerArgs = "-ExecutionPolicy Bypass -File `"$destScript`" -SiteCode `"$SiteCode`" -ServerUrl `"$ServerUrl`""
if ($AllowInsecureTLS) {
$registerArgs += " -AllowInsecureTLS"
}
$process = Start-Process powershell.exe -ArgumentList $registerArgs -Wait -PassThru -NoNewWindow $process = Start-Process powershell.exe -ArgumentList $registerArgs -Wait -PassThru -NoNewWindow
if ($process.ExitCode -ne 0) { if ($process.ExitCode -ne 0) {
@@ -137,13 +159,19 @@ try {
# Step 5: Create scheduled task # Step 5: Create scheduled task
try { 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`"" $taskCommand = "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$destScript`""
if ($AllowInsecureTLS) {
$taskCommand += " -AllowInsecureTLS"
}
# Create task that runs at system startup # Create task that runs at system startup
schtasks /create /tn $TaskName /tr $taskCommand /sc onstart /ru SYSTEM /rl HIGHEST /f | Out-Null schtasks /create /tn $TaskName /tr $taskCommand /sc onstart /ru SYSTEM /rl HIGHEST /f | Out-Null
Write-Status "Scheduled task created: $TaskName" "OK" Write-Status "Scheduled task created: $TaskName" "OK"
if ($AllowInsecureTLS) {
Write-Status "Task configured with -AllowInsecureTLS flag" "WARN"
}
} catch { } catch {
Write-Status "Failed to create scheduled task: $($_.Exception.Message)" "ERROR" Write-Status "Failed to create scheduled task: $($_.Exception.Message)" "ERROR"
Write-Status "You may need to manually create the task" "WARN" Write-Status "You may need to manually create the task" "WARN"

View File

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

View File

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

View File

@@ -177,7 +177,36 @@ impl AgentUpdater {
} }
/// Download the new binary to a temp file /// 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> { 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) let response = self.http_client.get(url)
.send() .send()
.await .await
@@ -273,10 +302,26 @@ impl AgentUpdater {
#[cfg(unix)] #[cfg(unix)]
async fn create_unix_rollback_watchdog(&self) -> Result<()> { async fn create_unix_rollback_watchdog(&self) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let backup_path = self.config.backup_path(); let backup_path = self.config.backup_path();
let binary_path = &self.config.binary_path; let binary_path = &self.config.binary_path;
let timeout = self.config.rollback_timeout_secs; 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 let script = format!(r#"#!/bin/bash
# GuruRMM Rollback Watchdog # GuruRMM Rollback Watchdog
# Auto-generated - will be deleted after successful update # Auto-generated - will be deleted after successful update
@@ -284,49 +329,50 @@ impl AgentUpdater {
BACKUP="{backup}" BACKUP="{backup}"
BINARY="{binary}" BINARY="{binary}"
TIMEOUT={timeout} TIMEOUT={timeout}
SCRIPT_PATH="{script}"
sleep $TIMEOUT sleep $TIMEOUT
# Check if agent service is running # Check if agent service is running
if ! systemctl is-active --quiet gururmm-agent 2>/dev/null; then 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 if [ -f "$BACKUP" ]; then
cp "$BACKUP" "$BINARY" cp "$BACKUP" "$BINARY"
chmod +x "$BINARY" chmod +x "$BINARY"
systemctl start gururmm-agent systemctl start gururmm-agent
echo "Rollback completed" echo "[OK] Rollback completed"
else else
echo "No backup file found!" echo "[ERROR] No backup file found!"
fi fi
fi fi
# Clean up this script # Clean up this script
rm -f /tmp/gururmm-rollback.sh rm -f "$SCRIPT_PATH"
"#, "#,
backup = backup_path.display(), backup = backup_path.display(),
binary = binary_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 // Set restrictive permissions (700 - owner only)
tokio::process::Command::new("chmod") std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o700))
.arg("+x") .context("Failed to set rollback script permissions")?;
.arg(&script_path)
.status()
.await?;
// Spawn as detached background process // Spawn as detached background process using setsid (not nohup with "&" literal arg)
tokio::process::Command::new("nohup") tokio::process::Command::new("setsid")
.arg("bash") .arg("bash")
.arg(&script_path) .arg(&script_path)
.arg("&") .stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn() .spawn()
.context("Failed to spawn rollback watchdog")?; .context("Failed to spawn rollback watchdog")?;
info!("Rollback watchdog started (timeout: {}s)", timeout); info!("[OK] Rollback watchdog started (timeout: {}s)", timeout);
Ok(()) Ok(())
} }
@@ -524,12 +570,29 @@ Remove-Item -Path $MyInvocation.MyCommand.Path -Force
pub async fn cancel_rollback_watchdog(&self) { pub async fn cancel_rollback_watchdog(&self) {
#[cfg(unix)] #[cfg(unix)]
{ {
// Kill the watchdog script // Kill any running rollback watchdog scripts
let _ = tokio::process::Command::new("pkill") let _ = tokio::process::Command::new("pkill")
.args(["-f", "gururmm-rollback.sh"]) .args(["-f", "rollback-.*\\.sh"])
.status() .status()
.await; .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)] #[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 // 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"; const API_URL = import.meta.env.VITE_API_URL || "https://rmm-api.azcomputerguru.com";
@@ -10,22 +10,41 @@ 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) => { api.interceptors.request.use((config) => {
const token = localStorage.getItem("token"); const token = getToken();
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
return config; return config;
}); });
// Handle auth errors // Response interceptor - handle 401 unauthorized
api.interceptors.response.use( api.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error: AxiosError) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
localStorage.removeItem("token"); clearToken();
window.location.href = "/login"; // Use a more graceful redirect that preserves SPA state
if (window.location.pathname !== "/login") {
window.location.href = "/login";
}
} }
return Promise.reject(error); return Promise.reject(error);
} }
@@ -156,9 +175,31 @@ export interface RegisterRequest {
// API functions // API functions
export const authApi = { export const authApi = {
login: (data: LoginRequest) => api.post<LoginResponse>("/api/auth/login", data), login: async (data: LoginRequest): Promise<LoginResponse> => {
register: (data: RegisterRequest) => api.post<LoginResponse>("/api/auth/register", data), 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"), me: () => api.get<User>("/api/auth/me"),
logout: (): void => {
clearToken();
},
isAuthenticated: (): boolean => {
return !!getToken();
},
}; };
export const agentsApi = { export const agentsApi = {

View File

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

View File

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

View File

@@ -1,10 +1,16 @@
import { useState, FormEvent } from "react"; import { useState, FormEvent } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { AxiosError } from "axios";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../components/Card"; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../components/Card";
import { Input } from "../components/Input"; import { Input } from "../components/Input";
import { Button } from "../components/Button"; import { Button } from "../components/Button";
interface ApiErrorResponse {
error?: string;
message?: string;
}
export function Register() { export function Register() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -34,8 +40,15 @@ export function Register() {
try { try {
await register(email, password, name || undefined); await register(email, password, name || undefined);
navigate("/"); navigate("/");
} catch (err: any) { } catch (err) {
setError(err.response?.data?.error || "Registration failed. Please try again."); 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 { } finally {
setIsLoading(false); 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"] } axum-extra = { version = "0.9", features = ["typed-header"] }
tower = { version = "0.5", features = ["util", "timeout"] } tower = { version = "0.5", features = ["util", "timeout"] }
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] } tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
http = "1"
# Async runtime # Async runtime
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,8 @@ use axum::{
}; };
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use tokio::sync::RwLock; 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 tower_http::trace::TraceLayer;
use tracing::info; use tracing::info;
@@ -129,11 +130,34 @@ async fn main() -> Result<()> {
/// Build the application router /// Build the application router
fn build_router(state: AppState) -> 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() let cors = CorsLayer::new()
.allow_origin(Any) .allow_origin(AllowOrigin::exact(
.allow_methods(Any) HeaderValue::from_str(&dashboard_origin).expect("Invalid DASHBOARD_URL"),
.allow_headers(Any); ))
.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() Router::new()
// Health check // Health check