forked from mirrors/kingfisher
preparing for v1.12
This commit is contained in:
commit
fc4aee9e41
249 changed files with 121395 additions and 0 deletions
128
src/cli/commands/github.rs
Normal file
128
src/cli/commands/github.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use clap::{Args, Subcommand, ValueEnum, ValueHint};
|
||||
use strum_macros::Display;
|
||||
use url::Url;
|
||||
|
||||
use crate::cli::commands::output::OutputArgs;
|
||||
|
||||
/// Top-level GitHub command group
|
||||
#[derive(Args, Debug)]
|
||||
pub struct GitHubArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: GitHubCommand,
|
||||
|
||||
/// Override GitHub API URL (e.g. Enterprise)
|
||||
#[arg(global = true, long, default_value = "https://api.github.com/", value_hint = ValueHint::Url)]
|
||||
pub github_api_url: Url,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum GitHubCommand {
|
||||
/// Interact with GitHub repositories
|
||||
#[command(subcommand)]
|
||||
Repos(GitHubReposCommand),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum GitHubReposCommand {
|
||||
/// List repositories for a user or organization
|
||||
List(GitHubReposListArgs),
|
||||
}
|
||||
|
||||
/// `kingfisher github repos`
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct GitHubReposListArgs {
|
||||
#[command(flatten)]
|
||||
pub repo_specifiers: GitHubRepoSpecifiers,
|
||||
|
||||
#[command(flatten)]
|
||||
pub output_args: OutputArgs<GitHubOutputFormat>,
|
||||
}
|
||||
|
||||
/// Options for selecting GitHub repos
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct GitHubRepoSpecifiers {
|
||||
/// Repositories belonging to these users
|
||||
#[arg(long, alias = "github-user")]
|
||||
pub user: Vec<String>,
|
||||
|
||||
/// Repositories belonging to these organizations
|
||||
#[arg(long, alias = "org", alias = "github-organization", alias = "github-org")]
|
||||
pub organization: Vec<String>,
|
||||
|
||||
/// Repositories for all organizations (Enterprise only)
|
||||
#[arg(
|
||||
long,
|
||||
alias = "all-orgs",
|
||||
alias = "all-github-organizations",
|
||||
alias = "all-github-orgs",
|
||||
requires = "github_api_url"
|
||||
)]
|
||||
pub all_organizations: bool,
|
||||
|
||||
/// Filter by repository type
|
||||
#[arg(long, default_value_t = GitHubRepoType::Source, alias = "github-repo-type")]
|
||||
pub repo_type: GitHubRepoType,
|
||||
}
|
||||
|
||||
impl GitHubRepoSpecifiers {
|
||||
/// Check if no GitHub sources are specified
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.user.is_empty() && self.organization.is_empty() && !self.all_organizations
|
||||
}
|
||||
}
|
||||
|
||||
/// GitHub repository type filter
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum GitHubRepoType {
|
||||
/// Both source and fork repositories
|
||||
All,
|
||||
/// Only source repositories (not forks)
|
||||
Source,
|
||||
/// Only fork repositories
|
||||
#[value(alias = "forks")]
|
||||
Fork,
|
||||
}
|
||||
|
||||
impl From<GitHubRepoType> for crate::github::RepoType {
|
||||
fn from(val: GitHubRepoType) -> Self {
|
||||
match val {
|
||||
GitHubRepoType::All => crate::github::RepoType::All,
|
||||
GitHubRepoType::Source => crate::github::RepoType::Source,
|
||||
GitHubRepoType::Fork => crate::github::RepoType::Fork,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Output formats for GitHub commands
|
||||
#[derive(Copy, Clone, Debug, ValueEnum, Display)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum GitHubOutputFormat {
|
||||
Pretty,
|
||||
Json,
|
||||
Jsonl,
|
||||
Bson,
|
||||
Sarif,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Git Repository Cloning/History
|
||||
// -----------------------------------------------------------------------------
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum GitCloneMode {
|
||||
/// Equivalent to `git clone --bare`
|
||||
Bare,
|
||||
/// Equivalent to `git clone --mirror`, often clones extra objects
|
||||
Mirror,
|
||||
}
|
||||
|
||||
/// Specifies how to handle a repository's Git history.
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum GitHistoryMode {
|
||||
/// Scan all history
|
||||
Full,
|
||||
/// Ignore history entirely
|
||||
None,
|
||||
}
|
||||
91
src/cli/commands/gitlab.rs
Normal file
91
src/cli/commands/gitlab.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
use clap::{Args, Subcommand, ValueEnum, ValueHint};
|
||||
use strum_macros::Display;
|
||||
use url::Url;
|
||||
|
||||
use crate::cli::commands::output::{GitHubOutputFormat, OutputArgs};
|
||||
|
||||
/// Top-level GitLab command group
|
||||
#[derive(Args, Debug)]
|
||||
pub struct GitLabArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: GitLabCommand,
|
||||
|
||||
/// Override GitLab API URL (e.g. Enterprise)
|
||||
#[arg(global = true, long, default_value = "https://gitlab.com/", value_hint = ValueHint::Url)]
|
||||
pub gitlab_api_url: Url,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum GitLabCommand {
|
||||
/// Interact with GitLab repositories
|
||||
#[command(subcommand)]
|
||||
Repos(GitLabReposCommand),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum GitLabReposCommand {
|
||||
/// List repositories for a user or group
|
||||
List(GitLabReposListArgs),
|
||||
}
|
||||
|
||||
/// `kingfisher gitlab repos`
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct GitLabReposListArgs {
|
||||
#[command(flatten)]
|
||||
pub repo_specifiers: GitLabRepoSpecifiers,
|
||||
|
||||
#[command(flatten)]
|
||||
pub output_args: OutputArgs<GitLabOutputFormat>,
|
||||
}
|
||||
|
||||
/// Options for selecting GitLab repos
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct GitLabRepoSpecifiers {
|
||||
/// Repositories belonging to these users
|
||||
#[arg(long, alias = "gitlab-user")]
|
||||
pub user: Vec<String>,
|
||||
|
||||
/// Repositories belonging to these groups
|
||||
#[arg(long, alias = "gitlab-group")]
|
||||
pub group: Vec<String>,
|
||||
|
||||
/// Repositories for all groups (Enterprise only)
|
||||
#[arg(long, alias = "all-groups", alias = "all-gitlab-groups", requires = "gitlab_api_url")]
|
||||
pub all_groups: bool,
|
||||
|
||||
/// Filter by repository type
|
||||
#[arg(long, default_value_t = GitLabRepoType::All, alias = "gitlab-repo-type")]
|
||||
pub repo_type: GitLabRepoType,
|
||||
}
|
||||
|
||||
impl GitLabRepoSpecifiers {
|
||||
/// Check if no GitLab sources are specified
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.user.is_empty() && self.group.is_empty() && !self.all_groups
|
||||
}
|
||||
}
|
||||
|
||||
/// GitLab repository type filter
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum GitLabRepoType {
|
||||
/// All repositories the user/group has access to
|
||||
All,
|
||||
/// Only repositories owned by the user/group
|
||||
Owner,
|
||||
/// Only repositories where the user is a member
|
||||
Member,
|
||||
}
|
||||
|
||||
/// Output formats for GitLab commands - reusing GitHub's formats
|
||||
pub type GitLabOutputFormat = GitHubOutputFormat;
|
||||
|
||||
impl From<GitLabRepoType> for crate::gitlab::RepoType {
|
||||
fn from(val: GitLabRepoType) -> Self {
|
||||
match val {
|
||||
GitLabRepoType::All => crate::gitlab::RepoType::All,
|
||||
GitLabRepoType::Owner => crate::gitlab::RepoType::Owner,
|
||||
GitLabRepoType::Member => crate::gitlab::RepoType::Member,
|
||||
}
|
||||
}
|
||||
}
|
||||
140
src/cli/commands/inputs.rs
Normal file
140
src/cli/commands/inputs.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Args, ValueHint};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
cli::commands::{
|
||||
github::{GitCloneMode, GitHistoryMode, GitHubRepoType},
|
||||
gitlab::GitLabRepoType,
|
||||
},
|
||||
git_url::GitUrl,
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Inputs
|
||||
// -----------------------------------------------------------------------------
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct InputSpecifierArgs {
|
||||
/// Scan this file, directory, or local Git repository
|
||||
#[arg(
|
||||
required_unless_present_any([
|
||||
"github_user",
|
||||
"github_organization",
|
||||
"gitlab_user",
|
||||
"gitlab_group",
|
||||
"git_url",
|
||||
"all_github_organizations",
|
||||
"all_gitlab_groups"
|
||||
]),
|
||||
value_hint = ValueHint::AnyPath
|
||||
)]
|
||||
pub path_inputs: Vec<PathBuf>,
|
||||
|
||||
/// Clone and scan the Git repository at the given URL
|
||||
#[arg(long, value_hint = ValueHint::Url)]
|
||||
pub git_url: Vec<GitUrl>,
|
||||
|
||||
/// Scan repositories belonging to the specified GitHub user
|
||||
#[arg(long)]
|
||||
pub github_user: Vec<String>,
|
||||
|
||||
/// Scan repositories belonging to the specified GitHub organization
|
||||
#[arg(long, alias = "github-org")]
|
||||
pub github_organization: Vec<String>,
|
||||
|
||||
/// Scan repositories from all GitHub organizations (requires non-default --github-api-url)
|
||||
#[arg(long, alias = "all-github-orgs", requires = "github_api_url")]
|
||||
pub all_github_organizations: bool,
|
||||
|
||||
/// Use the specified URL for GitHub API access (e.g. for GitHub Enterprise)
|
||||
#[arg(
|
||||
long,
|
||||
alias="api-url",
|
||||
default_value = "https://api.github.com/",
|
||||
value_hint = ValueHint::Url
|
||||
)]
|
||||
pub github_api_url: Url,
|
||||
|
||||
#[arg(long, default_value_t = GitHubRepoType::Source)]
|
||||
pub github_repo_type: GitHubRepoType,
|
||||
|
||||
// GitLab Options
|
||||
/// Scan repositories belonging to the specified GitLab user
|
||||
#[arg(long)]
|
||||
pub gitlab_user: Vec<String>,
|
||||
|
||||
/// Scan repositories belonging to the specified GitLab group
|
||||
#[arg(long, alias = "gitlab-group")]
|
||||
pub gitlab_group: Vec<String>,
|
||||
|
||||
/// Scan repositories from all GitLab groups (requires non-default --gitlab-api-url)
|
||||
#[arg(long, alias = "all-gitlab-groups", requires = "gitlab_api_url")]
|
||||
pub all_gitlab_groups: bool,
|
||||
|
||||
/// Use the specified URL for GitLab API access (e.g. for GitLab self-hosted)
|
||||
#[arg(
|
||||
long,
|
||||
alias="gitlab-api-url",
|
||||
default_value = "https://gitlab.com/",
|
||||
value_hint = ValueHint::Url
|
||||
)]
|
||||
pub gitlab_api_url: Url,
|
||||
|
||||
#[arg(long, default_value_t = GitLabRepoType::Owner)]
|
||||
pub gitlab_repo_type: GitLabRepoType,
|
||||
|
||||
/// Select how to clone Git repositories
|
||||
#[arg(long, default_value_t=GitCloneMode::Bare, alias="git-clone-mode")]
|
||||
pub git_clone: GitCloneMode,
|
||||
|
||||
/// Select whether to scan full Git history or not
|
||||
#[arg(long, default_value_t=GitHistoryMode::Full)]
|
||||
pub git_history: GitHistoryMode,
|
||||
|
||||
/// Include detailed Git commit context (author, date, commit hash) for findings.
|
||||
/// Set to 'false' to disable.
|
||||
#[arg(long, default_value_t = true, action = clap::ArgAction::Set, help_heading = "Git Options")]
|
||||
pub commit_metadata: bool,
|
||||
|
||||
/// Enable or disable scanning nested git repositories
|
||||
#[arg(long, default_value_t = true)]
|
||||
pub scan_nested_repos: bool,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Content Filtering
|
||||
// -----------------------------------------------------------------------------
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct ContentFilteringArgs {
|
||||
/// Ignore files larger than the given size in MB
|
||||
#[arg(long("max-file-size"), default_value_t = 25.0)]
|
||||
pub max_file_size_mb: f64,
|
||||
|
||||
/// Use custom path-based ignore rules from the given file(s)
|
||||
#[arg(long, short, value_hint = ValueHint::FilePath)]
|
||||
pub ignore: Vec<PathBuf>,
|
||||
|
||||
/// If true, do NOT extract archive files
|
||||
#[arg(long("no-extract-archives"), default_value_t = false)]
|
||||
pub no_extract_archives: bool,
|
||||
|
||||
/// Maximum allowed depth for extracting nested archives
|
||||
#[arg(long("extraction-depth"), default_value_t = 2, value_parser = clap::value_parser!(u8).range(1..=25))]
|
||||
pub extraction_depth: u8,
|
||||
|
||||
/// If true, do NOT scan binary files
|
||||
#[arg(long("no-binary"), default_value_t = false)]
|
||||
pub no_binary: bool,
|
||||
}
|
||||
|
||||
impl ContentFilteringArgs {
|
||||
/// Convert the maximum file size in MB to bytes
|
||||
pub fn max_file_size_bytes(&self) -> Option<u64> {
|
||||
if self.max_file_size_mb < 0.0 {
|
||||
Some(25 * 1024 * 1024) // default 25 MB if negative
|
||||
} else {
|
||||
Some((self.max_file_size_mb * 1024.0 * 1024.0) as u64)
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/cli/commands/mod.rs
Normal file
6
src/cli/commands/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod github;
|
||||
pub mod gitlab;
|
||||
pub mod inputs;
|
||||
pub mod output;
|
||||
pub mod rules;
|
||||
pub mod scan;
|
||||
77
src/cli/commands/output.rs
Normal file
77
src/cli/commands/output.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Args, ValueEnum, ValueHint};
|
||||
use strum::Display;
|
||||
|
||||
use crate::util::get_writer_for_file_or_stdout;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Output Options
|
||||
// -----------------------------------------------------------------------------
|
||||
#[derive(Args, Debug, Clone)]
|
||||
#[command(next_help_heading = "Output Options")]
|
||||
pub struct OutputArgs<Format: ValueEnum + Send + Sync + 'static> {
|
||||
/// Write output to the specified path (stdout if not given)
|
||||
#[arg(long, short, value_hint = ValueHint::FilePath)]
|
||||
pub output: Option<PathBuf>,
|
||||
|
||||
/// Output format (defaults to `pretty` if not specified)
|
||||
#[arg(long, short, default_value = "pretty")]
|
||||
pub format: Format,
|
||||
}
|
||||
|
||||
impl<Format: ValueEnum + Send + Sync> OutputArgs<Format> {
|
||||
/// Return a writer for the specified output destination
|
||||
pub fn get_writer(&self) -> std::io::Result<Box<dyn std::io::Write>> {
|
||||
get_writer_for_file_or_stdout(self.output.as_ref())
|
||||
}
|
||||
|
||||
/// Check if an output path was specified
|
||||
pub fn has_output(&self) -> bool {
|
||||
self.output.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Report Output Format
|
||||
// -----------------------------------------------------------------------------
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum ReportOutputFormat {
|
||||
/// A human-friendly text-based format
|
||||
Pretty,
|
||||
|
||||
/// Pretty-printed JSON
|
||||
Json,
|
||||
|
||||
/// JSON Lines (one JSON object per line)
|
||||
Jsonl,
|
||||
|
||||
/// BSON (binary JSON) format
|
||||
Bson,
|
||||
|
||||
/// SARIF format (experimental)
|
||||
Sarif,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GitHub Output Format
|
||||
// -----------------------------------------------------------------------------
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum GitHubOutputFormat {
|
||||
/// A human-friendly text-based format
|
||||
Pretty,
|
||||
|
||||
/// Pretty-printed JSON
|
||||
Json,
|
||||
|
||||
/// JSON Lines (one JSON object per line)
|
||||
Jsonl,
|
||||
|
||||
/// BSON (binary JSON) format
|
||||
Bson,
|
||||
|
||||
/// SARIF format (experimental)
|
||||
Sarif,
|
||||
}
|
||||
75
src/cli/commands/rules.rs
Normal file
75
src/cli/commands/rules.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{ArgAction, Args, Subcommand, ValueEnum, ValueHint};
|
||||
use strum::Display;
|
||||
|
||||
use crate::cli::commands::output::OutputArgs;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rule Specifiers
|
||||
// -----------------------------------------------------------------------------
|
||||
#[derive(Args, Debug, Clone, Default)]
|
||||
pub struct RuleSpecifierArgs {
|
||||
/// Load additional rules from file(s) or directories
|
||||
///
|
||||
/// Directories are walked recursively for YAML files. This option
|
||||
/// can be repeated.
|
||||
#[arg(long, alias="rules", value_hint=ValueHint::AnyPath)]
|
||||
pub rules_path: Vec<PathBuf>,
|
||||
|
||||
/// Enable the ruleset with the given ID (e.g. `all`, `default`, or custom)
|
||||
///
|
||||
/// Repeating this disables the default set unless `default` is explicitly included.
|
||||
#[arg(long, default_values_t=["all".to_string()])]
|
||||
pub rule: Vec<String>,
|
||||
|
||||
/// Load built-in rules
|
||||
#[arg(long, default_value_t=true, action=ArgAction::Set)]
|
||||
pub load_builtins: bool,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct RulesArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: RulesCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum RulesCommand {
|
||||
/// Check rules for problems
|
||||
Check(RulesCheckArgs),
|
||||
|
||||
/// List available rules
|
||||
List(RulesListArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct RulesCheckArgs {
|
||||
/// Treat warnings as errors
|
||||
#[arg(long, short = 'W')]
|
||||
pub warnings_as_errors: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub rules: RuleSpecifierArgs,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct RulesListArgs {
|
||||
#[command(flatten)]
|
||||
pub rules: RuleSpecifierArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub output_args: OutputArgs<RulesListOutputFormat>,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rules List Output Format
|
||||
// -----------------------------------------------------------------------------
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum RulesListOutputFormat {
|
||||
/// A human-friendly text-based format
|
||||
Pretty,
|
||||
/// Pretty-printed JSON
|
||||
Json,
|
||||
}
|
||||
124
src/cli/commands/scan.rs
Normal file
124
src/cli/commands/scan.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
use clap::{Args, ValueEnum};
|
||||
use strum::Display;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
commands::{
|
||||
inputs::{ContentFilteringArgs, InputSpecifierArgs},
|
||||
output::{OutputArgs, ReportOutputFormat},
|
||||
rules::RuleSpecifierArgs,
|
||||
},
|
||||
global::RAM_GB,
|
||||
},
|
||||
rules::rule::Confidence,
|
||||
};
|
||||
|
||||
/// Determine the default number of parallel scan jobs.
|
||||
///
|
||||
/// * Target = `num_cpus * 2`.
|
||||
/// * Cap by RAM at ≈ 1 GiB per job (so 16 GiB ⇒ max 16 jobs).
|
||||
/// * Always ≥ 1.
|
||||
/// * When `-v/--verbose` is passed, the computed value is logged at DEBUG.
|
||||
fn default_scan_jobs() -> usize {
|
||||
// How many logical CPUs do we see? (Falls back to 1 on error.)
|
||||
let cpu_count = std::thread::available_parallelism().map(usize::from).unwrap_or(1);
|
||||
|
||||
// Desired parallelism is CPU * 2.
|
||||
let desired = cpu_count * 2;
|
||||
|
||||
match *RAM_GB {
|
||||
// If we know how much RAM we have, cap by a 1 GiB-per-job heuristic.
|
||||
Some(ram_gb) => {
|
||||
let max_by_ram = ram_gb.ceil() as usize; // 1 GiB per job
|
||||
let jobs = desired.min(max_by_ram).max(1);
|
||||
|
||||
debug!(
|
||||
"Using {jobs} parallel scan jobs \
|
||||
(cpus = {cpu_count}, desired = {desired}, \
|
||||
ram = {ram_gb:.1} GiB, cap_by_ram = {max_by_ram})"
|
||||
);
|
||||
jobs
|
||||
}
|
||||
// If RAM is unknown, just use the desired value.
|
||||
None => {
|
||||
debug!("Using {desired} parallel scan jobs (cpus = {cpu_count}, ram unknown)");
|
||||
desired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `kingfisher scan` command and flags
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct ScanArgs {
|
||||
/// Number of parallel scanning threads
|
||||
#[arg(long = "jobs", short = 'j', default_value_t = default_scan_jobs())]
|
||||
pub num_jobs: usize,
|
||||
|
||||
#[command(flatten)]
|
||||
pub rules: RuleSpecifierArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub input_specifier_args: InputSpecifierArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub content_filtering_args: ContentFilteringArgs,
|
||||
|
||||
/// Minimum confidence level for reporting findings
|
||||
#[arg(long, short = 'c', default_value = "medium")]
|
||||
pub confidence: ConfidenceLevel,
|
||||
|
||||
/// Disable secret validation
|
||||
#[arg(long, short = 'n', default_value_t = false)]
|
||||
pub no_validate: bool,
|
||||
|
||||
/// Display only validated findings
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub only_valid: bool,
|
||||
|
||||
/// Override the default minimum entropy threshold
|
||||
#[arg(long, short = 'e')]
|
||||
pub min_entropy: Option<f32>,
|
||||
|
||||
/// Show performance statistics for each rule
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub rule_stats: bool,
|
||||
|
||||
/// Display every occurrence of a finding
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub no_dedup: bool,
|
||||
|
||||
/// Redact findings values using a secure hash
|
||||
#[arg(long, short = 'r', default_value_t = false)]
|
||||
pub redact: bool,
|
||||
|
||||
/// Timeout for Git repository scanning in seconds
|
||||
#[arg(long, default_value_t = 1800, value_name = "SECONDS")]
|
||||
pub git_repo_timeout: u64,
|
||||
|
||||
#[command(flatten)]
|
||||
pub output_args: OutputArgs<ReportOutputFormat>,
|
||||
|
||||
/// Bytes of context before and after each match
|
||||
#[arg(long, default_value_t = 256, value_name = "BYTES")]
|
||||
pub snippet_length: usize,
|
||||
}
|
||||
|
||||
/// Confidence levels for findings
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum ConfidenceLevel {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
impl From<ConfidenceLevel> for Confidence {
|
||||
fn from(level: ConfidenceLevel) -> Self {
|
||||
match level {
|
||||
ConfidenceLevel::Low => Confidence::Low,
|
||||
ConfidenceLevel::Medium => Confidence::Medium,
|
||||
ConfidenceLevel::High => Confidence::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
180
src/cli/global.rs
Normal file
180
src/cli/global.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
use std::io::IsTerminal;
|
||||
|
||||
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
|
||||
use once_cell::sync::Lazy;
|
||||
use strum::Display;
|
||||
use sysinfo::{MemoryRefreshKind, RefreshKind, System};
|
||||
use tracing::Level;
|
||||
|
||||
use crate::cli::commands::{
|
||||
github::GitHubArgs, gitlab::GitLabArgs, rules::RulesArgs, scan::ScanArgs,
|
||||
};
|
||||
|
||||
#[deny(missing_docs)]
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
after_help = "Made with \u{2764} by MongoDB",
|
||||
)]
|
||||
/// Kingfisher by MongoDB — Detect and validate secrets across files and full Git history
|
||||
pub struct CommandLineArgs {
|
||||
/// The command to execute
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Global arguments that apply to all subcommands
|
||||
#[command(flatten)]
|
||||
pub global_args: GlobalArgs,
|
||||
}
|
||||
impl CommandLineArgs {
|
||||
/// Parse command-line arguments.
|
||||
///
|
||||
/// Automatically respects `NO_COLOR` and maps `--quiet` into disabling progress bars.
|
||||
pub fn parse_args() -> Self {
|
||||
// Use standard `Parser::parse` for simplicity
|
||||
let mut args = CommandLineArgs::parse();
|
||||
|
||||
// Apply NO_COLOR environment variable
|
||||
if std::env::var("NO_COLOR").is_ok() {
|
||||
args.global_args.color = Mode::Never;
|
||||
}
|
||||
|
||||
// If quiet is enabled, disable progress
|
||||
if args.global_args.quiet {
|
||||
args.global_args.progress = Mode::Never;
|
||||
}
|
||||
|
||||
args
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level subcommands
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Command {
|
||||
/// Scan content for secrets and sensitive information
|
||||
Scan(ScanArgs),
|
||||
|
||||
/// Interact with the GitHub API
|
||||
#[command(name = "github")]
|
||||
GitHub(GitHubArgs),
|
||||
|
||||
/// Interact with the GitLab API
|
||||
#[command(name = "gitlab")]
|
||||
GitLab(GitLabArgs),
|
||||
|
||||
/// Manage rules
|
||||
#[command(alias = "rule")]
|
||||
Rules(RulesArgs),
|
||||
}
|
||||
|
||||
pub static RAM_GB: Lazy<Option<f64>> = Lazy::new(|| {
|
||||
if sysinfo::IS_SUPPORTED_SYSTEM {
|
||||
let s = System::new_with_specifics(
|
||||
RefreshKind::new().with_memory(MemoryRefreshKind::new().with_ram()),
|
||||
);
|
||||
Some(s.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
/// Advanced global options unlikely to be used in normal scenarios.
|
||||
#[derive(Args, Debug, Clone)]
|
||||
#[command(next_help_heading = "Advanced Global Options")]
|
||||
pub struct AdvancedArgs {
|
||||
/// Set the rlimit for the number of open files
|
||||
#[arg(long, default_value_t = 16384, value_name = "LIMIT")]
|
||||
pub rlimit_nofile: u64,
|
||||
}
|
||||
|
||||
/// Top-level global CLI arguments
|
||||
#[derive(Args, Debug, Clone)]
|
||||
#[command(next_help_heading = "Global Options")]
|
||||
pub struct GlobalArgs {
|
||||
/// Enable verbose output (up to 3 times for more detail)
|
||||
#[arg(global = true, long = "verbose", short = 'v', action = ArgAction::Count)]
|
||||
pub verbose: u8,
|
||||
|
||||
/// Suppress non-error messages and disable progress bars
|
||||
#[arg(global = true, long, short)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Ignore TLS certificate validation
|
||||
#[arg(global = true, long)]
|
||||
pub ignore_certs: bool,
|
||||
|
||||
/// Update the Kingfisher binary to the latest release
|
||||
#[arg(global = true, long = "self-update", default_value_t = false)]
|
||||
pub self_update: bool,
|
||||
|
||||
/// Disable automatic update checks
|
||||
#[arg(global = true, long = "no-update-check", default_value_t = false)]
|
||||
pub no_update_check: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub advanced: AdvancedArgs,
|
||||
|
||||
// Internal fields (not CLI arguments)
|
||||
#[clap(skip)]
|
||||
pub color: Mode,
|
||||
|
||||
#[clap(skip)]
|
||||
pub progress: Mode,
|
||||
}
|
||||
|
||||
impl Default for GlobalArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
verbose: 0,
|
||||
quiet: false,
|
||||
ignore_certs: false,
|
||||
self_update: false,
|
||||
no_update_check: false,
|
||||
advanced: AdvancedArgs { rlimit_nofile: 16384 },
|
||||
color: Mode::Auto,
|
||||
progress: Mode::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalArgs {
|
||||
pub fn use_color<T: IsTerminal>(&self, out: T) -> bool {
|
||||
match self.color {
|
||||
Mode::Never => false,
|
||||
Mode::Always => true,
|
||||
Mode::Auto => out.is_terminal(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_progress(&self) -> bool {
|
||||
match self.progress {
|
||||
Mode::Never => false,
|
||||
Mode::Always => true,
|
||||
Mode::Auto => std::io::stderr().is_terminal(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_level(&self) -> Level {
|
||||
if self.quiet {
|
||||
Level::INFO
|
||||
} else {
|
||||
match self.verbose {
|
||||
0 => Level::INFO, // Default level if no `-v` is provided
|
||||
1 => Level::DEBUG, // `-v`
|
||||
2 => Level::TRACE, // `-vv`
|
||||
_ => Level::TRACE, // `-vvv` or more
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mode for enabling or disabling features based on terminal capabilities
|
||||
/// Generic mode with `auto/never/always`.
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Auto,
|
||||
Never,
|
||||
Always,
|
||||
}
|
||||
5
src/cli/mod.rs
Normal file
5
src/cli/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod commands;
|
||||
pub mod global;
|
||||
|
||||
// re‑export the top‑level parser and subcommand enum so main.rs can see them:
|
||||
pub use global::{CommandLineArgs, GlobalArgs};
|
||||
Loading…
Add table
Add a link
Reference in a new issue