Files
Investidor-V2/tools/portal-country-https-package.py
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

109 lines
20 KiB
Python

"""Seal demonstrated COUNTRY02 + HTTPS proposal; never deploy sources, certs or readiness."""
from pathlib import Path
from datetime import datetime,timezone
import json,hashlib,shutil,difflib,zipfile
ROOT=Path(__file__).resolve().parent.parent;PRIVATE=ROOT/'storage/app/private/investmoz-v2/portal-audit';OUT=ROOT/'outputs/investmoz-country02-https-20261011'
read=lambda p:json.loads(Path(p).read_text(encoding='utf-8-sig'));sha=lambda p:hashlib.sha256(Path(p).read_bytes()).hexdigest()
meta=read(PRIVATE/'country-https-latest.json');source=Path(meta['source']);stage=read(PRIVATE/'country-https-production-stage-latest.json')
link=read(PRIVATE/'country-https-backup-latest.json');backup=read(link['path']);browser=read(PRIVATE/'country-https-chromium-20261011.json');sql=read(PRIVATE/'country-https-persistence-20261011.json')
old=read(ROOT/'outputs/investmoz-country02-c110-20261011/manifesto-candidata.json');original=read(ROOT/'outputs/investmoz-country02-candidate-20261011/manifesto-candidata.json')
assert browser['result']==sql['result']=='passed' and browser['code_hash']==sql['code_hash']==backup['code_hash']
assert all(backup[k] for k in ['restore_verified','files_verified','sequences_verified','compatibility_verified','reversal_verified','qa_passed'])
assert backup['production_manifest_sha256']==browser['build_sha256']==stage['build_manifest_sha256']
assert sha(backup['dump_path'])==backup['dump_sha256'] and sha(backup['files_path'])==backup['files_sha256']
baseline=read(PRIVATE/'country-c110-source-latest.json')
for file,digest in baseline['baseline_live_sources'].items():assert sha(ROOT/file)==digest
for file,digest in stage['active_assets'].items():assert sha(ROOT/'public/build'/file)==digest
for file,digest in stage['inputs'].items():assert sha(source/file)==digest
for file,digest in stage['assets'].items():assert sha(Path(stage['build_directory'])/file)==digest
sources={file:sha(source/file) for file in original['files']};changed=[f for f,d in sources.items() if sha(ROOT/f)!=d]
assert changed==['app/Providers/AppServiceProvider.php','app/Http/Controllers/OpportunityResourceController.php','resources/js/pages/management/opportunity-install.tsx']
deployment_files=['portal-investidor.test-https.proposta.conf','ambiente-https.proposta.txt','plano-https-ativo.json','certificado-ativo-proposto.json','certificados-publicos/portal-investidor.test-ca.cer','certificados-publicos/portal-investidor.test-fullchain.pem']
deploy={f:sha(OUT/f) for f in deployment_files}
revision_input={'code_hash':backup['code_hash'],'production_manifest':stage['build_manifest_sha256'],'input_hash':stage['input_hash'],'assets':stage['assets'],'deployment_files':deploy}
revision=hashlib.sha256(json.dumps(revision_input,sort_keys=True,separators=(',',':')).encode()).hexdigest();code=backup['code_hash']
receipts={'chromium-https.json':PRIVATE/'country-https-chromium-20261011.json','persistencia-qa.json':PRIVATE/'country-https-persistence-20261011.json','perfil-https.png':PRIVATE/'country-https-profile-20261011.png','validacao-qa.json':PRIVATE/'country-https-quality-20261011.json'}
for name,file in receipts.items():shutil.copyfile(file,OUT/name)
patch=''
for file in changed:
patch+=''.join(difflib.unified_diff((ROOT/file).read_text(encoding='utf-8').splitlines(True),(source/file).read_text(encoding='utf-8').splitlines(True),fromfile='a/'+file,tofile='b/'+file))
dest=OUT/'codigo'/file;dest.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(source/file,dest)
(OUT/'codigo-corrigido.patch').write_text(patch,encoding='utf-8')
backup_public={k:backup[k] for k in ['verified_at','source_code_hash','code_hash','target','restored_target','dump_sha256','files_sha256','restore_verified','files_verified','sequences_verified','compatibility_verified','reversal_verified','original_mfa_envelopes_recovered','file_count','production_manifest_sha256']}
backup_public.update({'tables':len(backup['tables']),'sequences':len(backup['sequences']),'missing_files':0,'all_row_sequence_file_comparisons_before_fixtures':True,'original_business_rows_preserved':sql['original_business_rows_preserved'],'active_writes':0})
(OUT/'backup-restauro.json').write_text(json.dumps(backup_public,indent=2),encoding='utf-8')
qa_cert={k:meta[k] for k in ['base','ca_thumbprint','ca_sha256','leaf_sha256','san','valid_until','ca_valid_until','trust_store']}
qa_cert.update({'user_confirmed_windows_trust':True,'chromium_accepted_without_bypass':True,'tls':'TLS 1.3','temporary_server_stopped_after_tests':True,'ca_signing_key_not_persisted':True,'active_certificate_trust_changed':False})
(OUT/'certificado-qa.json').write_text(json.dumps(qa_cert,indent=2),encoding='utf-8')
active_cert=read(OUT/'certificado-ativo-proposto.json')
manifest={'candidate_id':'CAND-COUNTRY02-HTTPS-20261011','at':datetime.now(timezone.utc).isoformat(),'code_hash':code,'delivery_revision':revision,
'hash_algorithm':'SHA256 sorted compact UTF8 JSON {code_hash,production_manifest,input_hash,assets,deployment_files}','revision_input':revision_input,'files':sources,'changed_source_files':changed,
'production':{'manifest_sha256':stage['build_manifest_sha256'],'input_hash':stage['input_hash'],'files':stage['assets'],'base':'/build/','inputs':stage['inputs']},
'deployment_files_sha256':deploy,'qa_origin':meta['base'],'qa_secure_context':browser['runtime'],'qa_cert':qa_cert,'qa_criteria':browser['checks'],'backup':backup_public,
'case_status':{'C110':'QA demonstrated; server key preserved','C111':'QA demonstrated on trusted HTTPS; encryption retained','C112':'QA demonstrated; LogoutResponse rotates encryption key after server session invalidation'},
'ready_for_review':True,'active_activation_performed':False,'active_configuration_changed':False,'active_writes':0,'original_live_code':backup['source_code_hash'],'original_active_manifest':baseline.get('active_manifest_sha256',original['active_manifest_sha256']),
'authorization_scope':['Exact reviewed three source changes and production assets, with COUNTRY02 flag initially false','HTTPS https://portal-investidor.test using proposed separate local certificate, user trust handoff, Apache vhost and public .env/cache values','Authenticated actual manager/MFA POST only050000; two nullable columns, zero new tables/accounts/organizations/representations','Enable only COUNTRY02; logout/history cleanup included; verify and reverse without dropping data'],
'active_preflight_required':['Explicit replacement revision + HTTPS authorization','Exact selected certificate SAN/trust/expiry/key/config hashes confirmed on active browser before authenticated action','Source/build/target/preview match; backup still <=24h and files/keys/restored rows match','Stop before POST on any divergence; no prerequisite030000/040000/190000'],
'excluded':['030000','040000','190000','cartography import/publication','profile grants/bootstrap/real holder designation','offers and SLA','other25UUID fixes','A/B and C37 repetition'],
'previous_revisions':{'original':{'code':original['code_hash'],'delivery':original['delivery_revision']},'C110':{'code':old['code_hash'],'delivery':old['delivery_revision']}},
'no_private_keys_or_accounts_in_bundle':True,'active_certificate':active_cert}
(OUT/'manifesto-candidata.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
report=f'''# COUNTRY02 com HTTPS — candidata para autorização
Entrega **`{revision}`**, código **`{code}`**, compilação **`{stage['build_manifest_sha256']}`**. Fontes e produção recompiladas na cópia isolada. O manifesto de produção mantém os mesmos bytes porque a alteração adicional é PHP: resposta de logout. O hash dos inputs e o código mudaram, e a entrega inclui agora as propostas HTTPS e o certificado público correspondente. Versões anteriores preservadas.
## O que ficou demonstrado em QA
Chromium em **{meta['base']}**, certificado reconhecido pelo Windows após confirmação manual do responsável, sem `ignoreHTTPSErrors`, sem SPKI bypass nem origem insegura marcada como segura. SAN apenas country02-qa.test. TLS1.3, `isSecureContext=true`, `crypto.subtle` disponível. A resolução do hostname usa apenas a regra DNS do Chromium de teste; não se alterou hosts ou DNS ativo.
Percurso: HTTP308 paraHTTPS preservando caminho → login → MFA normal → prévia correspondente → confirmação → POST050000 → resultado instalado → perfil privado → paísPT guardado → F5 recupera → atualizaçãoMZ/versão3 → voltar/avançar com histórico cifrado → logout/API401 → voltar apresenta formulário de login sem dados privados → nova sessão/MFA recuperaMZ e referência. Ao voltar depois de logout, o Inertia pode conservar `/investidor/profile` como destino enquanto apresenta login; não é uma sessão autenticada nem um ciclo. Foram conferidos **47 recursos** (46 de produção e favicon original), sem conteúdo misto nem erros JavaScript. A resposta401 é a recusa deliberada do teste de acesso após logout. Cookies: nome próprioQA, domínio só country02-qa.test, caminho/, Secure, SameSiteLax e HttpOnly na sessão. SHA256 do ID real de sessão comprova regeneração no MFA e logout; valores de cookies e códigos não estão nos recibos.
**C110:** chave emitida pelo servidor, vinculada a gestor/alvo/migração/revisão, estável nos erros/repetições. Provas HTTP anteriores de erro409 e replay preservadas, sem necessidade de reexecutarA/B ouC37. **C111:** transporteHTTPS resolve a indisponibilidade deWebCrypto e mantém `Inertia::encryptHistory()`/`private,no-store`. **C112 encontrado neste ensaio:** antes da correção a API recusava acesso após logout, mas o histórico ainda recuperava o perfil. A respostaFortify limpa o histórico depois de invalidar a sessão, conforme [Inertia](https://inertiajs.com/docs/v2/security/history-encryption). A correção está demonstrada pela interface e API. Nenhum middleware de cifragem foi desabilitado.
SQL READ ONLY no clone confirma uma instalação/auditoria, duas colunas opcionais, zero tabelas e duas alterações do país com snapshots cifrados recuperáveis null→PT→MZ. A organização, representação e conta utilizadas foram criadas explicitamente como fixturesQA depois da instalação, nunca por050000 nem no ativo. Nenhum030000/040000/190000. Valores originais preservados. Desabilitar/reabilitarCOUNTRY02 naQA esconde o consumidor e recupera exatamente país, versão e proveniência, semDDLouperda.
Os ensaios incompletos ficam preservados privadamente. Uma falha do próprio ensaio (`APIRequestContext` sem a resoluçãoDNS do Chromium) foi corrigida para usar o pedido do navegador. O diagnóstico que incluiu cabeçalhos foi sanitizado e a sessãoQA invalidada. Um404 de favicon provinha da cópiaQA sem esse ficheiro público; foi copiado o original sem alteração. Estes casos não foram ocultados como testes funcionais aprovados. Os critérios funcionais já demonstrados foram reutilizados na mesma base/revisão e somente as verificações afetadas foram repetidas.
## Backup/restauro correspondente
**{backup['verified_at']}**. Origem investemoz:5432 emREADONLY, restauro {backup['restored_target']['database']}:55441.288tabelas/31sequências/282ficheiros, zero ausentes, comparados antes de fixtures, dois envelopesMFA originais recuperados com a chave estável. Fonte efetivamente ativa36f7722ce5a5 e código preparado{code[:12]} estão distinguidos.282itens incluem ativos imutáveis conservados na reversão anterior;295tabelas era outroQA com sete tabelas de030/040. Dumps e ficheiros privados permanecem privados. A validade operacional é **máximo24h**, com revalidação imediatamente antes da instalação. [Prova sanitizada](backup-restauro.json).
## HTTPS equivalente para o ativo, preparado mas não instalado
URL **https://portal-investidor.test**. Preparados certificado e chave privados separados daQA: SAN portal-investidor.test, folha válida até **{active_cert['leaf_valid_until']}**, root `{active_cert['root_thumbprint']}`. A chave corresponde ao certificado, está fora doZIP e limitada ao utilizador atual/SYSTEM/Administradores. A CA de assinatura não foi persistida. A confiança dessaCA ativa ainda NÃO foi instalada. A distribuição de confiança aplica-se às máquinas que usam este hostname local; não se presume uma certificação pública ou acesso externo. Se for escolhido outro hostname/emissor, selar uma nova proposta antes de aplicar.
[Vhost proposto](portal-investidor.test-https.proposta.conf), [valores públicos propostos](ambiente-https.proposta.txt), [certificado público](certificado-ativo-proposto.json), [plano estruturado](plano-https-ativo.json). `httpd -t` aprovou a proposta usando uma cópia privada da configuração atual e os caminhos privados preparados. Nenhum ficheiro doApache, .env, cache, bundle ou dado ativo foi alterado. A prova de sintaxe não substitui a provaTLS do endereço ativo depois da autorização.
## Âmbito exato para autorização e sequência
1. Autorizar esta entrega/código e HTTPS com o certificado/configuração identificados. Revalidar hashes ativos, validade dobackup, cadeia/SAN/validade/permissões da chave e parâmetros do alvo. Discrepância interrompe antes da aplicação.
2. Disponibilizar três fontes corrigidas e157ativos de produção com COUNTRY02false: controlador/chave, formulário semUUIDbrowser e respostaFortify de limpeza do histórico. Preservar fontes/manifesto/ativos anteriores.
3. Copiar somente o certificado e chave preparados para o diretório deTLS previsto. Confirmar manualmente a confiança no Windows das máquinas autorizadas, configurar o vhostSSL e os valoresAPP_URL/cookieSecure/host-only/path/SameSite. PreservarAPP_KEY, base, guard e demaisflags. ValidarApache e recarregar. HTTPGET/HEAD308 conserva caminho/consulta; HTTPPOST não aceita credenciais. HSTS diferido para manter reversão possível.
4. HTTPS válido/contexto seguro/sem conteúdo misto na origem ativa antes do login. Gestor real já autorizado, email verificado eMFA normal. GET **https://portal-investidor.test/management/opportunity-resources-install/organization-country**. Conferir investemoz5432, código{code[:12]}, hash da prévia, dependências e efeito de somente050000. Não instalar030/040/190.
5. POST na mesmaURL com confirmação, motivo e chave emitida pelo servidor; manter corpo/chave nas repetições. Conferir uma migration/recibo e apenas duas colunasnullable, zero tabelas/contas/org/representações. Habilitar somenteCOUNTRY02 e verificar perfil, gravação autorizada, recuperação, histórico e logout. Ativo sem organização/vínculo mostra vazio controlado; a gravação real depende de cadastro/representação legítimos pelo fluxo autorizado. Não inventar entidades ou titulares para o ensaio.
Reversão: desabilitarCOUNTRY02 primeiro; manter colunas/valores/proveniência/ledger/auditoria. Restaurar fontes e manifesto anterior por troca atómica, preservando ativos. Preferir contenção comHTTPS mantido e flagfalse. Reversão completa deHTTPS repõe os ficheirosApache/.env/cache específicos guardados, valida/recarrega e muda o namespace do cookie; essa reversão também retira a entregaHTTPS e reabreC111 para páginas privadas, não pode ser declarada funcional emHTTP. Restauro de dados só pelo backup correspondente e ação autorizada própria. Não fazerdown/drop, key:generate ou desativarMFA.
## Exclusões e estado
Código preparado eQA demonstrada. **Nada ativado.** C110/C111/C112 fechados neste âmbitoQA; a autorização anteriorc68/36 não foi transferida silenciosamente. A/B eC37 continuam encerrados nos seus âmbitos. Não inclui perfis, cartografia, percurso190000, ofertas, SLA, outros cadastros ou as25chamadasUUID do [backlog por consumidor](uuid-backlog.md). Estas permanecem abertas, com prioridade para simulador público/autoguardar/calcular e040000 ao confirmar; os restantes ramos dependem de módulos/flags ainda indisponíveis no ativo.
'''
# Keep the generated text legible without changing quoted source facts or identifiers.
for a,b in {'paraHTTPS':'para HTTPS','POST050000':'POST 050000','paísPT':'país PT','atualizaçãoMZ':'atualização MZ','recuperaMZ':'recupera MZ','QA,':'QA,','nome próprioQA':'nome próprio QA','caminho/, Secure':'caminho /, Secure','SameSiteLax':'SameSite=Lax','a sessãoQA':'a sessão QA','reexecutarA/B':'reexecutar A/B','ouC37':'ou C37','transporteHTTPS':'transporte HTTPS','deWebCrypto':'de WebCrypto','respostaFortify':'resposta Fortify','fixturesQA':'fixtures QA','por050000':'por 050000','Nenhum030000/040000/190000':'Nenhum 030000/040000/190000','reabilitarCOUNTRY02':'reabilitar COUNTRY02','naQA':'na QA','semDDLouperda':'sem DDL ou perda','resoluçãoDNS':'resolução DNS','cópiaQA':'cópia QA','emREADONLY':'em READ ONLY','288tabelas/31sequências/282ficheiros':'288 tabelas/31 sequências/282 ficheiros','envelopesMFA':'envelopes MFA','ativa36f':'ativa 36f','282itens':'282 itens','outroQA':'outro QA','de030/040':'de 030/040','máximo24h':'máximo 24h','daQA':'da QA','doZIP':'do ZIP','dessaCA':'dessa CA','doApache':'do Apache','provaTLS':'prova TLS','dobackup':'do backup','e157ativos':'e 157 ativos','COUNTRY02false':'COUNTRY02=false','semUUIDbrowser':'sem UUID do navegador','deTLS':'de TLS','vhostSSL':'vhost SSL','valoresAPP_URL':'valores APP_URL','verificarAPP_KEY':'verificar APP_KEY','PreservarAPP_KEY':'Preservar APP_KEY','demaisflags':'demais flags','ValidarApache':'Validar Apache','HTTPGET/HEAD308':'HTTP GET/HEAD 308','HTTPPOST':'HTTP POST','eMFA':'e MFA','somente050000':'somente 050000','instalar030/040/190':'instalar 030/040/190','mesmaURL':'mesma URL','colunasnullable':'colunas nullable','somenteCOUNTRY02':'somente COUNTRY02','desabilitarCOUNTRY02':'desabilitar COUNTRY02','comHTTPS':'com HTTPS','flagfalse':'flag=false','deHTTPS':'de HTTPS','ficheirosApache':'ficheiros Apache','entregaHTTPS':'entrega HTTPS','reabreC111':'reabre C111','emHTTP':'em HTTP','fazerdown/drop':'fazer down/drop','desativarMFA':'desativar MFA','âmbitoQA':'âmbito QA','anteriorc68/36':'anterior c68/36','eC37':'e C37','percurso190000':'percurso 190000','as25chamadasUUID':'as 25 chamadas UUID','e040000':'e 040000','login. Gestor':'login. Gestor'}.items():report=report.replace(a,b)
(OUT/'REVER-CANDIDATA.md').write_text(report,encoding='utf-8')
# Public deliverables only. No private certificate key, runtime, seed or credentials.
members={}
for file in sources:members['codigo/'+file]=source/file
for name in ['tests/Feature/CountryHistoryLogoutTest.php','tests/Feature/OrganizationCountryInstallationTest.php']:members[name]=source/name
for file in stage['assets']:members['public/build/'+file]=Path(stage['build_directory'])/file
for name in list(receipts)+deployment_files+['manifesto-candidata.json','REVER-CANDIDATA.md','codigo-corrigido.patch','backup-restauro.json','certificado-qa.json','apache-syntax.json','uuid-backlog.json','uuid-backlog.md','configuracao-ativa-leitura.json']:members[name]=OUT/name
assert not any('key.pem' in name or any(p in ['.env','vendor','node_modules'] for p in Path(name).parts) for name in members)
archive=OUT/('country02-https-'+revision[:12]+'-revisao.zip')
with zipfile.ZipFile(archive,'w',zipfile.ZIP_DEFLATED) as z:
for name,file in members.items():z.write(file,name)
envelope={'archive':archive.name,'archive_sha256':sha(archive),'archive_entries':len(members),'manifesto_candidata_sha256':sha(OUT/'manifesto-candidata.json'),'verified_members_sha256':{name:sha(file) for name,file in members.items()},'private_keys_excluded':True}
with zipfile.ZipFile(archive) as z:
for name,digest in envelope['verified_members_sha256'].items():assert hashlib.sha256(z.read(name)).hexdigest()==digest
(OUT/'manifesto-pacote.json').write_text(json.dumps(envelope,indent=2),encoding='utf-8')
print(json.dumps({'result':'review package sealed','code':code,'revision':revision,'zip':archive.name,'zip_sha256':envelope['archive_sha256'],'members':len(members),'active_changes':0}))