@@ -2,13 +2,17 @@ package commands
22
33import (
44 "context"
5+ "crypto/rand"
56 "crypto/tls"
7+ "encoding/hex"
68 "encoding/json"
79 "fmt"
810 "net/http"
911 "os"
1012 "os/exec"
13+ "path/filepath"
1114 "strings"
15+ "syscall"
1216 "time"
1317
1418 "patchmon-agent/internal/client"
@@ -996,42 +1000,69 @@ func toggleIntegration(integrationName string, enabled bool) error {
9961000 }
9971001 }
9981002
999- // Handle Docker Bench installation when Docker is enabled AND Compliance is already enabled
1003+ // Handle Docker Bench and oscap-docker installation when Docker is enabled AND Compliance is already enabled
10001004 if integrationName == "docker" && enabled {
10011005 if cfgManager .IsIntegrationEnabled ("compliance" ) {
1002- logger .Info ("Docker enabled with Compliance already active - setting up Docker Bench ..." )
1006+ logger .Info ("Docker enabled with Compliance already active - setting up Docker scanning tools ..." )
10031007 httpClient := client .New (cfgManager , logger )
10041008 ctx := context .Background ()
10051009
1010+ openscapScanner := compliance .NewOpenSCAPScanner (logger )
1011+ scannerDetails := openscapScanner .GetScannerDetails ()
1012+
1013+ // Setup Docker Bench
10061014 dockerBenchScanner := compliance .NewDockerBenchScanner (logger )
10071015 if dockerBenchScanner .IsAvailable () {
10081016 if err := dockerBenchScanner .EnsureInstalled (); err != nil {
10091017 logger .WithError (err ).Warn ("Failed to pre-pull Docker Bench image (will pull on first scan)" )
10101018 } else {
10111019 logger .Info ("Docker Bench image pulled successfully" )
1012-
1013- // Update compliance status to include Docker Bench
1014- openscapScanner := compliance .NewOpenSCAPScanner (logger )
1015- scannerDetails := openscapScanner .GetScannerDetails ()
10161020 scannerDetails .DockerBenchAvailable = true
10171021 scannerDetails .AvailableProfiles = append (scannerDetails .AvailableProfiles , models.ScanProfileInfo {
10181022 ID : "docker-bench" ,
10191023 Name : "Docker Bench for Security" ,
10201024 Description : "CIS Docker Benchmark security checks" ,
10211025 Type : "docker-bench" ,
10221026 })
1027+ }
1028+ } else {
1029+ logger .Warn ("Docker daemon not available - Docker Bench cannot be used" )
1030+ }
10231031
1024- httpClient .SendIntegrationSetupStatus (ctx , & models.IntegrationSetupStatus {
1025- Integration : "compliance" ,
1026- Enabled : true ,
1027- Status : "ready" ,
1028- Message : "Docker Bench now available" ,
1029- ScannerInfo : scannerDetails ,
1032+ // Setup oscap-docker for container image CVE scanning
1033+ oscapDockerScanner := compliance .NewOscapDockerScanner (logger )
1034+ if ! oscapDockerScanner .IsAvailable () {
1035+ if err := oscapDockerScanner .EnsureInstalled (); err != nil {
1036+ logger .WithError (err ).Warn ("Failed to install oscap-docker (container CVE scanning won't be available)" )
1037+ } else {
1038+ logger .Info ("oscap-docker installed successfully" )
1039+ scannerDetails .AvailableProfiles = append (scannerDetails .AvailableProfiles , models.ScanProfileInfo {
1040+ ID : "docker-image-cve" ,
1041+ Name : "Docker Image CVE Scan" ,
1042+ Description : "Scan Docker images for known CVEs using OpenSCAP" ,
1043+ Type : "oscap-docker" ,
1044+ Category : "docker" ,
10301045 })
10311046 }
10321047 } else {
1033- logger .Warn ("Docker daemon not available - Docker Bench cannot be used" )
1048+ logger .Info ("oscap-docker already available" )
1049+ scannerDetails .AvailableProfiles = append (scannerDetails .AvailableProfiles , models.ScanProfileInfo {
1050+ ID : "docker-image-cve" ,
1051+ Name : "Docker Image CVE Scan" ,
1052+ Description : "Scan Docker images for known CVEs using OpenSCAP" ,
1053+ Type : "oscap-docker" ,
1054+ Category : "docker" ,
1055+ })
10341056 }
1057+
1058+ // Send updated compliance status with Docker scanning tools
1059+ httpClient .SendIntegrationSetupStatus (ctx , & models.IntegrationSetupStatus {
1060+ Integration : "compliance" ,
1061+ Enabled : true ,
1062+ Status : "ready" ,
1063+ Message : "Docker scanning tools now available" ,
1064+ ScannerInfo : scannerDetails ,
1065+ })
10351066 }
10361067 }
10371068
@@ -1071,10 +1102,12 @@ func toggleIntegration(integrationName string, enabled bool) error {
10711102 }
10721103
10731104 // Create a helper script that will restart the service after we exit
1074- // SECURITY NOTE: Writing scripts to disk has a TOCTOU race condition risk.
1075- // Mitigations: 1) 0700 permissions on dir and file (owner-only)
1076- // 2) Script is deleted immediately after execution
1077- // 3) Short window between write and exec (milliseconds)
1105+ // SECURITY: TOCTOU mitigation measures:
1106+ // 1) Use random suffix to prevent predictable paths
1107+ // 2) Use O_EXCL flag for atomic creation (fail if file exists)
1108+ // 3) 0700 permissions on dir and file (owner-only)
1109+ // 4) Script is deleted immediately after execution
1110+ // 5) Verify no symlink attacks before execution
10781111 helperScript := `#!/bin/sh
10791112# Wait a moment for the current process to exit
10801113sleep 2
@@ -1083,28 +1116,69 @@ rc-service patchmon-agent restart 2>&1 || rc-service patchmon-agent start 2>&1
10831116# Clean up this script
10841117rm -f "$0"
10851118`
1086- helperPath := "/etc/patchmon/patchmon-restart-helper.sh"
1087- // SECURITY: Use 0700 permissions (owner-only executable) to minimize TOCTOU risk
1088- if err := os .WriteFile (helperPath , []byte (helperScript ), 0700 ); err != nil {
1119+ // Generate random suffix to prevent predictable path attacks
1120+ randomBytes := make ([]byte , 8 )
1121+ if _ , err := rand .Read (randomBytes ); err != nil {
1122+ logger .WithError (err ).Warn ("Failed to generate random suffix, using fallback" )
1123+ randomBytes = []byte {0x01 , 0x02 , 0x03 , 0x04 , 0x05 , 0x06 , 0x07 , 0x08 }
1124+ }
1125+ helperPath := filepath .Join ("/etc/patchmon" , fmt .Sprintf ("restart-%s.sh" , hex .EncodeToString (randomBytes )))
1126+
1127+ // SECURITY: Verify the directory is not a symlink (prevent symlink attacks)
1128+ dirInfo , err := os .Lstat ("/etc/patchmon" )
1129+ if err == nil && dirInfo .Mode ()& os .ModeSymlink != 0 {
1130+ logger .Warn ("Security: /etc/patchmon is a symlink, refusing to create helper script" )
1131+ os .Exit (0 ) // Fall through to exit approach
1132+ }
1133+
1134+ // SECURITY: Use O_EXCL to atomically create file (fail if exists - prevents race conditions)
1135+ file , err := os .OpenFile (helperPath , os .O_WRONLY | os .O_CREATE | os .O_EXCL , 0700 )
1136+ if err != nil {
10891137 logger .WithError (err ).Warn ("Failed to create restart helper script, will exit and rely on OpenRC auto-restart" )
10901138 // Fall through to exit approach
10911139 } else {
1092- // Execute the helper script in background (detached from current process)
1093- // Use 'sh -c' with nohup to ensure it runs after we exit
1094- cmd := exec .Command ("sh" , "-c" , fmt .Sprintf ("nohup %s > /dev/null 2>&1 &" , helperPath ))
1095- if err := cmd .Start (); err != nil {
1096- logger .WithError (err ).Warn ("Failed to start restart helper script, will exit and rely on OpenRC auto-restart" )
1097- // Clean up script
1098- if removeErr := os .Remove (helperPath ); removeErr != nil {
1099- logger .WithError (removeErr ).Debug ("Failed to remove helper script" )
1100- }
1140+ // Write the script content to the file
1141+ if _ , err := file .WriteString (helperScript ); err != nil {
1142+ logger .WithError (err ).Warn ("Failed to write restart helper script" )
1143+ file .Close ()
1144+ os .Remove (helperPath )
11011145 // Fall through to exit approach
11021146 } else {
1103- logger .Info ("Scheduled service restart via helper script, exiting now..." )
1104- // Give the helper script a moment to start
1105- time .Sleep (500 * time .Millisecond )
1106- // Exit gracefully - the helper script will restart the service
1107- os .Exit (0 )
1147+ file .Close ()
1148+
1149+ // SECURITY: Verify the file we're about to execute is the one we created
1150+ // Check it's a regular file, not a symlink that was swapped in
1151+ fileInfo , err := os .Lstat (helperPath )
1152+ if err != nil || fileInfo .Mode ()& os .ModeSymlink != 0 {
1153+ logger .Warn ("Security: helper script may have been tampered with, refusing to execute" )
1154+ os .Remove (helperPath )
1155+ os .Exit (0 )
1156+ }
1157+
1158+ // Execute the helper script in background (detached from current process)
1159+ // SECURITY: Avoid shell interpretation by executing directly with nohup
1160+ cmd := exec .Command ("nohup" , helperPath )
1161+ cmd .Stdout = nil
1162+ cmd .Stderr = nil
1163+ // Detach from parent process group to ensure script continues after we exit
1164+ cmd .SysProcAttr = & syscall.SysProcAttr {
1165+ Setpgid : true ,
1166+ Pgid : 0 ,
1167+ }
1168+ if err := cmd .Start (); err != nil {
1169+ logger .WithError (err ).Warn ("Failed to start restart helper script, will exit and rely on OpenRC auto-restart" )
1170+ // Clean up script
1171+ if removeErr := os .Remove (helperPath ); removeErr != nil {
1172+ logger .WithError (removeErr ).Debug ("Failed to remove helper script" )
1173+ }
1174+ // Fall through to exit approach
1175+ } else {
1176+ logger .Info ("Scheduled service restart via helper script, exiting now..." )
1177+ // Give the helper script a moment to start
1178+ time .Sleep (500 * time .Millisecond )
1179+ // Exit gracefully - the helper script will restart the service
1180+ os .Exit (0 )
1181+ }
11081182 }
11091183 }
11101184
0 commit comments