70 lines
5.1 KiB
PHP
70 lines
5.1 KiB
PHP
<?php
|
|
|
|
use Illuminate\Foundation\Bootstrap\LoadConfiguration;
|
|
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
|
|
|
|
// Metadata, counts and hashes only; no active writes or personal records exported.
|
|
require dirname(__DIR__).'/vendor/autoload.php';
|
|
$app = require dirname(__DIR__).'/bootstrap/app.php';
|
|
(new LoadEnvironmentVariables)->bootstrap($app);
|
|
(new LoadConfiguration)->bootstrap($app);
|
|
$c = $app->make('config')->get('database.connections.'.$app->make('config')->get('database.default'));
|
|
if (($c['driver'] ?? '') !== 'pgsql' || ! in_array($c['url'] ?? null, [null, ''], true)) {
|
|
throw new RuntimeException('Explicit PostgreSQL target required');
|
|
}
|
|
$pdo = new PDO('pgsql:host='.$c['host'].';port='.$c['port'].';dbname='.$c['database'], $c['username'], $c['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
$pdo->exec('BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
try {
|
|
$result = ['captured_at' => gmdate('c'), 'identity' => $pdo->query("SELECT current_database() AS database,inet_server_port() AS port,current_setting('transaction_read_only') AS read_only")->fetch(PDO::FETCH_ASSOC), 'tables' => [], 'active_writes' => 0];
|
|
$tables = $pdo->query("SELECT table_schema,table_name FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_schema NOT IN ('information_schema','pg_catalog') AND table_schema NOT LIKE 'pg_%' ORDER BY table_schema,table_name")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($tables as $t) {
|
|
$qualified = '"'.str_replace('"', '""', $t['table_schema']).'"."'.str_replace('"', '""', $t['table_name']).'"';
|
|
$q = $pdo->prepare('SELECT column_name,data_type FROM information_schema.columns WHERE table_schema=? AND table_name=? ORDER BY ordinal_position');
|
|
$q->execute([$t['table_schema'], $t['table_name']]);
|
|
$columns = $q->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
$h = hash_init('sha256');
|
|
$count = 0;
|
|
foreach ($pdo->query('SELECT row_to_json(t)::text AS row FROM '.$qualified.' t ORDER BY row_to_json(t)::text') as $r) {
|
|
if ($count++) {
|
|
hash_update($h, "\n");
|
|
}
|
|
hash_update($h, $r['row']);
|
|
}
|
|
$entry = ['rows' => $count, 'sha256' => hash_final($h), 'columns' => $columns];
|
|
foreach (['status', 'data_classification', 'malware_scan_status', 'location_type'] as $field) {
|
|
if (isset($columns[$field])) {
|
|
$entry['groups'][$field] = $pdo->query('SELECT "'.$field.'", count(*) AS count FROM '.$qualified.' GROUP BY "'.$field.'" ORDER BY "'.$field.'"')->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
}
|
|
$result['tables'][$t['table_schema'].'.'.$t['table_name']] = $entry;
|
|
}
|
|
$result['migrations'] = $pdo->query('SELECT migration,batch FROM public.migrations ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
|
|
$result['installation_events'] = $pdo->query("SELECT action,count(*) AS count,max(occurred_at) AS last_at FROM public.audit_events WHERE action LIKE '%installed%' GROUP BY action ORDER BY action")->fetchAll(PDO::FETCH_ASSOC);
|
|
$result['opportunity_classifications'] = $pdo->query("SELECT DISTINCT ON(after_data->>'opportunity_id') after_data->>'opportunity_id' AS id,after_data->>'classification' AS classification FROM public.audit_events WHERE source_system='opportunity-origin-review' ORDER BY after_data->>'opportunity_id',occurred_at DESC,id DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
$result['geo_with_geometry'] = $result['tables']['public.geo_geometries']['rows'] ?? null;
|
|
$result['origin_counts'] = $pdo->query("SELECT metadata->>'declared_origin_country_code' AS code,count(*) AS count FROM public.interest_requests WHERE deleted_at IS NULL GROUP BY metadata->>'declared_origin_country_code'")->fetchAll(PDO::FETCH_ASSOC);
|
|
$result['features'] = ['education' => $app->make('config')->get('education.enabled'), 'suitecrm' => $app->make('config')->get('suitecrm.enabled'), 'ai' => $app->make('config')->get('investmoz_ai.enabled')];
|
|
} finally {
|
|
$pdo->exec('ROLLBACK');
|
|
}
|
|
$label = $argv[1] ?? 'before';
|
|
if (! in_array($label, ['before', 'after'], true)) {
|
|
throw new RuntimeException('before/after label required');
|
|
}
|
|
$dir = dirname(__DIR__).'/storage/app/private/investmoz-v2/portal-audit';
|
|
if (! is_dir($dir)) {
|
|
mkdir($dir, 0700, true);
|
|
}
|
|
$destination = $dir.'/estado-ativo-'.$label.'.json';
|
|
if (is_file($destination)) {
|
|
if ($label === 'before') {
|
|
throw new RuntimeException('Initial audit snapshot already exists; preserve it and use after');
|
|
}
|
|
$archive = $dir.'/estado-ativo-after-'.gmdate('Ymd_His').'-'.bin2hex(random_bytes(3)).'.json';
|
|
if (! copy($destination, $archive)) {
|
|
throw new RuntimeException('Cannot preserve previous audit snapshot');
|
|
}
|
|
}
|
|
file_put_contents($destination, json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
echo json_encode(['captured_at' => $result['captured_at'], 'identity' => $result['identity'], 'tables' => count($tables), 'geo_with_geometry' => $result['geo_with_geometry'], 'installation_events' => $result['installation_events'], 'features' => $result['features'], 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;
|