Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/stepsecurity-dev-machine-guard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ func main() {
if cfg.EnableNPMScan == nil && config.EnableNPMScan != nil {
cfg.EnableNPMScan = config.EnableNPMScan
}
if cfg.EnableBrewScan == nil && config.EnableBrewScan != nil {
cfg.EnableBrewScan = config.EnableBrewScan
}
if cfg.EnablePythonScan == nil && config.EnablePythonScan != nil {
cfg.EnablePythonScan = config.EnablePythonScan
}
if cfg.ColorMode == "auto" && config.ColorMode != "" {
cfg.ColorMode = config.ColorMode
}
Expand Down
47 changes: 46 additions & 1 deletion examples/sample-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,56 @@
}
],
"node_packages": [],
"node_projects": [
{ "path": "/Users/developer/projects/my-app", "package_manager": "npm" },
{ "path": "/Users/developer/projects/api-server", "package_manager": "yarn" },
{ "path": "/Users/developer/projects/frontend", "package_manager": "pnpm" }
],
"brew_package_manager": {
"name": "homebrew",
"version": "4.3.5",
"path": "/opt/homebrew/bin/brew"
},
"brew_formulae": [
{ "name": "ca-certificates", "version": "2024.2.2" },
{ "name": "curl", "version": "8.4.0" },
{ "name": "git", "version": "2.43.0" },
{ "name": "openssl@3", "version": "3.2.0" }
],
"brew_casks": [
{ "name": "visual-studio-code", "version": "1.85.0" },
{ "name": "firefox", "version": "120.0" }
],
"python_package_managers": [
{
"name": "python3",
"version": "3.12.0",
"path": "/usr/local/bin/python3"
},
{
"name": "pip",
"version": "24.0",
"path": "/usr/local/bin/pip3"
}
],
"python_packages": [
{ "name": "requests", "version": "2.31.0" },
{ "name": "numpy", "version": "1.26.2" },
{ "name": "pip", "version": "24.0" }
],
"python_projects": [
{ "path": "/Users/developer/projects/ml-pipeline", "package_manager": "poetry" },
{ "path": "/Users/developer/projects/data-analysis", "package_manager": "pip" },
{ "path": "/Users/developer/projects/web-scraper", "package_manager": "uv" }
],
"summary": {
"ai_agents_and_tools_count": 5,
"ide_installations_count": 3,
"ide_extensions_count": 4,
"mcp_configs_count": 2,
"node_projects_count": 0
"node_projects_count": 3,
"brew_formulae_count": 42,
"brew_casks_count": 15,
"python_projects_count": 3
}
}
46 changes: 32 additions & 14 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import (

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

// Parse parses CLI arguments and returns a Config.
Expand Down Expand Up @@ -69,6 +71,18 @@ func Parse(args []string) (*Config, error) {
case arg == "--disable-npm-scan":
v := false
cfg.EnableNPMScan = &v
case arg == "--enable-brew-scan":
v := true
cfg.EnableBrewScan = &v
case arg == "--disable-brew-scan":
v := false
cfg.EnableBrewScan = &v
case arg == "--enable-python-scan":
v := true
cfg.EnablePythonScan = &v
case arg == "--disable-python-scan":
v := false
cfg.EnablePythonScan = &v
case strings.HasPrefix(arg, "--color="):
mode := strings.TrimPrefix(arg, "--color=")
if mode != "auto" && mode != "always" && mode != "never" {
Expand Down Expand Up @@ -127,12 +141,16 @@ Output formats (community mode, mutually exclusive):

Options:
--search-dirs DIR [DIR...] Search DIRs instead of $HOME (replaces default; repeatable)
--enable-npm-scan Enable Node.js package scanning
--disable-npm-scan Disable Node.js package scanning
--verbose Show progress messages (suppressed by default)
--color=WHEN Color mode: auto | always | never (default: auto)
-v, --version Show version
-h, --help Show this help
--enable-npm-scan Enable Node.js package scanning
--disable-npm-scan Disable Node.js package scanning
--enable-brew-scan Enable Homebrew package scanning
--disable-brew-scan Disable Homebrew package scanning
--enable-python-scan Enable Python package scanning
--disable-python-scan Disable Python package scanning
--verbose Show progress messages (suppressed by default)
--color=WHEN Color mode: auto | always | never (default: auto)
-v, --version Show version
-h, --help Show this help

Examples:
%s # Pretty terminal output
Expand Down
58 changes: 56 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var (
ScanFrequencyHours = "{{SCAN_FREQUENCY_HOURS}}"
SearchDirs []string
EnableNPMScan *bool // nil=auto
EnableBrewScan *bool // nil=auto
EnablePythonScan *bool // nil=auto
ColorMode string // "" means auto
OutputFormat string // "" means default (pretty)
HTMLOutputFile string // "" means not set
Expand All @@ -31,6 +33,8 @@ type ConfigFile struct {
ScanFrequencyHours string `json:"scan_frequency_hours,omitempty"`
SearchDirs []string `json:"search_dirs,omitempty"`
EnableNPMScan *bool `json:"enable_npm_scan,omitempty"`
EnableBrewScan *bool `json:"enable_brew_scan,omitempty"`
EnablePythonScan *bool `json:"enable_python_scan,omitempty"`
ColorMode string `json:"color_mode,omitempty"`
OutputFormat string `json:"output_format,omitempty"`
HTMLOutputFile string `json:"html_output_file,omitempty"`
Expand Down Expand Up @@ -79,6 +83,12 @@ func Load() {
if cfg.EnableNPMScan != nil && EnableNPMScan == nil {
EnableNPMScan = cfg.EnableNPMScan
}
if cfg.EnableBrewScan != nil && EnableBrewScan == nil {
EnableBrewScan = cfg.EnableBrewScan
}
if cfg.EnablePythonScan != nil && EnablePythonScan == nil {
EnablePythonScan = cfg.EnablePythonScan
}
if cfg.ColorMode != "" && ColorMode == "" {
ColorMode = cfg.ColorMode
}
Expand Down Expand Up @@ -156,6 +166,48 @@ func RunConfigure() error {
existing.EnableNPMScan = nil // auto
}

// Enable brew scan
currentBrew := "auto"
if existing.EnableBrewScan != nil {
if *existing.EnableBrewScan {
currentBrew = "true"
} else {
currentBrew = "false"
}
}
brewInput := promptValue(reader, "Enable Homebrew Scan (auto/true/false)", currentBrew)
switch strings.ToLower(brewInput) {
case "true":
v := true
existing.EnableBrewScan = &v
case "false":
v := false
existing.EnableBrewScan = &v
default:
existing.EnableBrewScan = nil
}

// Enable python scan
currentPython := "auto"
if existing.EnablePythonScan != nil {
if *existing.EnablePythonScan {
currentPython = "true"
} else {
currentPython = "false"
}
}
pythonInput := promptValue(reader, "Enable Python Scan (auto/true/false)", currentPython)
switch strings.ToLower(pythonInput) {
case "true":
v := true
existing.EnablePythonScan = &v
case "false":
v := false
existing.EnablePythonScan = &v
default:
existing.EnablePythonScan = nil
}

// Color mode
currentColor := existing.ColorMode
if currentColor == "" {
Expand Down Expand Up @@ -287,7 +339,9 @@ func ShowConfigure() {
fmt.Printf(" %-24s %s\n", "API Key:", maskSecret(cfg.APIKey))
fmt.Printf(" %-24s %s\n", "Scan Frequency:", displayFrequency(cfg.ScanFrequencyHours))
fmt.Printf(" %-24s %s\n", "Search Directories:", displayDirs(cfg.SearchDirs))
fmt.Printf(" %-24s %s\n", "Enable NPM Scan:", displayNPMScan(cfg.EnableNPMScan))
fmt.Printf(" %-24s %s\n", "Enable NPM Scan:", displayBoolScan(cfg.EnableNPMScan))
fmt.Printf(" %-24s %s\n", "Enable Brew Scan:", displayBoolScan(cfg.EnableBrewScan))
fmt.Printf(" %-24s %s\n", "Enable Python Scan:", displayBoolScan(cfg.EnablePythonScan))
fmt.Printf(" %-24s %s\n", "Color Mode:", displayColorMode(cfg.ColorMode))
fmt.Printf(" %-24s %s\n", "Output Format:", displayOutputFormat(cfg.OutputFormat))
if cfg.OutputFormat == "html" {
Expand Down Expand Up @@ -330,7 +384,7 @@ func displayDirs(dirs []string) string {
return strings.Join(dirs, ", ")
}

func displayNPMScan(v *bool) string {
func displayBoolScan(v *bool) string {
if v == nil {
return "auto"
}
Expand Down
98 changes: 98 additions & 0 deletions internal/detector/brew.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package detector

import (
"context"
"strings"
"time"

"github.com/step-security/dev-machine-guard/internal/executor"
"github.com/step-security/dev-machine-guard/internal/model"
)

// BrewDetector detects Homebrew installation and packages.
type BrewDetector struct {
exec executor.Executor
}

func NewBrewDetector(exec executor.Executor) *BrewDetector {
return &BrewDetector{exec: exec}
}

// DetectBrew checks if Homebrew is installed and returns its version info.
// Returns nil if Homebrew is not found.
func (d *BrewDetector) DetectBrew(ctx context.Context) *model.PkgManager {
path, err := d.exec.LookPath("brew")
if err != nil {
return nil
}

version := "unknown"
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 10*time.Second, "brew", "--version")
if err == nil {
// "brew --version" outputs "Homebrew 4.3.5\n..."
if line := firstLine(stdout); line != "" {
version = strings.TrimPrefix(line, "Homebrew ")
}
}

return &model.PkgManager{
Name: "homebrew",
Version: version,
Path: path,
}
}

// ListFormulae returns installed Homebrew formulae with versions.
func (d *BrewDetector) ListFormulae(ctx context.Context) []model.BrewPackage {
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 30*time.Second, "brew", "list", "--formula", "--versions")
if err != nil {
return nil
}
return parseBrewList(stdout)
}

// ListCasks returns installed Homebrew casks with versions.
func (d *BrewDetector) ListCasks(ctx context.Context) []model.BrewPackage {
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 30*time.Second, "brew", "list", "--cask", "--versions")
if err != nil {
return nil
}
return parseBrewList(stdout)
}

// parseBrewList parses "name version" lines from `brew list --versions` output.
func parseBrewList(stdout string) []model.BrewPackage {
stdout = strings.TrimSpace(stdout)
if stdout == "" {
return nil
}
var packages []model.BrewPackage
for _, line := range strings.Split(stdout, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Format: "name version [version2 ...]"
parts := strings.Fields(line)
if len(parts) >= 2 {
packages = append(packages, model.BrewPackage{
Name: parts[0],
Version: parts[1],
})
} else if len(parts) == 1 {
packages = append(packages, model.BrewPackage{
Name: parts[0],
Version: "unknown",
})
}
}
return packages
}

func firstLine(s string) string {
s = strings.TrimSpace(s)
if i := strings.IndexByte(s, '\n'); i >= 0 {
return s[:i]
}
return s
}
Loading
Loading