Skip to content
Merged
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
96 changes: 96 additions & 0 deletions cmd/stepsecurity-dev-machine-guard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,21 @@ import (
"github.com/step-security/dev-machine-guard/internal/scan"
"github.com/step-security/dev-machine-guard/internal/schtasks"
"github.com/step-security/dev-machine-guard/internal/systemd"
"github.com/step-security/dev-machine-guard/internal/tcc"
"github.com/step-security/dev-machine-guard/internal/telemetry"
"github.com/step-security/dev-machine-guard/internal/winproc"
)

// auditSkipper builds a TCC skipper if scanning into TCC-protected dirs is
// not opted in. Mirrors scan.Run / telemetry.Run so the focused *Only audits
// don't accidentally prompt the user on macOS.
func auditSkipper(exec executor.Executor, cfg *cli.Config) *tcc.Skipper {
if !tcc.Enabled(cfg.IncludeTCCProtected) {
return nil
}
return tcc.New(executor.ResolveHome(exec))
}

// hookReconcileTimeout caps the entire reconcile step (fetch + cache
// write + install/uninstall). Generous because install can chown a
// handful of files under root; the actual GET cost is bounded by
Expand Down Expand Up @@ -389,6 +400,39 @@ func main() {
}
return
}
if cfg.PnpmRCOnly {
if !featuregate.IsEnabled(featuregate.FeaturePnpmConfigAudit) {
fmt.Fprintln(os.Stderr, featuregate.UnavailableMessage("--pnpmrc"))
os.Exit(1)
}
if err := runPnpmRCOnly(exec, cfg); err != nil {
log.Error("%v", err)
os.Exit(1)
}
return
}
if cfg.BunfigOnly {
if !featuregate.IsEnabled(featuregate.FeatureBunConfigAudit) {
fmt.Fprintln(os.Stderr, featuregate.UnavailableMessage("--bunfig"))
os.Exit(1)
}
if err := runBunfigOnly(exec, cfg); err != nil {
log.Error("%v", err)
os.Exit(1)
}
return
}
if cfg.YarnRCOnly {
if !featuregate.IsEnabled(featuregate.FeatureYarnConfigAudit) {
fmt.Fprintln(os.Stderr, featuregate.UnavailableMessage("--yarnrc"))
os.Exit(1)
}
if err := runYarnRCOnly(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 Down Expand Up @@ -450,6 +494,58 @@ func runPipConfigOnly(exec executor.Executor, cfg *cli.Config) error {
return nil
}

// runPnpmRCOnly executes only the pnpm detector and renders the verbose
// pretty view (or JSON when --json is also passed).
func runPnpmRCOnly(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.NewPnpmDetector(exec).WithSkipper(auditSkipper(exec, cfg)).Detect(ctx, searchDirs, loggedInUser)

Comment on lines +499 to +506
if cfg.OutputFormat == "json" {
return scanJSONEncoder(os.Stdout).Encode(audit)
}
output.PrettyPnpm(os.Stdout, &audit, dev, cfg.ColorMode)
return nil
}

// runBunfigOnly executes only the bun detector and renders the verbose
// pretty view (or JSON when --json is also passed).
func runBunfigOnly(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.NewBunDetector(exec).WithSkipper(auditSkipper(exec, cfg)).Detect(ctx, searchDirs, loggedInUser)

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

// runYarnRCOnly executes only the yarn detector (covering both .yarnrc and
// .yarnrc.yml) and renders the verbose pretty view (or JSON when --json is
// also passed).
func runYarnRCOnly(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.NewYarnDetector(exec).WithSkipper(auditSkipper(exec, cfg)).Detect(ctx, searchDirs, loggedInUser)

if cfg.OutputFormat == "json" {
return scanJSONEncoder(os.Stdout).Encode(audit)
}
output.PrettyYarn(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.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/tidwall/pretty v1.2.1
github.com/tidwall/sjson v1.2.5
gopkg.in/yaml.v3 v3.0.1
)

require github.com/tidwall/match v1.1.1 // indirect
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
28 changes: 26 additions & 2 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ type Config struct {
IncludeTCCProtected *bool
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
PnpmRCOnly bool // --pnpmrc: run only the pnpm config audit and render verbose pretty output
BunfigOnly bool // --bunfig: run only the bun config audit and render verbose pretty output
YarnRCOnly bool // --yarnrc: run only the yarn config audit (both flavors) and render verbose pretty output
SearchDirs []string // defaults to ["$HOME"]

// HooksAgent is the --agent value on `hooks install` / `hooks uninstall`;
Expand Down Expand Up @@ -76,6 +79,18 @@ type Config struct {
// Supported agents: claude-code and codex; the list grows as adapters are added.
var supportedHookAgents = []string{"claude-code", "codex"}

// boolCount returns how many of the booleans are true. Used to keep the
// "*-only" mutual-exclusion checks readable when the set grows past two.
func boolCount(bs ...bool) int {
n := 0
for _, b := range bs {
if b {
n++
}
}
return n
}

func isSupportedHookAgent(name string) bool {
return slices.Contains(supportedHookAgents, name)
}
Expand Down Expand Up @@ -163,6 +178,12 @@ func Parse(args []string) (*Config, error) {
cfg.NPMRCOnly = true
case arg == "--pipconfig":
cfg.PipConfigOnly = true
case arg == "--pnpmrc":
cfg.PnpmRCOnly = true
case arg == "--bunfig":
cfg.BunfigOnly = true
case arg == "--yarnrc":
cfg.YarnRCOnly = true
case strings.HasPrefix(arg, "--color="):
mode := strings.TrimPrefix(arg, "--color=")
if mode != "auto" && mode != "always" && mode != "never" {
Expand Down Expand Up @@ -265,8 +286,8 @@ func Parse(args []string) (*Config, error) {
i++
}

if cfg.NPMRCOnly && cfg.PipConfigOnly {
return nil, fmt.Errorf("--npmrc and --pipconfig are mutually exclusive; pick one")
if onlyCount := boolCount(cfg.NPMRCOnly, cfg.PipConfigOnly, cfg.PnpmRCOnly, cfg.BunfigOnly, cfg.YarnRCOnly); onlyCount > 1 {
return nil, fmt.Errorf("--npmrc, --pipconfig, --pnpmrc, --bunfig, and --yarnrc are mutually exclusive; pick one")
}

// --install-dir= (explicit empty) disables file logging by routing
Expand Down Expand Up @@ -426,6 +447,9 @@ Options:
include_tcc_protected: true.
--npmrc Run ONLY the npm config audit (verbose pretty view; --json supported)
--pipconfig Run ONLY the pip config audit (verbose pretty view; --json supported)
--pnpmrc Run ONLY the pnpm config audit (verbose pretty view; --json supported)
--bunfig Run ONLY the bun config audit (verbose pretty view; --json supported)
--yarnrc Run ONLY the yarn config audit covering both v1 (.yarnrc) and v2+ (.yarnrc.yml) (verbose pretty view; --json supported)
--log-level=LEVEL Log level: error | warn | info | debug (default: info)
--install-dir=DIR Base directory the agent puts ALL its files under
(logs, hook errors, binary placement via loader).
Expand Down
Loading
Loading