$ErrorActionPreference = 'Stop' $taskRoot = 'C:\laragon\www\portal-investidor' $privateRoot = Join-Path $taskRoot 'storage\app\private\investmoz-v2\portal-audit' $snapshotRoot = Join-Path $privateRoot ('workflow-execution-services-' + [DateTime]::UtcNow.ToString('yyyyMMdd_HHmmss')) if (Test-Path -LiteralPath $snapshotRoot) { throw 'An existing snapshot must not be replaced.' } New-Item -ItemType Directory -Path $snapshotRoot | Out-Null $utf8 = New-Object System.Text.UTF8Encoding($false) $files = New-Object 'System.Collections.Generic.List[object]' function Save-PrivateSnapshotFile { param([string]$Source,[string]$Entry) if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw 'Required service recovery file is absent.' } if ($Entry.Contains('..') -or $Entry.Contains(':') -or $Entry.StartsWith('/')) { throw 'Snapshot entry refused.' } $destination = Join-Path $snapshotRoot $Entry.Replace('/', '\') New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null $before = (Get-FileHash -LiteralPath $Source -Algorithm SHA256).Hash.ToLowerInvariant() Copy-Item -LiteralPath $Source -Destination $destination $saved = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash.ToLowerInvariant() $after = (Get-FileHash -LiteralPath $Source -Algorithm SHA256).Hash.ToLowerInvariant() if ($before -ne $saved -or $before -ne $after) { throw 'A live recovery file changed during the read-only snapshot.' } $files.Add([pscustomobject]@{ source=$Source; entry=$Entry; sha256=$saved }) } $service = New-Object -ComObject 'Schedule.Service' $service.Connect() $taskFolder = $service.GetFolder('\') $tasks = @() foreach ($name in @('InvestMoz ClamAV Loopback Scanner','InvestMoz ClamAV Signature Update','InvestMoz Portal Documentary Worker')) { $existing = $null try { $existing = $taskFolder.GetTask($name) } catch { if ($name -ne 'InvestMoz Portal Documentary Worker') { throw 'A required existing ClamAV task cannot be read.' } if ($_.Exception.HResult -ne -2147024894) { throw 'Cannot distinguish an absent worker task from an inaccessible task.' } } if ($null -eq $existing) { $tasks += [pscustomobject]@{name=$name;exists=$false}; continue } $entry = 'tasks/' + $name.Replace(' ','-') + '-before.xml' $xmlPath = Join-Path $snapshotRoot $entry.Replace('/', '\') New-Item -ItemType Directory -Path (Split-Path -Parent $xmlPath) -Force | Out-Null [System.IO.File]::WriteAllText($xmlPath,$existing.Xml.Replace('encoding="UTF-16"','encoding="UTF-8"'),$utf8) [xml]$parsed = [System.IO.File]::ReadAllText($xmlPath) $tasks += [pscustomobject]@{ name=$name;exists=$true;state=$existing.State;enabled=$existing.Enabled entry=$entry;sha256=(Get-FileHash -LiteralPath $xmlPath -Algorithm SHA256).Hash.ToLowerInvariant() security_descriptor=$existing.GetSecurityDescriptor(7);xml_roundtrip=$null -ne $parsed.Task } $files.Add([pscustomobject]@{source=$null;entry=$entry;sha256=(Get-FileHash -LiteralPath $xmlPath -Algorithm SHA256).Hash.ToLowerInvariant()}) } $clamRoot = Join-Path $privateRoot 'clamav' $clamRuntime = Join-Path $clamRoot 'runtime\clamav-1.5.4.win.x64' Save-PrivateSnapshotFile (Join-Path $clamRoot 'clamd.conf') 'scanner/clamd.conf' Save-PrivateSnapshotFile (Join-Path $clamRoot 'freshclam.conf') 'scanner/freshclam.conf' $clamConfigText = [System.IO.File]::ReadAllText((Join-Path $clamRoot 'clamd.conf')) $databaseMatch = [regex]::Match($clamConfigText,'(?m)^\s*DatabaseDirectory\s+([^\r\n#]+)') if (-not $databaseMatch.Success) { throw 'The actual scanner database directory is not explicit.' } $signatureRoot = $databaseMatch.Groups[1].Value.Trim().Trim('"') if (-not [System.IO.Path]::IsPathRooted($signatureRoot)) { throw 'Scanner signature directory must be absolute.' } foreach ($file in Get-ChildItem -LiteralPath $clamRuntime -File) { Save-PrivateSnapshotFile $file.FullName ('scanner/runtime/' + $file.Name) } $signatureFiles = @(Get-ChildItem -LiteralPath $signatureRoot -File | Where-Object { $_.Extension -in @('.cvd','.cld','.dat') }) if ($signatureFiles.Count -lt 3) { throw 'Scanner signature snapshot is incomplete.' } foreach ($file in $signatureFiles) { Save-PrivateSnapshotFile $file.FullName ('scanner/signatures/' + $file.Name) } Save-PrivateSnapshotFile 'C:\laragon\bin\php\php-8.5.11-Win32-vs17-x64\php.ini' 'php/php.ini' Save-PrivateSnapshotFile 'C:\laragon\etc\apache2\mod_php.conf' 'apache/mod_php.conf' # Read DACL/owner metadata only. No Set-Acl, registration, stopping or service command. $aclPaths = @( 'C:\laragon','C:\laragon\www',$taskRoot,(Join-Path $taskRoot '.env'), (Join-Path $taskRoot 'app'),(Join-Path $taskRoot 'vendor'),(Join-Path $taskRoot 'config'), (Join-Path $taskRoot 'routes'),(Join-Path $taskRoot 'resources'),(Join-Path $taskRoot 'database'), (Join-Path $taskRoot 'public'),(Join-Path $taskRoot 'public\build'), (Join-Path $taskRoot 'bootstrap'),(Join-Path $taskRoot 'bootstrap\cache'), (Join-Path $taskRoot 'storage'),(Join-Path $taskRoot 'storage\app'), (Join-Path $taskRoot 'storage\app\private'),(Join-Path $taskRoot 'storage\framework'), (Join-Path $taskRoot 'storage\logs'),(Join-Path $taskRoot 'storage\app\private\documents'), (Join-Path $taskRoot 'storage\app\private\quarantine'), (Join-Path $taskRoot 'storage\app\private\investmoz-v2'),$privateRoot, $clamRoot,$clamRuntime,$signatureRoot,(Join-Path $clamRoot 'clamd.conf'),(Join-Path $clamRoot 'freshclam.conf'), 'C:\laragon\bin\php\php-8.5.11-Win32-vs17-x64','C:\laragon\bin\php\php-8.5.11-Win32-vs17-x64\php.ini', 'C:\laragon\etc\apache2\mod_php.conf','C:\laragon\etc\apache2\sites-enabled\portal-investidor.test.conf' ) foreach ($dir in @('storage\framework','storage\app\private\documents','storage\app\private\quarantine')) { $path = Join-Path $taskRoot $dir if (Test-Path -LiteralPath $path -PathType Container) { $aclPaths += @(Get-ChildItem -LiteralPath $path -Directory -Recurse | ForEach-Object { $_.FullName }) } } foreach ($file in Get-ChildItem -LiteralPath (Join-Path $taskRoot 'storage\app\private') -File -Filter '*scan*') { $aclPaths += $file.FullName } $acls = @() foreach ($path in @($aclPaths | Sort-Object -Unique)) { if (-not (Test-Path -LiteralPath $path)) { $acls += [pscustomobject]@{path=$path;exists=$false};continue } $acl = Get-Acl -LiteralPath $path $sddl = $acl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::All) $check = New-Object System.Security.AccessControl.RawSecurityDescriptor($sddl) if ($check.GetSddlForm([System.Security.AccessControl.AccessControlSections]::All) -ne $sddl) { throw 'DACL metadata cannot round-trip.' } $acls += [pscustomobject]@{path=$path;exists=$true;owner=$acl.Owner;sddl=$sddl;inheritance_protected=$acl.AreAccessRulesProtected;roundtrip=$true} } $aclFile = Join-Path $snapshotRoot 'acl-before.json' [System.IO.File]::WriteAllText($aclFile,($acls | ConvertTo-Json -Depth 7),$utf8) $files.Add([pscustomobject]@{source=$null;entry='acl-before.json';sha256=(Get-FileHash -LiteralPath $aclFile -Algorithm SHA256).Hash.ToLowerInvariant()}) $proof = [pscustomobject]@{ at=[DateTime]::UtcNow.ToString('o');directory=$snapshotRoot;files=$files;tasks=$tasks acl_objects=@($acls | Where-Object { $_.exists }).Count;acl_metadata_roundtrip=$true acl_scope='Explicit targeted roots, ancestors, runtime/config and operational document/cache directory metadata; extend exact affected descendant exports before any recursive future ACL change.' acl_restore_applied_on_destination=$false;signature_files=$signatureFiles.Count;signature_directory=$signatureRoot listeners=@(Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in @(80,443,3310) } | Select-Object LocalAddress,LocalPort,OwningProcess) active_services_changes=0;registered_tasks=0;active_acl_changes=0 } [System.IO.File]::WriteAllText((Join-Path $snapshotRoot 'snapshot.json'),($proof | ConvertTo-Json -Depth 10),$utf8) [System.IO.File]::WriteAllText((Join-Path $privateRoot 'workflow-execution-services-latest.json'),($proof | ConvertTo-Json -Depth 10),$utf8) @{result='read_only_services_snapshot';files=$files.Count;signature_files=$signatureFiles.Count;acl_objects=$proof.acl_objects;existing_tasks=@($tasks | Where-Object { $_.exists }).Count;services_changed=0;acl_changes=0} | ConvertTo-Json -Compress