Skip to content

Resolved #307 Resolved #308 - Updated documentation based on implementation docs and clarified when an item should be created#310

Open
vanderpol wants to merge 4 commits into
5.12.3_developfrom
307-update-windows-ntuser-documentation-based-on-original-specificationimplemention-documents
Open

Resolved #307 Resolved #308 - Updated documentation based on implementation docs and clarified when an item should be created#310
vanderpol wants to merge 4 commits into
5.12.3_developfrom
307-update-windows-ntuser-documentation-based-on-original-specificationimplemention-documents

Conversation

@vanderpol
Copy link
Copy Markdown
Member

Updated docs to clear document how to go about determining which ntuser.dat files should be included, and when to create a ntuser_item

…tation docs and clarified when an item should be created
@vanderpol vanderpol added this to the 5.12.3 milestone May 8, 2026
@vanderpol vanderpol requested review from A-Biggs and solind May 8, 2026 15:55
@vanderpol vanderpol self-assigned this May 8, 2026
@vanderpol vanderpol changed the base branch from master to 5.12.3_develop May 8, 2026 18:21
Copy link
Copy Markdown

@solind solind left a comment

Choose a reason for hiding this comment

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

I'm not quite sure about these documentation changes. In particular, those that infer something about the existence of ntuser_objects based on state entities.

<xsd:element name="name" type="oval-def:EntityStateStringType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>This element describes the name of a value of a registry key.</xsd:documentation>
<xsd:documentation>Note: The name not existing on the target does not impact the overall existence of the ntuser_item.</xsd:documentation>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If there is an item with a corresponding name, the object will be said to exist.

Anyway, this is documentation for the state entity. If the state specifies a name that doesn't match any item, then no items will match that state. The effect on existence comes into play only if the state is used to filter an object or set.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks Dave, and yes this is likely a copy/paste issue from the item, I'll clear it up. Any discussions on existence should only be in the item documentation.

<xsd:element name="logged_on" type="oval-def:EntityStateBoolType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>The logged_on element describes if the user account is currently logged on to the computer.</xsd:documentation>
<xsd:documentation>This can be determined by comparing the SID’s against those populated in HKEY_USERS</xsd:documentation>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The active user logon sessions can also be determined using the WMI classes Win32_LoggedOnUser and Win32_LogonSession. I'm not sure whether the presence of a hive under HKEY_USERS is completely reliable for the purpose of whether the user should actually be considered "logged on".

I recall we considered interactive and remote interactive session types to be logged on.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@solind good points, I'll make some updates. Our rationale for uisng HKEY_USERS was two fold, if a user is logged in, their ntuser.dat file is locked, and the only way to get the data is to find it via HKEY_USERS<SID>. For 'interactive' and 'remote interactive', I assume that means 'local' and 'remote desktop'? If not, please elaborate. I may have to ponder if these keys are populated for remote ssh/winRm type sessions, as thay may not be.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Here is a link to the documentation for that WMI class: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logonsession

Interactive should include WinRM sessions, whereas remote interactive means a Windows terminal service session. Remember Windows terminal services? Is that still even a thing? Somewhere, probably...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In reading the documentation for Win32_LoggedOnUser and Win32_LogonSession, it would appear to me that those are only to obtain data about the session that the application is running in, not to obtain information listing all currently logged in users.

The Win32_LoggedOnUser association WMI class relates a session and a user account.

The Win32_LogonSession WMI class (see Retrieving a WMI class) describes the logon session or sessions associated with a user logged on to a computer system running Windows.

If you have concerns about 'mandating' our method we can make it clear that it is just a recommendation not a requirement. It's what we have been using for years, and just wanted to help new vendos not have to reinvent the wheel to find the data.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

it would appear to me that those are only to obtain data about the session that the application is running in, not to obtain information listing all currently logged in users

I don't know exactly what you mean by this. Application? Win32_LogonSession definitely "describes the logon session or sessions associated with a user logged on to a computer system running Windows", and Win32_LoggedOnUser "relates a session and a user account". It's the WMI way to determine who's currently logged in to a machine. The keys loaded under HKU may tell you the same information, but I never found any documentation stating that conclusively.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

OK, I'll test it out internally and then update documentation to list either method.

Copy link
Copy Markdown
Member Author

@vanderpol vanderpol May 13, 2026

Choose a reason for hiding this comment

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

I logged into the same Windows Server via RDP with 2 remote users, and a another user was logged in at the console. So there are 3 users concurrently logged into the system.

user1: When running Get-WmiObject Win32_LoggedOnUser | Select-Object -Unique Antecedent

  • Results back are name="user1"
    user2: When running Get-WmiObject Win32_LoggedOnUser | Select-Object -Unique Antecedent
  • Results back are name="user2"

So it would appear from my testing that Win32_LoggedOnUser only returns data for the current user, not all users on the system.

When testing Get-WmiObject Win32_LogonSession, I see that there are 3 logins, but I do not see any straight forward way to determine the username or SID from any of the logged in sessions. I get AuthenticationPackage, LogonID (which is not the SID), LogonType

In searching online, it seems that the quickest/easiest way to get the logged in users is to run 'quser', which returns back the username, but it doesn't seem entirely clear if they are local vs doman users, so doing a SID lookup might be tricky.

Back to HKEY_USERS, Microsoft documents it pretty well:
HKEY_USERS | Contains all the actively loaded user profiles on the computer.
https://learn.microsoft.com/en-us/troubleshoot/windows-server/performance/windows-registry-advanced-users

I can add this documentation to the methods to make it clear it is a documented method, not just our hack. Given that at least from our testing, reading the data from HKEY_USERS/<SID> is the only way to get to the data for logged on users, determining if the user is logged on from the same data seems straight forward.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's actually not that simple. See this StackExchange question, particularly the answer(s) by mjolinor.

<xsd:element name="days_since_last_logon" type="oval-def:EntityStateIntType" minOccurs="0" maxOccurs="1">
<xsd:annotation>
<xsd:documentation>The last_logon data, converted to days and then rounded down to the nearest integer (floor function). If the account is determined to be currently logged in, this date should be reported as 0.</xsd:documentation>
<xsd:documentation>The last_logon data which can be obtained from the LocalProfileLoadTimeHigh and LocalProfileLoadTimeLow values, converted to days and then rounded down to the nearest integer (floor function). If the account is determined to be currently logged in, this date should be reported as 0.</xsd:documentation>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Where are these obtained from? Registry values somewhere under HKLM?

Data can also be obtained from the WMI table Win32_NetworkLoginProfile.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@solind I'll update the documentation to list either the currently logged in users ntuser.dat file or Win32_NetworkLoginProfile.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In reading the Win32_NeworkLoginProfile documentation, I'm not confident that it contains the same data. I'm updating documentation with clear information on how to get the data, and a reference to the Micrsoft documentation: https://learn.microsoft.com/en-us/troubleshoot/windows-server/support-tools/scripts-to-retrieve-profile-age

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's hard to find documented means to determine some of this information, but Win32_NetworkLoginProfile "represents the network login information of a specific user on a computer system running Windows". It has datetime properties for LastLogon and LastLogoff.

Is the "profile age" really the same thing? I don't know. What happens if a user logs on when the machine is not connected to the domain? Is the profile age updated? Or is it updated only when group policy updates it on user logon?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We are looking for the last time the logged into this specific computer and profile, which is what the LocalProfileLoadTimeHigh data provides. Any time the profile is loaded it's updated. The LastLogon and LastLogoff could be anytime that user logged into any computer in the domain? I will have to test if the LocalProfileLoadTimeHigh is updated even when the system is disconnected from the network. More discussions to follow.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In my limited research, it appears that Win32_NetworkLoginProfile is tied to the last time you have logged into ActiveDirectory. Here was my test:

  1. I was logged in from quite some time ago
  2. I disconnected my ethernet cable
  3. I logged off and back into my computer
  4. I ran Get-CimInstance -ClassName Win32_NetworkLoginProfile | Select-Object Name, LastLogon,
  5. The data returns was 4/20/2026
  6. I then reconnected my ethernet and ran the same command
  7. Data was now correct with today datetime listed

At the same time, I exported off the HKLM\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\ registry key as text, and exported again after logging in without eithernet, and saw that the data changed with each login/logoff.

And given that my current documentation matches the original pull request R&D, (which should have included this documentation) I feel pretty strongly that documenting the LocalProfileLoadTimeHigh method is 'correct'

#221

<xsd:element name="enabled" type="oval-def:EntityStateBoolType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>The enabled element describes if the user account is enabled or disabled.</xsd:documentation>
<xsd:documentation>Note: For domain users, if a domain controller is not available, this will not return data. If using this data for a filter to include enabled accounts, it’s recommended to exclude accounts that are have been determined to be disabled, vs including ones that are enabled, as the later may filter out accounts for which the domain controller could not return data.</xsd:documentation>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we'd have to say that the element would be status="not_collected" (IIRC) if the domain server is unavailable, which (for good or ill) has documented evaluation effects. Or, it could have an error status.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, will update.

<xsd:element name="filepath" type="oval-def:EntityStateStringType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>This element describes the filepath of the ntuser.dat file.</xsd:documentation>
<xsd:documentation>The existance of each ntuser.dat file determines the overall ntuser_item existence.</xsd:documentation>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I hate to get into semantics, but I think you mean to be talking about the existence of the object, not the item. The object exists if all its criteria are satisfied by one or more items discovered on the target machine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this overlaps with your next comment, so I'll consolidate our discussions there.

<xsd:documentation> iii. Include Local and Domain User SIDs by including subkeys match the format of S-1-5-21-&lt;number*gt;-&lt;number*gt;-&lt;number*gt;-&lt;number*gt;</xsd:documentation>
<xsd:documentation> c. Obtain ntuser filepath from the ProfileImagePath value of 'human' profiles</xsd:documentation>
<xsd:documentation>2. Creating ntuser items</xsd:documentation>
<xsd:documentation>If the filepath obtained from the ProfileImagePath exists on the target system, create a ntuser_item with a status of 'exists'</xsd:documentation>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is basically saying that all NTUSER objects always exist, assuming there is at least one user profile on the target machine (there should always be at least one). I don't think that's right, but... it's been a long time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@solind this is definitely the key point of discussion for all of these updates, and the primary point of concern about consistency across products. Somehow we need to have a way to accurately perform checks for N ntuser.dat files. Our team feels that one key documentation item that is lacking from OVAL, and is on our list of planned updates is to document when an item should be created. Most are obvious and intuitive, but others are a bit more open for discussion/debate.

OVAL also doesn't handle N instances very well with existence. The ntuser test and TextFileContent54 and XMLFileContents all suffer from this. If you need to confirm that "ALL" instances are configured correctly, but the matching pattern (textfilecontent54 and xmlcontents) do not exist in a given file, then they are excluded from comparison, which works OK if there is only ONE file in scope, but if N files are in scope and 1 files has the required pattern, and is configured correctly and say 10 other file exist that should be configured do not have the requisite pattern, then the test passes, but should fail. The same issue exist with the NTUSER test due to the N ntuser items issue. If there are for simplicity's sake 2 users on the system, and one user has the required registry key/value and matches the state requirement and passes the test, but there is another user profile that does not have the required key or name, then in some OVAL interpreters, no item is created, and then the overall test passes, but it should fail because one of the nuser files is incorrectly configured.

From my view, we have 2 options:

  1. Mandate the creation of 'does not exist' items if they ntuser filepath exists, but the required key/value does not exist.
  2. Mandate the creation of an item with a status of 'exists', documenting that the key to existence is the 'filepath'.

What we did historically was to create items with a status of 'does not exist', which made the results accurate, but goes against the norms of OVAL which says to only create 'does not exist' items for debugging purposes. You can see an example in the attached.

I'm open to any other suggestions you may have, but some changes is needed, as some of our end users have reported different results from our SCC tool to Trellix Policy Auditor, as Trellix does not create items ('exist' or 'does not exist') and the overall results differ when at least one ntuser.dat file is not configured at all via GPO's etc..

The obvious side effect of mandating an ntuser_item being 'exist' is that existence only tests without a state won't be possible, or useful, but given the nature of this test, I don't see much of any use in that scenario, and the existence testing can then be moved down to the element in a state, which is what we do for the commandshell test, which by design always and only creates 1 item.
ntuser_existence_example.xml

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Well, a non-existing item can never have an impact on the result. That much I know for sure.

When we implemented various CIS benchmarks in pure OVAL we used to jump through a bunch of hoops in such instances. We'd create an object to count the number of instances of the file (for example) and then compare it to the unique set of filenames with content matching the pattern in the context of a complicated OVAL definition. It wasn't easy... but we made it fairly easy to do using our Slang language/tool (IDK if that still really even exists now, though). I prefer that approach over mandating the creation of so-called partial matches. It's trivially easy to create an ntuser_obect that's guaranteed to exist for every user:

<ntuser_object>
  <key>/</key>
  <value/>
</ntuser_object>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One more thing...

The obvious side effect of mandating an ntuser_item being 'exist' is that existence only tests without a state won't be possible, or useful, but given the nature of this test

Actually the result will be that it would lead to errors, as states would be compared to items with field entities that were not collected or don't exist, when they were not necessarily designed with that possibility in mind.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@solind I'm not following how your recommendations solve the issue at hand of verifying that all user profiles need to have a required key/name/value to pass. Can you provide an example? Maybe using Joval or SCC and attach some results?

Copy link
Copy Markdown
Member Author

@vanderpol vanderpol May 12, 2026

Choose a reason for hiding this comment

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

@solind thanks for that point. I understand that this is an acceptable way to make existing tools report correctly and might be something worth investigating, but it also means rewriting and retesting all of the ntuser_tests in existence. While not a huge number like Registry or File, there are a few hundred tests that would need to be redone. Granted this might be faster than getting other OVAL/SCAP Scanners up to SCAP 1.4.

How about the thought of a behavior to "create_item_based_on_ntuser_file_existence"? That would give control to the content author. Default would be 'true' in order to not have to update all existing content though.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

BTW this concept of a behavior would also be very useful for textfilecontent54 and xmlfilecontent, as you have to jump through similar convoluted methods with variables to check to ensure that all files that were in scope contain the required data, if the textfilecontent pattern doesn't exist, the item isn't created. Given the body of content that uses textfilecontent54 and xmlcontent, if we added behaviors to them, they would need to default to 'false' on creating items when the filepath exists.

My goal being making content easier to write and results be as intuitive as possible. I fully agree that we 'can' do a lot of things with OVAL, but if it makes content harder to write, or results less understandable, then change/progress is needed. OK off my soapbox.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Adding a (non-default) behavior would probably be the least intrusive way to introduce such a change. I'd personally call it something like "item_generation=(existing_values|every_ntuser)", with "existing_values" being the default.

The somewhat complicated approach I outlined above for writing the test is not unique to the ntuser_test. It also applies to many of the various types of user tests that must both look for a user file, and also check its contents. It's been so long that I can't recall every type of test where we had to use that approach, but there were at least several.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just a note -- choosing the default value is a matter of backwards-compatibility. It sounds like the existing content not implemented correctly to account for users lacking a specific user registry key. To the extent that, for example, a Joval customer needed to have such an ntuser_check performed for compliance content, my bet is that it was implemented properly. Adding a default behavior that creates partial matches might break that content.

Now, when I was in charge of this kind of thing and had a problem like yours, I'd also add a Joval property that customers could set to override the officially-specified behavior. That's how you can have your cake and eat it too, which is the key to successfully writing enterprise software.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It sounds like we are on the same page. I'm in full agreement on the backward compatibility note. I'll submit another round of updates tomorrow and see what you think. Hoping we can get others to chime in as well. Thanks again for your time and feedback. I appreciate all of your efforts, keeping me honest.

<xsd:documentation>The username entity holds a string that represents the name of a particular user. In Windows, user names are case-insensitive. As a
result, it is recommended that the case-insensitive operations are used for this entity. In a domain environment, users should be identified in
the form: "domain\user name". For local users use: "computer name\user name".</xsd:documentation>
<xsd:documentation>Note: When gathering the built-in Guest and build-in Administrator, they may not resolve and may need to have the ComputerName prepended to it.</xsd:documentation>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The built-in guest and administrator accounts may actually be renamed. They are really only recognizable using their SIDs. I believe the username convention here should follow those used for other Windows user objects.

Copy link
Copy Markdown
Member Author

@vanderpol vanderpol May 11, 2026

Choose a reason for hiding this comment

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

Agreed, and my proposed wording was poorly chosen, I was trying to say that Local Admin or Guest accounts (renamed or not) do not resolve to Hostname\Account they just resolve to and the local hostname scope needs to be prepended to it. I'll check other documentation to see how it's handled in other places.

@vanderpol vanderpol requested a review from solind May 13, 2026 13:51
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.

Update Windows ntuser documentation based on to ensure consistent results across OVAL interpreter implementations

2 participants