Skip to content

Commit c231c20

Browse files
authored
Merge pull request #17 from swarit-stepsecurity/swarit/feat/windows-support
feat: add windows support
2 parents 0646dc2 + 6259b62 commit c231c20

41 files changed

Lines changed: 1506 additions & 169 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,36 +68,53 @@ jobs:
6868
- name: Install cosign
6969
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
7070

71-
- name: Locate binary
72-
id: binary
71+
- name: Locate binaries
72+
id: binaries
7373
run: |
74-
BINARY=$(find dist -type f -name '*darwin_unnotarized' | head -1)
75-
if [ -z "$BINARY" ] || [ ! -f "$BINARY" ]; then
76-
echo "::error::Binary not found"
77-
find dist -type f
78-
exit 1
79-
fi
80-
echo "path=$BINARY" >> "$GITHUB_OUTPUT"
74+
DARWIN=$(find dist -type f -name '*darwin_unnotarized' | head -1)
75+
WIN_AMD64=$(find dist -type f -name '*windows_amd64.exe' | head -1)
76+
WIN_ARM64=$(find dist -type f -name '*windows_arm64.exe' | head -1)
77+
78+
for label in "darwin:${DARWIN}" "windows_amd64:${WIN_AMD64}" "windows_arm64:${WIN_ARM64}"; do
79+
name="${label%%:*}"
80+
path="${label#*:}"
81+
if [ -z "$path" ] || [ ! -f "$path" ]; then
82+
echo "::error::Binary not found for ${name}"
83+
find dist -type f
84+
exit 1
85+
fi
86+
done
87+
88+
echo "darwin=$DARWIN" >> "$GITHUB_OUTPUT"
89+
echo "win_amd64=$WIN_AMD64" >> "$GITHUB_OUTPUT"
90+
echo "win_arm64=$WIN_ARM64" >> "$GITHUB_OUTPUT"
8191
8292
- name: Sign artifacts with Sigstore
8393
run: |
84-
cosign sign-blob "${{ steps.binary.outputs.path }}" \
85-
--bundle "${{ steps.binary.outputs.path }}.bundle" --yes
86-
cosign sign-blob stepsecurity-dev-machine-guard.sh \
87-
--bundle dist/stepsecurity-dev-machine-guard.sh.bundle --yes
94+
for artifact in \
95+
"${{ steps.binaries.outputs.darwin }}" \
96+
"${{ steps.binaries.outputs.win_amd64 }}" \
97+
"${{ steps.binaries.outputs.win_arm64 }}" \
98+
stepsecurity-dev-machine-guard.sh; do
99+
cosign sign-blob "$artifact" --bundle "${artifact}.bundle" --yes
100+
done
88101
89102
- name: Upload cosign bundles
90103
env:
91104
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
92105
run: |
93106
gh release upload "${{ steps.version.outputs.tag }}" \
94-
"${{ steps.binary.outputs.path }}.bundle" \
107+
"${{ steps.binaries.outputs.darwin }}.bundle" \
108+
"${{ steps.binaries.outputs.win_amd64 }}.bundle" \
109+
"${{ steps.binaries.outputs.win_arm64 }}.bundle" \
95110
dist/stepsecurity-dev-machine-guard.sh.bundle \
96111
--clobber
97112
98113
- name: Attest build provenance
99114
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
100115
with:
101116
subject-path: |
102-
${{ steps.binary.outputs.path }}
117+
${{ steps.binaries.outputs.darwin }}
118+
${{ steps.binaries.outputs.win_amd64 }}
119+
${{ steps.binaries.outputs.win_arm64 }}
103120
stepsecurity-dev-machine-guard.sh

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@
1010
.vscode/
1111
.idea/
1212
.claude/
13+
.plans/
1314

1415
# Output files
1516
*.log
1617
*.html
1718
!docs/**/*.html
1819
!images/**/*.html
1920

20-
# Go build artifacts
21-
/stepsecurity-dev-machine-guard
21+
# Go build artifacts — never commit compiled binaries
22+
**/stepsecurity-dev-machine-guard
23+
*.exe
2224
dist/
2325

2426
# Temporary files

.goreleaser.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ builds:
77
binary: stepsecurity-dev-machine-guard
88
goos:
99
- darwin
10+
- windows
1011
goarch:
1112
- amd64
1213
- arm64
@@ -29,11 +30,18 @@ universal_binaries:
2930
name_template: "stepsecurity-dev-machine-guard-{{ .Version }}-darwin_unnotarized"
3031

3132
archives:
32-
- ids:
33+
- id: darwin
34+
ids:
3335
- universal
3436
formats:
3537
- binary
3638
name_template: "stepsecurity-dev-machine-guard-{{ .Version }}-darwin_unnotarized"
39+
- id: windows
40+
ids:
41+
- stepsecurity-dev-machine-guard
42+
formats:
43+
- binary
44+
name_template: "stepsecurity-dev-machine-guard-{{ .Version }}-windows_{{ .Arch }}"
3745

3846
release:
3947
draft: true

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ LDFLAGS := -s -w \
99
-X $(MODULE)/internal/buildinfo.ReleaseTag=$(TAG) \
1010
-X $(MODULE)/internal/buildinfo.ReleaseBranch=$(BRANCH)
1111

12-
.PHONY: build test lint clean smoke
12+
.PHONY: build build-windows deploy-windows test lint clean smoke
1313

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

17+
build-windows:
18+
GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o $(BINARY).exe ./cmd/stepsecurity-dev-machine-guard
19+
20+
deploy-windows:
21+
@bash scripts/deploy-windows.sh $(DEPLOY_ARGS)
22+
1723
test:
1824
go test ./... -v -race -count=1
1925

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"os"
6+
"runtime"
67

78
"github.com/step-security/dev-machine-guard/internal/buildinfo"
89
"github.com/step-security/dev-machine-guard/internal/cli"
@@ -11,6 +12,7 @@ import (
1112
"github.com/step-security/dev-machine-guard/internal/launchd"
1213
"github.com/step-security/dev-machine-guard/internal/progress"
1314
"github.com/step-security/dev-machine-guard/internal/scan"
15+
"github.com/step-security/dev-machine-guard/internal/schtasks"
1416
"github.com/step-security/dev-machine-guard/internal/telemetry"
1517
)
1618

@@ -84,14 +86,21 @@ func main() {
8486
}
8587

8688
case "install":
87-
fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n\n", buildinfo.Version)
89+
_, _ = fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n\n", buildinfo.Version)
8890
if !config.IsEnterpriseMode() {
8991
log.Error("Enterprise configuration not found. Run '%s configure' or download the script from your StepSecurity dashboard.", os.Args[0])
9092
os.Exit(1)
9193
}
92-
if err := launchd.Install(exec, log); err != nil {
93-
log.Error("%v", err)
94-
os.Exit(1)
94+
if runtime.GOOS == "windows" {
95+
if err := schtasks.Install(exec, log); err != nil {
96+
log.Error("%v", err)
97+
os.Exit(1)
98+
}
99+
} else {
100+
if err := launchd.Install(exec, log); err != nil {
101+
log.Error("%v", err)
102+
os.Exit(1)
103+
}
95104
}
96105
log.Progress("Sending initial telemetry...")
97106
fmt.Println()
@@ -101,10 +110,17 @@ func main() {
101110
}
102111

103112
case "uninstall":
104-
fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n\n", buildinfo.Version)
105-
if err := launchd.Uninstall(exec, log); err != nil {
106-
log.Error("%v", err)
107-
os.Exit(1)
113+
_, _ = fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n\n", buildinfo.Version)
114+
if runtime.GOOS == "windows" {
115+
if err := schtasks.Uninstall(exec, log); err != nil {
116+
log.Error("%v", err)
117+
os.Exit(1)
118+
}
119+
} else {
120+
if err := launchd.Uninstall(exec, log); err != nil {
121+
log.Error("%v", err)
122+
os.Exit(1)
123+
}
108124
}
109125

110126
default:

internal/cli/cli.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ func Parse(args []string) (*Config, error) {
9393
case arg == "--verbose":
9494
cfg.Verbose = true
9595
case arg == "-v" || arg == "--version" || arg == "version":
96-
fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n", buildinfo.VersionString())
96+
_, _ = fmt.Fprintf(os.Stdout, "StepSecurity Dev Machine Guard v%s\n", buildinfo.VersionString())
9797
os.Exit(0)
9898
case arg == "-h" || arg == "--help" || arg == "help":
9999
printHelp()
100100
os.Exit(0)
101101
default:
102-
return nil, fmt.Errorf("Unknown option: %s\nRun '%s --help' for usage information.", arg, filepath.Base(os.Args[0]))
102+
return nil, fmt.Errorf("unknown option: %s, run '%s --help' for usage information", arg, filepath.Base(os.Args[0]))
103103
}
104104
i++
105105
}
@@ -109,15 +109,15 @@ func Parse(args []string) (*Config, error) {
109109

110110
func printHelp() {
111111
name := filepath.Base(os.Args[0])
112-
fmt.Fprintf(os.Stdout, `StepSecurity Dev Machine Guard v%s
112+
_, _ = fmt.Fprintf(os.Stdout, `StepSecurity Dev Machine Guard v%s
113113
114114
Usage: %s [COMMAND] [OPTIONS]
115115
116116
Commands:
117117
configure Configure enterprise settings and search directories
118118
configure show Show current configuration
119-
install Install launchd for periodic scanning (enterprise)
120-
uninstall Remove launchd configuration (enterprise)
119+
install Install scheduled scanning (enterprise)
120+
uninstall Remove scheduled scanning (enterprise)
121121
send-telemetry Upload scan results to the StepSecurity dashboard (enterprise)
122122
123123
Output formats (community mode, mutually exclusive):

internal/detector/agent.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package detector
22

33
import (
44
"context"
5+
"path/filepath"
56
"regexp"
67
"strconv"
78
"strings"
@@ -67,7 +68,7 @@ func (d *AgentDetector) Detect(ctx context.Context, searchDirs []string) []model
6768
func (d *AgentDetector) findAgent(spec agentSpec, homeDir string) (string, bool) {
6869
// Check detection paths
6970
for _, relPath := range spec.DetectionPaths {
70-
fullPath := homeDir + "/" + relPath
71+
fullPath := filepath.Join(homeDir, relPath)
7172
if d.exec.DirExists(fullPath) || d.exec.FileExists(fullPath) {
7273
return fullPath, true
7374
}
@@ -103,12 +104,23 @@ func (d *AgentDetector) getVersion(ctx context.Context, spec agentSpec) string {
103104

104105
// detectClaudeCowork checks for Claude Cowork (a mode within Claude Desktop 0.7+).
105106
func (d *AgentDetector) detectClaudeCowork(ctx context.Context) (model.AITool, bool) {
106-
claudePath := "/Applications/Claude.app"
107-
if !d.exec.DirExists(claudePath) {
108-
return model.AITool{}, false
107+
var claudePath, version string
108+
109+
if d.exec.GOOS() == "windows" {
110+
localAppData := d.exec.Getenv("LOCALAPPDATA")
111+
claudePath = filepath.Join(localAppData, "Programs", "Claude")
112+
if !d.exec.DirExists(claudePath) {
113+
return model.AITool{}, false
114+
}
115+
version = readRegistryVersion(ctx, d.exec, "Claude")
116+
} else {
117+
claudePath = "/Applications/Claude.app"
118+
if !d.exec.DirExists(claudePath) {
119+
return model.AITool{}, false
120+
}
121+
version = readPlistVersion(ctx, d.exec, filepath.Join(claudePath, "Contents", "Info.plist"))
109122
}
110123

111-
version := readPlistVersion(ctx, d.exec, claudePath+"/Contents/Info.plist")
112124
if version == "unknown" {
113125
return model.AITool{}, false
114126
}

internal/detector/agent_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,45 @@ func TestIsCoworkVersion(t *testing.T) {
9696
}
9797
}
9898
}
99+
100+
func TestAgentDetector_Windows_ClaudeCowork(t *testing.T) {
101+
mock := executor.NewMock()
102+
mock.SetGOOS("windows")
103+
mock.SetHomeDir(`C:\Users\testuser`)
104+
mock.SetEnv("LOCALAPPDATA", `C:\Users\testuser\AppData\Local`)
105+
106+
// detectClaudeCowork on Windows uses filepath.Join(localAppData, "Programs", "Claude").
107+
// On macOS host, filepath.Join keeps backslashes and inserts "/":
108+
claudePath := `C:\Users\testuser\AppData\Local` + "/Programs/Claude"
109+
mock.SetDir(claudePath)
110+
111+
// Version via readRegistryVersion with appName "Claude".
112+
// First registry root tried by readRegistryVersion.
113+
mock.SetCommand(
114+
"HKLM\\SOFTWARE\\...\\Claude\n DisplayVersion REG_SZ 0.7.5\n",
115+
"", 0,
116+
"reg", "query", `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, "/s", "/f", "Claude", "/d",
117+
)
118+
119+
det := NewAgentDetector(mock)
120+
results := det.Detect(context.Background(), []string{`C:\Users\testuser`})
121+
122+
found := false
123+
for _, r := range results {
124+
if r.Name == "claude-cowork" {
125+
found = true
126+
if r.Vendor != "Anthropic" {
127+
t.Errorf("expected Anthropic, got %s", r.Vendor)
128+
}
129+
if r.Version != "0.7.5" {
130+
t.Errorf("expected 0.7.5, got %s", r.Version)
131+
}
132+
if r.InstallPath != claudePath {
133+
t.Errorf("expected install path %s, got %s", claudePath, r.InstallPath)
134+
}
135+
}
136+
}
137+
if !found {
138+
t.Error("claude-cowork not found")
139+
}
140+
}

0 commit comments

Comments
 (0)