From bb88c89e6f0009bd64c62b8b6a71fefc4a1244b5 Mon Sep 17 00:00:00 2001 From: Brigs Date: Wed, 29 Jul 2026 13:56:46 -0500 Subject: [PATCH] Add modern Windows Photos and Clock artifacts Modernize the WLEAPP Photos and Alarms coverage for current Windows app storage, add evidence-backed media previews, focused corpus validation tooling, tests, packaging support, and documentation. Co-authored-by: Codex --- admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md | 14 +- admin/docs/generated/module_info.md | 27 +- admin/scripts/module_info.py | 24 +- admin/test/scripts/test_windows_apps.py | 235 ++++++++ .../Get-DLEAPPAppStorageInventory.ps1 | 132 +++++ .../Invoke-DLEAPPClockValidation.ps1 | 33 ++ .../Invoke-DLEAPPPhotosValidation.ps1 | 48 ++ admin/windows_lab/Invoke-DLEAPPWaveOne.ps1 | 7 +- admin/windows_lab/README.md | 25 + .../windows_lab/Snapshot-DLEAPPLiveSqlite.py | 8 + requirements.txt | 3 +- scripts/artifacts/windowsApps.py | 502 ++++++++++++++++++ scripts/pyinstaller/dleapp.spec | 1 + scripts/pyinstaller/dleappGUI.spec | 3 +- scripts/pyinstaller/dleappGUI_macOS.spec | 1 + scripts/pyinstaller/dleapp_macOS.spec | 1 + windows-apps.dlprofile | 1 + 17 files changed, 1033 insertions(+), 32 deletions(-) create mode 100644 admin/test/scripts/test_windows_apps.py create mode 100644 admin/windows_lab/Get-DLEAPPAppStorageInventory.ps1 create mode 100644 admin/windows_lab/Invoke-DLEAPPClockValidation.ps1 create mode 100644 admin/windows_lab/Invoke-DLEAPPPhotosValidation.ps1 create mode 100644 scripts/artifacts/windowsApps.py create mode 100644 windows-apps.dlprofile 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"]}