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/screenclip.py b/dissect/target/plugins/os/windows/appdatapackages/screenclip.py new file mode 100644 index 0000000000..80f8b919cf --- /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 screenclip(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..38ee49f3fe --- /dev/null +++ b/dissect/target/plugins/os/windows/appdatapackages/settings_cache.py @@ -0,0 +1,113 @@ +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", "GroupID"), + ("string", "HostID"), + ("string", "Condition"), + ("string", "FontFamily"), + ("string", "Glyph"), + ("string", "GlyphRtl"), + ("string", "HighKeywords"), + ("string", "Comment"), + ("string", "CacheFilePath"), + ], +) + + +def normalize_none(value): + if value in ("", "N/A", "[]", [], None): + return None + return value + + +class settings_cache(Plugin): + """Extract Windows Search SettingsCache records.""" + + 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/SettingsCache.txt" + ) + + for cache_file in cache_files: + self.cachefiles.append((user_details.user, cache_file)) + + def check_compatible(self) -> None: + if not self.cachefiles: + raise UnsupportedPluginError("No SettingsCache files found") + + @export(record=WindowsSettingsCacheRecord) + def settingscache(self) -> Iterator[WindowsSettingsCacheRecord]: + """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: + 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")), + 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 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..cdfd89cd79 --- /dev/null +++ b/dissect/target/plugins/os/windows/appdatapackages/windows_search.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +from dissect.util.ts import wintimestamp + +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 + + +WindowsAppCacheRecord = create_extended_descriptor([UserRecordDescriptorExtension])( + "os/windows/appdata/packages/appcache", + [ + ("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"), + ("string", "CacheFilePath"), + ], +) + + +def normalize_none(input: str | list) -> str | list | None: + return None if input in ("", "N/A", "[]", []) else input + + +class app_cache(Plugin): + """Extract Windows Search AppCache records (Windows 10 only; 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") + 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=WindowsAppCacheRecord) + def appcache(self) -> Iterator[WindowsAppCacheRecord]: + """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: + 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 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"), + 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=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=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")), + CacheFilePath=cache_file, + _target=self.target, + _user=user, + ) 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/_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/_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_screen_clip.py b/tests/plugins/os/windows/appdatapackages/test_screen_clip.py new file mode 100644 index 0000000000..00e0e5ca3c --- /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 new file mode 100644 index 0000000000..df43144e47 --- /dev/null +++ b/tests/plugins/os/windows/appdatapackages/test_settings.py @@ -0,0 +1,36 @@ +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.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 + 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 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..369f674bea --- /dev/null +++ b/tests/plugins/os/windows/appdatapackages/test_windows_search.py @@ -0,0 +1,37 @@ +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.windows_search import app_cache + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.fixture +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\\AppCache134176063414577965.txt", + absolute_path("_data/plugins/os/windows/AppDataPackages/AppCache123123123.txt"), + ) + + target_win_users.add_plugin(app_cache) + + return target_win_users + + +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 == '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