Skip to content

Commit 6d0404b

Browse files
committed
more tweaks
1 parent 2352495 commit 6d0404b

File tree

4 files changed

+91
-74
lines changed

4 files changed

+91
-74
lines changed

README.md

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,27 @@ make all
4545
Run a server:
4646

4747
```bash
48-
./gitmdm-server -git /opt/compliance
48+
gitmdm-server -git /var/git
4949
```
5050

5151
If you are a fan of Google Cloud Run, check out `./hacks/deploy.sh` for a deployment script.
5252

53-
On a client:
53+
On a client, the --install flag establishes persistence:
5454

5555
```bash
56-
$ ./gitmdm-agent --install --server https://comply.internal --join XXXX
56+
$ gitmdm-agent --install --server https://gitmdm.cloud --join XXXX
5757
```
5858

59-
## What kind of checks do we do?
59+
## What compliance items does gitMDM check for?
6060

61+
Only the things that come up in a SOC-2 or ISO 27001 report:
62+
63+
* Antivirus
64+
* Firewall
6165
* Full Disk Encryption
62-
* Screen locks
6366
* OS updates
64-
* Firewall
67+
* Password complexity (respecting NIST 800-36B)
68+
* Screen locks
6569

6670
## What kind of bizarre platforms do you support?
6771

@@ -75,6 +79,13 @@ $ ./gitmdm-agent --install --server https://comply.internal --join XXXX
7579
- Windows 11/10 (though we've never tried it)
7680
```
7781
82+
## Installation That Respects Your OS
83+
84+
- **Linux**: systemd user service (falls back to cron)
85+
- **(Dragonfly|Net|Free|Open)BSD**: cron
86+
- **macOS**: launchd
87+
- **Windows**: Task Scheduler
88+
7889
We detect 11+ desktop environments because your team refuses to standardize.
7990
8091
## Security Architecture
@@ -91,7 +102,7 @@ We detect 11+ desktop environments because your team refuses to standardize.
91102

92103
The server literally cannot execute commands. We removed the code. It's not there.
93104

94-
## For Your Compliance Team
105+
## FAQ
95106

96107
> "What happens if someone compromises the server?"
97108
@@ -105,13 +116,6 @@ They can. It's their machine. They can also lie on spreadsheets. At least this h
105116
106117
No. But neither was Stripe when you started using it.
107118

108-
## Installation That Respects Your OS
109-
110-
- **Linux**: systemd user service (falls back to cron if you're systemd-free)
111-
- **(Dragonfly|Net|Free|Open)BSD**: cron (because rc.d requires root and we're not animals)
112-
- **macOS**: launchd (the least worst option)
113-
- **Windows**: Task Scheduler (runs as user, not SYSTEM)
114-
115119
---
116120

117121
*Because your security posture shouldn't require the missionary position.*

cmd/agent/install.go

Lines changed: 66 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"os"
78
"os/exec"
@@ -28,7 +29,7 @@ type AgentConfig struct {
2829
// os.UserConfigDir() returns:
2930
// - macOS: ~/Library/Application Support
3031
// - Linux/BSD: $XDG_CONFIG_HOME or ~/.config
31-
// - Windows: %AppData%
32+
// - Windows: %AppData%.
3233
func configDir() (string, error) {
3334
configDir, err := os.UserConfigDir()
3435
if err != nil {
@@ -58,6 +59,40 @@ func loadConfig() (*AgentConfig, error) {
5859
return &config, nil
5960
}
6061

62+
// installExecutable copies the executable to the target path, handling busy files.
63+
func installExecutable(exePath, targetPath string) error {
64+
// Stop any existing instance (but not ourselves)
65+
// Note: We accept the TOCTOU risk here as it's a best-effort cleanup
66+
if _, err := os.Stat(targetPath); err == nil {
67+
// Use exec.Command directly to avoid shell injection
68+
// The targetPath is safe (constructed from constants) but better to be explicit
69+
cmd := exec.Command("pkill", "-f", targetPath) //nolint:noctx // killing existing process doesn't need context
70+
_ = cmd.Run() //nolint:errcheck // Best effort
71+
time.Sleep(500 * time.Millisecond)
72+
}
73+
74+
// Try direct copy first
75+
if err := copyFile(exePath, targetPath); err != nil {
76+
// Handle "text file busy" error by copying to temp and renaming
77+
if !strings.Contains(strings.ToLower(err.Error()), "text file busy") {
78+
return fmt.Errorf("failed to copy executable: %w", err)
79+
}
80+
81+
// Copy to temp file and rename
82+
tempPath := targetPath + ".new"
83+
if err := copyFile(exePath, tempPath); err != nil {
84+
return fmt.Errorf("failed to copy executable to temp file: %w", err)
85+
}
86+
if err := os.Rename(tempPath, targetPath); err != nil {
87+
_ = os.Remove(targetPath) //nolint:errcheck // Try removing old file
88+
if err := os.Rename(tempPath, targetPath); err != nil {
89+
return fmt.Errorf("failed to replace executable: %w", err)
90+
}
91+
}
92+
}
93+
return nil
94+
}
95+
6196
// installAgent installs the agent to run automatically at system startup.
6297
func installAgent(serverURL, joinKey string) error {
6398
// Get home directory
@@ -68,7 +103,7 @@ func installAgent(serverURL, joinKey string) error {
68103

69104
// Create installation directory
70105
targetDir := filepath.Join(homeDir, installDir)
71-
if err := os.MkdirAll(targetDir, 0o755); err != nil {
106+
if err := os.MkdirAll(targetDir, 0o755); err != nil { //nolint:gosec // standard directory permissions for program dir
72107
return fmt.Errorf("failed to create directory %s: %w", targetDir, err)
73108
}
74109

@@ -86,39 +121,13 @@ func installAgent(serverURL, joinKey string) error {
86121
fmt.Printf("Agent is already installed at %s\n", targetPath)
87122
// Just need to ensure autostart is configured
88123
} else {
89-
// Stop any existing instance (but not ourselves)
90-
// Note: We accept the TOCTOU risk here as it's a best-effort cleanup
91-
if _, err := os.Stat(targetPath); err == nil {
92-
// Use exec.Command directly to avoid shell injection
93-
// The targetPath is safe (constructed from constants) but better to be explicit
94-
cmd := exec.Command("pkill", "-f", targetPath) //nolint:noctx
95-
_ = cmd.Run() //nolint:errcheck // Best effort
96-
time.Sleep(500 * time.Millisecond)
97-
}
98-
99-
// Try direct copy first
100-
if err := copyFile(exePath, targetPath); err != nil {
101-
// Handle "text file busy" error by copying to temp and renaming
102-
if !strings.Contains(strings.ToLower(err.Error()), "text file busy") {
103-
return fmt.Errorf("failed to copy executable: %w", err)
104-
}
105-
106-
// Copy to temp file and rename
107-
tempPath := targetPath + ".new"
108-
if err := copyFile(exePath, tempPath); err != nil {
109-
return fmt.Errorf("failed to copy executable to temp file: %w", err)
110-
}
111-
if err := os.Rename(tempPath, targetPath); err != nil {
112-
_ = os.Remove(targetPath) //nolint:errcheck // Try removing old file
113-
if err := os.Rename(tempPath, targetPath); err != nil {
114-
return fmt.Errorf("failed to replace executable: %w", err)
115-
}
116-
}
124+
if err := installExecutable(exePath, targetPath); err != nil {
125+
return err
117126
}
118127
}
119128

120129
// Make executable
121-
if err := os.Chmod(targetPath, 0o755); err != nil {
130+
if err := os.Chmod(targetPath, 0o755); err != nil { //nolint:gosec // executable needs execute permission
122131
return fmt.Errorf("failed to set executable permissions: %w", err)
123132
}
124133

@@ -205,7 +214,7 @@ func copyFile(src, dst string) error {
205214
if err != nil {
206215
return err
207216
}
208-
return os.WriteFile(dst, data, 0o755)
217+
return os.WriteFile(dst, data, 0o755) //nolint:gosec // executable needs execute permission
209218
}
210219

211220
// isSystemdUserAvailable checks if systemd user services are available and working.
@@ -243,7 +252,7 @@ func installMacOS(agentPath, serverURL, joinKey string) error {
243252

244253
// Create LaunchAgents directory if it doesn't exist
245254
launchAgentsDir := filepath.Join(homeDir, "Library", "LaunchAgents")
246-
if err := os.MkdirAll(launchAgentsDir, 0o755); err != nil {
255+
if err := os.MkdirAll(launchAgentsDir, 0o755); err != nil { //nolint:gosec // standard permissions for LaunchAgents
247256
return fmt.Errorf("failed to create LaunchAgents directory: %w", err)
248257
}
249258

@@ -298,13 +307,12 @@ func installMacOS(agentPath, serverURL, joinKey string) error {
298307

299308
// Load the launch agent
300309
cmd := exec.Command("launchctl", "load", plistPath) //nolint:noctx // local command
301-
output, err := cmd.CombinedOutput()
302-
if err != nil {
310+
if _, err := cmd.CombinedOutput(); err != nil {
303311
// Try to unload first in case it's already loaded
304312
_ = exec.Command("launchctl", "unload", plistPath).Run() //nolint:errcheck,noctx // Best effort
305313
// Try loading again
306314
cmd = exec.Command("launchctl", "load", plistPath) //nolint:noctx // local command
307-
if output, err = cmd.CombinedOutput(); err != nil {
315+
if output, err := cmd.CombinedOutput(); err != nil {
308316
return fmt.Errorf("failed to load launch agent: %w\nOutput: %s", err, output)
309317
}
310318
}
@@ -347,7 +355,7 @@ func installLinux(agentPath, serverURL, joinKey string) error {
347355
}
348356

349357
serviceDir := filepath.Join(homeDir, ".config", "systemd", "user")
350-
if err := os.MkdirAll(serviceDir, 0o755); err != nil {
358+
if err := os.MkdirAll(serviceDir, 0o755); err != nil { //nolint:gosec // standard permissions for systemd services
351359
return fmt.Errorf("failed to create systemd directory: %w", err)
352360
}
353361

@@ -447,7 +455,7 @@ func uninstallLinux() error {
447455
func installCron(agentPath, _, _ string) error {
448456
// Check if crontab is available
449457
if _, err := exec.LookPath("crontab"); err != nil {
450-
return fmt.Errorf("neither systemd user services nor cron are available - manual startup required")
458+
return errors.New("neither systemd user services nor cron are available - manual startup required")
451459
}
452460

453461
// Get current crontab
@@ -478,13 +486,13 @@ func installCron(agentPath, _, _ string) error {
478486
// Try to start the agent immediately in background
479487
// Security note: agentPath is constructed from filepath.Join with constants,
480488
// but we use exec.Command directly to avoid shell injection risks
481-
cmd = exec.Command(agentPath) //nolint:noctx
489+
cmd = exec.Command(agentPath) //nolint:noctx // agent spawns its own context
482490
cmd.Stdin = nil
483-
cmd.Stdout = nil
491+
cmd.Stdout = nil
484492
cmd.Stderr = nil
485493
if err := cmd.Start(); err == nil {
486494
// Detach from the process
487-
_ = cmd.Process.Release() //nolint:errcheck
495+
_ = cmd.Process.Release() //nolint:errcheck // best effort cleanup
488496
}
489497

490498
return nil
@@ -530,6 +538,8 @@ func uninstallCron() error {
530538
}
531539

532540
// installWindows installs Windows Task Scheduler task.
541+
//
542+
//nolint:unused // Windows-specific function needed for cross-platform support
533543
func installWindows(agentPath, _, _ string) error {
534544
// Create the task XML content
535545
taskXML := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-16"?>
@@ -583,39 +593,42 @@ func installWindows(agentPath, _, _ string) error {
583593
if err != nil {
584594
return fmt.Errorf("failed to create temp file: %w", err)
585595
}
586-
defer os.Remove(tempFile.Name()) //nolint:errcheck
596+
defer os.Remove(tempFile.Name()) //nolint:errcheck // best effort cleanup
587597

588598
if _, err := tempFile.WriteString(taskXML); err != nil {
589599
return fmt.Errorf("failed to write task XML: %w", err)
590600
}
591-
tempFile.Close() //nolint:errcheck
601+
_ = tempFile.Close() //nolint:errcheck // file already written
592602

593603
// Delete existing task if present (ignore errors)
594-
cmd := exec.Command("schtasks", "/Delete", "/TN", "GitMDM Agent", "/F") //nolint:noctx
595-
_ = cmd.Run() //nolint:errcheck
604+
cmd := exec.Command("schtasks", "/Delete", "/TN", "GitMDM Agent", "/F") //nolint:noctx // Windows task management doesn't need context
605+
_ = cmd.Run() //nolint:errcheck // best effort cleanup
596606

597607
// Create the scheduled task
598-
cmd = exec.Command("schtasks", "/Create", "/TN", "GitMDM Agent", "/XML", tempFile.Name()) //nolint:noctx
608+
//nolint:noctx,gosec,lll // Windows task management doesn't need context, XML file is trusted
609+
cmd = exec.Command("schtasks", "/Create", "/TN", "GitMDM Agent", "/XML", tempFile.Name())
599610
output, err := cmd.CombinedOutput()
600611
if err != nil {
601612
return fmt.Errorf("failed to create scheduled task: %w\nOutput: %s", err, output)
602613
}
603614

604615
// Start the task immediately
605-
cmd = exec.Command("schtasks", "/Run", "/TN", "GitMDM Agent") //nolint:noctx
606-
_ = cmd.Run() //nolint:errcheck // Best effort
616+
cmd = exec.Command("schtasks", "/Run", "/TN", "GitMDM Agent") //nolint:noctx // Windows task management doesn't need context
617+
_ = cmd.Run() //nolint:errcheck // Best effort
607618

608619
return nil
609620
}
610621

611622
// uninstallWindows removes Windows Task Scheduler task.
623+
//
624+
//nolint:unused // Windows-specific function needed for cross-platform support
612625
func uninstallWindows() error {
613626
// Stop the task
614-
cmd := exec.Command("schtasks", "/End", "/TN", "GitMDM Agent") //nolint:noctx
615-
_ = cmd.Run() //nolint:errcheck // Best effort
627+
cmd := exec.Command("schtasks", "/End", "/TN", "GitMDM Agent") //nolint:noctx // Windows task management doesn't need context
628+
_ = cmd.Run() //nolint:errcheck // Best effort
616629

617630
// Delete the task
618-
cmd = exec.Command("schtasks", "/Delete", "/TN", "GitMDM Agent", "/F") //nolint:noctx
631+
cmd = exec.Command("schtasks", "/Delete", "/TN", "GitMDM Agent", "/F") //nolint:noctx // Windows task management doesn't need context
619632
output, err := cmd.CombinedOutput()
620633
if err != nil {
621634
if !strings.Contains(string(output), "The system cannot find") {
@@ -625,8 +638,8 @@ func uninstallWindows() error {
625638
}
626639

627640
// Try to stop any running agent process
628-
cmd = exec.Command("taskkill", "/F", "/IM", "gitmdm-agent.exe") //nolint:noctx
629-
_ = cmd.Run() //nolint:errcheck // Best effort
641+
cmd = exec.Command("taskkill", "/F", "/IM", "gitmdm-agent.exe") //nolint:noctx // Windows process management doesn't need context
642+
_ = cmd.Run() //nolint:errcheck // Best effort
630643

631644
return nil
632645
}

cmd/agent/main.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010
"encoding/json"
1111
"flag"
1212
"fmt"
13+
"gitmdm/internal/analyzer"
14+
"gitmdm/internal/config"
15+
"gitmdm/internal/gitmdm"
1316
"io"
1417
"log"
1518
"net/http"
@@ -23,10 +26,6 @@ import (
2326
"syscall"
2427
"time"
2528

26-
"gitmdm/internal/analyzer"
27-
"gitmdm/internal/config"
28-
"gitmdm/internal/gitmdm"
29-
3029
"github.com/codeGROOVE-dev/retry"
3130

3231
"gopkg.in/yaml.v3"
@@ -156,7 +155,8 @@ func main() {
156155

157156
if err := agent.sendReport(ctx, report); err != nil {
158157
cancel()
159-
log.Fatalf("Failed to verify server connection: %v\nPlease check your --server and --join parameters", err) //nolint:gocritic // exitAfterDefer
158+
//nolint:gocritic,lll // exitAfterDefer
159+
log.Fatalf("Failed to verify server connection: %v\nPlease check your --server and --join parameters", err)
160160
}
161161

162162
log.Println("✓ Server connection verified successfully")

cmd/server/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ func (s *Server) updateComplianceCacheLocked(device *gitmdm.Device) {
408408
// Device hasn't checked in yet - use all data from git
409409
staleThreshold = time.Time{} // Zero time means no filtering
410410
}
411-
411+
412412
for _, check := range device.Checks {
413413
// Skip stale checks only if we have a threshold and the check has a timestamp
414414
if !staleThreshold.IsZero() && !check.Timestamp.IsZero() && check.Timestamp.Before(staleThreshold) {
@@ -648,7 +648,7 @@ func (s *Server) handleDevice(writer http.ResponseWriter, r *http.Request) {
648648

649649
s.mu.RLock()
650650
device, exists := s.devices[hardwareID]
651-
cache, _ := s.complianceCache[hardwareID]
651+
cache := s.complianceCache[hardwareID]
652652
s.mu.RUnlock()
653653

654654
if !exists {

0 commit comments

Comments
 (0)