Skip to main content

nyx_scanner/
cli.rs

1//! Command-line interface definition via clap.
2//!
3//! Defines [`Cli`] (the top-level parser) and the [`Commands`] enum of
4//! subcommands. Helpers on [`Commands`] answer routing questions the binary
5//! needs without pattern-matching on specific arms: [`Commands::effective_format`],
6//! [`Commands::is_structured_output`], [`Commands::is_serve`], and
7//! [`Commands::is_informational`].
8
9use clap::{Parser, Subcommand, ValueEnum};
10use serde::{Deserialize, Serialize};
11
12#[derive(Parser)]
13#[command(name = "nyx")]
14#[command(about = "A fast vulnerability scanner with project indexing")]
15#[command(version)]
16pub struct Cli {
17    #[command(subcommand)]
18    pub command: Commands,
19}
20
21impl Commands {
22    /// Resolve the effective output format, using the config default when the
23    /// CLI flag is omitted.
24    pub fn effective_format(&self, config: &crate::utils::config::Config) -> OutputFormat {
25        match self {
26            Commands::Scan { format, .. } => format.unwrap_or(config.output.default_format),
27            _ => OutputFormat::Console,
28        }
29    }
30
31    /// Whether this command produces structured (machine-readable) output on
32    /// stdout, meaning human status messages must be suppressed entirely.
33    pub fn is_structured_output(&self, config: &crate::utils::config::Config) -> bool {
34        let fmt = self.effective_format(config);
35        matches!(self, Commands::Scan { .. })
36            && (fmt == OutputFormat::Json || fmt == OutputFormat::Sarif)
37    }
38
39    /// Whether this is a long-running server command (skip timing output).
40    pub fn is_serve(&self) -> bool {
41        matches!(self, Commands::Serve { .. })
42    }
43
44    /// Pure read-only / informational commands that should run without the
45    /// "note: Using …" config preamble or the trailing "Finished in …"
46    /// timing line.  These commands' output is often piped or grepped; the
47    /// surrounding chrome is noise.
48    pub fn is_informational(&self) -> bool {
49        match self {
50            Commands::Scan { explain_engine, .. } => *explain_engine,
51            Commands::List { .. } => true,
52            Commands::Config { action } => {
53                matches!(action, ConfigAction::Show { .. } | ConfigAction::Path)
54            }
55            Commands::Index { action } => matches!(action, IndexAction::Status { .. }),
56            _ => false,
57        }
58    }
59}
60
61/// Output format for scan results.
62#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum, Default, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum OutputFormat {
65    #[default]
66    Console,
67    Json,
68    Sarif,
69}
70
71impl std::fmt::Display for OutputFormat {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            OutputFormat::Console => write!(f, "console"),
75            OutputFormat::Json => write!(f, "json"),
76            OutputFormat::Sarif => write!(f, "sarif"),
77        }
78    }
79}
80
81/// Index mode for scan operations.
82#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum, Default)]
83pub enum IndexMode {
84    /// Use index if available, build if missing (default)
85    #[default]
86    Auto,
87    /// Skip indexing entirely, scan filesystem directly
88    Off,
89    /// Force rebuild index before scanning
90    Rebuild,
91}
92
93/// Analysis mode for scan operations.
94#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum, Default)]
95pub enum ScanMode {
96    /// Run all analyses: AST analyses + CFG + taint (default)
97    #[default]
98    Full,
99    /// Run AST analyses only (tree-sitter patterns + auth analysis; no CFG/taint/state)
100    Ast,
101    /// Run CFG structural analyses + taint only (no AST analyses)
102    Cfg,
103    /// Alias for cfg (CFG + taint analysis)
104    Taint,
105}
106
107/// Engine-depth profile that sets the full stack of analysis toggles
108/// in one shot.  Individual engine flags override the profile.
109#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)]
110pub enum EngineProfile {
111    /// AST + CFG + basic taint. Disables symex, abstract-interp,
112    /// context-sensitive, backwards-analysis, and SMT.
113    Fast,
114    /// AST + CFG + SSA taint + abstract interpretation + context-sensitive
115    /// inlining. Disables symex, backwards-analysis, and SMT.
116    /// (This is the default engine posture.)
117    Balanced,
118    /// Everything in `balanced` plus symex (including cross-file and
119    /// interprocedural) and backwards-analysis. Still disables SMT
120    /// (requires the `smt` cargo feature).
121    Deep,
122}
123
124impl EngineProfile {
125    /// Apply this profile to an `AnalysisOptions` struct, returning the
126    /// new options.  Individual CLI flags are layered on top by the
127    /// caller after this runs.
128    pub fn apply(
129        &self,
130        mut opts: crate::utils::analysis_options::AnalysisOptions,
131    ) -> crate::utils::analysis_options::AnalysisOptions {
132        use crate::utils::analysis_options::SymexOptions;
133        match self {
134            EngineProfile::Fast => {
135                opts.constraint_solving = false;
136                opts.abstract_interpretation = false;
137                opts.context_sensitive = false;
138                opts.symex = SymexOptions {
139                    enabled: false,
140                    cross_file: false,
141                    interprocedural: false,
142                    smt: false,
143                };
144                opts.backwards_analysis = false;
145            }
146            EngineProfile::Balanced => {
147                opts.constraint_solving = true;
148                opts.abstract_interpretation = true;
149                opts.context_sensitive = true;
150                opts.symex = SymexOptions {
151                    enabled: false,
152                    cross_file: false,
153                    interprocedural: false,
154                    smt: false,
155                };
156                opts.backwards_analysis = false;
157            }
158            EngineProfile::Deep => {
159                opts.constraint_solving = true;
160                opts.abstract_interpretation = true;
161                opts.context_sensitive = true;
162                opts.symex = SymexOptions {
163                    enabled: true,
164                    cross_file: true,
165                    interprocedural: true,
166                    smt: false,
167                };
168                opts.backwards_analysis = true;
169            }
170        }
171        opts
172    }
173}
174
175impl std::fmt::Display for EngineProfile {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            EngineProfile::Fast => write!(f, "fast"),
179            EngineProfile::Balanced => write!(f, "balanced"),
180            EngineProfile::Deep => write!(f, "deep"),
181        }
182    }
183}
184
185#[derive(Subcommand)]
186pub enum Commands {
187    /// Scan project for vulnerabilities
188    Scan {
189        /// Path to scan (defaults to current directory)
190        #[arg(default_value = ".")]
191        path: String,
192
193        /// Index mode: auto (default), off (no index), rebuild (force rebuild)
194        #[arg(long, value_enum, default_value_t = IndexMode::Auto, help_heading = "Analysis")]
195        index: IndexMode,
196
197        /// Output format (defaults to config's default_format, or "console")
198        #[arg(short, long, value_enum, help_heading = "Output")]
199        format: Option<OutputFormat>,
200
201        /// Severity filter expression: HIGH, HIGH,MEDIUM, or >=MEDIUM
202        ///
203        /// Filters findings AFTER all severity normalization (e.g. nonprod
204        /// downgrades). Only findings matching the expression are emitted.
205        /// Case-insensitive. Shell-quote expressions containing ">".
206        #[arg(long, help_heading = "Output")]
207        severity: Option<String>,
208
209        /// Analysis mode: full (default), ast, cfg, taint
210        #[arg(long, value_enum, default_value_t = ScanMode::Full, help_heading = "Analysis")]
211        mode: ScanMode,
212
213        /// Named scan profile to apply (e.g. quick, full, ci, taint_only, conservative_large_repo)
214        ///
215        /// Profiles override scan-related config settings. CLI flags still
216        /// take precedence over profile values.
217        #[arg(long, help_heading = "Analysis")]
218        profile: Option<String>,
219
220        /// Engine-depth shortcut: fast, balanced, or deep.  Sets the full
221        /// stack of analysis toggles at once; individual engine flags still
222        /// override this after application.
223        #[arg(long, value_enum, help_heading = "Analysis")]
224        engine_profile: Option<EngineProfile>,
225
226        /// Print the effective engine configuration and exit without
227        /// scanning.  Useful for understanding how CLI flags and config
228        /// values resolve together.
229        #[arg(long, help_heading = "Analysis")]
230        explain_engine: bool,
231
232        /// Scan all targets (alias for --mode full)
233        #[arg(long, hide = true)]
234        all_targets: bool,
235
236        /// Preserve original severity for test/vendor/build paths
237        ///
238        /// By default, findings in non-production paths are downgraded by one
239        /// severity tier. This flag preserves original severity.
240        #[arg(long, alias = "include-nonprod", help_heading = "Output")]
241        keep_nonprod_severity: bool,
242
243        /// Suppress all human-readable status output
244        #[arg(long, help_heading = "Output")]
245        quiet: bool,
246
247        /// Exit with code 1 if any finding meets or exceeds this severity
248        ///
249        /// Useful for CI gating. Example: --fail-on HIGH
250        #[arg(long, help_heading = "Output")]
251        fail_on: Option<String>,
252
253        /// Disable state-model analysis (resource lifecycle, auth state)
254        #[arg(long, help_heading = "Analysis")]
255        no_state: bool,
256
257        /// Disable attack-surface ranking (findings are sorted by exploitability by default)
258        #[arg(long, help_heading = "Output")]
259        no_rank: bool,
260
261        /// Show inline-suppressed findings (dimmed, tagged \[SUPPRESSED\])
262        #[arg(long, help_heading = "Output")]
263        show_suppressed: bool,
264
265        /// Show all findings: disables category filtering, rollups, and LOW budgets
266        #[arg(long = "all", help_heading = "Output")]
267        show_all: bool,
268
269        /// Include Quality findings (excluded by default)
270        #[arg(long, help_heading = "Output")]
271        include_quality: bool,
272
273        /// Maximum total LOW findings to show
274        #[arg(long, default_value_t = 20, help_heading = "Output")]
275        max_low: u32,
276
277        /// Maximum LOW findings per file
278        #[arg(long, default_value_t = 1, help_heading = "Output")]
279        max_low_per_file: u32,
280
281        /// Maximum LOW findings per rule
282        #[arg(long, default_value_t = 10, help_heading = "Output")]
283        max_low_per_rule: u32,
284
285        /// Number of example locations in rollup findings
286        #[arg(long, default_value_t = 5, help_heading = "Output")]
287        rollup_examples: u32,
288
289        /// Show all instances for a specific rule (bypasses rollup for that rule)
290        #[arg(long, help_heading = "Output")]
291        show_instances: Option<String>,
292
293        /// Minimum attack-surface score to include in output
294        ///
295        /// Findings with a rank score below this threshold are suppressed.
296        /// Requires ranking to be enabled (has no effect with --no-rank).
297        /// Example: --min-score 50
298        #[arg(long, help_heading = "Output")]
299        min_score: Option<u32>,
300
301        /// Minimum confidence level to include in output
302        ///
303        /// Values: low, medium, high. Findings below this level are dropped.
304        /// JSON/SARIF include all unless filtered.
305        #[arg(long, help_heading = "Output")]
306        min_confidence: Option<String>,
307
308        /// Drop findings emitted from capped / widened / bailed analysis
309        ///
310        /// Suppresses any finding whose engine provenance notes indicate
311        /// over-reporting (predicate/path widening) or analysis bail
312        /// (SSA lowering failure, parse timeout).  Under-report notes
313        /// (where the emitted finding is still a real flow but the
314        /// result set is a lower bound) are kept.
315        ///
316        /// Intended for strict CI gates where a finding from non-converged
317        /// analysis is worse than no finding.  Applied after ranking and
318        /// before the `max_results` truncation.
319        #[arg(long, help_heading = "Output")]
320        require_converged: bool,
321
322        // ── Analysis engine toggles (override [analysis.engine] config) ───
323        /// Enable path-constraint solving (default: on)
324        #[arg(
325            long,
326            overrides_with = "no_constraint_solving",
327            help_heading = "Engine"
328        )]
329        constraint_solving: bool,
330        /// Disable path-constraint solving
331        #[arg(long, overrides_with = "constraint_solving", help_heading = "Engine")]
332        no_constraint_solving: bool,
333
334        /// Enable abstract interpretation (default: on)
335        #[arg(long, overrides_with = "no_abstract_interp", help_heading = "Engine")]
336        abstract_interp: bool,
337        /// Disable abstract interpretation
338        #[arg(long, overrides_with = "abstract_interp", help_heading = "Engine")]
339        no_abstract_interp: bool,
340
341        /// Enable k=1 context-sensitive callee inlining (default: on)
342        #[arg(long, overrides_with = "no_context_sensitive", help_heading = "Engine")]
343        context_sensitive: bool,
344        /// Disable context-sensitive callee inlining
345        #[arg(long, overrides_with = "context_sensitive", help_heading = "Engine")]
346        no_context_sensitive: bool,
347
348        /// Enable the symex pipeline (default: on)
349        #[arg(long, overrides_with = "no_symex", help_heading = "Symex")]
350        symex: bool,
351        /// Disable the symex pipeline entirely
352        #[arg(long, overrides_with = "symex", help_heading = "Symex")]
353        no_symex: bool,
354
355        /// Enable cross-file symbolic body execution (default: on)
356        #[arg(long, overrides_with = "no_cross_file_symex", help_heading = "Symex")]
357        cross_file_symex: bool,
358        /// Disable cross-file symbolic body execution
359        #[arg(long, overrides_with = "cross_file_symex", help_heading = "Symex")]
360        no_cross_file_symex: bool,
361
362        /// Enable interprocedural symex frame stack (default: on)
363        #[arg(long, overrides_with = "no_symex_interproc", help_heading = "Symex")]
364        symex_interproc: bool,
365        /// Disable interprocedural symex
366        #[arg(long, overrides_with = "symex_interproc", help_heading = "Symex")]
367        no_symex_interproc: bool,
368
369        /// Enable SMT solver backend when nyx is built with the `smt` feature (default: on)
370        #[arg(long, overrides_with = "no_smt", help_heading = "Symex")]
371        smt: bool,
372        /// Disable SMT solver backend
373        #[arg(long, overrides_with = "smt", help_heading = "Symex")]
374        no_smt: bool,
375
376        /// Enable demand-driven backwards analysis (default: off)
377        #[arg(
378            long,
379            overrides_with = "no_backwards_analysis",
380            help_heading = "Engine"
381        )]
382        backwards_analysis: bool,
383        /// Disable demand-driven backwards analysis
384        #[arg(long, overrides_with = "backwards_analysis", help_heading = "Engine")]
385        no_backwards_analysis: bool,
386
387        /// Override per-file tree-sitter parse timeout (ms). 0 disables the cap.
388        #[arg(long, help_heading = "Limits")]
389        parse_timeout_ms: Option<u64>,
390
391        /// Maximum taint origins retained per lattice value (default: 32).
392        ///
393        /// When origin sets exceed this cap, origins are truncated
394        /// deterministically (by source location) and an
395        /// `OriginsTruncated` engine note is recorded on affected findings.
396        /// Raise for very wide codebases where truncation is observed;
397        /// lower only when lattice width is a measured bottleneck.
398        #[arg(long, help_heading = "Limits")]
399        max_origins: Option<u32>,
400
401        /// Maximum abstract heap objects retained per points-to set (default: 32).
402        ///
403        /// When an intra-procedural points-to set would exceed this cap,
404        /// the largest-keyed heap objects are dropped and a
405        /// `PointsToTruncated` engine note is recorded on affected findings.
406        /// Raise for factory-heavy codebases where truncation is observed;
407        /// lower only when points-to width is a measured bottleneck.
408        #[arg(long, help_heading = "Limits")]
409        max_pointsto: Option<u32>,
410
411        // ── Deprecated aliases (hidden) ─────────────────────────────────
412        /// Deprecated: use --index off
413        #[arg(long, hide = true)]
414        no_index: bool,
415
416        /// Deprecated: use --index rebuild
417        #[arg(long, hide = true)]
418        rebuild_index: bool,
419
420        /// Deprecated: use --severity HIGH
421        #[arg(long, hide = true)]
422        high_only: bool,
423
424        /// Deprecated: use --mode ast
425        #[arg(long, hide = true)]
426        ast_only: bool,
427
428        /// Deprecated: use --mode cfg
429        #[arg(long, hide = true)]
430        cfg_only: bool,
431    },
432
433    /// Manage project indexes
434    Index {
435        #[command(subcommand)]
436        action: IndexAction,
437    },
438
439    /// List all indexed projects
440    List {
441        /// Show detailed information
442        #[arg(short, long)]
443        verbose: bool,
444    },
445
446    /// Remove project from index
447    Clean {
448        /// Project name or path to clean
449        project: Option<String>,
450
451        /// Clean all projects
452        #[arg(long)]
453        all: bool,
454    },
455
456    /// Manage analysis configuration
457    Config {
458        #[command(subcommand)]
459        action: ConfigAction,
460    },
461
462    /// Start the local web UI for browsing scan results
463    Serve {
464        /// Path to scan root (defaults to current directory)
465        #[arg(default_value = ".")]
466        path: String,
467
468        /// Port to bind to (overrides config)
469        #[arg(short, long)]
470        port: Option<u16>,
471
472        /// Host to bind to (overrides config)
473        #[arg(long)]
474        host: Option<String>,
475
476        /// Don't open browser automatically
477        #[arg(long)]
478        no_browser: bool,
479    },
480}
481
482#[derive(Subcommand)]
483pub enum ConfigAction {
484    /// Print configuration as TOML.  By default shows only the values
485    /// that differ from built-in defaults.  Pass `--all` for the full
486    /// effective configuration.
487    Show {
488        /// Print the full effective configuration instead of just
489        /// the user's overrides.
490        #[arg(long)]
491        all: bool,
492    },
493
494    /// Print configuration directory path
495    Path,
496
497    /// Add a label rule to nyx.local
498    AddRule {
499        /// Language slug (e.g. javascript, rust, python)
500        #[arg(long)]
501        lang: String,
502
503        /// Function or property name to match
504        #[arg(long)]
505        matcher: String,
506
507        /// Rule kind: source, sanitizer, or sink
508        #[arg(long)]
509        kind: String,
510
511        /// Capability: env_var, html_escape, shell_escape, url_encode, json_parse, file_io, or all
512        #[arg(long)]
513        cap: String,
514    },
515
516    /// Add a terminator function to nyx.local
517    AddTerminator {
518        /// Language slug (e.g. javascript, rust, python)
519        #[arg(long)]
520        lang: String,
521
522        /// Function name that terminates execution (e.g. process.exit)
523        #[arg(long)]
524        name: String,
525    },
526}
527
528#[derive(Subcommand)]
529pub enum IndexAction {
530    /// Build or update index for current project
531    Build {
532        /// Path to index (defaults to current directory)
533        #[arg(default_value = ".")]
534        path: String,
535
536        /// Force full rebuild
537        #[arg(short, long)]
538        force: bool,
539    },
540
541    /// Show index status and statistics
542    Status {
543        /// Project path to check
544        #[arg(default_value = ".")]
545        path: String,
546    },
547}