Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
144 changes: 144 additions & 0 deletions dissect/target/plugins/os/windows/appdatapackages/screenclip.py
Original file line number Diff line number Diff line change
@@ -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,
)
113 changes: 113 additions & 0 deletions dissect/target/plugins/os/windows/appdatapackages/settings_cache.py
Original file line number Diff line number Diff line change
@@ -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,
)
128 changes: 128 additions & 0 deletions dissect/target/plugins/os/windows/appdatapackages/windows_search.py
Original file line number Diff line number Diff line change
@@ -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,
)
Git LFS file not shown
Git LFS file not shown
Loading