Files
Investidor-V2/tools/portal-workflow-service-plan-finalize.py
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

141 lines
10 KiB
Python

"""Write the service review addendum; never mutate the fixed candidate or destination config."""
from pathlib import Path
from hashlib import sha256
import json
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
ROOT = Path(__file__).resolve().parent.parent
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
OUT = ROOT / 'outputs/investmoz-workflow-service-plan-20261011'
CANDIDATE = ROOT / 'outputs/investmoz-workflow-upgrade-20261011'
hash_file = lambda path: sha256(Path(path).read_bytes()).hexdigest()
package = json.loads((CANDIDATE / 'pacote.json').read_text(encoding='utf-8'))
assert package['revision'].startswith('98a8e0b70892')
assert hash_file(CANDIDATE / package['filename']) == package['sha256']
assert hash_file(CANDIDATE / 'manifesto.json') == package['manifest_sha256']
inventory = json.loads((PRIVATE / 'workflow-service-plan-inventory-20261011.json').read_text(encoding='utf-8'))
os_before = json.loads((PRIVATE / 'workflow-service-plan-20261011/os-before.json').read_text(encoding='utf-8'))
assert inventory['active_database_writes'] == 0 and not inventory['providers_booted']
assert inventory['target'] == 'investemoz:5432' and inventory['country_enabled']
assert inventory['queue'] == {'driver': 'database', 'name': 'default', 'retry_after': 210}
assert os_before['registered_tasks'] == 0 and os_before['active_services_changes'] == 0
for item in os_before['source_files']:
assert hash_file(item['path']) == item['sha256'], item['path']
clam = ROOT / 'storage/app/private/investmoz-v2/portal-audit/clamav'
freshclam = (clam / 'freshclam.conf').read_text(encoding='utf-8').rstrip()
assert 'NotifyClamd' not in freshclam
(OUT / 'freshclam-proposto.conf').write_text(freshclam + '\nNotifyClamd "' + (clam / 'clamd.conf').as_posix() + '"\n', encoding='utf-8')
(OUT / 'vhost-limites-propostos.conf').write_text('''# Proposed fragment inside the existing portal-investidor.test HTTPS VirtualHost only.
# Not installed. Keep existing TLS, redirects and directory access directives.
php_admin_value upload_max_filesize 20M
php_admin_value post_max_size 24M
php_admin_value memory_limit 256M
php_admin_value max_execution_time 180
''', encoding='utf-8')
ns = {'t': 'http://schemas.microsoft.com/windows/2004/02/mit/task'}
definitions = []
for path in sorted(OUT.glob('*.xml')):
tree = ET.parse(path)
text = lambda query: tree.find(query, ns).text
assert text('t:Principals/t:Principal/t:UserId') == 'S-1-5-19'
assert text('t:Settings/t:MultipleInstancesPolicy') == 'IgnoreNew'
assert text('t:Settings/t:AllowHardTerminate') == 'true'
definitions.append({'file': path.name, 'sha256': hash_file(path),
'executable': text('t:Actions/t:Exec/t:Command'),
'arguments': text('t:Actions/t:Exec/t:Arguments'),
'working_directory': text('t:Actions/t:Exec/t:WorkingDirectory'),
'execution_limit': text('t:Settings/t:ExecutionTimeLimit')})
assert len(definitions) == 3
worker = next(item for item in definitions if item['file'] == 'InvestMoz-Portal-Documentary-Worker.xml')
assert worker['execution_limit'] == 'PT3M' and '--once' in worker['arguments']
plan = {
'status': 'proposed_not_authorized_not_executed', 'at': datetime.now(timezone.utc).isoformat(),
'candidate_unchanged': package, 'application_code_delta': [], 'production_rebuild_required': False,
'target': 'investemoz:5432', 'origin': 'https://portal-investidor.test',
'planned_changes': {
'scanner_engine': 'keep existing ClamAV 1.5.4 x64 and bind 127.0.0.1:3310',
'signatures': 'freshclam official definitions in the directory actually used by port 3310',
'freshclam_config': str(clam / 'freshclam.conf'),
'config_delta': 'NotifyClamd points to the same existing clamd.conf; no APP_KEY/cookie/TLS/DLL changes',
'scheduled_tasks': definitions, 'replace_tasks': [item['name'] for item in os_before['tasks_before']],
'add_task': 'InvestMoz Portal Documentary Worker',
'technical_identity': {'sid': 'S-1-5-19', 'name': 'LocalService', 'new_account': False},
'acl': {'read_execute': 'application/vendor/config/runtime; read .env',
'modify': ['storage', 'bootstrap/cache', 'ClamAV database and logs'],
'write_code': False, 'Everyone': False, 'before_export_required': True,
'shared_builtin_service_identity_review_required': True},
'php_global_ini_changed': False,
'vhost': 'C:/laragon/etc/apache2/sites-enabled/portal-investidor.test.conf: HTTPS VirtualHost only',
'vhost_limits': {'upload_max_filesize': '20M', 'post_max_size': '24M', 'memory_limit': '256M', 'max_execution_time': '180'},
'worker': {'connection': 'database', 'queue': 'default', 'once': True,
'period_seconds': 60, 'external_time_limit_seconds': 180, 'retry_after_seconds': 210,
'timeout_option_without_pcntl_not_enforced': True,
'shared_consumers': ['ScanDocumentForMalware', 'RunSimulation', 'NotifyInterestReceived', 'CompareMneBusiness'],
'scope_authorization_required': True},
'apache_restarts': 'one, only via normal Laragon control; inspect all vhosts',
'schema': [], 'migrations': [], 'feature_flags': [], 'portal_accounts': [],
'organizations': [], 'representations': [], 'publications': [],
},
'observed': inventory,
'tests': {
'performed': ['read-only target identity/counts/config', 'actual CLI GD/WebP in-memory encoding',
'native Task Scheduler definitions round-trip in memory', 'XML byte parse and invariant checks',
'candidate ZIP/manifest and destination source/config hash preservation'],
'reused_QA': ['authenticated 030000/040000 and repetitions', 'PDF/PNG/DOCX/XLSX and three scanned WebP',
'reserved job crash and recovery after 210 seconds', 'authorization and original ciphertext preservation'],
'pending_before_destination': ['real scheduler 180-second deadline/retry under service identity in QA',
'HTTP uploads over 2 MiB and controlled contract maximum rejection in equivalent QA'],
'pending_destination': ['fresh 3310 scanner', 'service identity ACL/runtime and real supervised worker',
'web SAPI limits and affected vhosts', 'legitimate authorized document chain'],
},
'bootstrap': {
'approved_rule': 'Exactly two active holders; independent approval for replacement and operational elevation',
'third_initial_actor_explicitly_required_by_specification': False,
'current_code_requires_three_distinct_accounts': True,
'pre_initialization_authority': 'Verified email in PORTAL_MANAGER_EMAILS via manage-portal; recent session MFA',
'decision': 'Retain existing distinct authorized executor, or separately approve a two-stage initial ceremony',
'authorization_or_names_assigned': False,
},
'containment': 'disable new worker; preserve healthy scanner, queue/quarantine, COUNTRY02/TLS/keys/schema/history; no complete restore',
'active_database_writes': 0, 'active_services_changes': 0, 'tasks_registered': 0,
'activation_ready': False, 'activation_authorized': False,
}
(OUT / 'alteracoes-servicos.json').write_text(json.dumps(plan, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
audit = ROOT / 'docs/investmoz/v2/auditoria-portal'
marker = '<!-- workflow-service-plan-b1-b3-20261011 -->'
paragraph = '''<!-- workflow-service-plan-b1-b3-20261011 -->
Atualização de preparação dos serviços de 11/10/2026: [intervenção B1/B2](../../../../outputs/investmoz-workflow-service-plan-20261011/intervencao-b1-b2.md), [tabela de designação B3](../../../../outputs/investmoz-workflow-service-plan-20261011/designacoes-b3.md) e [fundamento do bootstrap](../../../../outputs/investmoz-workflow-service-plan-20261011/bootstrap-fundamento.md). A candidata `98a8e0b70892` permanece intacta. Nenhuma tarefa/serviço/privilégio/schema do destino foi alterado.
Correção de interpretação de B3: três contas distintas no bootstrap são uma restrição do código atual, não uma exigência organizacional expressa na v1. A especificação exige dois titulares e aprovação independente para substituição/elevação. Antes da inicialização, o executor usa a Gate legada `manage-portal` (`PORTAL_MANAGER_EMAILS` e email verificado), mais MFA recente; não recebe automaticamente Superutilizador. A forma da cerimónia requer decisão separada antes de alterar o código; nomes e âmbitos continuam por designar pelo responsável.
B1/B2 continuam abertos no destino. Foi identificado que as tarefas existentes atualizam outra instalação ClamAV; o portal usa 3310. CLI GD/WebP foi verificado, mas não comprova SAPI web ou supervisão. A proposta contém XMLs não registados, limites só do vhost e worker periódico da fila default partilhada; execução do watchdog/limites HTTP permanece por ensaiar em QA antes do destino. COUNTRY02 ativo, A/B/C37 encerrados, restauração de oportunidades e partilha individual indisponíveis. Nenhum requisito novo ou contagem da matriz foi criado.
'''
for name in ['backlog.md', 'plano-ativacao.md', 'perfis-v1-reconciliacao.md']:
path = audit / name
original = path.read_text(encoding='utf-8')
assert marker not in original, name
first, separator, rest = original.partition('\n')
path.write_text(first + separator + '\n' + paragraph + rest, encoding='utf-8')
assert hash_file(CANDIDATE / package['filename']) == package['sha256']
assert hash_file(CANDIDATE / 'manifesto.json') == package['manifest_sha256']
for item in os_before['source_files']:
assert hash_file(item['path']) == item['sha256'], item['path']
proof = {'result': 'passed', 'at': datetime.now(timezone.utc).isoformat(),
'candidate_revision': package['revision'], 'candidate_zip_sha256': package['sha256'],
'candidate_manifest_unchanged': True, 'source_config_binaries_unchanged': True,
'active_database_writes': 0, 'active_services_changes': 0, 'tasks_registered': 0,
'files': [{'file': p.name, 'sha256': hash_file(p)} for p in sorted(OUT.iterdir()) if p.is_file()],
'application_code_delta': [], 'recompile_required': False,
'QA_watchdog_and_HTTP_boundary_tests_pending': True,
'decision_required': 'D-BOOTSTRAP', 'real_people_selected': False}
(OUT / 'verificacao.json').write_text(json.dumps(proof, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(json.dumps({k: v for k, v in proof.items() if k != 'files'}, ensure_ascii=False))