"""Record C37 in the existing audit, without closing visual/active criteria.""" import json import re import xml.etree.ElementTree as ET from collections import deque from pathlib import Path root = Path(__file__).resolve().parent.parent private = root / 'storage/app/private/investmoz-v2/portal-audit' docs = root / 'docs/investmoz/v2/auditoria-portal' http = json.loads((private / 'mfa-http-qa.json').read_text(encoding='utf-8-sig')) diagnosis = json.loads((private / 'mfa-key-diagnosis-qa.json').read_text(encoding='utf-8-sig')) invalidated = json.loads((private / 'mfa-session-invalidation-qa.json').read_text(encoding='utf-8-sig')) persistence = json.loads((private / 'mfa-persistence-qa.json').read_text(encoding='utf-8-sig')) runtime = json.loads((private / 'investor-path-qa-runtime.json').read_text(encoding='utf-8-sig')) cases = list(ET.parse(private / 'mfa-auth-qa.xml').iter('testcase')) assert len(cases) == 17 and all(not list(c) for c in cases) assert http['target'] == diagnosis['target'] == invalidated['target'] == persistence['target'] == runtime['target'] assert http['login_mfa_logout_new_session_passed'] current_mfa_revision = http['code_hash'] == runtime['code_hash'] assert http['stable_key_matches_cli_web'] and not http['interface_observed'] and http['active_writes'] == 0 assert invalidated['qa_sessions_invalidated'] == 2 and invalidated['account_row_unchanged'] assert persistence['original_account_13_unchanged'] and persistence['source_env_unchanged'] and not persistence['key_generated_or_rotated'] assert diagnosis['encrypted_field_checks']['two_factor_secret'] == {'qa': False, 'source_env': True} # Retain every frame, omitting all arguments and request data. lines = list(deque((root / 'storage/logs/laravel.log').open(encoding='utf-8', errors='replace'), maxlen=5000)) stacks = [] for i, line in enumerate(lines): if 'testing.ERROR: The MAC is invalid.' not in line: continue frames = [] for frame in lines[i+1:]: if re.match(r'^\[\d{4}-', frame): break if frame.startswith('#'): location, sep, call = frame.partition('): ') if sep: frames.append(location + '): ' + call.split('(')[0] + '(arguments omitted)') elif '{main}' in frame: frames.append(frame.strip()) if frames and 'TwoFactorLoginRequest.php' in frames[0]: stacks.append({'observed': line.split(' {')[0].strip(), 'frames': frames, 'arguments_exported': False}) assert len(stacks) >= 2 and all(len(s['frames']) == 68 for s in stacks[-2:]) (private / 'mfa-original-stack-redacted.json').write_text(json.dumps(stacks[-2:], ensure_ascii=False, indent=2), encoding='utf-8') def insert(filename, heading, body): path = docs / filename text = path.read_text(encoding='utf-8-sig') block = heading + '\n\n' + body.strip() + '\n\n' start = text.find(heading + '\n') if start >= 0: end = text.find('\n## ', start + len(heading)) text = text[:start] + block + (text[end+1:] if end >= 0 else '') else: first = text.find('\n') text = text[:first+1] + '\n' + block + text[first+1:].lstrip('\n') path.write_text(text, encoding='utf-8') body = f'''**C37/P0 — histórico da correção do tratamento do MAC inválido e ensaio HTTP em {http['captured_at']}, revisão {http['code_hash']}.** Correspondência com a revisão atual: {current_mfa_revision}. Esta evidência não fecha o ciclo manual reaberto nem a interface; consultar o diagnóstico C37 mais recente no início deste documento. URL: http://127.0.0.1:18097/investimentos. Alvo real confirmado antes de qualquer escrita: `{runtime['target']['database']}:55441`. Stack completa dos erros20:39:16/20:41:52 (68frames por ocorrência) retida sem argumentos em mfa-original-stack-redacted.json. Falha em TwoFactorLoginRequest::hasValidCode() ao desencriptar users.two_factor_secret da conta13; PreserveFormFeedback apenas chama o próximo middleware. A conta restaurada não é sintética: secret/recovery abrem com a chave do .env de origem, mas não com a chaveQA. Cache de configuração não carregado; source cache também ausente. A ferramenta de preparação do percurso antes não fixava APP_KEY e podia herdar origem. portal-qa-environment.php centraliza a mesma chaveQA já existente entre preparação/web, força alvo/restauro, configura caminho QA de cache separado e recusa cache não reconciliado. Não gera chave, não usa chaves ativas como fallback nem modifica .env/cache ativo. Duas sessões pendentes da conta13 removidas exclusivamente do diretório de sessões QA, após guarda do destino e paths. Original conta13 preservado byte a byte pelo hash da linha; nenhuma chave/MFA ativa ou da conta13 alterada. Novo investidor sintético QA: actor{http['actor_id']}, inscrição normal pelos endpoints Fortify (confirmar password→enable→secret-key→confirm), oito recovery codes, sem injeção direta do segredo nem desativação de MFA. Dados de acesso/segredo somente em investor-mfa-qa-private.json local privado; não exportados aqui. POST /two-factor-challenge com DecryptException agora invalida desafio/sessão e retorna422 com mensagem controlada em JSON ou redireciona ao login com erro visível; nenhuma autenticação. Negativas reais de secret/recovery verificam401 após erro e remoção do desafio. A resposta HTML/Inertia contém a mensagem, sem ser prova de renderização visual. Logs controlados incluem apenas referência e ID pendente; as provas não exportam Cookie, X-XSRF-TOKEN, OTP ou corpos. No mesmo servidor18097: login/MFA/logout/nova sessão passaram. A retomado por HTTP em `/investimentos/{http['scenario_A']['id']}`: criação→logout→nova sessãoMFA→recuperação→aplicabilidade semterra/semfinanciamento→draft próprio→resultado→Balcão draft→tarefa concluída pela gestão→nova sessãoMFA recupera tudo. Gestão não recebe resultado financeiro privado. Função efetiva do dossier recalcula20unidades/0.00 a partir do resultado recuperado. QA atual{runtime['qa_path_journeys']}instâncias/{runtime['qa_path_steps']}etapas; originals anteriores preservados. Esta conta não vê os A/B da outra contaQA. Evidências: mfa-key-diagnosis-qa.json, mfa-session-invalidation-qa.json, mfa-persistence-qa.json, mfa-http-qa.json e mfa-auth-qa.xml. Os17 ensaios autenticam ou recusam os percursos concretos; não substituem interface. Fingerprints passam a incluir bootstrap/Fortify, invalidando ready/provas anteriores para ativação. `integrated_ui_ab_passed=false`, `candidate_revision_fixed=false`, `activation_ready=false`; nenhuma escrita ativa. Mesma matriz3354requisitos/5830origens, sem fecho amplo novo. ''' for filename in ['relatorio-executivo.md', 'execucao-percurso-i1-i3.md', 'cobertura-percursos-i0-i3.md']: insert(filename, '## C37 — MFA QA recuperado; cenário A visual por retomar', body) insert('backlog.md', '## Próximo passo após C37 — retomar A com MFA', ''' 1. Retomar A primeiro na interface18097 com o investidor sintético MFA; iniciar janela privada nova. Preservar o500 reportado como falha bloqueante histórica; conta13 restaurada não deve servir de conta sintéticaQA. Nova criação manual continua exigida, não apenas abrir o dossier HTTP. 2. Registar login/desafio/mensagem/recuperação, depois ferramenta→resultado→Balcão→acompanhamento e negativas; omitir Cookie/X-XSRF-TOKEN/corpo do código das capturas. Corrigir outras falhas antes de fixar candidata. 3. B, países/estatísticas e seletores/mapas/documentos continuam no mesmo backlog. API/persistência das operações não aguardam browser; ClamD mantém âmbito específico. 4. Após resultados manuais: renovar backup/restauro/compatibilidade sobre a revisão final. Bootstrap/Fortify agora entram nos fingerprints; não reutilizar ready/provas de revisão anterior. Nenhuma ativação ativa autorizada. ''') insert('plano-ativacao.md', '## C37 — revisão ainda sem candidatura ou ativação', ''' Correção MFA é código, não migration/importação/publicação. Aplicação e preparação QA usam chave estável existente; não executar key:generate nem rekey dos dados. Conta13 ativa/copiada preservada; contas sintéticas e invalidação de sessões exclusivamenteQA. Fingerprints incluem bootstrap/app.php e configuração/provider Fortify; provas antigas devem ser renovadas depois da retoma visual A/B e correções. Ordem de recursos030000, percurso190000, flag e cartografia/divulgação continua no plano abaixo, sem repetir instalações. Ação ativa não autorizada. ''') insert('roteiro-manual-qa-i0-i3.md', '## Retoma obrigatória após C37 — sessão e investidor MFA', ''' Endereço exato: http://127.0.0.1:18097/investimentos; login /login. Fechar a sessão anterior e abrir janela privada nova; as duas sessõesQA da conta13 foram invalidadas. Confirmar X-Investmoz-QA-Target=desk_restore_20261009_174726_test:55441 antes de escrever. Não usar a conta13 restaurada como fixtureQA. Usar o investidor sintético MFA preparado (actor18). As credenciais e segredo para configurar o autenticador ficam somente no ficheiro local privado storage/app/private/investmoz-v2/portal-audit/investor-mfa-qa-private.json; não colar conteúdo/código/cookies nesta conversa. MFA foi ativado/confirmado normalmente pelo Fortify e é obrigatório no login. Gestão e outra contaQA conservadas para os restantes passos/permissões. 1. Entrar com a contaQA, confirmar desafioMFA antes do acesso ao investimento, usar o autenticador e abrir /investimentos. Esperado: entrada depois do código; nenhum500. 2. Criar um A visual novo, guardar; logout, fechar janela e nova sessão com password+MFA. Esperado: mesmoID/título recuperado. 3. Continuar a sequência A/B deste roteiro: aplicabilidade→ferramenta→resultado→Balcão→acompanhamento; registar cada prova. O dossier A HTTP atual é apenas inspeção complementar, não prova de criação na interface. 4. Para falha de desencriptação, esperado422JSON/mensagem controlada ou retorno ao login; nunca autenticação. Esse negativo foi ensaiado com fixture sintética separada, sem alterar conta13. Não adulterar manualmente a conta do investidor para o reproduzir. Capturas/requisições exportadas devem omitir Cookie, X-XSRF-TOKEN, passwords e corpo com code/recovery_code. As provas exportadas pela ferramenta registam somente método/URL/status/alvo, não cabeçalhos/corpos. Retoma visual permanece pendente até resultados observados pelo utilizador. ''') insert('casos-reproducao.md', '## C37 — MAC inválido no desafio MFA bloqueia cenário A — P0', body) proof_file = private / 'investor-path-evidence.json' proof = json.loads(proof_file.read_text(encoding='utf-8-sig')) proof.update({'mfa_http_qa_evidence': 'mfa-http-qa.json', 'mfa_persistence_qa': 'mfa-persistence-qa.json', 'mfa_blocking_case': 'C37', 'manual_scenario_A_failed_before_fix': True, 'manual_scenario_A_retested_after_fix': False, 'integrated_ui_ab_passed': False, 'candidate_revision_fixed': False, 'activation_ready': False}) proof_file.write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8') if proof.get('c37_reopened_browser_login_loop'): # Historical HTTP evidence must never undo the user's reopened browser blocker. import runpy runpy.run_path(str(root / 'tools/portal-audit-reopen-c37.py'), run_name='__main__') else: print(json.dumps({'case': 'C37', 'qa_login_mfa_new_session': 'passed', 'sessions_invalidated': 2, 'scenario_A_http_resumed': True, 'visual_retest': 'pending', 'requirements_closed': 0, 'active_writes': 0}))