pub mod clean;
pub mod config;
pub mod index;
pub mod list;
pub mod scan;
#[cfg(feature = "serve")]
pub mod serve;
use crate::cli::{Commands, EngineProfile, IndexMode, ScanMode};
use crate::errors::NyxResult;
use crate::patterns::{Severity, SeverityFilter};
use crate::utils::config::{AnalysisMode, Config};
use std::path::Path;
pub fn handle_command(
command: Commands,
database_dir: &Path,
config_dir: &Path,
config: &mut Config,
) -> NyxResult<()> {
let install_from_config = |config: &Config| {
if config.analysis.engine.parse_timeout_ms == 0 {
tracing::warn!(
"parse_timeout_ms = 0 disables tree-sitter parse timeout entirely; \
this is unsafe for untrusted input."
);
}
let _ = crate::utils::analysis_options::install(config.analysis.engine);
};
match command {
Commands::Scan {
path,
index,
format,
severity,
mode,
profile,
engine_profile,
explain_engine,
all_targets,
keep_nonprod_severity,
quiet,
fail_on,
no_state,
no_rank,
show_suppressed,
show_all,
include_quality,
max_low,
max_low_per_file,
max_low_per_rule,
rollup_examples,
show_instances,
min_score,
min_confidence,
require_converged,
constraint_solving,
no_constraint_solving,
abstract_interp,
no_abstract_interp,
context_sensitive,
no_context_sensitive,
symex,
no_symex,
cross_file_symex,
no_cross_file_symex,
symex_interproc,
no_symex_interproc,
smt,
no_smt,
backwards_analysis,
no_backwards_analysis,
parse_timeout_ms,
max_origins,
max_pointsto,
no_index,
rebuild_index,
high_only,
ast_only,
cfg_only,
} => {
if let Some(ref name) = profile {
config.apply_profile(name)?;
}
use crate::cli::OutputFormat;
let effective_format = format.unwrap_or(config.output.default_format);
let structured = matches!(effective_format, OutputFormat::Json | OutputFormat::Sarif);
let suppress_warnings = quiet || config.output.quiet || structured;
let warn_dep = |old: &str, new: &str| {
if !suppress_warnings {
eprintln!(
"{}: {} is deprecated; use {} instead.",
console::style("warn").yellow().bold(),
console::style(old).bold(),
console::style(new).bold()
);
}
};
let effective_index = if no_index {
warn_dep("--no-index", "--index off");
IndexMode::Off
} else if rebuild_index {
warn_dep("--rebuild-index", "--index rebuild");
IndexMode::Rebuild
} else {
index
};
let effective_mode = if ast_only {
warn_dep("--ast-only", "--mode ast");
ScanMode::Ast
} else if cfg_only {
warn_dep("--cfg-only", "--mode cfg");
ScanMode::Cfg
} else if all_targets {
warn_dep("--all-targets", "--mode full");
ScanMode::Full
} else {
mode
};
let severity_filter = if let Some(ref expr) = severity {
Some(SeverityFilter::parse(expr).map_err(|e| {
crate::errors::NyxError::Msg(format!("invalid --severity expression: {e}"))
})?)
} else if high_only {
warn_dep("--high-only", "--severity HIGH");
Some(SeverityFilter::parse("HIGH").unwrap())
} else {
None
};
let fail_on_sev = if let Some(ref expr) = fail_on {
Some(expr.trim().parse::<Severity>().map_err(|e| {
crate::errors::NyxError::Msg(format!("invalid --fail-on value: {e}"))
})?)
} else {
None
};
match effective_mode {
ScanMode::Full => config.scanner.mode = AnalysisMode::Full,
ScanMode::Ast => config.scanner.mode = AnalysisMode::Ast,
ScanMode::Cfg => config.scanner.mode = AnalysisMode::Cfg,
ScanMode::Taint => config.scanner.mode = AnalysisMode::Taint,
}
if keep_nonprod_severity {
config.scanner.include_nonprod = true;
}
if quiet {
config.output.quiet = true;
}
if no_state {
config.scanner.enable_state_analysis = false;
}
if no_rank {
config.output.attack_surface_ranking = false;
}
if let Some(s) = min_score {
config.output.min_score = Some(s);
}
if let Some(ref expr) = min_confidence {
config.output.min_confidence =
Some(expr.parse::<crate::evidence::Confidence>().map_err(|e| {
crate::errors::NyxError::Msg(format!("invalid --min-confidence value: {e}"))
})?);
}
if require_converged {
config.output.require_converged = true;
}
if show_all {
config.output.show_all = true;
}
if include_quality {
config.output.include_quality = true;
}
config.output.max_low = max_low;
config.output.max_low_per_file = max_low_per_file;
config.output.max_low_per_rule = max_low_per_rule;
config.output.rollup_examples = rollup_examples;
let mut engine = config.analysis.engine;
if let Some(ref prof) = engine_profile {
engine = prof.apply(engine);
}
if constraint_solving {
engine.constraint_solving = true;
}
if no_constraint_solving {
engine.constraint_solving = false;
}
if abstract_interp {
engine.abstract_interpretation = true;
}
if no_abstract_interp {
engine.abstract_interpretation = false;
}
if context_sensitive {
engine.context_sensitive = true;
}
if no_context_sensitive {
engine.context_sensitive = false;
}
if symex {
engine.symex.enabled = true;
}
if no_symex {
engine.symex.enabled = false;
}
if cross_file_symex {
engine.symex.cross_file = true;
}
if no_cross_file_symex {
engine.symex.cross_file = false;
}
if symex_interproc {
engine.symex.interprocedural = true;
}
if no_symex_interproc {
engine.symex.interprocedural = false;
}
if smt {
engine.symex.smt = true;
}
if no_smt {
engine.symex.smt = false;
}
if backwards_analysis {
engine.backwards_analysis = true;
}
if no_backwards_analysis {
engine.backwards_analysis = false;
}
if let Some(ms) = parse_timeout_ms {
engine.parse_timeout_ms = ms;
}
if let Some(n) = max_origins {
engine.max_origins = n.max(crate::utils::analysis_options::MIN_MAX_ORIGINS);
}
if let Some(n) = max_pointsto {
engine.max_pointsto = n.max(crate::utils::analysis_options::MIN_MAX_POINTSTO);
}
config.analysis.engine = engine;
if engine.parse_timeout_ms == 0 {
tracing::warn!(
"parse_timeout_ms = 0 disables tree-sitter parse timeout entirely; \
this is unsafe for untrusted input."
);
}
if !crate::utils::analysis_options::install(engine) {
tracing::warn!(
"analysis-engine runtime already installed; CLI engine flags ignored"
);
}
if explain_engine {
print_engine_explanation(config, engine_profile);
return Ok(());
}
let effective_format = format.unwrap_or(config.output.default_format);
scan::handle(
&path,
effective_index,
effective_format,
severity_filter,
fail_on_sev,
show_suppressed,
show_instances.as_deref(),
database_dir,
config,
)?;
}
Commands::Index { action } => {
install_from_config(config);
index::handle(action, database_dir, config)?;
}
Commands::List { verbose } => {
list::handle(verbose, database_dir)?;
}
Commands::Clean { project, all } => {
clean::handle(project, all, database_dir)?;
}
Commands::Config { action } => {
use crate::cli::ConfigAction;
match action {
ConfigAction::Show { all } => self::config::show(config, all)?,
ConfigAction::Path => self::config::path(config_dir)?,
ConfigAction::AddRule {
lang,
matcher,
kind,
cap,
} => self::config::add_rule(config_dir, &lang, &matcher, &kind, &cap)?,
ConfigAction::AddTerminator { lang, name } => {
self::config::add_terminator(config_dir, &lang, &name)?
}
}
}
Commands::Serve {
path,
port,
host,
no_browser,
} => {
install_from_config(config);
#[cfg(feature = "serve")]
{
serve::handle(
&path,
port,
host.as_deref(),
no_browser,
config_dir,
database_dir,
config,
)?;
}
#[cfg(not(feature = "serve"))]
{
let _ = (path, port, host, no_browser);
return Err(crate::errors::NyxError::Msg(
"The `serve` feature is not enabled. Rebuild with `cargo build --features serve`.".into(),
));
}
}
}
Ok(())
}
fn print_engine_explanation(config: &Config, engine_profile: Option<EngineProfile>) {
use console::style;
fn onoff(b: bool) -> String {
if b {
style("on ").green().to_string()
} else {
style("off").red().dim().to_string()
}
}
let engine = config.analysis.engine;
let scanner = &config.scanner;
let profile_label = engine_profile
.map(|p| p.to_string())
.unwrap_or_else(|| "(none, using config defaults)".to_string());
let smt_compiled = cfg!(feature = "smt");
let pipeline_on = matches!(
config.scanner.mode,
AnalysisMode::Full | AnalysisMode::Cfg | AnalysisMode::Taint
);
let row_flag = |label: &str, on: bool, flags: &str| {
println!(
" {:<24} {} {}",
format!("{label}:"),
onoff(on),
style(flags).dim()
);
};
let row_plain = |label: &str, value: &str| {
println!(" {:<24} {}", format!("{label}:"), value);
};
let row_num = |label: &str, value: String, flags: &str| {
println!(
" {:<24} {:<10} {}",
format!("{label}:"),
value,
style(flags).dim()
);
};
let section = |title: &str| {
println!();
println!(" {}", style(title).cyan().bold());
};
println!("{}", style("Effective engine configuration").white().bold());
println!(
" {:<24} {}",
"Engine profile:",
style(&profile_label).bold()
);
section("Pipeline");
row_plain("AST patterns", &onoff(true));
row_plain("CFG construction", &onoff(pipeline_on));
row_plain("CFG analysis", &onoff(pipeline_on));
row_plain("Taint (SSA)", &onoff(pipeline_on));
row_plain("State analysis", &onoff(scanner.enable_state_analysis));
row_plain("Auth analysis", &onoff(scanner.enable_auth_analysis));
section("Engine toggles");
row_flag(
"Abstract interpretation",
engine.abstract_interpretation,
"--abstract-interp / NYX_ABSTRACT_INTERP",
);
row_flag(
"Context sensitivity",
engine.context_sensitive,
"--context-sensitive / NYX_CONTEXT_SENSITIVE (k=1)",
);
row_flag(
"Constraint solving",
engine.constraint_solving,
"--constraint-solving / NYX_CONSTRAINT",
);
row_flag(
"Backwards taint",
engine.backwards_analysis,
"--backwards-analysis / NYX_BACKWARDS",
);
section("Symbolic execution");
row_flag("Symex", engine.symex.enabled, "--symex / NYX_SYMEX");
row_flag(
"Cross-file symex",
engine.symex.cross_file,
"--cross-file-symex / NYX_CROSS_FILE_SYMEX",
);
row_flag(
"Interproc symex",
engine.symex.interprocedural,
"--symex-interproc / NYX_SYMEX_INTERPROC",
);
let smt_note = if smt_compiled {
"--smt"
} else {
"--smt (this binary built without `smt` feature)"
};
row_flag("SMT (Z3)", engine.symex.smt && smt_compiled, smt_note);
section("Limits");
row_num(
"Parse timeout",
format!("{} ms", engine.parse_timeout_ms),
"--parse-timeout-ms / NYX_PARSE_TIMEOUT_MS (0 disables)",
);
row_num(
"Max taint origins",
engine.max_origins.to_string(),
"--max-origins / NYX_MAX_ORIGINS (per-lattice-value cap)",
);
row_num(
"Max points-to set",
engine.max_pointsto.to_string(),
"--max-pointsto / NYX_MAX_POINTSTO (per-variable heap cap)",
);
println!();
}