diff --git a/admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md b/admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md
index 7588131..956204c 100644
--- a/admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md
+++ b/admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md
@@ -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
@@ -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
@@ -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.
diff --git a/admin/docs/generated/module_info.md b/admin/docs/generated/module_info.md
index 6ac27f0..7811e27 100644
--- a/admin/docs/generated/module_info.md
+++ b/admin/docs/generated/module_info.md
@@ -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
+Total number of artifacts: 84
+Number of v1 artifacts: 0
+Number of v2 artifacts: 84
+Number of artifacts with 'lava output': 84
+Number of artifacts using 'artifact_icon': 84
+Number of artifacts using 'last_update_date': 84
+Number of artifacts using context parameter: 84
+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
## DLEAPP V2 Artifacts Table
@@ -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*` |
diff --git a/admin/scripts/module_info.py b/admin/scripts/module_info.py
index b36de00..75713e8 100644
--- a/admin/scripts/module_info.py
+++ b/admin/scripts/module_info.py
@@ -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}
\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 += "Number of artifacts with 'lava output': " + \
- f"{lava_output_count} \n"
+ f"{lava_output_count}
\n"
new_module_info += "Number of artifacts using 'artifact_icon': " + \
- f"{artifact_icon_count} \n"
+ f"{artifact_icon_count}
\n"
new_module_info += "Number of artifacts using 'last_update_date': " + \
- f"{last_update_date_count} \n"
+ f"{last_update_date_count}
\n"
new_module_info += "Number of artifacts using context parameter: " + \
- f"{context_count} \n"
+ f"{context_count}
\n"
new_module_info += "Number of artifacts with errors or no recognized " + \
- f"artifacts: {error_count} \n"
+ f"artifacts: {error_count}
\n"
new_module_info += "Number of modules with artifact metadata issues: " + \
- f"{issue_count} \n"
+ f"{issue_count}
\n"
new_module_info += "Number of artifacts using **deprecated** 'version' " + \
- f"key: {version_count} \n"
+ f"key: {version_count}
\n"
new_module_info += "Number of artifacts using **deprecated** 'function' " + \
- f"key: {function_key_count} \n\n"
+ f"key: {function_key_count}
\n\n"
if v2_data:
new_module_info += \
diff --git a/admin/test/scripts/test_windows_apps.py b/admin/test/scripts/test_windows_apps.py
new file mode 100644
index 0000000..4975d15
--- /dev/null
+++ b/admin/test/scripts/test_windows_apps.py
@@ -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"]
diff --git a/admin/windows_lab/Get-DLEAPPAppStorageInventory.ps1 b/admin/windows_lab/Get-DLEAPPAppStorageInventory.ps1
new file mode 100644
index 0000000..7ba5965
--- /dev/null
+++ b/admin/windows_lab/Get-DLEAPPAppStorageInventory.ps1
@@ -0,0 +1,132 @@
+# Inventory modern Windows app storage without modifying app data.
+# Authors: @AlexisBrignoni, Codex
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [ValidatePattern("^[A-Za-z0-9_-]+$")]
+ [string]$Phase,
+
+ [string]$OutputRoot = "C:\DLEAPP_Lab\AppStorageInventory",
+
+ [long]$MaximumHashBytes = 67108864
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version 2.0
+. "$PSScriptRoot\DLEAPPLab.Common.ps1"
+
+$collectedUtc = [DateTime]::UtcNow
+$phaseRoot = Join-Path $OutputRoot $Phase
+New-DLEAPPDirectory -Path $phaseRoot
+
+$targets = @(
+ [pscustomobject]@{
+ Artifact = "Windows Photos"
+ PackagePattern = "Microsoft.Windows.Photos_*"
+ }
+ [pscustomobject]@{
+ Artifact = "Windows Clock"
+ PackagePattern = "Microsoft.WindowsAlarms_*"
+ }
+)
+
+$packagesRoot = Join-Path $env:LOCALAPPDATA "Packages"
+$packageRows = @()
+$fileRows = @()
+
+foreach ($target in $targets) {
+ $packageDirectories = @(
+ Get-ChildItem -LiteralPath $packagesRoot -Directory -Force `
+ -ErrorAction SilentlyContinue |
+ Where-Object Name -Like $target.PackagePattern
+ )
+
+ foreach ($packageDirectory in $packageDirectories) {
+ $package = Get-AppxPackage |
+ Where-Object PackageFamilyName -eq $packageDirectory.Name |
+ Select-Object -First 1
+
+ $packageRows += [pscustomobject][ordered]@{
+ CollectedUtc = $collectedUtc.ToString("o")
+ Artifact = $target.Artifact
+ PackageFamilyName = $packageDirectory.Name
+ PackageName = if ($null -ne $package) { $package.Name } else { "" }
+ PackageVersion = if ($null -ne $package) {
+ $package.Version.ToString()
+ }
+ else {
+ ""
+ }
+ PackagePath = $packageDirectory.FullName
+ }
+
+ $files = @(
+ Get-ChildItem -LiteralPath $packageDirectory.FullName -File `
+ -Recurse -Force -ErrorAction SilentlyContinue
+ )
+ foreach ($file in $files) {
+ $relativePath = $file.FullName.Substring(
+ $packageDirectory.FullName.Length
+ ).TrimStart("\")
+ $hash = ""
+ $hashStatus = "Not attempted"
+ if ($file.Length -le $MaximumHashBytes) {
+ try {
+ $hash = (
+ Get-FileHash -LiteralPath $file.FullName `
+ -Algorithm SHA256 -ErrorAction Stop 2>$null
+ ).Hash
+ $hashStatus = "Hashed"
+ }
+ catch {
+ $hashStatus = $_.Exception.Message
+ }
+ }
+ else {
+ $hashStatus = "Skipped: file exceeds MaximumHashBytes"
+ }
+
+ $fileRows += [pscustomobject][ordered]@{
+ ModifiedUtc = $file.LastWriteTimeUtc.ToString("o")
+ CreatedUtc = $file.CreationTimeUtc.ToString("o")
+ CollectedUtc = $collectedUtc.ToString("o")
+ Artifact = $target.Artifact
+ PackageFamilyName = $packageDirectory.Name
+ RelativePath = $relativePath
+ Extension = $file.Extension
+ Length = $file.Length
+ SHA256 = $hash
+ HashStatus = $hashStatus
+ }
+ }
+ }
+}
+
+$packageRows |
+ Sort-Object Artifact, PackageFamilyName |
+ Export-Csv -LiteralPath (Join-Path $phaseRoot "packages.tsv") `
+ -Delimiter "`t" -NoTypeInformation -Encoding UTF8
+
+$fileRows |
+ Sort-Object Artifact, PackageFamilyName, RelativePath |
+ Export-Csv -LiteralPath (Join-Path $phaseRoot "files.tsv") `
+ -Delimiter "`t" -NoTypeInformation -Encoding UTF8
+
+$summary = [ordered]@{
+ CollectedUtc = $collectedUtc.ToString("o")
+ Phase = $Phase
+ PackageCount = $packageRows.Count
+ FileCount = $fileRows.Count
+ OutputRoot = $phaseRoot
+ MaximumHashBytes = $MaximumHashBytes
+}
+$summary |
+ ConvertTo-Json -Depth 4 |
+ Set-Content -LiteralPath (Join-Path $phaseRoot "summary.json") `
+ -Encoding UTF8
+
+Write-Output (
+ "App storage inventory '{0}' written to {1}; packages={2}; files={3}" -f
+ $Phase, $phaseRoot, $packageRows.Count, $fileRows.Count
+)
diff --git a/admin/windows_lab/Invoke-DLEAPPClockValidation.ps1 b/admin/windows_lab/Invoke-DLEAPPClockValidation.ps1
new file mode 100644
index 0000000..17cfeb5
--- /dev/null
+++ b/admin/windows_lab/Invoke-DLEAPPClockValidation.ps1
@@ -0,0 +1,33 @@
+# Launch Windows Clock and journal the controlled validation session.
+# Authors: @AlexisBrignoni, Codex
+
+[CmdletBinding()]
+param(
+ [string]$Token = "DLEAPP-ALARM-WAVE2-001"
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version 2.0
+. "$PSScriptRoot\DLEAPPLab.Common.ps1"
+
+$journalScript = Join-Path $PSScriptRoot "Write-DLEAPPAction.ps1"
+$clock = Get-StartApps |
+ Where-Object Name -Match "^Clock$|Alarm" |
+ Select-Object -First 1
+if ($null -eq $clock) {
+ throw "Windows Clock is not registered in the Start menu."
+}
+
+& $journalScript `
+ -Artifact "Windows Clock" `
+ -Action "Started controlled alarm validation" `
+ -Token $Token `
+ -Details ("AppUserModelId={0}; create through app UI" -f $clock.AppID)
+
+Start-Process "explorer.exe" -ArgumentList (
+ "shell:AppsFolder\{0}" -f $clock.AppID
+)
+
+Write-Output (
+ "Clock launched. Create alarm token through the app UI: {0}" -f $Token
+)
diff --git a/admin/windows_lab/Invoke-DLEAPPPhotosValidation.ps1 b/admin/windows_lab/Invoke-DLEAPPPhotosValidation.ps1
new file mode 100644
index 0000000..e2fd0ec
--- /dev/null
+++ b/admin/windows_lab/Invoke-DLEAPPPhotosValidation.ps1
@@ -0,0 +1,48 @@
+# Create and open a controlled local image for Windows Photos validation.
+# Authors: @AlexisBrignoni, Codex
+
+[CmdletBinding()]
+param(
+ [string]$KnownImagePath = "",
+ [string]$Token = "DLEAPP-PHOTOS-WAVE2-001"
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version 2.0
+. "$PSScriptRoot\DLEAPPLab.Common.ps1"
+
+$journalScript = Join-Path $PSScriptRoot "Write-DLEAPPAction.ps1"
+if ([string]::IsNullOrWhiteSpace($KnownImagePath)) {
+ $KnownImagePath = Join-Path $PSScriptRoot "..\..\assets\DLEAPP_logo.png"
+}
+if (-not (Test-Path -LiteralPath $KnownImagePath)) {
+ throw "Known image does not exist: $KnownImagePath"
+}
+
+$destination = Join-Path $env:USERPROFILE ("Pictures\{0}.png" -f $Token)
+Copy-Item -LiteralPath $KnownImagePath -Destination $destination -Force
+$hash = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash
+
+& $journalScript `
+ -Artifact "Windows Photos" `
+ -Action "Copied controlled image into Pictures" `
+ -Token $Token `
+ -Details ("Path={0}; SHA256={1}" -f $destination, $hash)
+
+$photos = Get-StartApps |
+ Where-Object Name -Match "^Photos$|Microsoft Photos" |
+ Select-Object -First 1
+if ($null -eq $photos) {
+ throw "Microsoft Photos is not registered in the Start menu."
+}
+
+Start-Process "explorer.exe" -ArgumentList (
+ "shell:AppsFolder\{0}" -f $photos.AppID
+)
+& $journalScript `
+ -Artifact "Windows Photos" `
+ -Action "Launched Photos after controlled image copy" `
+ -Token ($Token -replace "-001$", "-LAUNCH-001") `
+ -Details $photos.AppID
+
+Write-Output ("Controlled Photos input: {0}; SHA256={1}" -f $destination, $hash)
diff --git a/admin/windows_lab/Invoke-DLEAPPWaveOne.ps1 b/admin/windows_lab/Invoke-DLEAPPWaveOne.ps1
index 0619baa..96e1e6c 100644
--- a/admin/windows_lab/Invoke-DLEAPPWaveOne.ps1
+++ b/admin/windows_lab/Invoke-DLEAPPWaveOne.ps1
@@ -5,9 +5,7 @@
[CmdletBinding()]
param(
[string]$LabRoot = "C:\DLEAPP_Lab",
- [string]$KnownImagePath = (
- Join-Path $PSScriptRoot "..\..\assets\DLEAPP_logo.png"
- ),
+ [string]$KnownImagePath = "",
[switch]$LaunchApplications
)
@@ -16,6 +14,9 @@ Set-StrictMode -Version 2.0
. "$PSScriptRoot\DLEAPPLab.Common.ps1"
$journalScript = Join-Path $PSScriptRoot "Write-DLEAPPAction.ps1"
+if ([string]::IsNullOrWhiteSpace($KnownImagePath)) {
+ $KnownImagePath = Join-Path $PSScriptRoot "..\..\assets\DLEAPP_logo.png"
+}
$knownInputRoot = Join-Path $LabRoot "KnownInputs"
New-DLEAPPDirectory -Path $knownInputRoot
diff --git a/admin/windows_lab/README.md b/admin/windows_lab/README.md
index 1294006..9fb43f3 100644
--- a/admin/windows_lab/README.md
+++ b/admin/windows_lab/README.md
@@ -31,6 +31,18 @@ The inventory is written to `C:\DLEAPP_Lab\Inventory`. It records the operating
system and time-zone context, relevant AppX packages and Start applications,
candidate artifact paths, file timestamps, sizes, and SHA-256 hashes.
+For before/after discovery of modern Photos and Clock storage:
+
+```powershell
+powershell.exe -ExecutionPolicy Bypass -File `
+ "\\Mac\Home\Documents\GitHub\DLEAPP\admin\windows_lab\Get-DLEAPPAppStorageInventory.ps1" `
+ -Phase "baseline"
+```
+
+Each phase is stored separately under
+`C:\DLEAPP_Lab\AppStorageInventory`. Hash failures caused by a live file lock
+are retained rather than silently omitted.
+
## Action journal
Record an action immediately before or after performing it:
@@ -63,6 +75,19 @@ The script intentionally does not enable firewall logging. That is a
security-sensitive system setting and should be handled as a separately
documented test.
+## Controlled Photos and Clock validation
+
+`Invoke-DLEAPPPhotosValidation.ps1` copies a known local image, hashes it,
+journals the action, and launches Photos. Adding its local folder through the
+Photos UI produced records in the current `LocalState\shared.sqlite` database;
+the script does not write that database.
+
+`Invoke-DLEAPPClockValidation.ps1` journals and launches a Clock session. Create
+the named alarm through the normal UI, journal its actual settings, close Clock,
+and then collect `Settings\settings.dat` and its transaction logs. DLEAPP reads
+the offline hive with `python-registry`; it does not load evidence into the
+examiner system registry.
+
## Logical collection
Choose a destination visible to Windows, such as a Parallels shared directory:
diff --git a/admin/windows_lab/Snapshot-DLEAPPLiveSqlite.py b/admin/windows_lab/Snapshot-DLEAPPLiveSqlite.py
index 7739999..e1266bb 100644
--- a/admin/windows_lab/Snapshot-DLEAPPLiveSqlite.py
+++ b/admin/windows_lab/Snapshot-DLEAPPLiveSqlite.py
@@ -33,6 +33,14 @@
"Windows Photos",
"Packages/Microsoft.Windows.Photos_*/LocalState/MediaDb*.sqlite",
),
+ (
+ "Windows Photos",
+ "Packages/Microsoft.Windows.Photos_*/LocalState/shared.sqlite",
+ ),
+ (
+ "Windows Photos",
+ "Packages/Microsoft.Windows.Photos_*/LocalState/standalone.sqlite",
+ ),
(
"Windows Sticky Notes",
"Packages/Microsoft.MicrosoftStickyNotes_*/LocalState/plum.sqlite",
diff --git a/requirements.txt b/requirements.txt
index 0e145d1..0cbb267 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,4 +19,5 @@ mammoth
bencoding
pytz
requests
-pdfminer.six
\ No newline at end of file
+pdfminer.six
+python-registry==1.3.1
diff --git a/scripts/artifacts/windowsApps.py b/scripts/artifacts/windowsApps.py
new file mode 100644
index 0000000..5999050
--- /dev/null
+++ b/scripts/artifacts/windowsApps.py
@@ -0,0 +1,502 @@
+"""Forensically useful Windows app artifacts modernized from WLEAPP.
+
+Authors: @AlexisBrignoni, Codex
+Predecessor: abrignoni/WLEAPP windowsAlarms.py and windowsPhotos.py.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import re
+from datetime import datetime, timedelta, timezone
+
+try:
+ from Registry import Registry
+except ImportError:
+ Registry = None
+
+from scripts.ilapfuncs import (
+ artifact_processor,
+ check_in_media,
+ logfunc,
+ open_sqlite_db_readonly,
+)
+
+
+__artifacts_v2__ = {
+ "windowsPhotos": {
+ "name": "Photos",
+ "description": "Media indexed by modern Microsoft Photos, including "
+ "ingestion and media times, original path, filename, "
+ "dimensions, tags, rating, and location metadata. The "
+ "original media is previewed when it remains present "
+ "at the database-recorded path in the acquisition.",
+ "author": "@AlexisBrignoni, Codex",
+ "creation_date": "2026-07-29",
+ "last_update_date": "2026-07-29",
+ "requirements": "none",
+ "category": "Windows Apps",
+ "notes": "Validated with Photos 2026.11020.20001.0. The modern "
+ "database is LocalState/shared.sqlite. Numeric timestamps "
+ "are converted from Windows FILETIME; their original "
+ "application-level timezone semantics may vary. Alternate "
+ "Date Taken is an app-maintained fallback and must not be "
+ "treated as proof of capture time. Zero coordinates are "
+ "retained as stored and do not by themselves prove a "
+ "location. A blank preview accompanied by 'Original file "
+ "not present in acquisition' means the database row remains "
+ "but the referenced file was not supplied to DLEAPP; it does "
+ "not prove deletion. ImageEmbeddings data is not treated as "
+ "image content.",
+ "paths": (
+ "*/AppData/Local/Packages/Microsoft.Windows.Photos_*/"
+ "LocalState/shared.sqlite*",
+ ),
+ "output_types": ["html", "tsv", "timeline", "lava"],
+ "artifact_icon": "image",
+ "sample_data": {
+ "windows11_arm_parallels": (
+ "Photos 2026.11020.20001.0 | 3 controlled rows"
+ ),
+ },
+ },
+ "windowsPhotosFolders": {
+ "name": "Photos Folders",
+ "description": "Folders indexed by modern Microsoft Photos, with "
+ "scan, modified, and created times and indexed media "
+ "counts.",
+ "author": "@AlexisBrignoni, Codex",
+ "creation_date": "2026-07-29",
+ "last_update_date": "2026-07-29",
+ "requirements": "none",
+ "category": "Windows Apps",
+ "notes": "Validated by adding a controlled local folder in Photos "
+ "2026.11020.20001.0. DateScanned uses 100-nanosecond units "
+ "from the Unix epoch; folder dates use Windows FILETIME.",
+ "paths": (
+ "*/AppData/Local/Packages/Microsoft.Windows.Photos_*/"
+ "LocalState/shared.sqlite*",
+ ),
+ "output_types": ["html", "tsv", "timeline", "lava"],
+ "artifact_icon": "folder",
+ "sample_data": {
+ "windows11_arm_parallels": (
+ "Photos 2026.11020.20001.0 | 6 folders"
+ ),
+ },
+ },
+ "windowsAlarms": {
+ "name": "Alarms",
+ "description": "Windows Clock alarms from the modern packaged-app "
+ "settings hive or the older Alarms.json format, "
+ "including scheduled, created, and updated times.",
+ "author": "@AlexisBrignoni, Codex",
+ "creation_date": "2026-07-29",
+ "last_update_date": "2026-07-29",
+ "requirements": "python-registry",
+ "category": "Windows Apps",
+ "notes": "Modernized from WLEAPP and validated with Clock "
+ "11.2605.10.0. Scheduled fields are device-local; created "
+ "and updated values are converted from Windows FILETIME. "
+ "The registry parser reads offline hives without loading "
+ "them into the examiner system registry.",
+ "paths": (
+ "*/AppData/Local/Packages/Microsoft.WindowsAlarms_*/"
+ "LocalState/Alarms/Alarms.json",
+ "*/AppData/Local/Packages/Microsoft.WindowsAlarms_*/"
+ "Settings/settings.dat",
+ ),
+ "output_types": ["html", "tsv", "timeline", "lava"],
+ "artifact_icon": "clock",
+ "sample_data": {
+ "windows11_arm_parallels": (
+ "Clock 11.2605.10.0 | 2 alarms"
+ ),
+ },
+ },
+}
+
+
+_UNIX_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
+_WINDOWS_FILETIME_EPOCH_TICKS = 116444736000000000
+_TICKS_PER_SECOND = 10_000_000
+
+
+def _utc_from_filetime(value):
+ if value in (None, "", 0):
+ return ""
+ try:
+ seconds = (int(value) - _WINDOWS_FILETIME_EPOCH_TICKS) / _TICKS_PER_SECOND
+ return _UNIX_EPOCH + timedelta(seconds=seconds)
+ except (OverflowError, TypeError, ValueError):
+ return ""
+
+
+def _utc_from_unix_100ns(value):
+ if value in (None, "", 0):
+ return ""
+ try:
+ return _UNIX_EPOCH + timedelta(seconds=int(value) / _TICKS_PER_SECOND)
+ except (OverflowError, TypeError, ValueError):
+ return ""
+
+
+def _yes_no(value):
+ if value is True or value == 1:
+ return "Yes"
+ if value is False or value == 0:
+ return "No"
+ return "Unknown" if value is not None else ""
+
+
+def _clean_text(value):
+ return value.rstrip("\x00") if isinstance(value, str) else value or ""
+
+
+def _recorded_media_path(folder, filename):
+ """Build a normalized Windows path suitable for evidence-file matching."""
+ if not folder or not filename:
+ return ""
+ normalized_folder = str(folder).replace("\\", "/").rstrip("/")
+ normalized_name = str(filename).replace("\\", "/").rsplit("/", 1)[-1]
+ return f"{normalized_folder}/{normalized_name}"
+
+
+def _escape_fnmatch(value):
+ """Escape evidence-controlled glob metacharacters for an exact search."""
+ return value.replace("[", "[[]").replace("*", "[*]").replace("?", "[?]")
+
+
+def _media_search_pattern(recorded_path):
+ """Return a suffix glob for a drive-letter, UNC, or relative Windows path."""
+ normalized = recorded_path.replace("\\", "/")
+ if re.match(r"^[A-Za-z]:/", normalized):
+ normalized = normalized[2:]
+ normalized = normalized.lstrip("/")
+ return f"*/{_escape_fnmatch(normalized)}"
+
+
+def _extract_referenced_media(context, recorded_paths):
+ """Extract only media files explicitly referenced by a Photos database."""
+ seeker = context.get_seeker()
+ original_files = list(context.get_files_found())
+ extracted_files = []
+ present_paths = set()
+
+ for recorded_path in dict.fromkeys(path for path in recorded_paths if path):
+ matches = seeker.search(_media_search_pattern(recorded_path))
+ if matches:
+ present_paths.add(recorded_path)
+ extracted_files.extend(matches)
+
+ if extracted_files:
+ context.set_files_found(list(dict.fromkeys(original_files + extracted_files)))
+ return present_paths
+
+
+def _photos_records(database):
+ return database.execute(
+ """
+ SELECT f.DateIngested, p.DateTaken, f.AlternateDateTaken,
+ f.DateModified, f.DateCreated, p.PropScanDate, f.MediaItemKey,
+ d.Path, f.FileName, f.FileSize, f.IsImage, p.Width, p.Height,
+ p.Media_Duration, p.Rating, p.UserTags, p.Latitude,
+ p.Longitude, a.Country, a.Region, a.Town, a.NormalizedAddress,
+ (
+ SELECT group_concat(
+ c.Category || ' [' || c.RelevanceScore || ']', ' | '
+ )
+ FROM mediaItemCategory AS c
+ WHERE c.MediaItemKey = f.MediaItemKey
+ ) AS Categories,
+ (
+ SELECT group_concat(
+ md.DateFormatterType || ': ' || md.FormattedDate, ' | '
+ )
+ FROM mediaItemDates AS md
+ WHERE md.MediaItemKey = f.MediaItemKey
+ ) AS FormattedDates
+ FROM mediaItemFile AS f
+ JOIN mediaFolder AS d ON d.FolderId = f.FolderId
+ LEFT JOIN mediaItemProps AS p ON p.MediaItemKey = f.MediaItemKey
+ LEFT JOIN mediaItemAddresses AS a
+ ON a.LatitudeBucket = p.LatitudeBucket
+ AND a.LongitudeBucket = p.LongitudeBucket
+ ORDER BY f.DateIngested DESC
+ """
+ ).fetchall()
+
+
+@artifact_processor
+def windowsPhotos(context):
+ data_headers = (
+ ("Date Ingested (UTC)", "datetime"),
+ ("Date Taken (UTC)", "datetime"),
+ ("Alternate Date Taken (UTC)", "datetime"),
+ ("Date Modified (UTC)", "datetime"),
+ ("Date Created (UTC)", "datetime"),
+ ("Property Scan Time (UTC)", "datetime"),
+ ("Media Preview", "media"),
+ "Original File Status",
+ "Media Item Key",
+ "Folder",
+ "Filename",
+ "File Size (bytes)",
+ "Image",
+ "Width",
+ "Height",
+ "Media Duration (database value)",
+ "Rating",
+ "User Tags",
+ "Latitude",
+ "Longitude",
+ "Country",
+ "Region",
+ "Town",
+ "Normalized Address",
+ "Categories [relevance]",
+ "Formatted Dates [formatter type]",
+ "Source File",
+ )
+ rows = []
+ sources = []
+ database_records = []
+ recorded_paths = []
+ for file_found in map(str, context.get_files_found()):
+ if os.path.basename(file_found).lower() != "shared.sqlite":
+ continue
+ database = open_sqlite_db_readonly(file_found)
+ if database is None:
+ continue
+ try:
+ records = _photos_records(database)
+ except Exception as exception: # pylint: disable=broad-exception-caught
+ logfunc(f"Photos: could not read '{file_found}': {exception}")
+ database.close()
+ continue
+ database.close()
+ sources.append(file_found)
+ source = context.get_relative_path(file_found)
+ database_records.append((records, source))
+ for record in records:
+ recorded_paths.append(_recorded_media_path(record[7], record[8]))
+
+ present_paths = _extract_referenced_media(context, recorded_paths)
+ for records, source in database_records:
+ for record in records:
+ recorded_path = _recorded_media_path(record[7], record[8])
+ media_reference = ""
+ if recorded_path in present_paths:
+ media_reference = check_in_media(
+ recorded_path, name=record[8] or ""
+ ) or ""
+ if media_reference:
+ original_status = "Present in acquisition; copied to report"
+ else:
+ original_status = (
+ "Present in acquisition; report preview unavailable"
+ )
+ else:
+ original_status = "Original file not present in acquisition"
+ rows.append((
+ _utc_from_filetime(record[0]),
+ _utc_from_filetime(record[1]),
+ _utc_from_filetime(record[2]),
+ _utc_from_filetime(record[3]),
+ _utc_from_filetime(record[4]),
+ _utc_from_filetime(record[5]),
+ media_reference,
+ original_status,
+ record[6],
+ record[7] or "",
+ record[8] or "",
+ record[9],
+ _yes_no(record[10]),
+ record[11] or "",
+ record[12] or "",
+ record[13] or "",
+ record[14] or "",
+ record[15] or "",
+ record[16] if record[16] is not None else "",
+ record[17] if record[17] is not None else "",
+ record[18] or "",
+ record[19] or "",
+ record[20] or "",
+ record[21] or "",
+ record[22] or "",
+ record[23] or "",
+ source,
+ ))
+ return data_headers, rows, "\n".join(sources)
+
+
+@artifact_processor
+def windowsPhotosFolders(context):
+ data_headers = (
+ ("Date Scanned (UTC)", "datetime"),
+ ("Date Modified (UTC)", "datetime"),
+ ("Date Created (UTC)", "datetime"),
+ "Folder ID",
+ "Parent Folder ID",
+ "Path",
+ "Library Folder",
+ "Scanned Media File Count",
+ "Provider Key",
+ "Folder Attributes",
+ "Source File",
+ )
+ rows = []
+ sources = []
+ for file_found in map(str, context.get_files_found()):
+ if os.path.basename(file_found).lower() != "shared.sqlite":
+ continue
+ database = open_sqlite_db_readonly(file_found)
+ if database is None:
+ continue
+ try:
+ records = database.execute(
+ """
+ SELECT DateScanned, DateModified, DateCreated, FolderId,
+ ParentFolderId, Path, IsLibraryFolder,
+ ScannedMediaFileCount, ProviderKey, FolderAttributes
+ FROM mediaFolder
+ ORDER BY DateScanned DESC, Path
+ """
+ ).fetchall()
+ except Exception as exception: # pylint: disable=broad-exception-caught
+ logfunc(f"Photos Folders: could not read '{file_found}': {exception}")
+ database.close()
+ continue
+ database.close()
+ sources.append(file_found)
+ source = context.get_relative_path(file_found)
+ for record in records:
+ rows.append((
+ _utc_from_unix_100ns(record[0]),
+ _utc_from_filetime(record[1]),
+ _utc_from_filetime(record[2]),
+ record[3],
+ record[4],
+ record[5] or "",
+ _yes_no(record[6]),
+ record[7],
+ record[8],
+ record[9],
+ source,
+ ))
+ return data_headers, rows, "\n".join(sources)
+
+
+def _scheduled_local(alarm):
+ fields = (
+ alarm.get("ScheduledYear"),
+ alarm.get("ScheduledMonth"),
+ alarm.get("ScheduledDay"),
+ alarm.get("ScheduledHour"),
+ alarm.get("ScheduledMinute"),
+ )
+ if any(value is None for value in fields):
+ return ""
+ try:
+ return datetime(*(int(value) for value in fields))
+ except (TypeError, ValueError):
+ return ""
+
+
+def _alarm_row(alarm, record_id, source_format, source):
+ days = alarm.get("DaysOfWeek")
+ recurring = alarm.get("IsRecurring")
+ if recurring is None and days is not None:
+ try:
+ recurring = int(days) != 0
+ except (TypeError, ValueError):
+ recurring = None
+ try:
+ alarm_time = (
+ f"{int(alarm.get('Hour')):02d}:{int(alarm.get('Minute')):02d}"
+ )
+ except (TypeError, ValueError):
+ alarm_time = ""
+ return (
+ _scheduled_local(alarm),
+ _utc_from_filetime(alarm.get("__Created")),
+ _utc_from_filetime(alarm.get("__Updated")),
+ _clean_text(alarm.get("Name")),
+ alarm_time,
+ _yes_no(alarm.get("IsEnabled")),
+ _yes_no(recurring),
+ days if days is not None else "",
+ alarm.get("SnoozeInterval", ""),
+ _clean_text(alarm.get("ChimeName")),
+ _clean_text(alarm.get("ChimePath")),
+ record_id,
+ source_format,
+ source,
+ )
+
+
+def _json_alarms(file_found):
+ with open(file_found, "r", encoding="utf-8-sig") as source:
+ document = json.load(source)
+ values = document.get("Alarms", []) if isinstance(document, dict) else []
+ return [
+ (value, str(index))
+ for index, value in enumerate(values)
+ if isinstance(value, dict)
+ ]
+
+
+def _registry_alarms(file_found):
+ if Registry is None:
+ raise RuntimeError(
+ "python-registry is required to read Windows Clock settings.dat"
+ )
+ hive = Registry.Registry(file_found)
+ key = hive.open(r"LocalState\Alarms")
+ return [
+ (value.value(), value.name())
+ for value in key.values()
+ if isinstance(value.value(), dict)
+ ]
+
+
+@artifact_processor
+def windowsAlarms(context):
+ data_headers = (
+ ("Next Scheduled Time (device local)", "datetime"),
+ ("Created Time (UTC)", "datetime"),
+ ("Updated Time (UTC)", "datetime"),
+ "Name",
+ "Alarm Time (device local)",
+ "Enabled",
+ "Recurring",
+ "Days of Week (bitmask)",
+ "Snooze Interval (minutes)",
+ "Chime Name / Resource",
+ "Chime Path",
+ "Record ID",
+ "Source Format",
+ "Source File",
+ )
+ rows = []
+ sources = []
+ for file_found in map(str, context.get_files_found()):
+ basename = os.path.basename(file_found).lower()
+ try:
+ if basename == "alarms.json":
+ alarms = _json_alarms(file_found)
+ source_format = "Alarms.json"
+ elif basename == "settings.dat":
+ alarms = _registry_alarms(file_found)
+ source_format = "Packaged-app settings hive"
+ else:
+ continue
+ except Exception as exception: # pylint: disable=broad-exception-caught
+ logfunc(f"Alarms: could not read '{file_found}': {exception}")
+ continue
+ sources.append(file_found)
+ source = context.get_relative_path(file_found)
+ for alarm, record_id in alarms:
+ rows.append(_alarm_row(alarm, record_id, source_format, source))
+ return data_headers, rows, "\n".join(sources)
diff --git a/scripts/pyinstaller/dleapp.spec b/scripts/pyinstaller/dleapp.spec
index e6adc3c..21b1c9a 100755
--- a/scripts/pyinstaller/dleapp.spec
+++ b/scripts/pyinstaller/dleapp.spec
@@ -20,6 +20,7 @@ a = Analysis(['..\\..\\dleapp.py'],
'openpyxl',
'pillow_heif',
'pypdf',
+ 'Registry',
'requests',
'simplekml',
'xlrd',
diff --git a/scripts/pyinstaller/dleappGUI.spec b/scripts/pyinstaller/dleappGUI.spec
index d5cd64d..04a714b 100755
--- a/scripts/pyinstaller/dleappGUI.spec
+++ b/scripts/pyinstaller/dleappGUI.spec
@@ -20,6 +20,7 @@ a = Analysis(['..\\..\\dleappGUI.py'],
'openpyxl',
'pillow_heif',
'pypdf',
+ 'Registry',
'requests',
'simplekml',
'xlrd',
@@ -49,4 +50,4 @@ exe = EXE(pyz,
disable_windowed_traceback=False,
upx_exclude=[],
version='dleappGUI-file_version_info.txt',
- runtime_tmpdir=None )
\ No newline at end of file
+ runtime_tmpdir=None )
diff --git a/scripts/pyinstaller/dleappGUI_macOS.spec b/scripts/pyinstaller/dleappGUI_macOS.spec
index 8da53e7..ae29b4e 100755
--- a/scripts/pyinstaller/dleappGUI_macOS.spec
+++ b/scripts/pyinstaller/dleappGUI_macOS.spec
@@ -19,6 +19,7 @@ a = Analysis(
'openpyxl',
'pillow_heif',
'pypdf',
+ 'Registry',
'requests',
'xlrd',
],
diff --git a/scripts/pyinstaller/dleapp_macOS.spec b/scripts/pyinstaller/dleapp_macOS.spec
index bf8cc37..ca06c4e 100644
--- a/scripts/pyinstaller/dleapp_macOS.spec
+++ b/scripts/pyinstaller/dleapp_macOS.spec
@@ -19,6 +19,7 @@ a = Analysis(
'openpyxl',
'pillow_heif',
'pypdf',
+ 'Registry',
'requests',
'xlrd',
],
diff --git a/windows-apps.dlprofile b/windows-apps.dlprofile
new file mode 100644
index 0000000..b19371a
--- /dev/null
+++ b/windows-apps.dlprofile
@@ -0,0 +1 @@
+{"leapp":"dleapp","format_version":1,"plugins":["windowsPhotos","windowsPhotosFolders","windowsAlarms"]}