Skip to content

Commit 92c1526

Browse files
authored
Merge pull request #25 from shubham-stepsecurity/sm/test
feat(mdm): add brew, python, IDE plugin scanning with upload resilience
2 parents 0c9b6e2 + ffdea0d commit 92c1526

30 files changed

Lines changed: 2812 additions & 158 deletions

cmd/stepsecurity-dev-machine-guard/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ func main() {
3434
if cfg.EnableNPMScan == nil && config.EnableNPMScan != nil {
3535
cfg.EnableNPMScan = config.EnableNPMScan
3636
}
37+
if cfg.EnableBrewScan == nil && config.EnableBrewScan != nil {
38+
cfg.EnableBrewScan = config.EnableBrewScan
39+
}
40+
if cfg.EnablePythonScan == nil && config.EnablePythonScan != nil {
41+
cfg.EnablePythonScan = config.EnablePythonScan
42+
}
3743
if cfg.ColorMode == "auto" && config.ColorMode != "" {
3844
cfg.ColorMode = config.ColorMode
3945
}

examples/sample-output.json

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@
7070
"install_path": "/Applications/Claude.app",
7171
"vendor": "Anthropic",
7272
"is_installed": true
73+
},
74+
{
75+
"ide_type": "goland",
76+
"version": "2024.3.1",
77+
"install_path": "/Applications/GoLand.app",
78+
"vendor": "JetBrains",
79+
"is_installed": true
7380
}
7481
],
7582
"ide_extensions": [
@@ -126,11 +133,56 @@
126133
}
127134
],
128135
"node_packages": [],
136+
"node_projects": [
137+
{ "path": "/Users/developer/projects/my-app", "package_manager": "npm" },
138+
{ "path": "/Users/developer/projects/api-server", "package_manager": "yarn" },
139+
{ "path": "/Users/developer/projects/frontend", "package_manager": "pnpm" }
140+
],
141+
"brew_package_manager": {
142+
"name": "homebrew",
143+
"version": "4.3.5",
144+
"path": "/opt/homebrew/bin/brew"
145+
},
146+
"brew_formulae": [
147+
{ "name": "ca-certificates", "version": "2024.2.2" },
148+
{ "name": "curl", "version": "8.4.0" },
149+
{ "name": "git", "version": "2.43.0" },
150+
{ "name": "openssl@3", "version": "3.2.0" }
151+
],
152+
"brew_casks": [
153+
{ "name": "visual-studio-code", "version": "1.85.0" },
154+
{ "name": "firefox", "version": "120.0" }
155+
],
156+
"python_package_managers": [
157+
{
158+
"name": "python3",
159+
"version": "3.12.0",
160+
"path": "/usr/local/bin/python3"
161+
},
162+
{
163+
"name": "pip",
164+
"version": "24.0",
165+
"path": "/usr/local/bin/pip3"
166+
}
167+
],
168+
"python_packages": [
169+
{ "name": "requests", "version": "2.31.0" },
170+
{ "name": "numpy", "version": "1.26.2" },
171+
{ "name": "pip", "version": "24.0" }
172+
],
173+
"python_projects": [
174+
{ "path": "/Users/developer/projects/ml-pipeline", "package_manager": "poetry" },
175+
{ "path": "/Users/developer/projects/data-analysis", "package_manager": "pip" },
176+
{ "path": "/Users/developer/projects/web-scraper", "package_manager": "uv" }
177+
],
129178
"summary": {
130179
"ai_agents_and_tools_count": 5,
131-
"ide_installations_count": 3,
180+
"ide_installations_count": 4,
132181
"ide_extensions_count": 4,
133182
"mcp_configs_count": 2,
134-
"node_projects_count": 0
183+
"node_projects_count": 3,
184+
"brew_formulae_count": 42,
185+
"brew_casks_count": 15,
186+
"python_projects_count": 3
135187
}
136188
}

internal/cli/cli.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ import (
1111

1212
// Config holds all parsed CLI flags.
1313
type Config struct {
14-
Command string // "", "install", "uninstall", "send-telemetry", "configure", "configure show"
15-
OutputFormat string // "pretty", "json", "html"
16-
OutputFormatSet bool // true if --pretty/--json/--html was explicitly passed (not persisted)
17-
HTMLOutputFile string // set by --html (not persisted)
18-
ColorMode string // "auto", "always", "never"
19-
Verbose bool // --verbose
20-
EnableNPMScan *bool // nil=auto, true/false=explicit
21-
SearchDirs []string // defaults to ["$HOME"]
14+
Command string // "", "install", "uninstall", "send-telemetry", "configure", "configure show"
15+
OutputFormat string // "pretty", "json", "html"
16+
OutputFormatSet bool // true if --pretty/--json/--html was explicitly passed (not persisted)
17+
HTMLOutputFile string // set by --html (not persisted)
18+
ColorMode string // "auto", "always", "never"
19+
Verbose bool // --verbose
20+
EnableNPMScan *bool // nil=auto, true/false=explicit
21+
EnableBrewScan *bool // nil=auto, true/false=explicit
22+
EnablePythonScan *bool // nil=auto, true/false=explicit
23+
SearchDirs []string // defaults to ["$HOME"]
2224
}
2325

2426
// Parse parses CLI arguments and returns a Config.
@@ -69,6 +71,18 @@ func Parse(args []string) (*Config, error) {
6971
case arg == "--disable-npm-scan":
7072
v := false
7173
cfg.EnableNPMScan = &v
74+
case arg == "--enable-brew-scan":
75+
v := true
76+
cfg.EnableBrewScan = &v
77+
case arg == "--disable-brew-scan":
78+
v := false
79+
cfg.EnableBrewScan = &v
80+
case arg == "--enable-python-scan":
81+
v := true
82+
cfg.EnablePythonScan = &v
83+
case arg == "--disable-python-scan":
84+
v := false
85+
cfg.EnablePythonScan = &v
7286
case strings.HasPrefix(arg, "--color="):
7387
mode := strings.TrimPrefix(arg, "--color=")
7488
if mode != "auto" && mode != "always" && mode != "never" {
@@ -127,12 +141,16 @@ Output formats (community mode, mutually exclusive):
127141
128142
Options:
129143
--search-dirs DIR [DIR...] Search DIRs instead of $HOME (replaces default; repeatable)
130-
--enable-npm-scan Enable Node.js package scanning
131-
--disable-npm-scan Disable Node.js package scanning
132-
--verbose Show progress messages (suppressed by default)
133-
--color=WHEN Color mode: auto | always | never (default: auto)
134-
-v, --version Show version
135-
-h, --help Show this help
144+
--enable-npm-scan Enable Node.js package scanning
145+
--disable-npm-scan Disable Node.js package scanning
146+
--enable-brew-scan Enable Homebrew package scanning
147+
--disable-brew-scan Disable Homebrew package scanning
148+
--enable-python-scan Enable Python package scanning
149+
--disable-python-scan Disable Python package scanning
150+
--verbose Show progress messages (suppressed by default)
151+
--color=WHEN Color mode: auto | always | never (default: auto)
152+
-v, --version Show version
153+
-h, --help Show this help
136154
137155
Examples:
138156
%s # Pretty terminal output

internal/config/config.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ var (
1717
ScanFrequencyHours = "{{SCAN_FREQUENCY_HOURS}}"
1818
SearchDirs []string
1919
EnableNPMScan *bool // nil=auto
20+
EnableBrewScan *bool // nil=auto
21+
EnablePythonScan *bool // nil=auto
2022
ColorMode string // "" means auto
2123
OutputFormat string // "" means default (pretty)
2224
HTMLOutputFile string // "" means not set
@@ -31,6 +33,8 @@ type ConfigFile struct {
3133
ScanFrequencyHours string `json:"scan_frequency_hours,omitempty"`
3234
SearchDirs []string `json:"search_dirs,omitempty"`
3335
EnableNPMScan *bool `json:"enable_npm_scan,omitempty"`
36+
EnableBrewScan *bool `json:"enable_brew_scan,omitempty"`
37+
EnablePythonScan *bool `json:"enable_python_scan,omitempty"`
3438
ColorMode string `json:"color_mode,omitempty"`
3539
OutputFormat string `json:"output_format,omitempty"`
3640
HTMLOutputFile string `json:"html_output_file,omitempty"`
@@ -79,6 +83,12 @@ func Load() {
7983
if cfg.EnableNPMScan != nil && EnableNPMScan == nil {
8084
EnableNPMScan = cfg.EnableNPMScan
8185
}
86+
if cfg.EnableBrewScan != nil && EnableBrewScan == nil {
87+
EnableBrewScan = cfg.EnableBrewScan
88+
}
89+
if cfg.EnablePythonScan != nil && EnablePythonScan == nil {
90+
EnablePythonScan = cfg.EnablePythonScan
91+
}
8292
if cfg.ColorMode != "" && ColorMode == "" {
8393
ColorMode = cfg.ColorMode
8494
}
@@ -156,6 +166,48 @@ func RunConfigure() error {
156166
existing.EnableNPMScan = nil // auto
157167
}
158168

169+
// Enable brew scan
170+
currentBrew := "auto"
171+
if existing.EnableBrewScan != nil {
172+
if *existing.EnableBrewScan {
173+
currentBrew = "true"
174+
} else {
175+
currentBrew = "false"
176+
}
177+
}
178+
brewInput := promptValue(reader, "Enable Homebrew Scan (auto/true/false)", currentBrew)
179+
switch strings.ToLower(brewInput) {
180+
case "true":
181+
v := true
182+
existing.EnableBrewScan = &v
183+
case "false":
184+
v := false
185+
existing.EnableBrewScan = &v
186+
default:
187+
existing.EnableBrewScan = nil
188+
}
189+
190+
// Enable python scan
191+
currentPython := "auto"
192+
if existing.EnablePythonScan != nil {
193+
if *existing.EnablePythonScan {
194+
currentPython = "true"
195+
} else {
196+
currentPython = "false"
197+
}
198+
}
199+
pythonInput := promptValue(reader, "Enable Python Scan (auto/true/false)", currentPython)
200+
switch strings.ToLower(pythonInput) {
201+
case "true":
202+
v := true
203+
existing.EnablePythonScan = &v
204+
case "false":
205+
v := false
206+
existing.EnablePythonScan = &v
207+
default:
208+
existing.EnablePythonScan = nil
209+
}
210+
159211
// Color mode
160212
currentColor := existing.ColorMode
161213
if currentColor == "" {
@@ -287,7 +339,9 @@ func ShowConfigure() {
287339
fmt.Printf(" %-24s %s\n", "API Key:", maskSecret(cfg.APIKey))
288340
fmt.Printf(" %-24s %s\n", "Scan Frequency:", displayFrequency(cfg.ScanFrequencyHours))
289341
fmt.Printf(" %-24s %s\n", "Search Directories:", displayDirs(cfg.SearchDirs))
290-
fmt.Printf(" %-24s %s\n", "Enable NPM Scan:", displayNPMScan(cfg.EnableNPMScan))
342+
fmt.Printf(" %-24s %s\n", "Enable NPM Scan:", displayBoolScan(cfg.EnableNPMScan))
343+
fmt.Printf(" %-24s %s\n", "Enable Brew Scan:", displayBoolScan(cfg.EnableBrewScan))
344+
fmt.Printf(" %-24s %s\n", "Enable Python Scan:", displayBoolScan(cfg.EnablePythonScan))
291345
fmt.Printf(" %-24s %s\n", "Color Mode:", displayColorMode(cfg.ColorMode))
292346
fmt.Printf(" %-24s %s\n", "Output Format:", displayOutputFormat(cfg.OutputFormat))
293347
if cfg.OutputFormat == "html" {
@@ -330,7 +384,7 @@ func displayDirs(dirs []string) string {
330384
return strings.Join(dirs, ", ")
331385
}
332386

333-
func displayNPMScan(v *bool) string {
387+
func displayBoolScan(v *bool) string {
334388
if v == nil {
335389
return "auto"
336390
}

internal/detector/brew.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package detector
2+
3+
import (
4+
"context"
5+
"strings"
6+
"time"
7+
8+
"github.com/step-security/dev-machine-guard/internal/executor"
9+
"github.com/step-security/dev-machine-guard/internal/model"
10+
)
11+
12+
// BrewDetector detects Homebrew installation and packages.
13+
type BrewDetector struct {
14+
exec executor.Executor
15+
}
16+
17+
func NewBrewDetector(exec executor.Executor) *BrewDetector {
18+
return &BrewDetector{exec: exec}
19+
}
20+
21+
// DetectBrew checks if Homebrew is installed and returns its version info.
22+
// Returns nil if Homebrew is not found.
23+
func (d *BrewDetector) DetectBrew(ctx context.Context) *model.PkgManager {
24+
path, err := d.exec.LookPath("brew")
25+
if err != nil {
26+
return nil
27+
}
28+
29+
version := "unknown"
30+
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 10*time.Second, "brew", "--version")
31+
if err == nil {
32+
// "brew --version" outputs "Homebrew 4.3.5\n..."
33+
if line := firstLine(stdout); line != "" {
34+
version = strings.TrimPrefix(line, "Homebrew ")
35+
}
36+
}
37+
38+
return &model.PkgManager{
39+
Name: "homebrew",
40+
Version: version,
41+
Path: path,
42+
}
43+
}
44+
45+
// ListFormulae returns installed Homebrew formulae with versions.
46+
func (d *BrewDetector) ListFormulae(ctx context.Context) []model.BrewPackage {
47+
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 30*time.Second, "brew", "list", "--formula", "--versions")
48+
if err != nil {
49+
return nil
50+
}
51+
return parseBrewList(stdout)
52+
}
53+
54+
// ListCasks returns installed Homebrew casks with versions.
55+
func (d *BrewDetector) ListCasks(ctx context.Context) []model.BrewPackage {
56+
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 30*time.Second, "brew", "list", "--cask", "--versions")
57+
if err != nil {
58+
return nil
59+
}
60+
return parseBrewList(stdout)
61+
}
62+
63+
// parseBrewList parses "name version" lines from `brew list --versions` output.
64+
func parseBrewList(stdout string) []model.BrewPackage {
65+
stdout = strings.TrimSpace(stdout)
66+
if stdout == "" {
67+
return nil
68+
}
69+
var packages []model.BrewPackage
70+
for _, line := range strings.Split(stdout, "\n") {
71+
line = strings.TrimSpace(line)
72+
if line == "" {
73+
continue
74+
}
75+
// Format: "name version [version2 ...]"
76+
parts := strings.Fields(line)
77+
if len(parts) >= 2 {
78+
packages = append(packages, model.BrewPackage{
79+
Name: parts[0],
80+
Version: parts[1],
81+
})
82+
} else if len(parts) == 1 {
83+
packages = append(packages, model.BrewPackage{
84+
Name: parts[0],
85+
Version: "unknown",
86+
})
87+
}
88+
}
89+
return packages
90+
}
91+
92+
func firstLine(s string) string {
93+
s = strings.TrimSpace(s)
94+
if i := strings.IndexByte(s, '\n'); i >= 0 {
95+
return s[:i]
96+
}
97+
return s
98+
}

0 commit comments

Comments
 (0)