mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
fix: resolve contract and SOAR correctness issues
This commit is contained in:
parent
fd95405ad8
commit
9a63a2e373
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
cache-dependency-path: net-guardia-frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm install
|
||||
run: npm ci
|
||||
working-directory: net-guardia-frontend
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
@ -72,14 +72,25 @@ jobs:
|
||||
ls -la "$HOME/.cargo/bin/bpf-linker"
|
||||
timeout-minutes: 45
|
||||
|
||||
- name: cargo check
|
||||
run: cargo check --package net-guardia
|
||||
- name: cargo check default workspace members
|
||||
run: cargo check
|
||||
|
||||
- name: cargo test
|
||||
run: cargo test --package net-guardia
|
||||
- name: cargo test default workspace members
|
||||
run: cargo test
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo clippy --package net-guardia -- -D warnings
|
||||
- name: cargo clippy default workspace members
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
- name: Frontend build
|
||||
run: npm run build
|
||||
working-directory: net-guardia-frontend
|
||||
|
||||
- name: Frontend tests
|
||||
run: npm test
|
||||
working-directory: net-guardia-frontend
|
||||
|
||||
- name: Trainer Python compile check
|
||||
run: python3 -m compileall -q net-guardia-trainer/src
|
||||
|
||||
integration-test:
|
||||
name: Integration Test (placeholder)
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
@ -5,6 +9,8 @@ use clap::{Parser, Subcommand};
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
const CSRF_HEADER: &str = "X-CSRF-Token";
|
||||
|
||||
/// NetGuardia CLI management tool.
|
||||
#[derive(Parser)]
|
||||
#[command(name = "ng", about = "NetGuardia CLI", version)]
|
||||
@ -94,14 +100,14 @@ impl ApiClient {
|
||||
}
|
||||
|
||||
fn load_token(&self) -> Option<String> {
|
||||
std::fs::read_to_string(&self.token_path).ok()
|
||||
fs::read_to_string(&self.token_path).ok()
|
||||
}
|
||||
|
||||
fn save_token(&self, token: &str) {
|
||||
fn save_token(&self, token: &str) -> Result<(), String> {
|
||||
if let Some(parent) = self.token_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
fs::create_dir_all(parent).map_err(|e| format!("Failed to create token directory: {}", e))?;
|
||||
}
|
||||
let _ = std::fs::write(&self.token_path, token);
|
||||
fs::write(&self.token_path, token).map_err(|e| format!("Failed to save token: {}", e))
|
||||
}
|
||||
|
||||
async fn get(&self, path: &str) -> Result<Value, String> {
|
||||
@ -127,10 +133,14 @@ impl ApiClient {
|
||||
|
||||
async fn request(&self, method: reqwest::Method, path: &str, body: Option<Value>) -> Result<Value, String> {
|
||||
let url = format!("{}{}", self.base_url, path);
|
||||
let include_csrf = should_send_csrf(&method);
|
||||
let mut req = self.client.request(method, &url);
|
||||
if let Some(token) = self.load_token() {
|
||||
req = req.header("Authorization", format!("Bearer {}", token.trim()));
|
||||
}
|
||||
if include_csrf {
|
||||
req = req.header(CSRF_HEADER, "ng-cli");
|
||||
}
|
||||
if let Some(b) = body {
|
||||
req = req.json(&b);
|
||||
}
|
||||
@ -187,11 +197,17 @@ fn print_json(data: &Value) {
|
||||
println!("{}", serde_json::to_string_pretty(data).unwrap_or_default());
|
||||
}
|
||||
|
||||
fn should_send_csrf(method: &reqwest::Method) -> bool {
|
||||
!matches!(
|
||||
*method,
|
||||
reqwest::Method::GET | reqwest::Method::HEAD | reqwest::Method::OPTIONS
|
||||
)
|
||||
}
|
||||
|
||||
fn read_password() -> String {
|
||||
// Disable echo for password input
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::io::AsRawFd;
|
||||
let fd = std::io::stdin().as_raw_fd();
|
||||
let mut termios = unsafe { std::mem::zeroed::<libc::termios>() };
|
||||
unsafe { libc::tcgetattr(fd, &mut termios) };
|
||||
@ -285,21 +301,24 @@ async fn main() {
|
||||
},
|
||||
Commands::Login => {
|
||||
print!("Username: ");
|
||||
std::io::Write::flush(&mut std::io::stdout()).unwrap();
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.flush().unwrap();
|
||||
let mut username = String::new();
|
||||
std::io::stdin().read_line(&mut username).unwrap();
|
||||
let username = username.trim();
|
||||
|
||||
print!("Password: ");
|
||||
std::io::Write::flush(&mut std::io::stdout()).unwrap();
|
||||
stdout.flush().unwrap();
|
||||
let password = read_password();
|
||||
|
||||
match api.login(username, &password).await {
|
||||
Ok(token) => {
|
||||
api.save_token(&token);
|
||||
println!("Login successful. Token saved to ~/.ng/token");
|
||||
Ok(())
|
||||
}
|
||||
Ok(token) => match api.save_token(&token) {
|
||||
Ok(()) => {
|
||||
println!("Login successful. Token saved to ~/.ng/token");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
@ -361,3 +380,18 @@ async fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn csrf_header_is_only_needed_for_state_changing_methods() {
|
||||
assert!(!should_send_csrf(&reqwest::Method::GET));
|
||||
assert!(!should_send_csrf(&reqwest::Method::HEAD));
|
||||
assert!(!should_send_csrf(&reqwest::Method::OPTIONS));
|
||||
assert!(should_send_csrf(&reqwest::Method::POST));
|
||||
assert!(should_send_csrf(&reqwest::Method::PUT));
|
||||
assert!(should_send_csrf(&reqwest::Method::DELETE));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit 9f1621ca8f2c9fe4e3a5e315abbc0b6c5f16498d
|
||||
Subproject commit dd0e9eae657a1240964a9f5e3c9781674885d623
|
||||
@ -157,7 +157,9 @@ async fn remove_ssh_service(
|
||||
}
|
||||
|
||||
async fn is_ssh_white_list_enable(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.is_ssh_white_list_enable())
|
||||
HttpResponse::Ok().json(serde_json::json!({
|
||||
"enabled": service.is_ssh_white_list_enable(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn enable_ssh_white_list(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
|
||||
@ -144,14 +144,10 @@ async fn complete_setup(
|
||||
match password::hash_password(&body.admin_password) {
|
||||
Ok(hash) => {
|
||||
// Find admin user and update password
|
||||
if let Ok(Some(user)) = db.find_user(DEFAULT_ADMIN_USERNAME).await {
|
||||
if let Err(e) = db.update_user_password(user.id, &hash).await {
|
||||
log!(SystemError::SetupPasswordUpdateFailed(e));
|
||||
}
|
||||
// Clear force_password_change since setup wizard set the password
|
||||
if let Err(e) = db.reset_user_password(user.id, &hash).await {
|
||||
log!(SystemError::SetupPasswordUpdateFailed(e));
|
||||
}
|
||||
if let Ok(Some(user)) = db.find_user(DEFAULT_ADMIN_USERNAME).await
|
||||
&& let Err(e) = db.update_user_password(user.id, &hash).await
|
||||
{
|
||||
log!(SystemError::SetupPasswordUpdateFailed(e));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@ -159,19 +159,29 @@ impl Database {
|
||||
let existing_acl_count: i64 = tx.query_row(
|
||||
"SELECT COUNT(*) FROM acl_rules
|
||||
WHERE ip_version = ?1 AND direction = ?2 AND list_type = ?3 AND ip_address = ?4 AND port = ?5",
|
||||
params![ip_version, "source", "blacklist", source_ip, 0i64],
|
||||
params![ip_version, "source", "blacklist", source_ip.as_str(), 0i64],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let created_acl_rule = existing_acl_count == 0;
|
||||
let active_soar_owned_count: i64 = tx.query_row(
|
||||
"SELECT COUNT(*) FROM soar_block_rules
|
||||
WHERE source_ip = ?1
|
||||
AND unblocked_at IS NULL
|
||||
AND expires_at > datetime('now')
|
||||
AND created_acl_rule = 1
|
||||
AND preserve_acl_on_unblock = 0",
|
||||
params![source_ip.as_str()],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let created_acl_rule = existing_acl_count == 0 || active_soar_owned_count > 0;
|
||||
tx.execute(
|
||||
"INSERT INTO soar_block_rules (source_ip, playbook_id, expires_at, created_acl_rule)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
params![source_ip, playbook_id, expires_at, created_acl_rule as i64],
|
||||
params![source_ip.as_str(), playbook_id, expires_at, created_acl_rule as i64],
|
||||
)?;
|
||||
let soar_block_id = tx.last_insert_rowid();
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO acl_rules (ip_version, direction, list_type, ip_address, port) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![ip_version, "source", "blacklist", source_ip, 0i64],
|
||||
params![ip_version, "source", "blacklist", source_ip.as_str(), 0i64],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(soar_block_id)
|
||||
@ -190,16 +200,24 @@ impl Database {
|
||||
.conn_mut_and_then(move |conn| {
|
||||
let tx = conn.transaction()?;
|
||||
let should_delete_acl: bool = tx.query_row(
|
||||
"SELECT created_acl_rule = 1 AND preserve_acl_on_unblock = 0
|
||||
"SELECT created_acl_rule = 1
|
||||
AND preserve_acl_on_unblock = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM soar_block_rules
|
||||
WHERE id != ?1
|
||||
AND source_ip = ?2
|
||||
AND unblocked_at IS NULL
|
||||
AND expires_at > datetime('now')
|
||||
)
|
||||
FROM soar_block_rules WHERE id = ?1",
|
||||
params![soar_block_id],
|
||||
params![soar_block_id, source_ip.as_str()],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if should_delete_acl {
|
||||
tx.execute(
|
||||
"DELETE FROM acl_rules
|
||||
WHERE ip_version = ?1 AND direction = ?2 AND list_type = ?3 AND ip_address = ?4 AND port = ?5",
|
||||
params![ip_version, "source", "blacklist", source_ip, 0i64],
|
||||
params![ip_version, "source", "blacklist", source_ip.as_str(), 0i64],
|
||||
)?;
|
||||
}
|
||||
tx.execute(
|
||||
@ -236,3 +254,69 @@ impl DbAdminRepo for Database {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SOURCE_IP: &str = "198.51.100.42";
|
||||
const ACTIVE_UNTIL: &str = "2999-01-01 00:00:00";
|
||||
|
||||
fn acl_contains(rules: &[AclRuleViewForTest], ip: &str) -> bool {
|
||||
rules
|
||||
.iter()
|
||||
.any(|rule| rule.ip_address == ip && rule.direction == "source" && rule.list_type == "blacklist")
|
||||
}
|
||||
|
||||
type AclRuleViewForTest = crate::domain::data_plane::acl_rule::AclRuleView;
|
||||
|
||||
#[tokio::test]
|
||||
async fn overlapping_soar_blocks_keep_acl_until_last_block_unblocks() {
|
||||
let db = Database::new(":memory:").await.expect("database");
|
||||
let first = db
|
||||
.commit_soar_block_to_db(SOURCE_IP, 4, 1, ACTIVE_UNTIL)
|
||||
.await
|
||||
.expect("first block");
|
||||
let second = db
|
||||
.commit_soar_block_to_db(SOURCE_IP, 4, 2, ACTIVE_UNTIL)
|
||||
.await
|
||||
.expect("second block");
|
||||
|
||||
db.commit_soar_unblock_to_db(first, 4, SOURCE_IP)
|
||||
.await
|
||||
.expect("first unblock");
|
||||
assert!(
|
||||
acl_contains(&db.list_acl_rules().await.expect("acl after first unblock"), SOURCE_IP),
|
||||
"ACL row must stay while an overlapping SOAR block is active"
|
||||
);
|
||||
|
||||
db.commit_soar_unblock_to_db(second, 4, SOURCE_IP)
|
||||
.await
|
||||
.expect("second unblock");
|
||||
assert!(
|
||||
!acl_contains(&db.list_acl_rules().await.expect("acl after second unblock"), SOURCE_IP),
|
||||
"last SOAR-owned block should remove the SOAR-owned ACL row"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manual_acl_existing_before_soar_block_is_preserved_on_unblock() {
|
||||
let db = Database::new(":memory:").await.expect("database");
|
||||
db.insert_acl_rule(4, "source", "blacklist", SOURCE_IP, 0)
|
||||
.await
|
||||
.expect("manual acl");
|
||||
let block = db
|
||||
.commit_soar_block_to_db(SOURCE_IP, 4, 1, ACTIVE_UNTIL)
|
||||
.await
|
||||
.expect("soar block");
|
||||
|
||||
db.commit_soar_unblock_to_db(block, 4, SOURCE_IP)
|
||||
.await
|
||||
.expect("unblock");
|
||||
|
||||
assert!(
|
||||
acl_contains(&db.list_acl_rules().await.expect("acl rules"), SOURCE_IP),
|
||||
"pre-existing manual ACL row must remain after SOAR unblock"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user