From 51fcbb00a9df6b5ae6b64d8911faf6183843217e Mon Sep 17 00:00:00 2001
From: Shubham Malik
Date: Fri, 3 Apr 2026 10:27:22 +0530
Subject: [PATCH 1/3] feat(mdm): migrate script to go module
---
.github/workflows/go.yml | 44 +++
.github/workflows/release.yml | 120 ++++--
.gitignore | 4 +
.goreleaser.yml | 34 ++
Makefile | 27 ++
README.md | 330 +++++++++++++----
cmd/stepsecurity-dev-machine-guard/main.go | 129 +++++++
go.mod | 3 +
internal/buildinfo/version.go | 27 ++
internal/cli/cli.go | 158 ++++++++
internal/cli/cli_test.go | 183 ++++++++++
internal/config/config.go | 345 ++++++++++++++++++
internal/config/config_test.go | 119 ++++++
internal/detector/agent.go | 146 ++++++++
internal/detector/agent_test.go | 98 +++++
internal/detector/aicli.go | 181 +++++++++
internal/detector/aicli_test.go | 72 ++++
internal/detector/extension.go | 144 ++++++++
internal/detector/extension_test.go | 82 +++++
internal/detector/framework.go | 116 ++++++
internal/detector/framework_test.go | 76 ++++
internal/detector/ide.go | 97 +++++
internal/detector/ide_test.go | 79 ++++
internal/detector/mcp.go | 170 +++++++++
internal/detector/mcp_test.go | 83 +++++
internal/detector/nodepm.go | 60 +++
internal/detector/nodepm_test.go | 106 ++++++
internal/detector/nodeproject.go | 84 +++++
internal/detector/nodescan.go | 327 +++++++++++++++++
internal/device/device.go | 88 +++++
internal/device/device_test.go | 59 +++
internal/executor/executor.go | 150 ++++++++
internal/executor/mock.go | 288 +++++++++++++++
internal/launchd/launchd.go | 189 ++++++++++
internal/lock/lock.go | 49 +++
internal/model/model.go | 98 +++++
internal/output/html.go | 226 ++++++++++++
internal/output/html_test.go | 86 +++++
internal/output/json.go | 16 +
internal/output/json_test.go | 180 +++++++++
internal/output/pretty.go | 211 +++++++++++
internal/output/pretty_test.go | 119 ++++++
internal/progress/progress.go | 133 +++++++
internal/scan/scanner.go | 166 +++++++++
internal/telemetry/logcapture.go | 94 +++++
internal/telemetry/telemetry.go | 404 +++++++++++++++++++++
tests/test_smoke_go.sh | 358 ++++++++++++++++++
47 files changed, 6257 insertions(+), 101 deletions(-)
create mode 100644 .github/workflows/go.yml
create mode 100644 .goreleaser.yml
create mode 100644 Makefile
create mode 100644 cmd/stepsecurity-dev-machine-guard/main.go
create mode 100644 go.mod
create mode 100644 internal/buildinfo/version.go
create mode 100644 internal/cli/cli.go
create mode 100644 internal/cli/cli_test.go
create mode 100644 internal/config/config.go
create mode 100644 internal/config/config_test.go
create mode 100644 internal/detector/agent.go
create mode 100644 internal/detector/agent_test.go
create mode 100644 internal/detector/aicli.go
create mode 100644 internal/detector/aicli_test.go
create mode 100644 internal/detector/extension.go
create mode 100644 internal/detector/extension_test.go
create mode 100644 internal/detector/framework.go
create mode 100644 internal/detector/framework_test.go
create mode 100644 internal/detector/ide.go
create mode 100644 internal/detector/ide_test.go
create mode 100644 internal/detector/mcp.go
create mode 100644 internal/detector/mcp_test.go
create mode 100644 internal/detector/nodepm.go
create mode 100644 internal/detector/nodepm_test.go
create mode 100644 internal/detector/nodeproject.go
create mode 100644 internal/detector/nodescan.go
create mode 100644 internal/device/device.go
create mode 100644 internal/device/device_test.go
create mode 100644 internal/executor/executor.go
create mode 100644 internal/executor/mock.go
create mode 100644 internal/launchd/launchd.go
create mode 100644 internal/lock/lock.go
create mode 100644 internal/model/model.go
create mode 100644 internal/output/html.go
create mode 100644 internal/output/html_test.go
create mode 100644 internal/output/json.go
create mode 100644 internal/output/json_test.go
create mode 100644 internal/output/pretty.go
create mode 100644 internal/output/pretty_test.go
create mode 100644 internal/progress/progress.go
create mode 100644 internal/scan/scanner.go
create mode 100644 internal/telemetry/logcapture.go
create mode 100644 internal/telemetry/telemetry.go
create mode 100755 tests/test_smoke_go.sh
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..c625e1c
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,44 @@
+name: Go
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ - uses: golangci/golangci-lint-action@v6
+ with:
+ version: latest
+
+ test:
+ name: Test
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ - run: make test
+
+ smoke:
+ name: Smoke Tests
+ runs-on: macos-latest
+ needs: test
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ - run: make smoke
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9d2a3d3..5c7e0e5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,11 +9,10 @@ jobs:
release:
name: Build, Sign & Release
runs-on: ubuntu-latest
- environment: release
permissions:
- contents: write # create tag, release, and upload assets
- id-token: write # Sigstore OIDC keyless signing
- attestations: write # SLSA build provenance
+ contents: write # create tag, release, and upload assets
+ id-token: write # OIDC token for cosign keyless signing and build provenance
+ attestations: write # SLSA build provenance
steps:
- name: Harden the runner (Audit all outbound calls)
@@ -23,13 +22,15 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
- - name: Extract version from script
+ - name: Extract version from source
id: version
run: |
- version=$(grep -m1 '^AGENT_VERSION=' stepsecurity-dev-machine-guard.sh | sed 's/AGENT_VERSION="//;s/"//')
+ version=$(grep -m1 'Version.*=' internal/buildinfo/version.go | sed 's/.*"\(.*\)".*/\1/')
if [ -z "$version" ]; then
- echo "::error::Could not extract AGENT_VERSION from script"
+ echo "::error::Could not extract Version from internal/buildinfo/version.go"
exit 1
fi
tag="v${version}"
@@ -40,52 +41,97 @@ jobs:
- name: Check tag does not already exist
run: |
if git rev-parse "refs/tags/${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
- echo "::error::Tag ${{ steps.version.outputs.tag }} already exists. Bump AGENT_VERSION in the script before releasing."
+ echo "::error::Tag ${{ steps.version.outputs.tag }} already exists. Bump Version in internal/buildinfo/version.go before releasing."
exit 1
fi
+ - name: Create tag
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
+ git push origin "${{ steps.version.outputs.tag }}"
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release --clean
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- - name: Sign script with Sigstore (keyless)
+ - name: Locate built binaries
+ id: binaries
run: |
- cosign sign-blob stepsecurity-dev-machine-guard.sh \
- --bundle stepsecurity-dev-machine-guard.sh.bundle \
- --yes
+ # GoReleaser keeps binaries in build subdirs (e.g. _amd64_v1, _arm64_v8.0)
+ AMD64=$(find dist -type f -name 'stepsecurity-dev-machine-guard' -path '*darwin_amd64*' | head -1)
+ ARM64=$(find dist -type f -name 'stepsecurity-dev-machine-guard' -path '*darwin_arm64*' | head -1)
- - name: Verify signature
+ for label in "amd64:${AMD64}" "arm64:${ARM64}"; do
+ name="${label%%:*}"
+ path="${label#*:}"
+ if [ -z "$path" ] || [ ! -f "$path" ]; then
+ echo "::error::Binary not found for ${name}"
+ echo "dist/ contents:"
+ find dist -type f
+ exit 1
+ fi
+ done
+
+ echo "amd64=${AMD64}" >> "$GITHUB_OUTPUT"
+ echo "arm64=${ARM64}" >> "$GITHUB_OUTPUT"
+ echo "Found amd64: ${AMD64}"
+ echo "Found arm64: ${ARM64}"
+
+ - name: Sign artifacts with Sigstore (keyless)
run: |
- cosign verify-blob stepsecurity-dev-machine-guard.sh \
- --bundle stepsecurity-dev-machine-guard.sh.bundle \
- --certificate-identity-regexp "github.com/step-security/dev-machine-guard" \
- --certificate-oidc-issuer "https://token.actions.githubusercontent.com"
+ cosign sign-blob "${{ steps.binaries.outputs.amd64 }}" \
+ --bundle dist/stepsecurity-dev-machine-guard_darwin_amd64.bundle --yes
+ cosign sign-blob "${{ steps.binaries.outputs.arm64 }}" \
+ --bundle dist/stepsecurity-dev-machine-guard_darwin_arm64.bundle --yes
+ cosign sign-blob stepsecurity-dev-machine-guard.sh \
+ --bundle dist/stepsecurity-dev-machine-guard.sh.bundle --yes
- name: Generate checksums
run: |
- sha256sum stepsecurity-dev-machine-guard.sh > checksums.txt
- sha256sum stepsecurity-dev-machine-guard.sh.bundle >> checksums.txt
- echo "Checksums:"
- cat checksums.txt
+ SUMS="dist/stepsecurity-dev-machine-guard_${{ steps.version.outputs.version }}_SHA256SUMS"
+ sha256sum "${{ steps.binaries.outputs.amd64 }}" >> "$SUMS"
+ sha256sum "${{ steps.binaries.outputs.arm64 }}" >> "$SUMS"
+ sha256sum stepsecurity-dev-machine-guard.sh >> "$SUMS"
- - name: Create tag
+ - name: Upload signature bundles and checksums to release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
- git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
- git push origin "${{ steps.version.outputs.tag }}"
+ gh release upload "${{ steps.version.outputs.tag }}" \
+ dist/stepsecurity-dev-machine-guard_darwin_amd64.bundle \
+ dist/stepsecurity-dev-machine-guard_darwin_arm64.bundle \
+ dist/stepsecurity-dev-machine-guard.sh.bundle \
+ dist/stepsecurity-dev-machine-guard_${{ steps.version.outputs.version }}_SHA256SUMS \
+ --clobber
- - name: Create GitHub Release
- uses: step-security/action-gh-release@d45511d7589f080cf54961ff056b9705a74fd160 # v2.5.0
- with:
- tag_name: ${{ steps.version.outputs.tag }}
- name: ${{ steps.version.outputs.tag }}
- generate_release_notes: true
- files: |
- stepsecurity-dev-machine-guard.sh
- stepsecurity-dev-machine-guard.sh.bundle
- checksums.txt
+ - name: Mark release as immutable (not a draft, not a prerelease)
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh release edit "${{ steps.version.outputs.tag }}" \
+ --draft=false \
+ --prerelease=false \
+ --latest
- name: Attest build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
- subject-path: stepsecurity-dev-machine-guard.sh
+ subject-path: |
+ ${{ steps.binaries.outputs.amd64 }}
+ ${{ steps.binaries.outputs.arm64 }}
+ stepsecurity-dev-machine-guard.sh
diff --git a/.gitignore b/.gitignore
index 5a7cd7e..e50978c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,5 +17,9 @@
!docs/**/*.html
!images/**/*.html
+# Go build artifacts
+/stepsecurity-dev-machine-guard
+dist/
+
# Temporary files
todo-remove/
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..74747e1
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,34 @@
+version: 2
+project_name: stepsecurity-dev-machine-guard
+
+builds:
+ - id: stepsecurity-dev-machine-guard
+ main: ./cmd/stepsecurity-dev-machine-guard
+ binary: stepsecurity-dev-machine-guard
+ goos:
+ - darwin
+ goarch:
+ - amd64
+ - arm64
+ mod_timestamp: "{{ .CommitTimestamp }}"
+ flags:
+ - -trimpath
+ ldflags:
+ - -s -w
+ - -X github.com/step-security/dev-machine-guard/internal/buildinfo.GitCommit={{.FullCommit}}
+ - -X github.com/step-security/dev-machine-guard/internal/buildinfo.ReleaseTag={{.Tag}}
+ - -X github.com/step-security/dev-machine-guard/internal/buildinfo.ReleaseBranch={{.Branch}}
+ env:
+ - CGO_ENABLED=0
+
+archives:
+ - format: binary
+ name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
+
+checksum:
+ name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS"
+ algorithm: sha256
+
+release:
+ extra_files:
+ - glob: stepsecurity-dev-machine-guard.sh
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3f99b0a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,27 @@
+BINARY := stepsecurity-dev-machine-guard
+MODULE := github.com/step-security/dev-machine-guard
+VERSION := $(shell grep -m1 'Version' internal/buildinfo/version.go | sed 's/.*"//;s/".*//')
+COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
+TAG := $(shell git describe --tags --exact-match 2>/dev/null || echo "dev")
+LDFLAGS := -s -w \
+ -X $(MODULE)/internal/buildinfo.GitCommit=$(COMMIT) \
+ -X $(MODULE)/internal/buildinfo.ReleaseTag=$(TAG) \
+ -X $(MODULE)/internal/buildinfo.ReleaseBranch=$(BRANCH)
+
+.PHONY: build test lint clean smoke
+
+build:
+ go build -trimpath -ldflags "$(LDFLAGS)" -o $(BINARY) ./cmd/stepsecurity-dev-machine-guard
+
+test:
+ go test ./... -v -race -count=1
+
+lint:
+ golangci-lint run ./...
+
+clean:
+ rm -f $(BINARY)
+
+smoke: build
+ bash tests/test_smoke_go.sh
diff --git a/README.md b/README.md
index be4e1ce..e8755bb 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,10 @@
@@ -26,17 +27,16 @@ Developer machines are the new attack surface. They hold high-value assets — G
-
**EDR and traditional MDM solutions** monitor device posture and compliance, but they have **zero visibility** into the developer tooling layer:
-| Capability | EDR / MDM | Dev Machine Guard |
-|-----------------------------------|:---------:|:-----------------:|
-| IDE extension audit | | Yes |
-| AI agent & tool inventory | | Yes |
-| MCP server config audit | | Yes |
-| Node.js package scanning | | Yes |
-| Device posture & compliance | Yes | |
-| Malware / virus detection | Yes | |
+| Capability | EDR / MDM | Dev Machine Guard |
+| --------------------------- | :-------: | :---------------: |
+| IDE extension audit | | Yes |
+| AI agent & tool inventory | | Yes |
+| MCP server config audit | | Yes |
+| Node.js package scanning | | Yes |
+| Device posture & compliance | Yes | |
+| Malware / virus detection | Yes | |
**Dev Machine Guard is complementary to EDR/MDM — not a replacement.** Deploy it alongside your existing tools via MDM (Jamf, Kandji, Intune) or run it standalone.
@@ -46,46 +46,205 @@ Developer machines are the new attack surface. They hold high-value assets — G
## Quick Start
+### Install from release (recommended)
+
+Download the latest binary for your platform from [GitHub Releases](https://github.com/step-security/dev-machine-guard/releases):
+
+```bash
+# Apple Silicon (M1/M2/M3/M4)
+curl -sSL https://github.com/step-security/dev-machine-guard/releases/latest/download/stepsecurity-dev-machine-guard_darwin_arm64 -o stepsecurity-dev-machine-guard
+chmod +x stepsecurity-dev-machine-guard
+
+# Intel Mac
+curl -sSL https://github.com/step-security/dev-machine-guard/releases/latest/download/stepsecurity-dev-machine-guard_darwin_amd64 -o stepsecurity-dev-machine-guard
+chmod +x stepsecurity-dev-machine-guard
+
+# Run the scan
+./stepsecurity-dev-machine-guard
+```
+
+### Build from source
+
```bash
-# Clone the repository
git clone https://github.com/step-security/dev-machine-guard.git
cd dev-machine-guard
+make build
+./stepsecurity-dev-machine-guard
+```
-# Make the script executable
-chmod +x stepsecurity-dev-machine-guard.sh
+Requires Go 1.22+. The binary has zero external dependencies.
-# Run the scan
-./stepsecurity-dev-machine-guard.sh
+## Usage
+
+```
+stepsecurity-dev-machine-guard [COMMAND] [OPTIONS]
+```
+
+### Commands
+
+| Command | Description |
+| ---------------- | --------------------------------------------------------------- |
+| _(none)_ | Run a scan (community mode, pretty output) |
+| `configure` | Interactively set all settings (enterprise, scan, output) |
+| `configure show` | Show current configuration (API key masked) |
+| `install` | Install launchd for periodic scanning (enterprise) |
+| `uninstall` | Remove launchd configuration (enterprise) |
+| `send-telemetry` | Upload scan results to the StepSecurity dashboard (enterprise) |
+
+### Output Formats
+
+| Flag | Description |
+| ------------- | ---------------------------------------- |
+| `--pretty` | Pretty terminal output (default) |
+| `--json` | JSON output to stdout |
+| `--html FILE` | Self-contained HTML report saved to FILE |
+
+### Options
+
+| Flag | Description |
+| ---------------------------- | ------------------------------------------------------------- |
+| `--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 help |
+
+### Examples
+
+```bash
+# Pretty terminal output (default)
+./stepsecurity-dev-machine-guard
+
+# JSON output
+./stepsecurity-dev-machine-guard --json
+./stepsecurity-dev-machine-guard --json | python3 -m json.tool # formatted
+./stepsecurity-dev-machine-guard --json > scan.json # to file
+
+# HTML report
+./stepsecurity-dev-machine-guard --html report.html
+
+# Verbose scan with npm packages — shows progress spinners and timing
+./stepsecurity-dev-machine-guard --verbose --enable-npm-scan
+
+# Scan specific directories instead of $HOME
+./stepsecurity-dev-machine-guard --search-dirs /Volumes/code
+./stepsecurity-dev-machine-guard --search-dirs /tmp /opt # multiple dirs
+
+# Pipe JSON through jq to extract just AI tools
+./stepsecurity-dev-machine-guard --json | jq '.ai_agents_and_tools'
+
+# Count IDE extensions
+./stepsecurity-dev-machine-guard --json | jq '.summary.ide_extensions_count'
+
+# Check for MCP configs (exit 1 if any found — useful in CI)
+count=$(./stepsecurity-dev-machine-guard --json | jq '.summary.mcp_configs_count')
+[ "$count" -gt 0 ] && echo "MCP servers detected!" && exit 1
+
+# Disable colors for piping or logging
+./stepsecurity-dev-machine-guard --color=never 2>&1 | tee scan.log
+
+# Enterprise: configure all settings interactively
+./stepsecurity-dev-machine-guard configure
+
+# Enterprise: view saved configuration (API key masked)
+./stepsecurity-dev-machine-guard configure show
+
+# Enterprise: install scheduled scanning via launchd
+./stepsecurity-dev-machine-guard install
+
+# Enterprise: one-time telemetry upload
+./stepsecurity-dev-machine-guard send-telemetry
+
+# Enterprise: remove scheduled scanning
+./stepsecurity-dev-machine-guard uninstall
+```
+
+## Configuration
+
+Run `configure` to set up enterprise credentials and default search directories:
+
+```bash
+./stepsecurity-dev-machine-guard configure
+```
+
+This interactively prompts for all configurable settings:
+
+| Setting | Description | Default |
+| ------------------ | ------------------------------------------- | --------------- |
+| Customer ID | Your StepSecurity customer identifier | _(not set)_ |
+| API Endpoint | StepSecurity backend URL | _(not set)_ |
+| API Key | Authentication key for telemetry uploads | _(not set)_ |
+| Scan Frequency | How often launchd runs scans (hours) | _(not set)_ |
+| Search Directories | Comma-separated list of directories to scan | `$HOME` |
+| Enable NPM Scan | Node.js package scanning | `auto` |
+| Color Mode | Terminal color output | `auto` |
+| Output Format | Default output format | `pretty` |
+| HTML Output File | Default path for HTML reports | _(not set)_ |
+| Quiet Mode | Suppress progress messages | `false` |
+
+View current settings:
+
+```bash
+./stepsecurity-dev-machine-guard configure show
+```
+
+```
+Configuration (~/.stepsecurity/config.json):
+
+ Customer ID: my-company
+ API Endpoint: https://api.stepsecurity.io
+ API Key: ***a1b2
+ Scan Frequency: 4 hours
+ Search Directories: $HOME, /Volumes/code
+ Enable NPM Scan: auto
+ Color Mode: auto
+ Output Format: pretty
+ Quiet Mode: false
```
-Or run directly without cloning:
+Configuration is saved to `~/.stepsecurity/config.json` with `0600` permissions (owner read/write only).
+
+**CLI flags always override config file values** — this matches the shell script behavior. For example, if your config has `output_format: json`, running `./stepsecurity-dev-machine-guard --pretty` uses pretty output. To clear a value during configuration, enter a single dash (`-`).
+
+### Verbose and Quiet Mode
+
+By default in community mode, progress messages (spinners, step details) are **suppressed** — you only see the final output. This keeps stdout clean for piping.
```bash
-curl -sSL https://raw.githubusercontent.com/step-security/dev-machine-guard/main/stepsecurity-dev-machine-guard.sh -o stepsecurity-dev-machine-guard.sh
-chmod +x stepsecurity-dev-machine-guard.sh
-./stepsecurity-dev-machine-guard.sh
+# Default: quiet — clean output, no progress spinners
+./stepsecurity-dev-machine-guard --json > scan.json
+
+# Verbose: show progress spinners and step timing
+./stepsecurity-dev-machine-guard --verbose
+
+# Save quiet=true in config so it persists across runs
+./stepsecurity-dev-machine-guard configure
```
+In enterprise mode (`send-telemetry`, `install`), progress is **always shown** regardless of the quiet setting — the output is captured as execution logs and sent to the backend for debugging.
+
## What It Detects
See [SCAN_COVERAGE.md](SCAN_COVERAGE.md) for the full catalog of supported detections.
-| Category | Examples |
-|-----------------------|-------------------------------------------------------|
-| IDEs & Desktop Apps | VS Code, Cursor, Windsurf, Zed, Claude, Copilot |
-| AI CLI Tools | Claude Code, Codex, Gemini CLI, Kiro, Aider |
-| AI Agents | Claude Cowork, OpenClaw, GPT-Engineer |
-| AI Frameworks | Ollama, LM Studio, LocalAI |
-| MCP Server Configs | Claude Desktop, Cursor, Windsurf, Zed, Codex |
-| IDE Extensions | VS Code, Cursor |
-| Node.js Packages | npm, yarn, pnpm, bun (opt-in) |
+| Category | Examples |
+| ------------------- | ---------------------------------------------------------------------------------------- |
+| IDEs & Desktop Apps | VS Code, Cursor, Windsurf, Antigravity, Zed, Claude, Copilot |
+| AI CLI Tools | Claude Code, Codex, Gemini CLI, Kiro, GitHub Copilot CLI, Aider, OpenCode |
+| AI Agents | Claude Cowork, OpenClaw, ClawdBot, GPT-Engineer |
+| AI Frameworks | Ollama, LM Studio, LocalAI, Text Generation WebUI |
+| MCP Server Configs | Claude Desktop, Claude Code, Cursor, Windsurf, Antigravity, Zed, Open Interpreter, Codex |
+| IDE Extensions | VS Code, Cursor (name, publisher, version, install date) |
+| Node.js Packages | npm, yarn, pnpm, bun (opt-in) |
## Output Formats
### Pretty Terminal Output (default)
```bash
-./stepsecurity-dev-machine-guard.sh
+./stepsecurity-dev-machine-guard
```
@@ -95,62 +254,73 @@ See [SCAN_COVERAGE.md](SCAN_COVERAGE.md) for the full catalog of supported detec
### JSON Output
```bash
-./stepsecurity-dev-machine-guard.sh --json
-./stepsecurity-dev-machine-guard.sh --json | python3 -m json.tool # formatted
-./stepsecurity-dev-machine-guard.sh --json > scan.json # to file
+./stepsecurity-dev-machine-guard --json
```
+See [examples/sample-output.json](examples/sample-output.json) for the full schema, or [Reading Scan Results](docs/reading-scan-results.md) for the schema reference.
+
### HTML Report
```bash
-./stepsecurity-dev-machine-guard.sh --html report.html
+./stepsecurity-dev-machine-guard --html report.html
```
Date: Fri, 3 Apr 2026 11:05:30 +0530
Subject: [PATCH 2/3] feat(mdm): bump version to v1.9.0
---
README.md | 2 +-
examples/sample-output.json | 2 +-
internal/buildinfo/version.go | 4 ++--
internal/output/html_test.go | 2 +-
internal/output/json_test.go | 2 +-
internal/output/pretty_test.go | 4 ++--
internal/telemetry/telemetry.go | 2 +-
7 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index e8755bb..6689ee8 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-
+
diff --git a/examples/sample-output.json b/examples/sample-output.json
index e605abe..6244830 100644
--- a/examples/sample-output.json
+++ b/examples/sample-output.json
@@ -1,5 +1,5 @@
{
- "agent_version": "1.8.2",
+ "agent_version": "1.9.0",
"scan_timestamp": 1741305600,
"scan_timestamp_iso": "2026-03-07T00:00:00Z",
"device": {
diff --git a/internal/buildinfo/version.go b/internal/buildinfo/version.go
index fb6f890..1d5dfc0 100644
--- a/internal/buildinfo/version.go
+++ b/internal/buildinfo/version.go
@@ -3,14 +3,14 @@ package buildinfo
import "fmt"
const (
- Version = "1.8.2"
+ Version = "1.9.0"
AgentURL = "https://github.com/step-security/dev-machine-guard"
)
// Build-time variables set via -ldflags by goreleaser or Makefile.
var (
GitCommit string // short commit hash (Makefile) or full commit (goreleaser)
- ReleaseTag string // e.g., "v1.8.2" (goreleaser only)
+ ReleaseTag string // e.g., "v1.9.0" (goreleaser only)
ReleaseBranch string // e.g., "main" (goreleaser only)
)
diff --git a/internal/output/html_test.go b/internal/output/html_test.go
index 4f260f7..9098a92 100644
--- a/internal/output/html_test.go
+++ b/internal/output/html_test.go
@@ -13,7 +13,7 @@ func TestHTML_GeneratesFile(t *testing.T) {
defer os.Remove(tmpFile)
result := &model.ScanResult{
- AgentVersion: "1.8.2",
+ AgentVersion: "1.9.0",
ScanTimestamp: 1700000000,
ScanTimestampISO: "2023-11-14T22:13:20Z",
Device: model.Device{
diff --git a/internal/output/json_test.go b/internal/output/json_test.go
index 9bbefbb..f7cd061 100644
--- a/internal/output/json_test.go
+++ b/internal/output/json_test.go
@@ -10,7 +10,7 @@ import (
func TestJSON_ValidOutput(t *testing.T) {
result := &model.ScanResult{
- AgentVersion: "1.8.2",
+ AgentVersion: "1.9.0",
AgentURL: "https://github.com/step-security/dev-machine-guard",
ScanTimestamp: 1700000000,
ScanTimestampISO: "2023-11-14T22:13:20Z",
diff --git a/internal/output/pretty_test.go b/internal/output/pretty_test.go
index 8373924..2fa656c 100644
--- a/internal/output/pretty_test.go
+++ b/internal/output/pretty_test.go
@@ -10,7 +10,7 @@ import (
func TestPretty_ContainsHeaders(t *testing.T) {
result := &model.ScanResult{
- AgentVersion: "1.8.2",
+ AgentVersion: "1.9.0",
ScanTimestamp: 1700000000,
ScanTimestampISO: "2023-11-14T22:13:20Z",
Device: model.Device{
@@ -40,7 +40,7 @@ func TestPretty_ContainsHeaders(t *testing.T) {
func TestPretty_ContainsBanner(t *testing.T) {
result := &model.ScanResult{
- AgentVersion: "1.8.2",
+ AgentVersion: "1.9.0",
ScanTimestamp: 1700000000,
Device: model.Device{Hostname: "test"},
AIAgentsAndTools: []model.AITool{},
diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go
index 967ddf9..cad5d31 100644
--- a/internal/telemetry/telemetry.go
+++ b/internal/telemetry/telemetry.go
@@ -65,7 +65,7 @@ type PerformanceMetrics struct {
// Output format matches the shell script's sample_log:
//
// ==========================================
-// StepSecurity Device Agent v1.8.2
+// StepSecurity Device Agent v1.9.0
// ==========================================
// [scanning] Lock acquired (PID: 32560)
// [scanning] Device ID (Serial): ...
From d96ec137bbfbbb8e070a205610407906f84d81ae Mon Sep 17 00:00:00 2001
From: Shubham Malik
Date: Fri, 3 Apr 2026 12:24:33 +0530
Subject: [PATCH 3/3] chore(mdm): update docs & address minor issues
---
.github/ISSUE_TEMPLATE/bug_report.yml | 8 +-
.github/pull_request_template.md | 7 +-
.github/workflows/release.yml | 43 +++----
.github/workflows/{go.yml => tests.yml} | 2 +-
CHANGELOG.md | 9 ++
CONTRIBUTING.md | 36 +++---
README.md | 2 +-
SECURITY.md | 2 +-
docs/adding-detections.md | 150 +++++++++---------------
docs/community-mode.md | 35 +++---
docs/reading-scan-results.md | 10 +-
docs/release-process.md | 51 ++++----
internal/config/config.go | 23 +++-
internal/detector/agent.go | 13 +-
internal/detector/mcp.go | 52 ++++++--
internal/detector/nodepm_test.go | 46 ++------
internal/lock/lock.go | 51 ++++++--
internal/output/pretty.go | 2 +-
internal/telemetry/logcapture.go | 12 +-
internal/telemetry/telemetry.go | 5 +-
20 files changed, 291 insertions(+), 268 deletions(-)
rename .github/workflows/{go.yml => tests.yml} (98%)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 76437fb..080fc0b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -10,9 +10,9 @@ body:
- type: input
id: version
attributes:
- label: Script Version
- description: "Run: ./stepsecurity-dev-machine-guard.sh --version"
- placeholder: "1.8.1"
+ label: Version
+ description: "Run: ./stepsecurity-dev-machine-guard --version"
+ placeholder: "1.9.0"
validations:
required: true
- type: input
@@ -28,7 +28,7 @@ body:
attributes:
label: Command Run
description: The exact command you ran
- placeholder: "./stepsecurity-dev-machine-guard.sh --json"
+ placeholder: "./stepsecurity-dev-machine-guard --json"
validations:
required: true
- type: textarea
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 12ef8a7..6372866 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -11,10 +11,11 @@
## Testing
- [ ] Tested on macOS (version: ___)
-- [ ] Script runs without errors: `./stepsecurity-dev-machine-guard.sh --verbose`
-- [ ] JSON output is valid: `./stepsecurity-dev-machine-guard.sh --json | python3 -m json.tool`
+- [ ] Binary runs without errors: `./stepsecurity-dev-machine-guard --verbose`
+- [ ] JSON output is valid: `./stepsecurity-dev-machine-guard --json | python3 -m json.tool`
- [ ] No secrets or credentials included
-- [ ] ShellCheck passes (if script was modified)
+- [ ] Lint passes: `make lint`
+- [ ] Tests pass: `make test`
## Related Issues
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5c7e0e5..ff0be71 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -69,44 +69,45 @@ jobs:
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- - name: Locate built binaries
- id: binaries
+ - name: Prepare release artifacts for signing
run: |
- # GoReleaser keeps binaries in build subdirs (e.g. _amd64_v1, _arm64_v8.0)
- AMD64=$(find dist -type f -name 'stepsecurity-dev-machine-guard' -path '*darwin_amd64*' | head -1)
- ARM64=$(find dist -type f -name 'stepsecurity-dev-machine-guard' -path '*darwin_arm64*' | head -1)
-
- for label in "amd64:${AMD64}" "arm64:${ARM64}"; do
+ # Copy binaries to match the exact names users download from the release.
+ # GoReleaser uploads as name_template (e.g. stepsecurity-dev-machine-guard_darwin_amd64)
+ # but keeps them in build subdirs locally. We copy to dist/ with release names
+ # so cosign signs the same bytes users verify against.
+ AMD64_SRC=$(find dist -type f -name 'stepsecurity-dev-machine-guard' -path '*darwin_amd64*' | head -1)
+ ARM64_SRC=$(find dist -type f -name 'stepsecurity-dev-machine-guard' -path '*darwin_arm64*' | head -1)
+
+ for label in "amd64:${AMD64_SRC}" "arm64:${ARM64_SRC}"; do
name="${label%%:*}"
path="${label#*:}"
if [ -z "$path" ] || [ ! -f "$path" ]; then
echo "::error::Binary not found for ${name}"
- echo "dist/ contents:"
find dist -type f
exit 1
fi
done
- echo "amd64=${AMD64}" >> "$GITHUB_OUTPUT"
- echo "arm64=${ARM64}" >> "$GITHUB_OUTPUT"
- echo "Found amd64: ${AMD64}"
- echo "Found arm64: ${ARM64}"
+ cp "$AMD64_SRC" dist/stepsecurity-dev-machine-guard_darwin_amd64
+ cp "$ARM64_SRC" dist/stepsecurity-dev-machine-guard_darwin_arm64
+ echo "Prepared release artifacts for signing"
- name: Sign artifacts with Sigstore (keyless)
run: |
- cosign sign-blob "${{ steps.binaries.outputs.amd64 }}" \
+ cosign sign-blob dist/stepsecurity-dev-machine-guard_darwin_amd64 \
--bundle dist/stepsecurity-dev-machine-guard_darwin_amd64.bundle --yes
- cosign sign-blob "${{ steps.binaries.outputs.arm64 }}" \
+ cosign sign-blob dist/stepsecurity-dev-machine-guard_darwin_arm64 \
--bundle dist/stepsecurity-dev-machine-guard_darwin_arm64.bundle --yes
cosign sign-blob stepsecurity-dev-machine-guard.sh \
--bundle dist/stepsecurity-dev-machine-guard.sh.bundle --yes
- name: Generate checksums
run: |
- SUMS="dist/stepsecurity-dev-machine-guard_${{ steps.version.outputs.version }}_SHA256SUMS"
- sha256sum "${{ steps.binaries.outputs.amd64 }}" >> "$SUMS"
- sha256sum "${{ steps.binaries.outputs.arm64 }}" >> "$SUMS"
- sha256sum stepsecurity-dev-machine-guard.sh >> "$SUMS"
+ # Separate checksum file for cosign-signed artifacts (script + bundles).
+ # GoReleaser already generates checksums for the Go binaries in its own SHA256SUMS file.
+ sha256sum dist/stepsecurity-dev-machine-guard_darwin_amd64 > dist/cosign-checksums.txt
+ sha256sum dist/stepsecurity-dev-machine-guard_darwin_arm64 >> dist/cosign-checksums.txt
+ sha256sum stepsecurity-dev-machine-guard.sh >> dist/cosign-checksums.txt
- name: Upload signature bundles and checksums to release
env:
@@ -116,7 +117,7 @@ jobs:
dist/stepsecurity-dev-machine-guard_darwin_amd64.bundle \
dist/stepsecurity-dev-machine-guard_darwin_arm64.bundle \
dist/stepsecurity-dev-machine-guard.sh.bundle \
- dist/stepsecurity-dev-machine-guard_${{ steps.version.outputs.version }}_SHA256SUMS \
+ dist/cosign-checksums.txt \
--clobber
- name: Mark release as immutable (not a draft, not a prerelease)
@@ -132,6 +133,6 @@ jobs:
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: |
- ${{ steps.binaries.outputs.amd64 }}
- ${{ steps.binaries.outputs.arm64 }}
+ dist/stepsecurity-dev-machine-guard_darwin_amd64
+ dist/stepsecurity-dev-machine-guard_darwin_arm64
stepsecurity-dev-machine-guard.sh
diff --git a/.github/workflows/go.yml b/.github/workflows/tests.yml
similarity index 98%
rename from .github/workflows/go.yml
rename to .github/workflows/tests.yml
index c625e1c..8bec676 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/tests.yml
@@ -1,4 +1,4 @@
-name: Go
+name: Tests
on:
push:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ae7f08..d4a2343 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
See [VERSIONING.md](VERSIONING.md) for why the version starts at 1.8.1.
+## [1.9.0] - 2026-04-03
+
+Migrated from shell script to a compiled Go binary. All existing scanning features, detection logic, CLI flags, output formats, and enterprise telemetry are preserved — this release changes the implementation, not the functionality.
+
+### Added
+- **Go binary**: Single compiled binary (`stepsecurity-dev-machine-guard`) replaces the shell script. Zero external dependencies, no runtime required.
+- **`configure` / `configure show` commands**: Interactive setup and display of enterprise credentials, search directories, and preferences. Saved to `~/.stepsecurity/config.json`.
+
## [1.8.2] - 2026-03-17
### Added
@@ -44,5 +52,6 @@ First open-source release. The scanning engine was previously an internal enterp
- Execution log capture and base64 encoding
- Instance locking to prevent concurrent runs
+[1.9.0]: https://github.com/step-security/dev-machine-guard/compare/v1.8.2...v1.9.0
[1.8.2]: https://github.com/step-security/dev-machine-guard/compare/v1.8.1...v1.8.2
[1.8.1]: https://github.com/step-security/dev-machine-guard/releases/tag/v1.8.1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b2ce56c..99b5e56 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,21 +9,15 @@ Thank you for your interest in contributing! Dev Machine Guard is an open-source
To add detection for a new AI tool, IDE, or framework:
1. Open an issue using the [Feature Request](.github/ISSUE_TEMPLATE/feature_request.yml) template, or
-2. Submit a PR modifying `stepsecurity-dev-machine-guard.sh`
+2. Submit a PR modifying the appropriate detector in `internal/detector/`
**How to add a new IDE/desktop app:**
-Find the `detect_ide_installations()` function and add an entry to the `apps` array:
-```bash
-"App Name|type_id|Vendor|/Applications/App.app|Contents/MacOS/binary|--version"
-```
+Find the IDE detector in `internal/detector/ide.go` and add an entry to the apps list. See [Adding Detections](docs/adding-detections.md) for the full guide.
**How to add a new AI CLI tool:**
-Find the `detect_ai_cli_tools()` function and add an entry to the `tools` array:
-```bash
-"tool-name|Vendor|binary1,binary2|~/.config-dir1,~/.config-dir2"
-```
+Find the AI CLI detector in `internal/detector/ai_cli.go` and add an entry to the tools list. See [Adding Detections](docs/adding-detections.md) for the full guide.
### Improve Documentation
@@ -37,37 +31,37 @@ Documentation lives in the `docs/` folder. Improvements, corrections, and new gu
cd dev-machine-guard
```
-2. Make the script executable:
+2. Build the binary:
```bash
- chmod +x stepsecurity-dev-machine-guard.sh
+ make build
```
3. Run locally:
```bash
# Pretty output with progress messages
- ./stepsecurity-dev-machine-guard.sh --verbose
+ ./stepsecurity-dev-machine-guard --verbose
# JSON output
- ./stepsecurity-dev-machine-guard.sh --json
+ ./stepsecurity-dev-machine-guard --json
# HTML report
- ./stepsecurity-dev-machine-guard.sh --html report.html
+ ./stepsecurity-dev-machine-guard --html report.html
```
## Code Style
-- The script must pass [ShellCheck](https://www.shellcheck.net/) (our CI runs it on every PR)
-- Follow the existing code patterns (section headers, function naming, JSON construction)
-- Use `print_progress` for status messages (they respect the `--verbose` flag)
-- Use `print_error` for error messages (always shown)
+- Go source code in `internal/` must pass `golangci-lint` (our CI runs it on every PR)
+- Follow the existing code patterns (package structure, naming conventions, JSON struct tags)
+- Use the `progress` package for status messages (they respect the `--verbose` flag)
+- Use standard Go error handling patterns
## Pull Request Process
1. Fork the repository
2. Create a feature branch (`git checkout -b add-new-tool-detection`)
-3. Make your changes
-4. Test locally: `./stepsecurity-dev-machine-guard.sh --verbose`
-5. Ensure ShellCheck passes: `shellcheck stepsecurity-dev-machine-guard.sh`
+3. Edit Go source in `internal/` (not the legacy shell script)
+4. Test locally: `./stepsecurity-dev-machine-guard --verbose`
+5. Ensure lint and tests pass: `make lint && make test && make smoke`
6. Submit a PR using our [PR template](.github/pull_request_template.md)
## Reporting Issues
diff --git a/README.md b/README.md
index 6689ee8..483910b 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ make build
./stepsecurity-dev-machine-guard
```
-Requires Go 1.22+. The binary has zero external dependencies.
+Requires Go 1.24+. The binary has zero external dependencies.
## Usage
diff --git a/SECURITY.md b/SECURITY.md
index 835d2ee..7a85b86 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -19,7 +19,7 @@ We will acknowledge your report within 48 hours and provide a detailed response
## Scope
This policy covers:
-- The `stepsecurity-dev-machine-guard.sh` script
+- The `stepsecurity-dev-machine-guard` binary and Go source code in `internal/`
- The StepSecurity backend API (for enterprise mode)
## Supported Versions
diff --git a/docs/adding-detections.md b/docs/adding-detections.md
index 92b0300..1abef5a 100644
--- a/docs/adding-detections.md
+++ b/docs/adding-detections.md
@@ -10,13 +10,13 @@ This guide walks you through adding new detections to Dev Machine Guard. Whether
Dev Machine Guard uses array-driven detection. Each detection category has a function that iterates over a defined array of entries. To add a new detection, you add an entry to the appropriate array and (optionally) handle any special cases.
-The main script file is `stepsecurity-dev-machine-guard.sh` (community mode). The enterprise script (`stepsecurity-agent-*.sh`) uses the same detection functions.
+The detection code lives in the `internal/detector/` directory, with each detector category in its own `.go` file.
---
## 1. Adding a New IDE or Desktop App
-### Function: `detect_ide_installations()`
+### File: `internal/detector/ide.go`
### Format String
@@ -35,39 +35,26 @@ The main script file is `stepsecurity-dev-machine-guard.sh` (community mode). Th
### Example: Adding a hypothetical "CodeForge" IDE
-Find the `apps` array inside `detect_ide_installations()` and add:
+Find the `apps` array inside the IDE detector and add:
-```bash
-local apps=(
- # ... existing entries ...
- "CodeForge|codeforge|CodeForge Inc|/Applications/CodeForge.app|Contents/MacOS/CodeForge|--version"
-)
-```
-
-If the app stores its version in `Info.plist` instead of a CLI binary, leave the binary path and version command empty:
-
-```bash
-"CodeForge|codeforge|CodeForge Inc|/Applications/CodeForge.app|||"
+```go
+{
+ Name: "CodeForge",
+ TypeID: "codeforge",
+ Vendor: "CodeForge Inc",
+ AppPath: "/Applications/CodeForge.app",
+ BinaryPath: "Contents/MacOS/CodeForge",
+ VersionCommand: "--version",
+}
```
-The scanner will automatically fall back to reading `CFBundleShortVersionString` from `Info.plist`.
-
-### Also update the pretty formatter
-
-If you want your IDE to display a friendly name in pretty output, add a case to the `format_pretty_output()` function:
-
-```bash
-case "$ide_type" in
- # ... existing cases ...
- codeforge) display_name="CodeForge" ;;
-esac
-```
+If the app stores its version in `Info.plist` instead of a CLI binary, leave the binary path and version command empty. The scanner will automatically fall back to reading `CFBundleShortVersionString` from `Info.plist`.
---
## 2. Adding a New AI CLI Tool
-### Function: `detect_ai_cli_tools()`
+### File: `internal/detector/ai_cli.go`
### Format String
@@ -84,11 +71,13 @@ esac
### Example: Adding a hypothetical "DevPilot" CLI
-```bash
-local tools=(
- # ... existing entries ...
- "devpilot|DevPilot Inc|devpilot,dp|~/.devpilot,~/.config/devpilot"
-)
+```go
+{
+ Name: "devpilot",
+ Vendor: "DevPilot Inc",
+ Binaries: []string{"devpilot", "dp"},
+ ConfigDirs: []string{"~/.devpilot", "~/.config/devpilot"},
+}
```
The scanner will:
@@ -96,24 +85,11 @@ The scanner will:
2. If found, run `devpilot --version` (or `dp --version`) to get the version
3. Check if `~/.devpilot` or `~/.config/devpilot` exists as a config directory
-### Special version handling
-
-If the tool requires non-standard version extraction (e.g., the `--version` flag produces output that needs to be verified or parsed differently), add a case to the `case` statement inside the function:
-
-```bash
-case "$tool_name" in
- # ... existing cases ...
- devpilot)
- version=$(run_as_logged_in_user "$logged_in_user" "$binary_name version 2>/dev/null | head -1" || echo "unknown")
- ;;
-esac
-```
-
---
## 3. Adding a New AI Agent
-### Function: `detect_general_ai_agents()`
+### File: `internal/detector/agent.go`
### Format String
@@ -125,16 +101,18 @@ esac
|-------|-------------|
| agent-name | Unique name for the agent (e.g., `openclaw`, `gpt-engineer`) |
| Vendor | The company or organization (e.g., "OpenSource", "Anthropic") |
-| detection_paths | Comma-separated paths (directories or files) that indicate the agent is installed. Use `$user_home` for the home directory variable. |
+| detection_paths | Comma-separated paths (directories or files) that indicate the agent is installed |
| binary_names | Comma-separated binary names for version extraction |
### Example: Adding a hypothetical "AutoDev" agent
-```bash
-local agents=(
- # ... existing entries ...
- "autodev|AutoDev Inc|$user_home/.autodev|autodev"
-)
+```go
+{
+ Name: "autodev",
+ Vendor: "AutoDev Inc",
+ DetectionPaths: []string{"~/.autodev"},
+ Binaries: []string{"autodev"},
+}
```
The scanner will:
@@ -142,24 +120,11 @@ The scanner will:
2. If not found, check if `autodev` binary exists in PATH
3. If found either way, try to run `autodev --version` for version info
-### Special case: Agents within existing apps
-
-The `detect_general_ai_agents()` function includes a special case for Claude Cowork (a mode within Claude Desktop). If your agent is a mode within an existing app rather than a standalone tool, add a similar special case block after the main detection loop:
-
-```bash
-# Check for special agent modes (like Claude Cowork)
-local some_app_path="/Applications/SomeApp.app"
-if [ -d "$some_app_path" ]; then
- # Check version to determine if agent mode is available
- # ... version check logic ...
-fi
-```
-
---
## 4. Adding a New MCP Config Source
-### Function: `collect_mcp_configs()`
+### File: `internal/detector/mcp.go`
### Format String
@@ -170,61 +135,53 @@ fi
| Field | Description |
|-------|-------------|
| source_name | Unique identifier for the source (e.g., `claude_desktop`, `cursor`) |
-| config_path | Full path to the config file. Use `$user_home` for the home directory variable. |
+| config_path | Full path to the config file. Use `~` for the home directory. |
| Vendor | The company or organization (e.g., "Anthropic", "Cursor") |
### Example: Adding a hypothetical "CodeAssist" MCP config
-```bash
-local config_sources=(
- # ... existing entries ...
- "codeassist|$user_home/.codeassist/mcp_config.json|CodeAssist Inc"
-)
+```go
+{
+ Source: "codeassist",
+ ConfigPath: "~/.codeassist/mcp_config.json",
+ Vendor: "CodeAssist Inc",
+}
```
The scanner will:
1. Check if the config file exists at the specified path
2. Read the file contents
-3. In enterprise mode: filter with `jq` to extract only server names and commands, then base64-encode
+3. In enterprise mode: filter to extract only server names and commands, then base64-encode
4. In community mode: display the server information locally
-### Handling non-JSON formats
-
-If the config file is not JSON (e.g., YAML or TOML), the `jq` filtering step will be skipped automatically, and the raw content will be used. The StepSecurity backend handles parsing of multiple config formats.
-
-### Handling JSONC (JSON with comments)
-
-If the tool uses JSONC (like Zed's `settings.json`), add a special case to strip comments before parsing:
-
-```bash
-if [ "$source_name" = "codeassist" ] && [ "$perl_available" = true ]; then
- json_input=$(echo "$config_content" | perl -0777 -pe 's{/\*.*?\*/}{}gs; s{//[^\n]*}{}g')
-fi
-```
-
---
## 5. Testing Your Changes Locally
-After making changes, test locally with all three output formats:
+After making changes, build and test locally with all three output formats:
```bash
+# Build the binary
+make build
+
# Pretty output with progress messages
-./stepsecurity-dev-machine-guard.sh --verbose
+./stepsecurity-dev-machine-guard --verbose
# JSON output (validate it is well-formed)
-./stepsecurity-dev-machine-guard.sh --json | python3 -m json.tool
+./stepsecurity-dev-machine-guard --json | python3 -m json.tool
# HTML report
-./stepsecurity-dev-machine-guard.sh --html test-report.html
+./stepsecurity-dev-machine-guard --html test-report.html
```
-### Run ShellCheck
+### Run the linter and tests
-The CI pipeline runs ShellCheck on every PR. Run it locally before submitting:
+The CI pipeline runs `golangci-lint` and tests on every PR. Run them locally before submitting:
```bash
-shellcheck stepsecurity-dev-machine-guard.sh
+make lint
+make test
+make smoke
```
### Verify your new detection appears
@@ -236,7 +193,7 @@ shellcheck stepsecurity-dev-machine-guard.sh
### If you do not have the tool installed
-You can still verify your format string is correct by:
+You can still verify your detection is correct by:
1. Creating a test directory or dummy binary that matches the detection path
2. Running the scanner against it
3. Cleaning up after testing
@@ -258,8 +215,7 @@ After adding a new detection, update the following:
2. Create a feature branch: `git checkout -b add-detection-codeforge`
3. Make your changes
4. Test locally (all three output formats)
-5. Run ShellCheck
+5. Run `make lint` and `make test`
6. Submit a PR using the [PR template](https://github.com/step-security/dev-machine-guard/blob/main/.github/pull_request_template.md)
See [CONTRIBUTING.md](../CONTRIBUTING.md) for full contribution guidelines.
-
diff --git a/docs/community-mode.md b/docs/community-mode.md
index 7b6f872..37c4972 100644
--- a/docs/community-mode.md
+++ b/docs/community-mode.md
@@ -11,16 +11,16 @@ Community mode is the free, open-source way to run Dev Machine Guard locally on
```bash
git clone https://github.com/step-security/dev-machine-guard.git
cd dev-machine-guard
-chmod +x stepsecurity-dev-machine-guard.sh
-./stepsecurity-dev-machine-guard.sh
+make build
+./stepsecurity-dev-machine-guard
```
-Or run directly without cloning:
+Or download a pre-built binary without cloning:
```bash
-curl -sSL https://raw.githubusercontent.com/step-security/dev-machine-guard/main/stepsecurity-dev-machine-guard.sh -o stepsecurity-dev-machine-guard.sh
-chmod +x stepsecurity-dev-machine-guard.sh
-./stepsecurity-dev-machine-guard.sh
+curl -sSL https://github.com/step-security/dev-machine-guard/releases/latest/download/stepsecurity-dev-machine-guard_darwin_arm64 -o stepsecurity-dev-machine-guard
+chmod +x stepsecurity-dev-machine-guard
+./stepsecurity-dev-machine-guard
```
---
@@ -32,7 +32,7 @@ Dev Machine Guard supports three mutually exclusive output formats.
### Pretty Terminal Output (default)
```bash
-./stepsecurity-dev-machine-guard.sh
+./stepsecurity-dev-machine-guard
```
Pretty mode prints a styled, human-readable report directly to your terminal, including sections for Device information, Summary counts, AI Agents and Tools, IDEs and Desktop Apps, MCP Servers, and IDE Extensions.
@@ -41,13 +41,13 @@ Pretty mode prints a styled, human-readable report directly to your terminal, in
```bash
# Print JSON to stdout
-./stepsecurity-dev-machine-guard.sh --json
+./stepsecurity-dev-machine-guard --json
# Pipe through python for formatted output
-./stepsecurity-dev-machine-guard.sh --json | python3 -m json.tool
+./stepsecurity-dev-machine-guard --json | python3 -m json.tool
# Save to a file
-./stepsecurity-dev-machine-guard.sh --json > scan.json
+./stepsecurity-dev-machine-guard --json > scan.json
```
JSON mode writes a single JSON object to stdout. This is useful for scripting, piping into other tools, or storing results for later analysis. See [Reading Scan Results](reading-scan-results.md) for the full schema reference.
@@ -55,7 +55,7 @@ JSON mode writes a single JSON object to stdout. This is useful for scripting, p
### HTML Report
```bash
-./stepsecurity-dev-machine-guard.sh --html report.html
+./stepsecurity-dev-machine-guard --html report.html
```
HTML mode generates a self-contained HTML file with a styled report. The report can be opened in any browser and is suitable for sharing with team leads or printing.
@@ -82,7 +82,7 @@ HTML mode generates a self-contained HTML file with a styled report. The report
### Basic scan with pretty output
```bash
-./stepsecurity-dev-machine-guard.sh
+./stepsecurity-dev-machine-guard
```
Runs the scan and prints a styled report to the terminal. Progress messages are suppressed by default.
@@ -90,7 +90,7 @@ Runs the scan and prints a styled report to the terminal. Progress messages are
### Verbose scan to see what is happening
```bash
-./stepsecurity-dev-machine-guard.sh --verbose
+./stepsecurity-dev-machine-guard --verbose
```
Same as above, but progress messages (e.g., "Detecting IDE installations...", "Found: Cursor (Cursor) v0.50.1") are printed to stderr so you can follow along.
@@ -98,7 +98,7 @@ Same as above, but progress messages (e.g., "Detecting IDE installations...", "F
### JSON scan piped to jq
```bash
-./stepsecurity-dev-machine-guard.sh --json | jq '.ai_agents_and_tools[] | .name'
+./stepsecurity-dev-machine-guard --json | jq '.ai_agents_and_tools[] | .name'
```
Extracts the name of every detected AI tool.
@@ -106,7 +106,7 @@ Extracts the name of every detected AI tool.
### JSON scan with npm packages included
```bash
-./stepsecurity-dev-machine-guard.sh --json --enable-npm-scan > full-scan.json
+./stepsecurity-dev-machine-guard --json --enable-npm-scan > full-scan.json
```
Produces a comprehensive JSON scan including globally installed npm/yarn/pnpm/bun packages and per-project dependency listings.
@@ -114,7 +114,7 @@ Produces a comprehensive JSON scan including globally installed npm/yarn/pnpm/bu
### HTML report without colors in progress messages
```bash
-./stepsecurity-dev-machine-guard.sh --html report.html --verbose --color=never
+./stepsecurity-dev-machine-guard --html report.html --verbose --color=never
```
Generates an HTML report while showing progress messages without ANSI color codes (useful when piping stderr to a log file).
@@ -126,7 +126,7 @@ Generates an HTML report while showing progress messages without ANSI color code
In community mode:
- **No data leaves your machine.** There is no backend, no API calls, no telemetry.
-- The script source code is fully open. You can audit exactly what it does.
+- The source code is fully open. You can audit exactly what it does.
- All output is written to stdout (JSON, pretty) or to a local file (HTML). Nothing is transmitted over the network.
If you need centralized visibility across a fleet of developer machines, [start a 14-day free trial](https://www.stepsecurity.io/start-free) by installing the StepSecurity GitHub App.
@@ -139,4 +139,3 @@ If you need centralized visibility across a fleet of developer machines, [start
- [Adding Detections](adding-detections.md) -- contribute new tool or IDE detections
- [SCAN_COVERAGE.md](../SCAN_COVERAGE.md) -- full catalog of supported detections
- [CONTRIBUTING.md](../CONTRIBUTING.md) -- how to contribute to the project
-
diff --git a/docs/reading-scan-results.md b/docs/reading-scan-results.md
index c718dda..95dca5d 100644
--- a/docs/reading-scan-results.md
+++ b/docs/reading-scan-results.md
@@ -184,7 +184,7 @@ When you run with `--json`, the scanner outputs a single JSON object to stdout.
| Field | Type | Description |
|-------|------|-------------|
-| `agent_version` | string | Version of the agent script |
+| `agent_version` | string | Version of the scanner binary |
| `scan_timestamp` | number | Unix timestamp (seconds) of the scan |
| `scan_timestamp_iso` | string | ISO 8601 timestamp |
| `device` | object | Device identification information |
@@ -238,25 +238,25 @@ The HTML report is styled with StepSecurity branding (purple accent colors) and
### Extract all AI tool names
```bash
-./stepsecurity-dev-machine-guard.sh --json | jq -r '.ai_agents_and_tools[].name'
+./stepsecurity-dev-machine-guard --json | jq -r '.ai_agents_and_tools[].name'
```
### Count extensions per IDE
```bash
-./stepsecurity-dev-machine-guard.sh --json | jq '[.ide_extensions[] | .ide_type] | group_by(.) | map({(.[0]): length}) | add'
+./stepsecurity-dev-machine-guard --json | jq '[.ide_extensions[] | .ide_type] | group_by(.) | map({(.[0]): length}) | add'
```
### Check if a specific extension is installed
```bash
-./stepsecurity-dev-machine-guard.sh --json | jq '.ide_extensions[] | select(.id == "ms-python.python")'
+./stepsecurity-dev-machine-guard --json | jq '.ide_extensions[] | select(.id == "ms-python.python")'
```
### Export extension list as CSV
```bash
-./stepsecurity-dev-machine-guard.sh --json | jq -r '.ide_extensions[] | [.id, .version, .publisher, .ide_type] | @csv'
+./stepsecurity-dev-machine-guard --json | jq -r '.ide_extensions[] | [.id, .version, .publisher, .ide_type] | @csv'
```
---
diff --git a/docs/release-process.md b/docs/release-process.md
index 322a25b..73c35c8 100644
--- a/docs/release-process.md
+++ b/docs/release-process.md
@@ -8,24 +8,25 @@ This document describes how releases are created, signed, and verified.
## Overview
-Releases are created via a manually triggered GitHub Actions workflow that requires approval from the `release` environment. The workflow:
+Releases are created via a manually triggered GitHub Actions workflow (`workflow_dispatch`) that requires approval from the `release` environment. The workflow uses [GoReleaser](https://goreleaser.com/) to:
-1. Extracts the version from `AGENT_VERSION` in the script
-2. Verifies the tag does not already exist (immutability)
-3. Signs the script with [Sigstore](https://www.sigstore.dev/) cosign (keyless)
-4. Generates SHA256 checksums
-5. Creates a Git tag and GitHub Release
-6. Attaches the script, Sigstore bundle, and checksums as release assets
-7. Generates SLSA build provenance attestation
+1. Read the version from `internal/buildinfo/version.go` (`const Version = "1.9.0"`)
+2. Verify the tag does not already exist (immutability)
+3. Build platform-specific binaries with GoReleaser
+4. Sign the binaries with [Sigstore](https://www.sigstore.dev/) cosign (keyless)
+5. Generate SHA256 checksums
+6. Create a Git tag and GitHub Release
+7. Attach binaries, Sigstore bundles, and checksums as release assets
+8. Generate SLSA build provenance attestation
## How to Create a Release
### 1. Bump the version
-Update `AGENT_VERSION` in `stepsecurity-dev-machine-guard.sh`:
+Update `Version` in `internal/buildinfo/version.go`:
-```bash
-AGENT_VERSION="1.9.0"
+```go
+const Version = "1.9.0"
```
Update the [CHANGELOG.md](../CHANGELOG.md) with a new section for the version.
@@ -45,19 +46,20 @@ The workflow uses a GitHub Environment called `release` that requires approval.
### 4. Verify the release
-Once approved, the workflow will create the tag, sign the script, create the GitHub Release, and upload the artifacts. Check the [Releases page](https://github.com/step-security/dev-machine-guard/releases) to confirm.
+Once approved, the workflow will create the tag, build the binaries, sign them, create the GitHub Release, and upload the artifacts. Check the [Releases page](https://github.com/step-security/dev-machine-guard/releases) to confirm.
---
## Release Artifacts
-Each release includes three artifacts:
+Each release includes the following artifacts:
| Artifact | Description |
|----------|-------------|
-| `stepsecurity-dev-machine-guard.sh` | The scanner script |
-| `stepsecurity-dev-machine-guard.sh.bundle` | Sigstore cosign bundle (signature, certificate, and Rekor transparency log entry) |
-| `checksums.txt` | SHA256 checksums of the script and bundle |
+| `stepsecurity-dev-machine-guard_darwin_amd64` | macOS Intel binary |
+| `stepsecurity-dev-machine-guard_darwin_arm64` | macOS Apple Silicon binary |
+| `checksums.txt` | SHA256 checksums of all release artifacts |
+| `*.bundle` | Sigstore cosign bundles (signature, certificate, and Rekor transparency log entry) |
---
@@ -74,24 +76,24 @@ brew install cosign
# Other platforms: https://docs.sigstore.dev/cosign/system_config/installation/
```
-### Verify the script signature
+### Verify the binary signature
```bash
# Download the release artifacts
-gh release download v1.8.1 --repo step-security/dev-machine-guard
+gh release download v1.9.0 --repo step-security/dev-machine-guard
-# Verify the Sigstore signature
-cosign verify-blob stepsecurity-dev-machine-guard.sh \
- --bundle stepsecurity-dev-machine-guard.sh.bundle \
+# Verify the Sigstore signature (example for Apple Silicon)
+cosign verify-blob stepsecurity-dev-machine-guard_darwin_arm64 \
+ --bundle stepsecurity-dev-machine-guard_darwin_arm64.bundle \
--certificate-identity-regexp "github.com/step-security/dev-machine-guard" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
```
A successful verification confirms:
-- The script was signed by the `step-security/dev-machine-guard` GitHub Actions workflow
+- The binary was signed by the `step-security/dev-machine-guard` GitHub Actions workflow
- The signature is recorded in the [Rekor transparency log](https://search.sigstore.dev/)
-- The script has not been tampered with since signing
+- The binary has not been tampered with since signing
### Verify the checksum
@@ -102,7 +104,7 @@ sha256sum -c checksums.txt
### Verify build provenance
```bash
-gh attestation verify stepsecurity-dev-machine-guard.sh \
+gh attestation verify stepsecurity-dev-machine-guard_darwin_arm64 \
--repo step-security/dev-machine-guard
```
@@ -136,4 +138,3 @@ The release workflow requires a GitHub Environment named `release` with required
- [VERSIONING.md](../VERSIONING.md) — versioning scheme
- [Sigstore documentation](https://docs.sigstore.dev/) — how keyless signing works
- [SLSA](https://slsa.dev/) — supply chain integrity framework
-
diff --git a/internal/config/config.go b/internal/config/config.go
index 7efca48..9db2440 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -113,7 +113,7 @@ func RunConfigure() error {
existing.CustomerID = promptValue(reader, "Customer ID", existing.CustomerID)
existing.APIEndpoint = promptValue(reader, "API Endpoint", existing.APIEndpoint)
- existing.APIKey = promptValue(reader, "API Key", existing.APIKey)
+ existing.APIKey = promptSecret(reader, "API Key", existing.APIKey)
existing.ScanFrequencyHours = promptValue(reader, "Scan Frequency (hours)", existing.ScanFrequencyHours)
// Search dirs
@@ -211,6 +211,27 @@ func RunConfigure() error {
return nil
}
+// promptSecret shows a masked current value but keeps the real value on Enter.
+func promptSecret(reader *bufio.Reader, label, current string) string {
+ masked := maskSecret(current)
+ if masked != "(not set)" {
+ fmt.Printf(" %s [%s]: ", label, masked)
+ } else {
+ fmt.Printf(" %s: ", label)
+ }
+
+ line, _ := reader.ReadString('\n')
+ line = strings.TrimSpace(line)
+
+ if line == "-" {
+ return "" // clear value
+ }
+ if line == "" {
+ return current // keep real value
+ }
+ return line
+}
+
func promptValue(reader *bufio.Reader, label, current string) string {
if current != "" {
fmt.Printf(" %s [%s]: ", label, current)
diff --git a/internal/detector/agent.go b/internal/detector/agent.go
index fc97b17..b5efcd8 100644
--- a/internal/detector/agent.go
+++ b/internal/detector/agent.go
@@ -3,6 +3,7 @@ package detector
import (
"context"
"regexp"
+ "strconv"
"strings"
"time"
@@ -134,13 +135,13 @@ func isCoworkVersion(version string) bool {
if len(m) < 3 {
return false
}
- major := m[1]
- minor := m[2]
- if major >= "1" {
- return true
+ major, err1 := strconv.Atoi(m[1])
+ minor, err2 := strconv.Atoi(m[2])
+ if err1 != nil || err2 != nil {
+ return false
}
- if major == "0" && minor >= "7" {
+ if major >= 1 {
return true
}
- return false
+ return major == 0 && minor >= 7
}
diff --git a/internal/detector/mcp.go b/internal/detector/mcp.go
index 37b23c4..b6e1f6e 100644
--- a/internal/detector/mcp.go
+++ b/internal/detector/mcp.go
@@ -4,7 +4,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
- "regexp"
"strings"
"github.com/step-security/dev-machine-guard/internal/executor"
@@ -158,13 +157,48 @@ func filterServerFields(serversRaw json.RawMessage) map[string]any {
return result
}
-// stripJSONCComments removes // and /* */ comments from JSONC content.
+// stripJSONCComments removes // and /* */ comments from JSONC content,
+// respecting quoted strings (won't strip // inside "https://...").
func stripJSONCComments(input []byte) []byte {
- // Remove block comments
- blockRe := regexp.MustCompile(`/\*[\s\S]*?\*/`)
- result := blockRe.ReplaceAll(input, nil)
- // Remove line comments
- lineRe := regexp.MustCompile(`//[^\n]*`)
- result = lineRe.ReplaceAll(result, nil)
- return result
+ var out []byte
+ i := 0
+ for i < len(input) {
+ // Skip over strings — don't modify content inside quotes
+ if input[i] == '"' {
+ out = append(out, input[i])
+ i++
+ for i < len(input) {
+ out = append(out, input[i])
+ if input[i] == '\\' && i+1 < len(input) {
+ i++
+ out = append(out, input[i])
+ } else if input[i] == '"' {
+ break
+ }
+ i++
+ }
+ i++
+ continue
+ }
+ // Block comment
+ if i+1 < len(input) && input[i] == '/' && input[i+1] == '*' {
+ i += 2
+ for i+1 < len(input) && !(input[i] == '*' && input[i+1] == '/') {
+ i++
+ }
+ i += 2 // skip */
+ continue
+ }
+ // Line comment
+ if i+1 < len(input) && input[i] == '/' && input[i+1] == '/' {
+ i += 2
+ for i < len(input) && input[i] != '\n' {
+ i++
+ }
+ continue
+ }
+ out = append(out, input[i])
+ i++
+ }
+ return out
}
diff --git a/internal/detector/nodepm_test.go b/internal/detector/nodepm_test.go
index 49b0e80..cb4687d 100644
--- a/internal/detector/nodepm_test.go
+++ b/internal/detector/nodepm_test.go
@@ -52,52 +52,22 @@ func TestNodePMDetector_NoneFound(t *testing.T) {
}
func TestDetectProjectPM(t *testing.T) {
- mock := executor.NewMock()
-
tests := []struct {
name string
- setup func()
+ file string
expected string
}{
- {
- name: "bun lock",
- setup: func() {
- mock.SetFile("/project/bun.lock", []byte{})
- },
- expected: "bun",
- },
- {
- name: "pnpm lock",
- setup: func() {
- mock.SetFile("/project/pnpm-lock.yaml", []byte{})
- },
- expected: "pnpm",
- },
- {
- name: "yarn lock",
- setup: func() {
- mock.SetFile("/project/yarn.lock", []byte{})
- },
- expected: "yarn",
- },
+ {"bun lock", "/project/bun.lock", "bun"},
+ {"pnpm lock", "/project/pnpm-lock.yaml", "pnpm"},
+ {"yarn lock", "/project/yarn.lock", "yarn"},
+ {"npm lock", "/project/package-lock.json", "npm"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- m := executor.NewMock()
- tt.setup = func() {} // reset
- // Need fresh mock for isolation
- freshMock := executor.NewMock()
- switch tt.expected {
- case "bun":
- freshMock.SetFile("/project/bun.lock", []byte{})
- case "pnpm":
- freshMock.SetFile("/project/pnpm-lock.yaml", []byte{})
- case "yarn":
- freshMock.SetFile("/project/yarn.lock", []byte{})
- }
- _ = m
- got := DetectProjectPM(freshMock, "/project")
+ mock := executor.NewMock()
+ mock.SetFile(tt.file, []byte{})
+ got := DetectProjectPM(mock, "/project")
if got != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, got)
}
diff --git a/internal/lock/lock.go b/internal/lock/lock.go
index a90f806..9654605 100644
--- a/internal/lock/lock.go
+++ b/internal/lock/lock.go
@@ -1,6 +1,7 @@
package lock
import (
+ "errors"
"fmt"
"os"
"strconv"
@@ -17,33 +18,59 @@ type Lock struct {
path string
}
-// Acquire obtains an exclusive instance lock. Returns error if another instance is running.
-func Acquire(exec executor.Executor) (*Lock, error) {
- // Check for existing lock
+// Acquire obtains an exclusive instance lock using atomic file creation.
+// Returns error if another instance is running.
+func Acquire(_ executor.Executor) (*Lock, error) {
+ // Check for existing lock file
if data, err := os.ReadFile(lockFilePath); err == nil {
pidStr := strings.TrimSpace(string(data))
- if pid, err := strconv.Atoi(pidStr); err == nil {
- // Check if the process is still alive
- if err := syscall.Kill(pid, 0); err == nil {
+ if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 {
+ if isProcessAlive(pid) {
return nil, fmt.Errorf("another instance is already running (PID %d)", pid)
}
}
- // Stale lock file, remove it
- os.Remove(lockFilePath)
+ // Stale lock — remove before attempting atomic create
+ _ = os.Remove(lockFilePath)
}
- // Write our PID
- pid := os.Getpid()
- if err := os.WriteFile(lockFilePath, []byte(strconv.Itoa(pid)), 0o644); err != nil {
+ // Atomic create: O_CREATE|O_EXCL fails if file already exists,
+ // preventing two processes from both creating the lock.
+ f, err := os.OpenFile(lockFilePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
+ if err != nil {
+ if errors.Is(err, os.ErrExist) {
+ // Another process created the lock between our check and create
+ return nil, fmt.Errorf("another instance is already running (lock file created concurrently)")
+ }
return nil, fmt.Errorf("creating lock file: %w", err)
}
+ _, err = fmt.Fprintf(f, "%d", os.Getpid())
+ f.Close()
+ if err != nil {
+ _ = os.Remove(lockFilePath)
+ return nil, fmt.Errorf("writing PID to lock file: %w", err)
+ }
+
return &Lock{path: lockFilePath}, nil
}
// Release removes the lock file.
func (l *Lock) Release() {
if l != nil && l.path != "" {
- os.Remove(l.path)
+ _ = os.Remove(l.path)
+ }
+}
+
+// isProcessAlive checks if a process with the given PID exists.
+// Returns true if the process is alive (signal 0 succeeds or returns EPERM).
+func isProcessAlive(pid int) bool {
+ err := syscall.Kill(pid, 0)
+ if err == nil {
+ return true // process exists and we can signal it
+ }
+ // EPERM means the process exists but we don't have permission to signal it
+ if errors.Is(err, syscall.EPERM) {
+ return true
}
+ return false // ESRCH or other error — process doesn't exist
}
diff --git a/internal/output/pretty.go b/internal/output/pretty.go
index 080c08b..73f6c58 100644
--- a/internal/output/pretty.go
+++ b/internal/output/pretty.go
@@ -109,7 +109,7 @@ func Pretty(w io.Writer, result *model.ScanResult, colorMode string) error {
switch ideType {
case "vscode":
displayType = "VSCode"
- case "cursor":
+ case "openvsx":
displayType = "Cursor"
}
fmt.Fprintf(w, " %s%s%s%s%*s%s%d found%s\n",
diff --git a/internal/telemetry/logcapture.go b/internal/telemetry/logcapture.go
index eda3a38..3a83722 100644
--- a/internal/telemetry/logcapture.go
+++ b/internal/telemetry/logcapture.go
@@ -60,21 +60,27 @@ func StartCapture() *LogCapture {
}
// Finalize stops capture and returns the base64-encoded output.
+// Safe to call multiple times — subsequent calls return the cached result.
func (lc *LogCapture) Finalize() string {
+ lc.mu.Lock()
+ defer lc.mu.Unlock()
+
if lc.pipeWrite == nil {
- return ""
+ // Already finalized or never started
+ return base64.StdEncoding.EncodeToString(lc.buf.Bytes())
}
// Close write end so the reader goroutine exits
lc.pipeWrite.Close()
+ lc.pipeWrite = nil
+ lc.mu.Unlock()
<-lc.done
+ lc.mu.Lock()
// Restore stderr
os.Stderr = lc.origErr
lc.pipeRead.Close()
- lc.mu.Lock()
- defer lc.mu.Unlock()
return base64.StdEncoding.EncodeToString(lc.buf.Bytes())
}
diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go
index cad5d31..4edc629 100644
--- a/internal/telemetry/telemetry.go
+++ b/internal/telemetry/telemetry.go
@@ -74,8 +74,11 @@ func Run(exec executor.Executor, log *progress.Logger, cfg *cli.Config) error {
ctx := context.Background()
startTime := time.Now()
- // Start capturing all stderr output for execution_logs
+ // Start capturing all stderr output for execution_logs.
+ // Defer Finalize immediately to ensure stderr is always restored,
+ // even on early returns (e.g., lock failure).
capture := StartCapture()
+ defer capture.Finalize()
// Banner (matches shell script format)
fmt.Fprintf(os.Stderr, "==========================================\n")