2025-06-24 21:43:26 +02:00
|
|
|
|
use crate::errors::NyxResult;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
use crate::patterns::Severity;
|
|
|
|
|
|
use console::style;
|
2025-06-16 16:46:22 +02:00
|
|
|
|
use serde::{Deserialize, Serialize};
|
2026-02-25 04:02:11 -05:00
|
|
|
|
use std::collections::HashMap;
|
2025-06-16 16:46:22 +02:00
|
|
|
|
use std::fs;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
use std::path::Path;
|
2025-06-16 16:46:22 +02:00
|
|
|
|
use toml;
|
|
|
|
|
|
|
2025-06-24 21:43:26 +02:00
|
|
|
|
static DEFAULT_CONFIG_TOML: &str = include_str!("../../default-nyx.conf");
|
|
|
|
|
|
|
2025-06-28 17:36:14 +02:00
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq)]
|
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
|
pub enum AnalysisMode {
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
Full,
|
|
|
|
|
|
Ast,
|
|
|
|
|
|
Taint,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-16 16:46:22 +02:00
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct ScannerConfig {
|
2025-06-28 17:36:14 +02:00
|
|
|
|
/// The analysis mode to use.
|
|
|
|
|
|
pub mode: AnalysisMode,
|
|
|
|
|
|
|
2025-06-17 01:17:48 +02:00
|
|
|
|
/// The minimum severity level to output
|
|
|
|
|
|
pub min_severity: Severity,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2025-06-23 16:51:39 +02:00
|
|
|
|
/// The maximum file size to scan, in megabytes.
|
|
|
|
|
|
pub max_file_size_mb: Option<u64>,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2025-06-23 16:51:39 +02:00
|
|
|
|
/// File extensions to exclude from scanning.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub excluded_extensions: Vec<String>,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2025-06-23 16:51:39 +02:00
|
|
|
|
/// Directories to exclude from scanning.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub excluded_directories: Vec<String>,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2025-06-16 23:47:50 +02:00
|
|
|
|
/// Excluded files
|
|
|
|
|
|
pub excluded_files: Vec<String>,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
2025-06-23 16:51:39 +02:00
|
|
|
|
/// Whether to respect the global ignore file or not.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub read_global_ignore: bool,
|
|
|
|
|
|
|
2025-06-23 16:51:39 +02:00
|
|
|
|
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub read_vcsignore: bool,
|
|
|
|
|
|
|
2025-06-23 16:51:39 +02:00
|
|
|
|
/// Whether to require a `.git` directory to respect gitignore files.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub require_git_to_read_vcsignore: bool,
|
|
|
|
|
|
|
2025-06-16 23:47:50 +02:00
|
|
|
|
/// Whether to limit the search to starting file system or not.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub one_file_system: bool,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
|
|
|
|
|
/// Whether to follow symlinks or not.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub follow_symlinks: bool,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2025-06-23 16:51:39 +02:00
|
|
|
|
/// Whether to scan hidden files or not.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub scan_hidden_files: bool,
|
2026-02-25 04:02:11 -05:00
|
|
|
|
|
|
|
|
|
|
/// Whether to include findings from non-production paths (tests, vendor,
|
|
|
|
|
|
/// benchmarks, etc.) at their original severity. When false (default),
|
|
|
|
|
|
/// findings in these paths are downgraded by one severity tier.
|
|
|
|
|
|
pub include_nonprod: bool,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
impl Default for ScannerConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
2025-06-28 17:36:14 +02:00
|
|
|
|
mode: AnalysisMode::Full,
|
2025-06-17 01:17:48 +02:00
|
|
|
|
min_severity: Severity::Low,
|
2025-06-23 16:51:39 +02:00
|
|
|
|
max_file_size_mb: None,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
excluded_extensions: vec![
|
2025-06-24 20:27:06 +02:00
|
|
|
|
"jpg", "png", "gif", "mp4", "avi", "mkv", "zip", "tar", "gz", "exe", "dll", "so",
|
2025-06-16 16:46:22 +02:00
|
|
|
|
]
|
2025-06-24 20:27:06 +02:00
|
|
|
|
.into_iter()
|
|
|
|
|
|
.map(str::to_owned)
|
|
|
|
|
|
.collect(),
|
2025-06-16 16:46:22 +02:00
|
|
|
|
excluded_directories: vec![
|
2025-06-24 20:27:06 +02:00
|
|
|
|
"node_modules",
|
|
|
|
|
|
".git",
|
|
|
|
|
|
"target",
|
|
|
|
|
|
".vscode",
|
|
|
|
|
|
".idea",
|
|
|
|
|
|
"build",
|
|
|
|
|
|
"dist",
|
2025-06-16 16:46:22 +02:00
|
|
|
|
]
|
2025-06-24 20:27:06 +02:00
|
|
|
|
.into_iter()
|
|
|
|
|
|
.map(str::to_owned)
|
|
|
|
|
|
.collect(),
|
|
|
|
|
|
excluded_files: vec![].into_iter().map(str::to_owned).collect(),
|
2025-06-16 16:46:22 +02:00
|
|
|
|
read_global_ignore: false,
|
|
|
|
|
|
read_vcsignore: true,
|
|
|
|
|
|
require_git_to_read_vcsignore: true,
|
|
|
|
|
|
one_file_system: false,
|
|
|
|
|
|
follow_symlinks: false,
|
|
|
|
|
|
scan_hidden_files: false,
|
2026-02-25 04:02:11 -05:00
|
|
|
|
include_nonprod: false,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct DatabaseConfig {
|
2025-06-24 20:27:06 +02:00
|
|
|
|
/// Custom path for database
|
|
|
|
|
|
pub path: String,
|
|
|
|
|
|
|
2025-06-16 16:46:22 +02:00
|
|
|
|
/// The number of days to keep database files for. TODO: IMPLEMENT
|
|
|
|
|
|
pub auto_cleanup_days: u32,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2025-06-16 16:46:22 +02:00
|
|
|
|
/// The maximum size of the database, in megabytes. TODO: IMPLEMENT
|
|
|
|
|
|
pub max_db_size_mb: u64,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
/// Whether to run a VACUUM on startup or not.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub vacuum_on_startup: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
impl Default for DatabaseConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
2025-06-24 20:27:06 +02:00
|
|
|
|
path: String::from(""),
|
2025-06-16 16:46:22 +02:00
|
|
|
|
auto_cleanup_days: 30,
|
|
|
|
|
|
max_db_size_mb: 1024,
|
|
|
|
|
|
vacuum_on_startup: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct OutputConfig {
|
2026-02-25 04:02:11 -05:00
|
|
|
|
/// The default output format.
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub default_format: String,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
/// Whether to print anything to the console or not.
|
2025-06-24 21:43:26 +02:00
|
|
|
|
pub quiet: bool,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
|
|
|
|
|
/// The maximum number of results to show.
|
|
|
|
|
|
pub max_results: Option<u32>,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for OutputConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
2025-06-17 16:46:45 +02:00
|
|
|
|
default_format: "console".into(),
|
2025-06-24 21:43:26 +02:00
|
|
|
|
quiet: false,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
max_results: None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct PerformanceConfig {
|
|
|
|
|
|
/// The maximum search depth, or `None` if no maximum search depth should be set.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// A depth of `1` includes all files under the current directory, a depth of `2` also includes
|
2025-06-24 20:27:06 +02:00
|
|
|
|
/// all files under subdirectories of the current directory, etc.
|
2026-02-25 04:02:11 -05:00
|
|
|
|
pub max_depth: Option<usize>,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
|
|
|
|
|
/// The minimum depth for reported entries, or `None`.
|
2026-02-25 04:02:11 -05:00
|
|
|
|
pub min_depth: Option<usize>,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
|
|
|
|
|
/// Whether to stop traversing into matching directories.
|
2025-06-24 20:27:06 +02:00
|
|
|
|
pub prune: bool,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
|
|
|
|
|
/// The maximum number of worker threads to use., or `None` to auto-detect.
|
2025-06-24 20:27:06 +02:00
|
|
|
|
pub worker_threads: Option<usize>,
|
|
|
|
|
|
|
2025-06-16 16:46:22 +02:00
|
|
|
|
/// The maximum number of entries to index in a single chunk.
|
2025-06-24 20:27:06 +02:00
|
|
|
|
pub batch_size: usize,
|
|
|
|
|
|
|
|
|
|
|
|
/// capacity = threads × this
|
|
|
|
|
|
pub channel_multiplier: usize,
|
|
|
|
|
|
|
2025-06-28 17:36:14 +02:00
|
|
|
|
/// The stack size for Rayon threads, in bytes.
|
|
|
|
|
|
pub rayon_thread_stack_size: usize,
|
|
|
|
|
|
|
2025-06-24 20:27:06 +02:00
|
|
|
|
/// Timeout on individual files // TODO: IMPLEMENT
|
|
|
|
|
|
pub scan_timeout_secs: Option<u64>,
|
|
|
|
|
|
|
2025-06-16 16:46:22 +02:00
|
|
|
|
/// The maximum amount of memory to use, in megabytes.
|
|
|
|
|
|
pub memory_limit_mb: u64, // TODO: IMPLEMENT
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for PerformanceConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
max_depth: None,
|
|
|
|
|
|
min_depth: None,
|
|
|
|
|
|
prune: false,
|
|
|
|
|
|
worker_threads: None,
|
2025-06-24 20:27:06 +02:00
|
|
|
|
batch_size: 100usize,
|
|
|
|
|
|
channel_multiplier: 4usize,
|
2025-06-28 17:36:14 +02:00
|
|
|
|
rayon_thread_stack_size: 8 * 1024 * 1024, // 2 MiB
|
2025-06-24 20:27:06 +02:00
|
|
|
|
scan_timeout_secs: None,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
memory_limit_mb: 512,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
/// A single user-defined label rule from config.
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
|
|
|
|
|
pub struct ConfigLabelRule {
|
|
|
|
|
|
pub matchers: Vec<String>,
|
|
|
|
|
|
/// "source", "sanitizer", or "sink"
|
|
|
|
|
|
pub kind: String,
|
|
|
|
|
|
/// Capability name: "html_escape", "shell_escape", "url_encode", "json_parse",
|
|
|
|
|
|
/// "env_var", "file_io", or "all"
|
|
|
|
|
|
pub cap: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Per-language analysis configuration from config file.
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct LanguageAnalysisConfig {
|
|
|
|
|
|
pub rules: Vec<ConfigLabelRule>,
|
|
|
|
|
|
pub terminators: Vec<String>,
|
|
|
|
|
|
pub event_handlers: Vec<String>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Top-level analysis rules config, keyed by language slug.
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct AnalysisRulesConfig {
|
|
|
|
|
|
pub languages: HashMap<String, LanguageAnalysisConfig>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-16 16:46:22 +02:00
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
|
|
|
|
#[serde(default)]
|
2025-06-16 23:52:39 +02:00
|
|
|
|
#[derive(Default)]
|
2025-06-16 16:46:22 +02:00
|
|
|
|
pub struct Config {
|
|
|
|
|
|
pub scanner: ScannerConfig,
|
|
|
|
|
|
pub database: DatabaseConfig,
|
|
|
|
|
|
pub output: OutputConfig,
|
|
|
|
|
|
pub performance: PerformanceConfig,
|
2026-02-25 04:02:11 -05:00
|
|
|
|
pub analysis: AnalysisRulesConfig,
|
2025-06-16 16:46:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Config {
|
2026-02-25 04:02:11 -05:00
|
|
|
|
/// Load config and return `(config, optional_note)`.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// The note is a formatted status message about which config file was
|
|
|
|
|
|
/// loaded (or that defaults are in use). The caller decides whether to
|
|
|
|
|
|
/// print it based on output format / quiet mode.
|
|
|
|
|
|
pub fn load(config_dir: &Path) -> NyxResult<(Self, Option<String>)> {
|
2025-06-16 16:46:22 +02:00
|
|
|
|
let mut config = Config::default();
|
|
|
|
|
|
|
2025-06-17 10:55:50 +02:00
|
|
|
|
let default_config_path = config_dir.join("nyx.conf");
|
2025-06-16 16:46:22 +02:00
|
|
|
|
if !default_config_path.exists() {
|
|
|
|
|
|
create_example_config(config_dir)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 10:55:50 +02:00
|
|
|
|
let user_config_path = config_dir.join("nyx.local");
|
2026-02-25 04:02:11 -05:00
|
|
|
|
let note = if user_config_path.exists() {
|
2025-06-16 16:46:22 +02:00
|
|
|
|
let user_config_content = fs::read_to_string(&user_config_path)?;
|
|
|
|
|
|
let user_config: Config = toml::from_str(&user_config_content)?;
|
|
|
|
|
|
|
|
|
|
|
|
config = merge_configs(config, user_config);
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
Some(format!(
|
2025-06-24 20:27:06 +02:00
|
|
|
|
"{}: Loaded user config from: {}\n",
|
|
|
|
|
|
style("note").green().bold(),
|
|
|
|
|
|
style(user_config_path.display())
|
|
|
|
|
|
.underlined()
|
|
|
|
|
|
.white()
|
|
|
|
|
|
.bold()
|
2026-02-25 04:02:11 -05:00
|
|
|
|
))
|
2025-06-16 16:46:22 +02:00
|
|
|
|
} else {
|
2026-02-25 04:02:11 -05:00
|
|
|
|
Some(format!(
|
|
|
|
|
|
"{}: Using {} configuration.\n Create file in '{}' to customize.\n",
|
2025-06-24 20:27:06 +02:00
|
|
|
|
style("note").green().bold(),
|
|
|
|
|
|
style("default").bold(),
|
|
|
|
|
|
style(user_config_path.display())
|
|
|
|
|
|
.underlined()
|
|
|
|
|
|
.white()
|
|
|
|
|
|
.bold()
|
2026-02-25 04:02:11 -05:00
|
|
|
|
))
|
|
|
|
|
|
};
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
Ok((config, note))
|
2025-06-16 16:46:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 21:43:26 +02:00
|
|
|
|
fn create_example_config(config_dir: &Path) -> NyxResult<()> {
|
2025-06-17 10:55:50 +02:00
|
|
|
|
let example_path = config_dir.join("nyx.conf");
|
2025-06-24 21:43:26 +02:00
|
|
|
|
if !example_path.exists() {
|
|
|
|
|
|
fs::write(&example_path, DEFAULT_CONFIG_TOML)?;
|
|
|
|
|
|
tracing::debug!("Example config created at: {}", example_path.display());
|
|
|
|
|
|
}
|
2025-06-16 16:46:22 +02:00
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Merge user config into default config, preserving defaults where the user didn't
|
|
|
|
|
|
/// supply new exclusions and overriding everything else.
|
|
|
|
|
|
fn merge_configs(mut default: Config, user: Config) -> Config {
|
|
|
|
|
|
// --- ScannerConfig ---
|
2025-06-28 17:36:14 +02:00
|
|
|
|
default.scanner.mode = user.scanner.mode;
|
2025-06-24 21:43:26 +02:00
|
|
|
|
default.scanner.min_severity = user.scanner.min_severity;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
default.scanner.max_file_size_mb = user.scanner.max_file_size_mb;
|
|
|
|
|
|
default.scanner.read_global_ignore = user.scanner.read_global_ignore;
|
|
|
|
|
|
default.scanner.read_vcsignore = user.scanner.read_vcsignore;
|
|
|
|
|
|
default.scanner.require_git_to_read_vcsignore = user.scanner.require_git_to_read_vcsignore;
|
|
|
|
|
|
default.scanner.one_file_system = user.scanner.one_file_system;
|
|
|
|
|
|
default.scanner.follow_symlinks = user.scanner.follow_symlinks;
|
|
|
|
|
|
default.scanner.scan_hidden_files = user.scanner.scan_hidden_files;
|
2026-02-25 04:02:11 -05:00
|
|
|
|
default.scanner.include_nonprod = user.scanner.include_nonprod;
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
|
|
|
|
|
// Merge exclusion lists (default ⊔ user), then sort & dedupe
|
2025-06-24 20:27:06 +02:00
|
|
|
|
default
|
|
|
|
|
|
.scanner
|
|
|
|
|
|
.excluded_extensions
|
|
|
|
|
|
.extend(user.scanner.excluded_extensions);
|
|
|
|
|
|
default
|
|
|
|
|
|
.scanner
|
|
|
|
|
|
.excluded_directories
|
|
|
|
|
|
.extend(user.scanner.excluded_directories);
|
2025-06-16 16:46:22 +02:00
|
|
|
|
default.scanner.excluded_extensions.sort_unstable();
|
|
|
|
|
|
default.scanner.excluded_extensions.dedup();
|
|
|
|
|
|
default.scanner.excluded_directories.sort_unstable();
|
|
|
|
|
|
default.scanner.excluded_directories.dedup();
|
|
|
|
|
|
|
|
|
|
|
|
// --- DatabaseConfig ---
|
2025-06-24 21:43:26 +02:00
|
|
|
|
default.database.path = user.database.path;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
default.database.auto_cleanup_days = user.database.auto_cleanup_days;
|
|
|
|
|
|
default.database.max_db_size_mb = user.database.max_db_size_mb;
|
|
|
|
|
|
default.database.vacuum_on_startup = user.database.vacuum_on_startup;
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
|
|
|
|
|
// --- OutputConfig ---
|
2025-06-24 20:27:06 +02:00
|
|
|
|
default.output.default_format = user.output.default_format;
|
2025-06-24 21:43:26 +02:00
|
|
|
|
default.output.quiet = user.output.quiet;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
default.output.max_results = user.output.max_results;
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
|
|
|
|
|
// --- PerformanceConfig ---
|
2025-06-24 20:27:06 +02:00
|
|
|
|
default.performance.max_depth = user.performance.max_depth;
|
|
|
|
|
|
default.performance.min_depth = user.performance.min_depth;
|
|
|
|
|
|
default.performance.prune = user.performance.prune;
|
|
|
|
|
|
default.performance.worker_threads = user.performance.worker_threads;
|
|
|
|
|
|
default.performance.batch_size = user.performance.batch_size;
|
|
|
|
|
|
default.performance.channel_multiplier = user.performance.channel_multiplier;
|
2025-06-28 17:36:14 +02:00
|
|
|
|
default.performance.rayon_thread_stack_size = user.performance.rayon_thread_stack_size;
|
2025-06-24 21:43:26 +02:00
|
|
|
|
default.performance.scan_timeout_secs = user.performance.scan_timeout_secs;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
default.performance.memory_limit_mb = user.performance.memory_limit_mb;
|
2025-06-16 16:46:22 +02:00
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
// --- AnalysisRulesConfig ---
|
|
|
|
|
|
for (lang, user_lang_cfg) in user.analysis.languages {
|
|
|
|
|
|
let entry = default.analysis.languages.entry(lang).or_default();
|
|
|
|
|
|
|
|
|
|
|
|
// Union-merge rules with dedup
|
|
|
|
|
|
for rule in user_lang_cfg.rules {
|
|
|
|
|
|
if !entry.rules.contains(&rule) {
|
|
|
|
|
|
entry.rules.push(rule);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Union-merge terminators with dedup
|
|
|
|
|
|
for t in user_lang_cfg.terminators {
|
|
|
|
|
|
if !entry.terminators.contains(&t) {
|
|
|
|
|
|
entry.terminators.push(t);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Union-merge event_handlers with dedup
|
|
|
|
|
|
for eh in user_lang_cfg.event_handlers {
|
|
|
|
|
|
if !entry.event_handlers.contains(&eh) {
|
|
|
|
|
|
entry.event_handlers.push(eh);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-16 16:46:22 +02:00
|
|
|
|
default
|
2025-06-24 20:27:06 +02:00
|
|
|
|
}
|
2025-06-24 23:18:01 +02:00
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn merge_configs_dedupes_and_keeps_order() {
|
2025-06-24 23:38:32 +02:00
|
|
|
|
let mut default_cfg = Config::default();
|
|
|
|
|
|
default_cfg.scanner.excluded_extensions = vec!["rs".into(), "toml".into()];
|
2025-06-24 23:18:01 +02:00
|
|
|
|
|
2025-06-24 23:38:32 +02:00
|
|
|
|
let mut user_cfg = Config::default();
|
|
|
|
|
|
user_cfg.scanner.excluded_extensions = vec!["jpg".into(), "rs".into()];
|
2025-06-24 23:18:01 +02:00
|
|
|
|
|
2025-06-24 23:38:32 +02:00
|
|
|
|
let merged = merge_configs(default_cfg, user_cfg);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
merged.scanner.excluded_extensions,
|
|
|
|
|
|
vec!["jpg", "rs", "toml"]
|
|
|
|
|
|
);
|
2025-06-24 23:18:01 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn merge_analysis_rules_unions_and_dedupes() {
|
|
|
|
|
|
let mut default_cfg = Config::default();
|
|
|
|
|
|
default_cfg.analysis.languages.insert(
|
|
|
|
|
|
"javascript".into(),
|
|
|
|
|
|
LanguageAnalysisConfig {
|
|
|
|
|
|
rules: vec![ConfigLabelRule {
|
|
|
|
|
|
matchers: vec!["escapeHtml".into()],
|
|
|
|
|
|
kind: "sanitizer".into(),
|
|
|
|
|
|
cap: "html_escape".into(),
|
|
|
|
|
|
}],
|
|
|
|
|
|
terminators: vec!["process.exit".into()],
|
|
|
|
|
|
event_handlers: vec![],
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let mut user_cfg = Config::default();
|
|
|
|
|
|
user_cfg.analysis.languages.insert(
|
|
|
|
|
|
"javascript".into(),
|
|
|
|
|
|
LanguageAnalysisConfig {
|
|
|
|
|
|
rules: vec![
|
|
|
|
|
|
ConfigLabelRule {
|
|
|
|
|
|
matchers: vec!["escapeHtml".into()],
|
|
|
|
|
|
kind: "sanitizer".into(),
|
|
|
|
|
|
cap: "html_escape".into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
ConfigLabelRule {
|
|
|
|
|
|
matchers: vec!["sanitizeUrl".into()],
|
|
|
|
|
|
kind: "sanitizer".into(),
|
|
|
|
|
|
cap: "url_encode".into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
terminators: vec!["process.exit".into(), "abort".into()],
|
|
|
|
|
|
event_handlers: vec!["addEventListener".into()],
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let merged = merge_configs(default_cfg, user_cfg);
|
|
|
|
|
|
let js = merged.analysis.languages.get("javascript").unwrap();
|
|
|
|
|
|
assert_eq!(js.rules.len(), 2); // deduped
|
|
|
|
|
|
assert_eq!(js.terminators, vec!["process.exit", "abort"]);
|
|
|
|
|
|
assert_eq!(js.event_handlers, vec!["addEventListener"]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn analysis_config_toml_roundtrip() {
|
|
|
|
|
|
let toml_str = r#"
|
|
|
|
|
|
[analysis.languages.javascript]
|
|
|
|
|
|
terminators = ["process.exit"]
|
|
|
|
|
|
event_handlers = ["addEventListener"]
|
|
|
|
|
|
|
|
|
|
|
|
[[analysis.languages.javascript.rules]]
|
|
|
|
|
|
matchers = ["escapeHtml"]
|
|
|
|
|
|
kind = "sanitizer"
|
|
|
|
|
|
cap = "html_escape"
|
|
|
|
|
|
"#;
|
|
|
|
|
|
let cfg: Config = toml::from_str(toml_str).unwrap();
|
|
|
|
|
|
let js = cfg.analysis.languages.get("javascript").unwrap();
|
|
|
|
|
|
assert_eq!(js.rules.len(), 1);
|
|
|
|
|
|
assert_eq!(js.rules[0].matchers, vec!["escapeHtml"]);
|
|
|
|
|
|
assert_eq!(js.rules[0].kind, "sanitizer");
|
|
|
|
|
|
assert_eq!(js.rules[0].cap, "html_escape");
|
|
|
|
|
|
assert_eq!(js.terminators, vec!["process.exit"]);
|
|
|
|
|
|
assert_eq!(js.event_handlers, vec!["addEventListener"]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 23:18:01 +02:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn load_creates_example_and_reads_user_overrides() {
|
2025-06-24 23:38:32 +02:00
|
|
|
|
let cfg_dir = tempfile::tempdir().unwrap();
|
|
|
|
|
|
let cfg_path = cfg_dir.path();
|
|
|
|
|
|
|
|
|
|
|
|
let user_toml = r#"
|
2025-06-24 23:18:01 +02:00
|
|
|
|
[scanner]
|
|
|
|
|
|
one_file_system = true
|
|
|
|
|
|
excluded_extensions = ["foo"]
|
|
|
|
|
|
|
|
|
|
|
|
[output]
|
|
|
|
|
|
quiet = true
|
|
|
|
|
|
"#;
|
2025-06-24 23:38:32 +02:00
|
|
|
|
fs::write(cfg_path.join("nyx.local"), user_toml).unwrap();
|
|
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
let (cfg, _note) = Config::load(cfg_path).expect("Config::load should succeed");
|
2025-06-24 23:18:01 +02:00
|
|
|
|
|
2025-06-24 23:38:32 +02:00
|
|
|
|
assert!(cfg_path.join("nyx.conf").is_file());
|
2025-06-24 23:18:01 +02:00
|
|
|
|
|
2025-06-24 23:38:32 +02:00
|
|
|
|
assert!(cfg.scanner.one_file_system);
|
|
|
|
|
|
assert!(cfg.output.quiet);
|
|
|
|
|
|
assert!(cfg.scanner.excluded_extensions.contains(&"foo".to_string()));
|
2025-06-24 23:18:01 +02:00
|
|
|
|
|
2025-06-24 23:38:32 +02:00
|
|
|
|
assert!(!cfg.scanner.follow_symlinks);
|
2025-06-24 23:18:01 +02:00
|
|
|
|
}
|