Skip to content

Commit 338fb9c

Browse files
authored
Merge branch 'main' into worktree-rename-api-endpoint
2 parents 3a604bc + b07fc1d commit 338fb9c

31 files changed

Lines changed: 1728 additions & 132 deletions

File tree

.github/workflows/msi-smoke.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ jobs:
257257
258258
- name: Upload install logs on failure
259259
if: failure()
260-
uses: actions/upload-artifact@v4
260+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
261261
with:
262262
name: msi-smoke-logs
263263
path: |

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
See [VERSIONING.md](VERSIONING.md) for why the version starts at 1.8.1.
99

10+
## [1.11.6] - 2026-05-27
11+
12+
### Fixed
13+
14+
- **macOS Tahoe Media Library prompt**: the project walker now skips `~/Library` wholesale instead of curating individual TCC-protected subpaths. This prevents new TCC prompts (e.g. `kTCCServiceMediaLibrary` from `~/Library/Application Support/com.apple.avfoundation/`) from firing after each macOS release adds Apple-managed subtrees behind new TCC services. Targeted detectors that read specific files under `~/Library` (JetBrains plugins, Claude desktop MCP config, pip global config) keep working unchanged.
15+
16+
## [1.11.5] - 2026-05-27
17+
18+
### Added
19+
20+
- **macOS TCC-protected directory skipping**: scanners now skip TCC-protected paths (Photos, Media Library, App Management, etc.) by default when running under launchd, avoiding spurious permission prompts and noisy denials. Hits are logged so operators can see which paths were skipped.
21+
- **PPPC configuration guide**: new docs explain how to grant the agent the necessary TCC permissions via a PPPC profile for environments that want full coverage.
22+
- **`verify-msi.ps1` script**: client-side PowerShell script for verifying the integrity and Authenticode signature of distributed MSI artifacts.
23+
24+
### Fixed
25+
26+
- **Empty `--install-dir` rejected**: install/uninstall commands now reject an empty `--install-dir` value instead of silently falling back to a default, preventing accidental installs to the wrong location.
27+
- **`install_dir` config field is authoritative**: the configured `install_dir` is now treated as the source of truth across install/uninstall paths, resolving inconsistencies when the field disagreed with runtime defaults.
28+
29+
## [1.11.4] - 2026-05-26
30+
31+
### Added
32+
33+
- **Authenticode-signed Windows binaries and MSIs**: release artifacts are now signed via Azure Trusted Signing, so installs no longer trip SmartScreen/EDR unsigned-binary heuristics on Windows.
34+
- **Feature gate for selective scanning**: new feature-gate mechanism allows disabling or enabling individual scanners at runtime, giving operators a way to scope what a deployment reports without rebuilding.
35+
- **Invocation method + in-flight status reporting**: telemetry now records how the agent was invoked (launchd / systemd / scheduled task / interactive) and emits structured per-phase status info while a scan is running.
36+
- **`$HOME` expansion in configured paths**: path-style config values now expand `$HOME` (and `~`) consistently across platforms.
37+
38+
### Fixed
39+
40+
- **Windows console window flashes during scheduled scans**: the scheduled task no longer pops a visible console window on each run.
41+
- **Telemetry post-phase is non-blocking**: post-phase telemetry submission can no longer stall scan completion if the backend is slow or unreachable; sandbox invocation tests added to cover the path.
42+
- **Canonicalised `$HOME`/`~` expansion**: path expansion now goes through `filepath.Join` so the resulting paths are normalised across `/`-vs-`\` and trailing-separator edge cases.
43+
44+
### Changed
45+
46+
- **Per-phase telemetry sub-progress incl. upload phase**: progress reporting now tracks sub-progress within each phase and adds an explicit upload phase, giving the dashboard finer-grained visibility into long-running scans.
47+
- **CI: on-demand test-binary + MSI workflow** added so non-release builds can be produced from a PR without cutting a tag.
48+
- **CI: msi-smoke workflow hardened** following StepSecurity best-practice review.
49+
1050
## [1.11.3] - 2026-05-21
1151

1252
### Added
@@ -181,6 +221,9 @@ First open-source release. The scanning engine was previously an internal enterp
181221
- Execution log capture and base64 encoding
182222
- Instance locking to prevent concurrent runs
183223

224+
[1.11.6]: https://github.com/step-security/dev-machine-guard/compare/v1.11.5...v1.11.6
225+
[1.11.5]: https://github.com/step-security/dev-machine-guard/compare/v1.11.4...v1.11.5
226+
[1.11.4]: https://github.com/step-security/dev-machine-guard/compare/v1.11.3...v1.11.4
184227
[1.11.3]: https://github.com/step-security/dev-machine-guard/compare/v1.11.1...v1.11.3
185228
[1.11.1]: https://github.com/step-security/dev-machine-guard/compare/v1.11.0...v1.11.1
186229
[1.11.0]: https://github.com/step-security/dev-machine-guard/compare/v1.10.2...v1.11.0

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
470470
- [Scan Coverage](SCAN_COVERAGE.md) — full catalog of detections
471471
- [Release Process](docs/release-process.md) — how releases are signed and verified
472472
- [Deploying via SCCM](docs/deploying-via-sccm.md) — Windows fleet rollout via Microsoft Configuration Manager (signed MSI, no PowerShell)
473+
- [macOS TCC Permissions](docs/macos-tcc-permissions.md) — how the agent handles Documents/Downloads/Mail TCC dirs, PPPC profile for MDM-pushed Full Disk Access, and the `include_tcc_protected` config field
473474
- [Versioning](VERSIONING.md) — why the version starts at 1.8.1
474475
- [Security Policy](SECURITY.md) — reporting vulnerabilities
475476
- [Code of Conduct](CODE_OF_CONDUCT.md)

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

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,85 @@
11
//go:build windows
22

33
// Command stepsecurity-dev-machine-guard-task is a GUI-subsystem
4-
// launcher that invokes the console-subsystem agent under
4+
// launcher that invokes a console-subsystem child under
55
// CREATE_NO_WINDOW. Built with `-ldflags "-H windowsgui"`.
66
//
77
// Why a separate binary: Windows allocates a console for any
8-
// console-subsystem process whose parent has none. Task Scheduler
9-
// under /ru INTERACTIVE is such a parent, so the agent itself would
10-
// always flash a window. The only fully-reliable suppression is for
11-
// the parent CreateProcess call to pass CREATE_NO_WINDOW, and the
12-
// only way to be that parent without flashing our own console is to
13-
// be GUI-subsystem. The agent stays console-subsystem so interactive
14-
// CLI use (install, configure, manual scans) still works normally.
8+
// console-subsystem process whose parent has none. Task Scheduler under
9+
// /ru INTERACTIVE is such a parent, so a console-subsystem child
10+
// invoked directly would always flash a window. The only fully reliable
11+
// suppression is for the parent CreateProcess call to pass
12+
// CREATE_NO_WINDOW, and the only way to be that parent without flashing
13+
// our own console is to be GUI-subsystem.
1514
//
16-
// Layout: both binaries sit in the same directory. The scheduled task
17-
// points at this launcher; arguments forward unchanged.
15+
// Two operating modes (target-resolution lives in internal/launcher so
16+
// it can be unit-tested cross-platform):
1817
//
19-
// Lifecycle: the agent is assigned to a Job Object with
20-
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so it dies when the launcher
21-
// does — including under Stop-ScheduledTask, which only terminates
22-
// the registered action's PID.
18+
// - Default. Invoked without --exec, the launcher spawns its sibling
19+
// stepsecurity-dev-machine-guard.exe and forwards argv unchanged.
20+
// This is what the MSI install layout's scheduled-task action uses.
21+
//
22+
// - --exec mode. Invoked as `task.exe --exec <exe> [args...]`, the
23+
// launcher spawns <exe> (exec.LookPath resolved) with the remaining
24+
// args. Used by the PowerShell loader's scheduled task to wrap
25+
// `powershell.exe -File loader.ps1 send-telemetry` in the same
26+
// no-console envelope the MSI flow uses for the agent.
27+
//
28+
// The agent (and any --exec target) stays console-subsystem so
29+
// interactive CLI use continues to work normally.
30+
//
31+
// Lifecycle: the child is assigned to a Job Object with
32+
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so it dies when the launcher does
33+
// — including under Stop-ScheduledTask, which only terminates the
34+
// registered action's PID.
2335
package main
2436

2537
import (
2638
"errors"
39+
"fmt"
2740
"os"
2841
"os/exec"
29-
"path/filepath"
3042
"syscall"
3143
"unsafe"
3244

45+
"github.com/step-security/dev-machine-guard/internal/launcher"
3346
"golang.org/x/sys/windows"
3447
)
3548

36-
const (
37-
createNoWindow uint32 = 0x08000000
38-
agentBinary = "stepsecurity-dev-machine-guard.exe"
39-
)
49+
const createNoWindow uint32 = 0x08000000
4050

4151
func main() {
42-
os.Exit(run())
52+
os.Exit(run(os.Args[1:]))
4353
}
4454

45-
func run() int {
46-
me, err := os.Executable()
55+
// run is split out so the entrypoint stays one line. argv is the slice
56+
// after the program name (os.Args[1:] in main); accepting it explicitly
57+
// keeps the windows-only test path open if we add one later.
58+
func run(argv []string) int {
59+
target, childArgs, err := launcher.ResolveTarget(argv)
4760
if err != nil {
48-
return 1
49-
}
50-
agent := filepath.Join(filepath.Dir(me), agentBinary)
51-
if _, err := os.Stat(agent); err != nil {
61+
// Two distinct failure shapes, matched to the legacy contract:
62+
//
63+
// - Default mode (no --exec): the launcher silently exits 1.
64+
// This preserves byte-for-byte compatibility with MSI installs
65+
// that the pre-1.11.5 launcher served. Task Scheduler records
66+
// "LastTaskResult=1" — same value MSI deployments have always
67+
// observed when the sibling agent is absent. A behavioral
68+
// change here would shift downstream dashboards/alerts that
69+
// key on the result code.
70+
//
71+
// - --exec mode: the caller asked for a feature; surface the
72+
// concrete misuse (missing target, unresolved PATH, etc.) on
73+
// stderr with a distinct exit code so dispatch failures are
74+
// diagnosable.
75+
if len(argv) > 0 && argv[0] == launcher.ExecFlag {
76+
fmt.Fprintln(os.Stderr, err)
77+
return 2
78+
}
5279
return 1
5380
}
5481

55-
cmd := exec.Command(agent, os.Args[1:]...)
82+
cmd := exec.Command(target, childArgs...)
5683
cmd.SysProcAttr = &syscall.SysProcAttr{
5784
HideWindow: true,
5885
CreationFlags: createNoWindow,
@@ -62,10 +89,11 @@ func run() int {
6289
return 1
6390
}
6491

65-
// Best-effort: bind the agent to a kill-on-close job. The job
92+
// Best-effort: bind the child to a kill-on-close job. The job
6693
// handle stays open in this process; the kernel closes it on our
6794
// exit, which fires the kill. Failure here only weakens lifecycle
68-
// (orphan possible on forced termination), not the scan itself.
95+
// (orphan possible on forced termination), not the work the child
96+
// was started to do.
6997
if job, jerr := newKillOnCloseJob(); jerr == nil {
7098
if h, oerr := windows.OpenProcess(
7199
windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE,

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ func main() {
8484
if cfg.EnablePythonScan == nil && config.EnablePythonScan != nil {
8585
cfg.EnablePythonScan = config.EnablePythonScan
8686
}
87+
if cfg.IncludeTCCProtected == nil && config.IncludeTCCProtected != nil {
88+
cfg.IncludeTCCProtected = config.IncludeTCCProtected
89+
}
8790
if cfg.ColorMode == "auto" && config.ColorMode != "" {
8891
cfg.ColorMode = config.ColorMode
8992
}
@@ -100,24 +103,32 @@ func main() {
100103
exec := executor.NewReal()
101104

102105
// Install dir resolution (see internal/paths.Home for the canonical
103-
// chain): --install-dir CLI flag > $STEPSECURITY_HOME env var >
104-
// install_dir config field > ~/.stepsecurity default. The CLI flag
105-
// wins because it is the most explicit per-invocation override the
106-
// operator can supply. We feed it to paths via SetOverride; an
107-
// explicit `--install-dir=` (empty) is preserved and short-circuits
108-
// the path computation below to disable file logging for this run.
106+
// chain): --install-dir CLI flag > install_dir config field >
107+
// $STEPSECURITY_HOME env var > ~/.stepsecurity default. Config beats
108+
// env because config.json is the source of truth that the loader
109+
// scripts write to and operators hand-edit; the env var baked into
110+
// service unit files at install time can otherwise become stale.
111+
// An explicit `--install-dir=` (empty) routes through SetDisabled,
112+
// after which paths.Home() returns "" so EVERY on-disk consumer
113+
// (filelog, ai-agent hook errors, any future file) uniformly skips
114+
// — not just file logging. cli.Parse rejects the empty form when
115+
// paired with `install` / `uninstall`, where the platform installers
116+
// need a real on-disk path for unit files and the log directory.
109117
//
110118
// The capture is installed before the logger so every subsequent
111119
// stderr write — including the pipe-tee in
112120
// internal/telemetry/logcapture.go, which nests inside this one —
113121
// flows through to disk.
114122
if cfg.InstallDirSet {
115-
paths.SetOverride(cfg.InstallDir) // may be "" = disabled
123+
if cfg.InstallDir == "" {
124+
paths.SetDisabled()
125+
} else {
126+
paths.SetOverride(cfg.InstallDir)
127+
}
116128
}
117-
installDir := paths.Home()
118-
disabled := cfg.InstallDirSet && cfg.InstallDir == ""
129+
installDir := paths.Home() // "" when SetDisabled or home unresolved
119130
logFilePath := ""
120-
if !disabled && installDir != "" {
131+
if installDir != "" {
121132
logFilePath = filepath.Join(installDir, filelog.Filename)
122133
// Pre-rotate BOTH files unconditionally. In interactive mode the
123134
// stderr rotation is redundant with filelog.Start's own rotation
@@ -164,7 +175,7 @@ func main() {
164175
// auto-move — too risky for v1 (silent overwrites, races with other
165176
// processes, perms changes). Just point at the leftovers.
166177
legacy := paths.LegacyHome()
167-
if !disabled && legacy != "" && installDir != "" && installDir != legacy {
178+
if legacy != "" && installDir != "" && installDir != legacy {
168179
if leftovers := findLegacyLeftovers(legacy); len(leftovers) > 0 {
169180
log.Warn("install dir is %s but the legacy default %s still has files: %s — copy them over manually if you want their history.",
170181
installDir, legacy, strings.Join(leftovers, ", "))

0 commit comments

Comments
 (0)