malwaredb 0.3.5

Service for storing malicious, benign, or unknown files and related metadata and relationships.
// SPDX-License-Identifier: Apache-2.0

mod groups;
mod labels;
mod sources;
mod users;

use crate::cli::config::Config;
use groups::GroupTable;
use labels::LabelTable;
use malwaredb_server::State;
use sources::SourceTable;
use users::UserTable;

use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;

use anyhow::Result;
use clap::{Args, ValueHint};
use eframe::egui::Color32;

const INVALID_BG: Color32 = Color32::from_rgb(255, 180, 180);

/// Admin Tabs
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
enum Tab {
    /// User information
    #[default]
    Users,

    /// Group information
    Groups,

    /// Source information
    Sources,

    /// List of labels
    Labels,

    /// About Malware DB
    About,
}

/// Admin GUI using egui
#[derive(Clone, Debug, Args)]
pub struct AdminGui {
    /// Path to a Malware DB config file for access to a database
    #[arg(value_name = "FILE", value_hint = ValueHint::FilePath)]
    config_file: PathBuf,
}

impl AdminGui {
    /// Launch the Admin GUI
    ///
    /// # Errors
    ///
    /// Returns an error if the GUI cannot be launched
    #[allow(clippy::unnecessary_wraps)]
    pub async fn execute(self) -> Result<ExitCode> {
        let state = Config::from_file(&self.config_file)?.into_state().await?;
        let admin_gui_state = AdminGuiState::new(state).await?;

        let options = eframe::NativeOptions {
            viewport: eframe::egui::ViewportBuilder::default()
                .with_inner_size([500.0, 300.0])
                .with_resizable(true),
            ..Default::default()
        };

        match eframe::run_native(
            "MalwareDB",
            options,
            Box::new(|cc| {
                egui_extras::install_image_loaders(&cc.egui_ctx);
                Ok(Box::new(admin_gui_state))
            }),
        ) {
            Ok(()) => Ok(ExitCode::SUCCESS),
            Err(_) => Ok(ExitCode::FAILURE),
        }
    }
}

struct AdminGuiState {
    state: Arc<State>,
    active_tab: Tab,
    user_table: UserTable,
    group_table: GroupTable,
    source_table: SourceTable,
    label_table: LabelTable,
}

impl AdminGuiState {
    async fn new(state: State) -> Result<Self> {
        let state = Arc::new(state);
        let user_table = UserTable::new(state.clone()).await?;
        let group_table = GroupTable::new(state.clone()).await?;
        let source_table = SourceTable::new(state.clone()).await?;
        let label_table = LabelTable::new(state.clone()).await?;

        Ok(Self {
            active_tab: Tab::default(),
            user_table,
            group_table,
            source_table,
            label_table,
            state,
        })
    }
}

impl eframe::App for AdminGuiState {
    fn ui(&mut self, ui: &mut eframe::egui::Ui, _frame: &mut eframe::Frame) {
        ui.request_repaint();
        eframe::egui::Panel::top("menu").show_inside(ui, |ui| {
            ui.horizontal(|ui| {
                ui.selectable_value(&mut self.active_tab, Tab::Users, "Users");
                ui.selectable_value(&mut self.active_tab, Tab::Groups, "Groups");
                ui.selectable_value(&mut self.active_tab, Tab::Sources, "Sources");
                ui.selectable_value(&mut self.active_tab, Tab::Labels, "Labels");
                ui.selectable_value(&mut self.active_tab, Tab::About, "About");
            });
        });

        eframe::egui::Panel::bottom("theme").show_inside(ui, |ui| {
            eframe::egui::widgets::global_theme_preference_buttons(ui);
        });

        eframe::egui::CentralPanel::default().show_inside(ui, |ui| match self.active_tab {
            Tab::Users => self.user_table.ui(ui),
            Tab::Groups => self.group_table.ui(ui),
            Tab::Sources => self.source_table.ui(ui),
            Tab::Labels => self.label_table.ui(ui),
            Tab::About => {
                ui.add(
                    eframe::egui::Image::new(eframe::egui::include_image!("logo.png"))
                        .max_width(30.0),
                );
                ui.label(format!("MalwareDB {}", crate::MDB_VERSION));
                ui.hyperlink_to("https://malwaredb.net", "https://malwaredb.net");
                ui.hyperlink_to(
                    "https://github.com/malwaredb/malwaredb-rs",
                    "https://github.com/malwaredb/malwaredb-rs",
                );

                ui.separator();
                #[cfg(feature = "vt")]
                ui.label("VirusTotal support available.");
                #[cfg(not(feature = "vt"))]
                ui.label("VirusTotal support not available.");
                #[cfg(feature = "yara")]
                ui.label("Yara support available.");
                #[cfg(not(feature = "yara"))]
                ui.label("Yara support not available.");

                if let Ok(server_info) = futures::executor::block_on(self.state.db_type.db_info()) {
                    ui.separator();
                    ui.label(format!("Database: {}", server_info.version));
                    ui.label(format!("Files: {}", server_info.num_files));
                    ui.label(format!("Size on disk (samples + database): {}", server_info.size));
                }
            }
        });
    }
}