64 lines
4.6 KiB
Python
64 lines
4.6 KiB
Python
"""Analyze the user's bounded manual attempt; never send authentication requests."""
|
||
import hashlib
|
||
import json
|
||
import re
|
||
from pathlib import Path
|
||
|
||
root = Path(__file__).resolve().parent.parent
|
||
private = root / 'storage/app/private/investmoz-v2/portal-audit'
|
||
file = private / 'auth-session-trace-qa.jsonl'
|
||
all_rows = [json.loads(line) for line in file.read_text(encoding='utf-8').splitlines()]
|
||
rows = [row for row in all_rows if '2026-10-09T21:22:00' <= row['at_utc'] <= '2026-10-09T21:24:59']
|
||
assert len(rows) == 6, 'Manual interval changed: inspect before classifying.'
|
||
assert [(r['method'], r['path']) for r in rows] == [
|
||
('GET', '/investimentos'), ('GET', '/login'), ('POST', '/login'),
|
||
('GET', '/two-factor-challenge'), ('POST', '/two-factor-challenge'), ('GET', '/login')]
|
||
assert all(row['qa_target'] == 'desk_restore_20261009_174726_test:55441' for row in rows)
|
||
login, challenge, mfa, next_request = rows[2:]
|
||
assert login['after']['pending_mfa_user'] == challenge['loaded']['pending_mfa_user'] == mfa['loaded']['pending_mfa_user'] == 13
|
||
assert login['after']['session'] == challenge['loaded']['session'] == mfa['loaded']['session']
|
||
assert mfa['status'] == 302 and mfa['redirect']['path'] == '/login'
|
||
assert all(not row['after']['authenticated_user'] for row in rows)
|
||
assert next_request['loaded']['session'] == mfa['after']['session'] == next_request['loaded']['accepted_cookie_session']
|
||
|
||
# Export only the correlated structured warning; no arguments, headers or bodies.
|
||
log = root / 'storage/logs/laravel.log'
|
||
with log.open('rb') as f:
|
||
f.seek(max(0, log.stat().st_size - 2 * 1024 * 1024))
|
||
lines = f.read().decode('utf-8', errors='replace').splitlines()
|
||
warnings = []
|
||
for line in lines:
|
||
match = re.match(r'^\[2026-10-09 21:23:04\] testing\.WARNING: Two-factor credential could not be decrypted; authentication refused\. (\{.*\})\s*$', line)
|
||
if match:
|
||
data = json.loads(match.group(1))
|
||
warnings.append({'at_utc': '2026-10-09T21:23:04+00:00', 'reference': data['reference'], 'pending_user_id': data['pending_user_id'], 'category': 'decryption_failure'})
|
||
snapshot_hash = hashlib.sha256(json.dumps(rows, sort_keys=True).encode()).hexdigest()
|
||
archive = private / 'c37-manual-2122-2124-qa.json'
|
||
if not warnings and archive.exists():
|
||
retained = json.loads(archive.read_text(encoding='utf-8'))
|
||
assert retained['manual_trace_snapshot_sha256'] == snapshot_hash, 'Archived proof must match the same six requests.'
|
||
warnings = [retained['warning']]
|
||
assert len(warnings) == 1 and warnings[0]['pending_user_id'] == 13
|
||
diagnosis = json.loads((private / 'mfa-key-diagnosis-qa.json').read_text(encoding='utf-8-sig'))
|
||
assert diagnosis['encrypted_field_checks']['two_factor_secret'] == {'qa': False, 'source_env': True}
|
||
assert diagnosis['encrypted_field_checks']['two_factor_recovery_codes'] == {'qa': False, 'source_env': True}
|
||
|
||
result = {'case': 'C37', 'manual_interval_utc': '2026-10-09 21:22:00–21:24:59',
|
||
'reported_expected_actor': 18, 'observed_challenged_actor': 13,
|
||
'mfa_screen_reached': True, 'manual_mfa_screen_observed_by_user': True,
|
||
'timeline': [{key: row[key] for key in ['at_utc', 'trace', 'method', 'path', 'status', 'redirect', 'loaded', 'after', 'persisted']} for row in rows],
|
||
'first_failure': {'at_utc': mfa['at_utc'], 'trace': mfa['trace'], 'path': mfa['path'], 'category': 'decryption_failure',
|
||
'result': 'Authentication refused before completion; controlled302 to /login and intentional session invalidation.'},
|
||
'setup_mismatch_first_seen_at': login['at_utc'], 'warning': warnings[0],
|
||
'mfa_invalid_code_rejection': False, 'authentication_completed': False,
|
||
'session_lost_after_authentication': False, 'next_request_accepts_regenerated_session': True,
|
||
'cookie_conflict_observed': False, 'server_key_or_guard_changed': False,
|
||
'request_code_or_recovery_value_recorded': False,
|
||
'exact_mfa_input_mode_in_manual_trace': 'not recorded; no request bodies retained',
|
||
'cause_confirmed': True, 'cause': 'Restored non-synthetic account13 selected; its MFA credentials use the source key, incompatible with the stable isolated QA key. Account18 was not challenged in this attempt.',
|
||
'browser_acceptance_passed': False, 'active_writes': 0,
|
||
'manual_trace_snapshot_sha256': snapshot_hash}
|
||
(private / 'c37-manual-2122-2124-qa.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
(private / 'auth-session-diagnosis-qa.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
print(json.dumps({key: value for key, value in result.items() if key != 'timeline'}, ensure_ascii=True, indent=2))
|