141 lines
10 KiB
Python
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))
|