112 lines
8.3 KiB
PowerShell
112 lines
8.3 KiB
PowerShell
$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
|