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"]}