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}