Skip to content

fix(device): one stable device ID across root and user runs - #243

Merged
ashishkurmi merged 17 commits into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/dmg-device-id-resolver
Oct 17, 2026
Merged

ashishkurmi merged 17 commits into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/dmg-device-id-resolver

Conversation

@swarit-stepsecurity

@swarit-stepsecurity swarit-stepsecurity commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Builds on #236 (agent-configured device IDs), which is reopened. This PR includes its original configuration feature; review #236 first. The temporary central-management reconciliation has been reverted.

The same machine could register under different IDs across root and user runs, accept placeholder serials shared by unrelated machines, or report unknown after failed probes.

  • Resolve machine identity once per process and use it consistently across telemetry, cadence, enforcers and WSL.
  • Root/SYSTEM publishes the machine-derived identity in a machine-wide file for user runs. A fallback probe does not replace a stored identity. Explicit config.json device_id still takes precedence.
  • Reject placeholder serials on every OS; persist a generated UUID when every source fails.
  • Revalidate run-gate identity every run and bind cadence/incremental state to that identity.
  • Use the bounded IOPlatformExpertDevice probe on macOS.

Devices currently reporting invalid placeholder serials intentionally get a new identity. Configured IDs must be unique within the tenant; there is no backend ownership preflight or central rename mechanism. Changing a configured ID creates a separate registration.

#246 builds on this resolver for VDI support. #247 is withdrawn.

Validation: original resolver/configuration regression coverage restored by reverting the central-management compatibility change. Diff reviewed; regular CI, smoke and cross-platform checks are left to CI. No new real root/SYSTEM/multi-user validation is claimed.

…evice-id-resolver

# Conflicts:
#	CHANGELOG.md
#	internal/device/device.go
#	internal/device/device_test.go
Firmware defaults like "Default string", "Not Specified", "System Serial
Number" or all-zeros merged distinct machines into one device_id. A shared
case-insensitive denylist now makes every probe fall through to the next
source.
ioreg -l dumps the whole I/O registry (several MB) and can blow the hooks'
1s budget on a busy Mac; -rd1 -c IOPlatformExpertDevice returns the same
IOPlatformSerialNumber line ~10x faster.
Telemetry, the run gate, enforcers and hooks each probed the serial with
their own budget and could disagree within one run. Gather and SerialNumber
now share a process-wide cached result; failures aren't cached so a later
caller with a longer budget retries.
Linux product_serial is root-only and dmidecode needs root, so root runs
reported the hardware serial while user runs (systemd --user, hooks) fell to
/etc/machine-id: one machine, two devices. Privileged runs now persist the
resolved ID atomically to /etc/stepsecurity/device-id (0644) or
C:\ProgramData\StepSecurity\device-id, and unprivileged runs read it before
probing. Invalid file content is ignored.
When every source failed, the literal "unknown" was uploaded as device_id,
merging all such machines. Now a random UUID is generated and persisted
(machine-wide when privileged, else the per-user state dir) and reused on
later runs. A probe cut short by its deadline mints nothing; device.ID
returns "" and telemetry retries on the run deadline or fails the run
instead of uploading without an identity (replacing a dead empty check).
The gate reused the device_id cached in last-run.json and never re-probed,
so after an identity change it gated as one device while telemetry uploaded
as another. It now always asks the process-wide resolver; the existing
mismatch reset then discards the stale cadence.
The event ingest contract for an empty device_id is unverified; keep the
previous behavior for hooks.
Comment thread internal/executor/executor.go Fixed
Comment thread internal/executor/executor.go Fixed
@ashishkurmi
ashishkurmi requested a balanced review from Copilot October 14, 2026 18:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Persistence failures, concurrent initialization, and transient probe fallbacks can still create inconsistent device identities.

3 open findings
What changed in this PR

Centralizes device identity across telemetry, hooks, policies, and WSL, building on configurable IDs to keep root and user runs consistent.

Changes:

  • Adds shared identity persistence, placeholder filtering, and UUID fallback.
  • Resets cadence and inventory caches when identity changes.
  • Adds regression coverage and documents configurable IDs.
File Description
README.md Documents custom device IDs.
internal/​telemetry/​wsl_scan.go Uses effective host identity.
internal/​telemetry/​telemetry.go Unifies identity and cache binding.
internal/​state/​state.go Binds inventory to device identity.
internal/​state/​device_id_test.go Tests identity-driven inventory resets.
internal/​rungate/​state.go Resets cadence on identity changes.
internal/​rungate/​state_test.go Tests identity-bound completion stamps.
internal/​rungate/​evaluate.go Revalidates identity each run.
internal/​rungate/​evaluate_test.go Tests overrides and stale identities.
internal/​heartbeat/​heartbeat.go Records configured identity.
internal/​executor/​write_test.go Tests atomic file replacement.
internal/​executor/​mock.go Adds mock identity persistence.
internal/​executor/​executor.go Adds atomic file writing.
internal/​device/​serial.go Rejects placeholder serials.
internal/​device/​serial_test.go Tests placeholder filtering.
internal/​device/​resources_test.go Updates identity-related test setup.
internal/​device/​id.go Resolves and persists shared identities.
internal/​device/​id_test.go Tests persistence and fallback behavior.
internal/​device/​device.go Centralizes identity caching and selection.
internal/​device/​device_windows.go Filters Windows placeholder values.
internal/​device/​device_test.go Tests configured and guest identities.
internal/​device/​device_other.go Filters fallback Windows probe values.
internal/​config/​config.go Supports device ID configuration.
internal/​config/​config_test.go Tests identity configuration persistence.
internal/​config/​config_nonint_test.go Tests file-supplied device IDs.
internal/​buildinfo/​version.go Bumps version to 1.18.0.
internal/​aiagents/​identity/​identity.go Uses effective identity for hooks.
internal/​aiagents/​identity/​identity_test.go Tests hook identity fallback behavior.
cmd/​stepsecurity-dev-machine-guard/​main.go Shares identity with policy consumers.
cmd/​stepsecurity-dev-machine-guard/​main_devicepolicy_test.go Tests WSL policy identity.
CHANGELOG.md Records configurable identity support.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/device/id.go Outdated
Comment on lines +48 to +50
if id := probeSerial(ctx, exec); usableSerial(id) {
if root && readIDFile(exec, path) != id {
writeIDFile(exec, path, id)
Comment thread internal/device/id.go Outdated
Comment on lines +73 to +77
id := uuid.NewString()
if path != "" {
writeIDFile(exec, path, id)
}
return id
Comment thread internal/device/id.go Outdated
Comment on lines +73 to +77
id := uuid.NewString()
if path != "" {
writeIDFile(exec, path, id)
}
return id
…ted generated IDs

A probe that falls back to MachineGuid or /etc/machine-id no longer overwrites
the machine-wide ID. Generated UUIDs are published create-once, so racing
processes converge on one ID, and an ID that can't be persisted reports
unknown instead of an ephemeral identity.
@ashishkurmi
ashishkurmi added this pull request to the merge queue Oct 17, 2026
Merged via the queue into step-security:main with commit cb14706 Oct 17, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants