Files
Investidor-V2/tools/legacy-sectors-import-qa.php
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

105 lines
6.5 KiB
PHP

<?php
use App\Models\User;
use App\Services\Malware\MalwareScanner;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
// Existing private-library importer, isolated QA only; disclosure is a separate HTTP action.
require dirname(__DIR__).'/vendor/autoload.php';
require __DIR__.'/portal-qa-environment.php';
$target = portalQaEnvironment();
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$identity = portalQaAssertTarget($target);
config(['portal.managers' => ['opportunity-manager@example.invalid', 'geo-manager-qa@example.invalid']]);
$directory = storage_path('app/private/investmoz-v2/portal-audit');
$stamp = gmdate('Ymd_His');
set_exception_handler(function (Throwable $error) use ($directory, $stamp): void {
file_put_contents($directory.'/legacy-sector-import-error-private-'.$stamp.'.json', json_encode(['type' => get_class($error), 'message' => $error->getMessage()], JSON_THROW_ON_ERROR));
fwrite(STDERR, "QA sector operation failed; inspect private diagnostic.\n");
exit(1);
});
$mode = $argv[1] ?? 'preview';
throw_unless(in_array($mode, ['preview', 'apply', 'verify', 'replay'], true), RuntimeException::class, 'Unknown operation');
$manifestName = 'legacy-setores-20261010/manifest.json';
$staging = storage_path('app/private/document-import-staging');
$manifest = json_decode(file_get_contents($staging.'/'.$manifestName), true, 512, JSON_THROW_ON_ERROR);
throw_unless(count($manifest['documents']) === 22, RuntimeException::class, 'Expected original 22-item sector lot');
$actorFile = json_decode(file_get_contents($directory.'/resource-http-actors-private.json'), true, 512, JSON_THROW_ON_ERROR);
$actor = User::findOrFail($actorFile['manager']['id']);
throw_unless($actor->can('manage-portal'), RuntimeException::class, 'Authorized existing library manager required');
function sectorCounts(): array
{
$result = [];
foreach (['documents', 'portal_resources', 'audit_events', 'document_malware_scans', 'opportunities', 'desk_cases', 'users', 'jobs'] as $table) {
if (Schema::hasTable($table)) {
$result[$table] = DB::table($table)->count();
}
}
return $result;
}
$before = sectorCounts();
$exit = null;
if (in_array($mode, ['preview', 'apply', 'replay'], true)) {
$args = ['manifest' => $manifestName];
if ($mode !== 'preview') {
$health = app(MalwareScanner::class)->health();
throw_unless($health['available'] ?? false, RuntimeException::class, 'Real ClamD not healthy; import refused');
// A fresh read-only preview is mandatory before each application, including repetition.
$previewExit = Artisan::call('documents:import-batch', $args);
$preview = Artisan::output();
file_put_contents($directory.'/legacy-sectors-pre-apply-'.$stamp.'.txt', $preview);
throw_unless($previewExit === 0 && ! str_contains($preview, 'review required') && ! str_contains($preview, 'not imported'), RuntimeException::class, 'Preview has unresolved conflicts');
$args += ['--apply' => true, '--actor' => (string) $actor->id];
config(['queue.default' => 'sync']); // Actual configured ClamD scanner; no fake scan result.
}
$exit = Artisan::call('documents:import-batch', $args);
$output = Artisan::output();
file_put_contents($directory.'/legacy-sectors-import-'.$mode.'-'.$stamp.'.txt', $output);
throw_unless($exit === 0 && ! str_contains($output, 'review required') && ! str_contains($output, 'not imported'), RuntimeException::class, 'Import/preview failed');
}
$rows = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
->whereIn('d.external_id', array_column($manifest['documents'], 'external_id'))
->orderBy('r.id')->get(['r.id', 'r.document_id', 'r.title', 'r.source_url', 'r.metadata_version', 'r.published', 'd.external_id', 'd.sha256',
'd.malware_scan_status', 'd.malware_scanned_sha256', 'd.status', 'd.file_name', 'd.storage_key', 'd.storage_encryption', 'd.data_classification']);
$checks = [];
foreach ($rows as $row) {
$record = collect($manifest['documents'])->firstWhere('external_id', $row->external_id);
$path = $staging.'/'.$record['relative_path'];
$bytes = Storage::disk('local')->get($row->storage_key);
if ($row->storage_encryption === 'laravel') {
$bytes = Crypt::decryptString($bytes);
}
$checks[] = ['resource_id' => $row->id, 'source_hash_equal' => hash_equals(trim($row->sha256), hash_file('sha256', $path)),
'stored_original_equal' => hash_equals(trim($row->sha256), hash('sha256', $bytes)), 'clean_scan_for_hash' => $row->malware_scan_status === 'clean'
&& hash_equals(trim($row->sha256), (string) $row->malware_scanned_sha256)];
unset($bytes);
}
$after = sectorCounts();
if ($mode === 'preview') {
throw_unless($before === $after, RuntimeException::class, 'Preview unexpectedly changed database');
}
if ($mode !== 'preview') {
throw_unless($rows->count() === 22 && collect($checks)->every(fn ($c) => $c['source_hash_equal'] && $c['stored_original_equal'] && $c['clean_scan_for_hash']), RuntimeException::class, 'Original/hash/scan checks incomplete');
if ($mode === 'apply') {
throw_unless($rows->every(fn ($r) => ! $r->published && $r->data_classification === 'internal'), RuntimeException::class, 'Importer must not publish');
}
if ($mode === 'replay') {
throw_unless($before === $after, RuntimeException::class, 'Repeat import is not idempotent');
}
}
$proof = ['mode' => $mode, 'captured_at' => gmdate('c'), 'target' => $identity, 'manifest_sha256' => hash_file('sha256', $staging.'/'.$manifestName),
'expected_items' => 22, 'resources' => $rows, 'checks' => $checks, 'before' => $before, 'after' => $after, 'command_exit' => $exit,
'scanner_class' => get_class(app(MalwareScanner::class)), 'active_writes' => 0, 'publication_is_separate' => true];
$path = $directory.'/legacy-sectors-'.$mode.'-'.$stamp.'.json';
file_put_contents($path, json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
file_put_contents($directory.'/legacy-sectors-'.$mode.'-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
echo json_encode(['mode' => $mode, 'expected_items' => 22, 'resources_found' => $rows->count(), 'before' => $before, 'after' => $after, 'command_exit' => $exit,
'target' => $identity, 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;