$_value) return $key; return null; }
}
function bg_path_is_socket($path) {
$path = (string)$path;
if ($path === '') return false;
if (function_exists('is_socket')) return @is_socket($path);
// Fallback per installazioni PHP senza estensione sockets: per il bus DBus basta sapere che il path esiste.
return @file_exists($path);
}
function bg_safe_mkdir($dir, $mode = 0750) {
$dir = rtrim((string)$dir, '/');
if ($dir === '') return false;
if (is_dir($dir)) return is_writable($dir);
$parent = dirname($dir);
if (!is_dir($parent) || !is_writable($parent)) return false;
return @mkdir($dir, $mode, true) || is_dir($dir);
}
if (!function_exists('bg_webui_error_handler')) {
function bg_webui_error_handler($errno, $errstr, $errfile, $errline) {
$msg = '[' . date('c') . '] PHP ' . $errno . ': ' . $errstr . ' in ' . $errfile . ':' . $errline . "\n";
$log = '/tmp/bastionguard-webui-php-error.log';
@file_put_contents($log, $msg, FILE_APPEND | LOCK_EX);
return false;
}
set_error_handler('bg_webui_error_handler');
@ini_set('log_errors', '1');
@ini_set('error_log', '/tmp/bastionguard-webui-php-error.log');
register_shutdown_function(function() {
$err = error_get_last();
if (!$err || !in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) return;
$msg = '[' . date('c') . '] PHP FATAL: ' . $err['message'] . ' in ' . $err['file'] . ':' . $err['line'] . "\n";
@file_put_contents('/tmp/bastionguard-webui-php-error.log', $msg, FILE_APPEND | LOCK_EX);
if (PHP_SAPI !== 'cli' && !headers_sent()) http_response_code(500);
if (PHP_SAPI !== 'cli' && (!headers_sent() || !empty($_GET['debug']))) {
echo '
BastionGuard WebUI - errorBastionGuard WebUI: PHP error
The page could not be loaded. Details were saved in /tmp/bastionguard-webui-php-error.log.
';
if (!empty($_GET['debug'])) echo '
' . htmlspecialchars($msg, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '
';
echo '
Open health.php to check the PHP version, extensions and disabled functions.
';
}
});
}
function bg_shell_exec($cmd) {
$cmd = (string)$cmd;
if ($cmd === '') return '';
if (function_exists('shell_exec')) {
$out = @shell_exec($cmd);
return is_string($out) ? $out : '';
}
if (function_exists('exec')) {
$lines = [];
@exec($cmd, $lines);
return implode("\n", $lines);
}
return '';
}
function bg_e($value) { return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function bg_is_cli() { return PHP_SAPI === 'cli'; }
function bg_data_dir() {
static $dir = null;
if ($dir !== null) return $dir;
$candidates = [
'/var/lib/bastionguard-webui',
dirname(__DIR__) . '/data',
sys_get_temp_dir() . '/bastionguard-webui-data',
];
foreach ($candidates as $candidate) {
if (!is_dir($candidate)) bg_safe_mkdir($candidate, 0750);
if (is_dir($candidate) && is_writable($candidate)) {
$dir = $candidate;
return $dir;
}
}
// Ultimo fallback: non genera warning; le scritture falliranno in modo controllato.
$dir = sys_get_temp_dir();
return $dir;
}
function bg_data_file($name) {
$safe = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $name);
return bg_data_dir() . DIRECTORY_SEPARATOR . $safe;
}
function bg_read_json($name, $default = []) {
$file = bg_data_file($name);
if (!is_file($file)) return $default;
$raw = @file_get_contents($file);
if ($raw === false || trim($raw) === '') return $default;
$data = json_decode($raw, true);
return is_array($data) ? $data : $default;
}
function bg_web_options() {
$detected = '';
if (function_exists('bg_detect_desktop_user')) $detected = bg_detect_desktop_user();
$defaults = [
'http_port' => 81,
'https_port' => 444,
'desktop_user' => $detected,
'webpanel_name' => 'WebPanel',
'dashboard_widgets' => bg_dashboard_widget_defaults(),
];
$stored = bg_read_json('web_options.json', $defaults);
if (!is_array($stored)) $stored = [];
// Language and theme are intentionally not loaded from persistent WebUI storage.
// They are per-session interface preferences only, with English/System as defaults.
unset($stored['language'], $stored['theme']);
$opts = array_merge($defaults, $stored);
$opts['language'] = bg_language();
$opts['theme'] = bg_theme();
return $opts;
}
function bg_sanitize_webpanel_name($name) {
$name = trim(preg_replace('/\s+/', ' ', strip_tags((string)$name)));
if ($name === '') return 'WebPanel';
if (function_exists('mb_substr')) return mb_substr($name, 0, 48, 'UTF-8');
return substr($name, 0, 48);
}
function bg_webpanel_name() {
$web = bg_read_json('web_options.json', []);
$name = is_array($web) ? ($web['webpanel_name'] ?? '') : '';
return bg_sanitize_webpanel_name($name);
}
function bg_dashboard_widget_defaults() {
return [
'user_services' => true,
'service_cards' => true,
'service_table' => true,
'clamav_db' => true,
'realtime_paths' => true,
'recent_events' => true,
];
}
function bg_dashboard_widget_labels() {
return [
'user_services' => bg_t('dash_widget_user_services'),
'service_cards' => bg_t('dash_widget_service_cards'),
'service_table' => bg_t('dash_widget_service_table'),
'clamav_db' => bg_t('dash_widget_clamav_db'),
'realtime_paths' => bg_t('dash_widget_realtime_paths'),
'recent_events' => bg_t('dash_widget_recent_events'),
];
}
function bg_dashboard_widgets() {
$defaults = bg_dashboard_widget_defaults();
$web = bg_read_json('web_options.json', []);
$stored = is_array($web) && isset($web['dashboard_widgets']) && is_array($web['dashboard_widgets']) ? $web['dashboard_widgets'] : [];
$out = [];
foreach ($defaults as $key => $enabled) {
$out[$key] = array_key_exists($key, $stored) ? (bool)$stored[$key] : (bool)$enabled;
}
return $out;
}
function bg_dashboard_widget_enabled($key) {
$widgets = bg_dashboard_widgets();
return !empty($widgets[$key]);
}
function bg_language() {
$lang = $_SESSION['bg_language'] ?? 'en';
return in_array($lang, ['en','it'], true) ? $lang : 'en';
}
function bg_theme() {
$theme = $_SESSION['bg_theme'] ?? 'system';
return in_array($theme, ['system','light','dark'], true) ? $theme : 'system';
}
function bg_t($key, array $vars = []) {
static $dict = null;
if ($dict === null) {
$dict = [
'it' => [
'webui_complete'=>'WebUI completa','dashboard'=>'Dashboard','base'=>'Base','protection'=>'Protezione','security'=>'Sicurezza','system'=>'Sistema','scan_realtime'=>'Scansione realtime','anti_ransomware'=>'Anti-Ransomware','safe_banks'=>'Banche sicure','samba'=>'Samba','logs'=>'Log','quarantine'=>'Quarantena','identity_leaks'=>'Violazioni identità','password_manager'=>'Password manager','updates'=>'Aggiornamenti','phishing_search'=>'Phishing Search','wizard'=>'Wizard','configurations'=>'Configurazioni','prediction'=>'Previsione','settings'=>'Impostazioni','about'=>'Informazioni','donations'=>'Donazioni','logout'=>'Logout',
'open_section'=>'Apri sezione','close_section'=>'Chiudi sezione','theme'=>'Tema','language'=>'Lingua','light'=>'Chiaro','dark'=>'Scuro','system_theme'=>'Sistema','italian'=>'Italiano','english'=>'English','save'=>'Salva','apply'=>'Applica','show'=>'Mostra','hide'=>'Nascondi','delete'=>'Elimina','cancel'=>'Annulla','import'=>'Importa','confirm_import'=>'Conferma importazione','choose_csv'=>'Scegli file CSV','supported_csv'=>'CSV esportato da Chrome, Edge, Firefox o Safari.','preview'=>'Anteprima','browser'=>'Browser','credentials_found'=>'Credenziali trovate','all_imported'=>'Tutte verranno importate','vault_unlocked'=>'Vault sbloccato','vault_locked'=>'Vault bloccato','create_vault'=>'Crea vault compatibile BastionGuard','open_vault'=>'Apri vault BastionGuard','master_password'=>'Master password','new_master_password'=>'Nuova master password','confirm'=>'Conferma','new_credential'=>'Nuova credenziale','name'=>'Nome','username'=>'Username','password'=>'Password','url'=>'URL','notes'=>'Note','add'=>'Aggiungi','lock_vault'=>'Blocca vault','credentials'=>'Credenziali','search'=>'Cerca','no_entries'=>'Nessuna entry nel vault.','detected_files'=>'File rilevati','source_format'=>'Formato sorgente','import_browser_credentials'=>'Importa credenziali da browser','select_csv_first'=>'Seleziona un file CSV prima di importare.','import_preview_ready'=>'Anteprima pronta: %count% credenziali rilevate.','import_completed'=>'Importate %count% credenziali.','invalid_csv_format'=>'Formato CSV non riconosciuto. Formati supportati: Chrome, Edge, Firefox, Safari.','empty_file'=>'Il file è vuoto.','no_credentials'=>'Nessuna credenziale trovata.','password_hidden'=>'password nascosta','theme_quick'=>'Tema rapido','language_quick'=>'Lingua rapida','dashboard_title'=>'Dashboard BastionGuard','dashboard_subtitle'=>'Stato generale di antivirus, protezioni realtime, moduli privacy, banca sicura e servizi collegati.','refresh_status'=>'Aggiorna stato','open_log'=>'Apri log','user_services'=>'Servizi utente','target_user'=>'utente target','present'=>'presente','not_found'=>'non trovato','installed'=>'installato','not_installed'=>'non installato','module'=>'Modulo','unit'=>'Unità','state'=>'Stato','reading'=>'Lettura','detected_services'=>'Servizi rilevati da systemd','manage_services'=>'Gestisci servizi','clamav_db'=>'Database firme ClamAV','size'=>'Dimensione','modified'=>'Modificato','realtime_paths'=>'Percorsi realtime da clamd.conf','recent_events'=>'Eventi recenti','no_recent_events'=>'Nessun evento recente trovato nei servizi monitorati.','settings_title'=>'Impostazioni','interface_options'=>'Opzioni interfaccia','web_options_saved'=>'Opzioni interfaccia salvate.','desktop_user_services'=>'Utente servizi --user','apply_web_config'=>'Applica configurazione web','email_config_saved'=>'Configurazione email salvata.','safe_payments_saved'=>'Safe payments salvati.','started'=>'Avviato','stopped'=>'Fermo','starting'=>'Avvio','stopping'=>'Arresto','error'=>'Errore','not_read'=>'Non letto','webpanel_users'=>'Utenti WebPanel','webpanel_users_setup'=>'Setup utenti WebPanel','users_setup_desc'=>'Crea e gestisci gli utenti che possono accedere al WebPanel. Gli utenti non amministratori possono usare la WebUI ma non possono modificare i file di configurazione BastionGuard.','admin'=>'Admin','standard_user'=>'Utente','role'=>'Ruolo','create_user'=>'Crea utente','edit_user'=>'Modifica utente','new_user'=>'Nuovo utente','existing_users'=>'Utenti esistenti','created_at'=>'Creato il','optional_password'=>'Nuova password opzionale','save_user'=>'Salva utente','delete_user'=>'Elimina utente','admin_required'=>'Permessi admin richiesti','admin_required_desc'=>'Questa sezione modifica configurazioni o utenti WebPanel.','admin_required_full'=>'Il tuo account non è amministratore: puoi usare la WebUI, ma non puoi modificare i file di configurazione BastionGuard o la gestione utenti.','user_created'=>'Utente creato.','user_updated'=>'Utente aggiornato.','user_deleted'=>'Utente eliminato.','current_user'=>'Utente attuale','current_admin_desc'=>'Il tuo account è amministratore: puoi gestire WebPanel, utenti e file di configurazione BastionGuard.','current_standard_desc'=>'Il tuo account non è amministratore: puoi usare la WebUI, ma non puoi modificare i file di configurazione BastionGuard o la gestione utenti.','actions'=>'Azioni','settings_source_equiv'=>'Sezioni allineate alla finestra impostazioni della GUI GTK: Base, Avanzate, Anti-Ransomware, Anti-Phishing, whitelist, servizi, email, DB ClamAV, pagamenti e opzioni Web.','anti_phishing'=>'Anti-Phishing','advanced'=>'Avanzate','advanced_antiphishing'=>'Anti-Phishing avanzate','advanced_antiransomware'=>'Anti-Ransomware avanzate','whitelist'=>'Whitelist','system_user_services'=>'Servizi sistema e utente','google_safe_key'=>'Chiave API Google Safe Browsing','auto_blacklist_update'=>'Aggiorna automaticamente la blacklist ogni 2 ore','update_blacklist'=>'Aggiorna blacklist','loaded_blacklists'=>'Blacklist caricate','firewall_ip_block'=>'Blocco IP automatico firewall','enable_firewall_ip_block'=>'Abilita blocco IP automatico','configure_firewall_ip_block'=>'Configura blocco IP firewall','update_yara'=>'Aggiorna regole YARA','update_sanesecurity'=>'Aggiorna Sanesecurity DB','scanner_configuration'=>'Configurazione scanner','use_yara_rules'=>'Usa regole YARA','use_sanesecurity_db'=>'Usa DB Sanesecurity','scan_interval_minutes'=>'Intervallo scansione (min)','folders_to_scan'=>'Cartelle da scansionare','ignore_paths'=>'Percorsi da ignorare','ignored_extensions'=>'Estensioni ignorate','suspicious_extensions'=>'Estensioni sospette','save_scanner_configuration'=>'Salva configurazione scanner','loaded_yara_rules'=>'Regole YARA in uso','config_write_denied'=>'Permesso negato: solo gli amministratori WebPanel possono modificare i file di configurazione.','email_address'=>'Indirizzo email','admin_email_hint'=>'Usato opzionalmente per inviare notifiche malware/ransomware agli admin.','invalid_email_address'=>'Indirizzo email non valido.','server_alerts'=>'Avvisi server','server_alert_email'=>'Email avvisi server','smtp_host'=>'Host SMTP','smtp_port'=>'Porta SMTP','smtp_security'=>'Sicurezza SMTP','smtp_username'=>'Username SMTP','smtp_password'=>'Password SMTP','alert_recipients'=>'Destinatari avvisi','use_admin_emails'=>'Usa email degli admin','send_test_email'=>'Invia email test','email_alerts_saved'=>'Configurazione email avvisi salvata.','malware_detected'=>'Malware rilevato','ransomware_detected'=>'Ransomware rilevato','quarantine_action'=>'Quarantena','ignore_action'=>'Ignora','inotify_monitor'=>'Monitor realtime inotify','start_monitor'=>'Avvia monitor','stop_monitor'=>'Ferma monitor','live_events'=>'Eventi live','samba_scan_helper'=>'Scansione Samba tramite helper',
],
'en' => [
'webui_complete'=>'Complete WebUI','dashboard'=>'Dashboard','base'=>'Base','protection'=>'Protection','security'=>'Security','system'=>'System','scan_realtime'=>'Realtime scan','anti_ransomware'=>'Anti-Ransomware','safe_banks'=>'Safe banking','samba'=>'Samba','logs'=>'Logs','quarantine'=>'Quarantine','identity_leaks'=>'Identity leaks','password_manager'=>'Password manager','updates'=>'Updates','phishing_search'=>'Phishing Search','wizard'=>'Wizard','configurations'=>'Configurations','prediction'=>'Prediction','settings'=>'Settings','about'=>'About','donations'=>'Donations','logout'=>'Logout',
'open_section'=>'Open section','close_section'=>'Close section','theme'=>'Theme','language'=>'Language','light'=>'Light','dark'=>'Dark','system_theme'=>'System','italian'=>'Italiano','english'=>'English','save'=>'Save','apply'=>'Apply','show'=>'Show','hide'=>'Hide','delete'=>'Delete','cancel'=>'Cancel','import'=>'Import','confirm_import'=>'Confirm import','choose_csv'=>'Choose CSV file','supported_csv'=>'CSV exported from Chrome, Edge, Firefox or Safari.','preview'=>'Preview','browser'=>'Browser','credentials_found'=>'Credentials found','all_imported'=>'All entries will be imported','vault_unlocked'=>'Vault unlocked','vault_locked'=>'Vault locked','create_vault'=>'Create BastionGuard-compatible vault','open_vault'=>'Open BastionGuard vault','master_password'=>'Master password','new_master_password'=>'New master password','confirm'=>'Confirm','new_credential'=>'New credential','name'=>'Name','username'=>'Username','password'=>'Password','url'=>'URL','notes'=>'Notes','add'=>'Add','lock_vault'=>'Lock vault','credentials'=>'Credentials','search'=>'Search','no_entries'=>'No entries in the vault.','detected_files'=>'Detected files','source_format'=>'Source format','import_browser_credentials'=>'Import browser credentials','select_csv_first'=>'Select a CSV file before importing.','import_preview_ready'=>'Preview ready: %count% credentials detected.','import_completed'=>'Imported %count% credentials.','invalid_csv_format'=>'Unrecognized CSV format. Supported formats: Chrome, Edge, Firefox, Safari.','empty_file'=>'The file is empty.','no_credentials'=>'No credentials found.','password_hidden'=>'password hidden','theme_quick'=>'Quick theme','language_quick'=>'Quick language','dashboard_title'=>'BastionGuard Dashboard','dashboard_subtitle'=>'Overall antivirus, realtime protection, privacy modules, safe banking and service status.','refresh_status'=>'Refresh status','open_log'=>'Open log','user_services'=>'User services','target_user'=>'target user','present'=>'present','not_found'=>'not found','installed'=>'installed','not_installed'=>'not installed','module'=>'Module','unit'=>'Unit','state'=>'State','reading'=>'Reading','detected_services'=>'Services detected by systemd','manage_services'=>'Manage services','clamav_db'=>'ClamAV signature database','size'=>'Size','modified'=>'Modified','realtime_paths'=>'Realtime paths from clamd.conf','recent_events'=>'Recent events','no_recent_events'=>'No recent events found in monitored services.','settings_title'=>'Settings','interface_options'=>'Interface options','web_options_saved'=>'Interface options saved.','desktop_user_services'=>'--user services account','apply_web_config'=>'Apply web configuration','email_config_saved'=>'Email configuration saved.','safe_payments_saved'=>'Safe payments saved.','started'=>'Started','stopped'=>'Stopped','starting'=>'Starting','stopping'=>'Stopping','error'=>'Error','not_read'=>'Not read','webpanel_users'=>'WebPanel users','webpanel_users_setup'=>'WebPanel users setup','users_setup_desc'=>'Create and manage users allowed to access the WebPanel. Non-admin users can use the WebUI but cannot modify BastionGuard configuration files.','admin'=>'Admin','standard_user'=>'User','role'=>'Role','create_user'=>'Create user','edit_user'=>'Edit user','new_user'=>'New user','existing_users'=>'Existing users','created_at'=>'Created at','optional_password'=>'Optional new password','save_user'=>'Save user','delete_user'=>'Delete user','admin_required'=>'Admin permission required','admin_required_desc'=>'This section changes WebPanel users or configuration files.','admin_required_full'=>'Your account is not an administrator: you can use the WebUI, but you cannot modify BastionGuard configuration files or user management.','user_created'=>'User created.','user_updated'=>'User updated.','user_deleted'=>'User deleted.','current_user'=>'Current user','current_admin_desc'=>'Your account is an administrator: you can manage WebPanel, users and BastionGuard configuration files.','current_standard_desc'=>'Your account is not an administrator: you can use the WebUI, but you cannot modify BastionGuard configuration files or user management.','actions'=>'Actions','settings_source_equiv'=>'Sections aligned with the GTK settings window: Base, Advanced, Anti-Ransomware, Anti-Phishing, whitelist, services, email, ClamAV DB, payments and Web options.','anti_phishing'=>'Anti-Phishing','advanced'=>'Advanced','advanced_antiphishing'=>'Advanced Anti-Phishing','advanced_antiransomware'=>'Advanced Anti-Ransomware','whitelist'=>'Whitelist','system_user_services'=>'System and user services','google_safe_key'=>'Google Safe Browsing API key','auto_blacklist_update'=>'Automatically update blacklist every 2 hours','update_blacklist'=>'Update blacklist','loaded_blacklists'=>'Loaded blacklists','firewall_ip_block'=>'Automatic firewall IP blocking','enable_firewall_ip_block'=>'Enable automatic IP blocking','configure_firewall_ip_block'=>'Configure firewall IP blocking','update_yara'=>'Update YARA rules','update_sanesecurity'=>'Update Sanesecurity DB','scanner_configuration'=>'Scanner configuration','use_yara_rules'=>'Use YARA rules','use_sanesecurity_db'=>'Use Sanesecurity DB','scan_interval_minutes'=>'Scan interval (min)','folders_to_scan'=>'Folders to scan','ignore_paths'=>'Paths to ignore','ignored_extensions'=>'Ignored extensions','suspicious_extensions'=>'Suspicious extensions','save_scanner_configuration'=>'Save scanner configuration','loaded_yara_rules'=>'Loaded YARA rules','config_write_denied'=>'Permission denied: only WebPanel administrators can modify configuration files.','email_address'=>'Email address','admin_email_hint'=>'Optionally used to send malware/ransomware notifications to administrators.','invalid_email_address'=>'Invalid email address.','server_alerts'=>'Server alerts','server_alert_email'=>'Server alert email','smtp_host'=>'SMTP host','smtp_port'=>'SMTP port','smtp_security'=>'SMTP security','smtp_username'=>'SMTP username','smtp_password'=>'SMTP password','alert_recipients'=>'Alert recipients','use_admin_emails'=>'Use admin emails','send_test_email'=>'Send test email','email_alerts_saved'=>'Alert email configuration saved.','malware_detected'=>'Malware detected','ransomware_detected'=>'Ransomware detected','quarantine_action'=>'Quarantine','ignore_action'=>'Ignore','inotify_monitor'=>'Realtime inotify monitor','start_monitor'=>'Start monitor','stop_monitor'=>'Stop monitor','live_events'=>'Live events','samba_scan_helper'=>'Samba scan through helper',
],
];
$extra = [
'it' => [
'auth_welcome_to' => 'Benvenuto in',
'auth_webpanel_bastionguard' => 'WebPanel BastionGuard',
'auth_tagline_trusted' => 'Accesso sicuro. Controllo affidabile.',
'auth_tagline_total' => 'Accesso sicuro. Controllo totale.',
'auth_login_title' => 'Login WebPanel',
'auth_login_page_title' => 'Login BastionGuard WebPanel',
'auth_setup_page_title' => 'Setup BastionGuard WebPanel',
'auth_signin_title' => 'BastionGuard WebPanel',
'auth_signin_subtitle' => 'Accedi per continuare',
'auth_email_or_username' => 'E-mail o nome utente',
'auth_password' => 'Password',
'auth_show_password' => 'Mostra password',
'auth_hide_password' => 'Nascondi password',
'auth_sign_in' => 'Accedi →',
'auth_invalid_credentials' => 'Credenziali non valide.',
'auth_error' => 'Errore di autenticazione: %message%',
'setup_title' => 'Setup BastionGuard',
'setup_subtitle' => 'Configurazione iniziale WebPanel',
'setup_user_backend' => 'Backend utenti',
'setup_local_sqlite' => 'SQLite locale',
'setup_mysql_mariadb' => 'MySQL / MariaDB',
'setup_postgresql' => 'PostgreSQL / Postgres',
'setup_db_port' => 'Porta DB',
'setup_db_maintenance' => 'Database manutenzione',
'setup_db_maintenance_hint' => 'Usato solo per creare o verificare il database PostgreSQL. Di solito: postgres.',
'setup_step_database_ui' => 'Database',
'setup_step_admin_ui' => 'Account admin',
'setup_step_complete_ui' => 'Completa',
'setup_database_title' => 'Step 1 · Database utenti',
'setup_database_subtitle' => 'Scegli SQLite, MySQL/MariaDB o PostgreSQL. Il setup verifica la connessione prima di proseguire.',
'setup_admin_title' => 'Step 2 · Account amministratore',
'setup_admin_subtitle' => 'Crea l’utente amministratore WebPanel e completa il setup.',
'setup_database_continue' => 'Avanti →',
'setup_database_validated' => 'Database verificato correttamente. Ora crea l’account amministratore.',
'setup_back_database' => '← Modifica database',
'setup_complete_set' => 'Completa setup',
'setup_database_driver_summary' => 'Database selezionato',
'setup_sqlite_note' => 'SQLite salva gli utenti in un database locale della WebUI. Non richiede server esterni.',
'setup_sqlite_unavailable' => 'Estensione PHP PDO SQLite non disponibile.',
'setup_sqlite_dir_error' => 'Impossibile creare la cartella db locale.',
'setup_sqlite_dir_not_writable' => 'La cartella db locale non è scrivibile dal processo web.',
'setup_mysql_unavailable' => 'Estensione PHP PDO MySQL non disponibile.',
'setup_pgsql_unavailable' => 'Estensione PHP PDO PostgreSQL non disponibile.',
'setup_pgsql_connect_created_error' => 'Database PostgreSQL creato o trovato, ma connessione al database target fallita: %message%',
'setup_pgsql_connect_error' => 'Connessione PostgreSQL fallita. Crea il database manualmente o usa un ruolo con permessi CREATEDB: %message%',
'setup_database_error' => 'Errore database: %message%',
'setup_database_required' => 'Completa prima lo step database.',
'setup_db_host' => 'Host DB',
'setup_db_name' => 'Nome DB',
'setup_db_user' => 'Utente DB',
'setup_db_password' => 'Password DB',
'setup_web_server_user' => 'Utente web server per sudoers',
'setup_examples' => 'Esempi: www-data, http, apache, nginx.',
'setup_admin_username' => 'Nome utente admin',
'setup_confirm_password' => 'Conferma',
'setup_info_note' => 'Il wizard crea la configurazione dell’amministratore WebPanel. Il file config.json dell’applicazione BastionGuard viene gestito dalla pagina Wizard BastionGuard e viene creato solo se manca.',
'setup_complete' => 'Completa setup →',
'setup_go_dashboard' => 'Vai alla dashboard →',
'setup_validation_admin_fields' => 'Compila i campi admin e conferma la stessa password.',
'setup_validation_password_len' => 'Usa una password di almeno 8 caratteri.',
'setup_missing_db_user' => 'Utente database mancante.',
'setup_write_config_error' => 'Impossibile scrivere includes/webui_config.php.',
'setup_success_config_created' => 'Configurazione creata e utente amministratore iniziale salvato.',
'setup_success_sudoers_generated' => 'File sudoers locale generato: %path%. Installalo con visudo se vuoi usare le azioni di sistema dalla UI.',
'setup_error' => 'Errore setup: %message%',
'setup_desktop_user' => 'Utente desktop BastionGuard',
'setup_desktop_user_hint' => 'Utente proprietario di ~/.config/BastionGuard. Verrà usato dal wizard dopo il setup.',
'wizard_page_title' => 'Wizard BastionGuard WebPanel',
'wizard_title' => 'BastionGuard Setup',
'wizard_subtitle' => 'Wizard di configurazione iniziale',
'wizard_desktop_user' => 'Utente desktop',
'wizard_desktop_user_hint' => 'Utente proprietario della cartella ~/.config/BastionGuard.',
'wizard_config_folder' => 'Cartella configurazione',
'wizard_completed' => 'completato',
'missing' => 'manca',
'invalid' => 'non valido',
'incomplete' => 'incompleto',
'wizard_info_note' => 'Il wizard crea config.json solo se manca. Se trova configurazioni desktop esistenti, non modifica nulla.',
'wizard_services_title' => 'Servizi di protezione',
'wizard_source_steps_title' => 'Passaggi del wizard desktop',
'wizard_run_system_steps' => 'Esegui anche i passaggi di sistema del wizard desktop',
'wizard_apply_services' => 'Dopo la scrittura di config.json, abilita e avvia i servizi selezionati',
'wizard_start_setup' => 'Avvia setup →',
'wizard_minimal' => 'Configurazione minima',
'wizard_enable_all' => 'Attiva tutto',
'wizard_http_port' => 'Porta HTTP',
'wizard_https_port' => 'Porta HTTPS',
'wizard_missing_desktop_user' => 'Imposta l’utente desktop prima di avviare il wizard.',
'wizard_already_completed_no_changes' => 'Wizard già completato: nessuna modifica eseguita.',
'wizard_config_created' => 'config.json creato o aggiornato correttamente.',
'wizard_config_error' => 'Errore durante la scrittura di config.json.',
'wizard_services_log_title' => 'Servizi selezionati',
'wizard_system_log_title' => 'Passaggi sistema',
'wizard_system_completed' => 'Passaggi di sistema completati.',
'wizard_system_error' => 'Errore durante i passaggi di sistema.',
'wizard_already_completed_desc' => 'config.json è presente, valido e contiene wizard_completed=true con la mappa dei servizi. Per sicurezza la WebUI non modifica nulla.',
'wizard_go_dashboard' => 'Vai alla dashboard →',
'wizard_open_configurations' => 'Apri configurazioni',
'wizard_show_config_json' => 'Mostra config.json',
'wizard_log' => 'Log wizard',
'wizard_step_resolver' => 'Resolver locale',
'wizard_step_resolver_desc' => 'Configura le policy DNS/DoH del browser verso il resolver locale BastionGuard.',
'wizard_step_dnsmasq' => 'DNSMasq',
'wizard_step_dnsmasq_desc' => 'Crea /etc/dnsmasq.d/BastionGuard.conf e abilita conf-dir quando possibile.',
'wizard_step_firewall' => 'Firewall',
'wizard_step_firewall_desc' => 'Apre le porte richieste da BastionGuard su UFW/firewalld dove disponibili.',
'wizard_step_nftables' => 'Firewall nftables avanzato',
'wizard_step_nftables_desc' => 'Installa o configura regole nftables di base quando non c’è un firewall attivo.',
'wizard_step_certs' => 'Certificati CA',
'wizard_step_certs_desc' => 'Genera la CA locale BastionGuard e la installa nel trust store di sistema.',
'wizard_step_webserver' => 'Webserver',
'wizard_step_webserver_desc' => 'Scrive /etc/BastionGuard/webports.conf e prova ad aggiornare il virtual host NGINX.',
'wizard_step_native' => 'Native Messaging Host',
'wizard_step_native_desc' => 'Installa i manifest Native Host per Firefox, Chromium, Chrome ed Edge.',
'wizard_step_useragent' => 'Servizio User Agent',
'wizard_step_useragent_desc' => 'Chiama lo script enable-user-agents.sh se è installato.',
'wizard_step_banks' => 'Lista banche MISP',
'wizard_step_banks_desc' => 'Scarica la lista banche in ~/.local/share/BastionGuard/banks.json.',
'wizard_step_services' => 'Servizi BastionGuard',
'wizard_step_services_desc' => 'Abilita e avvia i servizi scelti nella configurazione iniziale.',
'auth_language_switch' => 'Lingua',
'auth_theme_switch' => 'Tema',
'desktop_arbitration' => 'Arbitraggio desktop',
'setup_source_wizard_title' => 'Setup BastionGuard come wizard GTK/src',
'setup_run_source_wizard' => 'Esegui anche il wizard BastionGuard equivalente a src/wizard',
'desktop_gui_active_webui_no_proxy' => 'GUI attiva: la WebUI non gestisce proxy/PAC/CEF; tutto resta alla UI GTK.',
'desktop_headless_webui_setup' => 'Nessuna GUI rilevata: la WebUI può completare il setup headless, ma CEF/PAC restano disabilitati nella WebUI.',
'cef_pac_excluded_webui' => 'CEF/PAC esclusi dalla WebUI: usa la GUI GTK.',
'gtk_none_selected' => 'Come GTK: nessuno selezionato',
'wizard_step_environment_ui' => 'Ambiente',
'wizard_step_services_ui' => 'Servizi',
'wizard_step_system_ui' => 'Passaggi sistema',
'wizard_step_review_ui' => 'Riepilogo',
'wizard_next' => 'Avanti →',
'wizard_back' => '← Indietro',
'wizard_review_title' => 'Riepilogo prima dell’esecuzione',
'wizard_review_desc' => 'Controlla utente, porte, servizi e passaggi selezionati. L’esecuzione parte solo dall’ultimo step.',
'wizard_locked_title' => 'Wizard bloccato: configurazione desktop rilevata',
'wizard_locked_desc' => 'La WebUI passa in sola lettura per il wizard perché ha trovato configurazioni BastionGuard già presenti nel profilo desktop. Usa la GUI GTK per modificare quel profilo oppure rimuovi consapevolmente le configurazioni desktop prima di usare il wizard WebUI.',
'wizard_lock_reason_config_json' => 'config.json desktop già presente',
'wizard_lock_reason_config_dir' => 'cartella ~/.config/BastionGuard già popolata',
'wizard_locked_no_post' => 'Wizard bloccato: configurazioni desktop rilevate. Nessuna modifica eseguita.',
'wizard_locked_open_settings' => 'Apri impostazioni',
'wizard_locked_use_gtk' => 'Usa la GUI GTK per questa installazione desktop.',
'setup_source_wizard_locked' => 'Wizard BastionGuard non configurabile da setup: configurazioni desktop rilevate.',
'setup_success_next_wizard' => 'Setup WebPanel completato. Se non esistono configurazioni desktop, partirà il wizard BastionGuard a step.',
'wizard_started_after_setup' => 'Setup WebPanel completato: nessuna configurazione desktop trovata, avvio wizard BastionGuard.',
'wizard_config_file_title' => 'config.json / servizi first-run',
'wizard_no_system_steps_selected' => 'Nessun passaggio sistema selezionato: eseguita solo configurazione servizi.',
'wizard_system_skipped' => 'Passaggi sistema saltati: nessuno selezionato.',
'wizard_step_welcome_ui' => 'Benvenuto',
'wizard_step_nftables_short' => 'nftables',
'wizard_welcome_desc' => 'Questo wizard replica il flusso GTK/src: si naviga passo per passo, si selezionano servizi e azioni, poi l’esecuzione avviene solo nel riepilogo finale tramite helper e script privilegiati.',
'wizard_services_desc_gtk' => 'Come nella finestra first-run GTK, scegli i servizi BastionGuard da attivare. CEF e PAC restano esclusi dalla WebUI.',
'wizard_services_apply_final' => 'I servizi verranno abilitati o disabilitati solo alla fine del wizard, insieme alla scrittura di config.json.',
'wizard_enable_this_step' => 'Esegui questo passaggio alla fine',
'wizard_review_desc_gtk' => 'Riepilogo finale. Premendo Avvia setup la WebUI scrive config.json, applica i servizi e richiama gli helper/script per i passaggi selezionati.',
'wizard_final_execution_note' => 'L’esecuzione parte solo da questo ultimo step e passa attraverso gli helper installati.',
'wizard_review_selected_services' => 'Servizi selezionati',
'wizard_review_selected_steps' => 'Passaggi selezionati',
'scan_page_subtitle' => 'Scansione manuale, stato ClamAV OnAccess e monitor realtime root del WebPanel per malware e ransomware.',
'realtime_active' => 'Realtime attivo',
'realtime_stopped' => 'Realtime fermo',
'sudoers' => 'Sudoers',
'install_update_helpers_with' => 'installa/aggiorna gli helper con',
'scan_uses' => 'La scansione usa',
'realtime_monitor_runs_root' => 'il monitor realtime gira come root tramite',
'and_uses' => 'e usa',
'when_installed' => 'quando installato',
'manual_scan' => 'Scansione manuale',
'local_path_to_scan' => 'Percorso locale da scansionare',
'or' => 'oppure',
'start_scan' => 'Avvia scansione',
'paths_allowed_by_helper' => 'Percorsi consentiti dall’helper',
'automatic_scan' => 'Scansione automatica',
'enable_automatic_scanning_events' => 'Abilita la scansione automatica per gli eventi monitorati',
'malware_bazaar_api_key' => 'API key VirusTotal',
'optional' => 'opzionale',
'save_settings' => 'Salva impostazioni',
'detected_onaccess_paths' => 'Percorsi OnAccess rilevati',
'no_onaccess_path' => 'Nessun percorso OnAccess in clamd.conf.',
'status' => 'Stato',
'watch_paths_one_per_line' => 'Percorsi da monitorare, uno per riga',
'requires' => 'Richiede',
'events_scanned_immediately' => 'Gli eventi vengono scansionati subito con ClamAV e il rilevamento ransomware BastionGuard; la WebUI importa alert da',
'per_file_ransomware_scanner_when_available' => 'ed esegue anche lo scanner anti-ransomware per-file quando disponibile.',
'recommended_monitored_roots_include' => 'Le root consigliate per il monitoraggio includono',
'user_web_roots_under' => 'root web utente sotto',
'webserver_roots_such_as' => 'e root del webserver come',
'malware_ransomware_create_alerts' => 'Le rilevazioni malware e ransomware creano notifiche WebPanel e alert email quando configurati.',
'keep_monitor_active_after_logout' => 'Per mantenere il monitor attivo dopo il logout, abilita',
'auto_refresh' => 'aggiornamento automatico',
'no_realtime_events_yet' => 'Nessun evento realtime ancora.',
'scan_result' => 'Risultato scansione',
'command_executed' => 'Comando eseguito.',
'no_path_or_file_specified' => 'Nessun percorso o file specificato.',
'exit_code' => 'Codice uscita',
'scan_settings_saved' => 'Impostazioni scansione salvate nei file BastionGuard.',
'operation_log' => 'Log operazione',
'recommended_preset' => 'Preset consigliato',
],
'en' => [
'scan_page_subtitle' => 'Manual scan, ClamAV OnAccess status and root WebPanel realtime monitor for malware and ransomware.',
'realtime_active' => 'Realtime active',
'realtime_stopped' => 'Realtime stopped',
'sudoers' => 'Sudoers',
'install_update_helpers_with' => 'install/update the helpers with',
'scan_uses' => 'The scan uses',
'realtime_monitor_runs_root' => 'the realtime monitor runs as root through',
'and_uses' => 'and uses',
'when_installed' => 'when installed',
'manual_scan' => 'Manual scan',
'local_path_to_scan' => 'Local path to scan',
'or' => 'or',
'start_scan' => 'Start scan',
'paths_allowed_by_helper' => 'Paths allowed by the helper',
'automatic_scan' => 'Automatic scan',
'enable_automatic_scanning_events' => 'Enable automatic scanning for monitored events',
'malware_bazaar_api_key' => 'VirusTotal API key',
'optional' => 'optional',
'save_settings' => 'Save settings',
'detected_onaccess_paths' => 'Detected OnAccess paths',
'no_onaccess_path' => 'No OnAccess path in clamd.conf.',
'status' => 'Status',
'watch_paths_one_per_line' => 'Watch paths, one per line',
'requires' => 'Requires',
'events_scanned_immediately' => 'Events are scanned immediately with ClamAV and BastionGuard ransomware detection; the WebUI imports alerts from',
'per_file_ransomware_scanner_when_available' => 'and also runs the per-file anti-ransomware scanner when available.',
'recommended_monitored_roots_include' => 'Recommended monitored roots include',
'user_web_roots_under' => 'user web roots under',
'webserver_roots_such_as' => 'and web-server roots such as',
'malware_ransomware_create_alerts' => 'Malware and ransomware detections create WebPanel toasts and email alerts when configured.',
'keep_monitor_active_after_logout' => 'To keep the monitor active after logout, enable',
'auto_refresh' => 'auto refresh',
'no_realtime_events_yet' => 'No realtime events yet.',
'scan_result' => 'Scan result',
'command_executed' => 'Command executed.',
'no_path_or_file_specified' => 'No path or file specified.',
'exit_code' => 'Exit code',
'scan_settings_saved' => 'Scan settings saved in BastionGuard files.',
'auth_welcome_to' => 'Welcome to',
'auth_webpanel_bastionguard' => 'WebPanel BastionGuard',
'auth_tagline_trusted' => 'Secure access. Trusted control.',
'auth_tagline_total' => 'Secure access. Total control.',
'auth_login_title' => 'WebPanel Login',
'auth_login_page_title' => 'BastionGuard WebPanel Login',
'auth_setup_page_title' => 'BastionGuard WebPanel Setup',
'auth_signin_title' => 'BastionGuard WebPanel',
'auth_signin_subtitle' => 'Sign in to continue',
'auth_email_or_username' => 'E-mail or username',
'auth_password' => 'Password',
'auth_show_password' => 'Show password',
'auth_hide_password' => 'Hide password',
'auth_sign_in' => 'Sign in →',
'auth_invalid_credentials' => 'Invalid credentials.',
'auth_error' => 'Authentication error: %message%',
'setup_title' => 'BastionGuard Setup',
'setup_subtitle' => 'Initial WebPanel configuration',
'setup_user_backend' => 'User backend',
'setup_local_sqlite' => 'Local SQLite',
'setup_mysql_mariadb' => 'MySQL / MariaDB',
'setup_postgresql' => 'PostgreSQL / Postgres',
'setup_db_port' => 'DB port',
'setup_db_maintenance' => 'Maintenance database',
'setup_db_maintenance_hint' => 'Used only to create or verify the PostgreSQL database. Usually: postgres.',
'setup_step_database_ui' => 'Database',
'setup_step_admin_ui' => 'Admin account',
'setup_step_complete_ui' => 'Complete',
'setup_database_title' => 'Step 1 · User database',
'setup_database_subtitle' => 'Choose SQLite, MySQL/MariaDB or PostgreSQL. Setup verifies the connection before continuing.',
'setup_admin_title' => 'Step 2 · Administrator account',
'setup_admin_subtitle' => 'Create the WebPanel administrator user and complete setup.',
'setup_database_continue' => 'Next →',
'setup_database_validated' => 'Database verified successfully. Now create the administrator account.',
'setup_back_database' => '← Change database',
'setup_complete_set' => 'Complete setup',
'setup_database_driver_summary' => 'Selected database',
'setup_sqlite_note' => 'SQLite stores users in a local WebUI database. It does not require an external server.',
'setup_sqlite_unavailable' => 'PHP PDO SQLite extension is not available.',
'setup_sqlite_dir_error' => 'Unable to create the local db directory.',
'setup_sqlite_dir_not_writable' => 'The local db directory is not writable by the web process.',
'setup_mysql_unavailable' => 'PHP PDO MySQL extension is not available.',
'setup_pgsql_unavailable' => 'PHP PDO PostgreSQL extension is not available.',
'setup_pgsql_connect_created_error' => 'PostgreSQL database was created or found, but connecting to the target database failed: %message%',
'setup_pgsql_connect_error' => 'PostgreSQL connection failed. Create the database manually or use a role with CREATEDB permissions: %message%',
'setup_database_error' => 'Database error: %message%',
'setup_database_required' => 'Complete the database step first.',
'setup_db_host' => 'DB host',
'setup_db_name' => 'DB name',
'setup_db_user' => 'DB user',
'setup_db_password' => 'DB password',
'setup_web_server_user' => 'Web server user for sudoers',
'setup_examples' => 'Examples: www-data, http, apache, nginx.',
'setup_admin_username' => 'Admin username',
'setup_confirm_password' => 'Confirm',
'setup_info_note' => 'The wizard creates the WebPanel administrator configuration. BastionGuard application config.json is managed by the BastionGuard wizard page and is only created if missing.',
'setup_complete' => 'Complete setup →',
'setup_go_dashboard' => 'Go to dashboard →',
'setup_validation_admin_fields' => 'Fill in the admin fields and confirm the same password.',
'setup_validation_password_len' => 'Use a password of at least 8 characters.',
'setup_missing_db_user' => 'Missing database user.',
'setup_write_config_error' => 'Unable to write includes/webui_config.php.',
'setup_success_config_created' => 'Configuration created and initial administrator user saved.',
'setup_success_sudoers_generated' => 'Local sudoers file generated: %path%. Install it with visudo if you want to use system actions from the UI.',
'setup_error' => 'Setup error: %message%',
'setup_desktop_user' => 'BastionGuard desktop user',
'setup_desktop_user_hint' => 'User that owns ~/.config/BastionGuard. It will be used by the wizard after setup.',
'wizard_page_title' => 'BastionGuard WebPanel Wizard',
'wizard_title' => 'BastionGuard Setup',
'wizard_subtitle' => 'Initial configuration wizard',
'wizard_desktop_user' => 'Desktop user',
'wizard_desktop_user_hint' => 'User that owns the ~/.config/BastionGuard directory.',
'wizard_config_folder' => 'Configuration folder',
'wizard_completed' => 'completed',
'missing' => 'missing',
'invalid' => 'invalid',
'incomplete' => 'incomplete',
'wizard_info_note' => 'The wizard creates config.json only when it is missing. If existing desktop configuration is found, nothing is changed.',
'wizard_services_title' => 'Protection services',
'wizard_source_steps_title' => 'Desktop wizard steps',
'wizard_run_system_steps' => 'Also run the desktop wizard system steps',
'wizard_apply_services' => 'After writing config.json, enable and start the selected services',
'wizard_start_setup' => 'Start setup →',
'wizard_minimal' => 'Minimal configuration',
'wizard_enable_all' => 'Enable all',
'wizard_http_port' => 'HTTP port',
'wizard_https_port' => 'HTTPS port',
'wizard_missing_desktop_user' => 'Set the desktop user before starting the wizard.',
'wizard_already_completed_no_changes' => 'Wizard already completed: no changes were made.',
'wizard_config_created' => 'config.json created or updated successfully.',
'wizard_config_error' => 'Error while writing config.json.',
'wizard_services_log_title' => 'Selected services',
'wizard_system_log_title' => 'System steps',
'wizard_system_completed' => 'System steps completed.',
'wizard_system_error' => 'Error while running system steps.',
'wizard_already_completed_desc' => 'config.json is present, valid and contains wizard_completed=true with the service map. For safety, the WebUI does not change anything.',
'wizard_go_dashboard' => 'Go to dashboard →',
'wizard_open_configurations' => 'Open configurations',
'wizard_show_config_json' => 'Show config.json',
'wizard_log' => 'Wizard log',
'wizard_step_resolver' => 'Local resolver',
'wizard_step_resolver_desc' => 'Configures browser DNS/DoH policies toward the local BastionGuard resolver.',
'wizard_step_dnsmasq' => 'DNSMasq',
'wizard_step_dnsmasq_desc' => 'Creates /etc/dnsmasq.d/BastionGuard.conf and enables conf-dir when possible.',
'wizard_step_firewall' => 'Firewall',
'wizard_step_firewall_desc' => 'Opens the ports required by BastionGuard on UFW/firewalld when available.',
'wizard_step_nftables' => 'Advanced nftables firewall',
'wizard_step_nftables_desc' => 'Installs or configures basic nftables rules when no firewall is active.',
'wizard_step_certs' => 'CA certificates',
'wizard_step_certs_desc' => 'Generates the local BastionGuard CA and installs it into the system trust store.',
'wizard_step_webserver' => 'Webserver',
'wizard_step_webserver_desc' => 'Writes /etc/BastionGuard/webports.conf and tries to update the NGINX virtual host.',
'wizard_step_native' => 'Native Messaging Host',
'wizard_step_native_desc' => 'Installs Native Host manifests for Firefox, Chromium, Chrome and Edge.',
'wizard_step_useragent' => 'User Agent service',
'wizard_step_useragent_desc' => 'Calls enable-user-agents.sh when it is installed.',
'wizard_step_banks' => 'MISP bank list',
'wizard_step_banks_desc' => 'Downloads the bank list to ~/.local/share/BastionGuard/banks.json.',
'wizard_step_services' => 'BastionGuard services',
'wizard_step_services_desc' => 'Enables and starts the services selected in the initial configuration.',
'auth_language_switch' => 'Language',
'auth_theme_switch' => 'Theme',
'desktop_arbitration' => 'Desktop arbitration',
'setup_source_wizard_title' => 'BastionGuard setup like the GTK/src wizard',
'setup_run_source_wizard' => 'Also run the BastionGuard wizard equivalent to src/wizard',
'desktop_gui_active_webui_no_proxy' => 'GUI active: the WebUI does not manage proxy/PAC/CEF; everything remains under the GTK UI.',
'desktop_headless_webui_setup' => 'No GUI detected: the WebUI can complete the headless setup, but CEF/PAC remain disabled in the WebUI.',
'cef_pac_excluded_webui' => 'CEF/PAC excluded from the WebUI: use the GTK UI.',
'gtk_none_selected' => 'Like GTK: none selected',
'wizard_step_environment_ui' => 'Environment',
'wizard_step_services_ui' => 'Services',
'wizard_step_system_ui' => 'System steps',
'wizard_step_review_ui' => 'Review',
'wizard_next' => 'Next →',
'wizard_back' => '← Back',
'wizard_review_title' => 'Review before running',
'wizard_review_desc' => 'Check the user, ports, services and selected steps. Execution starts only from the last step.',
'wizard_locked_title' => 'Wizard locked: desktop configuration detected',
'wizard_locked_desc' => 'The WebUI switches the wizard to read-only because it found existing BastionGuard configuration in the desktop profile. Use the GTK UI to modify that profile, or intentionally remove the desktop configuration before using the WebUI wizard.',
'wizard_lock_reason_config_json' => 'desktop config.json already exists',
'wizard_lock_reason_config_dir' => '~/.config/BastionGuard already contains files',
'wizard_locked_no_post' => 'Wizard locked: desktop configuration detected. No changes were made.',
'wizard_locked_open_settings' => 'Open settings',
'wizard_locked_use_gtk' => 'Use the GTK UI for this desktop installation.',
'setup_source_wizard_locked' => 'BastionGuard wizard is not configurable from setup: desktop configuration detected.',
'setup_success_next_wizard' => 'WebPanel setup completed. If no desktop configuration exists, the step-by-step BastionGuard wizard will start.',
'wizard_started_after_setup' => 'WebPanel setup completed: no desktop configuration found, starting the BastionGuard wizard.',
'wizard_config_file_title' => 'config.json / first-run services',
'wizard_no_system_steps_selected' => 'No system step selected: only service configuration was applied.',
'wizard_system_skipped' => 'System steps skipped: none selected.',
'wizard_step_welcome_ui' => 'Welcome',
'wizard_step_nftables_short' => 'nftables',
'wizard_welcome_desc' => 'This wizard mirrors the GTK/src flow: navigate step by step, select services and actions, then execution happens only in the final review through privileged helpers and scripts.',
'wizard_services_desc_gtk' => 'As in the GTK first-run window, choose which BastionGuard services to activate. CEF and PAC remain excluded from the WebUI.',
'wizard_services_apply_final' => 'Services will be enabled or disabled only at the end of the wizard, together with writing config.json.',
'wizard_enable_this_step' => 'Run this step at the end',
'wizard_review_desc_gtk' => 'Final review. Press Start setup to write config.json, apply services and call helpers/scripts for the selected steps.',
'wizard_final_execution_note' => 'Execution starts only from this final step and goes through the installed helpers.',
'wizard_review_selected_services' => 'Selected services',
'wizard_review_selected_steps' => 'Selected steps',
'operation_log' => 'Operation log',
'recommended_preset' => 'Recommended preset',
],
];
foreach ($extra as $extraLang => $extraPairs) {
$dict[$extraLang] = array_merge($dict[$extraLang] ?? [], $extraPairs);
}
$dashboardI18n = [
'it' => [
'uid' => 'UID',
'dbus' => 'D-Bus',
'helper' => 'Helper',
'open_settings' => 'Apri impostazioni',
'dashboard_helper_missing_hint' => 'Per leggere e gestire sempre i servizi --user da Apache/PHP-FPM installa scripts/install-user-service-helper.sh.',
'dashboard_desktop_user_missing' => 'Utente desktop per i servizi systemctl --user non rilevato. Impostalo in Impostazioni → Opzioni Web.',
'dashboard_service_detection_hint' => 'La WebUI prova prima systemd, poi l’helper sicuro e infine il rilevamento processo per i servizi --user.',
'scope' => 'Ambito',
'scope_user' => 'utente',
'scope_system' => 'sistema',
'enabled' => 'Abilitazione',
'read' => 'Lettura',
'sub_state' => 'Sottostato',
'pid' => 'PID',
'file' => 'File',
'version' => 'Versione',
'unit_not_loaded_or_accessible' => 'unità non caricata o non accessibile',
'clamav_db_not_found' => 'Nessun database trovato in /var/lib/clamav.',
'onaccess_paths_not_found' => 'Nessun percorso OnAccess rilevato nelle posizioni clamd.conf note.',
'svc_clamd' => 'Demone ClamAV',
'svc_clamonacc' => 'Realtime ClamAV',
'svc_freshclam' => 'Freshclam',
'svc_phishing_scanner' => 'Scanner phishing',
'svc_ransomware_realtime' => 'Realtime ransomware',
'svc_ransomware_alert' => 'Avvisi ransomware',
'svc_ransomware_realtime_alert' => 'Avvisi realtime ransomware',
'svc_ransomware_scanner' => 'Scanner ransomware',
'svc_useragent' => 'Randomizzatore User-Agent',
'svc_mail' => 'Proxy mail',
'access_systemd' => 'systemd',
'access_helper' => 'helper',
'access_admin_helper' => 'helper amministrativo',
'access_direct' => 'lettura diretta',
'access_process' => 'rilevamento processo',
'systemd_active' => 'attivo',
'systemd_inactive' => 'inattivo',
'systemd_failed' => 'fallito',
'systemd_activating' => 'in avvio',
'systemd_deactivating' => 'in arresto',
'systemd_running' => 'in esecuzione',
'systemd_dead' => 'fermo',
'systemd_exited' => 'uscito',
'systemd_loaded' => 'caricato',
'systemd_not_found' => 'non trovato',
'systemd_unknown' => 'sconosciuto',
'systemd_enabled' => 'abilitato',
'systemd_disabled' => 'disabilitato',
'systemd_static' => 'statico',
'systemd_indirect' => 'indiretto',
'systemd_generated' => 'generato',
'systemd_masked' => 'mascherato',
'systemd_bad' => 'non valido',
'systemd_process_detected' => 'processo rilevato',
'service_action_not_allowed' => 'Azione servizio non consentita.',
'service_invalid' => 'Servizio non valido.',
'webui_disabled_service_msg' => 'BastionGuard WebUI: azione bloccata su %unit%. CEF/PAC, Webcam/Privacy e USB sono esclusi dalla WebUI server perché sono componenti desktop o hardware-locali. Usa la GUI GTK oppure gestiscili manualmente fuori dalla WebUI.',
'webpanel_identity' => 'Identità WebPanel',
'webpanel_display_name' => 'Nome visualizzato WebPanel',
'webpanel_display_name_hint' => 'Modifica solo il nome del WebPanel mostrato nel login e nella barra laterale. Logo e nome BastionGuard restano fissi e non sono modificabili.',
'dashboard_visibility' => 'Visibilità dashboard',
'dashboard_visibility_hint' => 'Scegli quali sezioni mostrare nella dashboard. Le sezioni nascoste non vengono eliminate e possono essere riattivate in qualsiasi momento.',
'dash_widget_user_services' => 'Diagnostica servizi utente',
'dash_widget_service_cards' => 'Schede riepilogo servizi',
'dash_widget_service_table' => 'Tabella servizi rilevati',
'dash_widget_clamav_db' => 'Database firme ClamAV',
'dash_widget_realtime_paths' => 'Percorsi realtime ClamAV',
'dash_widget_recent_events' => 'Eventi recenti',
'web_options_saved' => 'Opzioni WebPanel salvate.',
],
'en' => [
'uid' => 'UID',
'dbus' => 'D-Bus',
'helper' => 'Helper',
'open_settings' => 'Open settings',
'dashboard_helper_missing_hint' => 'To always read and manage --user services from Apache/PHP-FPM, install scripts/install-user-service-helper.sh.',
'dashboard_desktop_user_missing' => 'Desktop user for systemctl --user services was not detected. Set it in Settings → Web Options.',
'dashboard_service_detection_hint' => 'The WebUI tries systemd first, then the safe helper, then process detection for --user services.',
'scope' => 'Scope',
'scope_user' => 'user',
'scope_system' => 'system',
'enabled' => 'Enabled',
'read' => 'Read',
'sub_state' => 'SubState',
'pid' => 'PID',
'file' => 'File',
'version' => 'Version',
'unit_not_loaded_or_accessible' => 'unit not loaded or not accessible',
'clamav_db_not_found' => 'No database found in /var/lib/clamav.',
'onaccess_paths_not_found' => 'No OnAccess path detected in known clamd.conf locations.',
'svc_clamd' => 'ClamAV daemon',
'svc_clamonacc' => 'ClamAV realtime',
'svc_freshclam' => 'Freshclam',
'svc_phishing_scanner' => 'Phishing scanner',
'svc_ransomware_realtime' => 'Ransomware realtime',
'svc_ransomware_alert' => 'Ransomware alerts',
'svc_ransomware_realtime_alert' => 'Ransomware realtime alerts',
'svc_ransomware_scanner' => 'Ransomware scanner',
'svc_useragent' => 'User-Agent randomizer',
'svc_mail' => 'Mail proxy',
'access_systemd' => 'systemd',
'access_helper' => 'helper',
'access_admin_helper' => 'admin helper',
'access_direct' => 'direct read',
'access_process' => 'process detection',
'systemd_active' => 'active',
'systemd_inactive' => 'inactive',
'systemd_failed' => 'failed',
'systemd_activating' => 'activating',
'systemd_deactivating' => 'deactivating',
'systemd_running' => 'running',
'systemd_dead' => 'dead',
'systemd_exited' => 'exited',
'systemd_loaded' => 'loaded',
'systemd_not_found' => 'not found',
'systemd_unknown' => 'unknown',
'systemd_enabled' => 'enabled',
'systemd_disabled' => 'disabled',
'systemd_static' => 'static',
'systemd_indirect' => 'indirect',
'systemd_generated' => 'generated',
'systemd_masked' => 'masked',
'systemd_bad' => 'bad',
'systemd_process_detected' => 'process detected',
'service_action_not_allowed' => 'Service action not allowed.',
'service_invalid' => 'Invalid service.',
'webui_disabled_service_msg' => 'BastionGuard WebUI: action blocked on %unit%. CEF/PAC, Webcam/Privacy and USB are excluded from the server WebUI because they are desktop or local-hardware components. Use the GTK UI or manage them manually outside the WebUI.',
'webpanel_identity' => 'WebPanel identity',
'webpanel_display_name' => 'WebPanel display name',
'webpanel_display_name_hint' => 'Changes only the WebPanel name shown on the login screen and sidebar. The BastionGuard logo and BastionGuard name stay fixed and cannot be edited.',
'dashboard_visibility' => 'Dashboard visibility',
'dashboard_visibility_hint' => 'Choose which sections are shown on the dashboard. Hidden sections are not deleted and can be enabled again at any time.',
'dash_widget_user_services' => 'User services diagnostics',
'dash_widget_service_cards' => 'Service summary cards',
'dash_widget_service_table' => 'Detected services table',
'dash_widget_clamav_db' => 'ClamAV signature database',
'dash_widget_realtime_paths' => 'ClamAV realtime paths',
'dash_widget_recent_events' => 'Recent events',
'web_options_saved' => 'WebPanel options saved.',
],
];
foreach ($dashboardI18n as $extraLang => $extraPairs) {
$dict[$extraLang] = array_merge($dict[$extraLang] ?? [], $extraPairs);
}
}
$lang = bg_language();
$text = $dict[$lang][$key] ?? $dict['it'][$key] ?? $key;
foreach ($vars as $k => $v) $text = str_replace('%' . $k . '%', (string)$v, $text);
return $text;
}
function bg_lang_attr() { return bg_language() === 'en' ? 'en' : 'it'; }
function bg_write_json($name, $data) {
if ($name === 'web_options.json' && is_array($data)) {
// Keep interface language/theme non-persistent. They are session-only selectors.
unset($data['language'], $data['theme']);
}
$file = bg_data_file($name);
$tmp = $file . '.tmp';
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) return false;
if (@file_put_contents($tmp, $json . "\n", LOCK_EX) === false) return false;
@chmod($tmp, 0640);
return @rename($tmp, $file);
}
function bg_handle_quick_preferences() {
if (PHP_SAPI === 'cli') return;
$changed = false;
if (isset($_GET['bg_theme'])) {
$theme = (string)$_GET['bg_theme'];
if (in_array($theme, ['light','dark','system'], true)) {
$_SESSION['bg_theme'] = $theme;
$changed = true;
}
}
if (isset($_GET['bg_lang'])) {
$lang = (string)$_GET['bg_lang'];
if (in_array($lang, ['en','it'], true)) {
$_SESSION['bg_language'] = $lang;
$changed = true;
}
}
if (!$changed) return;
$params = $_GET;
unset($params['bg_theme'], $params['bg_lang']);
$target = $_SERVER['PHP_SELF'] ?? 'dashboard.php';
if ($params) $target .= '?' . http_build_query($params);
if (!headers_sent()) header('Location: ' . $target);
exit;
}
function bg_flash($type, $message) {
$_SESSION['flash'][] = ['type' => $type, 'message' => $message];
}
function bg_render_flash() {
if (empty($_SESSION['flash'])) return;
foreach ($_SESSION['flash'] as $f) {
$type = preg_replace('/[^a-z]/', '', $f['type'] ?? 'info');
echo '' . bg_e($f['message'] ?? '') . '
';
}
unset($_SESSION['flash']);
}
function bg_redirect($url) { header('Location: ' . $url); exit; }
function bg_cmd_exists($cmd) {
$out = trim((string)bg_shell_exec('command -v ' . escapeshellarg($cmd) . ' 2>/dev/null'));
return $out !== '';
}
function bg_run($cmd, $timeout = 60) {
$timeout = max(1, min(600, (int)$timeout));
$wrapped = (bg_cmd_exists('timeout') ? ('timeout ' . $timeout . 's ') : '') . $cmd;
if (function_exists('proc_open')) {
$desc = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$proc = @proc_open($wrapped, $desc, $pipes);
if (is_resource($proc)) {
$stdout = stream_get_contents($pipes[1]); fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
$code = proc_close($proc);
return ['code' => $code, 'output' => trim($stdout . ($stderr ? "\n" . $stderr : ''))];
}
}
$out = bg_shell_exec($wrapped . ' 2>&1');
return ['code' => $out === '' ? 127 : 0, 'output' => trim((string)$out)];
}
function bg_run_argv(array $argv, $timeout = 60) {
$parts = [];
foreach ($argv as $a) $parts[] = escapeshellarg((string)$a);
return bg_run(implode(' ', $parts), $timeout);
}
function bg_username_for_uid($uid) {
$uid = (int)$uid;
if ($uid < 0) return '';
if (function_exists('posix_getpwuid')) {
$pw = @posix_getpwuid($uid);
if (is_array($pw) && !empty($pw['name'])) return (string)$pw['name'];
}
$out = trim((string)bg_shell_exec('getent passwd ' . escapeshellarg((string)$uid) . ' 2>/dev/null | cut -d: -f1'));
return $out;
}
function bg_user_record($username) {
$username = trim((string)$username);
if (!preg_match('/^[a-z_][a-z0-9_-]*[$]?$/i', $username)) return null;
if (function_exists('posix_getpwnam')) {
$pw = @posix_getpwnam($username);
if (is_array($pw)) {
return [
'name' => $pw['name'] ?? $username,
'uid' => (int)($pw['uid'] ?? -1),
'gid' => (int)($pw['gid'] ?? -1),
'home' => $pw['dir'] ?? '',
'shell' => $pw['shell'] ?? '',
];
}
}
$line = trim((string)bg_shell_exec('getent passwd ' . escapeshellarg($username) . ' 2>/dev/null'));
if ($line === '') return null;
$p = explode(':', $line);
if (count($p) < 7) return null;
return ['name'=>$p[0], 'uid'=>(int)$p[2], 'gid'=>(int)$p[3], 'home'=>$p[5], 'shell'=>$p[6]];
}
function bg_valid_desktop_user($username) {
$rec = bg_user_record($username);
if (!$rec) return null;
if (($rec['uid'] ?? -1) < 0) return null;
return $rec;
}
function bg_current_process_user() {
if (function_exists('posix_geteuid')) {
$name = bg_username_for_uid((int)@posix_geteuid());
if ($name !== '') return $name;
}
$out = trim((string)bg_shell_exec('id -un 2>/dev/null'));
return $out !== '' ? $out : 'webserver';
}
function bg_detect_desktop_user() {
static $cached = null;
if ($cached !== null) return $cached;
$candidates = [];
$web = bg_read_json('web_options.json', []);
foreach ([$web['desktop_user'] ?? '', getenv('BASTIONGUARD_USER') ?: '', getenv('SUDO_USER') ?: ''] as $u) {
$u = trim((string)$u);
if ($u !== '') $candidates[] = $u;
}
foreach (['/etc/bastionguard-webui/user', '/etc/bastionguard/user'] as $file) {
if (is_readable($file)) {
$u = trim((string)@file_get_contents($file));
if ($u !== '') $candidates[] = preg_split('/\s+/', $u)[0] ?? $u;
}
}
foreach (glob('/run/user/[0-9]*', GLOB_ONLYDIR) ?: [] as $dir) {
$uid = basename($dir);
if (!ctype_digit($uid) || (int)$uid < 1000) continue;
if (!bg_path_is_socket($dir . '/bus')) continue;
$u = bg_username_for_uid((int)$uid);
if ($u !== '') $candidates[] = $u;
}
if (bg_cmd_exists('loginctl')) {
$out = trim((string)bg_shell_exec('loginctl list-sessions --no-legend 2>/dev/null'));
foreach (preg_split('/\R+/', $out) as $line) {
$cols = preg_split('/\s+/', trim($line));
if (count($cols) >= 3 && ctype_digit($cols[1]) && (int)$cols[1] >= 1000) $candidates[] = $cols[2];
}
}
foreach (glob('/home/*', GLOB_ONLYDIR) ?: [] as $home) {
$u = basename($home);
if (is_dir($home . '/.config/systemd/user') || is_dir($home . '/.config/bastionguard') || is_dir($home . '/.local/share/systemd/user')) $candidates[] = $u;
}
$seen = [];
foreach ($candidates as $u) {
$u = trim((string)$u);
if ($u === '' || isset($seen[$u])) continue;
$seen[$u] = true;
$rec = bg_valid_desktop_user($u);
if ($rec && (int)$rec['uid'] >= 1000) {
$cached = $rec['name'];
return $cached;
}
}
$cached = '';
return $cached;
}
function bg_user_systemd_context($username = null) {
$username = $username ?: bg_detect_desktop_user();
$rec = $username ? bg_user_record($username) : null;
if (!$rec) return null;
$uid = (int)$rec['uid'];
return [
'user' => $rec['name'],
'uid' => $uid,
'home' => $rec['home'] ?? '',
'runtime_dir' => '/run/user/' . $uid,
'bus' => '/run/user/' . $uid . '/bus',
];
}
function bg_systemctl_user_exec($verb, $unit, array $props = [], $timeout = 10) {
$unit = preg_replace('/[^a-zA-Z0-9@_.:-]/', '', (string)$unit);
$verb = preg_replace('/[^a-z-]/', '', (string)$verb);
if ($unit === '' || $verb === '') return ['code'=>2, 'output'=>'Unità o comando invalid.', 'method'=>'invalid'];
$ctx = bg_user_systemd_context();
if (!$ctx) return ['code'=>3, 'output'=>'Utente desktop non configurato o non rilevato.', 'method'=>'no-user'];
$props = array_values(array_filter(array_map(function($p){ return preg_replace('/[^A-Za-z0-9_]/', '', (string)$p); }, $props)));
$helper = '/usr/local/sbin/bastionguard-webui-systemctl';
$attempts = [];
if (is_executable($helper) && bg_cmd_exists('sudo')) {
$args = ['sudo','-n',$helper,'user',$ctx['user'],$verb,$unit];
foreach ($props as $p) $args[] = $p;
$attempts[] = ['helper', $args];
}
$env = ['env', 'XDG_RUNTIME_DIR=' . $ctx['runtime_dir'], 'DBUS_SESSION_BUS_ADDRESS=unix:path=' . $ctx['bus']];
$sysArgs = array_merge($env, ['systemctl','--user',$verb,$unit]);
if ($verb === 'show') {
foreach ($props as $p) { $sysArgs[] = '-p'; $sysArgs[] = $p; }
$sysArgs[] = '--no-pager';
}
if (function_exists('posix_geteuid') && (int)@posix_geteuid() === (int)$ctx['uid']) {
$attempts[] = ['same-user-dbus', $sysArgs];
}
// Tentativo diretto con XDG_RUNTIME_DIR/DBus già impostati. Su molte installazioni fallisce per policy DBus,
// ma quando PHP-FPM gira come utente desktop consente la lettura senza sudoers.
$attempts[] = ['direct-dbus', $sysArgs];
if (bg_cmd_exists('sudo')) {
$sudoArgs = array_merge(['sudo','-n','-u',$ctx['user']], $sysArgs);
$attempts[] = ['sudo-user-dbus', $sudoArgs];
}
if (function_exists('posix_geteuid') && (int)@posix_geteuid() === 0 && bg_cmd_exists('runuser')) {
$runArgs = array_merge(['runuser','-u',$ctx['user'],'--'], $sysArgs);
$attempts[] = ['runuser-dbus', $runArgs];
}
$last = ['code'=>127, 'output'=>'Nessun metodo disponibile per interrogare systemd --user.', 'method'=>'none'];
foreach ($attempts as [$method, $argv]) {
$res = bg_run_argv($argv, $timeout);
$res['method'] = $method;
$out = trim((string)($res['output'] ?? ''));
$accessError = $out !== '' && preg_match('/Failed to connect|Permission denied|Interactive authentication|a password is required|not allowed to execute|sudo:/i', $out);
if (($res['code'] ?? 1) === 0 || ($out !== '' && !$accessError)) return $res;
$last = $res;
}
return $last;
}
function bg_systemctl_system_exec($verb, $unit, array $props = [], $timeout = 10) {
$unit = preg_replace('/[^a-zA-Z0-9@_.:-]/', '', (string)$unit);
$verb = preg_replace('/[^a-z-]/', '', (string)$verb);
$args = ['systemctl', $verb, $unit];
if ($verb === 'show') {
foreach ($props as $p) { $args[] = '-p'; $args[] = $p; }
$args[] = '--no-pager';
}
$res = bg_run_argv($args, $timeout);
$res['method'] = 'system';
return $res;
}
function bg_parse_systemctl_show($raw) {
$data = [];
foreach (preg_split('/\R/', trim((string)$raw)) as $line) {
if (strpos($line, '=') === false) continue;
[$k, $v] = explode('=', $line, 2);
$data[$k] = $v;
}
return $data;
}
function bg_user_unit_file_probe($unit, $username = null) {
$ctx = bg_user_systemd_context($username);
$out = ['found'=>false, 'fragment'=>'', 'enabled'=>'unknown'];
if (!$ctx) return $out;
$home = rtrim($ctx['home'] ?? '', '/');
$paths = [];
if ($home !== '') {
$paths[] = $home . '/.config/systemd/user/' . $unit;
$paths[] = $home . '/.local/share/systemd/user/' . $unit;
}
$paths[] = '/etc/systemd/user/' . $unit;
$paths[] = '/usr/local/lib/systemd/user/' . $unit;
$paths[] = '/usr/lib/systemd/user/' . $unit;
$paths[] = '/lib/systemd/user/' . $unit;
foreach ($paths as $p) {
if (is_file($p) || is_link($p)) {
$out['found'] = true;
$out['fragment'] = $p;
break;
}
}
$wantPatterns = [];
if ($home !== '') $wantPatterns[] = $home . '/.config/systemd/user/*.wants/' . $unit;
$wantPatterns[] = '/etc/systemd/user/*.wants/' . $unit;
foreach ($wantPatterns as $pat) {
$g = glob($pat) ?: [];
if (!empty($g)) { $out['enabled'] = 'enabled'; break; }
}
if ($out['enabled'] === 'unknown' && $out['found']) $out['enabled'] = 'disabled';
return $out;
}
function bg_user_process_probe($unit, $username = null) {
$ctx = bg_user_systemd_context($username);
if (!$ctx || !bg_cmd_exists('pgrep')) return [];
$base = preg_replace('/\.service$/', '', basename($unit));
$patterns = array_values(array_unique([$base, strtolower($base), str_replace('BastionGuard-', 'bastionguard-', $base)]));
foreach ($patterns as $pattern) {
if ($pattern === '') continue;
$res = bg_run_argv(['pgrep','-u',$ctx['user'],'-f',$pattern], 5);
$pids = array_values(array_filter(array_map('trim', preg_split('/\R+/', trim($res['output']))), 'ctype_digit'));
if (!empty($pids)) return array_map('intval', $pids);
}
return [];
}
function bg_systemctl_query($name, $user = false) {
$name = preg_replace('/[^a-zA-Z0-9@_.:-]/', '', (string)$name);
if ($name === '') return [];
$props = ['ActiveState','SubState','LoadState','UnitFileState','MainPID','FragmentPath','Description'];
$data = ['name' => $name, 'user' => (bool)$user, 'raw_show' => '', 'AccessMethod' => '', 'AccessError' => ''];
if ($user) {
$ctx = bg_user_systemd_context();
$data['TargetUser'] = $ctx['user'] ?? '';
$show = bg_systemctl_user_exec('show', $name, $props, 10);
$data['AccessMethod'] = $show['method'] ?? '';
$data['raw_show'] = trim((string)($show['output'] ?? ''));
if (($show['code'] ?? 1) === 0 && $data['raw_show'] !== '') {
$data = array_merge($data, bg_parse_systemctl_show($data['raw_show']));
} else {
$data['AccessError'] = trim((string)($show['output'] ?? ''));
}
$active = bg_systemctl_user_exec('is-active', $name, [], 8);
$aout = trim((string)($active['output'] ?? ''));
$activeToken = $aout !== '' ? preg_split('/\s+/', $aout)[0] : '';
if (in_array($activeToken, ['active','inactive','failed','activating','deactivating','reloading','unknown'], true)) $data['ActiveState'] = $data['ActiveState'] ?? $activeToken;
if (empty($data['AccessMethod']) && !empty($active['method'])) $data['AccessMethod'] = $active['method'];
$enabled = bg_systemctl_user_exec('is-enabled', $name, [], 8);
$eout = trim((string)($enabled['output'] ?? ''));
$enabledToken = $eout !== '' ? preg_split('/\s+/', $eout)[0] : '';
if (in_array($enabledToken, ['enabled','enabled-runtime','linked','linked-runtime','alias','masked','masked-runtime','static','indirect','disabled','generated','transient','bad','not-found'], true)) $data['UnitFileState'] = $data['UnitFileState'] ?? $enabledToken;
$probe = bg_user_unit_file_probe($name, $ctx['user'] ?? null);
if (!empty($probe['found'])) {
$data['LoadState'] = $data['LoadState'] ?? 'loaded';
$data['FragmentPath'] = $data['FragmentPath'] ?? $probe['fragment'];
}
if (($data['UnitFileState'] ?? '') === '' || ($data['UnitFileState'] ?? '') === 'unknown') $data['UnitFileState'] = $probe['enabled'];
$activeState = $data['ActiveState'] ?? '';
if ($activeState === '' || in_array($activeState, ['unknown','inactive','failed'], true)) {
$pids = bg_user_process_probe($name, $ctx['user'] ?? null);
if (!empty($pids)) {
$data['ActiveState'] = 'active';
$data['SubState'] = $data['SubState'] ?? 'process-detected';
$data['MainPID'] = $data['MainPID'] ?? (string)$pids[0];
$data['ProcessDetected'] = implode(',', $pids);
if (empty($data['AccessMethod']) || $data['AccessMethod'] === 'direct-dbus') $data['AccessMethod'] = 'process-probe';
}
}
return $data;
}
$show = bg_systemctl_system_exec('show', $name, $props, 10);
$data['AccessMethod'] = $show['method'] ?? 'system';
$data['raw_show'] = trim((string)($show['output'] ?? ''));
if (($show['code'] ?? 1) === 0 && $data['raw_show'] !== '') $data = array_merge($data, bg_parse_systemctl_show($data['raw_show']));
else $data['AccessError'] = trim((string)($show['output'] ?? ''));
$active = bg_systemctl_system_exec('is-active', $name, [], 8);
$aout = trim((string)($active['output'] ?? ''));
$activeToken = $aout !== '' ? preg_split('/\s+/', $aout)[0] : '';
if (in_array($activeToken, ['active','inactive','failed','activating','deactivating','reloading','unknown'], true)) $data['ActiveState'] = $data['ActiveState'] ?? $activeToken;
$enabled = bg_systemctl_system_exec('is-enabled', $name, [], 8);
$eout = trim((string)($enabled['output'] ?? ''));
$enabledToken = $eout !== '' ? preg_split('/\s+/', $eout)[0] : '';
if (in_array($enabledToken, ['enabled','enabled-runtime','linked','linked-runtime','alias','masked','masked-runtime','static','indirect','disabled','generated','transient','bad','not-found'], true)) $data['UnitFileState'] = $data['UnitFileState'] ?? $enabledToken;
return $data;
}
function bg_service_status(array $names, $user = false) {
$fallback = null;
foreach ($names as $name) {
$name = preg_replace('/[^a-zA-Z0-9@_.:-]/', '', (string)$name);
if ($name === '') continue;
$q = bg_systemctl_query($name, $user);
$activeState = $q['ActiveState'] ?? '';
$subState = $q['SubState'] ?? '';
$loadState = $q['LoadState'] ?? '';
$unitFileState = $q['UnitFileState'] ?? '';
$pid = (int)($q['MainPID'] ?? 0);
$loaded = $loadState === 'loaded' || ($unitFileState !== '' && !in_array($unitFileState, ['not-found','unknown'], true)) || !empty($q['FragmentPath']) || !empty($q['ProcessDetected']);
$targetUser = $q['TargetUser'] ?? '';
$row = [
'active' => $activeState === 'active',
'name' => $name,
'raw' => $activeState !== '' ? $activeState : 'unknown',
'active_state' => $activeState !== '' ? $activeState : 'unknown',
'sub_state' => $subState !== '' ? $subState : 'unknown',
'load_state' => $loadState !== '' ? $loadState : 'unknown',
'unit_file_state' => $unitFileState !== '' ? $unitFileState : 'unknown',
'main_pid' => $pid,
'fragment_path' => $q['FragmentPath'] ?? '',
'description' => $q['Description'] ?? '',
'exists' => $loaded,
'user' => (bool)$user,
'target_user' => $targetUser,
'scope' => $user ? 'user' : 'system',
'access_method' => $q['AccessMethod'] ?? '',
'access_error' => $q['AccessError'] ?? '',
'process_detected' => $q['ProcessDetected'] ?? '',
];
if ($row['active']) return $row;
if ($fallback === null || ($row['exists'] && !$fallback['exists'])) $fallback = $row;
}
$targetUser = $user ? bg_detect_desktop_user() : '';
return $fallback ?? [
'active' => false,
'name' => $names[0] ?? '',
'raw' => 'unknown',
'active_state' => 'unknown',
'sub_state' => 'unknown',
'load_state' => 'unknown',
'unit_file_state' => 'unknown',
'main_pid' => 0,
'fragment_path' => '',
'description' => '',
'exists' => false,
'user' => (bool)$user,
'target_user' => $targetUser,
'scope' => $user ? 'user' : 'system',
'access_method' => '',
'access_error' => '',
'process_detected' => '',
];
}
function bg_service_scope_label(array $st) {
$scope = (string)($st['scope'] ?? 'system');
$label = $scope === 'user' ? bg_t('scope_user') : bg_t('scope_system');
$target = trim((string)($st['target_user'] ?? ''));
return $scope === 'user' && $target !== '' ? $label . ' (' . $target . ')' : $label;
}
function bg_systemd_value_label($value) {
$raw = trim((string)$value);
if ($raw === '') return '—';
$normalized = strtolower(str_replace(['-', ' '], '_', $raw));
$key = 'systemd_' . $normalized;
$translated = bg_t($key);
return $translated === $key ? $raw : $translated;
}
function bg_access_method_label($method) {
$raw = trim((string)$method);
if ($raw === '') return '—';
$normalized = strtolower(str_replace(['-', ' '], '_', $raw));
$key = 'access_' . $normalized;
$translated = bg_t($key);
return $translated === $key ? $raw : $translated;
}
function bg_service_state_badge(array $st) {
$state = $st['active_state'] ?? ($st['active'] ? 'active' : 'inactive');
if ($state === 'active') return '' . bg_e(bg_t('started')) . '';
if ($state === 'failed') return '' . bg_e(bg_t('error')) . '';
if ($state === 'activating') return '' . bg_e(bg_t('starting')) . '';
if ($state === 'deactivating') return '' . bg_e(bg_t('stopping')) . '';
if ($state === 'unknown') return '' . bg_e(bg_t('not_read')) . '';
return '' . bg_e(bg_t('stopped')) . '';
}
function bg_webui_disabled_service_unit($unit) {
$u = strtolower((string)$unit);
return in_array($u, ['bastionguard-cef.service','bastionguard-cef','bastionguard-pacd.service','bastionguard-pacd','bastionguard-privacyd.service','bastionguard-privacyd','bastionguard-usbd.service','bastionguard-usbd'], true);
}
function bg_proxy_sensitive_unit($unit) {
$u = strtolower((string)$unit);
return false;
}
function bg_desktop_mode_summary($username = null) {
$username = $username ?: bg_detect_desktop_user();
if ($username === '') return ['code'=>67, 'summary'=>'headless no-desktop-user-configured', 'gui'=>false];
$helper = '/usr/local/sbin/bastionguard-webui-admin';
if (is_executable($helper) && bg_cmd_exists('sudo')) {
$res = bg_run_argv(['sudo','-n',$helper,'desktop-mode',$username], 15);
$summary = trim((string)($res['output'] ?? ''));
if ($summary !== '') return ['code'=>(int)($res['code'] ?? 0), 'summary'=>$summary, 'gui'=>str_starts_with($summary, 'gui ')];
}
if (bg_cmd_exists('loginctl')) {
$out = trim((string)bg_shell_exec('loginctl list-sessions --no-legend 2>/dev/null'));
foreach (preg_split('/\R+/', $out) as $line) {
$cols = preg_split('/\s+/', trim($line));
if (count($cols) < 3 || $cols[2] !== $username) continue;
$sid = preg_replace('/[^A-Za-z0-9_.:-]/', '', $cols[0]);
if ($sid === '') continue;
$type = trim((string)bg_shell_exec('loginctl show-session '.escapeshellarg($sid).' -p Type --value 2>/dev/null'));
$active = trim((string)bg_shell_exec('loginctl show-session '.escapeshellarg($sid).' -p Active --value 2>/dev/null'));
if (in_array($type, ['x11','wayland','mir'], true)) return ['code'=>0, 'summary'=>'gui session='.$sid.' active='.$active.' type='.$type, 'gui'=>true];
}
}
return ['code'=>1, 'summary'=>'headless no-active-graphical-session user='.$username, 'gui'=>false];
}
function bg_service_action(array $names, $action, $user = false) {
$allowed = ['start','stop','restart','enable','disable'];
if (!in_array($action, $allowed, true)) return ['code' => 2, 'output' => bg_t('service_action_not_allowed')];
$primary = $names[0] ?? '';
$foundLoaded = false;
foreach ($names as $name) {
$st = bg_service_status([$name], $user);
if (!empty($st['active'])) { $primary = $name; break; }
if (!$foundLoaded && !empty($st['exists'])) { $primary = $name; $foundLoaded = true; }
}
$primary = preg_replace('/[^a-zA-Z0-9@_.:-]/', '', $primary);
if ($primary === '') return ['code' => 2, 'output' => bg_t('service_invalid')];
if (bg_webui_disabled_service_unit($primary) && in_array($action, ['start','restart','enable'], true)) {
return ['code'=>76, 'output'=>bg_t('webui_disabled_service_msg', ['unit'=>$primary])];
}
if ($user && bg_proxy_sensitive_unit($primary) && in_array($action, ['start','restart','enable'], true)) {
$targetForGuard = bg_detect_desktop_user();
$mode = bg_desktop_mode_summary($targetForGuard);
if (!empty($mode['gui'])) {
return ['code'=>75, 'output'=>"BastionGuard WebUI safe-desktop guard: azione bloccata su $primary. Desktop grafico attivo per $targetForGuard (" . $mode['summary'] . "). Usa la GUI GTK; CEF/PAC, Webcam/Privacy e USB restano esclusi dalla WebUI server."];
}
}
if ($user) {
$target = bg_detect_desktop_user();
$helper = '/usr/local/sbin/bastionguard-webui-admin';
if ($target !== '' && is_executable($helper) && bg_cmd_exists('sudo')) {
$res = bg_run_argv(['sudo','-n',$helper,'service-action','user',$target,$action,$primary], 45);
$out = trim((string)($res['output'] ?? ''));
$accessError = $out !== '' && preg_match('/not allowed to execute|a password is required|sudo:/i', $out);
if (($res['code'] ?? 1) === 0 || !$accessError) {
$res['method'] = 'admin-helper';
return $res;
}
}
$verb = $action;
if ($action === 'enable') $verb = 'enable';
if ($action === 'disable') $verb = 'disable';
$res = bg_systemctl_user_exec($verb, $primary, [], 30);
if (($res['code'] ?? 1) !== 0 && in_array($action, ['start','restart','enable','disable'], true)) {
$msg = trim((string)($res['output'] ?? ''));
$extra = "\nInstall the helper to let the WebUI manage systemd --user services: sudo bash scripts/install-webui-helpers.sh \"$USER\"";
return ['code'=>$res['code'] ?? 1, 'output'=>$msg . $extra];
}
return $res;
}
$helper = '/usr/local/sbin/bastionguard-webui-admin';
if (is_executable($helper) && bg_cmd_exists('sudo')) {
$res = bg_run_argv(['sudo','-n',$helper,'service-action','system','-',$action,$primary], 45);
$out = trim((string)($res['output'] ?? ''));
$accessError = $out !== '' && preg_match('/not allowed to execute|a password is required|sudo:/i', $out);
if (($res['code'] ?? 1) === 0 || !$accessError) {
$res['method'] = 'admin-helper';
return $res;
}
}
$systemAction = $action;
if ($systemAction === 'enable') $systemAction = 'enable --now';
if ($systemAction === 'disable') $systemAction = 'disable --now';
return bg_run('sudo -n systemctl ' . $systemAction . ' ' . escapeshellarg($primary) . ' 2>&1', 30);
}
function bg_journal(array $services, $lines = 100, $user = false) {
$lines = max(1, min(2000, (int)$lines));
$clean = [];
foreach ($services as $s) {
$s = preg_replace('/[^a-zA-Z0-9@_.:-]/', '', (string)$s);
if ($s !== '') $clean[] = $s;
}
if (!$clean) return '';
// Prefer the privileged helper. Apache/PHP-FPM usually cannot read
// system journals or the desktop user's systemd --user journal directly.
$helper = '/usr/local/sbin/bastionguard-webui-admin';
if (is_executable($helper) && bg_cmd_exists('sudo')) {
$mode = $user ? 'user' : 'system';
$target = '-';
if ($user) {
$target = bg_detect_desktop_user();
if ($target === '') {
return 'Desktop user not detected. Set it in Settings → Web Options, then reinstall the helper.';
}
}
$argv = array_merge(['sudo', '-n', $helper, 'journal', $mode, $target, (string)$lines], $clean);
$res = bg_run_argv($argv, 30);
$out = trim((string)($res['output'] ?? ''));
if (($res['code'] ?? 1) === 0 && $out !== '') return $out;
if ($out !== '' && (strpos($out, 'not allowed') !== false || strpos($out, 'password') !== false || strpos($out, 'sudo') !== false)) {
return "Journal helper error:\n" . $out . "\n\nRun: sudo bash scripts/install-webui-helpers.sh \"\$USER\"";
}
// Continue with direct fallback below; useful on permissive local installs.
}
$chunks = [];
foreach ($clean as $s) {
if ($user) {
$ctx = bg_user_systemd_context();
$cmd = 'journalctl --user -u ' . escapeshellarg($s) . ' -n ' . $lines . ' --no-pager 2>&1';
if ($ctx && bg_cmd_exists('sudo')) {
$cmd = 'sudo -n -u ' . escapeshellarg($ctx['user']) . ' env XDG_RUNTIME_DIR=' . escapeshellarg($ctx['runtime_dir']) . ' DBUS_SESSION_BUS_ADDRESS=' . escapeshellarg('unix:path=' . $ctx['bus']) . ' journalctl --user -u ' . escapeshellarg($s) . ' -n ' . $lines . ' --no-pager 2>&1';
}
$out = trim((string)bg_shell_exec($cmd));
} else {
$cmd = 'journalctl -u ' . escapeshellarg($s) . ' -n ' . $lines . ' --no-pager 2>&1';
$out = trim((string)bg_shell_exec($cmd));
}
if ($out !== '') $chunks[] = "### $s\n" . $out;
}
return implode("\n\n", $chunks);
}
function bg_user_services_diagnostics() {
$ctx = bg_user_systemd_context();
$helper = '/usr/local/sbin/bastionguard-webui-systemctl';
return [
'web_user' => bg_current_process_user(),
'target_user' => $ctx['user'] ?? '',
'uid' => $ctx['uid'] ?? '',
'runtime_dir' => $ctx['runtime_dir'] ?? '',
'runtime_exists' => $ctx ? is_dir($ctx['runtime_dir']) : false,
'bus' => $ctx['bus'] ?? '',
'bus_exists' => $ctx ? bg_path_is_socket($ctx['bus']) : false,
'helper_installed' => is_executable($helper),
'sudo_available' => bg_cmd_exists('sudo'),
];
}
function bg_status_badge($active, $textActive = 'Attivo', $textInactive = 'Non attivo') {
$class = $active ? 'success' : 'secondary';
$text = $active ? $textActive : $textInactive;
return '' . bg_e($text) . '';
}
function bg_file_size($bytes) {
$bytes = (float)$bytes;
$units = ['B','KB','MB','GB','TB'];
for ($i = 0; $bytes >= 1024 && $i < count($units)-1; $i++) $bytes /= 1024;
return ($i === 0 ? (string)(int)$bytes : number_format($bytes, 1, ',', '.')) . ' ' . $units[$i];
}
function bg_clamdb_versions() {
$files = ['/var/lib/clamav/daily.cvd','/var/lib/clamav/main.cvd','/var/lib/clamav/bytecode.cvd','/var/lib/clamav/daily.cld','/var/lib/clamav/main.cld','/var/lib/clamav/bytecode.cld'];
$versions = [];
foreach ($files as $file) {
if (!is_file($file)) continue;
$name = basename($file);
$info = '';
if (bg_cmd_exists('sigtool')) {
$res = bg_run('sigtool --info ' . escapeshellarg($file) . ' 2>/dev/null | head -20', 10);
$info = $res['output'];
}
$ver = 'presente';
if (preg_match('/Version:\s*(\S+)/i', $info, $m)) $ver = $m[1];
$versions[] = ['name' => $name, 'version' => $ver, 'size' => filesize($file), 'mtime' => filemtime($file)];
}
return $versions;
}
function bg_clamd_paths() {
$paths = ['/etc/clamav/clamd.conf','/etc/clamd/clamd.conf','/etc/clamd.d/clamd.conf','/etc/clamd.conf'];
$found = [];
foreach ($paths as $p) {
if (!is_readable($p)) continue;
foreach (@file($p) ?: [] as $line) {
$line = trim($line);
if (stripos($line, 'OnAccessIncludePath') === 0 || stripos($line, 'OnAccessExcludePath') === 0) $found[] = $line;
}
}
return $found;
}
function bg_safe_basename($name) { return basename(str_replace("\0", '', (string)$name)); }
function bg_realpath_inside($base, $name) {
$baseReal = realpath($base);
if ($baseReal === false) return false;
$target = realpath($base . DIRECTORY_SEPARATOR . bg_safe_basename($name));
if ($target === false) return false;
return (str_starts_with($target, $baseReal . DIRECTORY_SEPARATOR) || $target === $baseReal) ? $target : false;
}
function bg_normalize_lines($text) {
$out = [];
foreach (preg_split('/\R+/', (string)$text) as $line) {
$line = trim($line);
if ($line !== '' && !in_array($line, $out, true)) $out[] = $line;
}
return $out;
}
function bg_normalize_domain($value) {
$value = trim((string)$value);
$value = preg_replace('#^https?://#i', '', $value);
$value = preg_replace('#/.*$#', '', $value);
$value = strtolower($value);
return preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/', $value) ? $value : '';
}
function bg_services_catalog() {
return [
'clamd' => ['label'=>bg_t('svc_clamd'), 'services'=>['clamav-daemon.service','clamd.service'], 'user'=>false],
'clamonacc' => ['label'=>bg_t('svc_clamonacc'), 'services'=>['clamav-clamonacc.service','clamonacc.service'], 'user'=>false],
'freshclam' => ['label'=>bg_t('svc_freshclam'), 'services'=>['clamav-freshclam.service','freshclam.service'], 'user'=>false],
'phishing_scanner' => ['label'=>bg_t('svc_phishing_scanner'), 'services'=>['BastionGuard-phishing-scanner.service','BastionGuard-phishing.service','bastionguard-phishing.service'], 'user'=>false],
'ransomware_realtime' => ['label'=>bg_t('svc_ransomware_realtime'), 'services'=>['BastionGuard-ransomware-realtime.service','bastionguard-ransomware-realtime.service'], 'user'=>false],
'ransomware_alert' => ['label'=>bg_t('svc_ransomware_alert'), 'services'=>['BastionGuard-ransomware-alert.service','bastionguard-ransomware-alert.service'], 'user'=>true],
'ransomware_realtime_alert' => ['label'=>bg_t('svc_ransomware_realtime_alert'), 'services'=>['BastionGuard-ransomware-realtime-alert.service','bastionguard-ransomware-realtime-alert.service'], 'user'=>true],
'ransomware_scanner' => ['label'=>bg_t('svc_ransomware_scanner'), 'services'=>['BastionGuard-ransomware-scanner.service','bastionguard-ransomware-scanner.service'], 'user'=>true],
'useragent' => ['label'=>bg_t('svc_useragent'), 'services'=>['BastionGuard-useragent.service','bastionguard-useragent.service'], 'user'=>true],
'mail' => ['label'=>bg_t('svc_mail'), 'services'=>['BastionGuard-mailproxy.service','bastionguard-mailproxy.service'], 'user'=>true],
];
}
function bg_render_log($text, $empty=null) {
if ($empty === null) $empty = bg_language() === 'en' ? 'No log entries available.' : 'Nessun log disponibile.';
echo '' . bg_e(trim($text) !== '' ? $text : $empty) . '
';
}
function bg_http_get($url, $headers = [], $timeout = 30) {
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_FOLLOWLOCATION=>true, CURLOPT_CONNECTTIMEOUT=>10, CURLOPT_TIMEOUT=>$timeout, CURLOPT_USERAGENT=>'BastionGuard-WebUI/1.0']);
if ($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
return ['code'=>$code, 'body'=>$body === false ? '' : $body, 'error'=>$err];
}
$ctx = stream_context_create(['http'=>['timeout'=>$timeout, 'header'=>implode("\r\n", $headers)]]);
$body = @file_get_contents($url, false, $ctx);
return ['code'=> $body === false ? 0 : 200, 'body'=> $body === false ? '' : $body, 'error'=> $body === false ? 'Download fallito' : ''];
}
function bg_password_strength($password) {
$score = 0;
if (strlen($password) >= 10) $score++;
if (preg_match('/[a-z]/', $password)) $score++;
if (preg_match('/[A-Z]/', $password)) $score++;
if (preg_match('/[0-9]/', $password)) $score++;
if (preg_match('/[^a-zA-Z0-9]/', $password)) $score++;
return $score;
}
function bg_file_data_uri($path, $mime = null) {
$path = (string)$path;
if ($path === '' || !is_file($path) || !is_readable($path)) return '';
$data = @file_get_contents($path);
if ($data === false) return '';
if ($mime === null) {
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$mime = $ext === 'svg' ? 'image/svg+xml' : ($ext === 'png' ? 'image/png' : ($ext === 'jpg' || $ext === 'jpeg' ? 'image/jpeg' : 'application/octet-stream'));
}
return 'data:' . $mime . ';base64,' . base64_encode($data);
}
function bg_logo_html($class = 'brand-logo') {
$svg = '/usr/share/BastionGuard/data/logo.svg';
if (is_file($svg) && is_readable($svg)) {
$raw = @file_get_contents($svg);
if (is_string($raw) && trim($raw) !== '') {
// File locale installato da BastionGuard. Rimuove solo elementi script evidenti prima dell'inline.
$raw = preg_replace('##is', '', $raw);
$raw = preg_replace('/\son[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $raw);
return '' . $raw . '';
}
}
return '🛡️';
}
function bg_version_build() {
$version = '?.?';
$build = '?';
$path = '/usr/share/BastionGuard/data/version.bs';
$raw = '';
if (is_readable($path)) {
$raw = @file_get_contents($path);
}
if (!is_string($raw) || trim($raw) === '') {
// Fallback through the privileged helper, useful when PHP runs in a restricted
// server context but BastionGuard is installed system-wide.
if (!function_exists('bg_admin_helper')) {
$cm = __DIR__ . '/config_manager.php';
if (is_file($cm)) @require_once $cm;
}
if (function_exists('bg_admin_helper')) {
$res = bg_admin_helper(['read-version'], 30);
if ((int)($res['code'] ?? 1) === 0) $raw = (string)($res['output'] ?? '');
}
}
foreach (preg_split('/\R+/', (string)$raw) as $line) {
$line = trim($line);
if (strpos($line, 'version=') === 0) $version = substr($line, 8);
elseif (strpos($line, 'build=') === 0) $build = substr($line, 6);
}
return [$version, $build];
}
function bg_about_license_text() {
$candidates = [
'data/license/COPYING',
'/usr/share/BastionGuard/data/license/COPYING',
'COPYING',
'LICENSE',
'/usr/share/doc/BastionGuard/COPYING',
'/usr/share/common-licenses/GPL-3',
];
foreach ($candidates as $path) {
if (is_readable($path)) {
$txt = @file_get_contents($path);
if (is_string($txt) && $txt !== '') return $txt;
}
}
return "GNU GPL v3 license text not found.\nAssicurati che il file COPYING sia incluso nel progetto o che la tua distribuzione fornisca /usr/share/common-licenses/GPL-3.";
}
function bg_about_database_info() {
$path = '/var/lib/clamav';
if (!is_dir($path)) return "Directory /var/lib/clamav non trovata.\n";
$out = "📁 Database ClamAV trovati in /var/lib/clamav\n\n";
foreach (scandir($path) ?: [] as $file) {
if ($file === '.' || $file === '..') continue;
$lower = strtolower($file);
if (preg_match('/\.(hdb|ndb|ldb|cdb|hsb|fp)$/', $lower)) $out .= "✔ " . $file . "\n";
elseif (preg_match('/\.(cld|cvd)$/', $lower)) $out .= "🔒 " . $file . " (database ufficiale ClamAV)\n";
else $out .= "• " . $file . "\n";
}
return $out;
}
function bg_scan_path_privileged($path) {
require_once __DIR__ . '/config_manager.php';
$path = trim((string)$path);
if ($path === '') return ['code'=>64, 'output'=>'Percorso mancante.'];
return bg_admin_helper(['scan-path-b64', base64_encode($path)], 900);
}
function bg_phishing_remote_analyze($url) {
$url = trim((string)$url);
if ($url === '') return ['code'=>64, 'output'=>'Inserisci un URL.'];
$endpoint = 'https://bastionguard.eu/bastionguard-security-intelligence/?q=' . rawurlencode($url);
$res = bg_http_get($endpoint, [], 25);
if ((int)$res['code'] < 200 || (int)$res['code'] >= 400 || trim((string)$res['body']) === '') {
return ['code'=>1, 'output'=>'Unable to contact BastionGuard Security Intelligence. HTTP ' . ($res['code'] ?: '0') . ' ' . ($res['error'] ?? '')];
}
$html = (string)$res['body'];
$strip = function($s) {
$s = preg_replace('/<[^>]*>/', ' ', (string)$s);
$s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
return trim(preg_replace('/\s+/', ' ', $s));
};
$first = function($regex) use ($html, $strip) {
if (preg_match($regex, $html, $m)) return $strip($m[1] ?? '');
return '';
};
$classification = $first('/CLASSIFICATION<\/span>\s*([^<]+)/i');
$severity = $first('/([^<]+)<\/div>/i');
$normalized = $first('/
\s*Normalized Host:\s*<\/strong>\s*([^<]+)<\/code>/i');
$chain = $first('/\s*Evaluation Chain:\s*<\/strong>\s*([^<]+)<\/code>/i');
$matched = $first('/\s*Matched Indicator:\s*<\/strong>\s*([^<]+)<\/code>/i');
$probe = $first('/\s*Link Status \(Live Probe\):\s*<\/strong>\s*\s*([^<]+)\s*<\/span>/i');
$probeText = $first('/\s*([\s\S]*?)\s*<\/div>/i');
$details = [];
if (preg_match('/
\s*Analysis Details\s*:<\/strong>\s*([\s\S]*?)<\/ul>/i', $html, $um)) {
if (preg_match_all('/- ([\s\S]*?)<\/li>/i', $um[1], $lm)) foreach ($lm[1] as $li) $details[] = $strip($li);
}
if ($classification === '') return ['code'=>2, 'output'=>'Unexpected response format from remote analyzer. Nessun blocco CLASSIFICATION trovato.'];
$lines = [];
$lines[] = 'Classification: ' . $classification;
if ($severity !== '') $lines[] = 'Severity: ' . $severity;
if ($normalized !== '') $lines[] = 'Normalized Host: ' . $normalized;
if ($chain !== '') $lines[] = 'Evaluation Chain: ' . $chain;
if ($matched !== '') $lines[] = 'Matched Indicator: ' . $matched;
if ($probe !== '') $lines[] = 'Live Probe: ' . $probe;
if ($probeText !== '') $lines[] = 'Live Probe Details: ' . $probeText;
if ($details) {
$lines[] = '';
$lines[] = 'Analysis Details:';
foreach ($details as $d) $lines[] = ' - ' . $d;
}
return ['code'=>0, 'output'=>implode("\n", $lines), 'classification'=>$classification];
}
function bg_alerts_file() { return bg_data_dir() . '/alerts.tsv'; }
function bg_alert_state_file() { return bg_data_dir() . '/alerts_state.json'; }
function bg_alerts_state() { return bg_read_json('alerts_state.json', ['handled'=>[], 'emailed'=>[]]); }
function bg_alerts_save_state(array $state) { return bg_write_json('alerts_state.json', $state); }
function bg_import_ransomware_realtime_events() {
require_once __DIR__ . '/config_manager.php';
return bg_admin_helper(['import-ransomware-events'], 30);
}
function bg_alerts_read($limit = 20, $unhandledOnly = true) {
$file = bg_alerts_file();
$state = bg_alerts_state();
$handled = is_array($state['handled'] ?? null) ? $state['handled'] : [];
if (!is_file($file) || !is_readable($file)) return [];
$lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!is_array($lines)) return [];
$lines = array_reverse($lines);
$out = [];
foreach ($lines as $line) {
$p = explode("\t", $line);
if (count($p) < 6) continue;
$id = trim($p[1]);
if ($id === '') continue;
if ($unhandledOnly && in_array($id, $handled, true)) continue;
$path = base64_decode($p[4], true); if ($path === false) $path = '';
$family = base64_decode($p[3], true); if ($family === false) $family = $p[3];
$out[] = [
'ts' => (int)$p[0],
'id' => $id,
'type' => $p[2] ?: 'malware',
'family' => $family ?: 'Malware',
'path' => $path,
'source' => $p[5] ?? 'scan',
'time' => date('Y-m-d H:i:s', (int)$p[0]),
];
if (count($out) >= $limit) break;
}
return $out;
}
function bg_alert_mark_handled($id, $action) {
$id = trim((string)$id);
if ($id === '') return false;
$state = bg_alerts_state();
if (!isset($state['handled']) || !is_array($state['handled'])) $state['handled'] = [];
if (!in_array($id, $state['handled'], true)) $state['handled'][] = $id;
$state['actions'][$id] = ['action'=>(string)$action, 'at'=>time(), 'user'=>function_exists('bg_current_user') ? bg_current_user() : ''];
return bg_alerts_save_state($state);
}
function bg_alert_mail_config() {
$defaults = [
'enabled'=>false,
'use_admin_emails'=>true,
'recipients'=>'',
'from'=>'bastionguard@localhost',
'smtp_host'=>'',
'smtp_port'=>587,
'smtp_secure'=>'starttls',
'smtp_user'=>'',
'smtp_pass'=>'',
'sendmail_fallback'=>true,
];
$cfg = array_merge($defaults, bg_read_json('alert_email.json', $defaults));
$cfg['enabled'] = !empty($cfg['enabled']);
$cfg['use_admin_emails'] = !empty($cfg['use_admin_emails']);
$cfg['sendmail_fallback'] = !empty($cfg['sendmail_fallback']);
$cfg['smtp_port'] = max(1, min(65535, (int)($cfg['smtp_port'] ?? 587)));
return $cfg;
}
function bg_alert_mail_recipients(array $cfg = null) {
$cfg = $cfg ?: bg_alert_mail_config();
$out = [];
foreach (preg_split('/[,;\r\n]+/', (string)($cfg['recipients'] ?? '')) as $mail) {
$mail = trim($mail);
if ($mail !== '' && filter_var($mail, FILTER_VALIDATE_EMAIL) && !in_array($mail, $out, true)) $out[] = $mail;
}
if (!empty($cfg['use_admin_emails']) && function_exists('bg_admin_emails')) {
foreach (bg_admin_emails() as $mail) if (!in_array($mail, $out, true)) $out[] = $mail;
}
return $out;
}
function bg_smtp_read_line($fp) {
$data = '';
while (!feof($fp)) {
$line = fgets($fp, 515);
if ($line === false) break;
$data .= $line;
if (strlen($line) >= 4 && $line[3] === ' ') break;
}
return $data;
}
function bg_smtp_cmd($fp, $cmd, array $okCodes) {
if ($cmd !== null) fwrite($fp, $cmd . "\r\n");
$resp = bg_smtp_read_line($fp);
$code = (int)substr($resp, 0, 3);
if (!in_array($code, $okCodes, true)) throw new RuntimeException('SMTP error after ' . ($cmd ?: 'connect') . ': ' . trim($resp));
return $resp;
}
function bg_smtp_send(array $cfg, array $to, $subject, $body) {
$to = array_values(array_filter($to, function($m){ return filter_var($m, FILTER_VALIDATE_EMAIL); }));
if (!$to) return ['code'=>1, 'output'=>'No valid recipient.'];
$from = trim((string)($cfg['from'] ?? 'bastionguard@localhost')) ?: 'bastionguard@localhost';
$host = trim((string)($cfg['smtp_host'] ?? ''));
if ($host === '') {
if (!empty($cfg['sendmail_fallback']) && function_exists('mail')) {
$headers = "From: " . $from . "\r\nContent-Type: text/plain; charset=UTF-8\r\n";
$ok = @mail(implode(',', $to), $subject, $body, $headers);
return ['code'=>$ok?0:1, 'output'=>$ok?'Sent with PHP mail().':'PHP mail() failed.'];
}
return ['code'=>1, 'output'=>'SMTP host is not configured.'];
}
$port = max(1, min(65535, (int)($cfg['smtp_port'] ?? 587)));
$secure = (string)($cfg['smtp_secure'] ?? 'starttls');
$remote = ($secure === 'ssl' ? 'ssl://' : 'tcp://') . $host . ':' . $port;
$fp = @stream_socket_client($remote, $errno, $errstr, 20, STREAM_CLIENT_CONNECT);
if (!$fp) return ['code'=>1, 'output'=>'SMTP connect failed: ' . $errstr];
stream_set_timeout($fp, 25);
try {
bg_smtp_cmd($fp, null, [220]);
bg_smtp_cmd($fp, 'EHLO bastionguard-webui.local', [250]);
if ($secure === 'starttls') {
bg_smtp_cmd($fp, 'STARTTLS', [220]);
if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) throw new RuntimeException('STARTTLS negotiation failed.');
bg_smtp_cmd($fp, 'EHLO bastionguard-webui.local', [250]);
}
$user = (string)($cfg['smtp_user'] ?? '');
$pass = (string)($cfg['smtp_pass'] ?? '');
if ($user !== '') {
bg_smtp_cmd($fp, 'AUTH LOGIN', [334]);
bg_smtp_cmd($fp, base64_encode($user), [334]);
bg_smtp_cmd($fp, base64_encode($pass), [235]);
}
bg_smtp_cmd($fp, 'MAIL FROM:<' . $from . '>', [250]);
foreach ($to as $rcpt) bg_smtp_cmd($fp, 'RCPT TO:<' . $rcpt . '>', [250,251]);
bg_smtp_cmd($fp, 'DATA', [354]);
$headers = [];
$headers[] = 'From: ' . $from;
$headers[] = 'To: ' . implode(', ', $to);
$headers[] = 'Subject: ' . str_replace(["\r","\n"], ' ', $subject);
$headers[] = 'MIME-Version: 1.0';
$headers[] = 'Content-Type: text/plain; charset=UTF-8';
$msg = implode("\r\n", $headers) . "\r\n\r\n" . str_replace("\n.", "\n..", (string)$body) . "\r\n.";
bg_smtp_cmd($fp, $msg, [250]);
@fwrite($fp, "QUIT\r\n");
fclose($fp);
return ['code'=>0, 'output'=>'SMTP message sent.'];
} catch (Throwable $e) {
@fwrite($fp, "QUIT\r\n");
fclose($fp);
return ['code'=>1, 'output'=>$e->getMessage()];
}
}
function bg_alert_email_once(array $alert) {
$cfg = bg_alert_mail_config();
if (empty($cfg['enabled'])) return ['code'=>0, 'output'=>'Email alerts disabled.'];
$state = bg_alerts_state();
if (!isset($state['emailed']) || !is_array($state['emailed'])) $state['emailed'] = [];
$id = (string)($alert['id'] ?? '');
if ($id === '' || in_array($id, $state['emailed'], true)) return ['code'=>0, 'output'=>'Already emailed.'];
$to = bg_alert_mail_recipients($cfg);
if (!$to) return ['code'=>1, 'output'=>'No alert email recipients configured.'];
$subject = '[BastionGuard] ' . ucfirst((string)($alert['type'] ?? 'malware')) . ' detected';
$body = "BastionGuard WebPanel detected a security event.\n\n" .
"Type: " . ($alert['type'] ?? '') . "\n" .
"Family: " . ($alert['family'] ?? '') . "\n" .
"Path: " . ($alert['path'] ?? '') . "\n" .
"Source: " . ($alert['source'] ?? '') . "\n" .
"Time: " . ($alert['time'] ?? date('c')) . "\n";
$res = bg_smtp_send($cfg, $to, $subject, $body);
if (($res['code'] ?? 1) === 0) {
$state['emailed'][] = $id;
bg_alerts_save_state($state);
}
return $res;
}
function bg_quarantine_alert_path($path) {
require_once __DIR__ . '/config_manager.php';
$user = bg_detect_desktop_user();
return bg_admin_helper(['quarantine-path-b64', base64_encode((string)$path), $user ?: '-'], 120);
}
function bg_quarantine_restore_item($name, $restoreDir = '') {
require_once __DIR__ . '/config_manager.php';
$name = basename((string)$name);
if ($name === '' || $name === '.' || $name === '..') return ['code'=>64, 'output'=>'Invalid quarantine item.'];
$user = bg_detect_desktop_user();
return bg_admin_helper(['restore-quarantine-b64', base64_encode($name), base64_encode((string)$restoreDir), $user ?: '-'], 120);
}
function bg_quarantine_delete_item($name) {
require_once __DIR__ . '/config_manager.php';
$name = basename((string)$name);
if ($name === '' || $name === '.' || $name === '..') return ['code'=>64, 'output'=>'Invalid quarantine item.'];
$user = bg_detect_desktop_user();
return bg_admin_helper(['delete-quarantine-b64', base64_encode($name), $user ?: '-'], 120);
}
function bg_quarantine_metadata($qdir, $name) {
$name = basename((string)$name);
if ($name === '' || $name === '.' || $name === '..') return [];
$meta = rtrim((string)$qdir, '/') . '/.metadata/' . $name . '.json';
if (!is_readable($meta)) return [];
$raw = @file_get_contents($meta);
if (!is_string($raw) || trim($raw) === '') return [];
$json = json_decode($raw, true);
return is_array($json) ? $json : [];
}
function bg_samba_scan_privileged($path) {
require_once __DIR__ . '/config_manager.php';
$user = bg_detect_desktop_user();
if (!$user) return ['code'=>67, 'output'=>'Desktop user not detected.'];
return bg_admin_helper(['samba-scan-b64', base64_encode((string)$path), $user], 1200);
}
function bg_inotify_monitor_save(array $paths) {
require_once __DIR__ . '/config_manager.php';
$user = bg_detect_desktop_user();
if (!$user) return ['code'=>67, 'output'=>'Desktop user not detected.'];
$clean = [];
foreach ($paths as $p) { $p = trim((string)$p); if ($p !== '') $clean[] = $p; }
if (!$clean) return ['code'=>64, 'output'=>'No path configured for inotify monitoring.'];
return bg_admin_helper(['inotify-save', $user, base64_encode(implode("\n", $clean))], 60);
}
function bg_inotify_monitor_start(array $paths) {
require_once __DIR__ . '/config_manager.php';
$user = bg_detect_desktop_user();
if (!$user) return ['code'=>67, 'output'=>'Desktop user not detected.'];
$clean = [];
foreach ($paths as $p) { $p = trim((string)$p); if ($p !== '') $clean[] = $p; }
if (!$clean) return ['code'=>64, 'output'=>'No path configured for inotify monitoring.'];
return bg_admin_helper(['inotify-service-start', $user, base64_encode(implode("\n", $clean))], 90);
}
function bg_inotify_monitor_stop() {
require_once __DIR__ . '/config_manager.php';
$user = bg_detect_desktop_user();
if (!$user) return ['code'=>67, 'output'=>'Desktop user not detected.'];
return bg_admin_helper(['inotify-service-stop', $user], 45);
}
function bg_inotify_monitor_status() {
require_once __DIR__ . '/config_manager.php';
$user = bg_detect_desktop_user();
if (!$user) return ['code'=>67, 'output'=>'Desktop user not detected.'];
return bg_admin_helper(['inotify-service-status', $user], 30);
}
function bg_inotify_monitor_events($lines = 120) {
require_once __DIR__ . '/config_manager.php';
$user = bg_detect_desktop_user();
if (!$user) return ['code'=>67, 'output'=>'Desktop user not detected.'];
return bg_admin_helper(['inotify-events', $user, (string)max(1, min(500, (int)$lines))], 30);
}
bg_handle_quick_preferences();
?>