From 79590b9fde25b4494a9af68a9766d6e314d2666b Mon Sep 17 00:00:00 2001 From: Ashish Kurmi Date: Mon, 26 Oct 2026 03:12:49 -0700 Subject: [PATCH] chore(release): update to 1.18.0 Signed-off-by: Ashish Kurmi --- CHANGELOG.md | 28 +++++++++++++++++++++------- README.md | 2 +- examples/sample-output.json | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a768c9f..a322d078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,25 +7,38 @@ 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. -## [Unreleased] +## [1.18.0] - 2026-10-26 ### Added - **Windows MSI installs update themselves.** The MSI now installs a stable bootstrap, a seed copy of the agent and a SYSTEM `StepSecurity Dev Machine Guard (Update)` task. The task follows the tenant's `latest-binary` target. It verifies each release's SSH-signed checksums, sha256 and Authenticode publisher (it must match the running agent), stages it as an immutable `versions\` directory, smoke-tests it as SYSTEM and switches only when no scan is running. A new version is on probation until a scan reaches the backend; two crashes roll it back and quarantine it on that device. Every outcome is reported as a `run-status` event with `status: "update"`. The MSI never downgrades a self-updated device, and repairs keep the current version. `AUTOUPDATE=0` opts out (the device still reports `update_disabled`). Devices on older MSIs need one final MSI push. See `docs/windows-self-update.md`, and `docs/windows-self-update-admin.md` for SCCM/Intune admins (network and proxy requirements, the safe repair command, verifying devices and troubleshooting by update event). +- **Language runtime inventory on every platform, and Windows system packages.** A new `runtime_scan` phase finds every install of node, python, php, java, go, ruby, deno, bun and dotnet, including the ones a PATH probe misses: nvm, fnm, Volta, pyenv, uv, conda, sdkman, Scoop, plain zips and tarballs, and runtimes bundled inside other apps (Xcode, JetBrains JBR, VS Code Server, Cursor and other Electron apps, Visual Studio, snap bases). Versions are read without executing anything — from files the install ships, the Windows PE version resource, the Linux package database, the version string embedded in the binary, or the install path — and each row records which source answered. Launchers and version-manager shims are dropped so a shim never masquerades as an install, and each row carries its source, scope and `bundled_by`. The walk is budgeted per root and reports `truncated_roots` so the backend knows which roots were cut short. On by default; `--disable-runtime-scan` or `enable_runtime_scan: false` turns it off. Windows now also reports system packages in `system_package_scans`: Programs and Features (ARP) entries from the 64-bit, 32-bit and per-user registry views, Chocolatey nuspecs, Scoop manifests and winget portable installs, all from registry and file reads only. **Requires the matching backend**: on an older backend, Windows system packages would land under the Linux tab, so agent-api must be deployed first. +- **Go module inventory and configuration audit** (`go_inventory`, `go_config_audit`): `go.mod`/`go.work` requirements, replacements, excludes, tools and workspace membership; vendored modules; the extracted module cache and download archives; installed tools in `GOBIN` read via `debug/buildinfo`; and recorded checksums from `go.sum`, `go.work.sum` and `.ziphash`, always marked `not_verified`. The audit reads an allowlist of settings from the user `go/env` file, the process environment and `GOROOT/go.env`, and emits findings `go-001`–`go-006` (sumdb off, `GOINSECURE`, a private proxy falling back to a public one, credentials in URLs, a `GOAUTH` command, malformed or duplicate keys). No Go toolchain is executed; URL userinfo is stripped on the device; every refusal or limit marks the source `partial` so it never reads as absence. Community `scan` output is unchanged. +- **Rust package inventory and Cargo configuration audit** (`cargo_inventory`, `cargo_config_audit`): declared requirements from `Cargo.toml` with workspace inheritance applied, locked packages from `Cargo.lock` formats 3 and 4, the registry cache, Git checkouts, vendored directories, local registries and `cargo install` receipts, plus recorded checksums. The audit resolves `.cargo/config` files per invocation context in Cargo's precedence order, follows includes with depth and cycle bounds, and emits findings `cargo-001`–`cargo-004` (HTTP registry or source index, insecure Git source, plaintext token in config, `http.check-revoke = false`). Credentials files are stat-only; no `cargo`, `rustc` or `git` is run. - Passive PHP/Composer package inventory and configuration audit for developer machines. Collects project and global declarations, lockfiles and installed receipts, with redacted settings and configuration findings. PHP, Composer, plugins and scripts are never executed. +- **.NET/NuGet package inventory and `NuGet.Config` audit** (`nuget_inventory`, `nuget_config_audit`): declared requirements from SDK-style projects (`PackageReference`, `PackageDownload`, MSBuild SDKs), central `Directory.Packages.props` versions and legacy `packages.config`; restored packages from `project.assets.json`, lockfiles, the global, fallback and configured caches, global and local .NET tools, installed templates, `*.deps.json` binary dependencies and C#/F# file-based app directives. Cache and template identities are verified against package metadata. The audit covers project, user, machine and process scopes and emits findings `nuget-001`–`nuget-004`; credentials, proxy secrets and credential-provider paths are reported as presence only. dotnet, NuGet, MSBuild, credential providers and scripts are never executed; XML input rejects DTDs and `.nupkg` archives are read bounded and never extracted. +- **Ruby package inventory and RubyGems/Bundler configuration audit** (`ruby_inventory`, `ruby_config_audit`), collected in a new `ruby_scan` phase: declared requirements from `Gemfile`/`gems.rb` and gemspecs, locked packages from `Gemfile.lock`/`gems.locked` (GEM, GIT, PATH and PLUGIN sources, `CHECKSUMS`), installed and default gems from gemspec stubs, Bundler git checkouts, and cached `.gem` archives. Gem homes are discovered from rbenv, RVM, asdf, mise, chruby, macOS system Ruby, Homebrew, distribution prefixes, Windows RubyInstaller paths, `~/.gem`, XDG and verified `GEM_HOME`/`GEM_PATH`. The audit reads allowlisted gemrc and Bundler settings per context and emits findings `ruby-001`–`ruby-004`; credentials and URL userinfo are reported as configured only, and `~/.gem/credentials` is checked for existence only. Ruby, Bundler, gemspecs and plugins are never executed; YAML tags are never instantiated. - GitHub Copilot plugin inventory for the CLI, standalone app and VS Code. Collects recorded installations and live directory-marketplace selections, supplied skills, custom agents, command/rule/hook/LSP declarations, and sanitized plugin/user/project MCP definitions. Collection is file-only; uncertain formats report incomplete coverage. Runtime activation is not inferred. No Copilot usage or credential collection is added. +- Optional `device_id` in config.json for a stable custom device identity across telemetry, run cadence, hooks, and managed policies. The hardware serial remains separate, and changing identity resets cached cadence and incremental inventory. WSL guests continue deriving separate IDs from the host and distro. + +### Changed + +- **Releases go out as release candidates first.** A new `publish-prerelease.yml` workflow runs the full Verify Release suite and publishes the draft as a GitHub prerelease that is not marked latest, so agent-api can serve it to rc tenants (our own devices) before customers get it. `publish-release.yml` then promotes the same tag and bytes to latest. See `docs/release-process.md`. +- **The single-instance lock is an OS lock, not a PID file.** The PID lockfile could block every later run for good: a root run that died left a root-owned `0600` file in `/tmp` that user runs could neither read nor delete, a recycled PID or an `EPERM` counted as a live holder, anyone could write a PID into it to disable the agent, and SYSTEM and user runs on Windows never excluded each other. Unix now takes `flock` on `/tmp/stepsecurity-dev-machine-guard.flock` (a new name, so legacy PID files are ignored), opened read-only with mode `0644` so root and user runs can both hold it; Windows uses a `Global\StepSecurityDevMachineGuard` mutex. The kernel releases either on any exit, including `SIGKILL`. A refused run now resolves its serial before locking, so contention is reported as `lock_held` under a real device id instead of being rejected by the backend as an empty one. ### Fixed - **WSL distributions are scanned on installs that did not come from the MSI.** The WSL scan runs a Linux agent binary (`dmg-linux`) that must sit next to the Windows agent, and only the MSI put it there, so tenants who enabled WSL scanning got no WSL telemetry from PowerShell-loader or binary-only installs. When a running distribution is about to be scanned and no Linux agent is present, the Windows agent now downloads the `linux_` release asset for its own version from GitHub (the machine's native arch, so an emulated amd64 agent on Windows on Arm still gets `linux_arm64`) and installs it as `dmg-linux` only if its checksum carries a valid release signature (the same Ed25519 key and namespace the loaders verify). A copy installed by the MSI or a loader is used as is; a copy the agent installed is tracked by a `dmg-linux.fetched` marker and replaced when the agent version changes. The marker is claimed before the binary moves into place, so an interrupted install is finished on the next run instead of being mistaken for an installer's copy. The download is skipped when no distribution is running, is bounded to 90s inside the `wsl_scan` phase, and a failure only skips the WSL scan for that run. This adds `github.com` as an egress destination for the agent itself on WSL hosts. +- **One stable device ID across root and user runs.** The same machine could register under different IDs depending on who ran the agent, accept placeholder serials shared by unrelated machines, or report `unknown` after a failed probe. Identity is now resolved once per process and used consistently by telemetry, cadence, enforcers and WSL; a root or SYSTEM run publishes the machine-derived identity in a machine-wide file for user runs to read, a fallback probe never overwrites a stored identity, placeholder serials are rejected on every OS, and a generated UUID is persisted when every source fails. On Linux, where only root can read the DMI serial, a privileged run persists the serial only if root had already reported it, and otherwise persists `/etc/machine-id`, which every context can read, so root and user runs agree without coordination. Devices that were reporting placeholder serials intentionally get a new identity. A configured `device_id` still wins. +- **Scheduled scans stay registered and keep firing.** launchd upgrades ran `bootout` then `bootstrap` back to back, and launchd often answers 5 or 37 while the old job is still tearing down, which left the plist on disk with nothing loaded until the next login; the install now waits for the old job to disappear and retries. systemd user timers used a monotonic `OnUnitActiveSec` interval, which pauses in suspend and ignores `Persistent=true`, so a laptop awake an hour a day waited days per interval; the timer is now an `OnCalendar` schedule with `Persistent=true` and a 15-minute randomized delay. Task Scheduler tasks get an explicit `ExecutionTimeLimit` (the watchdog plus 30 minutes, or six hours) instead of the 72-hour default, the task is re-queried after import and a warning is logged if the battery flags or time limit did not stick, and UTF-16 XML without a BOM is decoded. The Windows launcher's job allows breakaway so a detached WSL guest scan is not killed when the host run ends. +- **A bad or locked file on disk no longer stops a device from reporting.** `config.json` is written atomically, and persisting the max execution duration no longer saves over a file that fails to parse — a truncated config was loaded as empty and written back, wiping `api_key`, after which every run exited "Enterprise configuration not found" without contacting the backend. `configure` backs up a corrupt file and rewrites it, `configure --from-file` merges into the existing config instead of replacing it (an MSI upgrade with `BOOTSTRAPFILE` was dropping an `AUTOUPDATE=0` opt-out), and a Windows rename that fails because the file is held open falls back to an in-place write. A full upload no longer aborts when the old `scan-state.json` cannot be deleted; the process-uploaded notify retries once on a transport error or 5xx; each S3 PUT attempt is capped at three minutes so a stalled attempt cannot consume the whole upload budget; a backwards clock change no longer makes the offline cache skip runs; and file logging survives a broken stderr. +- **Scans no longer hang past their deadline or crash on bad input.** Node, Python, MCP and config-audit directory walks now honor cancellation, so a huge or network-mounted home folder stops at the deadline instead of running until the watchdog kills it before upload; paths already in scan state still count as discovered when discovery is cut short, so delta does not report them as removed. Remote mounts are skipped on Linux (from `/proc/self/mountinfo`) and Windows (`GetDriveType`, plus OneDrive recall-on-access placeholders) unless `include_network_volumes: true`. The Windows command timeout now kills the process tree — it was a no-op, so a `.cmd` shim's grandchild holding stdout blocked the run until the watchdog — and each command runs in its own job object. A panic in one Node project's scan is recovered per project instead of crashing the run, the Windows watchdog posts the failed run-status before exiting, and native user-PATH lookups plus TTY-less children keep shell init from hanging the executor. +- **Windows device-policy state written by older agents is repaired, not rejected.** State files from 1.15 and 1.16 inherit their parent's DACL, so the strict metadata check refused them: reads reported no ownership and writes, writability probes and clears failed. Correctly owned state files, lock files and parent directories now have their DACL repaired under the existing state lock, with `READ_CONTROL|WRITE_DAC` only and all records preserved. A marker-based clear on Windows stops before touching configuration if the state store cannot be prepared. macOS and Linux are unchanged. +- **Homebrew installs of Pi, Copilot CLI and Amp are detected.** The `pi-coding-agent` formula ships a shell wrapper with no sibling manifest, the `copilot-cli` cask ships a quarantined binary that `spctl` rejects, and the vendor `ampcode` formula had no accept at all, so none produced an agent row. Each is now accepted from its exact keg or Caskroom layout with the version read from the manifest or the path segment; no binary is launched on any of the new paths, and `Cellar/amp` (the amp.rs editor) is still rejected. +- **Copilot CLI is found behind a rejected VS Code launcher.** VS Code 1.143.0 can put its Copilot launcher ahead of the real CLI on `PATH`; when that launcher could not find the CLI the detector rejected it and stopped looking. It now tries the remaining known Copilot locations after a candidate fails verification. A successful first candidate is reported exactly as before. +- **IDE settings device-policy validation** accepts the supported settings and the gallery URL format, and preserves existing extension rules and optional gallery configuration in both enforcement modes. - **macOS hostname no longer flips with the network.** The agent reported `kern.hostname`, which macOS rewrites from DHCP or reverse DNS whenever `HostName` is unset, so a Mac on a VPN resolving through AWS showed up as `ip-…ec2.internal` on some scans and its real name on others. It now reports the configured `HostName` when set, else `LocalHostName`, read from the SystemConfiguration preferences plist with `scutil --get` as the fallback, and only then the kernel hostname. Existing Macs will show a new name once. -## [1.18.0] - 2026-10-05 - -### Added - -- Optional `device_id` in config.json for a stable custom device identity across telemetry, run cadence, hooks, and managed policies. The hardware serial remains separate, and changing identity resets cached cadence and incremental inventory. WSL guests continue deriving separate IDs from the host and distro. - ## [1.17.0] - 2026-09-24 ### Added @@ -397,6 +410,7 @@ 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.18.0]: https://github.com/step-security/dev-machine-guard/compare/v1.17.0...v1.18.0 [1.17.0]: https://github.com/step-security/dev-machine-guard/compare/v1.16.0...v1.17.0 [1.16.0]: https://github.com/step-security/dev-machine-guard/compare/v1.15.0...v1.16.0 [1.15.0]: https://github.com/step-security/dev-machine-guard/compare/v1.14.0...v1.15.0 diff --git a/README.md b/README.md index ab2326ee..14a0f9c3 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The steps below install the binary directly and are intended for **community use ### Install from release (community) -Download the latest binary for your platform from [GitHub Releases](https://github.com/step-security/dev-machine-guard/releases). Release asset filenames embed the version (for example, `stepsecurity-dev-machine-guard-1.17.0-darwin`), so the snippets below resolve the latest tag first rather than hardcoding a version. +Download the latest binary for your platform from [GitHub Releases](https://github.com/step-security/dev-machine-guard/releases). Release asset filenames embed the version (for example, `stepsecurity-dev-machine-guard-1.18.0-darwin`), so the snippets below resolve the latest tag first rather than hardcoding a version. **macOS** (universal binary — Apple Silicon and Intel) diff --git a/examples/sample-output.json b/examples/sample-output.json index 5f8aacb4..5a903c76 100644 --- a/examples/sample-output.json +++ b/examples/sample-output.json @@ -1,5 +1,5 @@ { - "agent_version": "1.17.0", + "agent_version": "1.18.0", "scan_timestamp": 1741305600, "scan_timestamp_iso": "2026-03-07T00:00:00Z", "device": {