106 lines
14 KiB
PHP
106 lines
14 KiB
PHP
<?php
|
|
// Verification of authorized active effects by a repeatable READ ONLY transaction. No providers.
|
|
use Illuminate\Foundation\Bootstrap\LoadConfiguration;
|
|
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
|
|
use Illuminate\Encryption\Encrypter;
|
|
use App\Services\OrganizationCountryInstallation;
|
|
$root=dirname(__DIR__); require $root.'/vendor/autoload.php'; ini_set('zend.exception_ignore_args','1');
|
|
set_exception_handler(static function(Throwable $e):void { fwrite(STDERR,'Active verification stopped: '.$e->getMessage().PHP_EOL);exit(1); });
|
|
$app=require $root.'/bootstrap/app.php';(new LoadEnvironmentVariables)->bootstrap($app);(new LoadConfiguration)->bootstrap($app);
|
|
$dir=$root.'/storage/app/private/investmoz-v2/portal-audit';
|
|
$read=static fn($p)=>json_decode(file_get_contents($p),true,512,JSON_THROW_ON_ERROR);
|
|
$prepared=$read($dir.'/stability-active-preparation-latest.json');
|
|
$proof=$read($read($dir.'/stability-active-backup-latest.json')['path']);
|
|
$before=$proof;
|
|
$before['counts']=[];
|
|
foreach(['users','organizations','memberships','migrations','audit_events'] as $table){$before['counts'][$table]=$proof['tables']['"public"."'.$table.'"']['count'];}
|
|
throw_unless($proof['delivery_revision']==='485ee04c40b2754ab668f48fd84d8559fb19a8b98f4b9af50fc0517bb488ca76',RuntimeException::class,'Exact authorized revision required.');
|
|
$manifest=$read($root.'/outputs/investmoz-portal-stability-20261011/manifesto-candidata.json');
|
|
|
|
$mode=$argv[1] ?? 'before-apply';throw_unless(in_array($mode,['before-apply','installed-disabled','enabled','reverted'],true),RuntimeException::class,'Unknown phase.');
|
|
$expectedFingerprint=$mode==='reverted'?$before['source_code_hash']:$before['code_hash'];
|
|
throw_unless((new OrganizationCountryInstallation)->fingerprint()===$expectedFingerprint,RuntimeException::class,'Active code differs from expected phase.');
|
|
$c=config('database.connections.'.config('database.default'));
|
|
throw_unless($c['driver']==='pgsql' && empty($c['url']) && (int)$c['port']===5432 && $c['database']==='investemoz',RuntimeException::class,'Exact target required.');
|
|
$key=config('app.key');$zip=new ZipArchive;throw_unless($zip->open($proof['files_path'])===true,RuntimeException::class,'Backup unavailable.');
|
|
$runtime=json_decode($zip->getFromName('runtime/effective-runtime.json'),true,512,JSON_THROW_ON_ERROR);$zip->close();
|
|
throw_unless($runtime['app_key']===$key && $runtime['cipher']===config('app.cipher'),RuntimeException::class,'Encryption key/cipher changed.');
|
|
$key=str_starts_with($key,'base64:')?base64_decode(substr($key,7),true):$key;$crypt=new Encrypter($key,config('app.cipher'));
|
|
if($mode!=='reverted') {
|
|
throw_unless(config('app.url')==='https://portal-investidor.test' && config('session.cookie')==='portal_investidor_https'
|
|
&& config('session.secure')===true && config('session.domain')===null && config('session.path')==='/' && config('session.same_site')==='lax',RuntimeException::class,'Effective HTTPS/session config differs.');
|
|
} else {
|
|
$stage=$read($dir.'/stability-active-stage-latest.json');
|
|
foreach($stage['backup_files'] as $file=>$entry){throw_unless(hash_file('sha256',$file)===$entry['sha256'],RuntimeException::class,'Reversed file differs from snapshot.');}
|
|
}
|
|
if($mode!=='reverted'){
|
|
foreach($manifest['revision_input']['application_files'] as $file=>$entry){throw_unless(hash_file('sha256',$root.'/'.$file)===$entry['after'],RuntimeException::class,'Authorized application member differs.');}
|
|
foreach($manifest['revision_input']['assets'] as $file=>$digest){throw_unless(hash_file('sha256',$root.'/public/build/'.$file)===$digest,RuntimeException::class,'Authorized production member differs.');}
|
|
throw_unless(hash_file('sha256',$prepared['dll'])===$manifest['revision_input']['deployment_files']['runtime/nghttp2.dll'],RuntimeException::class,'Shared runtime DLL differs.');
|
|
throw_unless(hash_file('sha256',($read($dir.'/stability-active-stage-latest.json')['effective_vhost_path']??$prepared['active_vhost']))===$manifest['revision_input']['deployment_files']['portal-investidor.test-https.proposta.conf'],RuntimeException::class,'HTTPS vhost differs.');
|
|
}
|
|
$flags=[];foreach(['opportunity_dashboard_access_enabled','profile_access_enabled','opportunity_specialized_drafts_enabled','opportunity_modular_profiles_qa_enabled','legacy_content_qa_enabled'] as $flag){$flags[$flag]=(bool)config('portal.'.$flag);throw_if($flags[$flag],RuntimeException::class,'Unapproved flag enabled.');}
|
|
$flags['investor_path_enabled']=(bool)config('investor-path.enabled');throw_if($flags['investor_path_enabled'],RuntimeException::class,'Unapproved investor path flag enabled.');
|
|
$enabled=(bool)config('portal.organization_country_enabled');throw_unless($enabled===($mode==='enabled'),RuntimeException::class,'Country flag differs from phase.');
|
|
$pdo=new PDO('pgsql:host='.$c['host'].';port=5432;dbname=investemoz',$c['username'],$c['password'],[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
|
|
$pdo->exec('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
throw_unless($pdo->query('SELECT current_database()')->fetchColumn()==='investemoz' && (int)$pdo->query('SELECT inet_server_port()')->fetchColumn()===5432,RuntimeException::class,'Server identity differs.');
|
|
$tables=[];$changed=[];
|
|
foreach($pdo->query("SELECT schemaname,tablename FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname,tablename")->fetchAll(PDO::FETCH_ASSOC) as $t){
|
|
$name='"'.str_replace('"','""',$t['schemaname']).'"."'.str_replace('"','""',$t['tablename']).'"';
|
|
$rows=$pdo->query('SELECT row_to_json(t)::text FROM '.$name.' t ORDER BY row_to_json(t)::text')->fetchAll(PDO::FETCH_COLUMN);
|
|
$tables[$name]=['count'=>count($rows),'sha256'=>hash('sha256',implode("\n",$rows))];if(($before['tables'][$name]??null)!==$tables[$name])$changed[]=$name;
|
|
}
|
|
throw_unless(array_keys($tables)===array_keys($before['tables']),RuntimeException::class,'A table was created/removed.');
|
|
$allowed=['"public"."sessions"','"public"."cache"','"public"."cache_locks"'];
|
|
if(!in_array($mode,['before-apply','reverted'],true))$allowed=[...$allowed,'"public"."migrations"','"public"."audit_events"'];
|
|
$authenticationMetadata=[];
|
|
if($mode==='enabled' && in_array('"public"."users"',$changed,true)){
|
|
// Fortify's normal logout cycles remember_token. Compare every other field to the verified original clone.
|
|
$cloneTarget=$proof['restored_target'];throw_unless((int)$cloneTarget['port']===55441 && preg_match('/^country_candidate_\d{8}_\d{6}_test$/',$cloneTarget['database']),RuntimeException::class,'Original clone identity differs.');
|
|
$original=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$cloneTarget['database'],'postgres','',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);$original->exec('BEGIN READ ONLY');
|
|
$userSql='SELECT row_to_json(t)::text FROM users t ORDER BY row_to_json(t)::text';$originalRows=$original->query($userSql)->fetchAll(PDO::FETCH_COLUMN);$original->exec('ROLLBACK');
|
|
throw_unless(hash('sha256',implode("\n",$originalRows))===$before['tables']['"public"."users"']['sha256'],RuntimeException::class,'Original clone users differ from snapshot.');
|
|
$index=static function(array $rows):array{$out=[];foreach($rows as $row){$user=json_decode($row,true,512,JSON_THROW_ON_ERROR);$out[$user['id']]=$user;}ksort($out);return $out;};
|
|
$oldUsers=$index($originalRows);$newUsers=$index($pdo->query($userSql)->fetchAll(PDO::FETCH_COLUMN));
|
|
throw_unless(array_keys($oldUsers)===array_keys($newUsers),RuntimeException::class,'Identity set changed.');
|
|
$actors=$pdo->query("SELECT DISTINCT actor_user_id FROM audit_events WHERE source_system='organization-country-installation'")->fetchAll(PDO::FETCH_COLUMN);
|
|
$observedBrowser=$read($dir.'/stability-active-browser-status.json');
|
|
throw_unless(count(array_filter($observedBrowser['navigation'],static fn($r)=>$r['path']==='/logout' && $r['method']==='POST' && in_array($r['status'],[302,303],true)))>0,RuntimeException::class,'Actual normal logout not demonstrated.');
|
|
foreach($oldUsers as $id=>$oldUser){$newUser=$newUsers[$id];if($oldUser!==$newUser){throw_unless(in_array($id,$actors),RuntimeException::class,'Unrelated identity changed.');$originalToken=$oldUser['remember_token'];$newToken=$newUser['remember_token'];unset($oldUser['remember_token'],$newUser['remember_token']);throw_unless($oldUser===$newUser && $originalToken!==$newToken,RuntimeException::class,'User field other than normal logout token changed.');unset($originalToken,$newToken);$authenticationMetadata[]=['user_id'=>$id,'changed_fields'=>['remember_token'],'cause'=>'Laravel SessionGuard logout cycles the existing remember token; all other identity fields unchanged.'];}}
|
|
$allowed[]='"public"."users"';
|
|
}
|
|
$unexpected=array_values(array_diff($changed,$allowed));
|
|
if($unexpected!==[]){file_put_contents($dir.'/stability-active-unexpected-row-changes.json',json_encode(['at'=>gmdate('c'),'phase'=>$mode,'tables'=>$unexpected,'SQL_readonly'=>true,'row_values_or_secrets_exported'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));}
|
|
throw_unless($unexpected===[],RuntimeException::class,'Business rows changed outside reviewed effects.');
|
|
$counts=[];foreach(array_keys($before['counts']) as $table)$counts[$table]=(int)$pdo->query('SELECT count(*) FROM "'.$table.'"')->fetchColumn();
|
|
foreach(['users','organizations','memberships'] as $table)throw_unless($counts[$table]===$before['counts'][$table],RuntimeException::class,'An identity/organization/membership was created or removed.');
|
|
$migrations=$pdo->query("SELECT migration FROM migrations WHERE migration IN ('2026_10_09_030000_extend_opportunity_resources','2026_10_10_040000_create_portal_profile_authorization','2026_10_09_190000_prepare_investor_path','2026_10_10_050000_extend_organization_country') ORDER BY migration")->fetchAll(PDO::FETCH_COLUMN);
|
|
$columns=$pdo->query("SELECT column_name,data_type,is_nullable,character_maximum_length FROM information_schema.columns WHERE table_schema='public' AND table_name='organizations' AND column_name IN ('incorporation_country_code','incorporation_country_provenance') ORDER BY column_name")->fetchAll(PDO::FETCH_ASSOC);
|
|
$audits=$pdo->query("SELECT id,created_at,actor_user_id,correlation_id,after_data FROM audit_events WHERE source_system='organization-country-installation' ORDER BY created_at,id")->fetchAll(PDO::FETCH_ASSOC);
|
|
if(in_array($mode,['before-apply','reverted'],true)){throw_unless($migrations===[] && $columns===[] && $audits===[],RuntimeException::class,'Unexpected installation before authenticated action.');}
|
|
else {
|
|
throw_unless($migrations===[OrganizationCountryInstallation::MIGRATION] && count($columns)===2 && count($audits)===1,RuntimeException::class,'Installation effects differ.');
|
|
throw_unless($columns===[['column_name'=>'incorporation_country_code','data_type'=>'character varying','is_nullable'=>'YES','character_maximum_length'=>2],['column_name'=>'incorporation_country_provenance','data_type'=>'jsonb','is_nullable'=>'YES','character_maximum_length'=>null]],RuntimeException::class,'Country column contracts differ.');
|
|
throw_unless($counts['migrations']===$before['counts']['migrations']+1 && $counts['audit_events']===$before['counts']['audit_events']+1,RuntimeException::class,'Ledger/audit count differs.');
|
|
$receipt=json_decode($audits[0]['after_data'],true,512,JSON_THROW_ON_ERROR);throw_unless($receipt['code_hash']===$before['code_hash'] && $receipt['result']['new_columns']===2 && $receipt['result']['new_tables']===0 && $receipt['result']['new_organizations']===0,RuntimeException::class,'Audited receipt differs.');
|
|
}
|
|
$recovered=0;foreach($pdo->query('SELECT two_factor_secret FROM users WHERE two_factor_secret IS NOT NULL')->fetchAll(PDO::FETCH_COLUMN) as $encrypted){$v=$crypt->decrypt($encrypted,false);throw_unless(is_string($v)&&strlen($v)>0,RuntimeException::class,'Original MFA unreadable.');unset($v);$recovered++;}
|
|
$sequenceSql="SELECT * FROM pg_sequences WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname,sequencename";
|
|
$sequences=$pdo->query($sequenceSql)->fetchAll(PDO::FETCH_ASSOC);
|
|
$expectedSequences=$proof['sequences'];
|
|
if(in_array($mode,['installed-disabled','enabled'],true)){
|
|
foreach($expectedSequences as &$sequence){if($sequence['schemaname']==='public' && $sequence['sequencename']==='migrations_id_seq'){$sequence['last_value']++;}}unset($sequence);
|
|
}
|
|
throw_unless($sequences===$expectedSequences,RuntimeException::class,'Unapproved sequence change.');
|
|
$pdo->exec('ROLLBACK');
|
|
$result=['result'=>'passed','at'=>gmdate('c'),'phase'=>$mode,'code_hash'=>$expectedFingerprint,'prepared_code_hash'=>$before['code_hash'],'delivery_revision'=>$before['delivery_revision'],
|
|
'origin'=>config('app.url'),'country_enabled'=>$enabled,'other_flags'=>$flags,'tables'=>count($tables),'counts_before'=>$before['counts'],'counts_after'=>$counts,
|
|
'changed_tables'=>$changed,'business_rows_preserved'=>true,'columns'=>$columns,'installed_packages'=>$migrations,'installation_audits'=>count($audits),
|
|
'authentication_metadata_changes'=>$authenticationMetadata,
|
|
'original_mfa_envelopes_recovered'=>$recovered,'key_and_cipher_preserved'=>true,'active_SQL_writes_by_verifier'=>0,
|
|
'installation_audit_reference'=>array_map(static fn($audit)=>array_intersect_key($audit,array_flip(['id','created_at','actor_user_id','correlation_id'])),$audits),
|
|
'actual_master_save_available'=>$counts['organizations']>0 && $counts['memberships']>0,
|
|
'master_save_limitation'=>$counts['organizations']===0?'No real organization or membership; active master write/reuse cannot be demonstrated without separately authorized legitimate master data.':null];
|
|
file_put_contents($dir.'/stability-active-check-'.$mode.'-20261011.json',json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($result,JSON_THROW_ON_ERROR).PHP_EOL;
|