117 lines
17 KiB
Python
117 lines
17 KiB
Python
"""Package only reviewed source/assets/receipts; private configuration and archives stay private."""
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone
|
|
import hashlib, json, difflib, zipfile, shutil
|
|
|
|
ROOT=Path(__file__).resolve().parent.parent
|
|
PRIVATE=ROOT/'storage/app/private/investmoz-v2/portal-audit'
|
|
plan=json.loads((PRIVATE/'country-c110-source-latest.json').read_text());SOURCE=Path(plan['source']);OUT=Path(plan['output'])
|
|
read=lambda p: json.loads(Path(p).read_text(encoding='utf-8-sig'))
|
|
sha=lambda p: hashlib.sha256(Path(p).read_bytes()).hexdigest()
|
|
stage=read(PRIVATE/'country-c110-production-stage-latest.json')
|
|
browser=read(PRIVATE/'country-c110-chromium-20261011.json')
|
|
persistence=read(PRIVATE/'country-c110-persistence-20261011.json')
|
|
link=read(PRIVATE/'country-c110-backup-latest.json');backup=read(link['path'])
|
|
old=read(ROOT/'outputs/investmoz-country02-candidate-20261011/manifesto-candidata.json')
|
|
assert browser['result']==persistence['result']=='passed'
|
|
assert browser['code_hash']==persistence['code_hash']==backup['code_hash']
|
|
assert stage['build_manifest_sha256']==backup['production_manifest_sha256']==browser['build_sha256']
|
|
assert all(backup[k] for k in ['restore_verified','files_verified','sequences_verified','compatibility_verified','reversal_verified'])
|
|
assert backup['original_mfa_envelopes_recovered']==2
|
|
assert sha(backup['dump_path'])==backup['dump_sha256'] and sha(backup['files_path'])==backup['files_sha256']
|
|
sources={file:sha(SOURCE/file) for file in old['files']}
|
|
changed=[file for file,digest in sources.items() if digest!=sha(ROOT/file)]
|
|
assert changed==['app/Http/Controllers/OpportunityResourceController.php','resources/js/pages/management/opportunity-install.tsx']
|
|
for file,digest in stage['inputs'].items():assert sha(SOURCE/file)==digest,file
|
|
for file,digest in stage['assets'].items():assert sha(Path(stage['build_directory'])/file)==digest,file
|
|
assert sha(ROOT/'public/build/manifest.json')==old['active_manifest_sha256']
|
|
assert all(sha(ROOT/file)==digest for file,digest in plan['baseline_live_sources'].items()),'Live source was changed'
|
|
code=backup['code_hash']
|
|
# Explicit canonical format for the new delivery; not confused with the old PHP-JSON fingerprint.
|
|
revision_input={'code_hash':code,'production_manifest':stage['build_manifest_sha256'],'input_hash':stage['input_hash'],'assets':stage['assets']}
|
|
revision=hashlib.sha256(json.dumps(revision_input,sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
receipts={'chromium-http.json':PRIVATE/'country-c110-chromium-20261011.json','persistencia-qa.json':PRIVATE/'country-c110-persistence-20261011.json','instalador-http-390.png':PRIVATE/'country-c110-install-http-390.png'}
|
|
for name,file in receipts.items():shutil.copyfile(file,OUT/name)
|
|
def quality_read(file):
|
|
raw=file.read_bytes()
|
|
text=raw.decode('utf-16') if raw.startswith(b'\xff\xfe') else raw.decode('utf-8-sig')
|
|
return json.loads(text.strip())
|
|
unit=quality_read(PRIVATE/'country-c110-regression.json')
|
|
stan=quality_read(PRIVATE/'country-c110-phpstan.json')
|
|
assert unit['result']==stan['result']=='passed' and unit['tests']==7 and unit['assertions']==102
|
|
validation={'unit':unit,'static_analysis':stan,'php_syntax':'passed on prepared OpportunityResourceController.php','production_typescript_exit':0,'typescript_provenance':'tsc --noEmit on production workspace completed with exit0 in tool command','qa_unit_database':'investmoz_test:55441, guarded TestCase before RefreshDatabase','active_writes':0}
|
|
(OUT/'validacao-qa.json').write_text(json.dumps(validation,indent=2),encoding='utf-8')
|
|
patch=''
|
|
for file in changed:
|
|
dest=OUT/'codigo'/file;dest.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(SOURCE/file,dest)
|
|
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))
|
|
(OUT/'codigo-c110.patch').write_text(patch,encoding='utf-8')
|
|
backup_summary={'at':backup['verified_at'],'target':backup['target'],'restored_target':backup['restored_target'],'prepared_code_hash':code,'actual_source_code_hash':backup['source_code_hash'],'production_manifest_sha256':backup['production_manifest_sha256'],'source_tables':len(backup['tables']),'source_sequences':len(backup['sequences']),'files_and_runtime_assets':backup['file_count'],'missing_files':len(backup['missing_files']),'dump_sha256':backup['dump_sha256'],'archive_sha256':backup['files_sha256'],'restore_verified':True,'files_verified':True,'compatibility_verified':True,'disable_preserves_data':True,'original_mfa_envelopes_recovered':2,'active_writes':0,'note':'282 files include inactive immutable assets preserved after prior reversal, versus154 in previous snapshot. 288 tables are the same active target;295 was QA with030000/040000 extra tables. All row/sequence/file comparisons happened before QA fixtures.'}
|
|
(OUT/'backup-restauro.json').write_text(json.dumps(backup_summary,indent=2),encoding='utf-8')
|
|
manifest={'candidate_id':'CAND-COUNTRY02-C110-20261011','at':datetime.now(timezone.utc).isoformat(),'code_hash':code,'delivery_revision':revision,'delivery_hash_algorithm':'sha256 of sorted compact UTF8 JSON {code_hash,production_manifest,input_hash,assets}','previous_delivery':old['delivery_revision'],'previous_code':old['code_hash'],'files':sources,'source_prepared_in':str(SOURCE),'live_source_unchanged':True,'changed_source_files':changed,'production':{'manifest_sha256':stage['build_manifest_sha256'],'input_hash':stage['input_hash'],'directory':stage['build_directory'],'base':'/build/','files':stage['assets'],'transitive_inputs':stage['inputs']},'scope':'COUNTRY02/050000 only; two optional columns, no tables/accounts/org/representations','key':'Server Str::uuid; session scope binds actor,target,migration,code. Same key across errors/reloads/retries; altered completed payload409. No browser RNG used by included consumers.','qa_demonstrated':browser['checks'],'qa_http_runtime':browser['runtime'],'backup':backup_summary,'browser_receipts':{name:sha(OUT/name) for name in receipts},'uuid_inventory_sha256':sha(OUT/'inventario-uuid.json'),'C110':'corrected and demonstrated in QA','C111':browser['separate_private_consumer_failure'],'activation_ready':False,'activation_stop_conditions':['C111: private history encryption cannot use crypto.subtle on the actual-equivalent HTTP hostname; preserve protection, resolve transport/compatible secure behavior before activation.','Existing authorization named old code36f7722ce5a5/deliveryc68ceef9d4a3. Do not apply a different exact revision under that authorization.','Actual authenticated manager preview/apply not done.'],'active_installed':False,'active_country_enabled':False,'active_migration_applied':False,'active_writes':0,'preserved_acceptance':['A/B QA','C37 QA18'],'excluded':['030000','040000','190000','cartography','profiles','offers','SLA','Other25UUID occurrences outside COUNTRY02']}
|
|
(OUT/'manifesto-candidata.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
|
|
report=f'''# COUNTRY02 — C110 corrigido em QA
|
|
|
|
Código `{code}`. Entrega `{revision}`. Manifesto de produção `{stage['build_manifest_sha256']}`. A versão anterior `c68ceef9d4a3` / `36f7722ce5a5` permanece preservada.
|
|
|
|
## Correção e isolamento
|
|
|
|
O servidor emite `Str::uuid()` e guarda a operação na sessão, associada ao gestor, alvo PostgreSQL, migração e fingerprint do código. A prévia entrega a chave ao formulário. O POST exige a mesma chave emitida para esse contexto. A chave permanece após erro, F5 e repetição; logout ou mudança de contexto inicia outra operação. O recibo existente mantém hash do pedido e impede reutilizar uma chave concluída com conteúdo diferente. Preservados autorização, MFA recente, CSRF, confirmação, motivo e advisory lock. Não se usa `Math.random()`, timestamp ou UUID do navegador.
|
|
|
|
Alterações de aplicação: somente controlador `OpportunityResourceController.php` e componente partilhado `management/opportunity-install.tsx`, preparados numa cópia isolada. O Apache continua a servir os ficheiros anteriores. Código, configuração, manifesto e dados ativos não foram alterados. [Patch para revisão](codigo-c110.patch). A cópia de QA usa o mesmo código atual, com App/Tests vinculados explicitamente ao autoloader isolado.
|
|
|
|
## Provas por critério
|
|
|
|
Chromium em **http://country02-qa.test:18103**, resolvido internamente para127.0.0.1, sem sinalizar a origem como segura. `isSecureContext=false`, `randomUUID=undefined`, `subtle=undefined`. Alvo `country_candidate_20261011_071801_test:55441`, cabeçalhos e identidade SQL conferidos.
|
|
|
|
- MFA normal de gestor sintético; nenhuma intervenção ou repetição de QA18/A/B.
|
|
- Prévia visível, hash, codehash, alvo, dependências e readiness correspondentes. Confirmação obrigatória e UUIDv4 do servidor.
|
|
- F5 conserva a chave. Expiração exclusiva da evidência QA entre prévia e POST provoca409 antes da instalação. Após reposição da evidência e nova prévia, conserva a chave e permite recuperar a tentativa.
|
|
- POST pelo formulário cria apenas050000 e duas colunas opcionais. Resultado visível instalado. Os dois pedidos da tentativa usam a mesma chave e corpo.
|
|
- Duas repetições devolvem o recibo original `changed=true,new_columns=2`, sem executar novamente a alteração. Conteúdo diferente com a mesma chave concluída recebe409.
|
|
- SQL READ ONLY liga a chave da interface a **um** evento de auditoria e confirma288tabelas, zero organizações/vínculos automáticos, originais preservados e nenhum030000/040000/190000 instalado.
|
|
- Consumidores partilhados020000/030000 abrem porGET no mesmo hostname, sem aplicação nem chamadaUUID do navegador. Perfil do país mostra vazio controlado. Instalador a390px sem extravasamento.38ativos carregados conferidos com o bundle; zero errosJS do instalador.
|
|
- Testes dirigidos:7métodos/102asserções; PHPStan do controlador zero erros; sintaxePHP e TypeScript do workspace de produção aprovados. A contagem não substitui os critérios de navegador acima.
|
|
|
|
[Chromium](chromium-http.json), [persistência](persistencia-qa.json), [captura390px](instalador-http-390.png), [inventário de chamadas](inventario-uuid.json).
|
|
|
|
## Correspondências de UUID e nova condição C111
|
|
|
|
Pesquisa nos30ficheiros de dependências dos três consumidores incluídos: uma chamada anterior, removida; zero chamadas remanescentes. Encontradas25ocorrências fora dos consumidoresCOUNTRY02 em oportunidades/recursos, perfis, cartografia, simuladores, Balcão, parcerias e percurso; inventariadas sem implementação ou ativação por arrasto. Não se considera esses percursos compatíveis comHTTP por esta prova.
|
|
|
|
**C111 permanece aberta:** `/investidor/profile` apresenta `Unable to encrypt history` no hostname HTTP. `PrivateInvestmentResponse` mantém `Inertia::encryptHistory()` e `private,no-store`; o cliente não dispõe de `crypto.subtle`. A falha foi observada em separado, não ocultada ou incluída como sucesso geral. [A documentação oficial do Inertia confirma o requisito de contexto seguro para o histórico cifrado](https://inertiajs.com/docs/v2/security/history-encryption).
|
|
|
|
Decisão/ação concreta proposta: manter a cifragem e preparar acesso HTTPS com certificado confiável para as páginas privadas, com ensaio de histórico/voltar/logout; qualquer disponibilização no ativo exige configuração e autorização correspondentes. SeHTTP for obrigatório, definir e demonstrar um mecanismo equivalente que não retenha dados privados no histórico. Não desabilitar a cifragem nem marcar uma origem insegura como segura para fazer o teste passar. Nenhuma alteração de TLS, trust store ou middleware foi feita.
|
|
|
|
## Backup/restauro e pacote
|
|
|
|
Backup de **{backup['verified_at']}**, origem ativa `investemoz:5432` em READ ONLY. Restauro isolado `country_candidate_20261011_071801_test:55441`:288tabelas,31sequências,282ficheiros, zero ausentes, dois envelopes MFA originais recuperados. O aumento de154para282ficheiros corresponde aos ativos conservados após a reversão; não a novas tabelas.295era outro ambiente QA com sete tabelas de030000/040000. Dados e sequências comparados antes de fixtures. Dumps, arquivos, extração, chave estável e hashes verificados. Compatibilidade e desabilitação preservando valores demonstradas no clone; zero escritas no ativo. [Prova sanitizada](backup-restauro.json).
|
|
|
|
A prova distingue código ativo anterior36f7722ce5a5 e código preparado54fb610eb9da. O bundle novo tem157ficheiros em`/build/`, com fontes e hashes fixados. Pacote sem.env, chaves, credenciais, dados privados, dumps, vendor ou node_modules.
|
|
|
|
**Estado:** C110 corrigido e demonstrado em QA. COUNTRY02 não ativo; candidata não pronta para ativação enquanto C111 estiver aberta. A autorização registada nomeava hashes anteriores e não foi usada para aplicar este pacote substituto. A/B e C37 continuam encerrados nos seus âmbitos.
|
|
|
|
Sequência após resolver as condições de paragem e autorizar a revisão correspondente: revalidar fonte/build/alvo/backup<=24h; disponibilizar código revisto e ativos preservando os anteriores, com flagfalse; obter prévia pelo gestor real verificado comMFArecente no servidor definido; conferir target/hash/code/efeitos; POST somente050000 com a chave emitida e mesmo corpo nas repetições; conferir2colunas/0tabelas/contas/org/vínculos/auditoria única; habilitar somenteCOUNTRY02 e demonstrar consumidor privado/histórico/permissões. Qualquer divergência interrompe antes doPOST.
|
|
|
|
Reversão: somenteflagfalse e manifesto anterior75352185c2a85 por troca atómica; preservar ativos,050000, valores, fontes, snapshots, ledger e auditoria. Não fazerdown/drop, gerar chave ou desativarMFA. Recuperação de dados só com restauro correspondente e autorização própria.
|
|
'''
|
|
replacements={'para127.0.0.1':'para 127.0.0.1','provoca409':'provoca 409','apenas050000':'apenas 050000','recebe409':'recebe 409','confirma288tabelas':'confirma 288 tabelas','nenhum030000/040000/190000':'nenhum 030000/040000/190000','partilhados020000/030000':'partilhados 020000/030000','porGET':'por GET','a390px':'a 390 px','38ativos':'38 ativos','7métodos/102asserções':'7 métodos e 102 asserções','nos30ficheiros':'nos 30 ficheiros','consumidoresCOUNTRY02':'consumidores COUNTRY02','Encontradas25ocorrências':'Encontradas 25 ocorrências','288tabelas,31sequências,282ficheiros':'288 tabelas, 31 sequências e 282 ficheiros','de154para282ficheiros':'de 154 para 282 ficheiros','295era':'295 era','de030000/040000':'de 030000/040000','tem157ficheiros em`/build/`':'tem 157 ficheiros em `/build/`','sem.env':'sem .env','backup<=24h':'backup com idade máxima de 24 horas','flagfalse':'flag desabilitada','comMFArecente':'com MFA recente','POST somente050000':'POST somente 050000','conferir2colunas/0tabelas/contas/org/vínculos/auditoria única':'conferir duas colunas, zero tabelas, contas, organizações ou vínculos novos e auditoria única','somenteCOUNTRY02':'somente COUNTRY02','somenteflagfalse':'somente a flag desabilitada','anterior75352185c2a85':'anterior 75352185c2a85','ativos,050000':'ativos, 050000','fazerdown/drop':'fazer down/drop','desativarMFA':'desativar MFA'}
|
|
for a,b in replacements.items():report=report.replace(a,b)
|
|
(OUT/'REVER-CANDIDATA.md').write_text(report,encoding='utf-8')
|
|
archive=OUT/f'country02-c110-{revision[:12]}-revisao.zip'
|
|
members={}
|
|
def add(z,name,file):
|
|
assert name not in members
|
|
members[name]=sha(file);z.write(file,name)
|
|
with zipfile.ZipFile(archive,'w',compression=zipfile.ZIP_DEFLATED) as z:
|
|
for name in sources:add(z,name,SOURCE/name)
|
|
for name in stage['assets']:add(z,'public/build/'+name,Path(stage['build_directory'])/name)
|
|
for name in ['manifesto-candidata.json','REVER-CANDIDATA.md','codigo-c110.patch','backup-restauro.json','chromium-http.json','persistencia-qa.json','instalador-http-390.png','inventario-uuid.json','validacao-qa.json']:add(z,name,OUT/name)
|
|
add(z,'tests/Feature/OrganizationCountryInstallationTest.php',SOURCE/'tests/Feature/OrganizationCountryInstallationTest.php')
|
|
with zipfile.ZipFile(archive) as z:
|
|
assert len(z.namelist())==len(members)
|
|
for name,digest in members.items():assert hashlib.sha256(z.read(name)).hexdigest()==digest,name
|
|
envelope={'candidate_id':manifest['candidate_id'],'code_hash':code,'delivery_revision':revision,'manifesto_candidata_sha256':sha(OUT/'manifesto-candidata.json'),'archive':archive.name,'archive_sha256':sha(archive),'archive_entries':len(members),'bytes':archive.stat().st_size,'verified_members_sha256':members,'activation_ready':False,'C110':'QA demonstrated','stop_condition':'C111 private HTTP history and exact-revision authorization'}
|
|
(OUT/'manifesto-pacote.json').write_text(json.dumps(envelope,indent=2),encoding='utf-8')
|
|
print(json.dumps({k:envelope[k] for k in ['code_hash','delivery_revision','archive','archive_sha256','archive_entries','activation_ready']}))
|