yuzu

windows_optional_features

What it doesWindows optional OS feature state (enabled/disabled/pending) via the DISM API
Version1.0.0
KindCollector · read-only · gathered (windows.features.list, windows.features.info)
PlatformsWindows ✅ · macOS ⛔ unsupported · Linux ⛔ unsupported
Actionsinfo (definition windows.features.info) · list (definition windows.features.list)
Securitysecurable Inventory · operation Read · risk Low · dispatch ReadOnly · approval gate None
Rolesexecute: endpoint-admin, endpoint-operator · author: content-author

How it works

The plugin reads which optional Windows features (Telnet client, TFTP, the IIS web server role, Virtual Machine Platform, .NET 3.5 and so on) are enabled, disabled or waiting on a restart, and changes nothing. list returns every feature with its state, optionally narrowed by a state filter; info returns one named feature with its display name, description and restart requirement. Both go through the Windows servicing API (DISM) in-process: no dism.exe, no WMI, no PowerShell.

flowchart LR
  OP["Operator or workflow"] --> SRV["Server, authz Inventory Read"]
  SRV -->|"gRPC over mTLS"| EX["windows_optional_features execute: list or info"]
  EX --> SLOT["Windows leg: resolve the DismApi.dll exports, then acquire the agent-core DISM slot"]
  SLOT -->|"acquired"| WIN["17 second bounded worker: DismInitialize, DismOpenSession, DismGetFeatures or DismGetFeatureInfo, DismCloseSession, DismShutdown"]
  SLOT -->|"exports missing, busy or abandoned"| SENT["sentinel row, status UNAVAILABLE or CONSTRAINED"]
  WIN --> ROWS["feature rows, status UNDECLARED on success, else a sentinel row with UNAVAILABLE, CONSTRAINED or PERMISSION_DENIED"]
  EX --> MAC["macOS leg: no OS call"]
  EX --> LIN["Linux leg: no OS call"]
  MAC & LIN --> UNS["unsupported row, status UNAVAILABLE"]
  ROWS & SENT & UNS --> RS[("ResponseStore")] --> API["REST /api/responses"]

Each call takes the agent’s single DISM slot on the dispatching thread, then runs the same lifecycle on a worker thread: DismInitialize, DismOpenSession on the running OS, DismGetFeatures (list) or DismGetFeatureInfo (info), map each state number to a name, DismCloseSession, DismShutdown, release the slot. The whole call is bounded to 17 seconds. DismApi.dll is never linked; its eight exports are resolved from System32 on first use. The slot, the session and the timeout all live in agent-core, which is never unloaded, so a worker that outlives a timeout never runs code belonging to a plugin that has been unloaded. Only one DISM call can be in flight in the agent at a time.

What it is not: no enable, disable, add, remove or commit (the eight resolved exports are the plugin’s whole DISM surface and none of them mutates), no dism.exe subprocess, and no use of Win32_OptionalFeature.

What you can decide with it

QuestionActionHow to read it
Is a risky legacy feature switched on (for example SMB1Protocol or TelnetClient)?list, or info with feature=<name>state is enabled
How many feature rows are in each state across the fleet?list, grouped by statelist aggregates by state and counts feature rows. To count devices that have one named feature enabled, filter the name column (for example SMB1Protocol) and state=enabled
Is a feature change waiting for a restart?list with state=pending, or infopending_enable or pending_disable, and restart_required is 1 on list
What is this feature, and will changing it need a restart?infodisplay_name, description, and restart_type of no, possible, required or unknown

On Windows you get the answer from the running OS’s own servicing store. On macOS and Linux you get a single unsupported row and nothing else (a malformed feature on info is still refused first, with an invalid row). A clean read proves what DISM reports as the configured state; it does not prove the feature’s binaries are running, healthy or in use. disabled does not distinguish a feature whose payload is staged on disk from one that is removed (see caveat 4), and list says nothing about installed applications, MSI packages or patches (see the sibling plugins below).

Diagrams

The behaviour that matters operationally is the slot: what a second caller, or a call after a timeout, sees.

sequenceDiagram
  participant D as Dispatching thread
  participant S as DISM slot in agent-core
  participant W as Bounded worker in agent-core
  participant API as DISM API
  D->>S: try to acquire
  alt a call is running
    S-->>D: busy, row windows:dism:busy
  else a timed-out call has not returned
    S-->>D: abandoned, row windows:dism:abandoned
  else free
    D->>W: start the bounded call, 17 second limit
    W->>API: Initialize, OpenSession, GetFeatures or GetFeatureInfo
    API-->>W: features or an HRESULT
    W->>API: CloseSession, Shutdown
    W->>S: release the slot
    W-->>D: rows or a failure outcome
  end

A caller that finds the slot taken never touches DISM; it reports busy or abandoned and returns. The slot is released by the worker after DISM teardown, so the next caller never overlaps a half-closed session. The only exception is a call the agent-wide bounded-call ceiling refuses before it starts (64 outstanding calls): it never reaches DISM, the dispatching thread releases the slot, and the row is windows:dism:busy.

stateDiagram-v2
  [*] --> Free
  Free --> Busy: caller acquires
  Busy --> Free: worker finishes and releases
  Busy --> TimedOut: 17 seconds pass with no result
  TimedOut --> Free: worker finally returns and releases
  note right of TimedOut
    new callers see abandoned
    cleared only when DISM returns
  end note

After a timeout the dispatching thread reports windows:dism:timeout and moves the slot to TimedOut; every later call reports abandoned until the original worker returns. If DISM never returns, only an agent service restart clears it.

OS capability

ActionWindowsmacOSLinux
info✅ supported · rung 1 · DISM API DismOpenSession(DISM_ONLINE_IMAGE) + DismGetFeatures/DismGetFeatureInfo⛔ unsupported⛔ unsupported
list✅ supported · rung 1 · DISM API DismOpenSession(DISM_ONLINE_IMAGE) + DismGetFeatures/DismGetFeatureInfo⛔ unsupported⛔ unsupported

Declared limits per leg (descriptor fallback text, verbatim):

Privileges and prerequisites

OSRuns asExtra grant neededMeasuredIf the read is refused
Windowsagent service account (LocalSystem today, #1442)Elevated token. The DISM API refuses an unprivileged caller (E_ACCESSDENIED); LocalSystem already satisfies it, so no new grant is needed under the current deployment model.2026-09-08, bare-metal, NT AUTHORITY\SYSTEM (S4U scheduled task)rows report feature|unavailable|windows:dism:access_denied / feature_info|unavailable|windows:dism:access_denied; result status PERMISSION_DENIED
macOSagent daemon, root today (the LaunchDaemon carries no UserName key, see docs/agent-privilege-model.md)n/a — action returns the honest-unsupported sentinel unconditionallynot applicable (Windows-only plugin)n/a — no DISM call is attempted
Linuxagent daemon, unprivilegedn/a — action returns the honest-unsupported sentinel unconditionallynot applicable (Windows-only plugin)n/a — no DISM call is attempted

No subprocesses and no network access. DismApi.dll is resolved once, on first use, via LoadLibraryExW(..., LOAD_LIBRARY_SEARCH_SYSTEM32) + GetProcAddress against eight exports, never DismApi.lib/#include <dismapi.h> — no Windows ADK dependency (capture and provenance: tests/unit/fixtures/wave9/windows_optional_features/dism_features_system.txt.provenance.txt).

Data contract

Inputs

DefinitionParameterTypeRequiredDefaultConstraintsDescription
windows.features.infofeaturestringyes-pattern: ^[A-Za-z0-9._-]{1,256}$The DISM feature name (FeatureName), e.g. NetFx3. The plugin additionally refuses any value containing a ”..” substring (path-traversal guard) even though every individual character in such a value would otherwise match the pattern below.
windows.features.liststatestringno-enum: enabled, disabled, pendingOptional filter narrowing the result to one state bucket.

Outputs

Pipe-delimited rows via write_output(). list writes one feature| row per feature (or per matching feature when state filters; an unrecognised filter value is treated as no filter); info writes exactly one feature_info| row. An invalid feature value is refused before any DISM call with feature_info|invalid|invalid 'feature' parameter: '<value>' and exit code 1. On list, restart_required is 1 only for pending_enable and pending_disable; info reports DISM’s own restart type. On an unsupported or unavailable leg, both actions instead write a single sentinel row: feature|unsupported|<os>:dism:unsupported / feature|unavailable|windows:dism:<token> (or the feature_info equivalent for info) — never a blank or omitted row.

windows.features.info — name|display_name|state|restart_type|description

FieldTypeValuesAvailableExampleDescription
namestring-WindowsNetFx3DISM feature name (FeatureName), e.g. NetFx3.
display_namestring-Windows.NET Framework 3.5 (includes .NET 2.0 and 3.0)Operator-visible feature display name (DismFeatureInfo.DisplayName).
statestring-WindowsenabledFeature state. Values: enabled, disabled, pending_enable, pending_disable, superseded, partially_installed, unknown.
restart_typestring-WindowspossibleRestart requirement for a pending change (DismFeatureInfo.RestartRequired). Values: no, possible, required, unknown.
descriptionstring-Windows.NET Framework 3.5 (includes .NET 2.0 and 3.0)Feature description (DismFeatureInfo.Description).

windows.features.list — name|state|restart_required

FieldTypeValuesAvailableExampleDescription
namestring-WindowsNetFx3DISM feature name (FeatureName), e.g. NetFx3.
statestring-WindowsenabledFeature state. Values: enabled, disabled, pending_enable, pending_disable, superseded, partially_installed, unknown.
restart_requiredboolean-Windows0Whether a restart is needed to complete a pending enable/disable. Values: 1 (pending state), 0 (not pending) — a digit, never true/false.

Result status

A failed or constrained read still exits 0 (only an invalid parameter or an unhandled exception exits 1), so a consumer must key on the typed status, not the exit code. The windows:dism:* tokens below are carried in the sentinel row (feature|unavailable|<token>); the status reason is human text, and for a DISM failure it is hr=0x<code> plus DISM’s last error message.

StatusCompletenessProvenanceWhen
UNDECLARED (agent-derived OK)——a clean list/info read — the DISM call succeeded and the feature row(s) were written
UNAVAILABLEPARTIALwindows:dism:api_unavailableDismApi.dll or one of its eight required exports could not be resolved from System32
UNAVAILABLEPARTIALlinux:dism:unsupported, macos:dism:unsupportedLinux/macOS: DISM has no equivalent on this platform
UNAVAILABLEFULLwindows:dism:feature_not_foundinfo: the named feature does not exist (DismGetFeatureInfo returns DISMAPI_E_UNKNOWN_FEATURE)
CONSTRAINEDPARTIALwindows:dism:busyanother DISM call already holds the plugin’s in-process slot, or the bounded-call outstanding-call ceiling rejected this call before it started
CONSTRAINEDPARTIALwindows:dism:abandoneda previous call timed out and its worker has not returned; the DISM API is still held by that worker until it does
CONSTRAINEDPARTIALwindows:dism:timeoutDismGetFeatures/DismGetFeatureInfo did not return within the 17s bounded timeout
CONSTRAINEDPARTIALwindows:dism:initialize_failed, windows:dism:open_session_failed, windows:dism:get_features_failed, windows:dism:get_feature_info_faileda DISM session-lifecycle call failed with a non-access-denied HRESULT
CONSTRAINEDPARTIALwindows:dism:exceptionan unhandled exception was caught inside the bounded DISM call
PERMISSION_DENIEDPARTIALwindows:dism:access_denieda DISM call in the session returned E_ACCESSDENIED — the caller’s token lacks the elevated privilege the DISM API requires

Where the data goes

Sample output

Windows — captured: windows Windows 10.0.26200 x86_64 · bare-metal · 2026-09-08 · LocalSystem (elevated) · leg-hash f85a83ef6582

== action=list
feature|Printing-XPSServices-Features|enabled|0
feature|TelnetClient|disabled|0
feature|TFTP|disabled|0
feature|TIFFIFilter|disabled|0
feature|VirtualMachinePlatform|enabled|0
feature|Client-ProjFS|disabled|0
feature|SimpleTCP|disabled|0
feature|WorkFolders-Client|enabled|0
feature|NetFx3|enabled|0
feature|WCF-HTTP-Activation|disabled|0
feature|WCF-NonHTTP-Activation|disabled|0
feature|IIS-WebServerRole|disabled|0
… 12 of 137 rows shown
[result_status] UNDECLARED / UNKNOWN

== action=info feature=NetFx3
feature_info|NetFx3|.NET Framework 3.5 (includes .NET 2.0 and 3.0)|enabled|possible|.NET Framework 3.5 (includes .NET 2.0 and 3.0)
[result_status] UNDECLARED / UNKNOWN

Caveats and known gaps

  1. DISM is process-global and single-session. The DISM API cannot safely run concurrent sessions from one process, so every list or info call is serialised through agent-core’s DismSlot. A second caller receives windows:dism:busy rather than risking undefined behaviour, and a call rejected by the bounded-call outstanding-call ceiling is reported the same way.
  2. A timed-out call is abandoned, not cleared, until the worker returns. If DismGetFeatures or DismGetFeatureInfo exceeds the 17 second limit, the dispatching thread reports windows:dism:timeout and marks the slot abandoned. Every later call reports windows:dism:abandoned until the original worker finishes, which is permanent if DISM itself never returns and is cleared only by a service restart.
  3. Pending states come from the feature enum, not a reboot-pending registry key. pending_enable and pending_disable reflect DISM’s InstallPending and UninstallPending values, and list sets restart_required to 1 only for those two states. info reports DISM’s own restart_type, so an enabled feature can show restart_required 0 on list and possible on info.
  4. Feature names are DISM’s internal names, and disabled covers three states. NetFx3, SMB1Protocol and TelnetClient are the FeatureName values DISM uses, not Control Panel labels; info carries the operator-visible display_name. The disabled state also covers features whose payload is staged or removed, which the plugin does not tell apart.
  5. The macOS and Linux rows are placeholders by design. DISM has no equivalent on either platform, so both legs return the unsupported row unconditionally, never an empty read or a fabricated value.

Source and tests