97 lines
13 KiB
PowerShell
97 lines
13 KiB
PowerShell
# Authorized B1/B2 only. Authenticated migrations/bootstrap are NOT performed here.
|
|
$ErrorActionPreference='Stop'
|
|
trap {
|
|
$failed=@{at=[DateTime]::UtcNow.ToString('o');result='service_helper_stopped';line=$_.InvocationInfo.ScriptLineNumber;error_class=$_.Exception.GetType().FullName;hresult=$_.Exception.HResult;message=[string]$_.Exception.Message}
|
|
[IO.File]::WriteAllText('C:\laragon\www\portal-investidor\storage\app\private\investmoz-v2\portal-audit\workflow-activation-services-helper-error.json',($failed|ConvertTo-Json -Depth 3),[Text.UTF8Encoding]::new($false))
|
|
exit 1
|
|
}
|
|
$taskRoot='C:\laragon\www\portal-investidor'
|
|
$taskPrivate=Join-Path $taskRoot 'storage\app\private\investmoz-v2\portal-audit'
|
|
$taskStage=Get-Content -LiteralPath (Join-Path $taskPrivate 'workflow-activation-stage.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
if($taskStage.revision -ne '41caea0f900fe0705e84e065a65f9534ff3a585ee34098f860666f522875a3a5' -or -not $taskStage.payload_hashes_verified){throw 'Fixed authorized deployment required'}
|
|
$taskSnapshot=[IO.Path]::GetFullPath($taskStage.recovery_directory)
|
|
if(-not $taskSnapshot.StartsWith(([IO.Path]::GetFullPath($taskPrivate)+'\'),[StringComparison]::OrdinalIgnoreCase) -or [IO.Path]::GetFileName($taskSnapshot) -notmatch '^workflow-activation-recovery-\d{8}_\d{6}$'){throw 'Exact recovery scope required'}
|
|
$taskReceipt=Join-Path $taskPrivate 'workflow-activation-services.json'
|
|
if(Test-Path -LiteralPath $taskReceipt){throw 'Existing intervention receipt: inspect before repeating'}
|
|
$taskPlan=Join-Path $taskRoot 'outputs\investmoz-workflow-consolidated-20261011'
|
|
$taskManifest=Get-Content -LiteralPath (Join-Path $taskPlan 'manifesto.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
foreach($prop in $taskManifest.service_proposal.PSObject.Properties){if((Get-FileHash -LiteralPath (Join-Path $taskPlan $prop.Name) -Algorithm SHA256).Hash.ToLowerInvariant() -ne $prop.Value){throw 'Service proposal hash mismatch'}}
|
|
$taskUtf8=New-Object Text.UTF8Encoding($false)
|
|
$taskSid=[Security.Principal.SecurityIdentifier]::new('S-1-5-19')
|
|
$taskProof=@{at=[DateTime]::UtcNow.ToString('o');revision=$taskStage.revision;state='before_service_changes';queue_worker_enabled=$false;database_writes=0;apache_control_performed=$false;acl_changed=@();tasks_changed=@()}
|
|
function Save-Receipt{[IO.File]::WriteAllText($taskReceipt,($taskProof|ConvertTo-Json -Depth 8),$taskUtf8)}
|
|
function Resolve-Scope([string]$p){$v=[IO.Path]::GetFullPath($p);if(-not ($v.StartsWith($taskRoot+'\',[StringComparison]::OrdinalIgnoreCase) -or $v -eq $taskRoot -or $v -eq 'C:\laragon\bin\php\php-8.5.11-Win32-vs17-x64')){throw 'ACL scope escaped authorized directory'};return $v}
|
|
$taskClam=Join-Path $taskPrivate 'clamav'
|
|
$taskRuntime=Join-Path $taskClam 'runtime\clamav-1.5.4.win.x64'
|
|
$taskReadRoots=@(Get-ChildItem -LiteralPath $taskRoot -Directory -Force | Where-Object {$_.Name -ne 'storage'} | ForEach-Object {$_.FullName})
|
|
$taskReadRoots+='C:\laragon\bin\php\php-8.5.11-Win32-vs17-x64'
|
|
$taskScopes=@($taskReadRoots)+(Join-Path $taskRoot 'storage')
|
|
$taskAclExports=@()
|
|
# Native metadata export includes descendants, including protected DACLs, without following links.
|
|
foreach($p in $taskScopes){$p=Resolve-Scope $p; $idx=$taskAclExports.Count; $dest=Join-Path $taskSnapshot ('acl-scope-'+$idx+'.txt'); $log=Join-Path $taskSnapshot ('acl-export-'+$idx+'.log'); $extended='\\?\'+$p; & icacls.exe $extended /save $dest /T /L /Q 1>$log 2>&1; if($LASTEXITCODE -ne 0){throw 'Exact descendant ACL export failed before mutations'}; $taskAclExports+=@{root=$p;restore_parent=[IO.Path]::GetDirectoryName($p);path=$dest;sha256=(Get-FileHash -LiteralPath $dest -Algorithm SHA256).Hash.ToLowerInvariant()}}
|
|
$taskRootAcls=@(); foreach($p in (@($taskRoot)+@(Get-ChildItem -LiteralPath $taskRoot -File -Force | ForEach-Object {$_.FullName}))){$a=Get-Acl -LiteralPath $p; $taskRootAcls+=@{path=$p;sddl=[string]$a.Sddl;owner=[string]$a.Owner}}
|
|
[IO.File]::WriteAllText((Join-Path $taskSnapshot 'acl-exact-before.json'),(@{exports=$taskAclExports;root_objects=$taskRootAcls;owners_unchanged=$true}|ConvertTo-Json -Depth 6),$taskUtf8)
|
|
$taskScheduler=New-Object -ComObject 'Schedule.Service';$taskScheduler.Connect();$taskFolder=$taskScheduler.GetFolder('\')
|
|
foreach($name in @('InvestMoz ClamAV Loopback Scanner','InvestMoz ClamAV Signature Update')){$oldTask=$taskFolder.GetTask($name);[IO.File]::WriteAllText((Join-Path $taskSnapshot ($name.Replace(' ','-')+'-immediate-before.xml')),$oldTask.Xml,$taskUtf8);[IO.File]::WriteAllText((Join-Path $taskSnapshot ($name.Replace(' ','-')+'-security-before.txt')),$oldTask.GetSecurityDescriptor(7),$taskUtf8)}
|
|
try{$existingWorker=$taskFolder.GetTask('InvestMoz Portal Documentary Worker');throw 'Worker already exists: inspect before replacement'}catch{if($_.Exception.HResult -ne -2147024894){throw}}
|
|
$taskProof.exact_acl_exports=$taskAclExports.Count
|
|
Save-Receipt
|
|
function Set-ScopedAccess([string]$p,[string]$kind,[bool]$inherit=$true){
|
|
$p=Resolve-Scope $p;$a=Get-Acl -LiteralPath $p;$a.SetAccessRuleProtection($true,$true)
|
|
foreach($rule in @($a.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]))){if($rule.IdentityReference.Value -eq $taskSid.Value){$a.RemoveAccessRuleSpecific($rule)}}
|
|
$flags=if((Test-Path -LiteralPath $p -PathType Container) -and $inherit){[Security.AccessControl.InheritanceFlags]3}else{[Security.AccessControl.InheritanceFlags]0}
|
|
if($kind -eq 'deny'){$a.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($taskSid,[Security.AccessControl.FileSystemRights]::FullControl,$flags,'None','Deny'))}
|
|
elseif($kind -eq 'modify'){$a.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($taskSid,[Security.AccessControl.FileSystemRights]::Modify,$flags,'None','Allow'))}
|
|
else{$a.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($taskSid,[Security.AccessControl.FileSystemRights]852310,$flags,'None','Deny'));$a.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($taskSid,[Security.AccessControl.FileSystemRights]::ReadAndExecute,$flags,'None','Allow'))}
|
|
Set-Acl -LiteralPath $p -AclObject $a; $taskProof.acl_changed+=@{path=$p;mode=$kind;inherit=$inherit}
|
|
}
|
|
try{
|
|
Set-ScopedAccess $taskRoot 'readonly' $false
|
|
foreach($f in Get-ChildItem -LiteralPath $taskRoot -File -Force){Set-ScopedAccess $f.FullName 'readonly' $false}
|
|
foreach($p in $taskReadRoots){Set-ScopedAccess $p 'readonly'}
|
|
Set-ScopedAccess (Join-Path $taskRoot 'storage') 'readonly'
|
|
foreach($p in @('storage\framework','storage\logs','bootstrap\cache')){Set-ScopedAccess (Join-Path $taskRoot $p) 'modify'}
|
|
$privateDir=Join-Path $taskRoot 'storage\app\private'
|
|
# Existing unrelated private data remains inaccessible to the shared service identity.
|
|
foreach($p in Get-ChildItem -LiteralPath $privateDir -Directory){Set-ScopedAccess $p.FullName 'deny'}
|
|
foreach($p in Get-ChildItem -LiteralPath $privateDir -File){Set-ScopedAccess $p.FullName 'deny' $false}
|
|
$a=Get-Acl -LiteralPath $privateDir;$a.SetAccessRuleProtection($true,$true)
|
|
foreach($rule in @($a.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]))){if($rule.IdentityReference.Value -eq $taskSid.Value){$a.RemoveAccessRuleSpecific($rule)}}
|
|
$a.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($taskSid,'ReadAndExecute','None','None','Allow'))
|
|
$a.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($taskSid,'WriteData','None','None','Allow'))
|
|
# Only newly created immediate scan-temporary files inherit Modify, not child directories.
|
|
$a.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($taskSid,'Modify','ObjectInherit','NoPropagateInherit,InheritOnly','Allow'))
|
|
Set-Acl -LiteralPath $privateDir -AclObject $a;$taskProof.acl_changed+=@{path=$privateDir;mode='create_scan_temp_files_only'}
|
|
foreach($p in @('portal-documents','opportunity-resources')){if(Test-Path -LiteralPath (Join-Path $privateDir $p)){Set-ScopedAccess (Join-Path $privateDir $p) 'modify'}}
|
|
Set-ScopedAccess $taskClam 'readonly'
|
|
Set-ScopedAccess (Join-Path $taskClam 'database') 'modify'
|
|
foreach($p in @('clamd.log','freshclam.log')){$f=Join-Path $taskClam $p;if(-not (Test-Path -LiteralPath $f)){[IO.File]::WriteAllText($f,'',$taskUtf8)};Set-ScopedAccess $f 'modify' $false}
|
|
# Protected descendant DACLs do not inherit a parent's new policy; constrain them explicitly.
|
|
foreach($export in $taskAclExports){$lines=[IO.File]::ReadAllLines($export.path,[Text.Encoding]::Unicode); for($i=0;$i+1 -lt $lines.Length;$i+=2){if($lines[$i+1] -notmatch '^D:P'){continue}; $p=[IO.Path]::GetFullPath((Join-Path $export.restore_parent $lines[$i])); if(-not (Test-Path -LiteralPath $p)){continue};if($p.StartsWith($privateDir+'\',[StringComparison]::OrdinalIgnoreCase) -and -not $p.StartsWith($taskClam+'\',[StringComparison]::OrdinalIgnoreCase)){if($p.StartsWith((Join-Path $privateDir 'portal-documents')+'\',[StringComparison]::OrdinalIgnoreCase) -or $p.StartsWith((Join-Path $privateDir 'opportunity-resources')+'\',[StringComparison]::OrdinalIgnoreCase)){Set-ScopedAccess $p 'modify'}else{Set-ScopedAccess $p 'deny'}}elseif(-not $p.StartsWith((Join-Path $taskRoot 'storage\framework')+'\',[StringComparison]::OrdinalIgnoreCase) -and -not $p.StartsWith((Join-Path $taskRoot 'storage\logs')+'\',[StringComparison]::OrdinalIgnoreCase) -and -not $p.StartsWith((Join-Path $taskRoot 'bootstrap\cache')+'\',[StringComparison]::OrdinalIgnoreCase)){Set-ScopedAccess $p 'readonly'}}}
|
|
$taskProof.state='acl_applied_worker_disabled';Save-Receipt
|
|
# Register all definitions initially disabled. The old port3310 instance alone is stopped.
|
|
foreach($name in @('InvestMoz ClamAV Loopback Scanner','InvestMoz ClamAV Signature Update')){$oldTask=$taskFolder.GetTask($name);$oldTask.Enabled=$false;if($oldTask.State -eq 4){$oldTask.Stop(0)}}
|
|
$deadline=[DateTime]::UtcNow.AddSeconds(30);do{$live=@(Get-NetTCPConnection -State Listen -LocalPort 3310 -ErrorAction SilentlyContinue);if($live.Count -eq 0){break};Start-Sleep -Milliseconds 500}while([DateTime]::UtcNow -lt $deadline)
|
|
if($live.Count -ne 0){throw 'Port3310 did not stop via its task control; no replacement started'}
|
|
foreach($item in @(@('InvestMoz ClamAV Loopback Scanner','InvestMoz-ClamAV-Loopback-Scanner.xml'),@('InvestMoz ClamAV Signature Update','InvestMoz-ClamAV-Signature-Update.xml'),@('InvestMoz Portal Documentary Worker','InvestMoz-Portal-Documentary-Worker.xml'))){
|
|
$def=$taskScheduler.NewTask(0);$def.XmlText=[IO.File]::ReadAllText((Join-Path $taskPlan ('servicos\'+$item[1])));$def.Settings.Enabled=$false;$def.Principal.UserId='S-1-5-19';$def.Principal.LogonType=5;$def.Principal.RunLevel=0;$def.RegistrationInfo.Description='InvestMoz authorized41caea0f900f B1/B2; scoped LocalService; operating flags OFF.'
|
|
foreach($tr in $def.Triggers){if($tr.Type -eq 1){$tr.StartBoundary=[DateTime]::Now.AddMinutes(10).ToString('yyyy-MM-ddTHH:mm:ss')}}
|
|
$registered=$taskFolder.RegisterTaskDefinition($item[0],$def,6,'S-1-5-19',$null,5,$null);$taskProof.tasks_changed+=@{name=$item[0];principal='S-1-5-19';enabled=$false;execution_limit=$registered.Definition.Settings.ExecutionTimeLimit}
|
|
}
|
|
$scanner=$taskFolder.GetTask('InvestMoz ClamAV Loopback Scanner');$scanner.Enabled=$true;$null=$scanner.Run($null)
|
|
$taskProof.state='scanner_starting_worker_and_signature_tasks_disabled';Save-Receipt
|
|
$deadline=[DateTime]::UtcNow.AddSeconds(55);do{$live=@(Get-NetTCPConnection -State Listen -LocalPort 3310 -ErrorAction SilentlyContinue);if($live.Count -gt 0){break};Start-Sleep -Milliseconds 500}while([DateTime]::UtcNow -lt $deadline)
|
|
if($live.Count -ne 1){throw 'Replacement scanner did not bind exact loopback3310'}
|
|
$proc=Get-CimInstance Win32_Process -Filter ('ProcessId='+$live[0].OwningProcess)
|
|
if($proc.ExecutablePath -ne (Join-Path $taskRuntime 'clamd.exe')){throw 'Unexpected scanner process owns3310'}
|
|
$taskProof.scanner_pid=$proc.ProcessId;$taskProof.scanner_runtime='x64';Save-Receipt
|
|
$probeName='InvestMoz Workflow LocalService Preflight 41caea0f900f'
|
|
$def=$taskScheduler.NewTask(0);$def.Principal.Id='Service';$def.Principal.UserId='S-1-5-19';$def.Principal.LogonType=5;$def.Principal.RunLevel=0;$def.Actions.Context='Service'
|
|
$def.Settings.Enabled=$false;$def.Settings.ExecutionTimeLimit='PT3M';$def.Settings.MultipleInstances=2;$def.Settings.DisallowStartIfOnBatteries=$false;$def.Settings.StopIfGoingOnBatteries=$false
|
|
$a=$def.Actions.Create(0);$a.Path='C:\laragon\bin\php\php-8.5.11-Win32-vs17-x64\php.exe';$a.Arguments='-d memory_limit=256M tools/portal-workflow-activation-localservice-probe.php';$a.WorkingDirectory=$taskRoot
|
|
$probe=$taskFolder.RegisterTaskDefinition($probeName,$def,2,'S-1-5-19',$null,5,$null)
|
|
try{$probe.Enabled=$true;$null=$probe.Run($null);Start-Sleep -Milliseconds 750;$deadline=[DateTime]::UtcNow.AddSeconds(55);do{if($probe.State -ne 4){break};Start-Sleep -Milliseconds 500}while([DateTime]::UtcNow -lt $deadline);$taskProof.localservice_probe_task_result=$probe.LastTaskResult;if($probe.State -eq 4 -or $probe.LastTaskResult -ne 0){throw 'LocalService preflight failed: preserve worker OFF and inspect sanitized proof'}}finally{$probe.Enabled=$false;if($probe.State -eq 4){$probe.Stop(0)};$taskFolder.DeleteTask($probeName,0)}
|
|
$taskProof.state='scanner_running_LocalService_verified_worker_disabled';Save-Receipt
|
|
}catch{$taskProof.state='stopped_before_authenticated_installation';$taskProof.error_class=$_.Exception.GetType().FullName;$taskProof.hresult=$_.Exception.HResult;Save-Receipt;exit 1}
|
|
@{state=$taskProof.state;acl_roots_changed=$taskProof.acl_changed.Count;tasks_changed=$taskProof.tasks_changed.Count;worker_enabled=$false;database_writes=0}|ConvertTo-Json -Compress
|