155 lines
13 KiB
Python
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}))
|