# 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