Files
Investidor-V2/tools/portal-audit-logout-results-prepare.py
T
nala 20971d523f
tests / ci (push) Canceled after 0s
oportunidades 6
2026-10-10 21:17:46 +02:00

155 lines
13 KiB
Python

"""Reconcile C85 and the confirmed QA18 logout into the existing audit only.
CSV cells are prepared as JSON and authored by the existing Artifact Tool writer.
"""
import collections
import csv
import hashlib
import json
import re
import time
from datetime import datetime, timezone
from pathlib import Path
csv.field_size_limit(16000000)
ROOT = Path(__file__).resolve().parent.parent
DOC = ROOT / 'docs/investmoz/v2/auditoria-portal'
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
OUT = PRIVATE / 'logout-finalize-20261010'
OUT.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
tag = '[C85/logout QA 10/10/2026] '
proof = json.loads((PRIVATE / 'portal-logout-chromium-20261010.json').read_text())
assert proof['result'] == 'passed' and all(proof['checks'].values())
assert proof['active_writes'] == 0 and proof['checks']['no_qa18_mfa_consumed']
assert len(proof['presentation']) == 6 and len(proof['logout']) == 2
# Retain only non-secret trace fields for the manually confirmed QA18 logout.
traces = []
for file in PRIVATE.glob('auth-session-trace-qa*.jsonl'):
for attempt in range(10):
try:
lines = file.read_text(encoding='utf-8').splitlines()
break
except PermissionError:
time.sleep(.1)
for line in lines:
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
if row.get('at_utc', '') < '2026-10-10T18:09:00' or row.get('path') not in ['/logout', '/login', '/two-factor-challenge', '/investimentos']:
continue
phases = {name: {key: (row.get(name) or {}).get(key) for key in ['authenticated_user', 'pending_mfa_user', 'stored_web_user']} for name in ['loaded', 'after', 'persisted']}
if not any(18 in state.values() for state in phases.values()):
continue
traces.append({key: row.get(key) for key in ['at_utc', 'method', 'path', 'status', 'redirect']} | phases)
traces.sort(key=lambda row: row['at_utc'])
logout = next(row for row in reversed(traces) if row['method'] == 'POST' and row['path'] == '/logout' and row['loaded']['authenticated_user'] == 18)
assert logout['status'] == 302 and logout['redirect']['path'] == '/'
assert logout['after']['authenticated_user'] is None and logout['persisted']['stored_web_user'] is None
post_logout_mfa = any(row['at_utc'] > logout['at_utc'] and row['path'] == '/two-factor-challenge' and row['method'] == 'POST' and row['after']['authenticated_user'] == 18 for row in traces)
manual = {'recorded_at': stamp, 'source': 'Responsável: «já tem o botão e fiz o logout»; rastreio sanitizado QA18097',
'target': proof['target'], 'actor_id': 18, 'button_and_logout_confirmed_by_user': True,
'logout_at_utc': logout['at_utc'], 'session_authenticated_after_logout': False,
'manual_post_logout_mfa_observed': post_logout_mfa, 'manual_f5_confirmed': False,
'events': traces, 'active_writes': 0, 'secrets_recorded': False}
(OUT / 'manual-qa18-logout.json').write_text(json.dumps(manual, ensure_ascii=False, indent=2), encoding='utf-8')
readiness_path = ROOT / 'storage/app/private/investmoz-v2/portal-profile-installation/readiness.json'
backup = json.loads(readiness_path.read_text())
checks = json.loads((PRIVATE / 'profile-installation-checks-qa.json').read_text())
assert backup['code_hash'] == checks['code_hash'] and backup['restore_verified'] and backup['files_restore_verified']
alias = PRIVATE / 'profile-installation-backup-qa.json'
if alias.exists():
previous = OUT / 'previous-profile-installation-backup-qa.json'
if not previous.exists():
previous.write_bytes(alias.read_bytes())
alias.write_bytes(readiness_path.read_bytes())
evidence = ('C85: ação ausente na navegação do percurso, apesar de existir no menu do dashboard. '
'Navegação comum passa a mostrar conta e Terminar sessão, usando POST /logout do Fortify e limpeza do cache Inertia. '
'Responsável confirmou botão e logout QA18; rastreio ' + logout['at_utc'] + ': auth18 → null, persistência null, 302 → /. '
'Chromium QA21 demonstrou logout direto do dossier desktop/móvel, API privada401, página privada→login, novo contexto com MFA normal e dossier inalterado; seis verificações de apresentação/teclado/reflow 320/390/1440px sem violações automáticas no cabeçalho. '
'Provas: portal-logout-chromium-20261010.json e logout-finalize-20261010/manual-qa18-logout.json. '
'MFA, chaves e QA18 preservados; bundle ativo sem alteração. Não substitui A/B ou outros perfis.')
gap = ('C37: logout manual QA18 demonstrado; F5 explícito e novo login/MFA QA18 após este logout ainda por confirmar. '
'Nova sessão Chromium QA21 demonstrada separadamente. Restantes consumidores, perfis, critérios A/B, móvel/acessibilidade integral e ativação não encerrados por este ensaio.')
ids = {'AUD-UI-01', 'AUD-GUIDE-01', 'AUD-DASH-01', 'AUD-PROFILE-24', 'AUD-PROFILE-25'}
files = []
for filename in ['matriz-requisitos.csv', 'backlog-priorizado.csv']:
source = DOC / filename
rows = list(csv.reader(source.open(encoding='utf-8-sig', newline='')))
headers = rows[0]
found = set()
patches = []
for index, row in enumerate(rows[1:], 1):
old = dict(zip(headers, row))
rid = old['ID']
if rid not in ids:
continue
found.add(rid)
if filename.startswith('matriz'):
values = {'Evidência funcional': evidence, 'Lacuna': gap,
'Interface': 'Terminar sessão visível no topo de investimentos/dossier/artigo/simulador; confirmação manual QA18 e Chromium QA21 desktop/móvel.',
'API': 'POST logout302→/; API privada401 e página privada→login após saída. Novo login+MFA21 normal numa sessão distinta.',
'Gravação': 'Sessão invalidada; documentos, dossier, resultados, etapas e tarefas guardados não modificados.',
'Recuperação': 'Dossier QA21 recuperado noutra sessão com o mesmo hash do snapshot de case/results/steps/tasks; F5 manteve acesso.',
'Resultado apresentado': 'Conta atual identificada, botão acessível por teclado; após logout consulta pública sem botão e acesso privado exige login.',
'Consolidação': 'Subcritérios C85 e logout manual C37 atualizados; estado global, fontes, versões e decisões preservados.'}
variants = json.loads(old['Critérios e versões por origem (JSON)'])
for variant in variants:
variant['incremento_logout_qa_20261010'] = {'atualizado_em': stamp, 'evidencia': evidence, 'lacuna_atual': gap, 'ativacao': False}
values = {field: old[field] + ' | ' + tag + value for field, value in values.items()}
values['Critérios e versões por origem (JSON)'] = json.dumps(variants, ensure_ascii=False, separators=(',', ':'))
else:
values = {'Pendência': gap, 'Correção/componentes': 'C85 corrigido no PortalGroupedMenu/UserMenuContent; Fortify e proteção de sessão reutilizados. Não contornar MFA nem permissões.',
'Aceitação': 'Logout manual18 demonstrado. Confirmar F5 e novo login/MFA18 após saída; provas QA21 independentes preservadas.'}
values = {field: old[field] + ' | ' + tag + value for field, value in values.items()}
patches.append({'row': index, 'values': values})
assert found == ids and len(rows) == 3394
files.append({'filename': filename, 'input_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), 'rows': rows,
'patches': patches, 'allowed_columns': sorted({field for patch in patches for field in patch['values']}), 'append': []})
(OUT / 'csv-edits.json').write_text(json.dumps({'files': files}, ensure_ascii=False), encoding='utf-8')
section = f'''## Logout disponível e confirmação manual — {stamp}
**C85 corrigido em QA; nenhuma alteração no ativo.** A ação existia no menu de utilizador do dashboard, mas não na navegação do investidor. O cabeçalho comum mostra agora a conta atual e **Terminar sessão**, sem obrigar a regressar ao dashboard. Reutiliza POST `/logout` do Fortify e limpa o cache Inertia; chaves, MFA, perfis e dados guardados foram preservados.
O responsável confirmou o botão e o logout. Rastreio QA18 às **{logout['at_utc']}** (20:17:15 em Moçambique): POST logout302→/, conta18→sem autenticação e estado persistido vazio. C37 recebe apenas esse critério; F5 explícito e novo login/MFA QA18 após a saída continuam pendentes. O GET/login anterior com conta autenticada não era um novo login. A declaração geral de testes efetuados não substitui evidências dos restantes critérios.
Chromium QA21, contextos isolados: ação visível em investimentos, dossier, artigo e simulador; logout direto do dossier em desktop e 320px; API privada401 e página privada→login; nova sessão com MFA normal recuperou case/results/steps/tasks com hash igual e F5 manteve acesso. Seis verificações de cabeçalho em 320/390/1440px sem violações automáticas ou overflow; foco por teclado demonstrado. Não consumiu códigos QA18. Não encerra A/B, leitor de ecrã ou acessibilidade integral.
Provas sanitizadas: `portal-logout-chromium-20261010.json` e `logout-finalize-20261010/manual-qa18-logout.json`. Cinco requisitos receberam subcritérios; permanecem **3393 requisitos/3393 linhas/6845 ocorrências**, com estados globais inalterados.
Regressão de instalador/publicação: 9 testes/93 asserções e TypeScript passaram. Backup/restauro QA renovado **{backup['verified_at']}**, 295 tabelas/103 ficheiros, em `{backup['restored_target']['database']}:55441`, associado ao novo fingerprint que inclui os dois menus. O backup ativo READ ONLY de 17:48:52 UTC permanece um retrato anterior; candidata global, compatibilidade/reversão no clone do ativo e ativação continuam pendentes. Preservar **030000 → 040000 → 09/190000**, sem repetir instalações QA nem copiar contas/dossiers de teste.
As afirmações históricas abaixo sobre logout manual ainda por testar são superadas somente por esta confirmação; nenhuma aceitação adicional inferida.
'''
def prepend(filename, key, content):
file = DOC / filename
old = file.read_text(encoding='utf-8-sig')
pattern = re.compile(r'<!-- ' + re.escape(key) + r':begin -->.*?<!-- ' + re.escape(key) + r':end -->\s*', re.S)
old = pattern.sub('', old)
file.write_text(f'<!-- {key}:begin -->\n{content}\n<!-- {key}:end -->\n\n' + old, encoding='utf-8')
prepend('relatorio-executivo.md', 'qa-logout-results-20261010', section)
prepend('backlog.md', 'qa-logout-results-20261010', section)
prepend('casos-reproducao.md', 'qa-logout-c85-20261010', '''## C85 — logout ausente no percurso do investidor
- **Reprodução:** autenticar QA18, abrir `/investimentos` ou um dossier; procurar logout no cabeçalho. Não havia botão; era preciso conhecer o menu do dashboard.
- **Esperado:** terminar sessão diretamente no percurso, em desktop/móvel e por teclado; dados guardados preservados e acesso privado negado após saída.
- **Causa:** PortalNavigation usa PortalGroupedMenu sem ação de sessão; o logout de UserMenuContent só era apresentado pelo AppLayout do dashboard.
- **Correção QA:** conta e botão Terminar sessão no cabeçalho comum; POST Fortify/CSRF existentes, limpeza do cache Inertia, sem alteração de guard, MFA ou privilégios.
- **Evidência:** responsável confirmou botão/logout QA18; trace18 POST/logout302→/ com auth e persistência vazias. Chromium21 desktop/móvel, API401, página privada→login, nova sessão MFA e snapshot do dossier iguais. Cabeçalho acessível por teclado, sem overflow ou violações automáticas nos seis ensaios.
- **Estado:** causa corrigida e logout manual demonstrado. C37 continua limitado pelos critérios F5 explícito e novo login/MFA18 após saída; provas21 não substituem conta18. Provas sanitizadas e 0 escritas ativas no relatório atual.
''')
prepend('plano-ativacao.md', 'qa-logout-activation-20261010', f'''## Incremento C85 — revisão QA, sem ativação
Menus de sessão adicionados ao fingerprint; preparação anterior ligada ao hash antigo preservada. Checagens e backup/restauro QA renovados em {backup['verified_at']} para o código atual: 295 tabelas/103 ficheiros em `{backup['restored_target']['database']}:55441`; nenhum novo recurso, migração, titular ou grant necessário para o botão. Bundle ativo permaneceu com SHA256 `{proof['active_build_sha256']}`.
Correção futura: disponibilizar componente/build compatíveis, após revisão candidata e ensaios correspondentes; preservar a ordem 030000→040000→09/190000 e executar somente instalações ausentes. Não repetir instalações QA por causa do botão. Reversão do incremento UI: regressar ao bundle/componentes anteriores, preservando dados e MFA; não desfazer schema nem recuperar uma chave distinta. Backup ativo READ ONLY de 17:48:52 UTC não é aceitação da nova candidata; verificar compatibilidade e reversão no clone e renovar o retrato antes de qualquer corte autorizado. C37 manual parcialmente confirmado; sem autorização de ativação ativa.
''')
print(json.dumps({'prepared_tables': len(files), 'updated_requirements': len(ids), 'manual_qa18_logout': True, 'manual_post_logout_mfa_observed': post_logout_mfa, 'active_writes': 0}))