Skip to content

feat(developer-mdm): server-driven scan cadence with per-invocation run gating - #179

Open
shubham-stepsecurity wants to merge 2 commits into
step-security:mainfrom
shubham-stepsecurity:sm/run-gating
Open

feat(developer-mdm): server-driven scan cadence with per-invocation run gating#179
shubham-stepsecurity wants to merge 2 commits into
step-security:mainfrom
shubham-stepsecurity:sm/run-gating

Conversation

@shubham-stepsecurity

Copy link
Copy Markdown
Member

What does this PR do?

Type of change

  • Bug fix
  • Enhancement
  • Documentation

Testing

  • Tested on macOS (version: ___)
  • 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
  • Lint passes: make lint
  • Tests pass: make test

Related Issues

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.

Pull request overview

Adds an enterprise “run gate” mechanism that checks in with the backend for a server-driven scan cadence, allowing the agent to exit 0 early when a run isn’t due while maintaining a fail-open contract and an offline cached-interval fallback. This fits into the enterprise telemetry path by gating send-telemetry runs before expensive collection/upload and persisting minimal state alongside the existing heartbeat file.

Changes:

  • Introduces internal/rungate (check-in client, decision logic, state cache) and integrates gating into enterprise execution paths in main.go.
  • Extends last-run.json (heartbeat) to also store run-gate cache, and stamps “last full run” after successful telemetry completion.
  • Adds the --force-scan bypass flag and documents the new behavior in README/CHANGELOG.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
README.md Documents --force-scan and related env vars for enterprise run gating.
CHANGELOG.md Adds an Unreleased entry describing server-driven scan cadence (needs wording alignment with implementation).
cmd/stepsecurity-dev-machine-guard/main.go Calls the run gate before enterprise telemetry runs (including inline install scan).
internal/cli/cli.go Adds ForceScan config + --force-scan parsing (help/tests need follow-up).
internal/device/device.go Adds a lightweight SerialNumber accessor for pre-scan device-id resolution.
internal/heartbeat/heartbeat.go Extends last-run.json to preserve run-gate cache via read-modify-write.
internal/heartbeat/heartbeat_test.go Tests the new heartbeat + run-gate merge behavior.
internal/paths/paths.go Documents last-run.json as shared heartbeat + run-gate state storage.
internal/telemetry/telemetry.go Echoes gate proceed reason into captured logs and stamps last-full-run on completion.
internal/rungate/client.go Implements backend check-in + best-effort skip beacon.
internal/rungate/client_test.go Tests check-in request/response handling and error paths.
internal/rungate/decide.go Implements precedence and offline cached-interval fallback decision logic.
internal/rungate/decide_test.go Tests decision precedence and offline fallback behavior.
internal/rungate/directive.go Defines the directive wire shape (docs need to match the actual endpoint used).
internal/rungate/evaluate.go Orchestrates the gating flow (env access + logging level choices need adjustment).
internal/rungate/state.go Adapts run-gate persistence to the shared heartbeat file.
internal/rungate/state_test.go Tests shared-file invariants and corruption/future-schema behavior.
Suppressed comments (2)

internal/rungate/evaluate.go:74

  • Run-gate intermediate logging uses Progress level, which contradicts the stated goal of a single visible skip/proceed line (main.go comment + changelog) and will add extra log noise on every scheduler wakeup. Consider downgrading the “checking cadence”, directive details, and check-in-failure lines to Debug so only gateSkipsRun emits the single user-facing Progress line.
	log.Progress("Run gate: checking scan cadence with the dashboard...")
	directive, err := Checkin(ctx, config.APIEndpoint, config.APIKey, config.CustomerID, deviceID, st.LastFullRunAt)
	if err != nil {
		log.Progress("Run gate: dashboard check-in failed, using cached cadence: %v", err)
	} else {

internal/cli/cli.go:302

  • --force-scan is added to parsing but is not included in the CLI help text (printHelp Options list), so users running --help won’t discover it unless they read the README. Add it to the help output alongside other top-level flags.
		case arg == "--force-scan":
			cfg.ForceScan = true

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +37 to +42
func Evaluate(ctx context.Context, exec executor.Executor, log *progress.Logger, forceScan bool) Result {
in := Inputs{
ForceScan: forceScan || os.Getenv("STEPSEC_FORCE_SCAN") == "1",
KillSwitch: os.Getenv("STEPSEC_DISABLE_RUN_GATE") == "1",
Now: time.Now(),
}
Comment on lines +48 to +53
if in.ForceScan || in.KillSwitch {
if in.ForceScan {
log.Progress("Run gate: bypassed (--force-scan)")
}
return Result{Skip: false, Reason: Decide(in).Reason}
}
Comment on lines +1 to +17
// Package rungate implements the server-driven run gate: on every invocation
// the agent asks the backend's run-directive endpoint whether a full scan is
// due and exits quietly when it isn't. The scan cadence lives in the backend
// (per tenant, with temporary overrides and per-device refresh), so customers
// point their MDM/scheduler at a simple hourly launch and control the real
// frequency from the dashboard.
//
// Every failure path fails OPEN (the scan runs): a tenant that never opted
// in, an unreachable backend, a malformed response, an unresolvable device
// id, or unusable local state must never suppress scanning. The only
// deliberate skips are a backend "skip" directive, the offline cached-interval
// fallback, and the quiet back-off while another instance holds the lock.
package rungate

// Wire contract for GET /developer-mdm-agent/run-directive. Mode and reason
// strings are wire-permanent and mirrored by the backend's
// run_directive_handler.go.
Comment thread CHANGELOG.md

### Added

- **Server-driven scan cadence (run gating)**: on every invocation the agent asks the backend's new `run-directive` endpoint whether a full scan is due and exits quietly when it isn't, with no run-status row, no phases, and a single log line. The scan frequency lives in the StepSecurity dashboard (per tenant, minutes granularity, with a temporary override that auto-reverts at a set time and a per-device "re-scan now" request), so fleets deployed via an external MDM (for example JAMF's hourly cadence) get their real cadence from the backend with no MDM scheduling changes. Run gating is controlled entirely from the backend: the agent always makes the check-in call, and the backend decides. It is gated by a per-environment backend flag so it can be enabled for one environment at a time; while the flag is off the backend answers "scan" every time and the agent behaves exactly as before. Once enabled, gating applies to every device (a 4-hour default when a tenant has not set its own cadence). Any check-in failure fails open to a scan (with a cached-interval fallback so offline machines don't scan every wakeup), and an invocation that lands while another scan is running backs off quietly instead of reporting a failed run. Bypass for debugging: `--force-scan` / `STEPSEC_FORCE_SCAN=1`; per-device kill switch: `STEPSEC_DISABLE_RUN_GATE=1`.
Comment thread internal/cli/cli.go
Comment on lines 299 to +302
case arg == "--override-gate":
cfg.OverrideGate = true
case arg == "--force-scan":
cfg.ForceScan = true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants