Skip to content
Merged
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
14 changes: 11 additions & 3 deletions admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ single registry label as the operating-system name.
| `windowsNotification.py` | Notifications | 3 rows | Adds handler identity, payload type, extracted text, payload size and SHA-256, retains the raw payload, and reports FILETIME values in UTC. The controlled toast token was recovered. |
| `windowsStickyNotes.py` | Sticky Notes | 2 rows | Retains empty notes with metadata, removes the internal text marker, adds note identifiers and window state, and reports updated/created/deleted .NET-tick times first in UTC. The controlled note token was recovered. |
| `setupapiDev.py` | SetupAPI Sections | 1 row | Parses complete SetupAPI sections rather than assuming every timestamp is a device's first connection. Times are labeled device-local because the log does not record a UTC offset. |
| `windowsPhotos.py` | Photos; Photos Folders | 3 media rows; 6 folder rows | Replaces the obsolete `MediaDb.v1.sqlite` target with the verified modern `LocalState/shared.sqlite`. A controlled image and folder addition produced recoverable ingestion, scan, file, path, dimension, and timestamp metadata. |
| `windowsAlarms.py` | Alarms | 2 rows | Parses both the packaged-app `settings.dat` hive and legacy `Alarms.json`. A controlled alarm recovered its exact name, scheduled local time, enabled/repeat state, snooze, chime resource, record ID, and FILETIME creation/update values. The predecessor's ad-hoc structure extraction was replaced with the read-only `python-registry` parser. |

The focused profile is `windows-system.dlprofile`.
The focused profiles are `windows-system.dlprofile` and
`windows-apps.dlprofile`.

## Retest when a representative artifact is available

Expand All @@ -33,9 +36,7 @@ The focused profile is `windows-system.dlprofile`.
| `dropbox.py` | Dropbox databases were not present. | Current Dropbox installation and controlled sync/history actions. |
| `googleDrive.py` | DriveFS metadata database was not present. | Current Google Drive for desktop installation and controlled sync actions. |
| `pfirewall.py` | `pfirewall.log` was absent. | A separately approved test that enables firewall logging, records its policy state, and produces known allowed/blocked traffic. |
| `windowsAlarms.py` | Clock `11.2605.10.0` requested an update. `settings.dat` existed, but no controlled alarm could be created. The WLEAPP parser contains a structure TODO and requires `pyregf`, which DLEAPP does not currently require. | A usable Clock build, known alarms, the JSON/registry-store variants, and dependency review. |
| `windowsEdge.py` | `WebCacheV01.dat` existed but was live-locked. Its evidentiary scope is legacy Edge/Internet Explorer rather than current Chromium Edge. | An offline byte-for-byte copy and known legacy-WebCache activity. Do not present it as current Edge browsing history. |
| `windowsPhotos.py` | Photos `2026.11020.20001.0` was present, but the WLEAPP target `MediaDb.v1.sqlite` was not found after a known image was placed in Pictures and Photos was opened. | Storage discovery and schema research for this Photos version before porting the old query. |
| `windowsYourPhone.py` | Current Phone Link and CrossDevice packages were installed, but the targeted databases were not present in the unpaired profile. | A dedicated synthetic phone/account pairing. Personal accounts or devices should not be used merely to obtain parser coverage. |

## Legacy candidates
Expand All @@ -53,3 +54,10 @@ retained in the collection manifest. For parser testing only, the SQLite backup
API produced read-only-source logical snapshots containing committed WAL data;
each snapshot passed `PRAGMA quick_check` and was hashed. Those snapshots are
examiner-derived and must not be described as original acquired files.

The second-wave Photos database is also an examiner-derived SQLite backup. The
Clock hive was copied only after Clock was closed. No new SetupAPI event was
generated: the available Parallels device menu exposed the mounted corpus/data
storage and an installation ISO, but no dedicated disposable virtual device.
Disconnecting those devices solely to manufacture coverage would risk the
test data or VM state and would be detrimental to the forensic purpose.
27 changes: 15 additions & 12 deletions admin/docs/generated/module_info.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ The data below is generated by the [@module_info.py](/admin/scripts/module_info.

## DLEAPP Summary

Total number of modules: 33
Total number of artifacts: 81
Number of v1 artifacts: 0
Number of v2 artifacts: 81
Number of artifacts with 'lava output': 81
Number of artifacts using 'artifact_icon': 81
Number of artifacts using 'last_update_date': 81
Number of artifacts using context parameter: 81
Number of artifacts with errors or no recognized artifacts: 1
Number of modules with artifact metadata issues: 0
Number of artifacts using **deprecated** 'version' key: 0
Number of artifacts using **deprecated** 'function' key: 0
Total number of modules: 34<br>
Total number of artifacts: 84<br>
Number of v1 artifacts: 0<br>
Number of v2 artifacts: 84<br>
Number of artifacts with 'lava output': 84<br>
Number of artifacts using 'artifact_icon': 84<br>
Number of artifacts using 'last_update_date': 84<br>
Number of artifacts using context parameter: 84<br>
Number of artifacts with errors or no recognized artifacts: 1<br>
Number of modules with artifact metadata issues: 0<br>
Number of artifacts using **deprecated** 'version' key: 0<br>
Number of artifacts using **deprecated** 'function' key: 0<br>

## DLEAPP V2 Artifacts Table

Expand Down Expand Up @@ -85,6 +85,9 @@ Number of artifacts using **deprecated** 'function' key: 0
| [whatsappContacts.py](/scripts/artifacts/whatsappContacts.py) | whatsappPushNames | WhatsApp Push Names | WhatsApp (Apple) | html, tsv, lava | Yes | tag | | 2026-07-27 | The JID-to-display-name mapping WhatsApp caches in ZWAPROFILEPUSHNAME. A push name is the name a correspondent set for themselves, as their client adv | `*/ChatStorage.sqlite*` |
| [whatsappMedia.py](/scripts/artifacts/whatsappMedia.py) | whatsappMedia | WhatsApp Media | WhatsApp (Apple) | html, tsv, timeline, lava | Yes | image | | 2026-07-27 | Media items recorded in ChatStorage.sqlite's ZWAMEDIAITEM table, each joined to its message for the date and chat. The stored file is embedded where i | `*/ChatStorage.sqlite*`, `*/Message/Media/*` |
| [whatsappMessages.py](/scripts/artifacts/whatsappMessages.py) | whatsappMessages | WhatsApp Messages | WhatsApp (Apple) | html, tsv, timeline, lava | Yes | message-circle | | 2026-07-27 | Messages from WhatsApp's ChatStorage.sqlite. Each row is joined to its chat session for the conversation name, to the group member record where the me | `*/ChatStorage.sqlite*`, `*/Message/Media/*` |
| [windowsApps.py](/scripts/artifacts/windowsApps.py) | windowsPhotos | Photos | Windows Apps | html, tsv, timeline, lava | Yes | image | | 2026-07-29 | Media indexed by modern Microsoft Photos, including ingestion and media times, original path, filename, dimensions, tags, rating, and location metadat | `*/AppData/Local/Packages/Microsoft.Windows.Photos_*/LocalState/shared.sqlite*` |
| [windowsApps.py](/scripts/artifacts/windowsApps.py) | windowsPhotosFolders | Photos Folders | Windows Apps | html, tsv, timeline, lava | Yes | folder | | 2026-07-29 | Folders indexed by modern Microsoft Photos, with scan, modified, and created times and indexed media counts. | `*/AppData/Local/Packages/Microsoft.Windows.Photos_*/LocalState/shared.sqlite*` |
| [windowsApps.py](/scripts/artifacts/windowsApps.py) | windowsAlarms | Alarms | Windows Apps | html, tsv, timeline, lava | Yes | clock | | 2026-07-29 | Windows Clock alarms from the modern packaged-app settings hive or the older Alarms.json format, including scheduled, created, and updated times. | `*/AppData/Local/Packages/Microsoft.WindowsAlarms_*/LocalState/Alarms/Alarms.json`, `*/AppData/Local/Packages/Microsoft.WindowsAlarms_*/Settings/settings.dat` |
| [windowsSystem.py](/scripts/artifacts/windowsSystem.py) | activitiesCache | ActivitiesCache | Windows System | html, tsv, timeline, lava | Yes | activity | | 2026-07-29 | Windows Connected Devices Platform activity records, including event times, application identifiers, status fields, and preserved payload content. | `*/AppData/Local/ConnectedDevicesPlatform/*/ActivitiesCache.db*` |
| [windowsSystem.py](/scripts/artifacts/windowsSystem.py) | windowsNotifications | Notifications | Windows System | html, tsv, timeline, lava | Yes | bell | | 2026-07-29 | Windows notification records with arrival and expiry times, handler identity, notification type, extracted text, and the preserved payload. | `*/AppData/Local/Microsoft/Windows/Notifications/wpndatabase.db*` |
| [windowsSystem.py](/scripts/artifacts/windowsSystem.py) | windowsStickyNotes | Sticky Notes | Windows System | html, tsv, timeline, lava | Yes | file-text | | 2026-07-29 | Windows Sticky Notes content and state, including updated, created, and deleted times, note identifiers, open state, pin state, theme, and window posi | `*/AppData/Local/Packages/Microsoft.MicrosoftStickyNotes_*/LocalState/plum.sqlite*` |
Expand Down
24 changes: 12 additions & 12 deletions admin/scripts/module_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,26 +429,26 @@ def update_markdown_file(v1_data, v2_data, error_data, issue_data):

# Generate new markdown content
new_module_info = f"## {section_title('Summary')}\n\n"
new_module_info += f"Total number of modules: {total_modules} \n"
new_module_info += f"Total number of artifacts: {total_artifacts} \n"
new_module_info += f"Number of v1 artifacts: {v1_count} \n"
new_module_info += f"Number of v2 artifacts: {v2_count} \n"
new_module_info += f"Total number of modules: {total_modules}<br>\n"
new_module_info += f"Total number of artifacts: {total_artifacts}<br>\n"
new_module_info += f"Number of v1 artifacts: {v1_count}<br>\n"
new_module_info += f"Number of v2 artifacts: {v2_count}<br>\n"
new_module_info += "Number of artifacts with 'lava output': " + \
f"{lava_output_count} \n"
f"{lava_output_count}<br>\n"
new_module_info += "Number of artifacts using 'artifact_icon': " + \
f"{artifact_icon_count} \n"
f"{artifact_icon_count}<br>\n"
new_module_info += "Number of artifacts using 'last_update_date': " + \
f"{last_update_date_count} \n"
f"{last_update_date_count}<br>\n"
new_module_info += "Number of artifacts using context parameter: " + \
f"{context_count} \n"
f"{context_count}<br>\n"
new_module_info += "Number of artifacts with errors or no recognized " + \
f"artifacts: {error_count} \n"
f"artifacts: {error_count}<br>\n"
new_module_info += "Number of modules with artifact metadata issues: " + \
f"{issue_count} \n"
f"{issue_count}<br>\n"
new_module_info += "Number of artifacts using **deprecated** 'version' " + \
f"key: {version_count} \n"
f"key: {version_count}<br>\n"
new_module_info += "Number of artifacts using **deprecated** 'function' " + \
f"key: {function_key_count} \n\n"
f"key: {function_key_count}<br>\n\n"

if v2_data:
new_module_info += \
Expand Down
235 changes: 235 additions & 0 deletions admin/test/scripts/test_windows_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
"""Tests for modern Windows Photos and Clock artifacts."""

# pylint: disable=protected-access

import sqlite3
from datetime import datetime, timezone

from scripts.artifacts import windowsApps


class _Context:
def __init__(self, files, seeker=None):
self._files = files
self._seeker = seeker

def get_files_found(self):
return self._files

def set_files_found(self, files):
self._files = files

def get_seeker(self):
return self._seeker or _Seeker({})

@staticmethod
def get_relative_path(path):
return str(path)


class _Seeker:
def __init__(self, matches):
self._matches = matches
self.patterns = []

def search(self, pattern):
self.patterns.append(pattern)
return self._matches.get(pattern, [])


def _create_photos_database(path):
with sqlite3.connect(path) as database:
database.executescript(
"""
CREATE TABLE mediaFolder (
FolderId, Path, ParentFolderId, IsLibraryFolder, ProviderKey,
FolderAttributes, DateCreated, DateModified, SumAllFileDates,
SumMediaFileDates, ScannedMediaFileCount, DateScanned
);
CREATE TABLE mediaItemFile (
FolderId, FileName, MediaItemKey, FileAttributes, ProviderKey,
DateCreated, DateModified, DateIngested, FileSize, IsImage,
AlternateDateTaken
);
CREATE TABLE mediaItemProps (
MediaItemKey, PropVersion, PropScanDate, DateModified,
DateTaken, Width, Height, Media_Duration, Rating, UserTags,
Latitude, Longitude, LatitudeBucket, LongitudeBucket
);
CREATE TABLE mediaItemAddresses (
LatitudeBucket, LongitudeBucket, DatePreviouslyFailed, Version,
Locale, Country, Region, Town, NormalizedAddress
);
CREATE TABLE mediaItemCategory (
MediaItemKey, Category, RelevanceScore
);
CREATE TABLE mediaItemDates (
MediaItemKey, Locale, DateFormatterType, Date, FormattedDate
);
INSERT INTO mediaFolder VALUES (
1, 'C:\\Evidence\\Pictures', 0, 1, 2, 17,
116444736010000000, 116444736020000000, 0, 0, 1,
30000000
);
INSERT INTO mediaItemFile VALUES (
1, 'DLEAPP-PHOTO-TEST-001.png', 42, 128, 2,
116444736010000000, 116444736020000000,
116444736030000000, 1234, 1, 116444736010000000
);
INSERT INTO mediaItemProps VALUES (
42, 1, 116444736040000000, 116444736020000000,
116444736015000000, 1024, 768, NULL, 5, 'known-tag',
10.5, -20.25, 10.5, -20.25
);
INSERT INTO mediaItemAddresses VALUES (
10.5, -20.25, NULL, 1, 'en-US', 'Country', 'Region',
'Town', 'Known Address'
);
INSERT INTO mediaItemCategory VALUES (42, 'document', 0.75);
INSERT INTO mediaItemDates VALUES (
42, 'en-US', 7, 116444736015000000, 'January 1, 1970'
);
"""
)


def test_photos_timestamp_order_preview_and_metadata(tmp_path, monkeypatch):
database_path = tmp_path / "shared.sqlite"
_create_photos_database(database_path)
media_path = tmp_path / "C" / "Evidence" / "Pictures" / (
"DLEAPP-PHOTO-TEST-001.png"
)
media_path.parent.mkdir(parents=True)
media_path.write_bytes(b"known image")
seeker = _Seeker({
"*/Evidence/Pictures/DLEAPP-PHOTO-TEST-001.png": [media_path],
})
checked_in = []

def _check_in_media(path, name=""):
checked_in.append((path, name))
return "known-media-reference"

monkeypatch.setattr(windowsApps, "check_in_media", _check_in_media)

headers, rows, _ = windowsApps.windowsPhotos.__wrapped__(
_Context([database_path], seeker)
)

assert [header[0] for header in headers[:6]] == [
"Date Ingested (UTC)",
"Date Taken (UTC)",
"Alternate Date Taken (UTC)",
"Date Modified (UTC)",
"Date Created (UTC)",
"Property Scan Time (UTC)",
]
assert headers[6:8] == (
("Media Preview", "media"),
"Original File Status",
)
assert len(rows) == 1
assert rows[0][0] == datetime(1970, 1, 1, 0, 0, 3, tzinfo=timezone.utc)
assert rows[0][6] == "known-media-reference"
assert rows[0][7] == "Present in acquisition; copied to report"
assert rows[0][10] == "DLEAPP-PHOTO-TEST-001.png"
assert rows[0][17] == "known-tag"
assert rows[0][23] == "Known Address"
assert rows[0][24] == "document [0.75]"
assert checked_in == [(
"C:/Evidence/Pictures/DLEAPP-PHOTO-TEST-001.png",
"DLEAPP-PHOTO-TEST-001.png",
)]


def test_photos_reports_missing_original_without_inferring_deletion(tmp_path):
database_path = tmp_path / "shared.sqlite"
_create_photos_database(database_path)

_, rows, _ = windowsApps.windowsPhotos.__wrapped__(
_Context([database_path], _Seeker({}))
)

assert rows[0][6] == ""
assert rows[0][7] == "Original file not present in acquisition"


def test_photos_folders_unix_100ns_scan_time(tmp_path):
database_path = tmp_path / "shared.sqlite"
_create_photos_database(database_path)

headers, rows, _ = windowsApps.windowsPhotosFolders.__wrapped__(
_Context([database_path])
)

assert headers[:3] == (
("Date Scanned (UTC)", "datetime"),
("Date Modified (UTC)", "datetime"),
("Date Created (UTC)", "datetime"),
)
assert rows[0][0] == datetime(1970, 1, 1, 0, 0, 3, tzinfo=timezone.utc)
assert rows[0][5] == r"C:\Evidence\Pictures"


def test_alarm_composite_timestamp_order_and_fields():
alarm = {
"Name": "DLEAPP-ALARM-TEST-001\x00",
"Hour": 2,
"Minute": 5,
"IsEnabled": True,
"DaysOfWeek": 0,
"SnoozeInterval": 10,
"ScheduledYear": 2026,
"ScheduledMonth": 7,
"ScheduledDay": 30,
"ScheduledHour": 2,
"ScheduledMinute": 5,
"ChimeName": "Alarm1/SoundName\x00",
"ChimePath": "ms-winsoundevent:Notification.Looping.Alarm\x00",
"__Created": 116444736010000000,
"__Updated": 116444736020000000,
}

row = windowsApps._alarm_row(
alarm, "{KNOWN-RECORD}", "Packaged-app settings hive", "settings.dat"
)

assert row[:3] == (
datetime(2026, 7, 30, 2, 5),
datetime(1970, 1, 1, 0, 0, 1, tzinfo=timezone.utc),
datetime(1970, 1, 1, 0, 0, 2, tzinfo=timezone.utc),
)
assert row[3:9] == (
"DLEAPP-ALARM-TEST-001",
"02:05",
"Yes",
"No",
0,
10,
)


def test_alarm_json_retains_every_alarm(tmp_path):
alarms_path = tmp_path / "Alarms.json"
alarms_path.write_text(
"""
{
"Alarms": [
{"Name": "one", "Hour": 1, "Minute": 2, "IsEnabled": true},
{"Name": "two", "Hour": 3, "Minute": 4, "IsEnabled": false}
]
}
""",
encoding="utf-8",
)

headers, rows, _ = windowsApps.windowsAlarms.__wrapped__(
_Context([alarms_path])
)

assert headers[:3] == (
("Next Scheduled Time (device local)", "datetime"),
("Created Time (UTC)", "datetime"),
("Updated Time (UTC)", "datetime"),
)
assert [row[3] for row in rows] == ["one", "two"]
Loading
Loading