src/Public/Collect/Invoke-FotonFilesystemCollection.ps1
|
# Invoke-FotonFilesystemCollection # Live facts only. Called from Invoke-FotonTriage when that exists. # Endpoint collect. Rebuild dist/Foton-Collect.ps1 after edits. function Invoke-FotonFilesystemCollection { Write-FotonStatus 'Collecting filesystem artifacts, execution evidence, and device history' 'Step' Invoke-FotonCollection -Category 'Filesystem' -Name 'executables_in_user_writable_paths' -Format 'Csv' -Description 'Executable content in temp/appdata/programdata/public paths' -Script { $roots = @("$env:SystemRoot\Temp", "$env:ProgramData", "$env:PUBLIC") foreach ($userProfile in (Get-FotonUserProfile)) { if (-not $userProfile.Exists) { continue } $roots += (Join-Path $userProfile.ProfilePath 'AppData\Local\Temp') $roots += (Join-Path $userProfile.ProfilePath 'AppData\Roaming') $roots += (Join-Path $userProfile.ProfilePath 'Downloads') $roots += (Join-Path $userProfile.ProfilePath 'Desktop') } $depth = 3 if ($script:FotonSession.IncludeFileTimeline) { $depth = 6 } $exts = @('.exe', '.dll', '.ps1', '.bat', '.cmd', '.vbs', '.js', '.jse', '.wsf', '.hta', '.scr', '.jar', '.lnk', '.msi', '.sys', '.com', '.pif') $rows = @() foreach ($r in ($roots | Sort-Object -Unique)) { if (-not (Test-FotonPathAccessible -Path $r)) { continue } try { $files = Get-ChildItem -LiteralPath $r -Recurse -File -Force -Depth $depth -ErrorAction SilentlyContinue | Where-Object { $exts -contains $_.Extension.ToLower() -and -not (Test-FotonCaseOutputPath -Path $_.FullName) } foreach ($f in $files) { $sig = Get-FotonSignatureInfo -Path $f.FullName $rows += [PSCustomObject]@{ Path = $f.FullName SizeBytes = $f.Length CreatedUtc = $f.CreationTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') ModifiedUtc = $f.LastWriteTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') AccessedUtc = $f.LastAccessTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') Extension = $f.Extension SignatureStatus = $sig.Status Signer = $sig.Signer SHA256 = (Get-FotonSafeHash -Path $f.FullName) } } } catch { } } $rows | Sort-Object ModifiedUtc -Descending } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'recently_modified_system_files' -Format 'Csv' -Description 'System32/SysWOW64 binaries modified inside the collection window' -Script { $cutoff = (Get-Date).AddDays(-1 * $script:FotonSession.EventLogDays) $rows = @() foreach ($dir in @("$env:SystemRoot\System32", "$env:SystemRoot\SysWOW64", "$env:SystemRoot")) { if (-not (Test-Path -LiteralPath $dir)) { continue } try { Get-ChildItem -LiteralPath $dir -File -Force -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt $cutoff -and $_.Extension -match '\.(exe|dll|sys|ps1|bat|cmd)$' } | ForEach-Object { $sig = Get-FotonSignatureInfo -Path $_.FullName $rows += [PSCustomObject]@{ Path = $_.FullName SizeBytes = $_.Length ModifiedUtc = $_.LastWriteTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') CreatedUtc = $_.CreationTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') SignatureStatus = $sig.Status SHA256 = (Get-FotonSafeHash -Path $_.FullName) } } } catch { } } $rows | Sort-Object ModifiedUtc -Descending } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'prefetch_listing' -Format 'Csv' -Description 'Prefetch entries (execution evidence, names and timestamps only)' -RequiresElevation -Script { $pf = "$env:SystemRoot\Prefetch" if (-not (Test-Path -LiteralPath $pf)) { return @() } Get-ChildItem -LiteralPath $pf -Filter '*.pf' -Force -ErrorAction SilentlyContinue | Select-Object Name, Length, @{n = 'CreatedUtc'; e = { $_.CreationTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') } }, @{n = 'LastRunUtc_FileWrite'; e = { $_.LastWriteTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') } } | Sort-Object LastRunUtc_FileWrite -Descending } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'prefetch_files' -Format 'Csv' -Description 'Copied Prefetch .pf files (capped; parse these, not the live directory)' -RequiresElevation -Script { $pf = "$env:SystemRoot\Prefetch" $dest = Join-Path $script:FotonSession.Dirs['Filesystem'] 'prefetch' $rel = Join-Path $script:FotonSession.Categories['Filesystem'] 'prefetch' Copy-FotonCappedHostFiles -SourceDirectory $pf -Filter '*.pf' -DestinationDirectory $dest -PackageRelativeDirectory $rel } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'amcache_hive' -Format 'Csv' -Description 'Copied Amcache.hve plus hive logs (locked-file copy)' -RequiresElevation -Script { $src = Join-Path $env:SystemRoot 'AppCompat\Programs\Amcache.hve' $dest = Join-Path $script:FotonSession.Dirs['Filesystem'] 'amcache' $rel = Join-Path $script:FotonSession.Categories['Filesystem'] 'amcache' Copy-FotonHostFileFamily -SourcePath $src -DestinationDirectory $dest -PackageRelativeDirectory $rel } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'srum_database' -Format 'Csv' -Description 'Copied SRUDB.dat plus WAL/SHM if present (capped; locked-file copy)' -RequiresElevation -Script { $src = Join-Path $env:ProgramData 'Microsoft\Windows\SRUM\SRUDB.dat' $dest = Join-Path $script:FotonSession.Dirs['Filesystem'] 'srum' $rel = Join-Path $script:FotonSession.Categories['Filesystem'] 'srum' Copy-FotonHostFileFamily -SourcePath $src -DestinationDirectory $dest -PackageRelativeDirectory $rel -MaxTotalBytes 104857600 } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'registry_hives_machine' -Format 'Csv' -Description 'SYSTEM/SOFTWARE/DEFAULT hives exported with reg save (loaded hives cannot be file-copied; not parsed on the endpoint). SAM and SECURITY are not collected by design. For RECmd / Registry Explorer.' -RequiresElevation -Script { $destRoot = Join-Path $script:FotonSession.Dirs['Filesystem'] 'hives\machine' $relRoot = Join-Path $script:FotonSession.Categories['Filesystem'] 'hives\machine' $rows = @() foreach ($h in @( @{ Key = 'HKLM\SYSTEM'; File = 'SYSTEM' }, @{ Key = 'HKLM\SOFTWARE'; File = 'SOFTWARE' }, @{ Key = 'HKU\.DEFAULT'; File = 'DEFAULT' })) { $rows += Save-FotonRegistryHive -RegistryPath $h.Key -DestinationPath (Join-Path $destRoot $h.File) ` -PackageRelativePath (Join-Path $relRoot $h.File) } # SAM and SECURITY hold credential material (local password hashes, LSA # secrets). Exporting them is the textbook credential-theft step, so EDR and # AMSI treat it as such; the account and policy facts Foton needs come from # live queries instead. Recorded as a disclosed gap, not silently omitted. foreach ($name in @('SAM', 'SECURITY')) { $rows += [PSCustomObject]@{ SourcePath = "HKLM\$name" DestRelative = '' SizeBytes = 0 SHA256 = '' Status = 'NotCollectedByDesign' Note = 'Credential hive. Not exported: it holds password hashes / LSA secrets and exporting it matches credential-theft detections. Use a full image or KAPE if the case needs it.' } } $rows } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'registry_hives_user' -Format 'Csv' -Description 'NTUSER.DAT and UsrClass.dat per profile: file copy for unloaded hives, reg save for loaded ones (capped; not parsed on the endpoint).' -RequiresElevation -Script { $destRoot = Join-Path $script:FotonSession.Dirs['Filesystem'] 'hives\users' $relRoot = Join-Path $script:FotonSession.Categories['Filesystem'] 'hives\users' $rows = @() $budget = [int64]209715200 foreach ($userProfile in (Get-FotonUserProfile)) { if (-not $userProfile.Exists) { continue } $seg = Get-FotonSafePathSegment -Name $userProfile.UserName $userDest = Join-Path $destRoot $seg $userRel = Join-Path $relRoot $seg $hives = @( @{ Src = (Join-Path $userProfile.ProfilePath 'NTUSER.DAT'); Key = ('HKU\{0}' -f $userProfile.SID); File = 'NTUSER.DAT' }, @{ Src = (Join-Path $userProfile.ProfilePath 'AppData\Local\Microsoft\Windows\UsrClass.dat'); Key = ('HKU\{0}_Classes' -f $userProfile.SID); File = 'UsrClass.dat' } ) foreach ($h in $hives) { if ($budget -le 0) { break } $fam = @(Copy-FotonHostFileFamily -SourcePath $h.Src -DestinationDirectory $userDest -PackageRelativeDirectory $userRel -MaxTotalBytes $budget) $primary = $fam | Where-Object { [IO.Path]::GetFileName([string]$_.SourcePath) -eq $h.File } | Select-Object -First 1 # A signed-in user's hive is loaded and locked exactly like the machine # hives. Fall back to reg save of the loaded key; the copied sidecars # (.LOG*, .regtrans-ms) stay in the package as context. if ($primary -and $primary.Status -eq 'CopyError' -and $userProfile.HiveLoaded -and (Test-FotonPathAccessible -Path ('Registry::HKEY_USERS\{0}' -f ($h.Key -replace '^HKU\\', '')))) { # The on-disk hive size is readable even while locked; respect the # per-collection user-hive budget before exporting. $onDisk = [int64]0 try { $onDisk = (Get-Item -LiteralPath $h.Src -Force -ErrorAction Stop).Length } catch { } if ($onDisk -gt $budget) { $saved = [PSCustomObject]@{ SourcePath = $h.Key; DestRelative = ''; SizeBytes = $onDisk; SHA256 = ''; Status = 'CappedSkip' Note = 'Loaded hive exceeds the remaining user-hive copy budget; not exported.' } } else { $saved = Save-FotonRegistryHive -RegistryPath $h.Key -DestinationPath (Join-Path $userDest $h.File) ` -PackageRelativePath (Join-Path $userRel $h.File) } $rest = @($fam | Where-Object { $_ -ne $primary }) if ($saved.Status -eq 'Saved') { # Locked .LOG sidecars are moot once the hive is exported whole. foreach ($r in $rest) { if ($r.Status -eq 'CopyError') { $r.Status = 'SupersededBySave' $r.Note = 'Locked transaction log of a loaded hive; the reg save export already includes its changes.' } } } $fam = $rest + @($saved) } $rows += $fam foreach ($r in $fam) { if ($r.Status -in @('Copied', 'Saved')) { $budget = $budget - [int64]$r.SizeBytes } } } } if ($rows.Count -eq 0) { return @([PSCustomObject]@{ SourcePath = ''; DestRelative = ''; SizeBytes = 0; SHA256 = ''; Status = 'NoData' Note = 'No profile hives were copied. Standard-user collect skips other users; missing NTUSER is a visibility gap.' }) } $rows } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'usn_journal' -Format 'Csv' -Description 'USN queryjournal metadata; optional $J copy when -IncludeUsnJournal is set. $MFT is not copied (use KAPE/ORC).' -RequiresElevation -Script { $destDir = Join-Path $script:FotonSession.Dirs['Filesystem'] 'usn' $rel = Join-Path $script:FotonSession.Categories['Filesystem'] 'usn' if (-not (Test-Path -LiteralPath $destDir)) { New-Item -Path $destDir -ItemType Directory -Force | Out-Null } $rows = @() $drive = $env:SystemDrive if (-not $drive) { $drive = 'C:' } $metaPath = Join-Path $destDir 'usn_queryjournal.txt' try { $out = & fsutil.exe usn queryjournal $drive 2>&1 | Out-String Set-Content -LiteralPath $metaPath -Value $out -Encoding UTF8 $hash = Get-FotonSafeHash -Path $metaPath $len = (Get-Item -LiteralPath $metaPath).Length $rows += [PSCustomObject]@{ SourcePath = ("fsutil usn queryjournal {0}" -f $drive) DestRelative = (Join-Path $rel 'usn_queryjournal.txt') SizeBytes = $len SHA256 = $hash Status = 'Copied' Note = 'Journal metadata only. Not a full USN parse.' } } catch { $rows += [PSCustomObject]@{ SourcePath = ("fsutil usn queryjournal {0}" -f $drive) DestRelative = '' SizeBytes = 0 SHA256 = '' Status = 'CopyError' Note = $_.Exception.Message } } if ($script:FotonSession.IncludeUsnJournal) { $jPath = Join-Path $drive '\$Extend\$UsnJrnl:$J' $jDest = Join-Path $destDir 'UsnJrnl_J.bin' $copy = Copy-FotonHostFile -SourcePath $jPath -DestinationPath $jDest $jRel = Join-Path $rel 'UsnJrnl_J.bin' $note = $copy.Note if ($copy.Status -ne 'Copied') { $note = 'Optional $J copy failed or is unsupported on this volume. Use KAPE/ORC for USN. ' + [string]$copy.Note } $rows += [PSCustomObject]@{ SourcePath = $jPath DestRelative = $jRel SizeBytes = $copy.SizeBytes SHA256 = $copy.SHA256 Status = $copy.Status Note = $note } } else { $rows += [PSCustomObject]@{ SourcePath = '$Extend\$UsnJrnl:$J' DestRelative = '' SizeBytes = 0 SHA256 = '' Status = 'Skipped' Note = 'Full USN $J copy is off by default (size). Re-collect with -IncludeUsnJournal. $MFT is never copied by Foton.' } } $rows } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'bam_dam_user_settings' -Format 'Csv' -Description 'BAM/DAM UserSettings (path = value name; FILETIME in data). Admin. Not a baseline.' -RequiresElevation -Script { $rows = @() $pairs = @( @{ Kind = 'BAM'; Root = 'HKLM:\SYSTEM\CurrentControlSet\Services\bam\State\UserSettings' }, @{ Kind = 'BAM'; Root = 'HKLM:\SYSTEM\CurrentControlSet\Services\bam\UserSettings' }, @{ Kind = 'DAM'; Root = 'HKLM:\SYSTEM\CurrentControlSet\Services\dam\State\UserSettings' }, @{ Kind = 'DAM'; Root = 'HKLM:\SYSTEM\CurrentControlSet\Services\dam\UserSettings' } ) foreach ($p in $pairs) { $rows += @(Get-FotonBamDamRows -Kind $p.Kind -Root $p.Root) } $seen = @{} $out = @() foreach ($r in $rows) { $k = '{0}|{1}|{2}' -f $r.Kind, $r.SID, $r.Path if ($seen.ContainsKey($k)) { continue } $seen[$k] = $true $out += $r } $out | Sort-Object Kind, SID, Path } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'appcompatcache_export' -Format 'Csv' -Description 'Raw AppCompatCache (Shimcache) blob copied into the package; entries are not parsed' -RequiresElevation -Script { $key = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache' $blob = Get-FotonRegValue -Path $key -Name 'AppCompatCache' $destDir = Join-Path $script:FotonSession.Dirs['Filesystem'] 'appcompat' $dest = Join-Path $destDir 'AppCompatCache.bin' $row = [PSCustomObject]@{ SourceKey = $key DestName = 'AppCompatCache.bin' SizeBytes = 0 SHA256 = '' Status = 'Missing' HeaderHex = '' Note = '' } if ($null -eq $blob) { $row.Note = 'AppCompatCache value not present or not readable. Visibility gap, not proof Shimcache was empty.' return @($row) } $bytes = $null if ($blob -is [byte[]]) { $bytes = $blob } else { try { $bytes = [byte[]]$blob } catch { } } if ($null -eq $bytes -or $bytes.Length -eq 0) { $row.Status = 'CopyError' $row.Note = 'Value was not REG_BINARY. Structured Shimcache parse was not attempted.' return @($row) } try { if (-not (Test-Path -LiteralPath $destDir)) { New-Item -Path $destDir -ItemType Directory -Force | Out-Null } [IO.File]::WriteAllBytes($dest, $bytes) $item = Get-Item -LiteralPath $dest -Force -ErrorAction Stop $n = [Math]::Min(8, $bytes.Length) $hdr = New-Object byte[] $n [Array]::Copy($bytes, $hdr, $n) $row.SizeBytes = $item.Length $row.SHA256 = Get-FotonSafeHash -Path $dest $row.Status = 'Copied' $row.HeaderHex = ([BitConverter]::ToString($hdr) -replace '-', '').ToLowerInvariant() $row.Note = 'Raw blob at 08_Filesystem/appcompat/AppCompatCache.bin. Entry-level Shimcache parse is not implemented; wrong execution times would be worse than this export.' } catch { $row.Status = 'CopyError' $row.Note = $_.Exception.Message } @($row) } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'alternate_data_streams' -Format 'Csv' -Description 'Non-default NTFS streams in user paths (Zone.Identifier and beyond)' -Script { $rows = @() $roots = @("$env:PUBLIC") foreach ($userProfile in (Get-FotonUserProfile)) { if (-not $userProfile.Exists) { continue } $roots += (Join-Path $userProfile.ProfilePath 'Downloads') $roots += (Join-Path $userProfile.ProfilePath 'Desktop') $roots += (Join-Path $userProfile.ProfilePath 'AppData\Local\Temp') } foreach ($r in ($roots | Sort-Object -Unique)) { if (-not (Test-FotonPathAccessible -Path $r)) { continue } try { foreach ($f in (Get-ChildItem -LiteralPath $r -Recurse -File -Force -Depth 3 -ErrorAction SilentlyContinue)) { if (Test-FotonCaseOutputPath -Path $f.FullName) { continue } try { foreach ($s in (Get-Item -LiteralPath $f.FullName -Stream * -ErrorAction Stop)) { if ($s.Stream -eq ':$DATA') { continue } $content = '' if ($s.Length -lt 4096) { try { $content = ((Get-Content -LiteralPath $f.FullName -Stream $s.Stream -ErrorAction Stop) -join ' | ') } catch { } } $rows += [PSCustomObject]@{ File = $f.FullName Stream = $s.Stream LengthBytes = $s.Length Content = $content } } } catch { } } } catch { } } $rows } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'recycle_bin_contents' -Format 'Csv' -Description 'Deleted items still present in $Recycle.Bin per SID' -Script { $rows = @() foreach ($drive in (Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue)) { $rb = Join-Path $drive.Root '$Recycle.Bin' if (-not (Test-Path -LiteralPath $rb)) { continue } try { foreach ($f in (Get-ChildItem -LiteralPath $rb -Recurse -File -Force -ErrorAction SilentlyContinue)) { $rows += [PSCustomObject]@{ Drive = $drive.Root SidFolder = (Split-Path (Split-Path $f.FullName -Parent) -Leaf) FileName = $f.Name SizeBytes = $f.Length DeletedUtc = $f.CreationTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') FullName = $f.FullName } } } catch { } } $rows | Sort-Object DeletedUtc -Descending } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'usb_device_history' -Format 'Csv' -Description 'USBSTOR entries and mounted device history (exfiltration pivot)' -Script { $rows = @() $usbstor = 'HKLM:\SYSTEM\CurrentControlSet\Enum\USBSTOR' if (Test-Path -LiteralPath $usbstor) { foreach ($dev in (Get-ChildItem -LiteralPath $usbstor -ErrorAction SilentlyContinue)) { foreach ($inst in (Get-ChildItem -LiteralPath $dev.PSPath -ErrorAction SilentlyContinue)) { $friendly = Get-FotonRegValue -Path $inst.PSPath -Name 'FriendlyName' $rows += [PSCustomObject]@{ Source = 'USBSTOR' Device = (Split-Path $dev.Name -Leaf) SerialOrId = (Split-Path $inst.Name -Leaf) FriendlyName = [string]$friendly Value = '' } } } } foreach ($v in (Get-FotonRegValueList -Path 'HKLM:\SYSTEM\MountedDevices')) { $rows += [PSCustomObject]@{ Source = 'MountedDevices'; Device = $v.Name; SerialOrId = ''; FriendlyName = ''; Value = $v.Value } } $rows } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'shortcut_and_jumplist_inventory' -Format 'Csv' -Description 'Recent LNK and jump list files (file access evidence)' -Script { $rows = @() foreach ($userProfile in (Get-FotonUserProfile)) { if (-not $userProfile.Exists) { continue } $paths = @( (Join-Path $userProfile.ProfilePath 'AppData\Roaming\Microsoft\Windows\Recent'), (Join-Path $userProfile.ProfilePath 'AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations'), (Join-Path $userProfile.ProfilePath 'AppData\Roaming\Microsoft\Windows\Recent\CustomDestinations') ) foreach ($p in $paths) { if (-not (Test-FotonPathAccessible -Path $p)) { continue } foreach ($f in (Get-ChildItem -LiteralPath $p -File -Force -ErrorAction SilentlyContinue)) { $rows += [PSCustomObject]@{ User = $userProfile.UserName Folder = (Split-Path $p -Leaf) Name = $f.Name SizeBytes = $f.Length ModifiedUtc = $f.LastWriteTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') FullName = $f.FullName } } } } $rows | Sort-Object ModifiedUtc -Descending } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'shortcut_and_jumplist_files' -Format 'Csv' -Description 'Copied Recent .lnk and jumplist files (capped). Parse these copies, not the live profile.' -RequiresElevation -Script { $destRoot = Join-Path $script:FotonSession.Dirs['Filesystem'] 'lnk_jumplist' $relRoot = Join-Path $script:FotonSession.Categories['Filesystem'] 'lnk_jumplist' $items = @() foreach ($userProfile in (Get-FotonUserProfile)) { if (-not $userProfile.Exists) { continue } $seg = Get-FotonSafePathSegment -Name $userProfile.UserName $spec = @( @{ Dir = (Join-Path $userProfile.ProfilePath 'AppData\Roaming\Microsoft\Windows\Recent'); Filter = '*.lnk'; Folder = 'Recent' } @{ Dir = (Join-Path $userProfile.ProfilePath 'AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations'); Filter = '*.automaticDestinations-ms'; Folder = 'AutomaticDestinations' } @{ Dir = (Join-Path $userProfile.ProfilePath 'AppData\Roaming\Microsoft\Windows\Recent\CustomDestinations'); Filter = '*.customDestinations-ms'; Folder = 'CustomDestinations' } ) foreach ($s in $spec) { if (-not (Test-FotonPathAccessible -Path $s.Dir)) { continue } foreach ($f in (Get-ChildItem -LiteralPath $s.Dir -Filter $s.Filter -File -Force -ErrorAction SilentlyContinue)) { $items += [PSCustomObject]@{ SourcePath = $f.FullName DestRelative = (Join-Path $seg (Join-Path $s.Folder $f.Name)) } } } } Copy-FotonCappedHostFileList -Items $items -DestinationDirectory $destRoot -PackageRelativeDirectory $relRoot -MaxFiles 400 -MaxTotalBytes 83886080 } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'scheduled_task_xml' -Format 'Csv' -Description 'Copied %SystemRoot%\System32\Tasks XML (capped). Complements the task CSV; not parsed on the endpoint.' -RequiresElevation -Script { $srcRoot = Join-Path $env:SystemRoot 'System32\Tasks' $destRoot = Join-Path $script:FotonSession.Dirs['Filesystem'] 'task_xml' $relRoot = Join-Path $script:FotonSession.Categories['Filesystem'] 'task_xml' $items = @() if (Test-Path -LiteralPath $srcRoot) { foreach ($f in (Get-ChildItem -LiteralPath $srcRoot -File -Recurse -Force -ErrorAction SilentlyContinue)) { $rel = $f.FullName.Substring($srcRoot.Length).TrimStart('\') $items += [PSCustomObject]@{ SourcePath = $f.FullName; DestRelative = $rel } } } Copy-FotonCappedHostFileList -Items $items -DestinationDirectory $destRoot -PackageRelativeDirectory $relRoot -MaxFiles 500 -MaxTotalBytes 52428800 } | Out-Null # Deep only: copy script bodies referenced by scheduled-task Actions. The task CSV records the Actions, but without the script files the package cannot show what they ran. if ($script:FotonSession.Deep) { Invoke-FotonCollection -Category 'Filesystem' -Name 'task_action_scripts' -Format 'Csv' -Description 'Deep: copied local script files referenced by scheduled-task Execute/Arguments (raised caps; UNC skipped; under 2 GiB free skips copy).' -Script { $destRoot = Join-Path $script:FotonSession.Dirs['Filesystem'] 'task_scripts' $relRoot = Join-Path $script:FotonSession.Categories['Filesystem'] 'task_scripts' if (-not (Test-FotonDeepDiskBudget)) { return @([PSCustomObject]@{ SourcePath = '' DestRelative = '' SizeBytes = 0 SHA256 = '' Status = 'SkippedLowDisk' Note = 'Deep task-script copy skipped: output volume has under 2 GiB free. Visibility gap, not proof scripts were absent.' }) } $items = @() $preRows = New-Object System.Collections.ArrayList $seen = @{} try { foreach ($t in (Get-ScheduledTask -ErrorAction Stop)) { foreach ($a in @($t.Actions)) { $exe = '' $args = '' if ($a.Execute) { $exe = [string]$a.Execute } if ($a.Arguments) { $args = [string]$a.Arguments } foreach ($p in (Get-FotonTaskActionScriptPath -Execute $exe -Arguments $args)) { $key = $p.ToLowerInvariant() if ($seen.ContainsKey($key)) { continue } $seen[$key] = $true # UNC: record and do not open (NTLM / remote auth). if ($p.StartsWith('\\') -or $p.StartsWith('//')) { $null = $preRows.Add([PSCustomObject]@{ SourcePath = $p DestRelative = '' SizeBytes = 0 SHA256 = '' Status = 'SkippedUnc' Note = 'UNC task-script path refused. Deep Collect does not follow remote paths (NTLM / network footprint).' }) continue } # Resolve .. and env-expanded quirks to a local full path. $full = $null try { $full = [IO.Path]::GetFullPath($p) } catch { continue } if (-not $full -or $full -notmatch '^[A-Za-z]:\\') { continue } # Keep drive letter in DestRelative so C:\a.ps1 and D:\a.ps1 do not collide. # C:\Temp\x.ps1 -> C\Temp\x.ps1 (no colon; no ..). $rel = $full.Substring(0, 1) + $full.Substring(2) $rel = $rel -replace ':', '_' if ($rel -match '(^|[\\/])\.\.([\\/]|$)') { continue } $items += [PSCustomObject]@{ SourcePath = $full; DestRelative = $rel } } } } } catch { } $copyRows = @(Copy-FotonCappedHostFileList -Items $items -DestinationDirectory $destRoot -PackageRelativeDirectory $relRoot -MaxFiles 10000 -MaxTotalBytes 1073741824) @($preRows) + @($copyRows) } | Out-Null } Invoke-FotonCollection -Category 'Filesystem' -Name 'browser_databases' -Format 'Csv' -Description 'Copied browser History/Cookies/places.sqlite (capped). No cookie decryption, no Graph. Locked files are coverage gaps.' -RequiresElevation -Script { $destRoot = Join-Path $script:FotonSession.Dirs['Filesystem'] 'browser_db' $relRoot = Join-Path $script:FotonSession.Categories['Filesystem'] 'browser_db' $items = @() foreach ($userProfile in (Get-FotonUserProfile)) { if (-not $userProfile.Exists) { continue } $seg = Get-FotonSafePathSegment -Name $userProfile.UserName $local = Join-Path $userProfile.ProfilePath 'AppData\Local' $roam = Join-Path $userProfile.ProfilePath 'AppData\Roaming' $candidates = @( (Join-Path $local 'Google\Chrome\User Data\Default\History'), (Join-Path $local 'Google\Chrome\User Data\Default\Cookies'), (Join-Path $local 'Google\Chrome\User Data\Default\Login Data'), (Join-Path $local 'Microsoft\Edge\User Data\Default\History'), (Join-Path $local 'Microsoft\Edge\User Data\Default\Cookies'), (Join-Path $local 'Microsoft\Edge\User Data\Default\Login Data'), (Join-Path $roam 'Mozilla\Firefox\Profiles') ) foreach ($c in $candidates) { if ($c -like '*Firefox\Profiles') { if (-not (Test-Path -LiteralPath $c)) { continue } foreach ($ff in (Get-ChildItem -LiteralPath $c -Directory -ErrorAction SilentlyContinue)) { foreach ($leaf in @('places.sqlite', 'cookies.sqlite')) { $p = Join-Path $ff.FullName $leaf if (Test-Path -LiteralPath $p -PathType Leaf) { $items += [PSCustomObject]@{ SourcePath = $p DestRelative = (Join-Path $seg (Join-Path ('Firefox_' + $ff.Name) $leaf)) } } } } continue } if (Test-Path -LiteralPath $c -PathType Leaf) { $brand = 'Chrome' if ($c -match 'Microsoft\\Edge') { $brand = 'Edge' } $items += [PSCustomObject]@{ SourcePath = $c DestRelative = (Join-Path $seg (Join-Path $brand ([IO.Path]::GetFileName($c)))) } } } } Copy-FotonCappedHostFileList -Items $items -DestinationDirectory $destRoot -PackageRelativeDirectory $relRoot -MaxFiles 80 -MaxTotalBytes 104857600 } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'host_text_logs' -Format 'Csv' -Description 'Copied setupapi.dev.log, CBS.log (capped), and PowerShell transcripts if the policy path exists.' -RequiresElevation -Script { $destRoot = Join-Path $script:FotonSession.Dirs['Filesystem'] 'host_logs' $relRoot = Join-Path $script:FotonSession.Categories['Filesystem'] 'host_logs' $items = @() $setup = Join-Path $env:SystemRoot 'inf\setupapi.dev.log' if (Test-Path -LiteralPath $setup) { $items += [PSCustomObject]@{ SourcePath = $setup; DestRelative = 'setupapi.dev.log' } } $cbs = Join-Path $env:SystemRoot 'Logs\CBS\CBS.log' if (Test-Path -LiteralPath $cbs) { $items += [PSCustomObject]@{ SourcePath = $cbs; DestRelative = 'CBS.log' } } $tx = Get-FotonRegValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' -Name 'OutputDirectory' if ($tx -and (Test-Path -LiteralPath ([string]$tx))) { foreach ($f in (Get-ChildItem -LiteralPath ([string]$tx) -Filter '*.txt' -File -Recurse -Force -ErrorAction SilentlyContinue | Select-Object -First 40)) { $items += [PSCustomObject]@{ SourcePath = $f.FullName; DestRelative = (Join-Path 'powershell_transcript' $f.Name) } } } $ci = Join-Path $env:SystemRoot 'System32\CodeIntegrity' if (Test-Path -LiteralPath $ci) { foreach ($f in (Get-ChildItem -LiteralPath $ci -Include '*.cip', '*.p7b', 'SiPolicy.p7b' -File -Force -ErrorAction SilentlyContinue)) { $items += [PSCustomObject]@{ SourcePath = $f.FullName; DestRelative = (Join-Path 'codeintegrity' $f.Name) } } } Copy-FotonCappedHostFileList -Items $items -DestinationDirectory $destRoot -PackageRelativeDirectory $relRoot -MaxFiles 50 -MaxTotalBytes 52428800 } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'onedrive_teams_local' -Format 'Csv' -Description 'OneDrive/Teams local folders and account registry (no Graph, no cookie/DB copy)' -Script { Get-FotonOneDriveTeamsRows } | Out-Null Invoke-FotonCollection -Category 'Filesystem' -Name 'hidden_or_odd_root_items' -Format 'Csv' -Description 'Hidden or unexpected items at drive roots and ProgramData' -Script { $rows = @() foreach ($drive in (Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue)) { try { foreach ($i in (Get-ChildItem -LiteralPath $drive.Root -Force -ErrorAction SilentlyContinue)) { $isHidden = ($i.Attributes -band [IO.FileAttributes]::Hidden) -ne 0 if (-not $isHidden) { continue } $rows += [PSCustomObject]@{ Location = $drive.Root Name = $i.Name IsDirectory = $i.PSIsContainer Attributes = [string]$i.Attributes ModifiedUtc = $i.LastWriteTimeUtc.ToString('yyyy-MM-dd HH:mm:ss') } } } catch { } } $rows } | Out-Null } |