use std::path::PathBuf;
use clap::{Args, ValueHint};
use tracing::warn;
use url::Url;
use crate::{
cli::commands::{
azure::AzureRepoType,
bitbucket::{BitbucketAuthArgs, BitbucketRepoType},
gitea::GiteaRepoType,
github::{GitCloneMode, GitHistoryMode, GitHubRepoType},
gitlab::GitLabRepoType,
},
git_url::GitUrl,
};
const DEFAULT_GITHUB_API_URL: &str = "https://api.github.com/";
const DEFAULT_GITLAB_API_URL: &str = "https://gitlab.com/";
const DEFAULT_GITEA_API_URL: &str = "https://gitea.com/api/v1/";
const DEFAULT_BITBUCKET_API_URL: &str = "https://api.bitbucket.org/2.0/";
const DEFAULT_AZURE_BASE_URL: &str = "https://dev.azure.com/";
const DEFAULT_SLACK_API_URL: &str = "https://slack.com/api/";
const DEFAULT_TEAMS_API_URL: &str = "https://graph.microsoft.com/";
const DEFAULT_POSTMAN_API_URL: &str = "https://api.getpostman.com/";
#[derive(Args, Debug, Clone)]
pub struct InputSpecifierArgs {
#[arg(num_args = 0.., value_hint = ValueHint::AnyPath)]
pub path_inputs: Vec<PathBuf>,
#[arg(long = "git-url", value_hint = ValueHint::Url)]
pub git_url: Vec<GitUrl>,
#[arg(long = "git-clone-dir", value_hint = ValueHint::DirPath, help_heading = "Git Options")]
pub git_clone_dir: Option<PathBuf>,
#[arg(long = "keep-clones", default_value_t = false, help_heading = "Git Options")]
pub keep_clones: bool,
#[arg(long = "repo-clone-limit", value_name = "COUNT")]
pub repo_clone_limit: Option<usize>,
#[arg(long = "include-contributors", default_value_t = false)]
pub include_contributors: bool,
#[arg(long, hide = true)]
pub github_user: Vec<String>,
#[arg(skip)]
pub github_include_gists: bool,
#[arg(long, alias = "github-org", hide = true)]
pub github_organization: Vec<String>,
#[arg(
long = "github-exclude",
alias = "github-exclude-repo",
value_name = "OWNER/REPO",
hide = true
)]
pub github_exclude: Vec<String>,
#[arg(long, alias = "all-github-orgs", requires = "github_api_url", hide = true)]
pub all_github_organizations: bool,
#[arg(
long,
alias = "api-url",
default_value = "https://api.github.com/",
value_hint = ValueHint::Url,
hide = true
)]
pub github_api_url: Url,
#[arg(long, default_value_t = GitHubRepoType::Source, hide = true)]
pub github_repo_type: GitHubRepoType,
#[arg(skip)]
pub github_event_user: Vec<String>,
#[arg(skip)]
pub github_event_lookback_hours: u64,
#[arg(long, hide = true)]
pub gitlab_user: Vec<String>,
#[arg(skip)]
pub gitlab_include_snippets: bool,
#[arg(long, alias = "gitlab-group", hide = true)]
pub gitlab_group: Vec<String>,
#[arg(
long = "gitlab-exclude",
alias = "gitlab-exclude-project",
alias = "gitlab-exclude-repo",
value_name = "GROUP/PROJECT",
hide = true
)]
pub gitlab_exclude: Vec<String>,
#[arg(long, alias = "all-gitlab-groups", requires = "gitlab_api_url", hide = true)]
pub all_gitlab_groups: bool,
#[arg(
long,
alias="gitlab-api-url",
default_value = "https://gitlab.com/",
value_hint = ValueHint::Url,
hide = true
)]
pub gitlab_api_url: Url,
#[arg(long, default_value_t = GitLabRepoType::All, hide = true)]
pub gitlab_repo_type: GitLabRepoType,
#[arg(long, alias = "include-subgroups", hide = true)]
pub gitlab_include_subgroups: bool,
#[arg(long = "huggingface-user", hide = true)]
pub huggingface_user: Vec<String>,
#[arg(long = "huggingface-organization", alias = "huggingface-org", hide = true)]
pub huggingface_organization: Vec<String>,
#[arg(long = "huggingface-model", hide = true)]
pub huggingface_model: Vec<String>,
#[arg(long = "huggingface-dataset", hide = true)]
pub huggingface_dataset: Vec<String>,
#[arg(long = "huggingface-space", hide = true)]
pub huggingface_space: Vec<String>,
#[arg(long = "huggingface-bucket", alias = "hf-bucket", hide = true)]
pub huggingface_bucket: Vec<String>,
#[arg(long = "huggingface-exclude", value_name = "IDENTIFIER", hide = true)]
pub huggingface_exclude: Vec<String>,
#[arg(long, hide = true)]
pub gitea_user: Vec<String>,
#[arg(long, alias = "gitea-org", hide = true)]
pub gitea_organization: Vec<String>,
#[arg(
long = "gitea-exclude",
alias = "gitea-exclude-repo",
value_name = "OWNER/REPO",
hide = true
)]
pub gitea_exclude: Vec<String>,
#[arg(long, alias = "all-gitea-orgs", hide = true)]
pub all_gitea_organizations: bool,
#[arg(
long,
alias="gitea-api-url",
default_value = "https://gitea.com/api/v1/",
value_hint = ValueHint::Url,
hide = true
)]
pub gitea_api_url: Url,
#[arg(long, default_value_t = GiteaRepoType::Source, hide = true)]
pub gitea_repo_type: GiteaRepoType,
#[arg(long, hide = true)]
pub bitbucket_user: Vec<String>,
#[arg(skip)]
pub bitbucket_include_snippets: bool,
#[arg(long, alias = "bitbucket-workspace", alias = "bitbucket-team", hide = true)]
pub bitbucket_workspace: Vec<String>,
#[arg(long, alias = "bitbucket-project", hide = true)]
pub bitbucket_project: Vec<String>,
#[arg(long = "bitbucket-exclude", value_name = "OWNER/REPO", hide = true)]
pub bitbucket_exclude: Vec<String>,
#[arg(long, alias = "all-bitbucket-workspaces", requires = "bitbucket_api_url", hide = true)]
pub all_bitbucket_workspaces: bool,
#[arg(
long,
default_value = "https://api.bitbucket.org/2.0/",
value_hint = ValueHint::Url,
hide = true
)]
pub bitbucket_api_url: Url,
#[arg(long, default_value_t = BitbucketRepoType::Source, hide = true)]
pub bitbucket_repo_type: BitbucketRepoType,
#[command(flatten)]
pub bitbucket_auth: BitbucketAuthArgs,
#[arg(long = "azure-organization", hide = true)]
pub azure_organization: Vec<String>,
#[arg(long = "azure-project", value_name = "ORGANIZATION/PROJECT", hide = true)]
pub azure_project: Vec<String>,
#[arg(
long = "azure-exclude",
alias = "azure-exclude-repo",
value_name = "ORGANIZATION/PROJECT/REPOSITORY",
hide = true
)]
pub azure_exclude: Vec<String>,
#[arg(long = "all-azure-projects", hide = true)]
pub all_azure_projects: bool,
#[arg(
long = "azure-base-url",
default_value = "https://dev.azure.com/",
value_hint = ValueHint::Url,
hide = true
)]
pub azure_base_url: Url,
#[arg(long = "azure-repo-type", default_value_t = AzureRepoType::Source, hide = true)]
pub azure_repo_type: AzureRepoType,
#[arg(long, value_hint = ValueHint::Url, requires = "jql", hide = true)]
pub jira_url: Option<Url>,
#[arg(long, requires = "jira_url", hide = true)]
pub jql: Option<String>,
#[arg(long = "jira-include-comments", requires = "jira_url", hide = true)]
pub jira_include_comments: bool,
#[arg(long = "jira-include-changelog", requires = "jira_url", hide = true)]
pub jira_include_changelog: bool,
#[arg(long, value_hint = ValueHint::Url, requires = "cql", hide = true)]
pub confluence_url: Option<Url>,
#[arg(long, requires = "confluence_url", hide = true)]
pub cql: Option<String>,
#[arg(long, hide = true)]
pub slack_query: Option<String>,
#[arg(long, default_value = "https://slack.com/api/", value_hint = ValueHint::Url, hide = true)]
pub slack_api_url: Url,
#[arg(long, hide = true)]
pub teams_query: Option<String>,
#[arg(long, default_value = "https://graph.microsoft.com/", value_hint = ValueHint::Url, hide = true)]
pub teams_api_url: Url,
#[arg(long = "postman-workspace", value_name = "ID_OR_URL", hide = true)]
pub postman_workspaces: Vec<String>,
#[arg(long = "postman-collection", value_name = "UID_OR_URL", hide = true)]
pub postman_collections: Vec<String>,
#[arg(long = "postman-environment", value_name = "UID", hide = true)]
pub postman_environments: Vec<String>,
#[arg(
long = "postman-all",
hide = true,
conflicts_with_all = ["postman_workspaces", "postman_collections", "postman_environments"],
)]
pub postman_all: bool,
#[arg(long = "postman-include-mocks-monitors", hide = true)]
pub postman_include_mocks_monitors: bool,
#[arg(
long = "postman-api-url",
default_value = DEFAULT_POSTMAN_API_URL,
value_hint = ValueHint::Url,
hide = true,
)]
pub postman_api_url: Url,
#[arg(long, default_value_t = 100, hide = true)]
pub max_results: usize,
#[arg(long, hide = true)]
pub s3_bucket: Option<String>,
#[arg(long, requires = "s3_bucket", hide = true)]
pub s3_prefix: Option<String>,
#[arg(long, requires = "s3_bucket", hide = true)]
pub role_arn: Option<String>,
#[arg(long, requires = "s3_bucket", hide = true)]
pub aws_local_profile: Option<String>,
#[arg(long, hide = true)]
pub gcs_bucket: Option<String>,
#[arg(long, requires = "gcs_bucket", hide = true)]
pub gcs_prefix: Option<String>,
#[arg(long, value_hint = ValueHint::FilePath, requires = "gcs_bucket", hide = true)]
pub gcs_service_account: Option<PathBuf>,
#[arg(long = "docker-image", hide = true)]
pub docker_image: Vec<String>,
#[arg(skip)]
pub docker_archive: Vec<PathBuf>,
#[arg(long, default_value_t=GitCloneMode::Bare, alias="git-clone-mode")]
pub git_clone: GitCloneMode,
#[arg(long, default_value_t=GitHistoryMode::Full)]
pub git_history: GitHistoryMode,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set, help_heading = "Git Options")]
pub commit_metadata: bool,
#[arg(long, help_heading = "Git Options")]
pub repo_artifacts: bool,
#[arg(long, default_value_t = true)]
pub scan_nested_repos: bool,
#[arg(long = "since-commit", value_name = "GIT-REF", help_heading = "Git Options")]
pub since_commit: Option<String>,
#[arg(
long,
help_heading = "Git Options",
conflicts_with = "branch_root",
conflicts_with = "branch_root_commit"
)]
pub staged: bool,
#[arg(
long,
value_name = "GIT-REF",
help_heading = "Git Options",
alias = "ref",
visible_alias = "ref"
)]
pub branch: Option<String>,
#[arg(
long = "branch-root",
help_heading = "Git Options",
requires = "branch",
conflicts_with = "since_commit",
action = clap::ArgAction::SetTrue
)]
pub branch_root: bool,
#[arg(
long = "branch-root-commit",
value_name = "GIT-REF",
help_heading = "Git Options",
conflicts_with = "since_commit"
)]
pub branch_root_commit: Option<String>,
}
impl InputSpecifierArgs {
pub fn has_any_input(&self) -> bool {
!self.path_inputs.is_empty()
|| !self.git_url.is_empty()
|| !self.github_user.is_empty()
|| !self.github_organization.is_empty()
|| self.all_github_organizations
|| !self.github_event_user.is_empty()
|| !self.gitlab_user.is_empty()
|| !self.gitlab_group.is_empty()
|| self.all_gitlab_groups
|| !self.gitea_user.is_empty()
|| !self.gitea_organization.is_empty()
|| self.all_gitea_organizations
|| !self.huggingface_user.is_empty()
|| !self.huggingface_organization.is_empty()
|| !self.huggingface_model.is_empty()
|| !self.huggingface_dataset.is_empty()
|| !self.huggingface_space.is_empty()
|| !self.huggingface_bucket.is_empty()
|| !self.bitbucket_user.is_empty()
|| !self.bitbucket_workspace.is_empty()
|| !self.bitbucket_project.is_empty()
|| self.all_bitbucket_workspaces
|| !self.azure_organization.is_empty()
|| !self.azure_project.is_empty()
|| self.all_azure_projects
|| self.jira_url.is_some()
|| self.confluence_url.is_some()
|| self.slack_query.is_some()
|| self.teams_query.is_some()
|| !self.postman_workspaces.is_empty()
|| !self.postman_collections.is_empty()
|| !self.postman_environments.is_empty()
|| self.postman_all
|| self.s3_bucket.is_some()
|| self.gcs_bucket.is_some()
|| !self.docker_image.is_empty()
|| !self.docker_archive.is_empty()
}
pub fn has_artifact_sources(&self) -> bool {
self.repo_artifacts
|| self.jira_url.is_some()
|| self.confluence_url.is_some()
|| self.slack_query.is_some()
|| self.teams_query.is_some()
|| !self.postman_workspaces.is_empty()
|| !self.postman_collections.is_empty()
|| !self.postman_environments.is_empty()
|| self.postman_all
|| !self.docker_image.is_empty()
|| !self.docker_archive.is_empty()
}
pub fn emit_deprecated_warnings(&self, used_legacy_git_url_flag: bool) {
if used_legacy_git_url_flag {
warn_deprecated_provider(
"Git URL",
"Passing repository URLs with `--git-url` is deprecated. Pass the URL as a positional scan target instead, e.g. `kingfisher scan github.com/org/repo`.",
);
}
if self.using_legacy_github_flags() {
warn_deprecated_provider(
"GitHub",
"Use `kingfisher scan github …` instead of the legacy `--github-*` flags.",
);
}
if self.using_legacy_gitlab_flags() {
warn_deprecated_provider(
"GitLab",
"Use `kingfisher scan gitlab …` instead of the legacy `--gitlab-*` flags.",
);
}
if self.using_legacy_gitea_flags() {
warn_deprecated_provider(
"Gitea",
"Use `kingfisher scan gitea …` instead of the legacy `--gitea-*` flags.",
);
}
if self.using_legacy_bitbucket_flags() {
warn_deprecated_provider(
"Bitbucket",
"Use `kingfisher scan bitbucket …` instead of the legacy `--bitbucket-*` flags.",
);
}
if self.using_legacy_azure_flags() {
warn_deprecated_provider(
"Azure DevOps",
"Use `kingfisher scan azure …` instead of the legacy `--azure-*` flags.",
);
}
if self.using_legacy_huggingface_flags() {
warn_deprecated_provider(
"Hugging Face",
"Use `kingfisher scan huggingface …` instead of the legacy `--huggingface-*` flags.",
);
}
if self.slack_query.is_some() || self.slack_api_url.as_str() != DEFAULT_SLACK_API_URL {
warn_deprecated_provider(
"Slack",
"Use `kingfisher scan slack …` instead of the legacy `--slack-*` flags.",
);
}
if self.teams_query.is_some() || self.teams_api_url.as_str() != DEFAULT_TEAMS_API_URL {
warn_deprecated_provider(
"Microsoft Teams",
"Use `kingfisher scan teams …` instead of the legacy `--teams-*` flags.",
);
}
if self.jira_url.is_some() || self.jql.is_some() {
warn_deprecated_provider(
"Jira",
"Use `kingfisher scan jira …` instead of the legacy `--jira-*` flags.",
);
}
if self.confluence_url.is_some() || self.cql.is_some() {
warn_deprecated_provider(
"Confluence",
"Use `kingfisher scan confluence …` instead of the legacy `--confluence-*` flags.",
);
}
if self.using_legacy_s3_flags() {
warn_deprecated_provider(
"Amazon S3",
"Use `kingfisher scan s3 …` instead of the legacy `--s3-*` flags.",
);
}
if self.using_legacy_gcs_flags() {
warn_deprecated_provider(
"Google Cloud Storage",
"Use `kingfisher scan gcs …` instead of the legacy `--gcs-*` flags.",
);
}
if !self.docker_image.is_empty() {
warn_deprecated_provider(
"Docker",
"Use `kingfisher scan docker …` instead of the legacy `--docker-image` flag.",
);
}
}
fn using_legacy_github_flags(&self) -> bool {
!self.github_user.is_empty()
|| !self.github_organization.is_empty()
|| !self.github_exclude.is_empty()
|| self.all_github_organizations
|| self.github_repo_type != GitHubRepoType::Source
|| self.github_api_url.as_str() != DEFAULT_GITHUB_API_URL
}
fn using_legacy_gitlab_flags(&self) -> bool {
!self.gitlab_user.is_empty()
|| !self.gitlab_group.is_empty()
|| !self.gitlab_exclude.is_empty()
|| self.all_gitlab_groups
|| self.gitlab_include_subgroups
|| self.gitlab_repo_type != GitLabRepoType::All
|| self.gitlab_api_url.as_str() != DEFAULT_GITLAB_API_URL
}
fn using_legacy_gitea_flags(&self) -> bool {
!self.gitea_user.is_empty()
|| !self.gitea_organization.is_empty()
|| !self.gitea_exclude.is_empty()
|| self.all_gitea_organizations
|| self.gitea_repo_type != GiteaRepoType::Source
|| self.gitea_api_url.as_str() != DEFAULT_GITEA_API_URL
}
fn using_legacy_bitbucket_flags(&self) -> bool {
!self.bitbucket_user.is_empty()
|| !self.bitbucket_workspace.is_empty()
|| !self.bitbucket_project.is_empty()
|| !self.bitbucket_exclude.is_empty()
|| self.all_bitbucket_workspaces
|| self.bitbucket_repo_type != BitbucketRepoType::Source
|| self.bitbucket_api_url.as_str() != DEFAULT_BITBUCKET_API_URL
}
fn using_legacy_azure_flags(&self) -> bool {
!self.azure_organization.is_empty()
|| !self.azure_project.is_empty()
|| !self.azure_exclude.is_empty()
|| self.all_azure_projects
|| self.azure_repo_type != AzureRepoType::Source
|| self.azure_base_url.as_str() != DEFAULT_AZURE_BASE_URL
}
fn using_legacy_huggingface_flags(&self) -> bool {
!self.huggingface_user.is_empty()
|| !self.huggingface_organization.is_empty()
|| !self.huggingface_model.is_empty()
|| !self.huggingface_dataset.is_empty()
|| !self.huggingface_space.is_empty()
|| !self.huggingface_bucket.is_empty()
|| !self.huggingface_exclude.is_empty()
}
fn using_legacy_s3_flags(&self) -> bool {
self.s3_bucket.is_some()
|| self.s3_prefix.is_some()
|| self.role_arn.is_some()
|| self.aws_local_profile.is_some()
}
fn using_legacy_gcs_flags(&self) -> bool {
self.gcs_bucket.is_some() || self.gcs_prefix.is_some() || self.gcs_service_account.is_some()
}
}
fn warn_deprecated_provider(provider: &str, guidance: &str) {
warn!(
"{provider} legacy scan flags are deprecated and will be removed in a future release. {guidance}",
provider = provider,
guidance = guidance
);
}
#[derive(Args, Debug, Clone)]
pub struct ContentFilteringArgs {
#[arg(
global = true,
long = "max-file-size",
visible_alias = "max-filesize", // also show in --help
default_value_t = 256.0,
value_name = "MB"
)]
pub max_file_size_mb: f64,
#[arg(global = true, long, value_name = "PATTERN")]
pub exclude: Vec<String>,
#[arg(global = true, long = "no-extract-archives", default_value_t = false)]
pub no_extract_archives: bool,
#[arg(global = true, long = "extraction-depth", default_value_t = 2, value_parser = clap::value_parser!(u8).range(1..=25))]
pub extraction_depth: u8,
#[arg(global = true, long = "no-binary", default_value_t = false)]
pub no_binary: bool,
}
impl ContentFilteringArgs {
pub fn max_file_size_bytes(&self) -> Option<u64> {
if self.max_file_size_mb < 0.0 {
Some(256 * 1024 * 1024) } else {
Some((self.max_file_size_mb * 1024.0 * 1024.0) as u64)
}
}
}