From 768edaa63a84cf0a918135e9f52fb2db35def230 Mon Sep 17 00:00:00 2001 From: loaflover <34983803+loaflover@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:38:14 +0200 Subject: [PATCH 01/10] add --- .../os/windows/appdatapackages/__init__.py | 0 .../windows/appdatapackages/windows_search.py | 94 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 dissect/target/plugins/os/windows/appdatapackages/__init__.py create mode 100644 dissect/target/plugins/os/windows/appdatapackages/windows_search.py diff --git a/dissect/target/plugins/os/windows/appdatapackages/__init__.py b/dissect/target/plugins/os/windows/appdatapackages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dissect/target/plugins/os/windows/appdatapackages/windows_search.py b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py new file mode 100644 index 0000000000..e066a230d3 --- /dev/null +++ b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +import json +from dissect.util.ts import from_unix + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + + +WindowsSearchRecord = TargetRecordDescriptor( + "os/windows/appdatapackages/windows_search_record", + [ + ("string", "FileExtension"), + ("string", "ProductVersion"), + ("boolean", "IsSystemComponent"), + ("string", "Kind"), + ("string", "ParsingName"), + ("varint", "TimesUsed"), + ("varint", "Background"), + ("string", "PackageFullName"), + ("string", "Identity"), + ("string", "FileName"), + ("string[]", "JumpList"), + ("string[]", "VoiceCommandExamples"), + ("string", "ItemType"), + ("datetime", "DateAccessed"), + ("string", "EncodedTargetPath"), + ("string", "SmallLogoPath"), + ("string", "ItemNameDisplay"), + ], +) + +def normalize_none(string: str): + return None if string in ("", "N/A", "[]") else string + + +class WindowsSearch(Plugin): + """Plugin that parses the Windows search json file under appdata. known to work on windows a few windows 10 machines, unknown oif works on other versions.""" + + def __init__(self, target: Target): + super().__init__(target) + self.cachefiles = [] + + for user_details in target.user_details.all_with_home(): + full_path = user_details.home_path.joinpath("AppData/Local/Packages") + cache_files = full_path.glob("Microsoft.Windows.Search_*/LocalState/DeviceSearchCache/AppCache*.txt") + for cache_file in cache_files: + if cache_file.exists(): + self.cachefiles.append((user_details.user, cache_file)) + + def check_compatible(self) -> None: + if len(self.cachefiles) == 0: + raise UnsupportedPluginError("No AppCache files found") + + @export(record=WindowsSearchRecord) + def appcache(self) -> Iterator[WindowsSearchRecord]: + """ + """ + for user, cache_file in self.cachefiles: + target_path = self.target.fs.path(cache_file) + with target_path.open("r", encoding="utf-8") as f: + try: + entries = json.load(f) + except json.JSONDecodeError as e: + self.target.log.warning(f"Failed to parse {cache_file}: {e}") + continue + + for entry in entries: + yield WindowsSearchRecord( + FileExtension=normalize_none(entry.get("System.FileExtension", {}).get("Value")), + ProductVersion=normalize_none(entry.get("System.Software.ProductVersion", {}).get("Value")), + IsSystemComponent=entry.get("System.AppUserModel.IsSystemComponent", {}).get("Value"), + Kind=normalize_none(entry.get("System.Kind", {}).get("Value")), + ParsingName=normalize_none(entry.get("System.ParsingName", {}).get("Value")), + TimesUsed=entry.get("System.Software.TimesUsed", {}).get("Value"), + Background=entry.get("System.Tile.Background", {}).get("Value"), + PackageFullName=normalize_none(entry.get("System.AppUserModel.PackageFullName", {}).get("Value")), + Identity=normalize_none(entry.get("System.Identity", {}).get("Value")), + FileName=normalize_none(entry.get("System.FileName", {}).get("Value")), + JumpList=entry.get("System.ConnectedSearch.JumpList", {}).get("Value", []), + VoiceCommandExamples=entry.get("System.ConnectedSearch.VoiceCommandExamples", {}).get("Value", []), + ItemType=normalize_none(entry.get("System.ItemType", {}).get("Value")), + DateAccessed=from_unix(entry.get("System.DateAccessed", {}).get("Value")), + EncodedTargetPath=normalize_none(entry.get("System.Tile.EncodedTargetPath", {}).get("Value")), + SmallLogoPath=normalize_none(entry.get("System.Tile.SmallLogoPath", {}).get("Value")), + ItemNameDisplay=normalize_none(entry.get("System.ItemNameDisplay", {}).get("Value")), + ) \ No newline at end of file From 0c4541ba10d23e9605381b0848395b254dc47dc2 Mon Sep 17 00:00:00 2001 From: loaflover <34983803+loaflover@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:09:29 +0200 Subject: [PATCH 02/10] plugin done --- .../windows/appdatapackages/windows_search.py | 70 ++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/dissect/target/plugins/os/windows/appdatapackages/windows_search.py b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py index e066a230d3..b4f43cffdf 100644 --- a/dissect/target/plugins/os/windows/appdatapackages/windows_search.py +++ b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING import json -from dissect.util.ts import from_unix +from typing import TYPE_CHECKING + +from dissect.util.ts import wintimestamp from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension +from dissect.target.helpers.record import create_extended_descriptor from dissect.target.plugin import Plugin, export if TYPE_CHECKING: @@ -14,7 +16,7 @@ from dissect.target.target import Target -WindowsSearchRecord = TargetRecordDescriptor( +WindowsSearchRecord = create_extended_descriptor([UserRecordDescriptorExtension])( "os/windows/appdatapackages/windows_search_record", [ ("string", "FileExtension"), @@ -27,22 +29,24 @@ ("string", "PackageFullName"), ("string", "Identity"), ("string", "FileName"), - ("string[]", "JumpList"), - ("string[]", "VoiceCommandExamples"), + ("string", "JumpList"), + ("string", "VoiceCommandExamples"), ("string", "ItemType"), ("datetime", "DateAccessed"), ("string", "EncodedTargetPath"), ("string", "SmallLogoPath"), ("string", "ItemNameDisplay"), + ("string", "CacheFilePath"), ], ) -def normalize_none(string: str): - return None if string in ("", "N/A", "[]") else string + +def normalize_none(string: str | list) -> str | list | None: + return None if string in ("", "N/A", "[]", []) else string class WindowsSearch(Plugin): - """Plugin that parses the Windows search json file under appdata. known to work on windows a few windows 10 machines, unknown oif works on other versions.""" + """Extract Windows Search AppCache records (Windows 10 only; may not work on Windows 11).""" def __init__(self, target: Target): super().__init__(target) @@ -61,15 +65,40 @@ def check_compatible(self) -> None: @export(record=WindowsSearchRecord) def appcache(self) -> Iterator[WindowsSearchRecord]: - """ + """Return Windows Search AppCache records for all users. + + Yields WindowsSearchRecord with the following fields: + + FileExtension (string): The file extension of the cached item. + ProductVersion (string): Version of the software related to the item. + IsSystemComponent (bool): Whether the item is a system component. + Kind (string): Kind/type of the item. + ParsingName (string): Internal parsing name of the item. + TimesUsed (varint): Number of times the item has been used. + Background (varint): Tile background type. + PackageFullName (string): Full package name of the app. + Identity (string): Identity string of the app/item. + FileName (string): Name of the file. + JumpList (list): List of jump list entries associated with the item. + VoiceCommandExamples (list): Examples of voice commands linked to the item. + ItemType (string): Item type description. + DateAccessed (datetime): Timestamp of last access (converted from Windows FILETIME). + EncodedTargetPath (string): Encoded target path of the tile. + SmallLogoPath (string): Path to the small logo image. + ItemNameDisplay (string): Display name of the item. + CacheFilePath (path): Path to the cache file where this record came from. + + Notes: + - If a JSON cache file cannot be parsed, a warning is logged and processing continues. + - Fields with empty strings, "N/A", empty lists, or None are normalized to None. + - Timestamps are converted from Windows FILETIME format using `wintimestamp`. """ for user, cache_file in self.cachefiles: - target_path = self.target.fs.path(cache_file) - with target_path.open("r", encoding="utf-8") as f: + with cache_file.open("r", encoding="utf-8") as cachefileIO: try: - entries = json.load(f) + entries = json.load(cachefileIO) except json.JSONDecodeError as e: - self.target.log.warning(f"Failed to parse {cache_file}: {e}") + self.target.log.warning("Failed to parse %s: %s", cache_file, e) continue for entry in entries: @@ -84,11 +113,16 @@ def appcache(self) -> Iterator[WindowsSearchRecord]: PackageFullName=normalize_none(entry.get("System.AppUserModel.PackageFullName", {}).get("Value")), Identity=normalize_none(entry.get("System.Identity", {}).get("Value")), FileName=normalize_none(entry.get("System.FileName", {}).get("Value")), - JumpList=entry.get("System.ConnectedSearch.JumpList", {}).get("Value", []), - VoiceCommandExamples=entry.get("System.ConnectedSearch.VoiceCommandExamples", {}).get("Value", []), + JumpList=normalize_none(entry.get("System.ConnectedSearch.JumpList", {}).get("Value", [])), + VoiceCommandExamples=normalize_none( + entry.get("System.ConnectedSearch.VoiceCommandExamples", {}).get("Value", []) + ), ItemType=normalize_none(entry.get("System.ItemType", {}).get("Value")), - DateAccessed=from_unix(entry.get("System.DateAccessed", {}).get("Value")), + DateAccessed=wintimestamp(entry.get("System.DateAccessed", {}).get("Value")), EncodedTargetPath=normalize_none(entry.get("System.Tile.EncodedTargetPath", {}).get("Value")), SmallLogoPath=normalize_none(entry.get("System.Tile.SmallLogoPath", {}).get("Value")), ItemNameDisplay=normalize_none(entry.get("System.ItemNameDisplay", {}).get("Value")), - ) \ No newline at end of file + CacheFilePath=cache_file, + _target=self.target, + _user=user, + ) From 15d040d7aa96342f7c7dcfbada951101f6822ac3 Mon Sep 17 00:00:00 2001 From: loaflover <34983803+loaflover@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:31:20 +0200 Subject: [PATCH 03/10] settingscache --- .../os/windows/appdatapackages/windows_search.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dissect/target/plugins/os/windows/appdatapackages/windows_search.py b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py index b4f43cffdf..fc7e0c1ce1 100644 --- a/dissect/target/plugins/os/windows/appdatapackages/windows_search.py +++ b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py @@ -16,8 +16,8 @@ from dissect.target.target import Target -WindowsSearchRecord = create_extended_descriptor([UserRecordDescriptorExtension])( - "os/windows/appdatapackages/windows_search_record", +WindowsAppCacheRecord = create_extended_descriptor([UserRecordDescriptorExtension])( + "os/windows/appdata/packages/appcache", [ ("string", "FileExtension"), ("string", "ProductVersion"), @@ -45,7 +45,7 @@ def normalize_none(string: str | list) -> str | list | None: return None if string in ("", "N/A", "[]", []) else string -class WindowsSearch(Plugin): +class app_cache(Plugin): """Extract Windows Search AppCache records (Windows 10 only; may not work on Windows 11).""" def __init__(self, target: Target): @@ -63,8 +63,8 @@ def check_compatible(self) -> None: if len(self.cachefiles) == 0: raise UnsupportedPluginError("No AppCache files found") - @export(record=WindowsSearchRecord) - def appcache(self) -> Iterator[WindowsSearchRecord]: + @export(record=WindowsAppCacheRecord) + def appcache(self) -> Iterator[WindowsAppCacheRecord]: """Return Windows Search AppCache records for all users. Yields WindowsSearchRecord with the following fields: @@ -102,7 +102,7 @@ def appcache(self) -> Iterator[WindowsSearchRecord]: continue for entry in entries: - yield WindowsSearchRecord( + yield WindowsAppCacheRecord( FileExtension=normalize_none(entry.get("System.FileExtension", {}).get("Value")), ProductVersion=normalize_none(entry.get("System.Software.ProductVersion", {}).get("Value")), IsSystemComponent=entry.get("System.AppUserModel.IsSystemComponent", {}).get("Value"), From eb9203cdd16057a1b57b4bf176ca3a7a50e26a84 Mon Sep 17 00:00:00 2001 From: loaflover <34983803+loaflover@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:30:26 +0200 Subject: [PATCH 04/10] new record found! --- .../plugins/os/windows/appdatapackages/windows_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/windows/appdatapackages/windows_search.py b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py index fc7e0c1ce1..cdfd89cd79 100644 --- a/dissect/target/plugins/os/windows/appdatapackages/windows_search.py +++ b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py @@ -41,8 +41,8 @@ ) -def normalize_none(string: str | list) -> str | list | None: - return None if string in ("", "N/A", "[]", []) else string +def normalize_none(input: str | list) -> str | list | None: + return None if input in ("", "N/A", "[]", []) else input class app_cache(Plugin): From f2d3f0aecfdd45f365aff91d822d02993db8d851 Mon Sep 17 00:00:00 2001 From: loaflover <34983803+loaflover@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:32:59 +0200 Subject: [PATCH 05/10] whoops forgot to add --- .../os/windows/appdatapackages/screenclip.py | 144 ++++++++++++++++++ .../windows/appdatapackages/settings_cache.py | 101 ++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 dissect/target/plugins/os/windows/appdatapackages/screenclip.py create mode 100644 dissect/target/plugins/os/windows/appdatapackages/settings_cache.py diff --git a/dissect/target/plugins/os/windows/appdatapackages/screenclip.py b/dissect/target/plugins/os/windows/appdatapackages/screenclip.py new file mode 100644 index 0000000000..028c89f5b7 --- /dev/null +++ b/dissect/target/plugins/os/windows/appdatapackages/screenclip.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import hashlib +import json +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension +from dissect.target.helpers.record import create_extended_descriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + + +WindowsScreenClipJsonRecord = create_extended_descriptor([UserRecordDescriptorExtension])( + "os/windows/appdata/packages/screenclip/json", + [ + ("string[]", "clipPoints"), + ("string", "appActivityId"), + ("string", "appDisplayName"), + ("string", "activationUrl"), + ("boolean", "isRoamable"), + ("string", "visualElements"), + ("string[]", "cross_platform_identifiers"), + ("string", "description"), + ("string", "contentUrl"), + ("string", "contentInfo"), + ("string", "CacheFilePath"), + ], +) +WindowsScreenClipPngRecord = create_extended_descriptor([UserRecordDescriptorExtension])( + "os/windows/appdata/packages/screenclip/png", + [ + ("string", "sha256Hash"), + ("string", "sha1Hash"), + ("string", "md5Hash"), + ("string", "CacheFilePath"), + ], +) + + +def normalize_none(input: str | list) -> str | list | None: + return None if input in ("", "N/A", "[]", []) else input + + +class settings_cache(Plugin): + """Extract Windows screenclip records (Windows 10 only for now; may not work on Windows 11).""" + + def __init__(self, target: Target): + super().__init__(target) + self.jsonfiles = [] + self.pngfiles = [] + + for user_details in target.user_details.all_with_home(): + full_path = user_details.home_path.joinpath("AppData/Local/Packages") + json_files = full_path.glob("MicrosoftWindows.Client.CBS_*/TempState/ScreenClip/*.json") + for json_file in json_files: + if json_file.exists(): + self.jsonfiles.append((user_details.user, json_file)) + png_files = full_path.glob("MicrosoftWindows.Client.CBS_*/TempState/ScreenClip/*.png") + for png_file in png_files: + if png_file.exists(): + self.pngfiles.append((user_details.user, png_file)) + + def check_compatible(self) -> None: + if len(self.jsonfiles) == 0 and len(self.pngfiles) == 0: + raise UnsupportedPluginError("No screenclip files found") + + @export(record=WindowsScreenClipJsonRecord) + def screenclip(self) -> Iterator[WindowsScreenClipJsonRecord]: + """Yield Windows ScreenClip JSON and PNG records for all users. + + JSON Records (`WindowsScreenClipJsonRecord`): + clipPoints (string[]): Coordinates of the captured area as "x,y" strings. + appActivityId (string): The unique identifier of the app activity. + appDisplayName (string): Display name of the application associated with the clip. + activationUrl (string): URL used to activate or open the app activity. + isRoamable (boolean): Whether the activity can roam across devices. + visualElements (string): JSON or string representing visual properties of the clip. + cross_platform_identifiers (string[]): Identifiers linking the activity across platforms. + description (string): Description or note associated with the clip. + contentUrl (string): URL of the clip content, if any. + contentInfo (string): Additional information about the clip content. + CacheFilePath (string): Path to the source JSON file. + + PNG Records (`WindowsScreenClipPngRecord`): + md5Hash (string): MD5 hash of the PNG data. + sha1Hash (string): SHA1 hash of the PNG data. + sha256Hash (string): SHA256 hash of the PNG data. + CacheFilePath (string): Path to the source PNG file. + + Notes: + - Empty, "N/A", or invalid entries (such as empty lists) are normalized to `None`. + - PNG records are hashed to uniquely identify the clip content. + - JSON 'userActivity' fields may be nested; invalid or unparsable JSON is skipped with a warning. + """ + for user, json_cache_file in self.jsonfiles: + with json_cache_file.open("r", encoding="utf-8") as cachefileIO: + try: + parsed_json = json.load(cachefileIO) + except json.JSONDecodeError as e: + self.target.log.warning("Failed to parse %s: %s", json_cache_file, e) + continue + # Parse the escaped 'userActivity' JSON string + try: + user_activity = json.loads(parsed_json.get("userActivity", "{}")) + except json.JSONDecodeError: + user_activity = {} + + yield WindowsScreenClipJsonRecord( + clipPoints=[f"{pt['x']},{pt['y']}" for pt in parsed_json.get("clipPoints", [])], + appActivityId=normalize_none(user_activity.get("appActivityId")), + appDisplayName=normalize_none(user_activity.get("appDisplayName")), + activationUrl=normalize_none(user_activity.get("activationUrl")), + isRoamable=user_activity.get("isRoamable", False), + visualElements=normalize_none(user_activity.get("visualElements")), + cross_platform_identifiers=normalize_none(user_activity.get("cross-platform-identifiers", [])), + description=normalize_none(user_activity.get("description")), + contentUrl=normalize_none(user_activity.get("contentUrl")), + contentInfo=normalize_none(user_activity.get("contentInfo")), + CacheFilePath=json_cache_file, + _target=self.target, + _user=user, + ) + + for user, png_cache_file in self.pngfiles: + with png_cache_file.open("rb") as png_data: + data = png_data.read() + + md5_hash = hashlib.md5(data).hexdigest() + sha1_hash = hashlib.sha1(data).hexdigest() + sha256_hash = hashlib.sha256(data).hexdigest() + + yield WindowsScreenClipPngRecord( + md5Hash=md5_hash, + sha1Hash=sha1_hash, + sha256Hash=sha256_hash, + CacheFilePath=json_cache_file, + _target=self.target, + _user=user, + ) diff --git a/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py b/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py new file mode 100644 index 0000000000..a870382030 --- /dev/null +++ b/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension +from dissect.target.helpers.record import create_extended_descriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + + +WindowsSettingsCacheRecord = create_extended_descriptor([UserRecordDescriptorExtension])( + "os/windows/appdata/packages/settingscache", + [ + ("string", "ParsingName"), + ("string", "ActivationContext"), + ("string", "SmallLogoPath"), + ("string", "PageID"), + ("string", "SettingID"), + ("string", "HostID"), + ("string", "Condition"), + ("string", "Comment"), + ("string", "CacheFilePath"), + ], +) + + +def normalize_none(input: str | list) -> str | list | None: + return None if input in ("", "N/A", "[]", []) else input + + +class settings_cache(Plugin): + """Extract Windows SettingsCache records (Windows 10 only for now; may not work on Windows 11).""" + + def __init__(self, target: Target): + super().__init__(target) + self.cachefiles = [] + + for user_details in target.user_details.all_with_home(): + full_path = user_details.home_path.joinpath("AppData/Local/Packages") + # path location for windows 10. windows 11 path not implemented yet. + cache_files = full_path.glob("Microsoft.Windows.Search_*/LocalState/DeviceSearchCache/SettingsCache.txt") + for cache_file in cache_files: + if cache_file.exists(): + self.cachefiles.append((user_details.user, cache_file)) + + def check_compatible(self) -> None: + if len(self.cachefiles) == 0: + raise UnsupportedPluginError("No SettingsCache files found") + + @export(record=WindowsSettingsCacheRecord) + def settingscache(self) -> Iterator[WindowsSettingsCacheRecord]: + """Return Windows Search AppCache records for all users. + + Yields `WindowsSettingsCacheRecord` with the following fields: + + ParsingName (string): Internal parsing name of the cached item. + ActivationContext (string): Activation context associated with the item. + SmallLogoPath (string): Path to the small logo image for the tile. + PageID (string): Page identifier for the tile. + SettingID (string): Identifier for the setting tied to the item. + HostID (string): Host identifier for the system or app related to the item. + Condition (string): Condition of the setting or tile. + Comment (string): Comment or description attached to the item. + CacheFilePath (path): Path to the cache file from which this record is extracted. + + Notes: + - Empty, "N/A", or invalid entries (such as empty lists) are normalized to `None`. + - Timestamps are converted from Windows FILETIME format using `wintimestamp`. + - If a cache file cannot be parsed (e.g., due to invalid JSON format), a warning is logged, + and processing continues. + """ + for user, cache_file in self.cachefiles: + with cache_file.open("r", encoding="utf-8") as cachefileIO: + try: + entries = json.load(cachefileIO) + except json.JSONDecodeError as e: + self.target.log.warning("Failed to parse %s: %s", cache_file, e) + continue + + for entry in entries: + yield WindowsSettingsCacheRecord( + ParsingName=normalize_none(entry.get("System.ParsingName", {}).get("Value")), + ActivationContext=normalize_none( + entry.get("System.AppUserModel.ActivationContext", {}).get("Value") + ), + SmallLogoPath=normalize_none(entry.get("System.Tile.SmallLogoPath", {}).get("Value")), + PageID=normalize_none(entry.get("System.Setting.PageID", {}).get("Value")), + SettingID=normalize_none(entry.get("System.Setting.SettingID", {}).get("Value")), + HostID=normalize_none(entry.get("System.Setting.HostID", {}).get("Value")), + Condition=normalize_none(entry.get("System.Setting.Condition", {}).get("Value")), + Comment=normalize_none(entry.get("System.Comment", {}).get("Value")), + CacheFilePath=cache_file, + _target=self.target, + _user=user, + ) From 1de536cf9ff0e9d34ec76e8ed9064b383a064c47 Mon Sep 17 00:00:00 2001 From: loaflover <34983803+loaflover@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:36:04 +0200 Subject: [PATCH 06/10] first test --- .../windows/appdatapackages/settings_cache.py | 3 +- .../windows/AppDataPackages/settingscache.txt | 3 ++ .../os/windows/appdatapackages/__init__.py | 0 .../windows/appdatapackages/test_settings.py | 40 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/_data/plugins/os/windows/AppDataPackages/settingscache.txt create mode 100644 tests/plugins/os/windows/appdatapackages/__init__.py create mode 100644 tests/plugins/os/windows/appdatapackages/test_settings.py diff --git a/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py b/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py index a870382030..389560c41c 100644 --- a/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py +++ b/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py @@ -46,8 +46,7 @@ def __init__(self, target: Target): # path location for windows 10. windows 11 path not implemented yet. cache_files = full_path.glob("Microsoft.Windows.Search_*/LocalState/DeviceSearchCache/SettingsCache.txt") for cache_file in cache_files: - if cache_file.exists(): - self.cachefiles.append((user_details.user, cache_file)) + self.cachefiles.append((user_details.user, cache_file)) def check_compatible(self) -> None: if len(self.cachefiles) == 0: diff --git a/tests/_data/plugins/os/windows/AppDataPackages/settingscache.txt b/tests/_data/plugins/os/windows/AppDataPackages/settingscache.txt new file mode 100644 index 0000000000..4861022535 --- /dev/null +++ b/tests/_data/plugins/os/windows/AppDataPackages/settingscache.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0876dbaeba5e4fcb4b8634af2d195e87fc719680af88d8b27eaa78ae10211a4a +size 1459 diff --git a/tests/plugins/os/windows/appdatapackages/__init__.py b/tests/plugins/os/windows/appdatapackages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/windows/appdatapackages/test_settings.py b/tests/plugins/os/windows/appdatapackages/test_settings.py new file mode 100644 index 0000000000..c4929ee24c --- /dev/null +++ b/tests/plugins/os/windows/appdatapackages/test_settings.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +import pytest +from flow.record.fieldtypes import datetime as dt + +from dissect.target.helpers import keychain +from dissect.target.helpers.fsutil import TargetPath +from tests._utils import absolute_path + +from dissect.target.plugins.os.windows.AppDataPackages.settings_cache import settings_cache + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.fixture +def target_settings_cache(target_win_users: Target, fs_win: VirtualFilesystem) -> Target: + fs_win.map_file( + "Users\\John\\AppData\\Local\\Packages\\Microsoft.Windows.Search_cw5n1h2txyewy\\LocalState\\DeviceSearchCache\\SettingsCache.txt", + absolute_path("_data/plugins/os/windows/AppDataPackages/settingscache.txt"), + ) + + target_win_users.add_plugin(settings_cache) + + return target_win_users + + +def test_settings_cache(target_settings_cache: Target): + results = list(target_settings_cache.settingscache()) + + assert len(results) == 2 + print(results[0]) + assert results[0].ParsingName == 'examplesetting' + assert results[0].ActivationContext == '%windir%\\system32\\rundll32.exe %windir%\\system32\\example\\setting.dll' + assert results[0].SettingID == '{11223344-1234-ABCD-EFGH-123456789123}' + assert results[0].Comment == 'idkwhat to write here tbh' \ No newline at end of file From 3029877f3e9b25b68a9c4c9309d85a40c2639717 Mon Sep 17 00:00:00 2001 From: loaflover <34983803+loaflover@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:10:00 +0200 Subject: [PATCH 07/10] added second test --- .../os/windows/appdatapackages/screenclip.py | 2 +- ...2D28F969-129C-4F0B-B2E5-21C7D26B664C}.json | 3 ++ ...{609C108C-7028-4791-A81C-534002C11831}.png | 3 ++ ...93255F2C-BA80-493C-BEB2-42CC288017D3}.json | 3 ++ ...{C58906B3-7096-4EA1-BEC3-7E1457185FDD}.png | 3 ++ .../appdatapackages/test_screen_clip.py | 49 +++++++++++++++++++ .../windows/appdatapackages/test_settings.py | 4 -- .../appdatapackages/test_windows_search.py | 40 +++++++++++++++ 8 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 tests/_data/plugins/os/windows/AppDataPackages/screenclips/{2D28F969-129C-4F0B-B2E5-21C7D26B664C}.json create mode 100644 tests/_data/plugins/os/windows/AppDataPackages/screenclips/{609C108C-7028-4791-A81C-534002C11831}.png create mode 100644 tests/_data/plugins/os/windows/AppDataPackages/screenclips/{93255F2C-BA80-493C-BEB2-42CC288017D3}.json create mode 100644 tests/_data/plugins/os/windows/AppDataPackages/screenclips/{C58906B3-7096-4EA1-BEC3-7E1457185FDD}.png create mode 100644 tests/plugins/os/windows/appdatapackages/test_screen_clip.py create mode 100644 tests/plugins/os/windows/appdatapackages/test_windows_search.py diff --git a/dissect/target/plugins/os/windows/appdatapackages/screenclip.py b/dissect/target/plugins/os/windows/appdatapackages/screenclip.py index 028c89f5b7..80f8b919cf 100644 --- a/dissect/target/plugins/os/windows/appdatapackages/screenclip.py +++ b/dissect/target/plugins/os/windows/appdatapackages/screenclip.py @@ -46,7 +46,7 @@ def normalize_none(input: str | list) -> str | list | None: return None if input in ("", "N/A", "[]", []) else input -class settings_cache(Plugin): +class screenclip(Plugin): """Extract Windows screenclip records (Windows 10 only for now; may not work on Windows 11).""" def __init__(self, target: Target): diff --git a/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{2D28F969-129C-4F0B-B2E5-21C7D26B664C}.json b/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{2D28F969-129C-4F0B-B2E5-21C7D26B664C}.json new file mode 100644 index 0000000000..e6ef29749a --- /dev/null +++ b/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{2D28F969-129C-4F0B-B2E5-21C7D26B664C}.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24268dc03753fa7d44926ac9c3ecd0c8cfc77e56f61c7cc1ed32b8455cd815f8 +size 540 diff --git a/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{609C108C-7028-4791-A81C-534002C11831}.png b/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{609C108C-7028-4791-A81C-534002C11831}.png new file mode 100644 index 0000000000..e14abfbede --- /dev/null +++ b/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{609C108C-7028-4791-A81C-534002C11831}.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccf1f6398826aa716c90797aea202bc654166575c7f9a76303d10c6da838bdad +size 386964 diff --git a/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{93255F2C-BA80-493C-BEB2-42CC288017D3}.json b/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{93255F2C-BA80-493C-BEB2-42CC288017D3}.json new file mode 100644 index 0000000000..cbeb390924 --- /dev/null +++ b/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{93255F2C-BA80-493C-BEB2-42CC288017D3}.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23766879b9cda92c024a797c188f6fa6ae01bc6a5d45b226d26a0ae224ca2579 +size 540 diff --git a/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{C58906B3-7096-4EA1-BEC3-7E1457185FDD}.png b/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{C58906B3-7096-4EA1-BEC3-7E1457185FDD}.png new file mode 100644 index 0000000000..b69fc2e594 --- /dev/null +++ b/tests/_data/plugins/os/windows/AppDataPackages/screenclips/{C58906B3-7096-4EA1-BEC3-7E1457185FDD}.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e66ebe356132ad3837cef02818a48286ea5b9867f2a2e5085c8eb1ac0e7e1b6 +size 76277 diff --git a/tests/plugins/os/windows/appdatapackages/test_screen_clip.py b/tests/plugins/os/windows/appdatapackages/test_screen_clip.py new file mode 100644 index 0000000000..8f283a8904 --- /dev/null +++ b/tests/plugins/os/windows/appdatapackages/test_screen_clip.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from flow.record.fieldtypes import datetime as dt + +from tests._utils import absolute_path + +from dissect.target.plugins.os.windows.AppDataPackages.screenclip import screenclip, WindowsScreenClipJsonRecord, WindowsScreenClipPngRecord + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.fixture +def target_screenclip(target_win_users: Target, fs_win: VirtualFilesystem) -> Target: + fs_win.map_dir( + "Users\\John\\AppData\\Local\\Packages\\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\\TempState\\ScreenClip", + absolute_path("_data/plugins/os/windows/AppDataPackages/screenclips"), + ) + + target_win_users.add_plugin(screenclip) + + return target_win_users + + +def test_settings_cache(target_screenclip: Target): + results = list(target_screenclip.screenclip()) + png_records = [record for record in results if isinstance(record, type(WindowsScreenClipPngRecord()))] + json_records = [record for record in results if isinstance(record, type(WindowsScreenClipJsonRecord()))] + + + + assert len(png_records) == 2 + assert len(json_records) == 2 + + + assert json_records[0].appDisplayName == "Firefox" + assert json_records[0].activationUrl == "ms-shellactivity:" + assert json_records[0].username == "John" + assert json_records[0].visualElements == "{'backgroundColor': 'black', 'displayText': 'Firefox'}" + print(png_records[0]) + assert png_records[0].sha256Hash == "ccf1f6398826aa716c90797aea202bc654166575c7f9a76303d10c6da838bdad" + assert png_records[0].sha1Hash == "a166b3cb39cea116b8611a1533c3fcc6a8f2676b" + assert png_records[0].md5Hash == "7acaf36dde2f286c8da2f84e36a090ad" + + diff --git a/tests/plugins/os/windows/appdatapackages/test_settings.py b/tests/plugins/os/windows/appdatapackages/test_settings.py index c4929ee24c..0d0ae31d77 100644 --- a/tests/plugins/os/windows/appdatapackages/test_settings.py +++ b/tests/plugins/os/windows/appdatapackages/test_settings.py @@ -1,13 +1,10 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING import pytest from flow.record.fieldtypes import datetime as dt -from dissect.target.helpers import keychain -from dissect.target.helpers.fsutil import TargetPath from tests._utils import absolute_path from dissect.target.plugins.os.windows.AppDataPackages.settings_cache import settings_cache @@ -33,7 +30,6 @@ def test_settings_cache(target_settings_cache: Target): results = list(target_settings_cache.settingscache()) assert len(results) == 2 - print(results[0]) assert results[0].ParsingName == 'examplesetting' assert results[0].ActivationContext == '%windir%\\system32\\rundll32.exe %windir%\\system32\\example\\setting.dll' assert results[0].SettingID == '{11223344-1234-ABCD-EFGH-123456789123}' diff --git a/tests/plugins/os/windows/appdatapackages/test_windows_search.py b/tests/plugins/os/windows/appdatapackages/test_windows_search.py new file mode 100644 index 0000000000..c4929ee24c --- /dev/null +++ b/tests/plugins/os/windows/appdatapackages/test_windows_search.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +import pytest +from flow.record.fieldtypes import datetime as dt + +from dissect.target.helpers import keychain +from dissect.target.helpers.fsutil import TargetPath +from tests._utils import absolute_path + +from dissect.target.plugins.os.windows.AppDataPackages.settings_cache import settings_cache + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.fixture +def target_settings_cache(target_win_users: Target, fs_win: VirtualFilesystem) -> Target: + fs_win.map_file( + "Users\\John\\AppData\\Local\\Packages\\Microsoft.Windows.Search_cw5n1h2txyewy\\LocalState\\DeviceSearchCache\\SettingsCache.txt", + absolute_path("_data/plugins/os/windows/AppDataPackages/settingscache.txt"), + ) + + target_win_users.add_plugin(settings_cache) + + return target_win_users + + +def test_settings_cache(target_settings_cache: Target): + results = list(target_settings_cache.settingscache()) + + assert len(results) == 2 + print(results[0]) + assert results[0].ParsingName == 'examplesetting' + assert results[0].ActivationContext == '%windir%\\system32\\rundll32.exe %windir%\\system32\\example\\setting.dll' + assert results[0].SettingID == '{11223344-1234-ABCD-EFGH-123456789123}' + assert results[0].Comment == 'idkwhat to write here tbh' \ No newline at end of file From a39b8009d644d9a9b293fe12ce6a05b12fe7733a Mon Sep 17 00:00:00 2001 From: loaflover <34983803+loaflover@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:27:54 +0200 Subject: [PATCH 08/10] last test --- .../AppDataPackages/AppCache123123123.txt | 3 +++ .../appdatapackages/test_windows_search.py | 25 ++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 tests/_data/plugins/os/windows/AppDataPackages/AppCache123123123.txt diff --git a/tests/_data/plugins/os/windows/AppDataPackages/AppCache123123123.txt b/tests/_data/plugins/os/windows/AppDataPackages/AppCache123123123.txt new file mode 100644 index 0000000000..97687de095 --- /dev/null +++ b/tests/_data/plugins/os/windows/AppDataPackages/AppCache123123123.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e6f9f38f20e3eeb236a7317ea447bf72a8edb50745af4d24c9e59f357e11a83 +size 2999 diff --git a/tests/plugins/os/windows/appdatapackages/test_windows_search.py b/tests/plugins/os/windows/appdatapackages/test_windows_search.py index c4929ee24c..3ee2d30b8f 100644 --- a/tests/plugins/os/windows/appdatapackages/test_windows_search.py +++ b/tests/plugins/os/windows/appdatapackages/test_windows_search.py @@ -1,16 +1,13 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING import pytest from flow.record.fieldtypes import datetime as dt -from dissect.target.helpers import keychain -from dissect.target.helpers.fsutil import TargetPath from tests._utils import absolute_path -from dissect.target.plugins.os.windows.AppDataPackages.settings_cache import settings_cache +from dissect.target.plugins.os.windows.AppDataPackages.windows_search import app_cache if TYPE_CHECKING: from dissect.target.filesystem import VirtualFilesystem @@ -18,23 +15,23 @@ @pytest.fixture -def target_settings_cache(target_win_users: Target, fs_win: VirtualFilesystem) -> Target: +def target_app_cache(target_win_users: Target, fs_win: VirtualFilesystem) -> Target: fs_win.map_file( - "Users\\John\\AppData\\Local\\Packages\\Microsoft.Windows.Search_cw5n1h2txyewy\\LocalState\\DeviceSearchCache\\SettingsCache.txt", - absolute_path("_data/plugins/os/windows/AppDataPackages/settingscache.txt"), + "Users\\John\\AppData\\Local\\Packages\\Microsoft.Windows.Search_cw5n1h2txyewy\\LocalState\\DeviceSearchCache\\AppCache134176063414577965.txt", + absolute_path("_data/plugins/os/windows/AppDataPackages/AppCache123123123.txt"), ) - target_win_users.add_plugin(settings_cache) + target_win_users.add_plugin(app_cache) return target_win_users -def test_settings_cache(target_settings_cache: Target): - results = list(target_settings_cache.settingscache()) +def test_settings_cache(target_app_cache: Target): + results = list(target_app_cache.appcache()) assert len(results) == 2 print(results[0]) - assert results[0].ParsingName == 'examplesetting' - assert results[0].ActivationContext == '%windir%\\system32\\rundll32.exe %windir%\\system32\\example\\setting.dll' - assert results[0].SettingID == '{11223344-1234-ABCD-EFGH-123456789123}' - assert results[0].Comment == 'idkwhat to write here tbh' \ No newline at end of file + assert results[0].ParsingName == 'Z8X7C6V5B4N3M2L1' + assert results[0].FileExtension == '.qwe' + assert results[0].FileName == 'random_tool' + assert results[0].PackageFullName == 'com.random.tool' \ No newline at end of file From 4f2474b73b715f439d692b4cd81e3da484b04787 Mon Sep 17 00:00:00 2001 From: nir Date: Wed, 11 Mar 2026 12:13:01 +0200 Subject: [PATCH 09/10] fix --- tests/plugins/os/windows/appdatapackages/test_screen_clip.py | 2 +- tests/plugins/os/windows/appdatapackages/test_settings.py | 2 +- tests/plugins/os/windows/appdatapackages/test_windows_search.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/plugins/os/windows/appdatapackages/test_screen_clip.py b/tests/plugins/os/windows/appdatapackages/test_screen_clip.py index 8f283a8904..00e0e5ca3c 100644 --- a/tests/plugins/os/windows/appdatapackages/test_screen_clip.py +++ b/tests/plugins/os/windows/appdatapackages/test_screen_clip.py @@ -7,7 +7,7 @@ from tests._utils import absolute_path -from dissect.target.plugins.os.windows.AppDataPackages.screenclip import screenclip, WindowsScreenClipJsonRecord, WindowsScreenClipPngRecord +from dissect.target.plugins.os.windows.appdatapackages.screenclip import screenclip, WindowsScreenClipJsonRecord, WindowsScreenClipPngRecord if TYPE_CHECKING: from dissect.target.filesystem import VirtualFilesystem diff --git a/tests/plugins/os/windows/appdatapackages/test_settings.py b/tests/plugins/os/windows/appdatapackages/test_settings.py index 0d0ae31d77..df43144e47 100644 --- a/tests/plugins/os/windows/appdatapackages/test_settings.py +++ b/tests/plugins/os/windows/appdatapackages/test_settings.py @@ -7,7 +7,7 @@ from tests._utils import absolute_path -from dissect.target.plugins.os.windows.AppDataPackages.settings_cache import settings_cache +from dissect.target.plugins.os.windows.appdatapackages.settings_cache import settings_cache if TYPE_CHECKING: from dissect.target.filesystem import VirtualFilesystem diff --git a/tests/plugins/os/windows/appdatapackages/test_windows_search.py b/tests/plugins/os/windows/appdatapackages/test_windows_search.py index 3ee2d30b8f..369f674bea 100644 --- a/tests/plugins/os/windows/appdatapackages/test_windows_search.py +++ b/tests/plugins/os/windows/appdatapackages/test_windows_search.py @@ -7,7 +7,7 @@ from tests._utils import absolute_path -from dissect.target.plugins.os.windows.AppDataPackages.windows_search import app_cache +from dissect.target.plugins.os.windows.appdatapackages.windows_search import app_cache if TYPE_CHECKING: from dissect.target.filesystem import VirtualFilesystem From 8909ea497ac45fa20b8279a0ac7cb66b516ccf16 Mon Sep 17 00:00:00 2001 From: nir Date: Wed, 11 Mar 2026 15:10:04 +0200 Subject: [PATCH 10/10] found some extra values that are optional --- .../windows/appdatapackages/settings_cache.py | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py b/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py index 389560c41c..38ee49f3fe 100644 --- a/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py +++ b/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py @@ -10,7 +10,6 @@ if TYPE_CHECKING: from collections.abc import Iterator - from dissect.target.target import Target @@ -22,20 +21,27 @@ ("string", "SmallLogoPath"), ("string", "PageID"), ("string", "SettingID"), + ("string", "GroupID"), ("string", "HostID"), ("string", "Condition"), + ("string", "FontFamily"), + ("string", "Glyph"), + ("string", "GlyphRtl"), + ("string", "HighKeywords"), ("string", "Comment"), ("string", "CacheFilePath"), ], ) -def normalize_none(input: str | list) -> str | list | None: - return None if input in ("", "N/A", "[]", []) else input +def normalize_none(value): + if value in ("", "N/A", "[]", [], None): + return None + return value class settings_cache(Plugin): - """Extract Windows SettingsCache records (Windows 10 only for now; may not work on Windows 11).""" + """Extract Windows Search SettingsCache records.""" def __init__(self, target: Target): super().__init__(target) @@ -43,37 +49,39 @@ def __init__(self, target: Target): for user_details in target.user_details.all_with_home(): full_path = user_details.home_path.joinpath("AppData/Local/Packages") - # path location for windows 10. windows 11 path not implemented yet. - cache_files = full_path.glob("Microsoft.Windows.Search_*/LocalState/DeviceSearchCache/SettingsCache.txt") + + cache_files = full_path.glob( + "Microsoft.Windows.Search_*/LocalState/DeviceSearchCache/SettingsCache.txt" + ) + for cache_file in cache_files: self.cachefiles.append((user_details.user, cache_file)) def check_compatible(self) -> None: - if len(self.cachefiles) == 0: + if not self.cachefiles: raise UnsupportedPluginError("No SettingsCache files found") @export(record=WindowsSettingsCacheRecord) def settingscache(self) -> Iterator[WindowsSettingsCacheRecord]: - """Return Windows Search AppCache records for all users. - - Yields `WindowsSettingsCacheRecord` with the following fields: - - ParsingName (string): Internal parsing name of the cached item. - ActivationContext (string): Activation context associated with the item. - SmallLogoPath (string): Path to the small logo image for the tile. - PageID (string): Page identifier for the tile. - SettingID (string): Identifier for the setting tied to the item. - HostID (string): Host identifier for the system or app related to the item. - Condition (string): Condition of the setting or tile. - Comment (string): Comment or description attached to the item. - CacheFilePath (path): Path to the cache file from which this record is extracted. - - Notes: - - Empty, "N/A", or invalid entries (such as empty lists) are normalized to `None`. - - Timestamps are converted from Windows FILETIME format using `wintimestamp`. - - If a cache file cannot be parsed (e.g., due to invalid JSON format), a warning is logged, - and processing continues. + """Return Windows Search SettingsCache records for all users. + + Fields: + ParsingName: Internal parsing name of the settings entry + ActivationContext: Command executed when the setting launches + SmallLogoPath: Path to the tile logo + PageID: Settings page identifier + SettingID: Identifier for the setting + GroupID: Settings group identifier + HostID: GUID identifying the host component + Condition: Visibility condition + FontFamily: Font used for glyph + Glyph: Icon glyph + GlyphRtl: RTL glyph icon + HighKeywords: Search keywords for the setting + Comment: Human-readable description + CacheFilePath: Source cache file """ + for user, cache_file in self.cachefiles: with cache_file.open("r", encoding="utf-8") as cachefileIO: try: @@ -91,10 +99,15 @@ def settingscache(self) -> Iterator[WindowsSettingsCacheRecord]: SmallLogoPath=normalize_none(entry.get("System.Tile.SmallLogoPath", {}).get("Value")), PageID=normalize_none(entry.get("System.Setting.PageID", {}).get("Value")), SettingID=normalize_none(entry.get("System.Setting.SettingID", {}).get("Value")), + GroupID=normalize_none(entry.get("System.Setting.GroupID", {}).get("Value")), HostID=normalize_none(entry.get("System.Setting.HostID", {}).get("Value")), Condition=normalize_none(entry.get("System.Setting.Condition", {}).get("Value")), + FontFamily=normalize_none(entry.get("System.Setting.FontFamily", {}).get("Value")), + Glyph=normalize_none(entry.get("System.Setting.Glyph", {}).get("Value")), + GlyphRtl=normalize_none(entry.get("System.Setting.GlyphRtl", {}).get("Value")), + HighKeywords=normalize_none(entry.get("System.HighKeywords", {}).get("Value")), Comment=normalize_none(entry.get("System.Comment", {}).get("Value")), CacheFilePath=cache_file, _target=self.target, _user=user, - ) + ) \ No newline at end of file