Skip to content

Commit 2a61f78

Browse files
committed
make it more production worthy
1 parent db0e06f commit 2a61f78

27 files changed

+3744
-506
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ GOTEST=$(GOCMD) test
77
GOGET=$(GOCMD) get
88
GOMOD=$(GOCMD) mod
99

10-
SERVER_BINARY=gitmdm-server
11-
AGENT_BINARY=gitmdm-agent
10+
SERVER_BINARY=./out/gitmdm-server
11+
AGENT_BINARY=./out/gitmdm-agent
1212
SERVER_PATH=./cmd/server
1313
AGENT_PATH=./cmd/agent
1414

README.md

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Security-first compliance reporting that doesn't compromise your infrastructure.
44

5+
![gitMDM Logo](media/logo_small.png)
6+
57
## The Problem
68

79
Every MDM is a backdoor. They typically require root access and arbitrary remote code execution. They're incompatible with secure-by-default operating systems. Yet auditors require them for SOC 2.
@@ -14,11 +16,22 @@ gitMDM proves compliance without compromising security:
1416
- **No phone-home** - Your git repo, your endpoint, your control
1517
- **Works everywhere** - Including secure-by-default systems such as OpenBSD.
1618

19+
## Screenshots
20+
21+
**Dashboard: Compliance at a glance**
22+
<a href="media/dashboard.png"><img src="media/dashboard.png" alt="Dashboard" width="400"/></a>
23+
24+
**Agent Report: Detailed results**
25+
<a href="media/report.png"><img src="media/report.png" alt="Agent Report" width="400"/></a>
26+
27+
**Remediation: Plain-English guidance**
28+
<a href="media/remediate.png"><img src="media/remediate.png" alt="Remediation Steps" width="400"/></a>
29+
1730
## How It Works
1831

1932
```
2033
[Agent] [Server] [Git]
21-
Run compiled checks → Receive reports only → Immutable audit trail
34+
Run compiled checks → Receive reports only → Tamper-resistant audit trail
2235
```
2336

2437
The server **cannot** push commands. Ever. That's the point.
@@ -27,10 +40,36 @@ The server **cannot** push commands. Ever. That's the point.
2740

2841
```bash
2942
# Server
30-
./gitmdm-server -git [email protected]:org/compliance.git -api-key SECRET
43+
./out/gitmdm-server -git [email protected]:org/compliance.git -api-key SECRET
44+
45+
# Agent
46+
./out/gitmdm-agent -server https://server:8080
47+
```
48+
49+
## Local Checks
50+
51+
You can run the compliance checks even without a server:
52+
53+
```bash
54+
./out/gitmdm-agent -run all
55+
```
56+
57+
You'll see output similar to:
58+
59+
```log
60+
🔍 Running security checks...
61+
62+
⚠️ 3 issues require attention
63+
64+
🔸 screen lock
65+
🐞 Problem: Screen idle time too long (1 hour, SOC 2 requires ≤15 min); Screen lock delay too long (4 hours, SOC 2 requires ≤15 min)
66+
💻 Evidence: defaults -currentHost read com.apple.screensaver idleTime && sysadminctl -screenLock status
3167
32-
# Agent (checks compiled in from checks.yaml)
33-
./gitmdm-agent -server https://server:8080
68+
🔧 How to fix:
69+
1. Open System Settings > Lock Screen
70+
2. Set 'Start Screen Saver when inactive' to 15 minutes or less
71+
3. Open System Settings > Lock Screen
72+
4. Set 'Require password after screen saver begins' to 'immediately'
3473
```
3574

3675
## What You Get
@@ -70,8 +109,7 @@ Edit, compile, deploy. No runtime configuration files to tamper with.
70109
## Building
71110
72111
```bash
73-
vim checks.yaml # Define your compliance checks
74-
make build # Compiles checks into binary
112+
make all
75113
```
76114

77115
## FAQ
@@ -80,7 +118,7 @@ make build # Compiles checks into binary
80118
A: It generates the reports auditors need. Without the backdoors.
81119

82120
**Q: What if we need to change checks?**
83-
A: Rebuild and redeploy. Immutability is a feature.
121+
A: Rebuild and redeploy. Immutability is a feature, not a bug.
84122

85123
**Q: Why git?**
86124
A: Cryptographic proof, audit trail, existing tooling, no database.

cmd/agent/analyzer/analyzer.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Package analyzer provides compliance check analysis and grading.
2+
package analyzer
3+
4+
import (
5+
"strings"
6+
)
7+
8+
// Result represents the analysis result of a compliance check.
9+
type Result struct {
10+
Status string // "pass", "fail", "n/a"
11+
Description string // Human-readable description
12+
Remediation []string // Command-specific remediation steps
13+
}
14+
15+
// AnalyzeCheck analyzes the output of a compliance check and returns pass/fail status.
16+
// Takes into account the OS and specific command being run for more accurate analysis.
17+
func AnalyzeCheck(checkName string, osName string, command string, stdout string, stderr string, exitCode int) Result {
18+
// Handle timeout case explicitly (timeout results in exitCode -1)
19+
if exitCode == -1 && strings.Contains(stderr, "Command timed out") {
20+
return Result{Status: "n/a", Description: "Check timed out - try again later"}
21+
}
22+
23+
// Combine output for analysis
24+
output := strings.ToLower(stdout + stderr)
25+
26+
// Extract the base command (first word) for command-specific analysis
27+
baseCommand := ""
28+
if command != "" {
29+
parts := strings.Fields(command)
30+
if len(parts) > 0 {
31+
baseCommand = parts[0]
32+
}
33+
}
34+
35+
switch checkName {
36+
case "disk_encryption":
37+
return analyzeDiskEncryption(output, osName, baseCommand)
38+
case "firewall":
39+
return analyzeFirewall(output, exitCode, osName, baseCommand)
40+
case "app_firewall":
41+
return analyzeAppFirewall(output, exitCode, osName, baseCommand)
42+
case "screen_lock":
43+
return analyzeScreenLock(output, osName, baseCommand, command)
44+
case "auto_login":
45+
return analyzeAutoLogin(output, osName, baseCommand)
46+
case "password_policy":
47+
return analyzePasswordPolicy(output, osName, baseCommand, command)
48+
case "updates", "software_updates":
49+
return analyzeUpdates(output, osName, baseCommand, exitCode)
50+
case "antivirus":
51+
return analyzeAntivirus(output, osName)
52+
case "hostname", "uname", "users", "network", "system_info":
53+
// Informational checks - no pass/fail
54+
return Result{Status: "n/a", Description: "Informational"}
55+
default:
56+
// Unknown check - can't grade
57+
return Result{Status: "n/a", Description: "No compliance criteria defined"}
58+
}
59+
}

cmd/agent/analyzer/antivirus.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package analyzer
2+
3+
import "strings"
4+
5+
func analyzeAntivirus(output string, osName string) Result {
6+
// For systemctl commands, check if service is active
7+
if strings.Contains(output, "active") && !strings.Contains(output, "inactive") {
8+
return Result{Status: "pass", Description: "ClamAV daemon active"}
9+
}
10+
11+
// macOS XProtect detection via pgrep
12+
if osName == "darwin" {
13+
// XProtect processes typically include XProtectService or similar
14+
if strings.Contains(output, "xprotect") {
15+
// Parse pgrep output: "PID processname"
16+
lines := strings.Split(output, "\n")
17+
for _, line := range lines {
18+
if strings.Contains(strings.ToLower(line), "xprotect") {
19+
// Extract process name from "PID processname" format
20+
parts := strings.Fields(line)
21+
if len(parts) >= 2 {
22+
return Result{Status: "pass", Description: "XProtect running"}
23+
}
24+
return Result{Status: "pass", Description: "XProtect running"}
25+
}
26+
}
27+
}
28+
29+
// If XProtect is not found on macOS, it's a failure (should always be running)
30+
// Only check if this was a pgrep command that returned no results
31+
if strings.Contains(output, "pgrep") || len(output) < 10 {
32+
return Result{Status: "fail", Description: "XProtect not running"}
33+
}
34+
35+
// For macOS, we only care about XProtect, not third-party AV
36+
// Return n/a for other commands like ps aux
37+
return Result{Status: "n/a", Description: "Only XProtect is required for macOS"}
38+
}
39+
40+
// For ps output, look for AV processes
41+
if strings.Contains(output, "ps aux") || strings.Contains(output, "pid") {
42+
// Parse ps output looking for AV processes
43+
lines := strings.Split(output, "\n")
44+
for _, line := range lines {
45+
lower := strings.ToLower(line)
46+
// Look for common AV processes
47+
if (strings.Contains(lower, "clamav") && !strings.Contains(lower, "grep")) ||
48+
(strings.Contains(lower, "sophos") && !strings.Contains(lower, "grep")) ||
49+
(strings.Contains(lower, "mcafee") && !strings.Contains(lower, "grep")) ||
50+
(strings.Contains(lower, "symantec") && !strings.Contains(lower, "grep")) ||
51+
(strings.Contains(lower, "defender") && !strings.Contains(lower, "grep")) ||
52+
(strings.Contains(lower, "xprotect") && osName == "darwin") ||
53+
(strings.Contains(lower, "mrt.app") && osName == "darwin") ||
54+
strings.Contains(lower, "antivirus") {
55+
return Result{Status: "pass", Description: "Antivirus process detected"}
56+
}
57+
}
58+
}
59+
60+
// Windows WMI output
61+
if strings.Contains(output, "displayname") && strings.Contains(output, "productstate") {
62+
// If we got WMI output with AV products
63+
if strings.Contains(output, "defender") ||
64+
strings.Contains(output, "antivirus") ||
65+
strings.Contains(output, "mcafee") ||
66+
strings.Contains(output, "norton") ||
67+
strings.Contains(output, "sophos") {
68+
return Result{Status: "pass", Description: "Windows antivirus detected"}
69+
}
70+
}
71+
72+
// No AV is often fine for servers/Linux
73+
if osName == "linux" || osName == "freebsd" || osName == "openbsd" {
74+
return Result{Status: "n/a", Description: "Antivirus optional for " + osName}
75+
}
76+
77+
// Can't check
78+
if strings.Contains(output, "permission denied") {
79+
return Result{Status: "n/a", Description: "Cannot check antivirus status"}
80+
}
81+
82+
return Result{Status: "n/a", Description: "Antivirus not detected"}
83+
}

cmd/agent/analyzer/app_firewall.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package analyzer
2+
3+
import "strings"
4+
5+
func analyzeAppFirewall(output string, exitCode int, osName string, baseCommand string) Result {
6+
// macOS Application Firewall specific analysis (SOC 2 relevant check)
7+
if baseCommand == "socketfilterfw" {
8+
// Check global state - this is the main SOC 2 compliance indicator
9+
if strings.Contains(output, "firewall is enabled") {
10+
return Result{Status: "pass", Description: "macOS Application Firewall enabled"}
11+
}
12+
if strings.Contains(output, "firewall is disabled") {
13+
return Result{
14+
Status: "fail",
15+
Description: "macOS Application Firewall disabled",
16+
Remediation: []string{
17+
"Open System Settings > Network > Firewall",
18+
"Click on 'Options...'",
19+
"Turn on 'Enable Firewall'",
20+
},
21+
}
22+
}
23+
}
24+
25+
// Permission issues or not available
26+
if strings.Contains(output, "permission denied") ||
27+
strings.Contains(output, "not found") ||
28+
strings.Contains(output, "command not found") {
29+
return Result{Status: "n/a", Description: "Cannot access application firewall"}
30+
}
31+
32+
return Result{Status: "n/a", Description: "Application firewall status unknown"}
33+
}

0 commit comments

Comments
 (0)