Skip to content
82 changes: 82 additions & 0 deletions cmd/stepsecurity-dev-machine-guard/main.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"runtime"

"github.com/step-security/dev-machine-guard/internal/buildinfo"
"github.com/step-security/dev-machine-guard/internal/cli"
"github.com/step-security/dev-machine-guard/internal/config"
"github.com/step-security/dev-machine-guard/internal/detector/configaudit"
"github.com/step-security/dev-machine-guard/internal/device"
"github.com/step-security/dev-machine-guard/internal/executor"
"github.com/step-security/dev-machine-guard/internal/launchd"
"github.com/step-security/dev-machine-guard/internal/output"
"github.com/step-security/dev-machine-guard/internal/progress"
"github.com/step-security/dev-machine-guard/internal/scan"
"github.com/step-security/dev-machine-guard/internal/schtasks"
Expand Down Expand Up @@ -160,6 +166,22 @@ func main() {
}

default:
// --npmrc and --pipconfig: focused, verbose pretty audits that
// bypass everything else for a fast (~1s) deep dive.
if cfg.NPMRCOnly {
if err := runNPMRCOnly(exec, cfg); err != nil {
log.Error("%v", err)
os.Exit(1)
}
return
}
if cfg.PipConfigOnly {
if err := runPipConfigOnly(exec, cfg); err != nil {
log.Error("%v", err)
os.Exit(1)
}
return
}
// Community mode or auto-detect enterprise
switch {
case cfg.OutputFormatSet || cfg.HTMLOutputFile != "":
Expand All @@ -184,3 +206,63 @@ func main() {
}
}
}

// runNPMRCOnly executes only the npmrc detector and renders the verbose
// pretty view (or JSON when --json is also passed). Skips IDE / AI / Brew /
// Python / Node / pip detection so the run is fast and the output is
// exclusively about npm configuration.
func runNPMRCOnly(exec executor.Executor, cfg *cli.Config) error {
ctx := context.Background()
dev := device.Gather(ctx, exec)
loggedInUser, _ := exec.LoggedInUser()

searchDirs := resolveScanSearchDirs(exec, cfg.SearchDirs)
audit := configaudit.NewNPMRCDetector(exec).Detect(ctx, searchDirs, loggedInUser)

if cfg.OutputFormat == "json" {
return scanJSONEncoder(os.Stdout).Encode(audit)
}
output.PrettyNPMRC(os.Stdout, &audit, dev, cfg.ColorMode)
return nil
}

// runPipConfigOnly executes only the pip-config detector and renders the
// verbose pretty view (or JSON when --json is also passed).
func runPipConfigOnly(exec executor.Executor, cfg *cli.Config) error {
ctx := context.Background()
dev := device.Gather(ctx, exec)
loggedInUser, _ := exec.LoggedInUser()

audit := configaudit.NewPipConfigDetector(exec).Detect(ctx, loggedInUser)

if cfg.OutputFormat == "json" {
return scanJSONEncoder(os.Stdout).Encode(audit)
}
output.PrettyPipConfig(os.Stdout, &audit, dev, cfg.ColorMode)
return nil
}

// resolveScanSearchDirs expands `$HOME` to the logged-in user's home dir
// and leaves other entries unchanged. Mirrors the helper inside scan.Run
// so --npmrc walks the same project tree the full scan would.
func resolveScanSearchDirs(exec executor.Executor, dirs []string) []string {
resolved := make([]string, 0, len(dirs))
for _, d := range dirs {
if d == "$HOME" {
if u, err := exec.LoggedInUser(); err == nil {
d = u.HomeDir
}
}
resolved = append(resolved, d)
}
return resolved
}

// scanJSONEncoder returns a 2-space-indented JSON encoder that doesn't
// HTML-escape — same conventions as the standard scan output.
func scanJSONEncoder(w io.Writer) *json.Encoder {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
return enc
}
8 changes: 8 additions & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type Config struct {
EnableBrewScan *bool // nil=auto, true/false=explicit
EnablePythonScan *bool // nil=auto, true/false=explicit
IncludeBundledPlugins bool // --include-bundled-plugins: include bundled/platform plugins in output
NPMRCOnly bool // --npmrc: run only the npmrc audit and render verbose pretty output
PipConfigOnly bool // --pipconfig: run only the pip config audit and render verbose pretty output
SearchDirs []string // defaults to ["$HOME"]
}

Expand Down Expand Up @@ -87,6 +89,10 @@ func Parse(args []string) (*Config, error) {
cfg.EnablePythonScan = &v
case arg == "--include-bundled-plugins":
cfg.IncludeBundledPlugins = true
case arg == "--npmrc":
cfg.NPMRCOnly = true
case arg == "--pipconfig":
cfg.PipConfigOnly = true
case strings.HasPrefix(arg, "--color="):
mode := strings.TrimPrefix(arg, "--color=")
if mode != "auto" && mode != "always" && mode != "never" {
Expand Down Expand Up @@ -163,6 +169,8 @@ Options:
--enable-python-scan Enable Python package scanning
--disable-python-scan Disable Python package scanning
--include-bundled-plugins Include bundled/platform plugins in output (Windows)
--npmrc Run ONLY the npm config audit (verbose pretty view; --json supported)
--pipconfig Run ONLY the pip config audit (verbose pretty view; --json supported)
--log-level=LEVEL Log level: error | warn | info | debug (default: info)
--verbose Shortcut for --log-level=debug
--color=WHEN Color mode: auto | always | never (default: auto)
Expand Down
Loading
Loading