mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-24 19:00:27 +09:00
feat/complete-account-page (#15)
* wip * wip * wip * wip * wip * docs: Delete LICENSE * docs: Add GNU Affero General Public License v3 Added the GNU Affero General Public License version 3 to the project. * feat: inline frontend submodule and add auth + access control improvements * feat: Add live system log WebSocket API and frontend log viewer
This commit is contained in:
parent
5e146717d9
commit
d27af4e2e4
BIN
.github/images/architecture.png
vendored
Normal file
BIN
.github/images/architecture.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
BIN
.github/images/mantis-logo.png
vendored
Normal file
BIN
.github/images/mantis-logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
143
LICENSE
143
LICENSE
@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src=".github/images/mantis-logo.png" alt="log">
|
||||||
|
</p>
|
||||||
|
|
||||||
**Mantis** is a high-performance network security solution that combines eBPF XDP technology with deep learning models to provide advanced network protection. The system operates as a standalone network appliance that can run on any Ubuntu-based system with compatible network hardware.
|
**Mantis** is a high-performance network security solution that combines eBPF XDP technology with deep learning models to provide advanced network protection. The system operates as a standalone network appliance that can run on any Ubuntu-based system with compatible network hardware.
|
||||||
|
|
||||||
## Core Technologies
|
## Core Technologies
|
||||||
@ -10,6 +14,9 @@
|
|||||||
- **Deep Learning Models** - Identifies and predicts potential network attacks with intelligent threat detection
|
- **Deep Learning Models** - Identifies and predicts potential network attacks with intelligent threat detection
|
||||||
- **Hardware Integration** - Designed to work with Intel i350 T2 and similar enterprise-grade network interface cards
|
- **Hardware Integration** - Designed to work with Intel i350 T2 and similar enterprise-grade network interface cards
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|

|
||||||
|
|
||||||
[//]: # (## Functional Modules)
|
[//]: # (## Functional Modules)
|
||||||
|
|
||||||
[//]: # ()
|
[//]: # ()
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 66ae9de967702ef02ab47baa9597ab8f16e3ab91
|
Subproject commit 2b9c85de173c2810a8ffca183ddddcc751f4e227
|
||||||
@ -6,6 +6,7 @@ use crate::core::infrastructure::app_config::AppConfig;
|
|||||||
use crate::core::infrastructure::app_db::AppDB;
|
use crate::core::infrastructure::app_db::AppDB;
|
||||||
use crate::core::infrastructure::detection_alert::DetectionAlert;
|
use crate::core::infrastructure::detection_alert::DetectionAlert;
|
||||||
use crate::core::infrastructure::health::SystemHealth;
|
use crate::core::infrastructure::health::SystemHealth;
|
||||||
|
use crate::core::infrastructure::log_broadcaster::LogBroadcaster;
|
||||||
use crate::detection::ml::config_loader::InferenceConfig;
|
use crate::detection::ml::config_loader::InferenceConfig;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -17,4 +18,5 @@ pub struct AppState {
|
|||||||
pub health: Arc<SystemHealth>,
|
pub health: Arc<SystemHealth>,
|
||||||
pub detection_alert: Arc<DetectionAlert>,
|
pub detection_alert: Arc<DetectionAlert>,
|
||||||
pub app_db: Option<Arc<AppDB>>,
|
pub app_db: Option<Arc<AppDB>>,
|
||||||
|
pub log_broadcaster: Arc<LogBroadcaster>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use argon2::Argon2;
|
|||||||
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng};
|
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng};
|
||||||
use macros::log;
|
use macros::log;
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::model::error::Error;
|
use crate::model::error::Error;
|
||||||
use crate::model::error::auth::AuthError;
|
use crate::model::error::auth::AuthError;
|
||||||
@ -55,22 +56,24 @@ impl AppDB {
|
|||||||
Ok(Self { conn: Mutex::new(conn) })
|
Ok(Self { conn: Mutex::new(conn) })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_default_admin(&self, default_password: &str) -> Result<(), Error> {
|
pub fn has_any_account(&self) -> Result<bool, Error> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let count: i64 = conn
|
let count: i64 = conn
|
||||||
.query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
|
.query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
|
||||||
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
||||||
|
Ok(count > 0)
|
||||||
|
}
|
||||||
|
|
||||||
if count == 0 {
|
pub fn create_account(&self, username: &str, password: &str, role: &str) -> Result<(), Error> {
|
||||||
let hash = hash_password(default_password)?;
|
let hash = hash_password(password)?;
|
||||||
let now = chrono::Utc::now().timestamp();
|
let id = Uuid::new_v4().to_string();
|
||||||
conn.execute(
|
let now = chrono::Utc::now().timestamp();
|
||||||
"INSERT INTO accounts (id, username, password_hash, role, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
let conn = self.conn.lock().unwrap();
|
||||||
params!["admin", "admin", hash, "admin", now],
|
conn.execute(
|
||||||
)
|
"INSERT INTO accounts (id, username, password_hash, role, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
params![id, username, hash, role, now],
|
||||||
log!(AuthLog::DefaultAdminCreated);
|
)
|
||||||
}
|
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
53
mantis/src/core/infrastructure/log_broadcaster.rs
Normal file
53
mantis/src/core/infrastructure/log_broadcaster.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
const RING_BUFFER_SIZE: usize = 500;
|
||||||
|
const CHANNEL_CAPACITY: usize = 512;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LogRecord {
|
||||||
|
pub timestamp: String,
|
||||||
|
pub level: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LogBroadcaster {
|
||||||
|
tx: broadcast::Sender<LogRecord>,
|
||||||
|
recent: Mutex<VecDeque<LogRecord>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogBroadcaster {
|
||||||
|
pub fn new() -> Arc<Self> {
|
||||||
|
let (tx, _) = broadcast::channel(CHANNEL_CAPACITY);
|
||||||
|
Arc::new(Self {
|
||||||
|
tx,
|
||||||
|
recent: Mutex::new(VecDeque::with_capacity(RING_BUFFER_SIZE)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emit(&self, record: LogRecord) {
|
||||||
|
if let Ok(mut buf) = self.recent.lock() {
|
||||||
|
if buf.len() >= RING_BUFFER_SIZE {
|
||||||
|
buf.pop_front();
|
||||||
|
}
|
||||||
|
buf.push_back(record.clone());
|
||||||
|
}
|
||||||
|
if self.tx.receiver_count() > 0 {
|
||||||
|
let _ = self.tx.send(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self) -> broadcast::Receiver<LogRecord> {
|
||||||
|
self.tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recent_logs(&self) -> Vec<LogRecord> {
|
||||||
|
self.recent
|
||||||
|
.lock()
|
||||||
|
.map(|buf| buf.iter().cloned().collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ pub mod app_db;
|
|||||||
pub mod detection_alert;
|
pub mod detection_alert;
|
||||||
pub mod geoip;
|
pub mod geoip;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
|
pub mod log_broadcaster;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -37,11 +38,16 @@ pub struct AppServices {
|
|||||||
pub ml_engine: Arc<Engine>,
|
pub ml_engine: Arc<Engine>,
|
||||||
pub suricata_engine: Option<Arc<SuricataEngine>>,
|
pub suricata_engine: Option<Arc<SuricataEngine>>,
|
||||||
pub app_db: Option<Arc<AppDB>>,
|
pub app_db: Option<Arc<AppDB>>,
|
||||||
|
pub log_broadcaster: Arc<crate::core::infrastructure::log_broadcaster::LogBroadcaster>,
|
||||||
shutdowns: SegQueue<oneshot::Sender<()>>,
|
shutdowns: SegQueue<oneshot::Sender<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppServices {
|
impl AppServices {
|
||||||
pub fn new(app_config: Arc<AppConfig>, inference_config: Arc<InferenceConfig>) -> Result<Self, Error> {
|
pub fn new(
|
||||||
|
app_config: Arc<AppConfig>,
|
||||||
|
inference_config: Arc<InferenceConfig>,
|
||||||
|
log_broadcaster: Arc<crate::core::infrastructure::log_broadcaster::LogBroadcaster>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
let health = SystemHealth::new(app_config.clone())?;
|
let health = SystemHealth::new(app_config.clone())?;
|
||||||
let ml_models = Arc::new(MLModels::load_models(&app_config)?);
|
let ml_models = Arc::new(MLModels::load_models(&app_config)?);
|
||||||
|
|
||||||
@ -105,7 +111,6 @@ impl AppServices {
|
|||||||
let app_db = if let Some(ref auth) = app_config.auth {
|
let app_db = if let Some(ref auth) = app_config.auth {
|
||||||
let db_path = PathBuf::from(env!("DB_PATH")).join("app.db");
|
let db_path = PathBuf::from(env!("DB_PATH")).join("app.db");
|
||||||
let db = AppDB::open(db_path, &auth.db_key)?;
|
let db = AppDB::open(db_path, &auth.db_key)?;
|
||||||
db.ensure_default_admin(&auth.default_admin_password)?;
|
|
||||||
Some(Arc::new(db))
|
Some(Arc::new(db))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -119,6 +124,7 @@ impl AppServices {
|
|||||||
ml_engine,
|
ml_engine,
|
||||||
suricata_engine,
|
suricata_engine,
|
||||||
app_db,
|
app_db,
|
||||||
|
log_broadcaster,
|
||||||
shutdowns: SegQueue::new(),
|
shutdowns: SegQueue::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,9 @@ use crate::model::error::misc::MiscError;
|
|||||||
use crate::model::log::ml::MLLog;
|
use crate::model::log::ml::MLLog;
|
||||||
use crate::model::log::system::SystemLog;
|
use crate::model::log::system::SystemLog;
|
||||||
use crate::utils::logging::Logging;
|
use crate::utils::logging::Logging;
|
||||||
|
use crate::core::infrastructure::log_broadcaster::LogBroadcaster;
|
||||||
use crate::web::api::default::default_route;
|
use crate::web::api::default::default_route;
|
||||||
use crate::web::api::{auth, control, detection_alert, health, misc};
|
use crate::web::api::{auth, control, detection_alert, health, logs, misc};
|
||||||
|
|
||||||
pub struct System {
|
pub struct System {
|
||||||
pub app_config: Arc<AppConfig>,
|
pub app_config: Arc<AppConfig>,
|
||||||
@ -40,7 +41,8 @@ pub struct System {
|
|||||||
|
|
||||||
impl System {
|
impl System {
|
||||||
pub async fn new() -> Result<Self, Error> {
|
pub async fn new() -> Result<Self, Error> {
|
||||||
Logging::initialize()?;
|
let log_broadcaster = LogBroadcaster::new();
|
||||||
|
Logging::initialize(log_broadcaster.clone())?;
|
||||||
|
|
||||||
log!(SystemLog::Initializing);
|
log!(SystemLog::Initializing);
|
||||||
|
|
||||||
@ -59,7 +61,11 @@ impl System {
|
|||||||
&mut egress_ebpf,
|
&mut egress_ebpf,
|
||||||
)?);
|
)?);
|
||||||
|
|
||||||
let app_services = Arc::new(AppServices::new(app_config.clone(), inference_config.clone())?);
|
let app_services = Arc::new(AppServices::new(
|
||||||
|
app_config.clone(),
|
||||||
|
inference_config.clone(),
|
||||||
|
log_broadcaster,
|
||||||
|
)?);
|
||||||
|
|
||||||
let system = System {
|
let system = System {
|
||||||
app_config,
|
app_config,
|
||||||
@ -93,9 +99,10 @@ impl System {
|
|||||||
.run(app_services.ml_engine.clone(), app_services.suricata_engine.clone())
|
.run(app_services.ml_engine.clone(), app_services.suricata_engine.clone())
|
||||||
.await?;
|
.await?;
|
||||||
app_services.run().await?;
|
app_services.run().await?;
|
||||||
self.run_http_server().await?;
|
|
||||||
|
|
||||||
log!(SystemLog::InitializeComplete);
|
log!(SystemLog::InitializeComplete);
|
||||||
|
|
||||||
|
self.run_http_server().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,12 +160,14 @@ impl System {
|
|||||||
health: self.app_services.health.clone(),
|
health: self.app_services.health.clone(),
|
||||||
detection_alert: self.app_services.detection_alert.clone(),
|
detection_alert: self.app_services.detection_alert.clone(),
|
||||||
app_db: self.app_services.app_db.clone(),
|
app_db: self.app_services.app_db.clone(),
|
||||||
|
log_broadcaster: self.app_services.log_broadcaster.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/ebpf", control::router())
|
.nest("/ebpf", control::router())
|
||||||
.nest("/detection", detection_alert::router())
|
.nest("/detection", detection_alert::router())
|
||||||
.nest("/health", health::router())
|
.nest("/health", health::router())
|
||||||
|
.nest("/logs", logs::router())
|
||||||
.nest("/misc", misc::router())
|
.nest("/misc", misc::router())
|
||||||
.nest("/auth", auth::router())
|
.nest("/auth", auth::router())
|
||||||
.fallback(default_route)
|
.fallback(default_route)
|
||||||
|
|||||||
@ -6,8 +6,8 @@ loggable! {
|
|||||||
#[error("Auth DB initialized at {path}")]
|
#[error("Auth DB initialized at {path}")]
|
||||||
DbInitialized { path: String } => tracing::Level::INFO,
|
DbInitialized { path: String } => tracing::Level::INFO,
|
||||||
|
|
||||||
#[error("Default admin account created")]
|
#[error("First admin account registered: {username}")]
|
||||||
DefaultAdminCreated => tracing::Level::INFO,
|
FirstAdminRegistered { username: String } => tracing::Level::INFO,
|
||||||
|
|
||||||
#[error("Login successful for user: {username}")]
|
#[error("Login successful for user: {username}")]
|
||||||
LoginSuccess { username: String } => tracing::Level::INFO,
|
LoginSuccess { username: String } => tracing::Level::INFO,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||||
@ -6,13 +7,15 @@ use tracing_subscriber::filter::EnvFilter;
|
|||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|
||||||
|
use crate::core::infrastructure::log_broadcaster::LogBroadcaster;
|
||||||
use crate::model::error::Error;
|
use crate::model::error::Error;
|
||||||
use crate::model::error::io::IOError;
|
use crate::model::error::io::IOError;
|
||||||
|
use crate::utils::tracing_layer::BroadcastLayer;
|
||||||
|
|
||||||
pub struct Logging;
|
pub struct Logging;
|
||||||
|
|
||||||
impl Logging {
|
impl Logging {
|
||||||
pub fn initialize() -> Result<(), Error> {
|
pub fn initialize(broadcaster: Arc<LogBroadcaster>) -> Result<(), Error> {
|
||||||
let log_directory = "logs";
|
let log_directory = "logs";
|
||||||
fs::create_dir_all(log_directory).map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
|
fs::create_dir_all(log_directory).map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
|
||||||
|
|
||||||
@ -33,6 +36,8 @@ impl Logging {
|
|||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.with_writer(file_appender);
|
.with_writer(file_appender);
|
||||||
|
|
||||||
|
let broadcast_layer = BroadcastLayer::new(broadcaster);
|
||||||
|
|
||||||
let level = if cfg!(debug_assertions) {
|
let level = if cfg!(debug_assertions) {
|
||||||
Level::DEBUG
|
Level::DEBUG
|
||||||
} else {
|
} else {
|
||||||
@ -42,6 +47,7 @@ impl Logging {
|
|||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(stdout_layer)
|
.with(stdout_layer)
|
||||||
.with(file_layer)
|
.with(file_layer)
|
||||||
|
.with(broadcast_layer)
|
||||||
.with(EnvFilter::from_default_env().add_directive(level.into()))
|
.with(EnvFilter::from_default_env().add_directive(level.into()))
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
|||||||
@ -3,5 +3,6 @@ pub mod cpu_affinity;
|
|||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod packet_parser;
|
pub mod packet_parser;
|
||||||
pub mod static_files;
|
pub mod static_files;
|
||||||
|
pub mod tracing_layer;
|
||||||
|
|
||||||
pub mod ip_address;
|
pub mod ip_address;
|
||||||
|
|||||||
55
mantis/src/utils/tracing_layer.rs
Normal file
55
mantis/src/utils/tracing_layer.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tracing::Subscriber;
|
||||||
|
use tracing_subscriber::Layer;
|
||||||
|
use tracing_subscriber::layer::Context;
|
||||||
|
|
||||||
|
use crate::core::infrastructure::log_broadcaster::{LogBroadcaster, LogRecord};
|
||||||
|
|
||||||
|
pub struct BroadcastLayer {
|
||||||
|
broadcaster: Arc<LogBroadcaster>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BroadcastLayer {
|
||||||
|
pub fn new(broadcaster: Arc<LogBroadcaster>) -> Self {
|
||||||
|
Self { broadcaster }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MessageVisitor {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tracing::field::Visit for MessageVisitor {
|
||||||
|
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||||
|
if field.name() == "message" {
|
||||||
|
self.message = format!("{value:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
||||||
|
if field.name() == "message" {
|
||||||
|
self.message = value.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Subscriber> Layer<S> for BroadcastLayer {
|
||||||
|
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
|
||||||
|
let level = event.metadata().level().to_string();
|
||||||
|
let mut visitor = MessageVisitor { message: String::new() };
|
||||||
|
event.record(&mut visitor);
|
||||||
|
|
||||||
|
if visitor.message.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let record = LogRecord {
|
||||||
|
timestamp: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
||||||
|
level,
|
||||||
|
message: visitor.message,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.broadcaster.emit(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,11 +18,24 @@ use crate::web::middleware::auth::{AuthenticatedUser, Claims};
|
|||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/status", get(status))
|
||||||
|
.route("/register", post(register))
|
||||||
.route("/login", post(login))
|
.route("/login", post(login))
|
||||||
.route("/me", get(me))
|
.route("/me", get(me))
|
||||||
.route("/logout", post(logout))
|
.route("/logout", post(logout))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct StatusResponse {
|
||||||
|
initialized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RegisterRequest {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct LoginRequest {
|
struct LoginRequest {
|
||||||
username: String,
|
username: String,
|
||||||
@ -41,6 +54,71 @@ struct MeResponse {
|
|||||||
role: String,
|
role: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn status(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
if state.app_config.auth.is_none() {
|
||||||
|
return (StatusCode::NOT_IMPLEMENTED, "Auth not configured").into_response();
|
||||||
|
}
|
||||||
|
let db = match &state.app_db {
|
||||||
|
Some(db) => db,
|
||||||
|
None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not available").into_response(),
|
||||||
|
};
|
||||||
|
match db.has_any_account() {
|
||||||
|
Ok(initialized) => Json(StatusResponse { initialized }).into_response(),
|
||||||
|
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register(State(state): State<AppState>, Json(body): Json<RegisterRequest>) -> impl IntoResponse {
|
||||||
|
let auth_cfg = match state.app_config.auth.as_ref() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return (StatusCode::NOT_IMPLEMENTED, "Auth not configured").into_response(),
|
||||||
|
};
|
||||||
|
let db = match &state.app_db {
|
||||||
|
Some(db) => db,
|
||||||
|
None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not available").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match db.has_any_account() {
|
||||||
|
Ok(true) => return (StatusCode::FORBIDDEN, "System already initialized").into_response(),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.username.trim().is_empty() || body.password.len() < 8 {
|
||||||
|
return (StatusCode::BAD_REQUEST, "Username required and password must be at least 8 characters").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
if db.create_account(body.username.trim(), &body.password, "admin").is_err() {
|
||||||
|
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create account").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
log!(AuthLog::FirstAdminRegistered {
|
||||||
|
username: body.username.trim().to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
let account = match db.find_account_by_username(body.username.trim()) {
|
||||||
|
Ok(Some(a)) => a,
|
||||||
|
_ => return (StatusCode::INTERNAL_SERVER_ERROR, "Account lookup failed").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let exp = (Utc::now().timestamp() as usize) + (auth_cfg.token_ttl_secs as usize);
|
||||||
|
let claims = Claims {
|
||||||
|
sub: account.id,
|
||||||
|
username: account.username,
|
||||||
|
role: account.role,
|
||||||
|
exp,
|
||||||
|
};
|
||||||
|
|
||||||
|
match encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claims,
|
||||||
|
&EncodingKey::from_secret(auth_cfg.jwt_secret.as_bytes()),
|
||||||
|
) {
|
||||||
|
Ok(token) => Json(LoginResponse { token }).into_response(),
|
||||||
|
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Token generation failed").into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn login(State(state): State<AppState>, Json(body): Json<LoginRequest>) -> impl IntoResponse {
|
async fn login(State(state): State<AppState>, Json(body): Json<LoginRequest>) -> impl IntoResponse {
|
||||||
let auth_cfg = match state.app_config.auth.as_ref() {
|
let auth_cfg = match state.app_config.auth.as_ref() {
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
|
|||||||
25
mantis/src/web/api/logs.rs
Normal file
25
mantis/src/web/api/logs.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use axum::Router;
|
||||||
|
use axum::extract::{State, WebSocketUpgrade};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::routing::get;
|
||||||
|
|
||||||
|
use crate::core::app_state::AppState;
|
||||||
|
use crate::web::websocket::log_websocket::handle_log_stream;
|
||||||
|
|
||||||
|
pub fn router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/recent", get(recent_logs))
|
||||||
|
.route("/websocket", get(log_websocket))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recent_logs(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let logs = state.log_broadcaster.recent_logs();
|
||||||
|
axum::Json(logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn log_websocket(
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
ws.on_upgrade(move |socket| handle_log_stream(socket, state.log_broadcaster))
|
||||||
|
}
|
||||||
@ -3,4 +3,5 @@ pub mod control;
|
|||||||
pub mod default;
|
pub mod default;
|
||||||
pub mod detection_alert;
|
pub mod detection_alert;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
|
pub mod logs;
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
|
|||||||
64
mantis/src/web/websocket/log_websocket.rs
Normal file
64
mantis/src/web/websocket/log_websocket.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use macros::log;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
use crate::core::infrastructure::log_broadcaster::LogBroadcaster;
|
||||||
|
use crate::model::error::http::HttpError;
|
||||||
|
use crate::model::error::misc::MiscError;
|
||||||
|
use crate::model::log::http::HttpLog;
|
||||||
|
|
||||||
|
pub async fn handle_log_stream(socket: WebSocket, broadcaster: Arc<LogBroadcaster>) {
|
||||||
|
let (mut sender, mut receiver) = socket.split();
|
||||||
|
|
||||||
|
// Send buffered recent logs first
|
||||||
|
let recent = broadcaster.recent_logs();
|
||||||
|
for record in recent {
|
||||||
|
match serde_json::to_string(&record) {
|
||||||
|
Ok(json) => {
|
||||||
|
if sender.send(Message::Text(json.into())).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => log!(MiscError::SerializeError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rx = broadcaster.subscribe();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
msg = receiver.next() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(Message::Ping(data))) => {
|
||||||
|
if sender.send(Message::Pong(data)).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
Some(Ok(Message::Close(_))) | None => break,
|
||||||
|
Some(Err(e)) => {
|
||||||
|
log!(HttpError::WebSocketError { msg: e.to_string() });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = rx.recv() => {
|
||||||
|
match result {
|
||||||
|
Ok(record) => {
|
||||||
|
match serde_json::to_string(&record) {
|
||||||
|
Ok(json) => {
|
||||||
|
if sender.send(Message::Text(json.into())).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
Err(e) => log!(MiscError::SerializeError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
|
log!(HttpLog::WebSocketLaged { skipped: n });
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Closed) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
pub mod alert_websocket;
|
pub mod alert_websocket;
|
||||||
pub mod flow_websocket;
|
pub mod flow_websocket;
|
||||||
pub mod health_websocket;
|
pub mod health_websocket;
|
||||||
|
pub mod log_websocket;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user