diff --git a/.gitignore b/.gitignore
index abbf8d6..339a291 100755
--- a/.gitignore
+++ b/.gitignore
@@ -68,6 +68,7 @@ $RECYCLE.BIN/
?LEAPP_Reports_*
path_list.txt
coordinates.db
+.lab-output/
# Documentation
docs/_build/
diff --git a/admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md b/admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md
new file mode 100644
index 0000000..7588131
--- /dev/null
+++ b/admin/docs/WLEAPP_TO_DLEAPP_AUDIT.md
@@ -0,0 +1,55 @@
+# WLEAPP to DLEAPP modernization audit
+
+Authors: `@AlexisBrignoni, Codex`
+
+This audit compares the 15 artifact modules in the local WLEAPP repository at
+commit `7690aa4` with observations from the controlled Windows VM corpus. It is
+an implementation and test-status record, not a claim that an artifact is
+absent from Windows generally.
+
+The lab observation applies to a Parallels ARM virtual machine running Windows
+build `26200.8457`, display version `25H2`. The Windows registry value collected
+as `ProductName` says `Windows 10 Pro`; the build, display version, architecture,
+and captured UI are therefore reported separately rather than treating that
+single registry label as the operating-system name.
+
+## Migrated and corpus-validated
+
+| WLEAPP module | DLEAPP artifact | Corpus result | Modernization value |
+|---|---|---:|---|
+| `activitiesCache.py` | ActivitiesCache | 5 rows | Preserves non-JSON payloads that the predecessor discarded, exposes application identifiers, and reports Unix timestamps in UTC with the primary start time first. |
+| `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. |
+
+The focused profile is `windows-system.dlprofile`.
+
+## Retest when a representative artifact is available
+
+| WLEAPP module | Lab observation | Required next evidence |
+|---|---|---|
+| `betterDiscord.py` | BetterDiscord MessageLoggerV2 data was not present. | A consented test profile with that specific third-party plugin and known messages. |
+| `box.py` | Box databases were not present. | Current Box Drive installation, app version, and controlled local/cloud file actions. |
+| `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
+
+| WLEAPP module | Reason to avoid a blind port |
+|---|---|
+| `facebookMessenger.py` | The parser targets the retired Facebook Messenger UWP storage layout, and no matching package or database was present. Preserve it only with a representative legacy corpus. |
+| `windowsCortana.py` | The parser targets legacy Cortana `DeviceSearchCache` text files, and no matching package or files were present. Preserve it only with a representative legacy corpus. |
+
+## Acquisition note
+
+ActivitiesCache and Notifications could not be copied byte-for-byte while the
+Windows user session was active. Their raw-copy failures and error messages are
+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.
diff --git a/admin/test/scripts/test_windows_system.py b/admin/test/scripts/test_windows_system.py
new file mode 100644
index 0000000..1ce2564
--- /dev/null
+++ b/admin/test/scripts/test_windows_system.py
@@ -0,0 +1,161 @@
+"""Tests for the Windows system artifacts migrated from WLEAPP."""
+
+# pylint: disable=protected-access
+
+import sqlite3
+from datetime import datetime, timezone
+
+from scripts.artifacts import windowsSystem
+
+
+class _Context:
+ def __init__(self, files):
+ self._files = files
+
+ def get_files_found(self):
+ return self._files
+
+ @staticmethod
+ def get_relative_path(path):
+ return str(path)
+
+
+def test_windows_timestamp_epochs():
+ expected = datetime(1970, 1, 1, tzinfo=timezone.utc)
+ assert windowsSystem._utc_from_unix_seconds(1) == expected.replace(second=1)
+ assert windowsSystem._utc_from_filetime(116444736010000000) == expected.replace(
+ second=1
+ )
+ assert windowsSystem._utc_from_dotnet_ticks(621355968010000000) == (
+ expected.replace(second=1)
+ )
+ assert windowsSystem._utc_from_filetime(0) == ""
+
+
+def test_activities_cache_retains_non_json_payload(tmp_path):
+ database_path = tmp_path / "ActivitiesCache.db"
+ with sqlite3.connect(database_path) as database:
+ database.execute(
+ """
+ CREATE TABLE Activity (
+ StartTime, EndTime, LastModifiedTime, ExpirationTime,
+ LastModifiedOnClient, AppActivityId, AppId, Payload,
+ ActivityType, ActivityStatus, Tag, "Group", IsLocalOnly, IsRead
+ )
+ """
+ )
+ database.execute(
+ """
+ INSERT INTO Activity VALUES (
+ 1, 2, 3, 4, 5, 'activity-id',
+ '[{"application":"test.application"}]', 'Tk9OLUpTT04=',
+ 11, 1, 'tag', 'group', 1, 0
+ )
+ """
+ )
+
+ _, rows, _ = windowsSystem.activitiesCache.__wrapped__(
+ _Context([database_path])
+ )
+ assert len(rows) == 1
+ assert rows[0][5] == "activity-id"
+ assert rows[0][6] == "test.application"
+ assert rows[0][16] == "Tk9OLUpTT04="
+
+
+def test_notifications_include_handler_and_payload_hash(tmp_path):
+ database_path = tmp_path / "wpndatabase.db"
+ with sqlite3.connect(database_path) as database:
+ database.execute(
+ """
+ CREATE TABLE NotificationHandler (
+ RecordId INTEGER, PrimaryId TEXT, HandlerType TEXT,
+ CreatedTime TEXT, ModifiedTime TEXT
+ )
+ """
+ )
+ database.execute(
+ """
+ CREATE TABLE Notification (
+ ArrivalTime, ExpiryTime, BootId, Id, HandlerId, Type,
+ PayloadType, Payload, Tag, "Group", ExpiresOnReboot
+ )
+ """
+ )
+ database.execute(
+ "INSERT INTO NotificationHandler VALUES "
+ "(7, 'test.handler', 'app:test', 'created', 'modified')"
+ )
+ database.execute(
+ """
+ INSERT INTO Notification VALUES (
+ 116444736010000000, 116444736020000000,
+ 116444736000000000, 9, 7, 'toast', 'Xml',
+ 'DLEAPP-NOTIFICATION-TEST-001',
+ 'tag', 'group', 1
+ )
+ """
+ )
+
+ _, rows, _ = windowsSystem.windowsNotifications.__wrapped__(
+ _Context([database_path])
+ )
+ assert len(rows) == 1
+ assert rows[0][7] == "test.handler"
+ assert rows[0][11] == "DLEAPP-NOTIFICATION-TEST-001"
+ assert len(rows[0][16]) == 64
+
+
+def test_sticky_notes_timestamp_first_and_markup_removed(tmp_path):
+ database_path = tmp_path / "plum.sqlite"
+ with sqlite3.connect(database_path) as database:
+ database.execute(
+ """
+ CREATE TABLE Note (
+ UpdatedAt, CreatedAt, DeletedAt, Id, ParentId, Text,
+ IsOpen, IsAlwaysOnTop, Theme, WindowPosition
+ )
+ """
+ )
+ database.execute(
+ """
+ INSERT INTO Note VALUES (
+ 621355968020000000, 621355968010000000, NULL,
+ 'note-id', 'parent-id',
+ '\\id=01234567-89ab-cdef-0123-456789abcdef known text',
+ 1, 0, 'Yellow', 'ManagedPosition='
+ )
+ """
+ )
+
+ headers, rows, _ = windowsSystem.windowsStickyNotes.__wrapped__(
+ _Context([database_path])
+ )
+ assert headers[:3] == (
+ ("Updated Time (UTC)", "datetime"),
+ ("Created Time (UTC)", "datetime"),
+ ("Deleted Time (UTC)", "datetime"),
+ )
+ assert rows[0][5] == "known text"
+
+
+def test_setupapi_sections_do_not_claim_first_connection(tmp_path):
+ log_path = tmp_path / "setupapi.dev.log"
+ log_path.write_text(
+ """
+>>> [Device Install (Hardware initiated) - USB\\VID_1234&PID_5678\\ABC]
+>>> Section start 2026/07/29 10:00:00.100
+ dvi: test
+<<< Section end 2026/07/29 10:00:01.600
+<<< [Exit status: SUCCESS]
+""".lstrip(),
+ encoding="utf-8",
+ )
+ headers, rows, _ = windowsSystem.setupapiSections.__wrapped__(
+ _Context([log_path])
+ )
+ assert headers[0] == "Start Time (device local)"
+ assert len(rows) == 1
+ assert rows[0][3] == r"USB\VID_1234&PID_5678\ABC"
+ assert rows[0][4] == "SUCCESS"
+ assert rows[0][5] == 1.5
diff --git a/admin/windows_lab/DLEAPPLab.Common.ps1 b/admin/windows_lab/DLEAPPLab.Common.ps1
new file mode 100644
index 0000000..8a89ca7
--- /dev/null
+++ b/admin/windows_lab/DLEAPPLab.Common.ps1
@@ -0,0 +1,139 @@
+# DLEAPP Windows corpus laboratory helpers.
+# Authors: @AlexisBrignoni, Codex
+
+Set-StrictMode -Version 2.0
+
+function Get-DLEAPPArtifactDefinitions {
+ $local = $env:LOCALAPPDATA
+ $roaming = $env:APPDATA
+ $windows = $env:SystemRoot
+
+ @(
+ [pscustomobject]@{
+ Artifact = "ActivitiesCache"
+ Patterns = @("$local\ConnectedDevicesPlatform\*\ActivitiesCache.db*")
+ }
+ [pscustomobject]@{
+ Artifact = "BetterDiscord Message Logger"
+ Patterns = @(
+ "$roaming\BetterDiscord\plugins\MessageLoggerV2Data.config.json"
+ )
+ }
+ [pscustomobject]@{
+ Artifact = "Box Drive"
+ Patterns = @("$local\Box\Box\Data\*.db*")
+ }
+ [pscustomobject]@{
+ Artifact = "Dropbox"
+ Patterns = @(
+ "$local\Packages\*DROPBOX*\LocalState\users\*\*.sqlite*",
+ "$local\Dropbox\instance*\sync_history.db*"
+ )
+ }
+ [pscustomobject]@{
+ Artifact = "Facebook Messenger (Legacy)"
+ Patterns = @("$local\Packages\FACEBOOK.*\AC\Messenger\msys_*.db*")
+ }
+ [pscustomobject]@{
+ Artifact = "Google Drive"
+ Patterns = @("$local\Google\DriveFS\*\metadata_sqlite_db*")
+ }
+ [pscustomobject]@{
+ Artifact = "Windows Firewall"
+ Patterns = @("$windows\System32\LogFiles\Firewall\pfirewall.log")
+ }
+ [pscustomobject]@{
+ Artifact = "SetupAPI Device Installation"
+ Patterns = @("$windows\INF\setupapi.dev.log")
+ }
+ [pscustomobject]@{
+ Artifact = "Windows Clock and Alarms"
+ Patterns = @(
+ "$local\Packages\Microsoft.WindowsAlarms_*\LocalState\Alarms\Alarms.json",
+ "$local\Packages\Microsoft.WindowsAlarms_*\Settings\settings.dat"
+ )
+ }
+ [pscustomobject]@{
+ Artifact = "Cortana DeviceSearchCache (Legacy)"
+ Patterns = @(
+ "$local\Packages\Microsoft.Windows.Cortana_*\LocalState\DeviceSearchCache\AppCache*.txt"
+ )
+ }
+ [pscustomobject]@{
+ Artifact = "Microsoft Edge Legacy"
+ Patterns = @("$local\Microsoft\Windows\WebCache\WebCacheV01.dat*")
+ }
+ [pscustomobject]@{
+ Artifact = "Windows Notifications"
+ Patterns = @(
+ "$local\Microsoft\Windows\Notifications\wpndatabase.db*"
+ )
+ }
+ [pscustomobject]@{
+ Artifact = "Windows Photos"
+ Patterns = @(
+ "$local\Packages\Microsoft.Windows.Photos_*\LocalState\MediaDb*.sqlite*"
+ )
+ }
+ [pscustomobject]@{
+ Artifact = "Windows Sticky Notes"
+ Patterns = @(
+ "$local\Packages\Microsoft.MicrosoftStickyNotes_*\LocalState\plum.sqlite*"
+ )
+ }
+ [pscustomobject]@{
+ Artifact = "Phone Link"
+ Patterns = @(
+ "$local\Packages\Microsoft.YourPhone_*\LocalCache\Indexed\*\System\Database\*",
+ "$local\Packages\MicrosoftWindows.CrossDevice_*\LocalState\*"
+ )
+ }
+ )
+}
+
+function Get-DLEAPPTargetFiles {
+ $seen = @{}
+ foreach ($definition in Get-DLEAPPArtifactDefinitions) {
+ foreach ($pattern in $definition.Patterns) {
+ $matches = @(Get-ChildItem -Path $pattern -Force -File -ErrorAction SilentlyContinue)
+ foreach ($file in $matches) {
+ $key = $file.FullName.ToLowerInvariant()
+ if ($seen.ContainsKey($key)) {
+ continue
+ }
+ $seen[$key] = $true
+ [pscustomobject]@{
+ Artifact = $definition.Artifact
+ Pattern = $pattern
+ File = $file
+ }
+ }
+ }
+ }
+}
+
+function New-DLEAPPDirectory {
+ param([Parameter(Mandatory = $true)][string]$Path)
+
+ if (-not (Test-Path -LiteralPath $Path)) {
+ New-Item -ItemType Directory -Path $Path -Force | Out-Null
+ }
+}
+
+function ConvertTo-DLEAPPTsvValue {
+ param([AllowNull()][object]$Value)
+
+ if ($null -eq $Value) {
+ return ""
+ }
+ ([string]$Value).Replace("`t", " ").Replace("`r", " ").Replace("`n", " ")
+}
+
+function Get-DLEAPPRelativeCollectionPath {
+ param([Parameter(Mandatory = $true)][System.IO.FileInfo]$File)
+
+ $driveName = $File.PSDrive.Name
+ $driveRoot = $File.PSDrive.Root
+ $relative = $File.FullName.Substring($driveRoot.Length).TrimStart("\")
+ Join-Path $driveName $relative
+}
diff --git a/admin/windows_lab/Export-DLEAPPLabCorpus.ps1 b/admin/windows_lab/Export-DLEAPPLabCorpus.ps1
new file mode 100644
index 0000000..aab0c5e
--- /dev/null
+++ b/admin/windows_lab/Export-DLEAPPLabCorpus.ps1
@@ -0,0 +1,92 @@
+# Copy discovered artifacts into a path-preserving, hashed corpus stage.
+# Authors: @AlexisBrignoni, Codex
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)][string]$DestinationRoot,
+ [ValidatePattern("^[A-Za-z0-9._-]+$")][string]$Stage = "baseline",
+ [string]$LabRoot = "C:\DLEAPP_Lab"
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version 2.0
+. "$PSScriptRoot\DLEAPPLab.Common.ps1"
+
+$stageRoot = Join-Path $DestinationRoot $Stage
+$fileRoot = Join-Path $stageRoot "files"
+New-DLEAPPDirectory -Path $fileRoot
+
+$manifestRows = @()
+foreach ($match in Get-DLEAPPTargetFiles) {
+ $file = $match.File
+ $relativePath = Get-DLEAPPRelativeCollectionPath -File $file
+ $destination = Join-Path $fileRoot $relativePath
+ New-DLEAPPDirectory -Path (Split-Path -Parent $destination)
+
+ $copied = $false
+ $errorText = ""
+ $sourceHash = ""
+ $destinationHash = ""
+ try {
+ $sourceHash = (
+ Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256 `
+ -ErrorAction Stop 2>$null
+ ).Hash
+ Copy-Item -LiteralPath $file.FullName -Destination $destination -Force
+ $destinationHash = (
+ Get-FileHash -LiteralPath $destination -Algorithm SHA256 `
+ -ErrorAction Stop 2>$null
+ ).Hash
+ if ($sourceHash -ne $destinationHash) {
+ throw "Source and destination hashes differ."
+ }
+ $copied = $true
+ }
+ catch {
+ $errorText = $_.Exception.Message
+ }
+
+ $manifestRows += [pscustomobject]@{
+ Artifact = $match.Artifact
+ SourcePath = $file.FullName
+ CollectedPath = $destination
+ Length = $file.Length
+ CreatedUtc = $file.CreationTimeUtc.ToString("o")
+ ModifiedUtc = $file.LastWriteTimeUtc.ToString("o")
+ SHA256 = $sourceHash
+ Copied = $copied
+ CollectionError = $errorText
+ }
+}
+
+$manifestRows |
+ Sort-Object Artifact, SourcePath |
+ Export-Csv -LiteralPath (Join-Path $stageRoot "collection-manifest.tsv") `
+ -Delimiter "`t" -NoTypeInformation -Encoding UTF8
+
+$metadata = [ordered]@{
+ CollectedUtc = [DateTime]::UtcNow.ToString("o")
+ Stage = $Stage
+ HostLabel = "windows11_arm_parallels"
+ FileCount = @($manifestRows | Where-Object Copied).Count
+ FailedCount = @($manifestRows | Where-Object { -not $_.Copied }).Count
+ SourceCollection = "Logical copy from controlled Windows VM"
+}
+$metadata |
+ ConvertTo-Json -Depth 5 |
+ Set-Content -LiteralPath (Join-Path $stageRoot "collection-metadata.json") `
+ -Encoding UTF8
+
+foreach ($supportName in @("Inventory", "action-journal.tsv")) {
+ $supportPath = Join-Path $LabRoot $supportName
+ if (Test-Path -LiteralPath $supportPath) {
+ Copy-Item -LiteralPath $supportPath -Destination $stageRoot `
+ -Recurse -Force
+ }
+}
+
+Write-Output ("Corpus stage written to {0}" -f $stageRoot)
+Write-Output (
+ "Copied: {0}; failed: {1}" -f
+ $metadata.FileCount, $metadata.FailedCount
+)
diff --git a/admin/windows_lab/Get-DLEAPPLabInventory.ps1 b/admin/windows_lab/Get-DLEAPPLabInventory.ps1
new file mode 100644
index 0000000..1976c98
--- /dev/null
+++ b/admin/windows_lab/Get-DLEAPPLabInventory.ps1
@@ -0,0 +1,134 @@
+# Record a passive Windows/app/artifact inventory for the DLEAPP corpus lab.
+# Authors: @AlexisBrignoni, Codex
+
+[CmdletBinding()]
+param(
+ [string]$OutputRoot = "C:\DLEAPP_Lab\Inventory"
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version 2.0
+. "$PSScriptRoot\DLEAPPLab.Common.ps1"
+
+New-DLEAPPDirectory -Path $OutputRoot
+
+$currentVersion = Get-ItemProperty `
+ -LiteralPath "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
+$operatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem
+$computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem
+$timeZone = Get-TimeZone
+$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
+
+$systemInventory = [ordered]@{
+ CollectedUtc = [DateTime]::UtcNow.ToString("o")
+ ProductName = $currentVersion.ProductName
+ DisplayVersion = $currentVersion.DisplayVersion
+ CurrentBuild = $currentVersion.CurrentBuild
+ UBR = $currentVersion.UBR
+ BuildLabEx = $currentVersion.BuildLabEx
+ OSArchitecture = $operatingSystem.OSArchitecture
+ PowerShellVersion = $PSVersionTable.PSVersion.ToString()
+ ProcessorArchitecture = $env:PROCESSOR_ARCHITECTURE
+ ComputerModel = $computerSystem.Model
+ Manufacturer = $computerSystem.Manufacturer
+ TimeZoneId = $timeZone.Id
+ UtcOffset = $timeZone.GetUtcOffset([DateTime]::Now).ToString()
+ UserSid = $identity.User.Value
+ UserProfileName = Split-Path -Leaf $env:USERPROFILE
+}
+$systemInventory |
+ ConvertTo-Json -Depth 6 |
+ Set-Content -LiteralPath (Join-Path $OutputRoot "system-inventory.json") `
+ -Encoding UTF8
+
+$targetPackageExpression = (
+ "StickyNotes|WindowsAlarms|Windows\.Photos|YourPhone|CrossDevice|" +
+ "Cortana|Dropbox|Box|GoogleDrive|Messenger"
+)
+$packages = @(
+ Get-AppxPackage |
+ Where-Object {
+ $_.Name -match $targetPackageExpression -or
+ $_.PackageFamilyName -match $targetPackageExpression
+ } |
+ Sort-Object Name |
+ Select-Object Name, PackageFullName, PackageFamilyName, Version,
+ Architecture, InstallLocation, Status, PublisherId
+)
+$packages |
+ ConvertTo-Json -Depth 6 |
+ Set-Content -LiteralPath (Join-Path $OutputRoot "target-appx-packages.json") `
+ -Encoding UTF8
+
+$startApplications = @(
+ Get-StartApps |
+ Where-Object {
+ $_.Name -match (
+ "Sticky|Clock|Alarm|Photos|Phone Link|Your Phone|" +
+ "Dropbox|Box|Google Drive|Messenger|PowerShell|Terminal"
+ )
+ } |
+ Sort-Object Name
+)
+$startApplications |
+ ConvertTo-Json -Depth 4 |
+ Set-Content -LiteralPath (Join-Path $OutputRoot "target-start-apps.json") `
+ -Encoding UTF8
+
+$artifactRows = @()
+foreach ($match in Get-DLEAPPTargetFiles) {
+ $file = $match.File
+ $hash = ""
+ $hashError = ""
+ try {
+ $hash = (
+ Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256 `
+ -ErrorAction Stop 2>$null
+ ).Hash
+ }
+ catch {
+ $hashError = $_.Exception.Message
+ }
+ $artifactRows += [pscustomobject]@{
+ Artifact = $match.Artifact
+ Path = $file.FullName
+ Length = $file.Length
+ CreatedUtc = $file.CreationTimeUtc.ToString("o")
+ ModifiedUtc = $file.LastWriteTimeUtc.ToString("o")
+ SHA256 = $hash
+ HashError = $hashError
+ MatchedPattern = $match.Pattern
+ }
+}
+$artifactRows |
+ Sort-Object Artifact, Path |
+ Export-Csv -LiteralPath (Join-Path $OutputRoot "artifact-paths.tsv") `
+ -Delimiter "`t" -NoTypeInformation -Encoding UTF8
+
+$definitions = @(
+ foreach ($definition in Get-DLEAPPArtifactDefinitions) {
+ [pscustomobject]@{
+ Artifact = $definition.Artifact
+ Patterns = $definition.Patterns
+ Matches = @(
+ $artifactRows |
+ Where-Object Artifact -eq $definition.Artifact |
+ Select-Object -ExpandProperty Path
+ )
+ }
+ }
+)
+$definitions |
+ ConvertTo-Json -Depth 8 |
+ Set-Content -LiteralPath (Join-Path $OutputRoot "artifact-coverage.json") `
+ -Encoding UTF8
+
+$journalPath = "C:\DLEAPP_Lab\action-journal.tsv"
+if (-not (Test-Path -LiteralPath $journalPath)) {
+ New-DLEAPPDirectory -Path (Split-Path -Parent $journalPath)
+ "TimestampUtc`tTimestampLocal`tUtcOffset`tArtifact`tAction`tToken`tDetails" |
+ Set-Content -LiteralPath $journalPath -Encoding UTF8
+}
+
+Write-Output ("Inventory written to {0}" -f $OutputRoot)
+Write-Output ("Target files found: {0}" -f $artifactRows.Count)
diff --git a/admin/windows_lab/Invoke-DLEAPPWaveOne.ps1 b/admin/windows_lab/Invoke-DLEAPPWaveOne.ps1
new file mode 100644
index 0000000..0619baa
--- /dev/null
+++ b/admin/windows_lab/Invoke-DLEAPPWaveOne.ps1
@@ -0,0 +1,139 @@
+# Prepare controlled, non-account Windows corpus inputs and a native toast.
+# This script does not alter firewall/security settings or app databases.
+# Authors: @AlexisBrignoni, Codex
+
+[CmdletBinding()]
+param(
+ [string]$LabRoot = "C:\DLEAPP_Lab",
+ [string]$KnownImagePath = (
+ Join-Path $PSScriptRoot "..\..\assets\DLEAPP_logo.png"
+ ),
+ [switch]$LaunchApplications
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version 2.0
+. "$PSScriptRoot\DLEAPPLab.Common.ps1"
+
+$journalScript = Join-Path $PSScriptRoot "Write-DLEAPPAction.ps1"
+$knownInputRoot = Join-Path $LabRoot "KnownInputs"
+New-DLEAPPDirectory -Path $knownInputRoot
+
+& $journalScript `
+ -Artifact "Corpus Lab" `
+ -Action "Wave initialized" `
+ -Token "DLEAPP-WAVE1-START-001" `
+ -Details "Non-account Windows artifact wave"
+
+$knownText = Join-Path $knownInputRoot "DLEAPP-KNOWN-FILE-001.txt"
+@(
+ "DLEAPP controlled Windows corpus input"
+ "Token: DLEAPP-KNOWN-FILE-001"
+ ("Created UTC: {0}" -f [DateTime]::UtcNow.ToString("o"))
+) | Set-Content -LiteralPath $knownText -Encoding UTF8
+& $journalScript `
+ -Artifact "Known Input" `
+ -Action "Created text file" `
+ -Token "DLEAPP-KNOWN-FILE-001" `
+ -Details $knownText
+
+if (Test-Path -LiteralPath $KnownImagePath) {
+ $knownImage = Join-Path $env:USERPROFILE `
+ "Pictures\DLEAPP-CORPUS-PHOTO-001.png"
+ Copy-Item -LiteralPath $KnownImagePath -Destination $knownImage -Force
+ & $journalScript `
+ -Artifact "Windows Photos" `
+ -Action "Copied known image into Pictures" `
+ -Token "DLEAPP-PHOTOS-COPY-001" `
+ -Details $knownImage
+}
+else {
+ & $journalScript `
+ -Artifact "Windows Photos" `
+ -Action "Known image unavailable" `
+ -Token "DLEAPP-PHOTOS-COPY-FAIL-001" `
+ -Details $KnownImagePath
+}
+
+$toastToken = "DLEAPP-NOTIFICATION-TOAST-001"
+$toastResult = "Not attempted"
+try {
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
+ [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
+
+ $startApp = Get-StartApps |
+ Where-Object { $_.Name -match "PowerShell|Terminal" } |
+ Select-Object -First 1
+ if ($null -eq $startApp) {
+ throw "No registered PowerShell or Terminal AppUserModelID was found."
+ }
+
+ $toastXml = New-Object Windows.Data.Xml.Dom.XmlDocument
+ $toastXml.LoadXml(
+ "" +
+ "DLEAPP Corpus Lab$toastToken" +
+ ""
+ )
+ $toast = New-Object Windows.UI.Notifications.ToastNotification $toastXml
+ $notifier = [Windows.UI.Notifications.ToastNotificationManager]::
+ CreateToastNotifier($startApp.AppID)
+ $notifier.Show($toast)
+ $toastResult = "Shown through $($startApp.AppID)"
+ & $journalScript `
+ -Artifact "Windows Notifications" `
+ -Action "Displayed controlled toast" `
+ -Token $toastToken `
+ -Details $toastResult
+}
+catch {
+ $toastResult = $_.Exception.Message
+ & $journalScript `
+ -Artifact "Windows Notifications" `
+ -Action "Controlled toast failed" `
+ -Token "DLEAPP-NOTIFICATION-TOAST-FAIL-001" `
+ -Details $toastResult
+}
+
+if ($LaunchApplications) {
+ $launchTargets = @(
+ [pscustomobject]@{
+ Artifact = "Windows Photos"
+ Expression = "Photos"
+ Token = "DLEAPP-PHOTOS-LAUNCH-001"
+ }
+ [pscustomobject]@{
+ Artifact = "Windows Sticky Notes"
+ Expression = "Sticky"
+ Token = "DLEAPP-STICKY-LAUNCH-001"
+ }
+ [pscustomobject]@{
+ Artifact = "Windows Clock and Alarms"
+ Expression = "Clock|Alarm"
+ Token = "DLEAPP-CLOCK-LAUNCH-001"
+ }
+ )
+ foreach ($target in $launchTargets) {
+ $application = Get-StartApps |
+ Where-Object Name -match $target.Expression |
+ Select-Object -First 1
+ if ($null -eq $application) {
+ & $journalScript `
+ -Artifact $target.Artifact `
+ -Action "Application not registered" `
+ -Token ($target.Token -replace "-001$", "-FAIL-001") `
+ -Details $target.Expression
+ continue
+ }
+ Start-Process "explorer.exe" -ArgumentList (
+ "shell:AppsFolder\{0}" -f $application.AppID
+ )
+ & $journalScript `
+ -Artifact $target.Artifact `
+ -Action "Application launched" `
+ -Token $target.Token `
+ -Details $application.AppID
+ }
+}
+
+Write-Output ("Wave one prepared. Toast result: {0}" -f $toastResult)
diff --git a/admin/windows_lab/README.md b/admin/windows_lab/README.md
new file mode 100644
index 0000000..1294006
--- /dev/null
+++ b/admin/windows_lab/README.md
@@ -0,0 +1,98 @@
+# DLEAPP Windows corpus laboratory
+
+These scripts create a documented logical corpus from a controlled Windows VM.
+They do not modify application databases directly. Normal application actions
+must create the evidence being studied.
+
+Authors: `@AlexisBrignoni, Codex`
+
+## Forensic constraints
+
+- Use only the laboratory VM clone. Keep the source VM powered off.
+- Record the Windows build, architecture, time zone, app version, and every
+ action that is intended to produce evidence.
+- Use synthetic tokens beginning with `DLEAPP-` rather than personal data.
+- Close the relevant application before collection where practical.
+- Collect SQLite databases with `-wal` and `-shm` sidecars.
+- Treat a missing row as an observation, not proof that an action did not occur.
+- Findings from the initial VM apply to Windows 11 ARM under Parallels until
+ validated on other Windows architectures and builds.
+
+## Passive inventory
+
+Run from Windows PowerShell:
+
+```powershell
+powershell.exe -ExecutionPolicy Bypass -File `
+ "\\Mac\Home\Documents\GitHub\DLEAPP\admin\windows_lab\Get-DLEAPPLabInventory.ps1"
+```
+
+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.
+
+## Action journal
+
+Record an action immediately before or after performing it:
+
+```powershell
+& "\\Mac\Home\Documents\GitHub\DLEAPP\admin\windows_lab\Write-DLEAPPAction.ps1" `
+ -Artifact "Windows Sticky Notes" `
+ -Action "Created note" `
+ -Token "DLEAPP-STICKY-CREATE-001" `
+ -Details "Synthetic note created through the normal UI"
+```
+
+The first column in `C:\DLEAPP_Lab\action-journal.tsv` is the UTC timestamp.
+The local timestamp and recorded UTC offset follow it.
+
+## First non-account wave
+
+```powershell
+powershell.exe -ExecutionPolicy Bypass -File `
+ "\\Mac\Home\Documents\GitHub\DLEAPP\admin\windows_lab\Invoke-DLEAPPWaveOne.ps1" `
+ -LaunchApplications
+```
+
+This creates a known text file, copies the DLEAPP logo into the Windows Pictures
+folder, attempts a native controlled toast, and optionally launches Photos,
+Sticky Notes, and Clock. It does not create notes or alarms by writing their
+databases; those actions must be completed through the applications.
+
+The script intentionally does not enable firewall logging. That is a
+security-sensitive system setting and should be handled as a separately
+documented test.
+
+## Logical collection
+
+Choose a destination visible to Windows, such as a Parallels shared directory:
+
+```powershell
+powershell.exe -ExecutionPolicy Bypass -File `
+ "\\Mac\Home\Documents\GitHub\DLEAPP\admin\windows_lab\Export-DLEAPPLabCorpus.ps1" `
+ -DestinationRoot "\\Mac\Home\Documents\GitHub\DLEAPP\.lab-output" `
+ -Stage "baseline"
+```
+
+The collector preserves Windows paths beneath `files\C`, includes discovered
+SQLite sidecars, hashes source and destination copies, and writes a manifest
+with any collection errors. Corpus output is excluded from Git.
+
+## Live SQLite snapshot for parser testing
+
+Some Windows databases remain open while the user is signed in. A failed raw
+copy is valuable acquisition information and must remain in the collection
+manifest. For a separate parser-testing input, Python's SQLite backup API can
+create a transactionally consistent database containing committed WAL data:
+
+```powershell
+py "\\Mac\Home\Documents\GitHub\DLEAPP\admin\windows_lab\Snapshot-DLEAPPLiveSqlite.py" `
+ --destination-root "\\Mac\Home\Documents\GitHub\DLEAPP\.lab-output" `
+ --stage "live-sqlite-snapshots"
+```
+
+The snapshot manifest places its creation timestamp first and records source
+file timestamps, sizes, snapshot SHA-256, SQLite quick-check result, page count,
+user version, and the acquisition method. These outputs are examiner-derived
+logical snapshots. They are useful for reproducible parser tests, but they are
+not substitutes for byte-for-byte acquired source files and sidecars.
diff --git a/admin/windows_lab/Snapshot-DLEAPPLiveSqlite.py b/admin/windows_lab/Snapshot-DLEAPPLiveSqlite.py
new file mode 100644
index 0000000..7739999
--- /dev/null
+++ b/admin/windows_lab/Snapshot-DLEAPPLiveSqlite.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+"""Create examiner-derived SQLite snapshots from a running Windows lab VM.
+
+The SQLite backup API includes committed WAL content in a consistent database
+copy. The result is suitable for parser testing, but it is not a byte-for-byte
+copy of the acquired source and must remain labeled as derived evidence.
+
+Authors: @AlexisBrignoni, Codex
+"""
+
+from __future__ import annotations
+
+import argparse
+import csv
+import hashlib
+import json
+import os
+import sqlite3
+from datetime import datetime, timezone
+from pathlib import Path
+
+
+TARGETS = (
+ (
+ "ActivitiesCache",
+ "ConnectedDevicesPlatform/*/ActivitiesCache.db",
+ ),
+ (
+ "Windows Notifications",
+ "Microsoft/Windows/Notifications/wpndatabase.db",
+ ),
+ (
+ "Windows Photos",
+ "Packages/Microsoft.Windows.Photos_*/LocalState/MediaDb*.sqlite",
+ ),
+ (
+ "Windows Sticky Notes",
+ "Packages/Microsoft.MicrosoftStickyNotes_*/LocalState/plum.sqlite",
+ ),
+)
+
+
+def utc_iso(timestamp: float | None = None) -> str:
+ value = datetime.now(timezone.utc) if timestamp is None else datetime.fromtimestamp(
+ timestamp, timezone.utc
+ )
+ return value.isoformat().replace("+00:00", "Z")
+
+
+def sha256(path: Path) -> str:
+ digest = hashlib.sha256()
+ with path.open("rb") as source:
+ for chunk in iter(lambda: source.read(1024 * 1024), b""):
+ digest.update(chunk)
+ return digest.hexdigest().upper()
+
+
+def windows_relative_path(path: Path) -> Path:
+ drive = path.drive.rstrip(":") or "C"
+ relative = str(path)[len(path.drive) :].lstrip("\\/")
+ return Path(drive, *Path(relative).parts)
+
+
+def discover(local_app_data: Path) -> list[tuple[str, Path]]:
+ discovered: list[tuple[str, Path]] = []
+ seen: set[str] = set()
+ for artifact, pattern in TARGETS:
+ for path in local_app_data.glob(pattern):
+ key = str(path).lower()
+ if path.is_file() and key not in seen:
+ seen.add(key)
+ discovered.append((artifact, path))
+ return discovered
+
+
+def snapshot_database(source: Path, destination: Path) -> tuple[str, int, int]:
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ if destination.exists():
+ destination.unlink()
+
+ source_uri = f"file:{source.as_posix()}?mode=ro"
+ with sqlite3.connect(source_uri, uri=True, timeout=10) as source_db:
+ source_db.execute("PRAGMA query_only=ON")
+ with sqlite3.connect(destination) as destination_db:
+ source_db.backup(destination_db)
+ integrity = destination_db.execute("PRAGMA quick_check").fetchone()[0]
+ page_count = destination_db.execute("PRAGMA page_count").fetchone()[0]
+ user_version = destination_db.execute("PRAGMA user_version").fetchone()[0]
+ return str(integrity), int(page_count), int(user_version)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--destination-root", type=Path, required=True)
+ parser.add_argument("--stage", default="live-sqlite-snapshots")
+ args = parser.parse_args()
+
+ local_app_data_value = os.environ.get("LOCALAPPDATA")
+ if not local_app_data_value:
+ raise SystemExit("LOCALAPPDATA is unavailable; run this script in Windows.")
+
+ stage_root = args.destination_root / args.stage
+ files_root = stage_root / "files"
+ stage_root.mkdir(parents=True, exist_ok=True)
+ snapshot_utc = utc_iso()
+ rows: list[dict[str, object]] = []
+
+ for artifact, source in discover(Path(local_app_data_value)):
+ stat = source.stat()
+ destination = files_root / windows_relative_path(source)
+ error = ""
+ integrity = ""
+ page_count = ""
+ user_version = ""
+ snapshot_hash = ""
+ snapshot_size = ""
+ succeeded = False
+ try:
+ integrity, page_count, user_version = snapshot_database(
+ source, destination
+ )
+ snapshot_hash = sha256(destination)
+ snapshot_size = destination.stat().st_size
+ succeeded = integrity == "ok"
+ except (OSError, sqlite3.Error) as exception:
+ error = str(exception)
+
+ rows.append(
+ {
+ "SnapshotUtc": snapshot_utc,
+ "SourceCreatedUtc": utc_iso(stat.st_ctime),
+ "SourceModifiedUtc": utc_iso(stat.st_mtime),
+ "Artifact": artifact,
+ "SourcePath": str(source),
+ "SnapshotPath": str(destination),
+ "SourceSize": stat.st_size,
+ "SnapshotSize": snapshot_size,
+ "SnapshotSHA256": snapshot_hash,
+ "QuickCheck": integrity,
+ "PageCount": page_count,
+ "UserVersion": user_version,
+ "Succeeded": succeeded,
+ "SnapshotError": error,
+ "AcquisitionMethod": (
+ "Python sqlite3 backup API from read-only source connection"
+ ),
+ "EvidenceStatus": (
+ "Examiner-derived logical SQLite snapshot; not byte-for-byte"
+ ),
+ }
+ )
+
+ manifest_path = stage_root / "snapshot-manifest.tsv"
+ with manifest_path.open("w", encoding="utf-8-sig", newline="") as output:
+ writer = csv.DictWriter(
+ output,
+ fieldnames=list(rows[0]) if rows else [
+ "SnapshotUtc",
+ "SourceCreatedUtc",
+ "SourceModifiedUtc",
+ "Artifact",
+ "SourcePath",
+ "SnapshotPath",
+ "SourceSize",
+ "SnapshotSize",
+ "SnapshotSHA256",
+ "QuickCheck",
+ "PageCount",
+ "UserVersion",
+ "Succeeded",
+ "SnapshotError",
+ "AcquisitionMethod",
+ "EvidenceStatus",
+ ],
+ delimiter="\t",
+ )
+ writer.writeheader()
+ writer.writerows(rows)
+
+ metadata = {
+ "SnapshotUtc": snapshot_utc,
+ "Stage": args.stage,
+ "DatabaseCount": sum(bool(row["Succeeded"]) for row in rows),
+ "FailedCount": sum(not bool(row["Succeeded"]) for row in rows),
+ "EvidenceStatus": (
+ "Examiner-derived logical SQLite snapshots for parser testing"
+ ),
+ }
+ (stage_root / "snapshot-metadata.json").write_text(
+ json.dumps(metadata, indent=2), encoding="utf-8"
+ )
+ print(f"SQLite snapshot stage written to {stage_root}")
+ print(
+ f"Succeeded: {metadata['DatabaseCount']}; "
+ f"failed: {metadata['FailedCount']}"
+ )
+ return 0 if metadata["FailedCount"] == 0 else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/admin/windows_lab/Write-DLEAPPAction.ps1 b/admin/windows_lab/Write-DLEAPPAction.ps1
new file mode 100644
index 0000000..057f176
--- /dev/null
+++ b/admin/windows_lab/Write-DLEAPPAction.ps1
@@ -0,0 +1,39 @@
+# Append a controlled action to the DLEAPP Windows corpus journal.
+# Authors: @AlexisBrignoni, Codex
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)][string]$Artifact,
+ [Parameter(Mandatory = $true)][string]$Action,
+ [Parameter(Mandatory = $true)][string]$Token,
+ [string]$Details = "",
+ [string]$JournalPath = "C:\DLEAPP_Lab\action-journal.tsv"
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version 2.0
+. "$PSScriptRoot\DLEAPPLab.Common.ps1"
+
+if ($Token -notmatch "^DLEAPP-[A-Z0-9-]+$") {
+ throw "Token must start with DLEAPP- and contain only A-Z, 0-9, and hyphens."
+}
+
+New-DLEAPPDirectory -Path (Split-Path -Parent $JournalPath)
+if (-not (Test-Path -LiteralPath $JournalPath)) {
+ "TimestampUtc`tTimestampLocal`tUtcOffset`tArtifact`tAction`tToken`tDetails" |
+ Set-Content -LiteralPath $JournalPath -Encoding UTF8
+}
+
+$now = Get-Date
+$row = @(
+ $now.ToUniversalTime().ToString("o"),
+ $now.ToString("o"),
+ [TimeZoneInfo]::Local.GetUtcOffset($now).ToString(),
+ (ConvertTo-DLEAPPTsvValue $Artifact),
+ (ConvertTo-DLEAPPTsvValue $Action),
+ (ConvertTo-DLEAPPTsvValue $Token),
+ (ConvertTo-DLEAPPTsvValue $Details)
+) -join "`t"
+Add-Content -LiteralPath $JournalPath -Value $row -Encoding UTF8
+
+Write-Output $row
diff --git a/scripts/artifacts/windowsSystem.py b/scripts/artifacts/windowsSystem.py
new file mode 100644
index 0000000..8ee1fb3
--- /dev/null
+++ b/scripts/artifacts/windowsSystem.py
@@ -0,0 +1,514 @@
+"""Modern Windows system artifacts migrated from WLEAPP.
+
+The implementations retain records with unfamiliar payloads instead of
+silently discarding them and label timestamp epochs and device-local values.
+
+Authors: @AlexisBrignoni, Codex
+Predecessor: abrignoni/WLEAPP activitiesCache.py, windowsNotification.py,
+windowsStickyNotes.py, and setupapiDev.py.
+"""
+
+from __future__ import annotations
+
+import base64
+import hashlib
+import json
+import os
+import re
+from datetime import datetime, timedelta, timezone
+
+from bs4 import BeautifulSoup
+
+from scripts.ilapfuncs import artifact_processor, logfunc, open_sqlite_db_readonly
+
+
+__artifacts_v2__ = {
+ "activitiesCache": {
+ "name": "ActivitiesCache",
+ "description": "Windows Connected Devices Platform activity records, "
+ "including event times, application identifiers, status "
+ "fields, and preserved payload content.",
+ "author": "@AlexisBrignoni, Codex",
+ "creation_date": "2026-07-29",
+ "last_update_date": "2026-07-29",
+ "requirements": "none",
+ "category": "Windows System",
+ "notes": "Modernized from WLEAPP. Timestamps are interpreted as Unix "
+ "seconds in UTC. Payloads that are not JSON remain visible.",
+ "paths": (
+ "*/AppData/Local/ConnectedDevicesPlatform/*/ActivitiesCache.db*",
+ ),
+ "output_types": ["html", "tsv", "timeline", "lava"],
+ "artifact_icon": "activity",
+ "sample_data": {
+ "windows11_arm_parallels": "Windows build 26200 | 5 rows",
+ },
+ },
+ "windowsNotifications": {
+ "name": "Notifications",
+ "description": "Windows notification records with arrival and expiry "
+ "times, handler identity, notification type, extracted "
+ "text, and the preserved payload.",
+ "author": "@AlexisBrignoni, Codex",
+ "creation_date": "2026-07-29",
+ "last_update_date": "2026-07-29",
+ "requirements": "beautifulsoup4",
+ "category": "Windows System",
+ "notes": "Modernized from WLEAPP. Arrival, expiry, and boot values use "
+ "the Windows FILETIME epoch and are reported in UTC.",
+ "paths": (
+ "*/AppData/Local/Microsoft/Windows/Notifications/wpndatabase.db*",
+ ),
+ "output_types": ["html", "tsv", "timeline", "lava"],
+ "artifact_icon": "bell",
+ "sample_data": {
+ "windows11_arm_parallels": "Windows build 26200 | 3 rows",
+ },
+ },
+ "windowsStickyNotes": {
+ "name": "Sticky Notes",
+ "description": "Windows Sticky Notes content and state, including "
+ "updated, created, and deleted times, note identifiers, "
+ "open state, pin state, theme, and window position.",
+ "author": "@AlexisBrignoni, Codex",
+ "creation_date": "2026-07-29",
+ "last_update_date": "2026-07-29",
+ "requirements": "none",
+ "category": "Windows System",
+ "notes": "Modernized from WLEAPP. Times are .NET ticks converted to "
+ "UTC. Empty notes are retained because their metadata can "
+ "still have forensic value.",
+ "paths": (
+ "*/AppData/Local/Packages/Microsoft.MicrosoftStickyNotes_*/"
+ "LocalState/plum.sqlite*",
+ ),
+ "output_types": ["html", "tsv", "timeline", "lava"],
+ "artifact_icon": "file-text",
+ "sample_data": {
+ "windows11_arm_parallels": "Sticky Notes 6.1.4.0 | 2 rows",
+ },
+ },
+ "setupapiSections": {
+ "name": "SetupAPI Sections",
+ "description": "Section-level events from setupapi.dev.log, including "
+ "device-install and other setup operations, their local "
+ "start/end times, device instance identifiers when "
+ "present, and recorded exit status.",
+ "author": "@AlexisBrignoni, Codex",
+ "creation_date": "2026-07-29",
+ "last_update_date": "2026-07-29",
+ "requirements": "none",
+ "category": "Windows System",
+ "notes": "Modernized from WLEAPP. Times are explicitly device-local "
+ "because setupapi.dev.log does not record a UTC offset. A "
+ "section start is not automatically labeled as a device's "
+ "first connection.",
+ "paths": (
+ "*/Windows/INF/setupapi.dev.log",
+ "*/WINDOWS/INF/setupapi.dev.log",
+ ),
+ "output_types": ["html", "tsv", "timeline", "lava"],
+ "artifact_icon": "plug",
+ "sample_data": {
+ "windows11_arm_parallels": "Windows build 26200 | 1 row",
+ },
+ },
+}
+
+
+_UNIX_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
+_WINDOWS_FILETIME_EPOCH_TICKS = 116444736000000000
+_DOTNET_UNIX_EPOCH_TICKS = 621355968000000000
+_TICKS_PER_SECOND = 10_000_000
+_NOTE_MARKUP = re.compile(
+ r"\\id=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
+ r"[0-9a-f]{4}-[0-9a-f]{12}\s*",
+ re.IGNORECASE,
+)
+_SETUP_SECTION = re.compile(
+ r"^>>>\s+\[(?P.+?)\]\s*$"
+ r"(?P.*?)"
+ r"^<<<\s+Section end\s+(?P\d{4}/\d{2}/\d{2}\s+"
+ r"\d{2}:\d{2}:\d{2}\.\d+)\s*$"
+ r"(?P.*?)(?=^>>>\s+\[|\Z)",
+ re.MULTILINE | re.DOTALL,
+)
+_SETUP_START = re.compile(
+ r"^>>>\s+Section start\s+(?P\d{4}/\d{2}/\d{2}\s+"
+ r"\d{2}:\d{2}:\d{2}\.\d+)\s*$",
+ re.MULTILINE,
+)
+_SETUP_STATUS = re.compile(r"^<<<\s+\[Exit status:\s*(.+?)\]\s*$", re.MULTILINE)
+
+
+def _utc_from_unix_seconds(value):
+ if value in (None, "", 0):
+ return ""
+ try:
+ return _UNIX_EPOCH + timedelta(seconds=float(value))
+ except (OverflowError, TypeError, ValueError):
+ return ""
+
+
+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_dotnet_ticks(value):
+ if value in (None, "", 0):
+ return ""
+ try:
+ seconds = (int(value) - _DOTNET_UNIX_EPOCH_TICKS) / _TICKS_PER_SECOND
+ return _UNIX_EPOCH + timedelta(seconds=seconds)
+ except (OverflowError, TypeError, ValueError):
+ return ""
+
+
+def _yes_no(value):
+ if value == 1:
+ return "Yes"
+ if value == 0:
+ return "No"
+ return "Unknown" if value is not None else ""
+
+
+def _decode_text(value):
+ if value is None:
+ return ""
+ if isinstance(value, bytes):
+ return value.decode("utf-8", "replace")
+ return str(value)
+
+
+def _application_ids(raw_app_id):
+ text = _decode_text(raw_app_id)
+ try:
+ values = json.loads(text)
+ except (TypeError, ValueError):
+ return "", text
+ applications = []
+ for value in values if isinstance(values, list) else ():
+ application = value.get("application") if isinstance(value, dict) else ""
+ if application and application not in applications:
+ applications.append(application)
+ return " | ".join(applications), text
+
+
+def _activity_payload(raw_payload):
+ text = _decode_text(raw_payload)
+ display_text = ""
+ app_display_name = ""
+ preview = ""
+ try:
+ payload = json.loads(text)
+ if isinstance(payload, dict):
+ display_text = payload.get("displayText") or ""
+ app_display_name = payload.get("appDisplayName") or ""
+ except (TypeError, ValueError):
+ try:
+ decoded = base64.b64decode(text, validate=True)
+ except (ValueError, TypeError):
+ decoded = b""
+ readable = []
+ for match in re.finditer(rb"(?:[\x20-\x7e]\x00){3,}", decoded):
+ value = match.group().decode("utf-16-le", "ignore").strip()
+ if value and value not in readable:
+ readable.append(value)
+ for match in re.finditer(rb"[\x20-\x7e]{4,}", decoded):
+ value = match.group().decode("utf-8", "ignore").strip()
+ if value and value not in readable:
+ readable.append(value)
+ preview = " | ".join(readable)
+ return display_text, app_display_name, preview, text
+
+
+@artifact_processor
+def activitiesCache(context):
+ data_headers = (
+ ("Start Time (UTC)", "datetime"),
+ ("End Time (UTC)", "datetime"),
+ ("Last Modified (UTC)", "datetime"),
+ ("Expiration Time (UTC)", "datetime"),
+ ("Last Modified on Client (UTC)", "datetime"),
+ "App Activity ID",
+ "Applications",
+ "Display Text",
+ "App Display Name",
+ "Activity Type",
+ "Activity Status",
+ "Tag",
+ "Group",
+ "Local Only",
+ "Read",
+ "Payload Preview",
+ "Payload",
+ "App ID JSON",
+ "Source File",
+ )
+ rows = []
+ sources = []
+ for file_found in map(str, context.get_files_found()):
+ if os.path.basename(file_found).lower() != "activitiescache.db":
+ continue
+ database = open_sqlite_db_readonly(file_found)
+ if database is None:
+ continue
+ try:
+ records = database.execute(
+ """
+ SELECT StartTime, EndTime, LastModifiedTime, ExpirationTime,
+ LastModifiedOnClient, AppActivityId, AppId, Payload,
+ ActivityType, ActivityStatus, Tag, "Group", IsLocalOnly,
+ IsRead
+ FROM Activity
+ ORDER BY StartTime DESC
+ """
+ ).fetchall()
+ except Exception as exception: # pylint: disable=broad-exception-caught
+ logfunc(f"ActivitiesCache: could not read '{file_found}': {exception}")
+ database.close()
+ continue
+ database.close()
+ sources.append(file_found)
+ relative_source = context.get_relative_path(file_found)
+ for record in records:
+ applications, raw_app_id = _application_ids(record[6])
+ display_text, app_display_name, preview, payload = _activity_payload(
+ record[7]
+ )
+ rows.append((
+ _utc_from_unix_seconds(record[0]),
+ _utc_from_unix_seconds(record[1]),
+ _utc_from_unix_seconds(record[2]),
+ _utc_from_unix_seconds(record[3]),
+ _utc_from_unix_seconds(record[4]),
+ record[5] or "",
+ applications,
+ display_text,
+ app_display_name,
+ record[8],
+ record[9],
+ record[10] or "",
+ record[11] or "",
+ _yes_no(record[12]),
+ _yes_no(record[13]),
+ preview,
+ payload,
+ raw_app_id,
+ relative_source,
+ ))
+ return data_headers, rows, "\n".join(sources)
+
+
+def _payload_details(raw_payload):
+ payload = _decode_text(raw_payload)
+ text = BeautifulSoup(payload, "html.parser").get_text(" ", strip=True)
+ payload_hash = hashlib.sha256(
+ raw_payload if isinstance(raw_payload, bytes) else payload.encode("utf-8")
+ ).hexdigest().upper()
+ return text, payload, len(payload.encode("utf-8")), payload_hash
+
+
+@artifact_processor
+def windowsNotifications(context):
+ data_headers = (
+ ("Arrival Time (UTC)", "datetime"),
+ ("Expiry Time (UTC)", "datetime"),
+ "Handler Created (database value)",
+ "Handler Modified (database value)",
+ ("Boot ID Time (UTC)", "datetime"),
+ "Notification ID",
+ "Handler ID",
+ "Handler Primary ID",
+ "Handler Type",
+ "Type",
+ "Payload Type",
+ "Text",
+ "Tag",
+ "Group",
+ "Expires on Reboot",
+ "Payload Bytes",
+ "Payload SHA-256",
+ "Payload",
+ "Source File",
+ )
+ rows = []
+ sources = []
+ for file_found in map(str, context.get_files_found()):
+ if os.path.basename(file_found).lower() != "wpndatabase.db":
+ continue
+ database = open_sqlite_db_readonly(file_found)
+ if database is None:
+ continue
+ try:
+ records = database.execute(
+ """
+ SELECT n.ArrivalTime, n.ExpiryTime, h.CreatedTime,
+ h.ModifiedTime, n.BootId, n.Id, n.HandlerId,
+ h.PrimaryId, h.HandlerType, n.Type, n.PayloadType,
+ n.Payload, n.Tag, n."Group", n.ExpiresOnReboot
+ FROM Notification AS n
+ LEFT JOIN NotificationHandler AS h
+ ON h.RecordId = n.HandlerId
+ ORDER BY n.ArrivalTime DESC
+ """
+ ).fetchall()
+ except Exception as exception: # pylint: disable=broad-exception-caught
+ logfunc(f"Notifications: could not read '{file_found}': {exception}")
+ database.close()
+ continue
+ database.close()
+ sources.append(file_found)
+ relative_source = context.get_relative_path(file_found)
+ for record in records:
+ text, payload, payload_size, payload_hash = _payload_details(record[11])
+ rows.append((
+ _utc_from_filetime(record[0]),
+ _utc_from_filetime(record[1]),
+ record[2] or "",
+ record[3] or "",
+ _utc_from_filetime(record[4]),
+ record[5],
+ record[6],
+ record[7] or "",
+ record[8] or "",
+ record[9] or "",
+ record[10] or "",
+ text,
+ record[12] or "",
+ record[13] or "",
+ _yes_no(record[14]),
+ payload_size,
+ payload_hash,
+ payload,
+ relative_source,
+ ))
+ return data_headers, rows, "\n".join(sources)
+
+
+@artifact_processor
+def windowsStickyNotes(context):
+ data_headers = (
+ ("Updated Time (UTC)", "datetime"),
+ ("Created Time (UTC)", "datetime"),
+ ("Deleted Time (UTC)", "datetime"),
+ "Note ID",
+ "Parent ID",
+ "Text",
+ "Open",
+ "Always on Top",
+ "Theme",
+ "Window Position",
+ "Source File",
+ )
+ rows = []
+ sources = []
+ for file_found in map(str, context.get_files_found()):
+ if os.path.basename(file_found).lower() != "plum.sqlite":
+ continue
+ database = open_sqlite_db_readonly(file_found)
+ if database is None:
+ continue
+ try:
+ records = database.execute(
+ """
+ SELECT UpdatedAt, CreatedAt, DeletedAt, Id, ParentId, Text,
+ IsOpen, IsAlwaysOnTop, Theme, WindowPosition
+ FROM Note
+ ORDER BY UpdatedAt DESC
+ """
+ ).fetchall()
+ except Exception as exception: # pylint: disable=broad-exception-caught
+ logfunc(f"Sticky Notes: could not read '{file_found}': {exception}")
+ database.close()
+ continue
+ database.close()
+ sources.append(file_found)
+ relative_source = context.get_relative_path(file_found)
+ for record in records:
+ rows.append((
+ _utc_from_dotnet_ticks(record[0]),
+ _utc_from_dotnet_ticks(record[1]),
+ _utc_from_dotnet_ticks(record[2]),
+ record[3] or "",
+ record[4] or "",
+ _NOTE_MARKUP.sub("", record[5] or "", count=1),
+ _yes_no(record[6]),
+ _yes_no(record[7]),
+ record[8] or "",
+ record[9] or "",
+ relative_source,
+ ))
+ return data_headers, rows, "\n".join(sources)
+
+
+def _setup_device(header):
+ match = re.match(
+ r"Device Install \([^)]+\)\s*-\s*(.+)$",
+ header,
+ re.IGNORECASE,
+ )
+ return match.group(1).strip() if match else ""
+
+
+def _parse_setup_sections(text, source):
+ rows = []
+ for section in _SETUP_SECTION.finditer(text):
+ start_match = _SETUP_START.search(section.group("body"))
+ if not start_match:
+ continue
+ status_match = _SETUP_STATUS.search(section.group("trailer"))
+ start_text = start_match.group("start")
+ end_text = section.group("end")
+ try:
+ start_value = datetime.strptime(start_text, "%Y/%m/%d %H:%M:%S.%f")
+ end_value = datetime.strptime(end_text, "%Y/%m/%d %H:%M:%S.%f")
+ duration = round((end_value - start_value).total_seconds(), 3)
+ except ValueError:
+ duration = ""
+ header = section.group("header").strip()
+ rows.append((
+ start_text,
+ end_text,
+ header,
+ _setup_device(header),
+ status_match.group(1).strip() if status_match else "",
+ duration,
+ source,
+ ))
+ return rows
+
+
+@artifact_processor
+def setupapiSections(context):
+ data_headers = (
+ "Start Time (device local)",
+ "End Time (device local)",
+ "Section",
+ "Device Instance ID",
+ "Exit Status",
+ "Duration (seconds)",
+ "Source File",
+ )
+ rows = []
+ sources = []
+ for file_found in map(str, context.get_files_found()):
+ if os.path.basename(file_found).lower() != "setupapi.dev.log":
+ continue
+ try:
+ with open(file_found, "r", encoding="utf-8-sig", errors="replace") as source:
+ text = source.read()
+ except OSError as exception:
+ logfunc(f"SetupAPI Sections: could not read '{file_found}': {exception}")
+ continue
+ sources.append(file_found)
+ rows.extend(_parse_setup_sections(
+ text, context.get_relative_path(file_found)
+ ))
+ return data_headers, rows, "\n".join(sources)
diff --git a/windows-system.dlprofile b/windows-system.dlprofile
new file mode 100644
index 0000000..e4922f3
--- /dev/null
+++ b/windows-system.dlprofile
@@ -0,0 +1 @@
+{"leapp":"dleapp","format_version":1,"plugins":["activitiesCache","windowsNotifications","windowsStickyNotes","setupapiSections"]}