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
47 changes: 32 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,36 +68,53 @@ jobs:
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0

- name: Locate binary
id: binary
- name: Locate binaries
id: binaries
run: |
BINARY=$(find dist -type f -name '*darwin_unnotarized' | head -1)
if [ -z "$BINARY" ] || [ ! -f "$BINARY" ]; then
echo "::error::Binary not found"
find dist -type f
exit 1
fi
echo "path=$BINARY" >> "$GITHUB_OUTPUT"
DARWIN=$(find dist -type f -name '*darwin_unnotarized' | head -1)
WIN_AMD64=$(find dist -type f -name '*windows_amd64.exe' | head -1)
WIN_ARM64=$(find dist -type f -name '*windows_arm64.exe' | head -1)

for label in "darwin:${DARWIN}" "windows_amd64:${WIN_AMD64}" "windows_arm64:${WIN_ARM64}"; do
name="${label%%:*}"
path="${label#*:}"
if [ -z "$path" ] || [ ! -f "$path" ]; then
echo "::error::Binary not found for ${name}"
find dist -type f
exit 1
fi
done

echo "darwin=$DARWIN" >> "$GITHUB_OUTPUT"
echo "win_amd64=$WIN_AMD64" >> "$GITHUB_OUTPUT"
echo "win_arm64=$WIN_ARM64" >> "$GITHUB_OUTPUT"

- name: Sign artifacts with Sigstore
run: |
cosign sign-blob "${{ steps.binary.outputs.path }}" \
--bundle "${{ steps.binary.outputs.path }}.bundle" --yes
cosign sign-blob stepsecurity-dev-machine-guard.sh \
--bundle dist/stepsecurity-dev-machine-guard.sh.bundle --yes
for artifact in \
"${{ steps.binaries.outputs.darwin }}" \
"${{ steps.binaries.outputs.win_amd64 }}" \
"${{ steps.binaries.outputs.win_arm64 }}" \
stepsecurity-dev-machine-guard.sh; do
cosign sign-blob "$artifact" --bundle "${artifact}.bundle" --yes
done

- name: Upload cosign bundles
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ steps.version.outputs.tag }}" \
"${{ steps.binary.outputs.path }}.bundle" \
"${{ steps.binaries.outputs.darwin }}.bundle" \
"${{ steps.binaries.outputs.win_amd64 }}.bundle" \
"${{ steps.binaries.outputs.win_arm64 }}.bundle" \
dist/stepsecurity-dev-machine-guard.sh.bundle \
--clobber

- name: Attest build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: |
${{ steps.binary.outputs.path }}
${{ steps.binaries.outputs.darwin }}
${{ steps.binaries.outputs.win_amd64 }}
${{ steps.binaries.outputs.win_arm64 }}
stepsecurity-dev-machine-guard.sh
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
.vscode/
.idea/
.claude/
.plans/

# Output files
*.log
*.html
!docs/**/*.html
!images/**/*.html

# Go build artifacts
/stepsecurity-dev-machine-guard
# Go build artifacts — never commit compiled binaries
**/stepsecurity-dev-machine-guard
*.exe
dist/

# Temporary files
Expand Down
10 changes: 9 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ builds:
binary: stepsecurity-dev-machine-guard
goos:
- darwin
- windows
goarch:
- amd64
- arm64
Expand All @@ -29,11 +30,18 @@ universal_binaries:
name_template: "stepsecurity-dev-machine-guard-{{ .Version }}-darwin_unnotarized"

archives:
- ids:
- id: darwin
ids:
- universal
formats:
- binary
name_template: "stepsecurity-dev-machine-guard-{{ .Version }}-darwin_unnotarized"
- id: windows
ids:
- stepsecurity-dev-machine-guard
formats:
- binary
name_template: "stepsecurity-dev-machine-guard-{{ .Version }}-windows_{{ .Arch }}"

release:
draft: true
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ LDFLAGS := -s -w \
-X $(MODULE)/internal/buildinfo.ReleaseTag=$(TAG) \
-X $(MODULE)/internal/buildinfo.ReleaseBranch=$(BRANCH)

.PHONY: build test lint clean smoke
.PHONY: build build-windows deploy-windows test lint clean smoke

build:
go build -trimpath -ldflags "$(LDFLAGS)" -o $(BINARY) ./cmd/stepsecurity-dev-machine-guard

build-windows:
GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o $(BINARY).exe ./cmd/stepsecurity-dev-machine-guard

deploy-windows:
@bash scripts/deploy-windows.sh $(DEPLOY_ARGS)

test:
go test ./... -v -race -count=1

Expand Down
32 changes: 24 additions & 8 deletions cmd/stepsecurity-dev-machine-guard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"runtime"

"github.com/step-security/dev-machine-guard/internal/buildinfo"
"github.com/step-security/dev-machine-guard/internal/cli"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/step-security/dev-machine-guard/internal/launchd"
"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"
"github.com/step-security/dev-machine-guard/internal/telemetry"
)

Expand Down Expand Up @@ -84,14 +86,21 @@ func main() {
}

case "install":
fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n\n", buildinfo.Version)
_, _ = fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n\n", buildinfo.Version)
if !config.IsEnterpriseMode() {
log.Error("Enterprise configuration not found. Run '%s configure' or download the script from your StepSecurity dashboard.", os.Args[0])
os.Exit(1)
}
if err := launchd.Install(exec, log); err != nil {
log.Error("%v", err)
os.Exit(1)
if runtime.GOOS == "windows" {
if err := schtasks.Install(exec, log); err != nil {
log.Error("%v", err)
os.Exit(1)
}
} else {
if err := launchd.Install(exec, log); err != nil {
log.Error("%v", err)
os.Exit(1)
}
}
log.Progress("Sending initial telemetry...")
fmt.Println()
Expand All @@ -101,10 +110,17 @@ func main() {
}

case "uninstall":
fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n\n", buildinfo.Version)
if err := launchd.Uninstall(exec, log); err != nil {
log.Error("%v", err)
os.Exit(1)
_, _ = fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n\n", buildinfo.Version)
if runtime.GOOS == "windows" {
if err := schtasks.Uninstall(exec, log); err != nil {
log.Error("%v", err)
os.Exit(1)
}
} else {
if err := launchd.Uninstall(exec, log); err != nil {
log.Error("%v", err)
os.Exit(1)
}
}

default:
Expand Down
10 changes: 5 additions & 5 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,13 @@ func Parse(args []string) (*Config, error) {
case arg == "--verbose":
cfg.Verbose = true
case arg == "-v" || arg == "--version" || arg == "version":
fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n", buildinfo.VersionString())
_, _ = fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n", buildinfo.VersionString())
os.Exit(0)
case arg == "-h" || arg == "--help" || arg == "help":
printHelp()
os.Exit(0)
default:
return nil, fmt.Errorf("Unknown option: %s\nRun '%s --help' for usage information.", arg, filepath.Base(os.Args[0]))
return nil, fmt.Errorf("unknown option: %s, run '%s --help' for usage information", arg, filepath.Base(os.Args[0]))
}
i++
}
Expand All @@ -109,15 +109,15 @@ func Parse(args []string) (*Config, error) {

func printHelp() {
name := filepath.Base(os.Args[0])
fmt.Fprintf(os.Stdout, `StepSecurity Dev Machine Guard v%s
_, _ = fmt.Fprintf(os.Stdout, `StepSecurity Dev Machine Guard v%s

Usage: %s [COMMAND] [OPTIONS]

Commands:
configure Configure enterprise settings and search directories
configure show Show current configuration
install Install launchd for periodic scanning (enterprise)
uninstall Remove launchd configuration (enterprise)
install Install scheduled scanning (enterprise)
uninstall Remove scheduled scanning (enterprise)
send-telemetry Upload scan results to the StepSecurity dashboard (enterprise)

Output formats (community mode, mutually exclusive):
Expand Down
22 changes: 17 additions & 5 deletions internal/detector/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package detector

import (
"context"
"path/filepath"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -67,7 +68,7 @@ func (d *AgentDetector) Detect(ctx context.Context, searchDirs []string) []model
func (d *AgentDetector) findAgent(spec agentSpec, homeDir string) (string, bool) {
// Check detection paths
for _, relPath := range spec.DetectionPaths {
fullPath := homeDir + "/" + relPath
fullPath := filepath.Join(homeDir, relPath)
if d.exec.DirExists(fullPath) || d.exec.FileExists(fullPath) {
return fullPath, true
}
Expand Down Expand Up @@ -103,12 +104,23 @@ func (d *AgentDetector) getVersion(ctx context.Context, spec agentSpec) string {

// detectClaudeCowork checks for Claude Cowork (a mode within Claude Desktop 0.7+).
func (d *AgentDetector) detectClaudeCowork(ctx context.Context) (model.AITool, bool) {
claudePath := "/Applications/Claude.app"
if !d.exec.DirExists(claudePath) {
return model.AITool{}, false
var claudePath, version string

if d.exec.GOOS() == "windows" {
localAppData := d.exec.Getenv("LOCALAPPDATA")
claudePath = filepath.Join(localAppData, "Programs", "Claude")
if !d.exec.DirExists(claudePath) {
return model.AITool{}, false
}
version = readRegistryVersion(ctx, d.exec, "Claude")
} else {
claudePath = "/Applications/Claude.app"
if !d.exec.DirExists(claudePath) {
return model.AITool{}, false
}
version = readPlistVersion(ctx, d.exec, filepath.Join(claudePath, "Contents", "Info.plist"))
}

version := readPlistVersion(ctx, d.exec, claudePath+"/Contents/Info.plist")
if version == "unknown" {
return model.AITool{}, false
}
Expand Down
42 changes: 42 additions & 0 deletions internal/detector/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,45 @@ func TestIsCoworkVersion(t *testing.T) {
}
}
}

func TestAgentDetector_Windows_ClaudeCowork(t *testing.T) {
mock := executor.NewMock()
mock.SetGOOS("windows")
mock.SetHomeDir(`C:\Users\testuser`)
mock.SetEnv("LOCALAPPDATA", `C:\Users\testuser\AppData\Local`)

// detectClaudeCowork on Windows uses filepath.Join(localAppData, "Programs", "Claude").
// On macOS host, filepath.Join keeps backslashes and inserts "/":
claudePath := `C:\Users\testuser\AppData\Local` + "/Programs/Claude"
mock.SetDir(claudePath)

// Version via readRegistryVersion with appName "Claude".
// First registry root tried by readRegistryVersion.
mock.SetCommand(
"HKLM\\SOFTWARE\\...\\Claude\n DisplayVersion REG_SZ 0.7.5\n",
"", 0,
"reg", "query", `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, "/s", "/f", "Claude", "/d",
)

det := NewAgentDetector(mock)
results := det.Detect(context.Background(), []string{`C:\Users\testuser`})

found := false
for _, r := range results {
if r.Name == "claude-cowork" {
found = true
if r.Vendor != "Anthropic" {
t.Errorf("expected Anthropic, got %s", r.Vendor)
}
if r.Version != "0.7.5" {
t.Errorf("expected 0.7.5, got %s", r.Version)
}
if r.InstallPath != claudePath {
t.Errorf("expected install path %s, got %s", claudePath, r.InstallPath)
}
}
}
if !found {
t.Error("claude-cowork not found")
}
}
Loading
Loading