diff --git a/src/secret-detect/cmd/detect.rs b/src/secret-detect/cmd/detect.rs new file mode 100644 index 0000000..2d94ddc --- /dev/null +++ b/src/secret-detect/cmd/detect.rs @@ -0,0 +1,145 @@ +use std::time::Instant; +use std::process::exit; + +use clap::{arg, ArgMatches, Command}; +use log::{error, info, LevelFilter}; +use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; +use std::io::{self, Read}; + +use crate::config::Config; +use crate::detect::Detector; +use crate::report::{Finding, FindingSummary}; +use crate::sources::{DirectoryTargets, GitLogCmd, PipeReader}; + +// Function to initialize logging based on verbosity level +fn init_logging(verbose: u64) { + let log_level = match verbose { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + _ => LevelFilter::Trace, + }; + + let config = ConfigBuilder::new() + .set_target_level(log_level) + .set_thread_level(LevelFilter::Off) + .build(); + + TermLogger::init( + log_level, + config, + TerminalMode::Mixed, + ColorChoice::Auto, + ) + .unwrap(); +} + +// Function to run the detect command +pub fn run_detect(matches: &ArgMatches) { + let start_time = Instant::now(); + + // Initialize logging + init_logging(matches.get_count("verbose") as u64); + + // Load the configuration + let config = match Config::load_from_file(matches.get_one::("config").unwrap()) { + Ok(cfg) => cfg, + Err(e) => { + error!("Failed to load configuration: {}", e); + exit(1); + } + }; + + // Get source path + let source_path = matches.get_one::("source").unwrap(); + + // Create the detector + let mut detector = Detector::new(&config, source_path); + + // Determine the scan type + let no_git = matches.get_flag("no-git"); + let from_pipe = matches.get_flag("pipe"); + + // Perform the scan + let findings: Vec = if no_git { + // Scan directory + match DirectoryTargets::new(source_path, detector.sema.clone(), detector.follow_symlinks) { + Ok(paths) => match detector.detect_files(&paths) { + Ok(f) => f, + Err(e) => { + error!("Failed to scan directory: {}", e); + vec![] // Return an empty vector on error + } + }, + Err(e) => { + error!("Failed to get directory targets: {}", e); + exit(1); + } + } + } else if from_pipe { + // Scan from pipe + let mut buffer = String::new(); + match io::stdin().read_to_string(&mut buffer) { + Ok(_) => match detector.detect_reader(&buffer) { + Ok(f) => f, + Err(e) => { + error!("Failed to scan from pipe: {}", e); + exit(1); + } + }, + Err(e) => { + error!("Failed to read from pipe: {}", e); + exit(1); + } + } + } else { + // Scan git history + let log_opts = matches.get_one::("log-opts").unwrap(); + match GitLogCmd::new(source_path, log_opts) { + Ok(git_cmd) => match detector.detect_git(&git_cmd) { + Ok(f) => f, + Err(e) => { + error!("Failed to scan git history: {}", e); + vec![] // Return an empty vector on error + } + }, + Err(e) => { + error!("Failed to create git log command: {}", e); + exit(1); + } + } + }; + + // Create the finding summary + let finding_summary = FindingSummary::new(&findings, start_time.elapsed(), config); + + // Handle the findings and exit + finding_summary.handle_findings(&matches); +} + +// Function to write the configuration to a file +fn write_config_to_file(config: &Config, output_path: &str) -> Result<(), std::io::Error> { + let mut file = File::create(output_path)?; + // Template file or logic to generate the TOML content + let toml_content = config.generate_toml(); + file.write_all(toml_content.as_bytes())?; + Ok(()) +} + +// CLI definition using clap +pub fn cli() -> Command { + Command::new("gitleaks") + .about("Detect secrets in code") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("detect") + .about("Detect secrets") + .arg(arg!(-c --config "Path to gitleaks.toml config file").required(true)) + .arg(arg!(-s --source "Source to scan (file, directory, or git repository URL)").required(true)) + .arg(arg!(--no-git "Treat git repo as a regular directory")) + .arg(arg!(--pipe "Scan input from stdin")) + .arg(arg!(-l --log-opts "Git log options (only applicable for git scans)")) + .arg(arg!(-e --exit-code "Exit code to use if secrets are found (default: 1)").default_value("1")) + .arg(arg!(-v ... "Increase verbosity level")) + ) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/main.rs b/src/secret-detect/cmd/generate/config/main.rs new file mode 100644 index 0000000..e98ec58 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/main.rs @@ -0,0 +1,228 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::Write; + +use log::{error, info}; + +use crate::config::{Allowlist, Config, Rule}; +use crate::rules::*; + +// Assuming rules module contains all the rule functions + +const TEMPLATE_PATH: &str = "rules/config.tmpl"; + +// Entry point for generating the keyshade.toml configuration file +pub fn generate_config_file(output_path: &str) { + let mut config = Config::default(); + config.rules = build_rule_lookup(); + + if let Err(e) = write_config_to_file(&config, output_path) { + error!("Failed to write config to file: {}", e); + } else { + info!("Successfully generated keyshade.toml at {}", output_path); + } +} + +// Build a HashMap of rules, ensuring unique rule ID +fn build_rule_lookup() -> HashMap { + let mut rule_lookup = HashMap::new(); + + // Macro to add rules to the lookup table + macro_rules! add_rule { + ($rule_fn:ident) => { + let rule = $rule_fn(); + if rule_lookup.insert(rule.rule_id.clone(), rule).is_some() { + error!("Rule ID {} is not unique", rule.rule_id); + } + }; + } + + // Add each rule using the macro + aadd_rule!(adafruit_api_key); + add_rule!(adobe_client_id); + add_rule!(adobe_client_secret); + add_rule!(age_secret_key); + add_rule!(airtable); + add_rule!(algolia_api_key); + add_rule!(alibaba_access_key); + add_rule!(alibaba_secret_key); + add_rule!(asana_client_id); + add_rule!(asana_client_secret); + add_rule!(atlassian); + add_rule!(authress); + add_rule!(aws); + add_rule!(beamer); + add_rule!(bitbucket_client_id); + add_rule!(bitbucket_client_secret); + add_rule!(bittrex_access_key); + add_rule!(bittrex_secret_key); + add_rule!(clojars); + add_rule!(cloudflare_global_api_key); + add_rule!(cloudflare_api_key); + add_rule!(cloudflare_origin_ca_key); + add_rule!(codecov_access_token); + add_rule!(coinbase_access_token); + add_rule!(confluent_secret_key); + add_rule!(confluent_access_token); + add_rule!(contentful); + add_rule!(databricks); + add_rule!(datadogtoken_access_token); + add_rule!(defined_networking_api_token); + add_rule!(digitalocean_pat); + add_rule!(digitalocean_oauth_token); + add_rule!(digitalocean_refresh_token); + add_rule!(discord_api_token); + add_rule!(discord_client_id); + add_rule!(discord_client_secret); + add_rule!(doppler); + add_rule!(drop_box_api_secret); + add_rule!(drop_box_long_lived_api_token); + add_rule!(drop_box_short_lived_api_token); + add_rule!(droneci_access_token); + add_rule!(duffel); + add_rule!(dynatrace); + add_rule!(easy_post); + add_rule!(easy_post_test_api); + add_rule!(etsy_access_token); + add_rule!(facebook_secret); + add_rule!(facebook_access_token); + add_rule!(facebook_page_access_token); + add_rule!(fastly_api_token); + add_rule!(finicity_api_token); + add_rule!(finicity_client_secret); + add_rule!(finnhub_access_token); + add_rule!(flickr_access_token); + add_rule!(flutterwave_enc_key); + add_rule!(flutterwave_public_key); + add_rule!(flutterwave_secret_key); + add_rule!(frame_io); + add_rule!(freshbooks_access_token); + add_rule!(gcp_api_key); + add_rule!(gcp_service_account); + add_rule!(generic_credential); + add_rule!(github_app); + add_rule!(github_fine_grained_pat); + add_rule!(github_oauth); + add_rule!(github_pat); + add_rule!(github_refresh); + add_rule!(gitlab_pat); + add_rule!(gitlab_pipeline_trigger_token); + add_rule!(gitlab_runner_registration_token); + add_rule!(hashicorp); + add_rule!(hashicorp_field); + add_rule!(heroku); + add_rule!(hubspot); + add_rule!(hugging_face_access_token); + add_rule!(hugging_face_organization_api_token); + add_rule!(infracost_api_token); + add_rule!(intercom); + add_rule!(jfrog_api_key); + add_rule!(jfrog_identity_token); + add_rule!(jwt); + add_rule!(jwt_base64); + add_rule!(kraken_access_token); + add_rule!(kucoin_access_token); + add_rule!(kucoin_secret_key); + add_rule!(launchdarkly_access_token); + add_rule!(linear_api_token); + add_rule!(linear_client_secret); + add_rule!(linkedin_client_id); + add_rule!(linkedin_client_secret); + add_rule!(lob_api_token); + add_rule!(lob_pub_api_token); + add_rule!(mailchimp); + add_rule!(mailgun_private_api_token); + add_rule!(mailgun_pub_api_token); + add_rule!(mailgun_signing_key); + add_rule!(mapbox); + add_rule!(mattermost_access_token); + add_rule!(messagebird_api_token); + add_rule!(messagebird_client_id); + add_rule!(netlify_access_token); + add_rule!(new_relic_browser_api_key); + add_rule!(new_relic_user_id); + add_rule!(new_relic_user_key); + add_rule!(npm); + add_rule!(nytimes_access_token); + add_rule!(okta_access_token); + add_rule!(openai); + add_rule!(plaid_access_id); + add_rule!(plaid_secret_key); + add_rule!(plaid_access_token); + add_rule!(planetscale_api_token); + add_rule!(planetscale_oauth_token); + add_rule!(planetscale_password); + add_rule!(postman_api); + add_rule!(prefect); + add_rule!(private_key); + add_rule!(pulumi_api_token); + add_rule!(pypi_upload_token); + add_rule!(rapidapi_access_token); + add_rule!(readme); + add_rule!(rubygems_api_token); + add_rule!(scalingo_api_token); + add_rule!(sendbird_access_id); + add_rule!(sendbird_access_token); + add_rule!(sendgrid_api_token); + add_rule!(sendinblue_api_token); + add_rule!(sentry_access_token); + add_rule!(shippo_api_token); + add_rule!(shopify_access_token); + add_rule!(shopify_custom_access_token); + add_rule!(shopify_private_app_access_token); + add_rule!(shopify_shared_secret); + add_rule!(sidekiq_secret); + add_rule!(sidekiq_sensitive_url); + add_rule!(slack_app_level_token); + add_rule!(slack_bot_token); + add_rule!(slack_configuration_refresh_token); + add_rule!(slack_configuration_token); + add_rule!(slack_legacy_bot_token); + add_rule!(slack_legacy_token); + add_rule!(slack_legacy_workspace_token); + add_rule!(slack_user_token); + add_rule!(slack_webhook_url); + add_rule!(snyk); + add_rule!(square_access_token); + add_rule!(square_secret); + add_rule!(squarespace_access_token); + add_rule!(sumologic_access_id); + add_rule!(sumologic_access_token); + add_rule!(teams_webhook); + add_rule!(telegram_bot_token); + add_rule!(travis_ci_access_token); + add_rule!(trello_access_token); + add_rule!(twilio); + add_rule!(twitch_api_token); + add_rule!(twitter_api_key); + add_rule!(twitter_api_secret); + add_rule!(twitter_access_secret); + add_rule!(twitter_access_token); + add_rule!(twitter_bearer_token); + add_rule!(typeform); + add_rule!(vault_batch_token); + add_rule!(vault_service_token); + add_rule!(yandex_api_key); + add_rule!(yandex_aws_access_token); + add_rule!(yandex_access_token); + add_rule!(zendesk_secret_key); + + rule_lookup +} + +// Write the configuration to a TOML file +fn write_config_to_file(config: &Config, output_path: &str) -> Result<(), std::io::Error> { + let template = include_str!(TEMPLATE_PATH); + let rendered_config = match tera::Tera::one_off(template, &tera::Context::from_serialize(config).unwrap(), true) { + Ok(s) => s, + Err(e) => { + error!("Failed to render config template: {}", e); + return Err(std::io::Error::new(std::io::ErrorKind::Other, "Template rendering failed")); + } + }; + + let mut file = File::create(output_path)?; + file.write_all(rendered_config.as_bytes())?; + + Ok(()) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/adafruit.rs b/src/secret-detect/cmd/generate/config/rules/adafruit.rs new file mode 100644 index 0000000..846ad20 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/adafruit.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn adafruit_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a potential Adafruit API Key, which could lead to unauthorized access to Adafruit services and sensitive data exposure.".to_string(), + rule_id: "adafruit-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["adafruit"], &alpha_numeric_extended_short("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["adafruit".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("adafruit", &secrets::new_secret(&alpha_numeric_extended_short("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/adobe.rs b/src/secret-detect/cmd/generate/config/rules/adobe.rs new file mode 100644 index 0000000..c08aaa6 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/adobe.rs @@ -0,0 +1,52 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn adobe_client_id() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a pattern that resembles an Adobe OAuth Web Client ID, posing a risk of compromised Adobe integrations and data breaches.".to_string(), + rule_id: "adobe-client-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["adobe"], &hex("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["adobe".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("adobe", &secrets::new_secret(&hex("32"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn adobe_client_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a potential Adobe Client Secret, which, if exposed, could allow unauthorized Adobe service access and data manipulation.".to_string(), + rule_id: "adobe-client-secret".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"(p8e-)(?i)[a-z0-9]{32}", true)).unwrap(), + tags: vec![], + keywords: vec!["p8e-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + &format!("adobeClient := \"p8e-{} +\"", secrets::new_secret(&hex("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/age.rs b/src/secret-detect/cmd/generate/config/rules/age.rs new file mode 100644 index 0000000..d16eeef --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/age.rs @@ -0,0 +1,26 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn age_secret_key() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a potential Age encryption tool secret key, risking data decryption and unauthorized access to sensitive information.".to_string(), + rule_id: "age-secret-key".to_string(), + regex: Regex::new(r"AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}").unwrap(), + tags: vec![], + keywords: vec!["AGE-SECRET-KEY-1".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"apiKey := "AGE-SECRET-KEY-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ"#, // gitleaks:allow + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/airtable.rs b/src/secret-detect/cmd/generate/config/rules/airtable.rs new file mode 100644 index 0000000..317dd4e --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/airtable.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn airtable() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a possible Airtable API Key, potentially compromising database access and leading to data leakage or alteration.".to_string(), + rule_id: "airtable-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["airtable"], &alpha_numeric("17"), true)).unwrap(), + tags: vec![], + keywords: vec!["airtable".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("airtable", &secrets::new_secret(&alpha_numeric("17"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/algolia.rs b/src/secret-detect/cmd/generate/config/rules/algolia.rs new file mode 100644 index 0000000..fa79e00 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/algolia.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn algolia_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Identified an Algolia API Key, which could result in unauthorized search operations and data exposure on Algolia-managed platforms.".to_string(), + rule_id: "algolia-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["algolia"], r"[a-z0-9]{32}", true)).unwrap(), + tags: vec![], + keywords: vec!["algolia".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + &format!("algolia_key := {}", secrets::new_secret(&hex("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/alibaba.rs b/src/secret-detect/cmd/generate/config/rules/alibaba.rs new file mode 100644 index 0000000..d2b6a71 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/alibaba.rs @@ -0,0 +1,54 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn alibaba_access_key() -> Rule { + // Define rule + let rule = Rule { + description: "Detected an Alibaba Cloud AccessKey ID, posing a risk of unauthorized cloud resource access and potential data compromise.".to_string(), + rule_id: "alibaba-access-key-id".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"(LTAI)(?i)[a-z0-9]{20}", true)).unwrap(), + tags: vec![], + keywords: vec!["LTAI".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + &format!("alibabaKey := \"LTAI{} +\"", secrets::new_secret(&hex("20"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn alibaba_secret_key() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a potential Alibaba Cloud Secret Key, potentially allowing unauthorized operations and data access within Alibaba Cloud.".to_string(), + rule_id: "alibaba-secret-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["alibaba"], &alpha_numeric("30"), true)).unwrap(), + tags: vec![], + keywords: vec!["alibaba".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("alibaba", &secrets::new_secret(&alpha_numeric("30"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/asana.rs b/src/secret-detect/cmd/generate/config/rules/asana.rs new file mode 100644 index 0000000..198952a --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/asana.rs @@ -0,0 +1,51 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - numeric(length: &str) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn asana_client_id() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a potential Asana Client ID, risking unauthorized access to Asana projects and sensitive task information.".to_string(), + rule_id: "asana-client-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["asana"], &numeric("16"), true)).unwrap(), + tags: vec![], + keywords: vec!["asana".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("asana", &secrets::new_secret(&numeric("16"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn asana_client_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Identified an Asana Client Secret, which could lead to compromised project management integrity and unauthorized access.".to_string(), + rule_id: "asana-client-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["asana"], &alpha_numeric("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["asana".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("asana", &secrets::new_secret(&alpha_numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/atlassian.rs b/src/secret-detect/cmd/generate/config/rules/atlassian.rs new file mode 100644 index 0000000..0981b37 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/atlassian.rs @@ -0,0 +1,31 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn atlassian() -> Rule { + // Define rule + let rule = Rule { + description: "Detected an Atlassian API token, posing a threat to project management and collaboration tool security and data confidentiality.".to_string(), + rule_id: "atlassian-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["atlassian", "confluence", "jira"], &alpha_numeric("24"), true)).unwrap(), + tags: vec![], + keywords: vec!["atlassian".to_string(), "confluence".to_string(), "jira".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("atlassian", &secrets::new_secret(&alpha_numeric("24"))), + generate_sample_secret("confluence", &secrets::new_secret(&alpha_numeric("24"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/authress.rs b/src/secret-detect/cmd/generate/config/rules/authress.rs new file mode 100644 index 0000000..fb7e70c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/authress.rs @@ -0,0 +1,35 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn authress() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a possible Authress Service Client Access Key, which may compromise access control services and sensitive data.".to_string(), + rule_id: "authress-service-client-access-key".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"(?:sc|ext|scauth|authress)_[a-z0-9]{5,30}\.[a-z0-9]{4,6}\.acc[_-][a-z0-9-]{10,32}\.[a-z0-9+/_=-]{30,120}", true)).unwrap(), + tags: vec![], + keywords: vec!["sc_".to_string(), "ext_".to_string(), "scauth_".to_string(), "authress_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let service_client_id = format!("sc_{}", alpha_numeric("10")); + let access_key_id = alpha_numeric("4"); + let account_id = format!("acc_{}", alpha_numeric("10")); + let signature_key = alpha_numeric_extended_short("40"); + + let test_positives = vec![ + generate_sample_secret("authress", &secrets::new_secret(&format!("{}.{}.{}.{}", service_client_id, access_key_id, account_id, signature_key))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/aws.rs b/src/secret-detect/cmd/generate/config/rules/aws.rs new file mode 100644 index 0000000..a3d0f11 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/aws.rs @@ -0,0 +1,31 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + + +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn aws() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms.".to_string(), + rule_id: "aws-access-token".to_string(), + regex: Regex::new(r"(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}").unwrap(), + tags: vec![], + keywords: vec![ + "AKIA".to_string(), + "ASIA".to_string(), + "ABIA".to_string(), + "ACCA".to_string(), + ], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![generate_sample_secret("AWS", "AKIALALEMEL33243OLIB")]; // gitleaks:allow + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/beamer.rs b/src/secret-detect/cmd/generate/config/rules/beamer.rs new file mode 100644 index 0000000..dcb2ed6 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/beamer.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn beamer() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Beamer API token, potentially compromising content management and exposing sensitive notifications and updates.".to_string(), + rule_id: "beamer-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["beamer"], r"b_[a-z0-9=_\-]{44}", true)).unwrap(), + tags: vec![], + keywords: vec!["beamer".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("beamer", &format!("b_{}", secrets::new_secret(&alpha_numeric_extended("44")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/bitbucket.rs b/src/secret-detect/cmd/generate/config/rules/bitbucket.rs new file mode 100644 index 0000000..5c1ae25 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/bitbucket.rs @@ -0,0 +1,51 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn bitbucket_client_id() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a potential Bitbucket Client ID, risking unauthorized repository access and potential codebase exposure.".to_string(), + rule_id: "bitbucket-client-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["bitbucket"], &alpha_numeric("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["bitbucket".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("bitbucket", &secrets::new_secret(&alpha_numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn bitbucket_client_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a potential Bitbucket Client Secret, posing a risk of compromised code repositories and unauthorized access.".to_string(), + rule_id: "bitbucket-client-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["bitbucket"], &alpha_numeric_extended("64"), true)).unwrap(), + tags: vec![], + keywords: vec!["bitbucket".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("bitbucket", &secrets::new_secret(&alpha_numeric("64"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/bittrex.rs b/src/secret-detect/cmd/generate/config/rules/bittrex.rs new file mode 100644 index 0000000..6de4d67 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/bittrex.rs @@ -0,0 +1,50 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn bittrex_access_key() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Bittrex Access Key, which could lead to unauthorized access to cryptocurrency trading accounts and financial loss.".to_string(), + rule_id: "bittrex-access-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["bittrex"], &alpha_numeric("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["bittrex".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("bittrex", &secrets::new_secret(&alpha_numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn bittrex_secret_key() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Bittrex Secret Key, potentially compromising cryptocurrency transactions and financial security.".to_string(), + rule_id: "bittrex-secret-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["bittrex"], &alpha_numeric("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["bittrex".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("bittrex", &secrets::new_secret(&alpha_numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/clojars.rs b/src/secret-detect/cmd/generate/config/rules/clojars.rs new file mode 100644 index 0000000..ef0187b --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/clojars.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn clojars() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a possible Clojars API token, risking unauthorized access to Clojure libraries and potential code manipulation.".to_string(), + rule_id: "clojars-api-token".to_string(), + regex: Regex::new(r"(?i)(CLOJARS_)[a-z0-9]{60}").unwrap(), + tags: vec![], + keywords: vec!["clojars".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("clojars", &format!("CLOJARS_{}", secrets::new_secret(&alpha_numeric("60")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/cloudflare.rs b/src/secret-detect/cmd/generate/config/rules/cloudflare.rs new file mode 100644 index 0000000..1dd9254 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/cloudflare.rs @@ -0,0 +1,90 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +// Test data (move to appropriate testing module) +static GLOBAL_KEYS: &[&str] = &[ + r#"cloudflare_global_api_key = "d3d1443e0adc9c24564c6c5676d679d47e2ca""#, + r#"CLOUDFLARE_GLOBAL_API_KEY: 674538c7ecac77d064958a04a83d9e9db068c"#, + r#"cloudflare: "0574b9f43978174cc2cb9a1068681225433c4""#, +]; + +static API_KEYS: &[&str] = &[ + r#"cloudflare_api_key = "Bu0rrK-lerk6y0Suqo1qSqlDDajOk61wZchCkje4""#, + r#"CLOUDFLARE_API_KEY: 5oK0U90ME14yU6CVxV90crvfqVlNH2wRKBwcLWDc"#, + r#"cloudflare: "oj9Yoyq0zmOyWmPPob1aoY5YSNNuJ0fbZSOURBlX""#, +]; + +static ORIGIN_CA_KEYS: &[&str] = &[ + r#"CLOUDFLARE_ORIGIN_CA: v1.0-aaa334dc886f30631ba0a610-0d98ef66290d7e50aac7c27b5986c99e6f3f1084c881d8ac0eae5de1d1aa0644076ff57022069b3237d19afe60ad045f207ef2b16387ee37b749441b2ae2e9ebe5b4606e846475d4a5"#, + r#"CLOUDFLARE_ORIGIN_CA: v1.0-15d20c7fccb4234ac5cdd756-d5c2630d1b606535cf9320ae7456b090e0896cec64169a92fae4e931ab0f72f111b2e4ffed5b2bb40f6fba6b2214df23b188a23693d59ce3fb0d28f7e89a2206d98271b002dac695ed"#, +]; + +static IDENTIFIERS: &[&str] = &["cloudflare"]; + +pub fn cloudflare_global_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Cloudflare Global API Key, potentially compromising cloud application deployments and operational security.".to_string(), + rule_id: "cloudflare-global-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(IDENTIFIERS, &hex("37"), true)).unwrap(), + tags: vec![], + keywords: IDENTIFIERS.iter().map(|s| s.to_string()).collect(), + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let false_positives: Vec<&str> = API_KEYS.iter().chain(ORIGIN_CA_KEYS.iter()).copied().collect(); + + validate(rule, GLOBAL_KEYS, Some(&false_positives)) +} + +pub fn cloudflare_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Cloudflare API Key, potentially compromising cloud application deployments and operational security.".to_string(), + rule_id: "cloudflare-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(IDENTIFIERS, &alpha_numeric_extended_short("40"), true)).unwrap(), + tags: vec![], + keywords: IDENTIFIERS.iter().map(|s| s.to_string()).collect(), + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let false_positives: Vec<&str> = GLOBAL_KEYS.iter().chain(ORIGIN_CA_KEYS.iter()).copied().collect(); + + validate(rule, API_KEYS, Some(&false_positives)) +} + +pub fn cloudflare_origin_ca_key() -> Rule { + let mut ca_identifiers = IDENTIFIERS.to_vec(); + ca_identifiers.push("v1.0-"); + + // Define rule + let rule = Rule { + description: "Detected a Cloudflare Origin CA Key, potentially compromising cloud application deployments and operational security.".to_string(), + rule_id: "cloudflare-origin-ca-key".to_string(), + regex: Regex::new(&generate_unique_token_regex(&format!("v1.0-{} +-{}", hex("24"), hex("146")), false)).unwrap(), + tags: vec![], + keywords: ca_identifiers.iter().map(|s| s.to_string()).collect(), + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let false_positives: Vec<&str> = GLOBAL_KEYS.iter().chain(API_KEYS.iter()).copied().collect(); + + validate(rule, ORIGIN_CA_KEYS, Some(&false_positives)) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/codecov.rs b/src/secret-detect/cmd/generate/config/rules/codecov.rs new file mode 100644 index 0000000..08d40ca --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/codecov.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn codecov_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a pattern resembling a Codecov Access Token, posing a risk of unauthorized access to code coverage reports and sensitive data.".to_string(), + rule_id: "codecov-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["codecov"], &alpha_numeric("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["codecov".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("codecov", &secrets::new_secret(&alpha_numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/coinbase.rs b/src/secret-detect/cmd/generate/config/rules/coinbase.rs new file mode 100644 index 0000000..ba0c917 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/coinbase.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn coinbase_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Coinbase Access Token, posing a risk of unauthorized access to cryptocurrency accounts and financial transactions.".to_string(), + rule_id: "coinbase-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["coinbase"], &alpha_numeric_extended_short("64"), true)).unwrap(), + tags: vec![], + keywords: vec!["coinbase".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("coinbase", &secrets::new_secret(&alpha_numeric_extended_short("64"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/confluent.rs b/src/secret-detect/cmd/generate/config/rules/confluent.rs new file mode 100644 index 0000000..d6775b8 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/confluent.rs @@ -0,0 +1,50 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn confluent_secret_key() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Confluent Secret Key, potentially risking unauthorized operations and data access within Confluent services.".to_string(), + rule_id: "confluent-secret-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["confluent"], &alpha_numeric("64"), true)).unwrap(), + tags: vec![], + keywords: vec!["confluent".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("confluent", &secrets::new_secret(&alpha_numeric("64"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn confluent_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Confluent Access Token, which could compromise access to streaming data platforms and sensitive data flow.".to_string(), + rule_id: "confluent-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["confluent"], &alpha_numeric("16"), true)).unwrap(), + tags: vec![], + keywords: vec!["confluent".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("confluent", &secrets::new_secret(&alpha_numeric("16"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/contentful.rs b/src/secret-detect/cmd/generate/config/rules/contentful.rs new file mode 100644 index 0000000..3d32d7b --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/contentful.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn contentful() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Contentful delivery API token, posing a risk to content management systems and data integrity.".to_string(), + rule_id: "contentful-delivery-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["contentful"], &alpha_numeric_extended("43"), true)).unwrap(), + tags: vec![], + keywords: vec!["contentful".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("contentful", &secrets::new_secret(&alpha_numeric("43"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/databricks.rs b/src/secret-detect/cmd/generate/config/rules/databricks.rs new file mode 100644 index 0000000..0578c05 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/databricks.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn databricks() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Databricks API token, which may compromise big data analytics platforms and sensitive data processing.".to_string(), + rule_id: "databricks-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"dapi[a-h0-9]{32}", true)).unwrap(), + tags: vec![], + keywords: vec!["dapi".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("databricks", &format!("dapi{}", secrets::new_secret(&hex("32")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/datadog.rs b/src/secret-detect/cmd/generate/config/rules/datadog.rs new file mode 100644 index 0000000..5ef0e26 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/datadog.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn datadogtoken_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Datadog Access Token, potentially risking monitoring and analytics data exposure and manipulation.".to_string(), + rule_id: "datadog-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["datadog"], &alpha_numeric("40"), true)).unwrap(), + tags: vec![], + keywords: vec!["datadog".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("datadog", &secrets::new_secret(&alpha_numeric("40"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/definednetworking.rs b/src/secret-detect/cmd/generate/config/rules/definednetworking.rs new file mode 100644 index 0000000..6a83c5a --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/definednetworking.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn defined_networking_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Defined Networking API token, which could lead to unauthorized network operations and data breaches.".to_string(), + rule_id: "defined-networking-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["dnkey"], r"dnkey-[a-z0-9=_\-]{26}-[a-z0-9=_\-]{52}", true)).unwrap(), + tags: vec![], + keywords: vec!["dnkey".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("dnkey", &format!("dnkey-{}-{}", secrets::new_secret(&alpha_numeric_extended("26")), secrets::new_secret(&alpha_numeric_extended("52")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/digitalocean.rs b/src/secret-detect/cmd/generate/config/rules/digitalocean.rs new file mode 100644 index 0000000..14a20b2 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/digitalocean.rs @@ -0,0 +1,65 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn digitalocean_pat() -> Rule { + let rule = Rule { + description: "Discovered a DigitalOcean Personal Access Token, posing a threat to cloud infrastructure security and data privacy.".to_string(), + rule_id: "digitalocean-pat".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"dop_v1_[a-f0-9]{64}", true)).unwrap(), + tags: vec![], + keywords: vec!["dop_v1_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = vec![ + generate_sample_secret("do", &format!("dop_v1_{}", secrets::new_secret(&hex("64")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn digitalocean_oauth_token() -> Rule { + let rule = Rule { + description: "Found a DigitalOcean OAuth Access Token, risking unauthorized cloud resource access and data compromise.".to_string(), + rule_id: "digitalocean-access-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"doo_v1_[a-f0-9]{64}", true)).unwrap(), + tags: vec![], + keywords: vec!["doo_v1_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = vec![ + generate_sample_secret("do", &format!("doo_v1_{}", secrets::new_secret(&hex("64")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn digitalocean_refresh_token() -> Rule { + let rule = Rule { + description: "Uncovered a DigitalOcean OAuth Refresh Token, which could allow prolonged unauthorized access and resource manipulation.".to_string(), + rule_id: "digitalocean-refresh-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"dor_v1_[a-f0-9]{64}", true)).unwrap(), + tags: vec![], + keywords: vec!["dor_v1_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = vec![ + generate_sample_secret("do", &format!("dor_v1_{}", secrets::new_secret(&hex("64")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/discord.rs b/src/secret-detect/cmd/generate/config/rules/discord.rs new file mode 100644 index 0000000..87f0740 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/discord.rs @@ -0,0 +1,73 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - numeric(length: &str) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn discord_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Discord API key, potentially compromising communication channels and user data privacy on Discord.".to_string(), + rule_id: "discord-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["discord"], &hex("64"), true)).unwrap(), + tags: vec![], + keywords: vec!["discord".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("discord", &secrets::new_secret(&hex("64"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn discord_client_id() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Discord client ID, which may lead to unauthorized integrations and data exposure in Discord applications.".to_string(), + rule_id: "discord-client-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["discord"], &numeric("18"), true)).unwrap(), + tags: vec![], + keywords: vec!["discord".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("discord", &secrets::new_secret(&numeric("18"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn discord_client_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a potential Discord client secret, risking compromised Discord bot integrations and data leaks.".to_string(), + rule_id: "discord-client-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["discord"], &alpha_numeric_extended("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["discord".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("discord", &secrets::new_secret(&numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/doppler.rs b/src/secret-detect/cmd/generate/config/rules/doppler.rs new file mode 100644 index 0000000..17ef395 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/doppler.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn doppler() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Doppler API token, posing a risk to environment and secrets management security.".to_string(), + rule_id: "doppler-api-token".to_string(), + regex: Regex::new(r"(dp\.pt\.)(?i)[a-z0-9]{43}").unwrap(), + tags: vec![], + keywords: vec!["doppler".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("doppler", &format!("dp.pt.{}", secrets::new_secret(&alpha_numeric("43")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/droneci.rs b/src/secret-detect/cmd/generate/config/rules/droneci.rs new file mode 100644 index 0000000..4747998 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/droneci.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn droneci_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Droneci Access Token, potentially compromising continuous integration and deployment workflows.".to_string(), + rule_id: "droneci-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["droneci"], &alpha_numeric("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["droneci".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("droneci", &secrets::new_secret(&alpha_numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/dropbox.rs b/src/secret-detect/cmd/generate/config/rules/dropbox.rs new file mode 100644 index 0000000..a6d7e43 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/dropbox.rs @@ -0,0 +1,63 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn drop_box_api_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Dropbox API secret, which could lead to unauthorized file access and data breaches in Dropbox storage.".to_string(), + rule_id: "dropbox-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["dropbox"], &alpha_numeric("15"), true)).unwrap(), + tags: vec![], + keywords: vec!["dropbox".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("dropbox", &secrets::new_secret(&alpha_numeric("15"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn drop_box_short_lived_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Dropbox short-lived API token, posing a risk of temporary but potentially harmful data access and manipulation.".to_string(), + rule_id: "dropbox-short-lived-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["dropbox"], r"sl\.[a-z0-9\-=_]{135}", true)).unwrap(), + tags: vec![], + keywords: vec!["dropbox".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // TODO: Implement validation for short-lived token + rule +} + +pub fn drop_box_long_lived_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Dropbox long-lived API token, risking prolonged unauthorized access to cloud storage and sensitive data.".to_string(), + rule_id: "dropbox-long-lived-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["dropbox"], r"[a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43}", true)).unwrap(), + tags: vec![], + keywords: vec!["dropbox".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // TODO: Implement validation for long-lived token + rule +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/duffel.rs b/src/secret-detect/cmd/generate/config/rules/duffel.rs new file mode 100644 index 0000000..d2a603d --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/duffel.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn duffel() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Duffel API token, which may compromise travel platform integrations and sensitive customer data.".to_string(), + rule_id: "duffel-api-token".to_string(), + regex: Regex::new(r"duffel_(test|live)_(?i)[a-z0-9_\-=]{43}").unwrap(), + tags: vec![], + keywords: vec!["duffel".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("duffel", &format!("duffel_test_{}", secrets::new_secret(&alpha_numeric_extended("43")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/dynatrace.rs b/src/secret-detect/cmd/generate/config/rules/dynatrace.rs new file mode 100644 index 0000000..5a3628f --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/dynatrace.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn dynatrace() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Dynatrace API token, potentially risking application performance monitoring and data exposure.".to_string(), + rule_id: "dynatrace-api-token".to_string(), + regex: Regex::new(r"dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}").unwrap(), + tags: vec![], + keywords: vec!["dynatrace".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("dynatrace", &format!("dt0c01.{}.{}", secrets::new_secret(&alpha_numeric("24")), secrets::new_secret(&alpha_numeric("64")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/easypost.rs b/src/secret-detect/cmd/generate/config/rules/easypost.rs new file mode 100644 index 0000000..f467354 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/easypost.rs @@ -0,0 +1,49 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn easy_post() -> Rule { + // Define rule + let rule = Rule { + description: "Identified an EasyPost API token, which could lead to unauthorized postal and shipment service access and data exposure.".to_string(), + rule_id: "easypost-api-token".to_string(), + regex: Regex::new(r"\bEZAK(?i)[a-z0-9]{54}").unwrap(), + tags: vec![], + keywords: vec!["EZAK".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("EZAK", &format!("EZAK{}", secrets::new_secret(&alpha_numeric("54")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn easy_post_test_api() -> Rule { + // Define rule + let rule = Rule { + description: "Detected an EasyPost test API token, risking exposure of test environments and potentially sensitive shipment data.".to_string(), + rule_id: "easypost-test-api-token".to_string(), + regex: Regex::new(r"\bEZTK(?i)[a-z0-9]{54}").unwrap(), + tags: vec![], + keywords: vec!["EZTK".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("EZTK", &format!("EZTK{}", secrets::new_secret(&alpha_numeric("54")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/etsy.rs b/src/secret-detect/cmd/generate/config/rules/etsy.rs new file mode 100644 index 0000000..f89da0c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/etsy.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn etsy_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found an Etsy Access Token, potentially compromising Etsy shop management and customer data.".to_string(), + rule_id: "etsy-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["etsy"], &alpha_numeric("24"), true)).unwrap(), + tags: vec![], + keywords: vec!["etsy".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("etsy", &secrets::new_secret(&alpha_numeric("24"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/facebook.rs b/src/secret-detect/cmd/generate/config/rules/facebook.rs new file mode 100644 index 0000000..5d3b56a --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/facebook.rs @@ -0,0 +1,79 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - alpha_numeric(length: &str) -> String +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn facebook_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Facebook Application secret, posing a risk of unauthorized access to Facebook accounts and personal data exposure.".to_string(), + rule_id: "facebook-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["facebook"], &hex("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["facebook".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("facebook", &secrets::new_secret(&hex("32"))), + r#"facebook_app_secret = "6dca6432e45d933e13650d1882bd5e69""#, + r#"facebook_client_access_token: 26f5fd13099f2c1331aafb86f6489692"#, + ]; + + validate(rule, &test_positives, None) +} + +pub fn facebook_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Facebook Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure.".to_string(), + rule_id: "facebook-access-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"\d{15,16}\|[0-9a-z\-_]{27}", true)).unwrap(), + tags: vec![], + keywords: vec![], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"{"access_token":"911602140448729|AY-lRJZq9BoDLobvAiP25L7RcMg","token_type":"bearer"}"#, + r#"1308742762612587|rhoK1cbv0DOU_RTX_87O4MkX7AI"#, + r#"1477036645700765|wRPf2v3mt2JfMqCLK8n7oltrEmc"#, + ]; + + validate(rule, &test_positives, None) +} + +pub fn facebook_page_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Facebook Page Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure.".to_string(), + rule_id: "facebook-page-access-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"EAA[MC]".to_string() + &alpha_numeric("20,"), true)).unwrap(), + tags: vec![], + keywords: vec!["EAAM".to_string(), "EAAC".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"EAAM9GOnCB9kBO2frzOAWGN2zMnZClQshlWydZCrBNdodesbwimx1mfVJgqZBP5RSpMfUzWhtjTTXHG5I1UlvlwRZCgjm3ZBVGeTYiqAAoxyED6HaUdhpGVNoPUwAuAWWFsi9OvyYBQt22DGLqMIgD7VktuCTTZCWKasz81Q822FPhMTB9VFFyClNzQ0NLZClt9zxpsMMrUZCo1VU1rL3CKavir5QTfBjfCEzHNlWAUDUV2YZD"#, // gitleaks:allow + r#"EAAM9GOnCB9kBO2zXpAtRBmCrsPPjdA3KeBl4tqsEpcYd09cpjm9MZCBIklZBjIQBKGIJgFwm8IE17G5pipsfRBRBEHMWxvJsL7iHLUouiprxKRQfAagw8BEEDucceqxTiDhVW2IZAQNNbf0d1JhcapAGntx5S1Csm4j0GgZB3DuUfI2HJ9aViTtdfH2vjBy0wtpXm2iamevohGfoF4NgyRHusDLjqy91uYMkfrkc"#, // gitleaks:allow + r#"- name: FACEBOOK_TOKEN + value: "EAACEdEose0cBA1bad3afsf2aew""#, + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/fastly.rs b/src/secret-detect/cmd/generate/config/rules/fastly.rs new file mode 100644 index 0000000..76b83b3 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/fastly.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn fastly_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Fastly API key, which may compromise CDN and edge cloud services, leading to content delivery and security issues.".to_string(), + rule_id: "fastly-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["fastly"], &alpha_numeric_extended("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["fastly".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("fastly", &secrets::new_secret(&alpha_numeric_extended("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/finicity.rs b/src/secret-detect/cmd/generate/config/rules/finicity.rs new file mode 100644 index 0000000..aef4f0a --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/finicity.rs @@ -0,0 +1,51 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn finicity_client_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Finicity Client Secret, which could lead to compromised financial service integrations and data breaches.".to_string(), + rule_id: "finicity-client-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["finicity"], &alpha_numeric("20"), true)).unwrap(), + tags: vec![], + keywords: vec!["finicity".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("finicity", &secrets::new_secret(&alpha_numeric("20"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn finicity_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Finicity API token, potentially risking financial data access and unauthorized financial operations.".to_string(), + rule_id: "finicity-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["finicity"], &hex("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["finicity".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("finicity", &secrets::new_secret(&hex("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/finnhub.rs b/src/secret-detect/cmd/generate/config/rules/finnhub.rs new file mode 100644 index 0000000..5e15670 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/finnhub.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn finnhub_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Finnhub Access Token, risking unauthorized access to financial market data and analytics.".to_string(), + rule_id: "finnhub-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["finnhub"], &alpha_numeric("20"), true)).unwrap(), + tags: vec![], + keywords: vec!["finnhub".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("finnhub", &secrets::new_secret(&alpha_numeric("20"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/flickr.rs b/src/secret-detect/cmd/generate/config/rules/flickr.rs new file mode 100644 index 0000000..dc50c3e --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/flickr.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn flickr_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Flickr Access Token, posing a risk of unauthorized photo management and potential data leakage.".to_string(), + rule_id: "flickr-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["flickr"], &alpha_numeric("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["flickr".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("flickr", &secrets::new_secret(&alpha_numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/flutterwave.rs b/src/secret-detect/cmd/generate/config/rules/flutterwave.rs new file mode 100644 index 0000000..edf2211 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/flutterwave.rs @@ -0,0 +1,70 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn flutterwave_public_key() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Finicity Public Key, potentially exposing public cryptographic operations and integrations.".to_string(), + rule_id: "flutterwave-public-key".to_string(), + regex: Regex::new(r"FLWPUBK_TEST-(?i)[a-h0-9]{32}-X").unwrap(), + tags: vec![], + keywords: vec!["FLWPUBK_TEST".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("flutterwavePubKey", &format!("FLWPUBK_TEST-{}-X", secrets::new_secret(&hex("32")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn flutterwave_secret_key() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Flutterwave Secret Key, risking unauthorized financial transactions and data breaches.".to_string(), + rule_id: "flutterwave-secret-key".to_string(), + regex: Regex::new(r"FLWSECK_TEST-(?i)[a-h0-9]{32}-X").unwrap(), + tags: vec![], + keywords: vec!["FLWSECK_TEST".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("flutterwavePubKey", &format!("FLWSECK_TEST-{}-X", secrets::new_secret(&hex("32")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn flutterwave_enc_key() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Flutterwave Encryption Key, which may compromise payment processing and sensitive financial information.".to_string(), + rule_id: "flutterwave-encryption-key".to_string(), + regex: Regex::new(r"FLWSECK_TEST-(?i)[a-h0-9]{12}").unwrap(), + tags: vec![], + keywords: vec!["FLWSECK_TEST".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("flutterwavePubKey", &format!("FLWSECK_TEST-{}", secrets::new_secret(&hex("12")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/frameio.rs b/src/secret-detect/cmd/generate/config/rules/frameio.rs new file mode 100644 index 0000000..d3222fe --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/frameio.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn frame_io() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Frame.io API token, potentially compromising video collaboration and project management.".to_string(), + rule_id: "frameio-api-token".to_string(), + regex: Regex::new(r"fio-u-(?i)[a-z0-9\-_=]{64}").unwrap(), + tags: vec![], + keywords: vec!["fio-u-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("frameio", &format!("fio-u-{}", secrets::new_secret(&alpha_numeric_extended("64")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/freshbooks.rs b/src/secret-detect/cmd/generate/config/rules/freshbooks.rs new file mode 100644 index 0000000..9e8b94c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/freshbooks.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn freshbooks_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Freshbooks Access Token, posing a risk to accounting software access and sensitive financial data exposure.".to_string(), + rule_id: "freshbooks-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["freshbooks"], &alpha_numeric("64"), true)).unwrap(), + tags: vec![], + keywords: vec!["freshbooks".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("freshbooks", &secrets::new_secret(&alpha_numeric("64"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/gcp.rs b/src/secret-detect/cmd/generate/config/rules/gcp.rs new file mode 100644 index 0000000..9af0a27 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/gcp.rs @@ -0,0 +1,49 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn gcp_service_account() -> Rule { + // Define rule + let rule = Rule { + description: "Google (GCP) Service-account".to_string(), + rule_id: "gcp-service-account".to_string(), + regex: Regex::new(r#""type": "service_account""#).unwrap(), + tags: vec![], + keywords: vec![r#""type": "service_account""#.to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#""type": "service_account""#, + ]; + + validate(rule, &test_positives, None) +} + +pub fn gcp_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.".to_string(), + rule_id: "gcp-api-key".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"AIza[0-9A-Za-z\\-_]{35}", true)).unwrap(), + tags: vec![], + keywords: vec!["AIza".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("gcp", &secrets::new_secret(r"AIza[0-9A-Za-z\\-_]{35}")), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/generic.rs b/src/secret-detect/cmd/generate/config/rules/generic.rs new file mode 100644 index 0000000..390403a --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/generic.rs @@ -0,0 +1,44 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn generic_credential() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.".to_string(), + rule_id: "generic-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["key", "api", "token", "secret", "client", "passwd", "password", "auth", "access"], r"[0-9a-z\-_.=]{10,150}", true)).unwrap(), + tags: vec![], + keywords: vec!["key".to_string(), "api".to_string(), "token".to_string(), "secret".to_string(), "client".to_string(), "passwd".to_string(), "password".to_string(), "auth".to_string(), "access".to_string()], + allowlist: Allowlist { + stop_words: DefaultStopWords, // Assuming DefaultStopWords is defined elsewhere + ..Allowlist::default() + }, + entropy: Some(3.5), + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("generic", "CLOJARS_34bf0e88955ff5a1c328d6a7491acc4f48e865a7b8dd4d70a70749037443"), + generate_sample_secret("generic", "Zf3D0LXCM3EIMbgJpUNnkRtOfOueHznB"), + + //TODO: Fix Regex + /* + r#""client_id" : "0afae57f3ccfd9d7f5767067bc48b30f719e271ba470488056e37ab35d4b6506""#, + r#""client_secret" : "6da89121079f83b2eb6acccf8219ea982c3d79bccc3e9c6a85856480661f8fde",", + ]; + let false_positives = vec![ + r#"client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.client-vpn-endpoint.id"#, + r#"password combination. + +R5: Regulatory--21"#, */ + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/github.rs b/src/secret-detect/cmd/generate/config/rules/github.rs new file mode 100644 index 0000000..9d1ca95 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/github.rs @@ -0,0 +1,114 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// Assuming the following functions exist (based on the Golang code): +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn github_pat() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a GitHub Personal Access Token, potentially leading to unauthorized repository access and sensitive content exposure.".to_string(), + rule_id: "github-pat".to_string(), + regex: Regex::new(r"ghp_[0-9a-zA-Z]{36}").unwrap(), + tags: vec![], + keywords: vec!["ghp_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("github", &format!("ghp_{}", secrets::new_secret(&alpha_numeric("36")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn github_fine_grained_pat() -> Rule { + // Define rule + let rule = Rule { + description: "Found a GitHub Fine-Grained Personal Access Token, risking unauthorized repository access and code manipulation.".to_string(), + rule_id: "github-fine-grained-pat".to_string(), + regex: Regex::new(r"github_pat_[0-9a-zA-Z_]{82}").unwrap(), + tags: vec![], + keywords: vec!["github_pat_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("github", &format!("github_pat_{}", secrets::new_secret(&alpha_numeric("82")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn github_oauth() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a GitHub OAuth Access Token, posing a risk of compromised GitHub account integrations and data leaks.".to_string(), + rule_id: "github-oauth".to_string(), + regex: Regex::new(r"gho_[0-9a-zA-Z]{36}").unwrap(), + tags: vec![], + keywords: vec!["gho_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("github", &format!("gho_{}", secrets::new_secret(&alpha_numeric("36")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn github_app() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a GitHub App Token, which may compromise GitHub application integrations and source code security.".to_string(), + rule_id: "github-app-token".to_string(), + regex: Regex::new(r"(ghu|ghs)_[0-9a-zA-Z]{36}").unwrap(), + tags: vec![], + keywords: vec!["ghu_".to_string(), "ghs_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("github", &format!("ghu_{}", secrets::new_secret(&alpha_numeric("36")))), + generate_sample_secret("github", &format!("ghs_{}", secrets::new_secret(&alpha_numeric("36")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn github_refresh() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a GitHub Refresh Token, which could allow prolonged unauthorized access to GitHub services.".to_string(), + rule_id: "github-refresh-token".to_string(), + regex: Regex::new(r"ghr_[0-9a-zA-Z]{36}").unwrap(), + tags: vec![], + keywords: vec!["ghr_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("github", &format!("ghr_{}", secrets::new_secret(&alpha_numeric("36")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/gitlab.rs b/src/secret-detect/cmd/generate/config/rules/gitlab.rs new file mode 100644 index 0000000..9206a1e --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/gitlab.rs @@ -0,0 +1,71 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn gitlab_pat() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a GitLab Personal Access Token, risking unauthorized access to GitLab repositories and codebase exposure.".to_string(), + rule_id: "gitlab-pat".to_string(), + regex: Regex::new(r"glpat-[0-9a-zA-Z\-_]{20}").unwrap(), + tags: vec![], + keywords: vec!["glpat-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("gitlab", &format!("glpat-{}", secrets::new_secret(&alpha_numeric("20")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn gitlab_pipeline_trigger_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a GitLab Pipeline Trigger Token, potentially compromising continuous integration workflows and project security.".to_string(), + rule_id: "gitlab-ptt".to_string(), + regex: Regex::new(r"glptt-[0-9a-f]{40}").unwrap(), + tags: vec![], + keywords: vec!["glptt-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("gitlab", &format!("glptt-{}", secrets::new_secret(&hex("40")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn gitlab_runner_registration_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a GitLab Runner Registration Token, posing a risk to CI/CD pipeline integrity and unauthorized access.".to_string(), + rule_id: "gitlab-rrt".to_string(), + regex: Regex::new(r"GR1348941[0-9a-zA-Z\-_]{20}").unwrap(), + tags: vec![], + keywords: vec!["GR1348941".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("gitlab", &format!("GR1348941{}", secrets::new_secret(&alpha_numeric("20")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/gitter.rs b/src/secret-detect/cmd/generate/config/rules/gitter.rs new file mode 100644 index 0000000..dc144e2 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/gitter.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn gitter_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Gitter Access Token, which may lead to unauthorized access to chat and communication services.".to_string(), + rule_id: "gitter-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["gitter"], &alpha_numeric_extended_short("40"), true)).unwrap(), + tags: vec![], + keywords: vec!["gitter".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("gitter", &secrets::new_secret(&alpha_numeric_extended_short("40"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/gocardless.rs b/src/secret-detect/cmd/generate/config/rules/gocardless.rs new file mode 100644 index 0000000..88c5442 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/gocardless.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn gocardless() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a GoCardless API token, potentially risking unauthorized direct debit payment operations and financial data exposure.".to_string(), + rule_id: "gocardless-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["gocardless"], r"live_(?i)[a-z0-9\-_=]{40}", true)).unwrap(), + tags: vec![], + keywords: vec!["live_".to_string(), "gocardless".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("gocardless", &format!("live_{}", secrets::new_secret(&alpha_numeric_extended("40")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/grafana.rs b/src/secret-detect/cmd/generate/config/rules/grafana.rs new file mode 100644 index 0000000..2dcfb7c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/grafana.rs @@ -0,0 +1,72 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn grafana_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Grafana API key, which could compromise monitoring dashboards and sensitive data analytics.".to_string(), + rule_id: "grafana-api-key".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"eyJrIjoi[A-Za-z0-9]{70,400}={0,2}", true)).unwrap(), + tags: vec![], + keywords: vec!["eyJrIjoi".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("grafana-api-key", &format!("eyJrIjoi{}", secrets::new_secret(&alpha_numeric("70")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn grafana_cloud_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Grafana cloud API token, risking unauthorized access to cloud-based monitoring services and data exposure.".to_string(), + rule_id: "grafana-cloud-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"glc_[A-Za-z0-9+/]{32,400}={0,2}", true)).unwrap(), + tags: vec![], + keywords: vec!["glc_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("grafana-cloud-api-token", &format!("glc_{}", secrets::new_secret(&alpha_numeric("32")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn grafana_service_account_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Grafana service account token, posing a risk of compromised monitoring services and data integrity.".to_string(), + rule_id: "grafana-service-account-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}", true)).unwrap(), + tags: vec![], + keywords: vec!["glsa_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("grafana-service-account-token", &format!("glsa_{}_{}", secrets::new_secret(&alpha_numeric("32")), secrets::new_secret(&hex("8")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/hashicorp.rs b/src/secret-detect/cmd/generate/config/rules/hashicorp.rs new file mode 100644 index 0000000..79da0a5 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/hashicorp.rs @@ -0,0 +1,60 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - hex(length: &str) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn hashicorp() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a HashiCorp Terraform user/org API token, which may lead to unauthorized infrastructure management and security breaches.".to_string(), + rule_id: "hashicorp-tf-api-token".to_string(), + regex: Regex::new(r"(?i)[a-z0-9]{14}\.atlasv1\.[a-z0-9\-_=]{60,70}").unwrap(), + tags: vec![], + keywords: vec!["atlasv1".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("hashicorpToken", &format!("{}.atlasv1.{}", secrets::new_secret(&hex("14")), secrets::new_secret(&alpha_numeric_extended("60,70")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn hashicorp_field() -> Rule { + let keywords = vec!["administrator_login_password".to_string(), "password".to_string()]; + + // Define rule + let rule = Rule { + description: "Identified a HashiCorp Terraform password field, risking unauthorized infrastructure configuration and security breaches.".to_string(), + rule_id: "hashicorp-tf-password".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&keywords, &format!(r#""{}""#, alpha_numeric_extended("8,20")), true)).unwrap(), + tags: vec![], + keywords: keywords.clone(), + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + // Example from: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/sql_server.html + &format!("administrator_login_password = {}", r#""thisIsDog11""#), + // https://registry.terraform.io/providers/petoju/mysql/latest/docs + &format!("password = {}", r#""rootpasswd""#), + ]; + let false_positives = vec![ + "administrator_login_password = var.db_password", + r#"password = "${aws_db_instance.default.password}""#, + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/heroku.rs b/src/secret-detect/cmd/generate/config/rules/heroku.rs new file mode 100644 index 0000000..e3e4bea --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/heroku.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex8_4_4_4_12() -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn heroku() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security.".to_string(), + rule_id: "heroku-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["heroku"], &hex8_4_4_4_12(), true)).unwrap(), + tags: vec![], + keywords: vec!["heroku".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"const HEROKU_KEY = "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"heroku_api_key = "832d2129-a846-4e27-99f4-7004b6ad53ef""#, + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/hubspot.rs b/src/secret-detect/cmd/generate/config/rules/hubspot.rs new file mode 100644 index 0000000..fe5d3f2 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/hubspot.rs @@ -0,0 +1,27 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn hubspot() -> Rule { + // Define rule + let rule = Rule { + description: "Found a HubSpot API Token, posing a risk to CRM data integrity and unauthorized marketing operations.".to_string(), + rule_id: "hubspot-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["hubspot"], r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", true)).unwrap(), + tags: vec![], + keywords: vec!["hubspot".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"const hubspotKey = "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/huggingface.rs b/src/secret-detect/cmd/generate/config/rules/huggingface.rs new file mode 100644 index 0000000..3574755 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/huggingface.rs @@ -0,0 +1,106 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn hugging_face_access_token() -> Rule { + let rule = Rule { + description: "Discovered a Hugging Face Access token, which could lead to unauthorized access to AI models and sensitive data.".to_string(), + rule_id: "huggingface-access-token".to_string(), + regex: Regex::new(r#"(?:^|[\'"` >=:])(hf_[a-zA-Z]{34})(?:$|[\'"` <])"#).unwrap(), + tags: vec![], + keywords: vec!["hf_".to_string()], + allowlist: Allowlist::default(), + entropy: Some(1.0), + secret_group: None, + }; + + let test_positives = vec![ + r#"huggingface-cli login --token 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"#, + r#"huggingface-cli login --token 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"#, + r#"huggingface-cli login --token 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"#, + r#"huggingface-cli login --token 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"#, + r#"huggingface-cli login --token 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"#, + r#"huggingface-cli login --token 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"#, + r#"huggingface-cli login --token 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"#, + r#"huggingface-cli login --token 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"#, + r#"-H "Authorization: Bearer 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""#, + r#"DEV=1 HF_TOKEN=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 poetry run python app.py"#, + r#"use_auth_token='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')"#, + r#"CI_HUB_USER_TOKEN = "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""#, + r#"- Change line 5 and add your Hugging Face token, that is, instead of 'hf_token = "ADD_YOUR_HUGGING_FACE_TOKEN_HERE"', you will need to change it to something like'hf_token = "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"'"#, + r#"" hf_token = \"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\"\n","#, + r#"# Not critical, only usable on the sandboxed CI instance. + TOKEN = "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""#, + r#" parser.add_argument("--hf_token", type=str, default='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', help="Hugging Face Access Token to access PyAnnote gated models")"#, + ]; + let false_positives = vec![ + r#"- (id)hf_requiredCharacteristicTypesForDisplayMetadata;"#, + r#"amazon.de#@#div[data-cel-widget="desktop-rhf_SponsoredProductsRemoteRHFSearchEXPSubsK2ClickPagination"]"#, + r#" _kHMSymptomhf_generatedByHomeAppForDebuggingPurposesKey,"#, + r#" #define OSCHF_DebugGetExpectedAverageCrystalAmplitude NOROM_OSCHF_DebugGetExpectedAverageCrystalAmplitude"#, + r#" M_UINT (ServingCellPriorityParametersDescription_t, H_PRIO, 2, &hf_servingcellpriorityparametersdescription_h_prio),"#, + r#"+HWI-ST565_0092:4:1101:5508:5860#ACTTGA/1 + bb_eeeeegfgffhiiiiiiiiiiihiiiiicgafhf_eefghihhiiiifhifhhdhifhiiiihifdgdhggf\bbceceedbcd + @HWI-ST565_0092:4:1101:7621:5770#ACTTGA/1"#, + r#"y{}x|~|} {~}}~| ~}||�~|� {��| {} {|~z{}{{|{||{|}|{}{~|y}vjoePbUBJ7&;" ; < ; : ; ?!!; < 7%$IACa_ecghbfbaebejhahfbhf_ddbficghbgfbhhcghdghfhigiifhhehhdggcgfchf_fgcei^[[.40&54"56 66 6"#, + r#" change_dir(cwd) + subdirs = glob.glob('HF_CAASIMULIAComputeServicesBuildTime.HF*.Linux64') + if len(subdirs) == 1:"#, + r#" os.environ.get("HF_AUTH_TOKEN", + "hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"),"#, + r#"# HuggingFace API Token https://huggingface.co/settings/tokens + HUGGINGFACE_API_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,"#, + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn hugging_face_organization_api_token() -> Rule { + let rule = Rule { + description: "Uncovered a Hugging Face Organization API token, potentially compromising AI organization accounts and associated data.".to_string(), + rule_id: "huggingface-organization-api-token".to_string(), + regex: Regex::new(r#"(?:^|[\'"` >=:\(,)])(api_org_[a-zA-Z]{34})(?:$|[\'"` <\),])"#).unwrap(), + tags: vec![], + keywords: vec!["api_org_".to_string()], + allowlist: Allowlist::default(), + entropy: Some(2.0), + secret_group: None, + }; + + let test_positives = vec![ + r#"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"#, + r#"`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`"#, + r#"\'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'\"#, + r#"\"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\""#, + r#",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,"#, + r#"(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)"#, + r#"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"#, + r#"def test_private_space(self): + hf_token = "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" # Intentionally revealing this key for testing purposes + io = gr.load("#, + r#"hf_token = "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" # Intentionally revealing this key for testing purposes"#, + r#""news_train_dataset = datasets.load_dataset('nlpHakdang/aihub-news30k', data_files = "train_news_text.csv", use_auth_token='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')\n""#, + r#"os.environ['HUGGINGFACEHUB_API_TOKEN'] = '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'"#, + &format!("api_org_{}", secrets::new_secret(r"[a-zA-Z]{34}")), + ]; + let false_positives = vec![ + r#"public static final String API_ORG_EXIST = "APIOrganizationExist";"#, + r#"const api_org_controller = require('../../controllers/api/index').organizations;"#, + r#"API_ORG_CREATE("https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=ACCESS_TOKEN"),"#, + r#"def test_internal_api_org_inclusion_with_href(api_name, href, expected, monkeypatch, called_with): + monkeypatch.setattr("requests.sessions.Session.request", called_with)"#, + r#" def _api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a(self, method, url, body, headers): + body = self.fixtures.load("api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK]"#, + r#"

You should see a token hf_xxxxx (old tokens are api_XXXXXXXX or api_org_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX).

"#, + r#" From Hugging Face docs: + You should see a token hf_xxxxx (old tokens are api_XXXXXXXX or api_org_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx). + If you do not submit your API token when sending requests to the API, you will not be able to run inference on your private models."#, + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/infracost.rs b/src/secret-detect/cmd/generate/config/rules/infracost.rs new file mode 100644 index 0000000..7fb86d9 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/infracost.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn infracost_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected an Infracost API Token, risking unauthorized access to cloud cost estimation tools and financial data.".to_string(), + rule_id: "infracost-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"ico-[a-zA-Z0-9]{32}", true)).unwrap(), + tags: vec![], + keywords: vec!["ico-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("ico", &format!("ico-{}", secrets::new_secret(r"[A-Za-z0-9]{32}"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/intercom.rs b/src/secret-detect/cmd/generate/config/rules/intercom.rs new file mode 100644 index 0000000..54e5ee3 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/intercom.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn intercom() -> Rule { + // Define rule + let rule = Rule { + description: "Identified an Intercom API Token, which could compromise customer communication channels and data privacy.".to_string(), + rule_id: "intercom-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["intercom"], &alpha_numeric_extended("60"), true)).unwrap(), + tags: vec![], + keywords: vec!["intercom".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("intercom", &secrets::new_secret(&alpha_numeric_extended("60"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/jfrog.rs b/src/secret-detect/cmd/generate/config/rules/jfrog.rs new file mode 100644 index 0000000..284a02c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/jfrog.rs @@ -0,0 +1,57 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn jfrog_api_key() -> Rule { + let keywords = vec!["jfrog".to_string(), "artifactory".to_string(), "bintray".to_string(), "xray".to_string()]; + + // Define rule + let rule = Rule { + description: "Found a JFrog API Key, posing a risk of unauthorized access to software artifact repositories and build pipelines.".to_string(), + rule_id: "jfrog-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&keywords, &alpha_numeric("73"), true)).unwrap(), + tags: vec![], + keywords, + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + format!("--set imagePullSecretJfrog.password={}", secrets::new_secret(&alpha_numeric("73"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn jfrog_identity_token() -> Rule { + let keywords = vec!["jfrog".to_string(), "artifactory".to_string(), "bintray".to_string(), "xray".to_string()]; + + // Define rule + let rule = Rule { + description: "Discovered a JFrog Identity Token, potentially compromising access to JFrog services and sensitive software artifacts.".to_string(), + rule_id: "jfrog-identity-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&keywords, &alpha_numeric("64"), true)).unwrap(), + tags: vec![], + keywords: keywords.clone(), + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("jfrog", &secrets::new_secret(&alpha_numeric("64"))), + generate_sample_secret("artifactory", &secrets::new_secret(&alpha_numeric("64"))), + generate_sample_secret("bintray", &secrets::new_secret(&alpha_numeric("64"))), + generate_sample_secret("xray", &secrets::new_secret(&alpha_numeric("64"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/jwt.rs b/src/secret-detect/cmd/generate/config/rules/jwt.rs new file mode 100644 index 0000000..358a46c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/jwt.rs @@ -0,0 +1,183 @@ +use regex::Regex; +use base64::{Engine as _, engine::{general_purpose, url_safe}}; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn jwt() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.".to_string(), + rule_id: "jwt".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?", false)).unwrap(), + tags: vec![], + keywords: vec!["ey".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"eyJhbGciOieeeiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic3ViZSI6IjEyMzQ1Njc4OTAiLCJuYW1lZWEiOiJKb2huIERvZSIsInN1ZmV3YWZiIjoiMTIzNDU2Nzg5MCIsIm5hbWVmZWF3ZnciOiJKb2huIERvZSIsIm5hbWVhZmV3ZmEiOiJKb2huIERvZSIsInN1ZndhZndlYWIiOiIxMjM0NTY3ODkwIiwibmFtZWZ3YWYiOiJKb2huIERvZSIsInN1YmZ3YWYiOiIxMjM0NTY3ODkwIiwibmFtZndhZSI6IkpvaG4gRG9lIiwiaWZ3YWZhYXQiOjE1MTYyMzkwMjJ9.a_5icKBDo-8EjUlrfvz2k2k-FYaindQ0DEYNrlsnRG0=="#, + r#"JWT := eyJhbGciOieeeiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic3ViZSI6IjEyMzQ1Njc4OTAiLCJuYW1lZWEiOiJKb2huIERvZSIsInN1ZmV3YWZiIjoiMTIzNDU2Nzg5MCIsIm5hbWVmZWF3ZnciOiJKb2huIERvZSIsIm5hbWVhZmV3ZmEiOiJKb2huIERvZSIsInN1ZndhZndlYWIiOiIxMjM0NTY3ODkwIiwibmFtZWZ3YWYiOiJKb2huIERvZSIsInN1YmZ3YWYiOiIxMjM0NTY3ODkwIiwibmFtZndhZSI6IkpvaG4gRG9lIiwiaWZ3YWZhYXQiOjE1MTYyMzkwMjJ9.a_5icKBDo-8EjUlrfvz2k2k-FYaindQ0DEYNrlsnRG0"#, + r#""access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJRMzFDVlMxUFNDSjRPVEsyWVZFTSIsImF0X2hhc2giOiI4amItZFE2OXRtZEVueUZaMUttNWhnIiwiYXVkIjoiZXhhbXBsZS1hcHAiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6IjE1OTQ2MDAxODIiLCJpYXQiOjE1OTQ1ODkzODQsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTU1Ni9kZXgiLCJuYW1lIjoiYWRtaW4iLCJzdWIiOiJDaVF3T0dFNE5qZzBZaTFrWWpnNExUUmlOek10T1RCaE9TMHpZMlF4TmpZeFpqVTBOallTQld4dlkyRnMifQ.nrbzIJz99Om7TvJ04jnSTmhvlM7aR9hMM1Aqjp2ONJ1UKYCvegBLrTu6cYR968_OpmnAGJ8vkd7sIjUjtR4zbw""#, + r#"https://dai2-playlistserver.aws.syncbak.com/cpl/20980038/dai2v5/1.0/7b2264657669636554797065223a387d/master.m3u8?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkdyYXkyMDE2MDgyOSJ9.eyJtaWQiOiIyMDk4MDAzOCIsImNpZCI6MjE5MDMsInNpZCI6MTU4LCJtZDUiOiIwN2QxMmRjNjAwOTM2MGI0MmY3NjNkNTRiMWIwZjI1NCIsImlhdCI6MTY2MDkxMzU2MCwiZXhwIjoxNjkyNDQ5NTYwLCJpc3MiOiJTeW5jYmFrIChURykifQ.JrWVgwzIn_RcNuWhkzIjr1i4qjXU1v4n0KFrSzoTQvQ"#, + r#""SessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI2TjJCQUxYN0VMTzgyN0RYUzNHSyIsImFjciI6IjAiLCJhdWQiOiJhY2NvdW50IiwiYXV0aF90aW1lIjoxNTY5OTEwNTUyLCJhenAiOiJhY2NvdW50IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJleHAiOjE1Njk5MTQ1NTQsImlhdCI6MTU2OTkxMDk1NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL2F1dGgvcmVhbG1zL2RlbW8iLCJqdGkiOiJkOTk4YTBlZS01NDk2LTQ4OWYtYWJlMi00ZWE5MjJiZDlhYWYiLCJuYmYiOjAsInBvbGljeSI6InJlYWR3cml0ZSIsInByZWZlcnJlZF91c2VybmFtZSI6Im5ld3VzZXIxIiwic2Vzc2lvbl9zdGF0ZSI6IjJiYTAyYTI2LWE5MTUtNDUxNC04M2M1LWE0YjgwYjc4ZTgxNyIsInN1YiI6IjY4ZmMzODVhLTA5MjItNGQyMS04N2U5LTZkZTdhYjA3Njc2NSIsInR5cCI6IklEIn0._UG_-ZHgwdRnsp0gFdwChb7VlbPs-Gr_RNUz9EV7TggCD59qjCFAKjNrVHfOSVkKvYEMe0PvwfRKjnJl3A_mBA""#, + r#"2020/11/04 21:08:40 Access Token: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYTAwYzI3ZDEtYjVhYS00NjU0LWFmMTYtYjExNzNkZTY1NjI5Iiwicm9sZXMiOlsiYWRtaW4iXSwiaWF0IjoxNjA0NTE2OTIwLCJleHAiOjE2MDQ1MTc4MjAsImp0aSI6IjYzNmVmMDc0LTE2MzktNGJhZi1hNGNiLTQ4ZDM4NGMxMzliYSIsImlzcyI6Im15YXBwIn0.T9B0zG0AHShO5JfQgrMQBlToH33KHgp8nLMPFpN6QmM""#, + r#""idToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik56azVNREl5TVRnNFJqWTBORGswT0VJM1JrRXpORGN4UmtVMU1FWXdNemczT1VKQlFqRTBNZyJ9.eyJuaWNrbmFtZSI6InRlc3QtaW50ZXJhY3RpdmUtY2xpIiwibmFtZSI6IlRlc3RpbmcgSW50ZXJhY3RpdmUgQ2xpIiwicGljdHVyZSI6Imh0dHBzOi8vaW50ZXJhdGNpdmUuY2xpL3Rlc3RpbmcucG5nIiwidXBkYXRlZF9hdCI6IjIwMTktMDktMTZUMTU6MTg6NDMuOTk5WiIsImVtYWlsIjoidGVzdGluZ0BpbnRlcmFjdGl2ZS5jbGkiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXJsZXNzaW5jLmF1dGgwLmNvbS8iLCJzdWIiOiJ0ZXN0LWludGVyYWN0aXZlLWNsaSIsImF1ZCI6IlhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYIiwiaWF0IjoxNTYwMDAwMDAwLCJleHAiOjMwMDAwMDAwMDB9.GcNQtWSxv9CHTABw-HIjYSvRxTEapDUDqIIWRGmz01XmShQxRGOHRuUg1NKU4w9MpOlB6txHKs8UWd2eZkzw_Z4QmIuLyAVhVklpWP2-xeysPLUyqVTgqAg8kgIUAwdKjmrdpQqHhGd-Q1BIX62-E-qKKx8prmADSw_hgmuvlMuSCa1ajCnfyUXycQxDmbFrvjd24lJER0FSpB2nWWW3KxZ_UBX-TuVmiEtRXg9GYeSv6oIU78PrIhYgJ0QjERRF1yAYamIXNRs-KZ7Z4YiFNC4uKzFH1524pZkS4Q0-pweIvBrrsjekz-vEYcbaVG1zAxDu_yNrYPk5phCy8MHTrQ""#, + r#"TokenIssuer1WithAzp = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRUX3c5TFJOclk3d0phbEdzVFlTdDdydXRaaTg2R3Z5YzBFS1I0Q2FRQXciLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJiYXIiLCJleHAiOjQ3MzQxMjU0NTMsImlhdCI6MTU4MDUyNTQ1MywiaXNzIjoidGVzdC1pc3N1ZXItMUBpc3Rpby5pbyIsInN1YiI6InN1Yi0xIn0.SO4qjRJPYItkpGGpCDfEhaUfdthO8L9b_aawao4dJKyqqXN0uYdsJau_JZzyPQ1emAmJP7VyjwELrlszA6xV65na_O-eny23iwhEoroChQMpcr9DWqSUBUfpbHSPFAjUv38SUbQfLgar0HrMxQlTAzB0vyzn2g6-cukP469ZlOUmzvi9b4UpolTLp_WPgEHKjZw8CL56CcuJqBIKgfn0M7ta2bY_qx-UrsEW0CqnXol7vhXuDAfMeWZYKuDP8qc2VH1T6wpO2JnZ0EaNDuZfQLOWFYKsFGlaYcus9j462AfJQBSFQTbkIjkvKMK6aI_rMEesAnJr2eei1UYi14JYiQ""#, + r#"eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjM1MzczOTExMDQsImdyb3VwcyI6WyJncm91cDEiLCJncm91cDIiXSwiaWF0IjoxNTM3MzkxMTA0LCJpc3MiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyIsInNjb3BlIjpbInNjb3BlMSIsInNjb3BlMiJdLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.EdJnEZSH6X8hcyEii7c8H5lnhgjB5dwo07M5oheC8Xz8mOllyg--AHCFWHybM48reunF--oGaG6IXVngCEpVF0_P5DwsUoBgpPmK1JOaKN6_pe9sh0ZwTtdgK_RP01PuI7kUdbOTlkuUi2AO-qUyOm7Art2POzo36DLQlUXv8Ad7NBOqfQaKjE9ndaPWT7aexUsBHxmgiGbz1SyLH879f7uHYPbPKlpHU6P9S-DaKnGLaEchnoKnov7ajhrEhGXAQRukhDPKUHO9L30oPIr5IJllEQfHYtt6IZvlNUGeLUcif3wpry1R5tBXRicx2sXMQ7LyuDremDbcNy_iE76Upg"#, + r#"python examples/cli.py eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkJhZFNlY3JldHMiLCJleHAiOjE1OTMxMzM0ODMsImlhdCI6MTQ2NjkwMzA4M30.ovqRikAo_0kKJ0GVrAwQlezymxrLGjcEiW_s3UJMMCo"#, + r#""Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw""#, + r#""value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU","#, + r#"// authorization: 'eyJhbGciOiJIUzUxMiIsImlhdCI6MTU3Njk5Njc5OSwiZXhwIjoxNTg0ODU5MTk5fQ.eyJ1aWQiOjQ1NzQyN30.0ei5UE6OgLBzN2_IS7xUIbIfW_S1Wzl42q2UeusbboxuzvctO_4Mz6YRr6f0PBLUVZMETxt8F0_4-yqIJ3_kUQ',"#, + r#"eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJpc3Rpby1jbmktdG9rZW4tcGpwYnciLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiaXN0aW8tY25pIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiZmY2MDY0ODAtY2MxMC0xMWU4LTkxYzctMDAwYWY3MGE5YmE4Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOmlzdGlvLWNuaSJ9.0YHfuEwYn6tbtgy1YOhOjEtuQ8TnvmA_1RkuggfVGigMpCMGoOkIWwxpeDVXZm7dNwRmQVwLhchA3MeXz0QGRmStLa-VncedkmPOGSC-FyPvPybhZI53w3nhIVU3Vkh9_s-E2H2zFTwRQthxlDAlldNqEHpM9fINIVs0Z3bAogz2DYHwerSOtfZU-6d8b5nn73gnNhl6zBJ_0qg22SZjc6TrDYk--WwjUbU5_OIW6YxEmFnNVqfSeCrpg18IiJCsB0XRkixgu46Ev63jsrJ1vi41PVvBN79X7F-SiNNwTqwACRZvlX1zRw_GV7o4iPvnKn685WLOyMfoB5K6hSxrpQ"#, + r#"const TestKubernetesJWT_A = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImFkbWluLXRva2VuLXFsejQyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNzM4YmMyNTEtNjUzMi0xMWU5LWI2N2YtNDhlNmM4YjhlY2I1Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6YWRtaW4ifQ.ixMlnWrAG7NVuTTKu8cdcYfM7gweS3jlKaEsIBNGOVEjPE7rtXtgMkAwjQTdYR08_0QBjkgzy5fQC5ZNyglSwONJ-bPaXGvhoH1cTnRi1dz9H_63CfqOCvQP1sbdkMeRxNTGVAyWZT76rXoCUIfHP4LY2I8aab0KN9FTIcgZRF0XPTtT70UwGIrSmRpxW38zjiy2ymWL01cc5VWGhJqVysmWmYk3wNp0h5N57H_MOrz4apQR4pKaamzskzjLxO55gpbmZFC76qWuUdexAR7DT2fpbHLOw90atN_NlLMY-VrXyW3-Ei5EhYaVreMB9PSpKwkrA4jULITohV-sxpa1LA""#, + r#"string: grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkZXYtdG8tYW5hbHl0aWMtYXBpLW1hYy10ZXN0QGRldnRvLTE3NTQxOS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImF1ZCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92NC90b2tlbiIsImV4cCI6MTUxOTIyOTAxOSwiaWF0IjoxNTE5MjI4ODk5LCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvYW5hbHl0aWNzLnJlYWRvbmx5In0.V8CSfSS7sKfoE5857jE9WDrGFHF1CyRr3cZpdUv9MjaaTcPRSLuNxB8yrxRP_7hNmlRgx_KdUzBgDJp3M_9tU4rZgFaIC7-bctvz_0rqbnMqSTniHYNGo7w__zO0bRaTpR3ILOfoxCQLcVC-tA4eCIMzRCznkY0VAaoLM7K-hnwQz6fCqSF31fmOwzAdVBPi5qnMETogh_7SiHn4WNUYI0FQf5SFLhcCbBZtORcbANe9hXp9po2P-VTBqs6u9dAZw5kZ2c1l5zbzrjYp5VcYl1XQFQTxP2zgMxhpX3k1UH9ObggOMUxvASyLbPZ7viOPKRlFxkAAHPTN2N1FYbpVeA"#, + r#"eyJhbGciOiJSUzI1NiIsImtpZCI6IlM1WGxrRnVIclJRaEVDbmg3cndZZFVTRTFpT0lfQzZsZ2NXbHZoOS1pbVUifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLWo1a3B2Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJiZTZjZmUwZS0yYzFhLTRkNTYtYmVkMC1jYWRmYjYxNzA1N2YiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6YWRtaW4tdXNlciJ9.LaBPEh6Qantd8tAc0X5DY9dDwUqZpxu38FHnp9TSJw-ghs3TsjrscFulUeEAtp2ng3ElLcU4SbNKPGJflF2dyW9Tmfn-Kt_6Jwq8HQ9GOCwAicEz0JVireHA7EWhATzuT56eO6MTe-2j5bpGnPQRJJtQ8AbtAN3nVK7RPjSzmc8Ppqx1z5i4oCGwiyRlGwqT-FkCtQLbQaQ4XmrASQoN4pJ_OBy5slztUhk32HdGP6pQx5c-nfei-of_4ij_fHrP0xEEfmVVvXqi9WKv1PLkQ3qTiSFDzv8M2sE4T6XmCGBbw7gyHzEGSpOAPZr00bX_YMCUvEF0lyP4YK696xWCBA"#, + r#"$ curl "http://admin:password@127.0.0.1:8080/api/v2/token" + {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiQVBJIl0sImV4cCI6MTYxMzMzNTI2MSwianRpIjoiYzBrb2gxZmNkcnBjaHNzMGZwZmciLCJuYmYiOjE2MTMzMzQ2MzEsInBlcm1pc3Npb25zIjpbIioiXSwic3ViIjoiYUJ0SHUwMHNBUmxzZ29yeEtLQ1pZZWVqSTRKVTlXbThHSGNiVWtWVmc1TT0iLCJ1c2VybmFtZSI6ImFkbWluIn0.WiyqvUF-92zCr--y4Q_sxn-tPnISFzGZd_exsG-K7ME","expires_at":"2021-02-14T20:41:01Z"}"#, + r#"curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiQVBJIl0sImV4cCI6MTYxMzMzNTI2MSwianRpIjoiYzBrb2gxZmNkcnBjaHNzMGZwZmciLCJuYmYiOjE2MTMzMzQ2MzEsInBlcm1pc3Npb25zIjpbIioiXSwic3ViIjoiYUJ0SHUwMHNBUmxzZ29yeEtLQ1pZZWVqSTRKVTlXbThHSGNiVWtWVmc1TT0iLCJ1c2VybmFtZSI6ImFkbWluIn0.WiyqvUF-92zCr--y4Q_sxn-tPnISFzGZd_exsG-K7ME" "http://127.0.0.1:8080/api/v2/dumpdata?output-data=1""#, + r#""authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU""#, + r#"{"signatures": [ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWxlcyI6W3sibmFtZSI6Ii5tYW5pZmVzdCIsImhhc2giOiJjMjEzMTU0NGM3MTZhMjVhNWUzMWY1MDQzMDBmNTI0MGU4MjM1Y2FkYjlhNTdmMGJkMWI2ZjRiZDc0YjI2NjEyIiwiYWxnb3JpdGhtIjoiU0hBMjU2In0seyJuYW1lIjoicm9sZXMvYmluZGluZ3MvZGF0YS5qc29uIiwiaGFzaCI6IjQyY2ZlNjc2OGI1N2JiNWY3NTAzYzE2NWMyOGRkMDdhYzViODEzNTU0ZWJjODUwZjJjYzM1ODQzZTcxMzdiMWQifV0sImlhdCI6MTU5MjI0ODAyNywiaXNzIjoiSldUU2VydmljZSIsImtleWlkIjoibXlQdWJsaWNLZXkiLCJzY29wZSI6IndyaXRlIn0.ZjtUgXC6USwmhv4XP9gFH6MzZwpZrGpAL_2sTK1P-mg"]}"#, + r#""id_token": "eyJ4NXQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJraWQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiYXpwIjoiUG9FZ1hQNnVWTzQ1SXNFTlJuZ0RYajVBdTVZYSIsImlzcyI6Imh0dHBzOlwvXC9sb2NhbGhvc3Q6OTQ0M1wvb2F1dGgyXC90b2tlbiIsImV4cCI6MTUzNDg5MTc3OCwiaWF0IjoxNTM0ODg4MTc4LCJqdGkiOiIxODQ0MzI5Yy1kNjVhLTQ4YTMtODIyOC05ZGY3M2ZlODNkNTYifQ.ELZ8ujk2Xp9xTGgMqnCa5ehuimaAPXWlSCW5QeBbTJIT4M5OB_2XEVIV6p89kftjUdKu50oiYe4SbfrxmLm6NGSGd2qxkjzJK3SRKqsrmVWEn19juj8fz1neKtUdXVHuSZu6ws_bMDy4f_9hN2Jv9dFnkoyeNT54r4jSTJ4A2FzN2rkiURheVVsc8qlm8O7g64Az-5h4UGryyXU4zsnjDCBKYk9jdbEpcUskrFMYhuUlj1RWSASiGhHHHDU5dTRqHkVLIItfG48k_fb-ehU60T7EFWH1JBdNjOxM9oN_yb0hGwOjLUyCUJO_Y7xcd5F4dZzrBg8LffFmvJ09wzHNtQ""#, + r#" # The following default key is generated by the local Supabase start and doesn't change + - SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"#, + r#"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJiIjoyLCJjIjozfQ.hxhGCCCmGV9nT1slief1WgEsOsfdnlVizNrODxfh1M8"#, + r#"--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJhY3RvclR5cGUiOiJVU0VSIiwiYWN0b3JJZCI6ImRhdGFodWIiLCJ0eXBlIjoiUEVSU09OQUwiLCJ2ZXJzaW9uIjoiMSIsImV4cCI6MTY1MDY2MDY1NSwianRpIjoiM2E4ZDY3ZTItOTM5Yi00NTY3LWE0MjYtZDdlMDA1ZGU3NjJjIiwic3ViIjoiZGF0YWh1YiIsImlzcyI6ImRhdGFodWItbWV0YWRhdGEtc2VydmljZSJ9.pp_vW2u1tiiTT7U0nDF2EQdcayOMB8jatiOA8Je4JJA' \"#, + r#""Cookie": "auth-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDIxMTEsImlzQWRtaW4iOmZhbHNlLCJnaXRodWJJZCI6ImR5ZXhwbG9kZSIsImFwcFJvbGVzIjpbInVzZXIiXSwicm9sZXMiOnsiNDciOiJzdHVkZW50IiwiNTYiOiJzdHVkZW50In0sImNvdXJzZXNSb2xlcyI6eyI0NyI6WyJzdHVkZW50Il0sIjU2IjpbInN0dWRlbnQiXX0sImNvdXJzZXMiOnsiNDciOnsibWVudG9ySWQiOm51bGwsInN0dWRlbnRJZCI6NjY3OTMsInJvbGVzIjpbInN0dWRlbnQiXX0sIjU2Ijp7Im1lbnRvcklkIjpudWxsLCJzdHVkZW50SWQiOjYzMzk4LCJyb2xlcyI6WyJzdHVkZW50Il19fSwiaWF0IjoxNjQ1MjA5NzI2LCJleHAiOjE2NDUzODI1MjZ9.btpYeSioEDUNMI6bAPqgu5zndA8XR5DT8P9U9kotOEA""#, + r#"
  • + 在线演示
  • "#, + r#"eyJhbGciOiJSUzI1NiIsImtpZCI6IlM1WGxrRnVIclJRaEVDbmg3cndZZFVTRTFpT0lfQzZsZ2NXbHZoOS1pbVUifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLWo1a3B2Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJiZTZjZmUwZS0yYzFhLTRkNTYtYmVkMC1jYWRmYjYxNzA1N2YiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6YWRtaW4tdXNlciJ9.LaBPEh6Qantd8tAc0X5DY9dDwUqZpxu38FHnp9TSJw-ghs3TsjrscFulUeEAtp2ng3ElLcU4SbNKPGJflF2dyW9Tmfn-Kt_6Jwq8HQ9GOCwAicEz0JVireHA7EWhATzuT56eO6MTe-2j5bpGnPQRJJtQ8AbtAN3nVK7RPjSzmc8Ppqx1z5i4oCGwiyRlGwqT-FkCtQLbQaQ4XmrASQoN4pJ_OBy5slztUhk32HdGP6pQx5c-nfei-of_4ij_fHrP0xEEfmVVvXqi9WKv1PLkQ3qTiSFDzv8M2sE4T6XmCGBbw7gyHzEGSpOAPZr00bX_YMCUvEF0lyP4YK696xWCBA"#, + r#"eyJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6IlRlc3QiLCJsb2dnZWQtaW4tdXNlciI6eyJzY29wZWRQZXJtaXNzaW9uIjpbXSwicGVybWlzc2lvbnMiOlsiQS5hZG1pbl9jdXN0b21lcl9kZWxldGUiLCJBLm5vcm1hbF91c2VyX2FwcCIsIkEubm9ybWFsX3VzZXJfY29uZmlndXJhdGlvbiIsIkEubm9ybWFsX3VzZXJfd2VsY29tZV9jb250cm9scyIsIkEubm9ybWFsX3VzZXJfb3JkZXIiLCJBLm5vcm1hbF91c2VyX3NlYXJjaCIsIkEubm9ybWFsX3VzZXJfc2VhcmNoX3BjIiwiQS5ub3JtYWxfdXNlcl9zZWFyY2hfcHJpdmF0ZSIsIkEubm9ybWFsX3VzZXJfcHJpY2luZyIsIkEubm9ybWFsX3VzZXJfcHJpdmF0ZSIsIkEubm9ybWFsX3VzZXJfY29tbWVyY2lhbCIsIkEubm9ybWFsX3VzZXJfcGMiXSwibmFtZSI6WyJUZXN0Il0sIm1haWwiOlsiVGVzdEBleGFtcGxlLmNvbSJdLCJvcmdhbml6YXRpb24iOlsiWC1YeHh4eHguMTIzNCJdLCJsb2NhdGlvbiI6WyIxMjMyMSJdLCJ1bml0IjpbIjEwMyJdLCJjb3VudHJ5IjpbIkNOIl0sInVzZXJUeXBlIjoiZW1wbG95ZWUifSwiaWRlbnRpdHktaWQiOiJNb2NrIn0.EK5TbwsIgde3mT3n7NK2W7TCvpgQQLzshvPPANRQeUmKOv2AWbo_7vNEDTSkwUlaHRN3-dknv8F95p5MsGTzH6Uva8aOPJG6JdBIoYX_ud3aBN-hY1i2Xpf8pqjeINfY3_gDNAB9gdMznEej2uqhPwUXmZtcuWPdUCCeNqPJbRUAJeVXxLr_JtQzO2jmuwNY_YYp7KaEIANZwG1spvLuIGZ0HA03u8ye9c2lfqYcjgfIkjMrwgWPamR7joZOZPdQSO2EHrF7bUWMjRNY-FF5V7tOjEijkknE_nDq5THcEvx1seHYFdFNwy9LSSGGPVmZMKTKQ3UUlZZyBMXcOpOA9w"#, + // TODO: Detect newlines or escapes (\)? + // https://github.com/mongodb/mongo/blob/1960b792ade4e179ddc6113a3cd400e9492ca11d/src/mongo/crypto/README.JWT.md?plain=1#L115-L117 + // TODO: Detect empty claims section? + // `eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52Nm02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ`, + r#"String tokenWithNoneAlg = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJ0ZXN0LXVzZXIifQ.";"#, + r#"# Req: Invoke-RestMethod -Uri 'http://localhost:8085/users' -Headers @{ 'X-API-KEY' = 'eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6Im1vcnR5Iiwic3ViIjoiMTIzIn0.' }"#, + ]; + let false_positives = vec![]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn jwt_base64() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Base64-encoded JSON Web Token, posing a risk of exposing encoded authentication and data exchange information.".to_string(), + rule_id: "jwt-base64".to_string(), + regex: Regex::new( + r"\bZXlK(?:(?PaGJHY2lPaU)|(?PaGNIVWlPaU)|(?PaGNIWWlPaU)|(?PaGRXUWlPaU)|(?PaU5qUWlP)|(?PamNtbDBJanBi)|(?PamRIa2lPaU)|(?PbGNHc2lPbn)|(?PbGJtTWlPaU)|(?PcWEzVWlPaU)|(?PcWQyc2lPb)|(?PcGMzTWlPaU)|(?PcGRpSTZJ)|(?PcmFXUWlP)|(?PclpYbGZiM0J6SWpwY)|(?PcmRIa2lPaUp)|(?PdWIyNWpaU0k2)|(?Pd01tTWlP)|(?Pd01uTWlPaU)|(?Pd2NIUWlPaU)|(?PemRXSWlPaU)|(?PemRuUWlP)|(?PMFlXY2lPaU)|(?PMGVYQWlPaUp)|(?PMWNtd2l)|(?PMWMyVWlPaUp)|(?PMlpYSWlPaU)|(?PMlpYSnphVzl1SWpv)|(?PNElqb2)|(?PNE5XTWlP)|(?PNE5YUWlPaU)|(?PNE5YUWpVekkxTmlJNkl)|(?PNE5YVWlPaU)|(?PNmFYQWlPaU))[a-zA-Z0-9\/\\_+\-\r\n]{40,}={0,2}" + ).unwrap(), + tags: vec![], + keywords: vec!["zxlk".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = generate_test_values(); + let false_positives = vec![ + r#"-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG/MacGPG2 v2 +Comment: GPGTools - https://gpgtools.org + +mQENBFOrMNcBCAC+gLI4s3bUkobS5NpOQbWfjWXbqC0Ixpc5bZYDOvsmfstmswna +UWUXkRH9RONabzrAu4TGvW0f5DkC2fuWWHJhZWEccn+VE83+avMZN4/mzldSXPNX +A6F7+wHb1DjG+FCDcxMghkwDjGc16LOtZGufUo5iRQaC5pmNBOgDWdiObGPKOTEL +/uU8zLtKi2cibbkhRm22IGOzGyZMZN6zvEtPzlCp3eZEGMW0Ig+kbl6SaSDrSJNK +wElYcr/kJ9QF6CQ2iwZCGeL2jH5QaOi5uj1LXONpCd9nPeyDXc+Z20gXZiqkwRLc +IBPKza6hq/+4nwHBq8DNLv0W4xNC59jLbIhpABEBAAG0LEZyYW5rIE5vdGhhZnQg +PGZub3RoYWZ0QGFsdW1uaS5zdGFuZm9yZC5lZHU+iQE9BBMBCgAnBQJTqzDXAhsD +BQkHhh+ABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJECqYOYG1w7FJOr4IAK8x +ec4jbjd6jkKe0YJGdPzg6TM73ISV5VrUlJX7O3jgxHB4M8KIHN/8A/+ZxLk7WM96 +iq0C8atWHkCkQBtNduWhzAFccQlpxrrb18T5/oItcrmX9Dx2H5WeIl4WAoqe0MTk +iMPv29RMMH9RJvXL0ihuuH4Z0VxV5nurI9QmGzG69QOzfP9qY1EfQEceO7OqXXvY +vvBEUbmWshLuHJ6tOQY5ib3+aLO7m+yJTgJ7s6DHBDqLhqJPW0g7jiJcrDhYXsoS +JMMhMdUhckeZfTXm1N3Dc+/t9/E8NZjdZ2q/ZZzvygC4mu0uihwBKFqoFvXCHRaW +Rf4uYxE+/Upna/mbXQi5AQ0EU6sw1wEIALSp9Cc4t/F2k+rwEfEMXihXLcLM9Dmh +ukz++kMSCSuq4QHE+I4rLda/lVSNJCXaXrGVkzJuzmpEeQFdhr6nLW9ZYhzK8FIc +YyfsYTQxXUVf5W4e/XfKNoG9lrwQd5XHxJTBJ57XjjoWJYPQ69NWH8622foOBpux +xewgR2LEFgl+ksu7aQL4cQif6D3dko3EiIf1t0LDBXxFEREFCg+vkDFsDIW5bdaI +mDYwewGj7dAPLuo0sx1We+uBxb+j30xw/ASDOBhO3ratQWs+4w2FC7gw7cuf0CJC +/hMEw8nloGsIbqBmAnLdlQBxkfFG9DqRBNdSUM8xI66F0eGaaPHJ/S8AEQEAAYkB +JQQYAQoADwUCU6sw1wIbDAUJB4YfgAAKCRAqmDmBtcOxSZvvB/4w6S5YZQUmDVYK +9HPOm49qWxGPd5dLHr2g388sJ4LDK98Q9oicHgf2R35OXTyqhv4kFJL3eukQ6oLW +QOqQKLRycrQUu7eSESAiVmJ1gbuXLAWJmvUABGSYzj3BjWQRexYW/MZ51XqNftF9 +oOSAoFWrdqeJbHoxPTXIb6P8EWk4Ei2j2bSIfBBSbBMSB6Kfk9IjpISM8+97RS5o +6605rmM5MY1Lz8cq8AZIYJs/MnUCZrpQ0c9QSWrflHAeWAy6xMGfdSynbEExQ4z8 +AIfKWMro74nRFbAnv/BzrF2R8uHmdE7T6q9ZZsAydlaxQbdmyhCeGAdw93CwAypb +vHVobJ8A +=mIC1 +-----END PGP PUBLIC KEY BLOCK-----"#, + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +fn generate_test_values() -> Vec { + // Sample JWT from RFC-7519 + let jwt_suffix = ".eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; + + // Validate known header parameters + // https://www.iana.org/assignments/jose/jose.xhtml + let headers: HashMap<&str, &[&str]> = [ + ("alg", &[r#""ES256""#, r#""ES384""#, r#""ES512""#, r#""ECDH-ES""#, r#""HS256""#, r#""HS384""#, r#""HS512""#, r#""RS256""#, r#""RS384""#, r#""RS512""#, r#""PS256""#, r#""PS384""#, r#""PS512""#, r#""none""#, r#""A128KW""#, r#""ECDH-ES+A256KW""#, r#""RSA-OAEP""#]), + ("apu", &[r#""Tx9qG69ZfodhRos-8qfhTPc6ZFnNUcgNDVdHqX1UR3s""#, r#""RfXdxTfIzilWBzWWX3ovHBDzgDcLNy0BFJWSxa0dqnw""#]), + ("apv", &[r#""ZGlkOmVsZW06cm9wc3RlbjpFa""#, r#""ZGlkOmtleTp6Nk1rak1TWWF1dU1neDlhekV5VW5UVzVvNWpGUmtiQnU1VDgzZjM5dU53bnNHbW0jejZMU29HdFpTclVNWnVkQWFnekVmWWY3azhqSFpjR0Q3OVNveDd2NHdDa0RLTlN3""#]), + ("aud", &[r#""https://vault.example.com""#, r#""http://example.com""#]), + ("b64", &["true", "false"]), + ("crit", &[r#"["exp"]"#]), + ("cty", &[r#""example""#, r#""json""#]), + ("epk", &[r#"{"crv":"X25519","kty":"OKP","x":"Tx9qG69ZfodhRos-8qfhTPc6ZFnNUcgNDVdHqX1UR3s"}"#, r#"{"kty":"OKP","crv":"X25519","x":"RfXdxTfIzilWBzWWX3ovHBDzgDcLNy0BFJWSxa0dqnw"}"#]), + ("enc", &[r#""A256GCM""#, r#""A256CBC-HS512""#, r#""A128CBC-HS256""#]), + ("jku", &[r#""https://c2id.com/jwks.json""#]), + ("jwk", &[r#"{"crv":"P-256","kid":"DB2X:GSG2:72H3:AE3R:KCMI:Y77E:W7TF:ERHK:V5HR:JJ2Y:YMS6:HFGJ","kty":"EC","x":"jyr9-xZBorSC9fhqNsmfU_Ud31wbaZ-bVGz0HmySvbQ","y":"vkE6qZCCvYRWjSUwgAOvibQx_s8FipYkAiHS0VnAFNs"}"#, r#"{"kty":"RSA","n":"jyTwiSJACtW_SW-aiihQS5Y5QR704zUwjhlevY0oK-y5wP7SlIc2hq2OPVRarCzjhOxZl2AQFzM5VCR7xRDcnIn9t_pl7Mgsnx9hKDS9yQ24YXzhQ4cMEVVuqwcHvXqPdWDSoCZ1ccMqiiPyBSNGQTXMPY5PBxMOR47XwOb4eNMOPqnzVio3MEtL2wphtEonP3MY6pxJJzzel04wSCRZ4n06reqwER3KwRFPnRpRxAgmSEot5IBLIT3jj-amT5sD7YoUDbPmLk23zgDBIhX88fkClilg1W-fUi1XxYZomEPGvV7OrE1yszt4YDPqKgjJT8t2JPy__1ri-8rZgSxn5Q","e":"AQAB"}"#]), + ("iss", &[r#""http://localhost:8087/realms/grafana""#, r#""kubernetes/serviceaccount""#]), + ("iv", &[r#""zjJPRrj0TGez9JYkChTrB3iqKoDkiBhn""#, r#""10PlAIteHLVABtt""#]), + ("kid", &[r#""my_key_id""#, r#""did:example:123#zC1Rnuvw9rVa6E5TKF4uQVRuQuaCpVgB81Um2u17Fu7UK""#]), + ("key_ops", &[r#"["sign"]"#, r#"["decrypt"]"#, r#"["encrypt"]"#]), + ("kty", &[r#""EC""#, r#""RSA""#]), + ("nonce", &[r#""Os_sBjfWzVZenwwjvLrwXA""#, r#""LDDZAGcBuKYpuNlFTCxPYw""#]), + ("p2c", &["4096", "1000"]), + ("p2s", &[r#""c_ORk4HSsqZD2LvVeCUHqg""#]), + ("ppt", &[r#""foo""#]), + ("sub", &[r#""project_path:my-group/my-project:ref_type:branch:ref:feature-branch-1""#, r#""1234567890""#]), + // I cannot find any real-world examples of the svt header + // and the RFC doesn't seem to explicitly state the content. + ("svt", &[]), + ("tag", &[r#""h6mJqHn33oCsDd5X57MI-g""#]), + ("typ", &[r#""JWT""#, r#""JWK""#, r#""JSOE""#, r#""JSON""#, r#""plain""#]), + ("url", &[r#""https://example.com""#]), + ("use", &[r#""enc""#, r#""sig""#]), + ("ver", &[r#""2""#, r#""3""#]), + ("version", &[r#""2""#, r#""3""#]), + ("x5c", &[r#"["MIICmzCCAYMCBgF4HR7HNDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwMzEwMTcwOTE5WhcNMzEwMzEwMTcxMDU5WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCpLzXHp8i09R2HU5YJPyncC4tiAWWmaDrVZenqynWlWKOjIXb0Y5JoP3ET68u16Bf7mHQGc/u9rRvCw4A92HpJ15WyUSJ80YcK0gPTE0Woc1ZxdK3h4t9AoA8VSrROwQ77w/VAdGrrJ4bwkAVrFqSRpqAsW5XxV3/bU8YkVaG8mh0kuFf/5ib0vxdSkg+mz+ZCuIxQ5YN77kNaMecO19XuaBo7FsG9WjfCPxXYuajkuLgdptPgwTN4np70h0WjaSP/jhjL2ixf48w+27wFDP+ic+B/TCOtVa3fj1GPo6RLxHGU0Zh64jFhmvRM6E/kX7IQ+FJcOwp1VPA9/vMABopAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALILq1Z4oQNJZEUt24VZcvknsWtQtvPxl3JNcBQgDR5/IMgl5VndRZ9OT56KUqrR5xRsWiCvh5Lgv4fUEzAAo9ToiPLub1SKP063zWrvfgi3YZ19bty0iXFm7l2cpQ3ejFV7WpcdLJE0lapFdPLo6QaRdgNu/1p4vbYg7zSK1fQ0OY5b3ajhAx/bhWlrN685owRbO5/r4rUOa6oo9l4Qn7jUxKUx4rcoe7zUM7qrpOPqKvn0DBp3n1/+9pOZXCjIfZGvYwP5NhzBDCkRzaXcJHlOqWzMBzyovVrzVmUilBcj+EsTYJs0gVXKzduX5zO6YWhFs23lu7AijdkxTY65YM0="]"#]), + ("x5t", &[r#""IYIeevIT57t8ppUejM42Bqx6f3I""#]), + ("x5t#S256", &[r#""TuOrBy2NcTlFSWuZ8Kh8W8AjQagb4fnfP1SlKMO8-So""#]), + ("x5u", &[r#""https://tel.example.org/passport.cer""#]), + ("zip", &[r#""DEF""#]), + ].iter().cloned().collect(); + + let mut examples = Vec::new(); + for (key, values) in headers { + for value in values { + let header = format!(r#"{{"{}":{} }}"#, key, value); + let jwt = general_purpose::STANDARD_NO_PAD.encode(header.as_bytes()) + jwt_suffix; + + examples.push(general_purpose::STANDARD.encode(jwt.as_bytes())); + examples.push(general_purpose::STANDARD_NO_PAD.encode(jwt.as_bytes())); + examples.push(url_safe::URL_SAFE.encode(jwt.as_bytes())); + examples.push(url_safe::URL_SAFE_NO_PAD.encode(jwt.as_bytes())); + } + } + + examples +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/kraken.rs b/src/secret-detect/cmd/generate/config/rules/kraken.rs new file mode 100644 index 0000000..8899641 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/kraken.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended_long(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn kraken_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Kraken Access Token, potentially compromising cryptocurrency trading accounts and financial security.".to_string(), + rule_id: "kraken-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["kraken"], &alpha_numeric_extended_long("80,90"), true)).unwrap(), + tags: vec![], + keywords: vec!["kraken".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("kraken", &secrets::new_secret(&alpha_numeric_extended_long("80,90"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/kucoin.rs b/src/secret-detect/cmd/generate/config/rules/kucoin.rs new file mode 100644 index 0000000..e13cd90 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/kucoin.rs @@ -0,0 +1,51 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - hex8_4_4_4_12() -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn kucoin_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Kucoin Access Token, risking unauthorized access to cryptocurrency exchange services and transactions.".to_string(), + rule_id: "kucoin-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["kucoin"], &hex("24"), true)).unwrap(), + tags: vec![], + keywords: vec!["kucoin".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("kucoin", &secrets::new_secret(&hex("24"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn kucoin_secret_key() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Kucoin Secret Key, which could lead to compromised cryptocurrency operations and financial data breaches.".to_string(), + rule_id: "kucoin-secret-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["kucoin"], &hex8_4_4_4_12(), true)).unwrap(), + tags: vec![], + keywords: vec!["kucoin".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("kucoin", &secrets::new_secret(&hex8_4_4_4_12())), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/launchdarkly.rs b/src/secret-detect/cmd/generate/config/rules/launchdarkly.rs new file mode 100644 index 0000000..f057c94 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/launchdarkly.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn launchdarkly_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Launchdarkly Access Token, potentially compromising feature flag management and application functionality.".to_string(), + rule_id: "launchdarkly-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["launchdarkly"], &alpha_numeric_extended("40"), true)).unwrap(), + tags: vec![], + keywords: vec!["launchdarkly".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("launchdarkly", &secrets::new_secret(&alpha_numeric_extended("40"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/linear.rs b/src/secret-detect/cmd/generate/config/rules/linear.rs new file mode 100644 index 0000000..7ffeb35 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/linear.rs @@ -0,0 +1,51 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - hex(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn linear_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Linear API Token, posing a risk to project management tools and sensitive task data.".to_string(), + rule_id: "linear-api-key".to_string(), + regex: Regex::new(r"lin_api_(?i)[a-z0-9]{40}").unwrap(), + tags: vec![], + keywords: vec!["lin_api_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("linear", &format!("lin_api_{}", secrets::new_secret(&alpha_numeric("40")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn linear_client_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Linear Client Secret, which may compromise secure integrations and sensitive project management data.".to_string(), + rule_id: "linear-client-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["linear"], &hex("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["linear".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("linear", &secrets::new_secret(&hex("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/linkedin.rs b/src/secret-detect/cmd/generate/config/rules/linkedin.rs new file mode 100644 index 0000000..e50d5dd --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/linkedin.rs @@ -0,0 +1,50 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn linkedin_client_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a LinkedIn Client secret, potentially compromising LinkedIn application integrations and user data.".to_string(), + rule_id: "linkedin-client-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["linkedin", "linked-in"], &alpha_numeric("16"), true)).unwrap(), + tags: vec![], + keywords: vec!["linkedin".to_string(), "linked-in".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("linkedin", &secrets::new_secret(&alpha_numeric("16"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn linkedin_client_id() -> Rule { + // Define rule + let rule = Rule { + description: "Found a LinkedIn Client ID, risking unauthorized access to LinkedIn integrations and professional data exposure.".to_string(), + rule_id: "linkedin-client-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["linkedin", "linked-in"], &alpha_numeric("14"), true)).unwrap(), + tags: vec![], + keywords: vec!["linkedin".to_string(), "linked-in".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("linkedin", &secrets::new_secret(&alpha_numeric("14"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/lob.rs b/src/secret-detect/cmd/generate/config/rules/lob.rs new file mode 100644 index 0000000..e0deae5 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/lob.rs @@ -0,0 +1,54 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - hex(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn lob_pub_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Lob Publishable API Key, posing a risk of exposing mail and print service integrations.".to_string(), + rule_id: "lob-pub-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["lob"], r"(test|live)_pub_[a-f0-9]{31}", true)).unwrap(), + tags: vec![], + keywords: vec![ + "test_pub".to_string(), + "live_pub".to_string(), + "_pub".to_string(), + ], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("lob", &format!("test_pub_{}", secrets::new_secret(&hex("31")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn lob_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Lob API Key, which could lead to unauthorized access to mailing and address verification services.".to_string(), + rule_id: "lob-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["lob"], r"(live|test)_[a-f0-9]{35}", true)).unwrap(), + tags: vec![], + keywords: vec!["test_".to_string(), "live_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("lob", &format!("test_{}", secrets::new_secret(&hex("35")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/mailchimp.rs b/src/secret-detect/cmd/generate/config/rules/mailchimp.rs new file mode 100644 index 0000000..5be98ea --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/mailchimp.rs @@ -0,0 +1,37 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn mailchimp() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Mailchimp API key, potentially compromising email marketing campaigns and subscriber data.".to_string(), + rule_id: "mailchimp-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["MailchimpSDK.initialize", "mailchimp"], &format!("{} +-us\\d\\d", hex("32")), true)).unwrap(), + tags: vec![], + keywords: vec!["mailchimp".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("mailchimp", &format!("{} +-us20", secrets::new_secret(&hex("32")))), + r#"mailchimp_api_key: cefa780880ba5f5696192a34f6292c35-us18"#, + r#"MAILCHIMPE_KEY = "b5b9f8e50c640da28993e8b6a48e3e53-us18""#, + ]; + let false_positives = vec![ + // False Negative + r#"MailchimpSDK.initialize(token: 3012a5754bbd716926f99c028f7ea428-us18)"#, + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/mailgun.rs b/src/secret-detect/cmd/generate/config/rules/mailgun.rs new file mode 100644 index 0000000..a15ea3c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/mailgun.rs @@ -0,0 +1,71 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn mailgun_private_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Mailgun private API token, risking unauthorized email service operations and data breaches.".to_string(), + rule_id: "mailgun-private-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["mailgun"], r"key-[a-f0-9]{32}", true)).unwrap(), + tags: vec![], + keywords: vec!["mailgun".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("mailgun", &format!("key-{}", secrets::new_secret(&hex("32")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn mailgun_pub_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Mailgun public validation key, which could expose email verification processes and associated data.".to_string(), + rule_id: "mailgun-pub-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["mailgun"], r"pubkey-[a-f0-9]{32}", true)).unwrap(), + tags: vec![], + keywords: vec!["mailgun".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("mailgun", &format!("pubkey-{}", secrets::new_secret(&hex("32")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn mailgun_signing_key() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Mailgun webhook signing key, potentially compromising email automation and data integrity.".to_string(), + rule_id: "mailgun-signing-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["mailgun"], r"[a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8}", true)).unwrap(), + tags: vec![], + keywords: vec!["mailgun".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("mailgun", &format!("{}-00001111-22223333", secrets::new_secret(&hex("32")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/mapbox.rs b/src/secret-detect/cmd/generate/config/rules/mapbox.rs new file mode 100644 index 0000000..1c573d1 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/mapbox.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn mapbox() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a MapBox API token, posing a risk to geospatial services and sensitive location data exposure.".to_string(), + rule_id: "mapbox-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["mapbox"], r"pk\.[a-z0-9]{60}\.[a-z0-9]{22}", true)).unwrap(), + tags: vec![], + keywords: vec!["mapbox".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("mapbox", &format!("pk.{}.{}", secrets::new_secret(&alpha_numeric("60")), secrets::new_secret(&alpha_numeric("22")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/mattermost.rs b/src/secret-detect/cmd/generate/config/rules/mattermost.rs new file mode 100644 index 0000000..0b74eb8 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/mattermost.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn mattermost_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Mattermost Access Token, which may compromise team communication channels and data privacy.".to_string(), + rule_id: "mattermost-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["mattermost"], &alpha_numeric("26"), true)).unwrap(), + tags: vec![], + keywords: vec!["mattermost".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("mattermost", &secrets::new_secret(&alpha_numeric("26"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/messagebird.rs b/src/secret-detect/cmd/generate/config/rules/messagebird.rs new file mode 100644 index 0000000..8500a08 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/messagebird.rs @@ -0,0 +1,53 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - hex8_4_4_4_12() -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn messagebird_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a MessageBird API token, risking unauthorized access to communication platforms and message data.".to_string(), + rule_id: "messagebird-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["messagebird", "message-bird", "message_bird"], &alpha_numeric("25"), true)).unwrap(), + tags: vec![], + keywords: vec!["messagebird".to_string(), "message-bird".to_string(), "message_bird".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("messagebird", &secrets::new_secret(&alpha_numeric("25"))), + generate_sample_secret("message-bird", &secrets::new_secret(&alpha_numeric("25"))), + generate_sample_secret("message_bird", &secrets::new_secret(&alpha_numeric("25"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn messagebird_client_id() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a MessageBird client ID, potentially compromising API integrations and sensitive communication data.".to_string(), + rule_id: "messagebird-client-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["messagebird", "message-bird", "message_bird"], &hex8_4_4_4_12(), true)).unwrap(), + tags: vec![], + keywords: vec!["messagebird".to_string(), "message-bird".to_string(), "message_bird".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"const MessageBirdClientID = "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/netlify.rs b/src/secret-detect/cmd/generate/config/rules/netlify.rs new file mode 100644 index 0000000..d41840a --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/netlify.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn netlify_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Netlify Access Token, potentially compromising web hosting services and site management.".to_string(), + rule_id: "netlify-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["netlify"], &alpha_numeric_extended("40,46"), true)).unwrap(), + tags: vec![], + keywords: vec!["netlify".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("netlify", &secrets::new_secret(&alpha_numeric_extended("40,46"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/newrelic.rs b/src/secret-detect/cmd/generate/config/rules/newrelic.rs new file mode 100644 index 0000000..9267e4b --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/newrelic.rs @@ -0,0 +1,72 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - hex(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn new_relic_user_id() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a New Relic user API Key, which could lead to compromised application insights and performance monitoring.".to_string(), + rule_id: "new-relic-user-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["new-relic", "newrelic", "new_relic"], r"NRAK-[a-z0-9]{27}", true)).unwrap(), + tags: vec![], + keywords: vec!["NRAK".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("new-relic", &format!("NRAK-{}", secrets::new_secret(&alpha_numeric("27")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn new_relic_user_key() -> Rule { + // Define rule + let rule = Rule { + description: "Found a New Relic user API ID, posing a risk to application monitoring services and data integrity.".to_string(), + rule_id: "new-relic-user-api-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["new-relic", "newrelic", "new_relic"], &alpha_numeric("64"), true)).unwrap(), + tags: vec![], + keywords: vec!["new-relic".to_string(), "newrelic".to_string(), "new_relic".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("new-relic", &secrets::new_secret(&alpha_numeric("64"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn new_relic_browser_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a New Relic ingest browser API token, risking unauthorized access to application performance data and analytics.".to_string(), + rule_id: "new-relic-browser-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["new-relic", "newrelic", "new_relic"], r"NRJS-[a-f0-9]{19}", true)).unwrap(), + tags: vec![], + keywords: vec!["NRJS-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("new-relic", &format!("NRJS-{}", secrets::new_secret(&hex("19")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/npm.rs b/src/secret-detect/cmd/generate/config/rules/npm.rs new file mode 100644 index 0000000..7a602e7 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/npm.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn npm() -> Rule{ + // Define rule + let rule = Rule { + description: "Uncovered an npm access token, potentially compromising package management and code repository access.".to_string(), + rule_id: "npm-access-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"npm_[a-z0-9]{36}", true)).unwrap(), + tags: vec![], + keywords: vec!["npm_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("npmAccessToken", &format!("npm_{}", secrets::new_secret(&alpha_numeric("36")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/nytimes.rs b/src/secret-detect/cmd/generate/config/rules/nytimes.rs new file mode 100644 index 0000000..69461d2 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/nytimes.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn nytimes_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Nytimes Access Token, risking unauthorized access to New York Times APIs and content services.".to_string(), + rule_id: "nytimes-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["nytimes", "new-york-times,", "newyorktimes"], &alpha_numeric_extended("32"), true)).unwrap(), + tags: vec![], + keywords: vec!["nytimes".to_string(), "new-york-times".to_string(), "newyorktimes".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("nytimes", &secrets::new_secret(&alpha_numeric("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/okta.rs b/src/secret-detect/cmd/generate/config/rules/okta.rs new file mode 100644 index 0000000..258fd1c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/okta.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn okta_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified an Okta Access Token, which may compromise identity management services and user authentication data.".to_string(), + rule_id: "okta-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["okta"], &alpha_numeric_extended("42"), true)).unwrap(), + tags: vec![], + keywords: vec!["okta".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("okta", &secrets::new_secret(&alpha_numeric("42"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/openai.rs b/src/secret-detect/cmd/generate/config/rules/openai.rs new file mode 100644 index 0000000..f0bd158 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/openai.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn openai() -> Rule { + // Define rule + let rule = Rule { + description: "Found an OpenAI API Key, posing a risk of unauthorized access to AI services and data manipulation.".to_string(), + rule_id: "openai-api-key".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20}", true)).unwrap(), + tags: vec![], + keywords: vec!["T3BlbkFJ".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("openaiApiKey", &format!("sk-{T3BlbkFJ{}", secrets::new_secret(&alpha_numeric("20")), secrets::new_secret(&alpha_numeric("20")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/plaid.rs b/src/secret-detect/cmd/generate/config/rules/plaid.rs new file mode 100644 index 0000000..8603675 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/plaid.rs @@ -0,0 +1,73 @@ +use regex::Regex; +use std::fmt; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - hex8_4_4_4_12() -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn plaid_access_id() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Plaid Client ID, which could lead to unauthorized financial service integrations and data breaches.".to_string(), + rule_id: "plaid-client-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["plaid"], &alpha_numeric("24"), true)).unwrap(), + tags: vec![], + keywords: vec!["plaid".to_string()], + allowlist: Allowlist::default(), + entropy: Some(3.5), + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("plaid", &secrets::new_secret(&alpha_numeric("24"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn plaid_secret_key() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Plaid Secret key, risking unauthorized access to financial accounts and sensitive transaction data.".to_string(), + rule_id: "plaid-secret-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["plaid"], &alpha_numeric("30"), true)).unwrap(), + tags: vec![], + keywords: vec!["plaid".to_string()], + allowlist: Allowlist::default(), + entropy: Some(3.5), + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("plaid", &secrets::new_secret(&alpha_numeric("30"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn plaid_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Plaid API Token, potentially compromising financial data aggregation and banking services.".to_string(), + rule_id: "plaid-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["plaid"], &format!("access-(?:sandbox|development|production)-{}", hex8_4_4_4_12()), true)).unwrap(), + tags: vec![], + keywords: vec!["plaid".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("plaid", &secrets::new_secret(&format!("access-(?:sandbox|development|production)-{}", hex8_4_4_4_12()))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/planetscale.rs b/src/secret-detect/cmd/generate/config/rules/planetscale.rs new file mode 100644 index 0000000..b0506dd --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/planetscale.rs @@ -0,0 +1,77 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn planetscale_password() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a PlanetScale password, which could lead to unauthorized database operations and data breaches.".to_string(), + rule_id: "planetscale-password".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"pscale_pw_(?i)[a-z0-9=\-_\.]{32,64}", true)).unwrap(), + tags: vec![], + keywords: vec!["pscale_pw_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("planetScalePassword", &format!("pscale_pw_{}", secrets::new_secret(&alpha_numeric_extended("32")))), + generate_sample_secret("planetScalePassword", &format!("pscale_pw_{}", secrets::new_secret(&alpha_numeric_extended("43")))), + generate_sample_secret("planetScalePassword", &format!("pscale_pw_{}", secrets::new_secret(&alpha_numeric_extended("64")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn planetscale_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a PlanetScale API token, potentially compromising database management and operations.".to_string(), + rule_id: "planetscale-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"pscale_tkn_(?i)[a-z0-9=\-_\.]{32,64}", true)).unwrap(), + tags: vec![], + keywords: vec!["pscale_tkn_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("planetScalePassword", &format!("pscale_tkn_{}", secrets::new_secret(&alpha_numeric_extended("32")))), + generate_sample_secret("planetScalePassword", &format!("pscale_tkn_{}", secrets::new_secret(&alpha_numeric_extended("43")))), + generate_sample_secret("planetScalePassword", &format!("pscale_tkn_{}", secrets::new_secret(&alpha_numeric_extended("64")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn planetscale_oauth_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a PlanetScale OAuth token, posing a risk to database access control and sensitive data integrity.".to_string(), + rule_id: "planetscale-oauth-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"pscale_oauth_(?i)[a-z0-9=\-_\.]{32,64}", true)).unwrap(), + tags: vec![], + keywords: vec!["pscale_oauth_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("planetScalePassword", &format!("pscale_oauth_{}", secrets::new_secret(&alpha_numeric_extended("32")))), + generate_sample_secret("planetScalePassword", &format!("pscale_oauth_{}", secrets::new_secret(&alpha_numeric_extended("43")))), + generate_sample_secret("planetScalePassword", &format!("pscale_oauth_{}", secrets::new_secret(&alpha_numeric_extended("64")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/postman.rs b/src/secret-detect/cmd/generate/config/rules/postman.rs new file mode 100644 index 0000000..6f3ae7c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/postman.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn postman_api() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Postman API token, potentially compromising API testing and development workflows.".to_string(), + rule_id: "postman-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"PMAK-(?i)[a-f0-9]{24}-[a-f0-9]{34}", true)).unwrap(), + tags: vec![], + keywords: vec!["PMAK-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("postmanAPItoken", &format!("PMAK-{}-{}", secrets::new_secret(&hex("24")), secrets::new_secret(&hex("34")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/prefect.rs b/src/secret-detect/cmd/generate/config/rules/prefect.rs new file mode 100644 index 0000000..bcb3c0e --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/prefect.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn prefect() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Prefect API token, risking unauthorized access to workflow management and automation services.".to_string(), + rule_id: "prefect-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"pnu_[a-z0-9]{36}", true)).unwrap(), + tags: vec![], + keywords: vec!["pnu_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("api-token", &format!("pnu_{}", secrets::new_secret(&alpha_numeric("36")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/privatekey.rs b/src/secret-detect/cmd/generate/config/rules/privatekey.rs new file mode 100644 index 0000000..aa22cd7 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/privatekey.rs @@ -0,0 +1,35 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn private_key() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.".to_string(), + rule_id: "private-key".to_string(), + regex: Regex::new(r"(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY( BLOCK)?-----[\s\S-]*KEY( BLOCK)?-----").unwrap(), + tags: vec![], + keywords: vec!["-----BEGIN".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"-----BEGIN PRIVATE KEY----- +anything +-----END PRIVATE KEY-----"#, + r#"-----BEGIN RSA PRIVATE KEY----- +abcdefghijklmnopqrstuvwxyz +-----END RSA PRIVATE KEY----- +"#, + r#"-----BEGIN PRIVATE KEY BLOCK----- +anything +-----END PRIVATE KEY BLOCK-----"#, + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/pulumi.rs b/src/secret-detect/cmd/generate/config/rules/pulumi.rs new file mode 100644 index 0000000..861b237 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/pulumi.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn pulumi_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Pulumi API token, posing a risk to infrastructure as code services and cloud resource management.".to_string(), + rule_id: "pulumi-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"pul-[a-f0-9]{40}", true)).unwrap(), + tags: vec![], + keywords: vec!["pul-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("pulumi-api-token", &format!("pul-{}", secrets::new_secret(&hex("40")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/pypi.rs b/src/secret-detect/cmd/generate/config/rules/pypi.rs new file mode 100644 index 0000000..d1cc86c --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/pypi.rs @@ -0,0 +1,30 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn pypi_upload_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a PyPI upload token, potentially compromising Python package distribution and repository integrity.".to_string(), + rule_id: "pypi-upload-token".to_string(), + regex: Regex::new(r"pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}").unwrap(), + tags: vec![], + keywords: vec!["pypi-AgEIcHlwaS5vcmc".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![&format!( + "pypiToken := \"pypi-AgEIcHlwaS5vcmc{}{}\"", + secrets::new_secret(&hex("32")), + secrets::new_secret(&hex("32")) + )]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/rapidapi.rs b/src/secret-detect/cmd/generate/config/rules/rapidapi.rs new file mode 100644 index 0000000..3168bb6 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/rapidapi.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn rapidapi_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a RapidAPI Access Token, which could lead to unauthorized access to various APIs and data services.".to_string(), + rule_id: "rapidapi-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["rapidapi"], &alpha_numeric_extended_short("50"), true)).unwrap(), + tags: vec![], + keywords: vec!["rapidapi".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("rapidapi", &secrets::new_secret(&alpha_numeric_extended_short("50"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/readme.rs b/src/secret-detect/cmd/generate/config/rules/readme.rs new file mode 100644 index 0000000..26d54a7 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/readme.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn readme() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Readme API token, risking unauthorized documentation management and content exposure.".to_string(), + rule_id: "readme-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"rdme_[a-z0-9]{70}", true)).unwrap(), + tags: vec![], + keywords: vec!["rdme_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("api-token", &format!("rdme_{}", secrets::new_secret(&alpha_numeric("70")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/rubygems.rs b/src/secret-detect/cmd/generate/config/rules/rubygems.rs new file mode 100644 index 0000000..8d7c8b0 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/rubygems.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn rubygems_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Rubygem API token, potentially compromising Ruby library distribution and package management.".to_string(), + rule_id: "rubygems-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"rubygems_[a-f0-9]{48}", true)).unwrap(), + tags: vec![], + keywords: vec!["rubygems_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("rubygemsAPIToken", &format!("rubygems_{}", secrets::new_secret(&hex("48")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/rule.rs b/src/secret-detect/cmd/generate/config/rules/rule.rs new file mode 100644 index 0000000..4387bad --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/rule.rs @@ -0,0 +1,130 @@ +use regex::Regex; +use std::collections::HashMap; + +use crate::config::{Config, Rule}; +use crate::detect::Detector; + +//TODO: Fix regex pattern errors + +// Constants for regular expression patterns +const CASE_INSENSITIVE: &str = r"(?i)"; +const IDENTIFIER_CASE_INSENSITIVE_PREFIX: &str = r"(?i:"; +const IDENTIFIER_CASE_INSENSITIVE_SUFFIX: &str = r")"; +const IDENTIFIER_PREFIX: &str = r"(?:"; +const IDENTIFIER_SUFFIX: &str = r")(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}"; +const OPERATOR: &str = r"(?:=|>|:{1,3}=|\|\|:|<=|=>|:|\?=) "; +const SECRET_PREFIX_UNIQUE: &str = r"\b("; +const SECRET_PREFIX: &str = r"(?:'|\"|\s|=|\x60){0,5}( "; +const SECRET_SUFFIX: &str = r")(?:['|\"|\n|\r|\s|\x60|;]|$) "; + +// Function to generate a semi-generic regex pattern +pub fn generate_semi_generic_regex(identifiers: &[&str], secret_regex: &str, case_insensitive: bool) -> String { + let mut pattern = String::new(); + + if case_insensitive { + pattern.push_str(CASE_INSENSITIVE); + write_identifiers(&mut pattern, identifiers); + } else { + pattern.push_str(IDENTIFIER_CASE_INSENSITIVE_PREFIX); + write_identifiers(&mut pattern, identifiers); + pattern.push_str(IDENTIFIER_CASE_INSENSITIVE_SUFFIX); + } + + pattern.push_str(OPERATOR); + pattern.push_str(SECRET_PREFIX); + pattern.push_str(secret_regex); + pattern.push_str(SECRET_SUFFIX); + + pattern +} + +// Helper function to write identifiers to the pattern string +fn write_identifiers(pattern: &mut String, identifiers: &[&str]) { + pattern.push_str(IDENTIFIER_PREFIX); + pattern.push_str(&identifiers.join("|")); + pattern.push_str(IDENTIFIER_SUFFIX); +} + +// Function to generate a unique token regex pattern +pub fn generate_unique_token_regex(secret_regex: &str, case_insensitive: bool) -> String { + let mut pattern = String::new(); + + if case_insensitive { + pattern.push_str(CASE_INSENSITIVE); + } + + pattern.push_str(SECRET_PREFIX_UNIQUE); + pattern.push_str(secret_regex); + pattern.push_str(SECRET_SUFFIX); + + pattern +} + +// Function to generate a sample secret for testing +pub fn generate_sample_secret(identifier: &str, secret: &str) -> String { + format!("{}_api_token = \"{}\"", identifier, secret) +} + +// Function to validate a rule using test positives and negatives +pub fn validate(rule: Rule, true_positives: &[&str], false_positives: Option<&[&str]>) -> Rule { + // Normalize keywords + let keywords: Vec = rule.keywords.iter().map(|k| k.to_lowercase()).collect(); + + let mut rules = HashMap::new(); + rules.insert(rule.rule_id.clone(), rule.clone()); + + let config = Config { + rules, + keywords: keywords.clone(), + ..Default::default() + }; + + let detector = Detector::new(config); + + for tp in true_positives { + let detections = detector.detect_string(tp); + if detections.len() != 1 { + panic!("Failed to validate rule: {} - True positive not detected: {}", rule.rule_id, tp); + } + } + + if let Some(false_positives) = false_positives { + for fp in false_positives { + let detections = detector.detect_string(fp); + if !detections.is_empty() { + panic!("Failed to validate rule: {} - False positive detected: {}", rule.rule_id, fp); + } + } + } + + rule +} + +// Helper functions to generate regex patterns for specific formats +pub fn numeric(size: &str) -> String { + format!("[0-9]{{{}}}", size) +} + +pub fn hex(size: &str) -> String { + format!("[a-f0-9]{{{}}}", size) +} + +pub fn alpha_numeric(size: &str) -> String { + format!("[a-z0-9]{{{}}}", size) +} + +pub fn alpha_numeric_extended_short(size: &str) -> String { + format!("[a-z0-9_-]{{{}}}", size) +} + +pub fn alpha_numeric_extended(size: &str) -> String { + format!("[a-z0-9=_\-]{{{}}}", size) +} + +pub fn alpha_numeric_extended_long(size: &str) -> String { + format!("[a-z0-9\/=_\+\-]{{{}}}", size) +} + +pub fn hex8_4_4_4_12() -> String { + "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}".to_string() +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/scalingo.rs b/src/secret-detect/cmd/generate/config/rules/scalingo.rs new file mode 100644 index 0000000..76b62b4 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/scalingo.rs @@ -0,0 +1,30 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn scalingo_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Scalingo API token, posing a risk to cloud platform services and application deployment security.".to_string(), + rule_id: "scalingo-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"tk-us-[a-zA-Z0-9-_]{48}", false)).unwrap(), + tags: vec![], + keywords: vec!["tk-us-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("scalingo", &format!("tk-us-{}", secrets::new_secret(&alpha_numeric_extended_short("48")))), + r#"scalingo_api_token = "tk-us-loys7ib9yrxcys_ta2sq85mjar6lgcsspkd9x61s7h5epf_-""#, // gitleaks:allow + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/sendbird.rs b/src/secret-detect/cmd/generate/config/rules/sendbird.rs new file mode 100644 index 0000000..ee7f036 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/sendbird.rs @@ -0,0 +1,51 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - hex8_4_4_4_12() -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn sendbird_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Sendbird Access Token, potentially risking unauthorized access to communication services and user data.".to_string(), + rule_id: "sendbird-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["sendbird"], &hex("40"), true)).unwrap(), + tags: vec![], + keywords: vec!["sendbird".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("sendbird", &secrets::new_secret(&hex("40"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn sendbird_access_id() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Sendbird Access ID, which could compromise chat and messaging platform integrations.".to_string(), + rule_id: "sendbird-access-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["sendbird"], &hex8_4_4_4_12(), true)).unwrap(), + tags: vec![], + keywords: vec!["sendbird".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("sendbird", &secrets::new_secret(&hex8_4_4_4_12())), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/sendgrid.rs b/src/secret-detect/cmd/generate/config/rules/sendgrid.rs new file mode 100644 index 0000000..a36c14e --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/sendgrid.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn sendgrid_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a SendGrid API token, posing a risk of unauthorized email service operations and data exposure.".to_string(), + rule_id: "sendgrid-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"SG\.(?i)[a-z0-9=_\-\.]{66}", true)).unwrap(), + tags: vec![], + keywords: vec!["SG.".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("sengridAPIToken", &format!("SG.{}", secrets::new_secret(&alpha_numeric_extended("66")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/sendinblue.rs b/src/secret-detect/cmd/generate/config/rules/sendinblue.rs new file mode 100644 index 0000000..67ef297 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/sendinblue.rs @@ -0,0 +1,30 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn sendinblue_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Sendinblue API token, which may compromise email marketing services and subscriber data privacy.".to_string(), + rule_id: "sendinblue-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"xkeysib-[a-f0-9]{64}-(?i)[a-z0-9]{16}", true)).unwrap(), + tags: vec![], + keywords: vec!["xkeysib-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("sendinblue", &format!("xkeysib-{}-{}", secrets::new_secret(&hex("64")), secrets::new_secret(&alpha_numeric("16")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/sentry.rs b/src/secret-detect/cmd/generate/config/rules/sentry.rs new file mode 100644 index 0000000..916847d --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/sentry.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn sentry_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Sentry Access Token, risking unauthorized access to error tracking services and sensitive application data.".to_string(), + rule_id: "sentry-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["sentry"], &hex("64"), true)).unwrap(), + tags: vec![], + keywords: vec!["sentry".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("sentry", &secrets::new_secret(&hex("64"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/shippo.rs b/src/secret-detect/cmd/generate/config/rules/shippo.rs new file mode 100644 index 0000000..96f4166 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/shippo.rs @@ -0,0 +1,30 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - hex(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn shippo_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Shippo API token, potentially compromising shipping services and customer order data.".to_string(), + rule_id: "shippo-api-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"shippo_(live|test)_[a-f0-9]{40}", true)).unwrap(), + tags: vec![], + keywords: vec!["shippo_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("shippo", &format!("shippo_live_{}", secrets::new_secret(&hex("40")))), + generate_sample_secret("shippo", &format!("shippo_test_{}", secrets::new_secret(&hex("40")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/shopify.rs b/src/secret-detect/cmd/generate/config/rules/shopify.rs new file mode 100644 index 0000000..1af6a31 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/shopify.rs @@ -0,0 +1,82 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - hex(length: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn shopify_shared_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Shopify shared secret, posing a risk to application authentication and e-commerce platform security.".to_string(), + rule_id: "shopify-shared-secret".to_string(), + regex: Regex::new(r"shpss_[a-fA-F0-9]{32}").unwrap(), + tags: vec![], + keywords: vec!["shpss_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![&format!("shopifySecret := \"shpss_{}\"", secrets::new_secret(&hex("32")))]; + + validate(rule, &test_positives, None) +} + +pub fn shopify_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Shopify access token, which could lead to unauthorized e-commerce platform access and data breaches.".to_string(), + rule_id: "shopify-access-token".to_string(), + regex: Regex::new(r"shpat_[a-fA-F0-9]{32}").unwrap(), + tags: vec![], + keywords: vec!["shpat_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![&format!("shopifyToken := \"shpat_{}\"", secrets::new_secret(&hex("32")))]; + + validate(rule, &test_positives, None) +} + +pub fn shopify_custom_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Shopify custom access token, potentially compromising custom app integrations and e-commerce data security.".to_string(), + rule_id: "shopify-custom-access-token".to_string(), + regex: Regex::new(r"shpca_[a-fA-F0-9]{32}").unwrap(), + tags: vec![], + keywords: vec!["shpca_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![&format!("shopifyToken := \"shpca_{}\"", secrets::new_secret(&hex("32")))]; + + validate(rule, &test_positives, None) +} + +pub fn shopify_private_app_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Shopify private app access token, risking unauthorized access to private app data and store operations.".to_string(), + rule_id: "shopify-private-app-access-token".to_string(), + regex: Regex::new(r"shppa_[a-fA-F0-9]{32}").unwrap(), + tags: vec![], + keywords: vec!["shppa_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![&format!("shopifyToken := \"shppa_{}\"", secrets::new_secret(&hex("32")))]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/sidekiq.rs b/src/secret-detect/cmd/generate/config/rules/sidekiq.rs new file mode 100644 index 0000000..3b2fbf0 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/sidekiq.rs @@ -0,0 +1,66 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn sidekiq_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Sidekiq Secret, which could lead to compromised background job processing and application data breaches.".to_string(), + rule_id: "sidekiq-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["BUNDLE_ENTERPRISE__CONTRIBSYS__COM", "BUNDLE_GEMS__CONTRIBSYS__COM"], r"[a-f0-9]{8}:[a-f0-9]{8}", true)).unwrap(), + tags: vec![], + keywords: vec!["BUNDLE_ENTERPRISE__CONTRIBSYS__COM".to_string(), "BUNDLE_GEMS__CONTRIBSYS__COM".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + "BUNDLE_ENTERPRISE__CONTRIBSYS__COM: cafebabe:deadbeef", + "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef", + "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM = cafebabe:deadbeef", + r#"BUNDLE_GEMS__CONTRIBSYS__COM: "cafebabe:deadbeef""#, + r#"export BUNDLE_GEMS__CONTRIBSYS__COM="cafebabe:deadbeef""#, + r#"export BUNDLE_GEMS__CONTRIBSYS__COM = "cafebabe:deadbeef""#, + "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;", + "export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef && echo 'hello world'", + ]; + + validate(rule, &test_positives, None) +} + +pub fn sidekiq_sensitive_url() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Sidekiq Sensitive URL, potentially exposing internal job queues and sensitive operation details.".to_string(), + rule_id: "sidekiq-sensitive-url".to_string(), + regex: Regex::new(r"(?i)\b(http(?:s??):\/\/)([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)").unwrap(), + tags: vec![], + keywords: vec!["gems.contribsys.com".to_string(), "enterprise.contribsys.com".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: Some(2), + }; + + // Validate + let test_positives = vec![ + "https://cafebabe:deadbeef@gems.contribsys.com/", + "https://cafebabe:deadbeef@gems.contribsys.com", + "https://cafeb4b3:d3adb33f@enterprise.contribsys.com/", + "https://cafeb4b3:d3adb33f@enterprise.contribsys.com", + "http://cafebabe:deadbeef@gems.contribsys.com/", + "http://cafebabe:deadbeef@gems.contribsys.com", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com/", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com#heading1", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com?param1=true¶m2=false", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80", + "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true¶m2=false#heading1", + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/slack.rs b/src/secret-detect/cmd/generate/config/rules/slack.rs new file mode 100644 index 0000000..8261967 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/slack.rs @@ -0,0 +1,329 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - numeric(length: &str) -> String +// - alpha_numeric(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn slack_bot_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Slack Bot token, which may compromise bot integrations and communication channel security.".to_string(), + rule_id: "slack-bot-token".to_string(), + regex: Regex::new(r"(xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*)").unwrap(), + tags: vec![], + keywords: vec!["xoxb".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#""bot_token1": "xoxb-781236542736-2364535789652-GkwFDQoHqzXDVsC6GzqYUypD""#, + r#""bot_token2": "xoxb-263594206564-2343594206574-FGqddMF8t08v8N7Oq4i57vs1MBS""#, + r#""bot_token3": "xoxb-4614724432022-5152386766518-O5WzjWGLG0wcCm2WPrjEmnys""#, + &format!( + r#""bot_token4": "xoxb-{}-{}-{}""#, + secrets::new_secret(&numeric("13")), + secrets::new_secret(&numeric("12")), + secrets::new_secret(&alpha_numeric("24")) + ), + ]; + let false_positives = vec![ + "xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx", + "xoxb-xxx", + "xoxb-12345-abcd234", + "xoxb-xoxb-my-bot-token", + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn slack_user_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Slack User token, posing a risk of unauthorized user impersonation and data access within Slack workspaces.".to_string(), + rule_id: "slack-user-token".to_string(), + regex: Regex::new(r"(xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34})").unwrap(), + tags: vec![], + keywords: vec!["xoxp-".to_string(), "xoxe-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#""user_token1": "xoxp-41684372915-1320496754-45609968301-e708ba56e1517a99f6b5fb07349476ef""#, + r#""user_token2": "xoxp-283316862324-298911817009-298923149681-44f585044dace54f5701618e97cd1c0b""#, + r#""user_token3": "xoxp-11873098179-111402824422-234336993777-b96c9fb3b69f82ebb79d12f280779de1""#, + r#""user_token4": "xoxp-254112160503-252950188691-252375361712-6cbf56aada30951a9d310a5f23d032a0""#, + r#""user_token5": "xoxp-4614724432022-4621207627011-5182682871568-1ddad9823e8528ad0f4944dfa3c6fc6c""#, + &format!( + r#""user_token6": "xoxp-{}-{}-{}-{}""#, + secrets::new_secret(&numeric("12")), + secrets::new_secret(&numeric("13")), + secrets::new_secret(&numeric("13")), + secrets::new_secret(&alpha_numeric("32")) + ), + // It's unclear what the `xoxe-` token means in this context, however, the format is similar to a user token. + r#""url_private": "https:\/\/files.slack.com\/files-pri\/T04MCQMEXQ9-F04MAA1PKE3\/image.png?t=xoxe-4726837507825-4848681849303-4856614048758-e0b1f3d4cb371f92260edb0d9444d206""#, + ]; + let false_positives = vec![ + r#"https://docs.google.com/document/d/1W7KCxOxP-1Fy5EyF2lbJGE2WuKmu5v0suYqoHas1jRM"#, + r#""token1": "xoxp-1234567890""#, + r#""token2": "xoxp-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX""#, + r#""token3": "xoxp-1234-1234-1234-4ddbc191d40ee098cbaae6f3523ada2d""#, + r#""token4": "xoxp-572370529330-573807301142-572331691188-####################""#, + // This technically matches the pattern but is an obvious false positive. + // r#""token5": "xoxp-000000000000-000000000000-000000000000-00000000000000000000000000000000""#, + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn slack_app_level_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Slack App-level token, risking unauthorized access to Slack applications and workspace data.".to_string(), + rule_id: "slack-app-token".to_string(), + regex: Regex::new(r"(?i)(xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+)").unwrap(), + tags: vec![], + keywords: vec!["xapp".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = vec![ + r#""token1": "xapp-1-A052FGTS2DL-5171572773297-610b6a11f4b7eb819e87b767d80e6575a3634791acb9a9ead051da879eb5b55e""#, + r#""token2": "xapp-1-IEMF8IMY1OQ-4037076220459-85c370b433e366de369c4ef5abdf41253519266982439a75af74a3d68d543fb6""#, + r#""token3": "xapp-1-BM3V7LC51DA-1441525068281-86641a2582cd0903402ab523e5bcc53b8253098c31591e529b55b41974d2e82f""#, + &format!( + r#""token4": "xapp-1-A{}-{}-{}""#, + secrets::new_secret(&numeric("10")), + secrets::new_secret(&numeric("13")), + secrets::new_secret(&alpha_numeric("64")) + ), + ]; + + validate(rule, &test_positives, None) +} + +pub fn slack_configuration_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Slack Configuration access token, posing a risk to workspace configuration and sensitive data access.".to_string(), + rule_id: "slack-config-access-token".to_string(), + regex: Regex::new(r"(?i)(xoxe.xox[bp]-\d-[A-Z0-9]{163,166})").unwrap(), + tags: vec![], + keywords: vec!["xoxe.xoxb-".to_string(), "xoxe.xoxp-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = vec![ + r#""access_token1": "xoxe.xoxp-1-Mi0yLTM0MTQwNDE0MDE3Ni0zNjU5NDY0Njg4MTctNTE4MjA3NTQ5NjA4MC01NDEyOTYyODY5NzUxLThhMTBjZmI1ZWIzMGIwNTg0ZDdmMDI5Y2UxNzVlZWVhYzU2ZWQyZTZiODNjNDZiMGUxMzRlNmNjNDEwYmQxMjQ""#, + r#""access_token2": "xoxe.xoxp-1-Mi0yLTMxNzcwMjQ0MTcxMy0zNjU5NDY0Njg4MTctNTE1ODE1MjY5MTcxNC01MTU4MDI0MTgyOTc5LWRmY2YwY2U4ODhhNzY5ZGU5MTAyNDU4MDJjMGQ0ZDliMTZhMjNkMmEyYzliNjkzMDRlN2VjZTI4MWNiMzRkNGQ""#, + &format!( + r#""access_token3": "xoxe.xoxp-1-{}""#, + secrets::new_secret(&alpha_numeric("163")) + ), + r#""access_token4": "xoxe.xoxb-1-Mi0yLTMxNzcwMjQ0MTcxMy0zNjU5NDY0Njg4MTctNTE1ODE1MjY5MTcxNC01MTU4MDI0MTgyOTc5LWRmY2YwY2U4ODhhNzY5ZGU5MTAyNDU4MDJjMGQ0ZDliMTZhMjNkMmEyYzliNjkzMDRlN2VjZTI4MWNiMzRkNGQ""#, + &format!( + r#""access_token5": "xoxe.xoxb-1-{}""#, + secrets::new_secret(&alpha_numeric("165")) + ), + ]; + let false_positives = vec![ + "xoxe.xoxp-1-SlackAppConfigurationAccessTokenHere", + "xoxe.xoxp-1-RANDOMSTRINGHERE", + "xoxe.xoxp-1-initial", + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn slack_configuration_refresh_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Slack Configuration refresh token, potentially allowing prolonged unauthorized access to configuration settings.".to_string(), + rule_id: "slack-config-refresh-token".to_string(), + regex: Regex::new(r"(?i)(xoxe-\d-[A-Z0-9]{146})").unwrap(), + tags: vec![], + keywords: vec!["xoxe-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = vec![ + r#""refresh_token1": "xoxe-1-My0xLTMxNzcwMjQ0MTcxMy01MTU4MTUyNjkxNzE0LTUxODE4NDI0MDY3MzYtMjA5MGFkOTFlZThkZWE2OGFlZDYwYWJjODNhYzAxYjA5ZjVmODBhYjgzN2QyNDdjOTNlOGY5NTg2YWM1OGM4Mg""#, + r#""refresh_token2": "xoxe-1-My0xLTM0MTQwNDE0MDE3Ni01MTgyMDc1NDk2MDgwLTU0MjQ1NjIwNzgxODEtNGJkYTZhYTUxY2M1ODk3ZTNkN2YzMTgxMDI1ZDQzNzgwNWY4NWQ0ODdhZGIzM2ViOGI0MTM0MjdlNGVmYzQ4Ng""#, + &format!( + r#""refresh_token3": "xoxe-1-{}""#, + secrets::new_secret(&alpha_numeric("146")) + ), + ]; + let false_positives = vec!["xoxe-1-xxx", "XOxE-RROAmw, Home and Garden, 5:24, 20120323"]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn slack_legacy_bot_token() -> Rule { + let rule = Rule { + description: "Uncovered a Slack Legacy bot token, which could lead to compromised legacy bot operations and data exposure.".to_string(), + rule_id: "slack-legacy-bot-token".to_string(), + regex: Regex::new(r"(xoxb-[0-9]{8,14}-[a-zA-Z0-9]{18,26})").unwrap(), + tags: vec![], + keywords: vec!["xoxb".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = vec![ + r#""bot_token1": "xoxb-263594206564-FGqddMF8t08v8N7Oq4i57vs1""#, + r#""bot_token2": "xoxb-282029623751-BVtmnS3BQitmjZvjpQL7PSGP""#, + r#""bot_token3": "xoxb-47834520726-N3otsrwj8Cf99cs8GhiRZsX1""#, + r#""bot_token4": "xoxb-123456789012-Xw937qtWSXJss1lFaKe""#, + r#""bot_token5": "xoxb-312554961652-uSmliU84rFhnUSBq9YdKh6lS""#, + r#""bot_token6": "xoxb-51351043345-Lzwmto5IMVb8UK36MghZYMEi""#, + r#""bot_token7": "xoxb-130154379991-ogFL0OFP3w6AwdJuK7wLojpK""#, + r#""bot_token8": "xoxb-159279836768-FOst5DLfEzmQgkz7cte5qiI""#, + r#""bot_token9": "xoxb-50014434-slacktokenx29U9X1bQ""#, + &format!( + r#""bot_token10": "xoxb-{}-{}""#, + secrets::new_secret(&numeric("10")), + secrets::new_secret(&alpha_numeric("24")) + ), + &format!( + r#""bot_token11": "xoxb-{}-{}""#, + secrets::new_secret(&numeric("12")), + secrets::new_secret(&alpha_numeric("23")) + ), + ]; + let false_positives = vec![ + "xoxb-xxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx", + "xoxb-Slack_BOT_TOKEN", + "xoxb-abcdef-abcdef", + // "xoxb-0000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn slack_legacy_workspace_token() -> Rule { + let rule = Rule { + description: "Identified a Slack Legacy Workspace token, potentially compromising access to workspace data and legacy features.".to_string(), + rule_id: "slack-legacy-workspace-token".to_string(), + regex: Regex::new(r"(xox[ar](?:-\d-)?[0-9a-zA-Z]{8,48})").unwrap(), + tags: vec![], + keywords: vec!["xoxa".to_string(), "xoxr".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + let test_positives = vec![ + r#""access_token": "xoxa-2-511111111-31111111111-3111111111111-e039d02840a0b9379c""#, + &format!( + r#""access_token1": "xoxa-{}-{}""#, + secrets::new_secret(&numeric("1")), + secrets::new_secret(&alpha_numeric("12")) + ), + &format!( + r#""access_token2": "xoxa-{}""#, + secrets::new_secret(&alpha_numeric("12")) + ), + &format!( + r#""refresh_token1": "xoxr-{}-{}""#, + secrets::new_secret(&numeric("1")), + secrets::new_secret(&alpha_numeric("12")) + ), + &format!( + r#""refresh_token2": "xoxr-{}""#, + secrets::new_secret(&alpha_numeric("12")) + ), + ]; + let false_positives = vec![ + // "xoxa-faketoken", + // "xoxa-access-token-string", + // "XOXa-nx991k", + "https://github.com/xoxa-nyc/xoxa-nyc.github.io/blob/master/README.md", + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn slack_legacy_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Slack Legacy token, risking unauthorized access to older Slack integrations and user data.".to_string(), + rule_id: "slack-legacy-token".to_string(), + regex: Regex::new(r"(xox[os]-\d+-\d+-\d+-[a-fA-F\d]+)").unwrap(), + tags: vec![], + keywords: vec!["xoxo".to_string(), "xoxs".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#""access_token1": "xoxs-3206092076-3204538285-3743137121-836b042620""#, + r#""access_token2": "xoxs-416843729158-132049654-5609968301-e708ba56e1""#, + r#""access_token3": "xoxs-420083410720-421837374423-440811613314-977844f625b707d5b0b268206dbc92cbc85feef3e71b08e44815a8e6e7657190""#, + r#""access_token4": "xoxs-4829527689-4829527691-4814341714-d0346ec616""#, + r#""access_token5": "xoxs-155191149137-155868813314-338998331396-9f6d235915""#, + &format!( + r#""access_token6": "xoxs-{}-{}-{}-{}""#, + secrets::new_secret(&numeric("10")), + secrets::new_secret(&numeric("10")), + secrets::new_secret(&numeric("10")), + secrets::new_secret(&hex("10")) + ), + r#""access_token7": "xoxo-523423-234243-234233-e039d02840a0b9379c""#, + ]; + let false_positives = vec![ + "https://indieweb.org/images/3/35/2018-250-xoxo-indieweb-1.jpg", + "https://lh3.googleusercontent.com/-tWXjX3LUD6w/Ua4La_N5E2I/AAAAAAAAACg/qcm19xbEYa4/s640/EXO-XOXO-teaser-exo-k-34521098-720-516.jpg", + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn slack_webhook_url() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Slack Webhook, which could lead to unauthorized message posting and data leakage in Slack channels.".to_string(), + rule_id: "slack-webhook-url".to_string(), + regex: Regex::new(r"(https?:\/\/)?hooks.slack.com\/(services|workflows)\/[A-Za-z0-9+\/]{43,46}").unwrap(), + tags: vec![], + keywords: vec!["hooks.slack.com".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + &format!("hooks.slack.com/services/{}", secrets::new_secret(&alpha_numeric("44"))), + &format!("http://hooks.slack.com/services/{}", secrets::new_secret(&alpha_numeric("45"))), + &format!("https://hooks.slack.com/services/{}", secrets::new_secret(&alpha_numeric("46"))), + "http://hooks.slack.com/services/T024TTTTT/BBB72BBL/AZAAA9u0pA4ad666eMgbi555", + "https://hooks.slack.com/services/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", + &format!("hooks.slack.com/workflows/{}", secrets::new_secret(&alpha_numeric("44"))), + &format!("http://hooks.slack.com/workflows/{}", secrets::new_secret(&alpha_numeric("45"))), + &format!("https://hooks.slack.com/workflows/{}", secrets::new_secret(&alpha_numeric("46"))), + "https://hooks.slack.com/workflows/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", + "http://hooks.slack.com/workflows/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", + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/snyk.rs b/src/secret-detect/cmd/generate/config/rules/snyk.rs new file mode 100644 index 0000000..52107e8 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/snyk.rs @@ -0,0 +1,44 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex8_4_4_4_12() -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn snyk() -> Rule { + let keywords = vec![ + "snyk_token".to_string(), + "snyk_key".to_string(), + "snyk_api_token".to_string(), + "snyk_api_key".to_string(), + "snyk_oauth_token".to_string(), + ]; + + // Define rule + let rule = Rule { + description: "Uncovered a Snyk API token, potentially compromising software vulnerability scanning and code security.".to_string(), + rule_id: "snyk-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&keywords, &hex8_4_4_4_12(), true)).unwrap(), + tags: vec![], + keywords, + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"const SNYK_TOKEN = "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"const SNYK_KEY = "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"SNYK_TOKEN := "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"SNYK_TOKEN ::= "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"SNYK_TOKEN :::= "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"SNYK_TOKEN ?= "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"SNYK_API_KEY ?= "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"SNYK_API_TOKEN = "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + r#"SNYK_OAUTH_TOKEN = "12345678-ABCD-ABCD-ABCD-1234567890AB""#, + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/square.rs b/src/secret-detect/cmd/generate/config/rules/square.rs new file mode 100644 index 0000000..81bd31f --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/square.rs @@ -0,0 +1,52 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn square_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Square Access Token, risking unauthorized payment processing and financial transaction exposure.".to_string(), + rule_id: "square-access-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"(EAAA|sq0atp-)[0-9A-Za-z\-_]{22,60}", true)).unwrap(), + tags: vec![], + keywords: vec!["sq0atp-".to_string(), "EAAA".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("square", &secrets::new_secret(r"sq0atp-[0-9A-Za-z\-_]{22}")), + "ARG token=sq0atp-812erere3wewew45678901", + "ARG token=EAAAlsBxkkVgvmr7FasTFbM6VUGZ31EJ4jZKTJZySgElBDJ_wyafHuBFquFexY7E", + ]; + + validate(rule, &test_positives, None) +} + +pub fn square_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Square Secret".to_string(), + rule_id: "square-secret".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"sq0csp-[0-9A-Za-z\\-_]{43}", true)).unwrap(), + tags: vec![], + keywords: vec!["sq0csp-".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("square", &secrets::new_secret(r"sq0csp-[0-9A-Za-z\\-_]{43}")), + r#"value: "sq0csp-0p9h7g6f4s3s3s3-4a3ardgwa6ADRDJDDKUFYDYDYDY""#, + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/squarespace.rs b/src/secret-detect/cmd/generate/config/rules/squarespace.rs new file mode 100644 index 0000000..4911248 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/squarespace.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - hex8_4_4_4_12() -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn squarespace_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Squarespace Access Token, which may compromise website management and content control on Squarespace.".to_string(), + rule_id: "squarespace-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["squarespace"], &hex8_4_4_4_12(), true)).unwrap(), + tags: vec![], + keywords: vec!["squarespace".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("squarespace", &secrets::new_secret(&hex8_4_4_4_12())), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/stopwords.rs b/src/secret-detect/cmd/generate/config/rules/stopwords.rs new file mode 100644 index 0000000..2e34314 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/stopwords.rs @@ -0,0 +1,1479 @@ +pub static DEFAULT_STOP_WORDS: &[&str] = &[ + "000000", + "aaaaaa", + "about", + "abstract", + "academy", + "acces", + "account", + "act-", + "act.", + "act_", + "action", + "active", + "actively", + "activity", + "adapter", + "add-", + "add.", + "add_", + "add-on", + "addon", + "addres", + "admin", + "adobe", + "advanced", + "adventure", + "agent", + "agile", + "air-", + "air.", + "air_", + "ajax", + "akka", + "alert", + "alfred", + "algorithm", + "all-", + "all.", + "all_", + "alloy", + "alpha", + "amazon", + "amqp", + "analysi", + "analytic", + "analyzer", + "android", + "angular", + "angularj", + "animate", + "animation", + "another", + "ansible", + "answer", + "ant-", + "ant.", + "ant_", + "any-", + "any.", + "any_", + "apache", + // "api-", + // "api.", + // "api_", lin_api_ is used for linear + "app-", + "app-", + "app.", + "app.", + "app_", + "app_", + "apple", + "arch", + "archive", + "archived", + "arduino", + "array", + "art-", + "art.", + "art_", + "article", + "asp-", + "asp.", + "asp_", + "asset", + "async", + "atom", + "attention", + "audio", + "audit", + "aura", + "auth", + "author", + "author", + "authorize", + "auto", + "automated", + "automatic", + "awesome", + "aws_", + "azure", + "back", + "backbone", + "backend", + "backup", + "bar-", + "bar.", + "bar_", + "base", + "based", + "bash", + "basic", + "batch", + "been", + "beer", + "behavior", + "being", + "benchmark", + "best", + "beta", + "better", + "big-", + "big.", + "big_", + "binary", + "binding", + "bit-", + "bit.", + "bit_", + "bitcoin", + "block", + "blog", + "board", + "book", + "bookmark", + "boost", + "boot", + "bootstrap", + "bosh", + "bot-", + "bot.", + "bot_", + "bower", + "box-", + "box.", + "box_", + "boxen", + "bracket", + "branch", + "bridge", + "browser", + "brunch", + "buffer", + "bug-", + "bug.", + "bug_", + "build", + "builder", + "building", + "buildout", + "buildpack", + "built", + "bundle", + "busines", + "but-", + "but.", + "but_", + "button", + "cache", + "caching", + "cakephp", + "calendar", + "call", + "camera", + "campfire", + "can-", + "can.", + "can_", + "canva", + "captcha", + "capture", + "card", + "carousel", + "case", + "cassandra", + "cat-", + "cat.", + "cat_", + "category", + "center", + "cento", + "challenge", + "change", + "changelog", + "channel", + "chart", + "chat", + "cheat", + "check", + "checker", + "chef", + "ches", + "chinese", + "chosen", + "chrome", + "ckeditor", + "clas", + "classe", + "classic", + "clean", + "cli-", + "cli.", + "cli_", + "client", + "client", + "clojure", + "clone", + "closure", + "cloud", + "club", + "cluster", + "cms-", + "cms_", + "coco", + "code", + "coding", + "coffee", + "color", + "combination", + "combo", + "command", + "commander", + "comment", + "commit", + "common", + "community", + "compas", + "compiler", + "complete", + "component", + "composer", + "computer", + "computing", + "con-", + "con.", + "con_", + "concept", + "conf", + "config", + "config", + "connect", + "connector", + "console", + "contact", + "container", + "contao", + "content", + "contest", + "context", + "control", + "convert", + "converter", + "conway'", + "cookbook", + "cookie", + "cool", + "copy", + "cordova", + "core", + "couchbase", + "couchdb", + "countdown", + "counter", + "course", + "craft", + "crawler", + "create", + "creating", + "creator", + "credential", + "crm-", + "crm.", + "crm_", + "cros", + "crud", + "csv-", + "csv.", + "csv_", + "cube", + "cucumber", + "cuda", + "current", + "currently", + "custom", + "daemon", + "dark", + "dart", + "dash", + "dashboard", + "data", + "database", + "date", + "day-", + "day.", + "day_", + "dead", + "debian", + "debug", + "debug", + "debugger", + "deck", + "define", + "del-", + "del.", + "del_", + "delete", + "demo", + "deploy", + "design", + "designer", + "desktop", + "detection", + "detector", + "dev-", + "dev.", + "dev_", + "develop", + "developer", + "device", + "devise", + "diff", + "digital", + "directive", + "directory", + "discovery", + "display", + "django", + "dns-", + "dns_", + "doc-", + "doc-", + "doc.", + "doc.", + "doc_", + "doc_", + "docker", + "docpad", + "doctrine", + "document", + "doe-", + "doe.", + "doe_", + "dojo", + "dom-", + "dom.", + "dom_", + "domain", + "done", + "don't", + "dot-", + "dot.", + "dot_", + "dotfile", + "download", + "draft", + "drag", + "drill", + "drive", + "driven", + "driver", + "drop", + "dropbox", + "drupal", + "dsl-", + "dsl.", + "dsl_", + "dynamic", + "easy", + "_ec2_", + "ecdsa", + "eclipse", + "edit", + "editing", + "edition", + "editor", + "element", + "emac", + "email", + "embed", + "embedded", + "ember", + "emitter", + "emulator", + "encoding", + "endpoint", + "engine", + "english", + "enhanced", + "entity", + "entry", + "env_", + "episode", + "erlang", + "error", + "espresso", + "event", + "evented", + "example", + "example", + "exchange", + "exercise", + "experiment", + "expire", + "exploit", + "explorer", + "export", + "exporter", + "expres", + "ext-", + "ext.", + "ext_", + "extended", + "extension", + "external", + "extra", + "extractor", + "fabric", + "facebook", + "factory", + "fake", + "fast", + "feature", + "feed", + "fewfwef", + "ffmpeg", + "field", + "file", + "filter", + "find", + "finder", + "firefox", + "firmware", + "first", + "fish", + "fix-", + "fix_", + "flash", + "flask", + "flat", + "flex", + "flexible", + "flickr", + "flow", + "fluent", + "fluentd", + "fluid", + "folder", + "font", + "force", + "foreman", + "fork", + "form", + "format", + "formatter", + "forum", + "foundry", + "framework", + "free", + "friend", + "friendly", + "front-end", + "frontend", + "ftp-", + "ftp.", + "ftp_", + "fuel", + "full", + "fun-", + "fun.", + "fun_", + "func", + "future", + "gaia", + "gallery", + "game", + "gateway", + "gem-", + "gem.", + "gem_", + "gen-", + "gen.", + "gen_", + "general", + "generator", + "generic", + "genetic", + "get-", + "get.", + "get_", + "getenv", + "getting", + "ghost", + "gist", + "git-", + "git.", + "git_", + "github", + "gitignore", + "gitlab", + "glas", + "gmail", + "gnome", + "gnu-", + "gnu.", + "gnu_", + "goal", + "golang", + "gollum", + "good", + "google", + "gpu-", + "gpu.", + "gpu_", + "gradle", + "grail", + "graph", + "graphic", + "great", + "grid", + "groovy", + "group", + "grunt", + "guard", + "gui-", + "gui.", + "gui_", + "guide", + "guideline", + "gulp", + "gwt-", + "gwt.", + "gwt_", + "hack", + "hackathon", + "hacker", + "hacking", + "hadoop", + "haml", + "handler", + "hardware", + "has-", + "has_", + "hash", + "haskell", + "have", + "haxe", + "hello", + "help", + "helper", + "here", + "hero", + "heroku", + "high", + "hipchat", + "history", + "home", + "homebrew", + "homepage", + "hook", + "host", + "hosting", + "hot-", + "hot.", + "hot_", + "house", + "how-", + "how.", + "how_", + "html", + "http", + "hub-", + "hub.", + "hub_", + "hubot", + "human", + "icon", + "ide-", + "ide.", + "ide_", + "idea", + "identity", + "idiomatic", + "image", + "impact", + "import", + "important", + "importer", + "impres", + "index", + "infinite", + "info", + "injection", + "inline", + "input", + "inside", + "inspector", + "instagram", + "install", + "installer", + "instant", + "intellij", + "interface", + "internet", + "interview", + "into", + "intro", + "ionic", + "iphone", + "ipython", + "irc-", + "irc_", + "iso-", + "iso.", + "iso_", + "issue", + "jade", + "jasmine", + "java", + "jbos", + "jekyll", + "jenkin", + "job-", + "job.", + "job_", + "joomla", + "jpa-", + "jpa.", + "jpa_", + "jquery", + "json", + "just", + "kafka", + "karma", + "kata", + "kernel", + "keyboard", + "kindle", + "kit-", + "kit.", + "kit_", + "kitchen", + "knife", + "koan", + "kohana", + "lab-", + "lab-", + "lab.", + "lab.", + "lab_", + "lab_", + "lambda", + "lamp", + "language", + "laravel", + "last", + "latest", + "latex", + "launcher", + "layer", + "layout", + "lazy", + "ldap", + "leaflet", + "league", + "learn", + "learning", + "led-", + "led.", + "led_", + "leetcode", + "les-", + "les.", + "les_", + "level", + "leveldb", + "lib-", + "lib.", + "lib_", + "librarie", + "library", + "license", + "life", + "liferay", + "light", + "lightbox", + "like", + "line", + "link", + "linked", + "linkedin", + "linux", + "lisp", + "list", + "lite", + "little", + "load", + "loader", + "local", + "location", + "lock", + "log-", + "log.", + "log_", + "logger", + "logging", + "logic", + "login", + "logstash", + "longer", + "look", + "love", + "lua-", + "lua.", + "lua_", + "mac-", + "mac.", + "mac_", + "machine", + "made", + "magento", + "magic", + "mail", + "make", + "maker", + "making", + "man-", + "man.", + "man_", + "manage", + "manager", + "manifest", + "manual", + "map-", + "map-", + "map.", + "map.", + "map_", + "map_", + "mapper", + "mapping", + "markdown", + "markup", + "master", + "math", + "matrix", + "maven", + "md5", + "mean", + "media", + "mediawiki", + "meetup", + "memcached", + "memory", + "menu", + "merchant", + "message", + "messaging", + "meta", + "metadata", + "meteor", + "method", + "metric", + "micro", + "middleman", + "migration", + "minecraft", + "miner", + "mini", + "minimal", + "mirror", + "mit-", + "mit.", + "mit_", + "mobile", + "mocha", + "mock", + "mod-", + "mod.", + "mod_", + "mode", + "model", + "modern", + "modular", + "module", + "modx", + "money", + "mongo", + "mongodb", + "mongoid", + "mongoose", + "monitor", + "monkey", + "more", + "motion", + "moved", + "movie", + "mozilla", + "mqtt", + "mule", + "multi", + "multiple", + "music", + "mustache", + "mvc-", + "mvc.", + "mvc_", + "mysql", + "nagio", + "name", + "native", + "need", + "neo-", + "neo.", + "neo_", + "nest", + "nested", + "net-", + "net.", + "net_", + "nette", + "network", + "new-", + "new-", + "new.", + "new.", + "new_", + "new_", + "next", + "nginx", + "ninja", + "nlp-", + "nlp.", + "nlp_", + "node", + "nodej", + "nosql", + "not-", + "not.", + "not_", + "note", + "notebook", + "notepad", + "notice", + "notifier", + "now-", + "now.", + "now_", + "number", + "oauth", + "object", + "objective", + "obsolete", + "ocaml", + "octopres", + "official", + "old-", + "old.", + "old_", + "onboard", + "online", + "only", + "open", + "opencv", + "opengl", + "openshift", + "openwrt", + "option", + "oracle", + "org-", + "org.", + "org_", + "origin", + "original", + "orm-", + "orm.", + "orm_", + "osx-", + "osx_", + "our-", + "our.", + "our_", + "out-", + "out.", + "out_", + "output", + "over", + "overview", + "own-", + "own.", + "own_", + "pack", + "package", + "packet", + "page", + "page", + "panel", + "paper", + "paperclip", + "para", + "parallax", + "parallel", + "parse", + "parser", + "parsing", + "particle", + "party", + "password", + "patch", + "path", + "pattern", + "payment", + "paypal", + "pdf-", + "pdf.", + "pdf_", + "pebble", + "people", + "perl", + "personal", + "phalcon", + "phoenix", + "phone", + "phonegap", + "photo", + "php-", + "php.", + "php_", + "physic", + "picker", + "pipeline", + "platform", + "play", + "player", + "please", + "plu-", + "plu.", + "plu_", + "plug-in", + "plugin", + "plupload", + "png-", + "png.", + "png_", + "poker", + "polyfill", + "polymer", + "pool", + "pop-", + "pop.", + "pop_", + "popcorn", + "popup", + "port", + "portable", + "portal", + "portfolio", + "post", + "power", + "powered", + "powerful", + "prelude", + "pretty", + "preview", + "principle", + "print", + "pro-", + "pro.", + "pro_", + "problem", + "proc", + "product", + "profile", + "profiler", + "program", + "progres", + "project", + "protocol", + "prototype", + "provider", + "proxy", + "public", + "pull", + "puppet", + "pure", + "purpose", + "push", + "pusher", + "pyramid", + "python", + "quality", + "query", + "queue", + "quick", + "rabbitmq", + "rack", + "radio", + "rail", + "railscast", + "random", + "range", + "raspberry", + "rdf-", + "rdf.", + "rdf_", + "react", + "reactive", + "read", + "reader", + "readme", + "ready", + "real", + "reality", + "real-time", + "realtime", + "recipe", + "recorder", + "red-", + "red.", + "red_", + "reddit", + "redi", + "redmine", + "reference", + "refinery", + "refresh", + "registry", + "related", + "release", + "remote", + "rendering", + "repo", + "report", + "request", + "require", + "required", + "requirej", + "research", + "resource", + "response", + "resque", + "rest", + "restful", + "resume", + "reveal", + "reverse", + "review", + "riak", + "rich", + "right", + "ring", + "robot", + "role", + "room", + "router", + "routing", + "rpc-", + "rpc.", + "rpc_", + "rpg-", + "rpg.", + "rpg_", + "rspec", + "ruby-", + "ruby.", + "ruby_", + "rule", + "run-", + "run.", + "run_", + "runner", + "running", + "runtime", + "rust", + "rvm-", + "rvm.", + "rvm_", + "salt", + "sample", + "sample", + "sandbox", + "sas-", + "sas.", + "sas_", + "sbt-", + "sbt.", + "sbt_", + "scala", + "scalable", + "scanner", + "schema", + "scheme", + "school", + "science", + "scraper", + "scratch", + "screen", + "script", + "scroll", + "scs-", + "scs.", + "scs_", + "sdk-", + "sdk.", + "sdk_", + "sdl-", + "sdl.", + "sdl_", + "search", + "secure", + "security", + "see-", + "see.", + "see_", + "seed", + "select", + "selector", + "selenium", + "semantic", + "sencha", + "send", + "sentiment", + "serie", + "server", + "service", + "session", + "set-", + "set.", + "set_", + "setting", + "setting", + "setup", + "sha1", + "sha2", + "sha256", + "share", + "shared", + "sharing", + "sheet", + "shell", + "shield", + "shipping", + "shop", + "shopify", + "shortener", + "should", + "show", + "showcase", + "side", + "silex", + "simple", + "simulator", + "single", + "site", + "skeleton", + "sketch", + "skin", + "slack", + "slide", + "slider", + "slim", + "small", + "smart", + "smtp", + "snake", + "snippet", + "soap", + "social", + "socket", + "software", + "solarized", + "solr", + "solution", + "solver", + "some", + "soon", + "source", + "space", + "spark", + "spatial", + "spec", + "sphinx", + "spine", + "spotify", + "spree", + "spring", + "sprite", + "sql-", + "sql.", + "sql_", + "sqlite", + "ssh-", + "ssh.", + "ssh_", + "stack", + "staging", + "standard", + "stanford", + "start", + "started", + "starter", + "startup", + "stat", + "statamic", + "state", + "static", + "statistic", + "statsd", + "statu", + "steam", + "step", + "still", + "stm-", + "stm.", + "stm_", + "storage", + "store", + "storm", + "story", + "strategy", + "stream", + "streaming", + "string", + "stripe", + "structure", + "studio", + "study", + "stuff", + "style", + "sublime", + "sugar", + "suite", + "summary", + "super", + "support", + "supported", + "svg-", + "svg.", + "svg_", + "svn-", + "svn.", + "svn_", + "swagger", + "swift", + "switch", + "switcher", + "symfony", + "symphony", + "sync", + "synopsi", + "syntax", + "system", + "system", + "tab-", + "tab-", + "tab.", + "tab.", + "tab_", + "tab_", + "table", + "tag-", + "tag-", + "tag.", + "tag.", + "tag_", + "tag_", + "talk", + "target", + "task", + "tcp-", + "tcp.", + "tcp_", + "tdd-", + "tdd.", + "tdd_", + "team", + "tech", + "template", + "term", + "terminal", + "testing", + "tetri", + "text", + "textmate", + "theme", + "theory", + "three", + "thrift", + "time", + "timeline", + "timer", + "tiny", + "tinymce", + "tip-", + "tip.", + "tip_", + "title", + "todo", + "todomvc", + "token", + "tool", + "toolbox", + "toolkit", + "top-", + "top.", + "top_", + "tornado", + "touch", + "tower", + "tracker", + "tracking", + "traffic", + "training", + "transfer", + "translate", + "transport", + "tree", + "trello", + "try-", + "try.", + "try_", + "tumblr", + "tut-", + "tut.", + "tut_", + "tutorial", + "tweet", + "twig", + "twitter", + "type", + "typo", + "ubuntu", + "uiview", + "ultimate", + "under", + "unit", + "unity", + "universal", + "unix", + "update", + "updated", + "upgrade", + "upload", + "uploader", + "uri-", + "uri.", + "uri_", + "url-", + "url.", + "url_", + "usage", + "usb-", + "usb.", + "usb_", + "use-", + "use.", + "use_", + "used", + "useful", + "user", + "using", + "util", + "utilitie", + "utility", + "vagrant", + "validator", + "value", + "variou", + "varnish", + "version", + "via-", + "via.", + "via_", + "video", + "view", + "viewer", + "vim-", + "vim.", + "vim_", + "vimrc", + "virtual", + "vision", + "visual", + "vpn", + "want", + "warning", + "watch", + "watcher", + "wave", + "way-", + "way.", + "way_", + "weather", + "web-", + "web_", + "webapp", + "webgl", + "webhook", + "webkit", + "webrtc", + "website", + "websocket", + "welcome", + "welcome", + "what", + "what'", + "when", + "where", + "which", + "why-", + "why.", + "why_", + "widget", + "wifi", + "wiki", + "win-", + "win.", + "win_", + "window", + "wip-", + "wip.", + "wip_", + "within", + "without", + "wizard", + "word", + "wordpres", + "work", + "worker", + "workflow", + "working", + "workshop", + "world", + "wrapper", + "write", + "writer", + "writing", + "written", + "www-", + "www.", + "www_", + "xamarin", + "xcode", + "xml-", + "xml.", + "xml_", + "xmpp", + "xxxxxx", + "yahoo", + "yaml", + "yandex", + "yeoman", + "yet-", + "yet.", + "yet_", + "yii-", + "yii.", + "yii_", + "youtube", + "yui-", + "yui.", + "yui_", + "zend", + "zero", + "zip-", + "zip.", + "zip_", + "zsh-", + "zsh.", + "zsh_", +]; \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/stripe.rs b/src/secret-detect/cmd/generate/config/rules/stripe.rs new file mode 100644 index 0000000..8c3f796 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/stripe.rs @@ -0,0 +1,39 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn stripe_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data.".to_string(), + rule_id: "stripe-access-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"(sk|rk)_(test|live|prod)_[0-9a-z]{10,99}", true)).unwrap(), + tags: vec![], + keywords: vec![ + "sk_test".to_string(), + "sk_live".to_string(), + "sk_prod".to_string(), + "rk_test".to_string(), + "rk_live".to_string(), + "rk_prod".to_string(), + ], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + &format!("stripeToken := \"sk_test_{}\"", secrets::new_secret(&alpha_numeric("30"))), + "sk_test_51OuEMLAlTWGaDypq4P5cuDHbuKeG4tAGPYHJpEXQ7zE8mKK3jkhTFPvCxnSSK5zB5EQZrJsYdsatNmAHGgb0vSKD00GTMSWRHs", + "rk_prod_51OuEMLAlTWGaDypquDn9aZigaJOsa9NR1w1BxZXs9JlYsVVkv5XDu6aLmAxwt5Tgun5WcSwQMKzQyqV16c9iD4sx00BRijuoon", + ]; + let false_positives = vec![&format!("nonMatchingToken := \"task_test_{}\"", secrets::new_secret(&alpha_numeric("30")))]; + + validate(rule, &test_positives, Some(&false_positives)) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/sumologic.rs b/src/secret-detect/cmd/generate/config/rules/sumologic.rs new file mode 100644 index 0000000..cc65e2f --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/sumologic.rs @@ -0,0 +1,81 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn sumologic_access_id() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a SumoLogic Access ID, potentially compromising log management services and data analytics integrity.".to_string(), + rule_id: "sumologic-access-id".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["sumo"], r"su[a-zA-Z0-9]{12}", false)).unwrap(), + tags: vec![], + keywords: vec!["sumo".to_string()], + allowlist: Allowlist { + regex_target: "line".to_string(), + regexes: vec![Regex::new(r"sumOf").unwrap()], + ..Allowlist::default() + }, + entropy: Some(3.0), + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"sumologic.accessId = "su9OL59biWiJu7""#, + r#"sumologic_access_id = "sug5XpdpaoxtOH""#, + r#"export SUMOLOGIC_ACCESSID="suDbJw97o9WVo0""#, + r#"SUMO_ACCESS_ID = "suGyI5imvADdvU""#, + generate_sample_secret("sumo", &format!("su{}", secrets::new_secret(&alpha_numeric("12")))), + ]; + let false_positives = vec![ + r#"- (NSNumber *)sumOfProperty:(NSString *)property;"#, + r#"- (NSInteger)sumOfValuesInRange:(NSRange)range;"#, + r#"+ (unsigned char)byteChecksumOfData:(id)arg1;"#, + r#"sumOfExposures = sumOfExposures;"#, + r#".si-sumologic.si--color::before { color: #000099; }"#, + r#"/// Based on the SumoLogic keyword syntax:"#, + r#"sumologic_access_id = """#, + r#"SUMOLOGIC_ACCESSID: ${SUMOLOGIC_ACCESSID}"#, + r#"export SUMOLOGIC_ACCESSID=XXXXXXXXXXXXXX"#, + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} + +pub fn sumologic_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a SumoLogic Access Token, which could lead to unauthorized access to log data and analytics insights.".to_string(), + rule_id: "sumologic-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["sumo"], &alpha_numeric("64"), true)).unwrap(), + tags: vec![], + keywords: vec!["sumo".to_string()], + allowlist: Allowlist::default(), + entropy: Some(3.0), + secret_group: None, + }; + + // Validate + let test_positives = vec![ + r#"export SUMOLOGIC_ACCESSKEY="3HSa1hQfz6BYzlxf7Yb1WKG3Hyovm56LMFChV2y9LgkRipsXCujcLb5ej3oQUJlx""#, + r#"SUMO_ACCESS_KEY: gxq3rJQkS6qovOg9UY2Q70iH1jFZx0WBrrsiAYv4XHodogAwTKyLzvFK4neRN8Dk"#, + r#"SUMOLOGIC_ACCESSKEY: 9RITWb3I3kAnSyUolcVJq4gwM17JRnQK8ugRaixFfxkdSl8ys17ZtEL3LotESKB7"#, + r#"sumo_access_key = "3Kof2VffNQ0QgYIhXUPJosVlCaQKm2hfpWE6F1fT9YGY74blQBIPsrkCcf1TwKE5""#, + generate_sample_secret("sumo", &secrets::new_secret(&alpha_numeric("64"))), + ]; + let false_positives = vec![ + r#"# SUMO_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"#, + "-e SUMO_ACCESS_KEY=`etcdctl get /sumologic_secret`", + r#"SUMO_ACCESS_KEY={SumoAccessKey}"#, + r#"SUMO_ACCESS_KEY=${SUMO_ACCESS_KEY:=$2}"#, + r#"sumo_access_key = """#, + "SUMO_ACCESS_KEY: AbCeFG123", + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/teams.rs b/src/secret-detect/cmd/generate/config/rules/teams.rs new file mode 100644 index 0000000..de83328 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/teams.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn teams_webhook() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Microsoft Teams Webhook, which could lead to unauthorized access to team collaboration tools and data leaks.".to_string(), + rule_id: "microsoft-teams-webhook".to_string(), + regex: Regex::new(r"https:\/\/[a-z0-9]+\.webhook\.office\.com\/webhookb2\/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}\/IncomingWebhook\/[a-z0-9]{32}\/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}").unwrap(), + tags: vec![], + keywords: vec![ + "webhook.office.com".to_string(), + "webhookb2".to_string(), + "IncomingWebhook".to_string(), + ], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![&format!("https://mycompany.webhook.office.com/webhookb2/{}", secrets::new_secret(r"[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}\/IncomingWebhook\/[a-z0-9]{32}\/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}"))]; // gitleaks:allow + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/telegram.rs b/src/secret-detect/cmd/generate/config/rules/telegram.rs new file mode 100644 index 0000000..7e31f11 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/telegram.rs @@ -0,0 +1,51 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - numeric(length: &str) -> String +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn telegram_bot_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Telegram Bot API Token, risking unauthorized bot operations and message interception on Telegram.".to_string(), + rule_id: "telegram-bot-api-token".to_string(), + regex: Regex::new(r"(?i)(?:^|[^0-9])([0-9]{5,16}:A[a-zA-Z0-9_\-]{34})(?:$|[^a-zA-Z0-9_\-])").unwrap(), + tags: vec![], + keywords: vec![ + "telegram".to_string(), + "api".to_string(), + "bot".to_string(), + "token".to_string(), + "url".to_string(), + ], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let valid_token = secrets::new_secret(&(numeric("8") + ":A" + alpha_numeric_extended_short("34"))); + let min_token = secrets::new_secret(&(numeric("5") + ":A" + alpha_numeric_extended_short("34"))); + let max_token = secrets::new_secret(&(numeric("16") + ":A" + alpha_numeric_extended_short("34"))); + let test_positives = vec![ + generate_sample_secret("telegram", &valid_token), + generate_sample_secret("url", &format!("https://api.telegram.org/bot{}/sendMessage", valid_token)), + &format!("const bot = new Telegraf(\"{}\")", valid_token), + &format!("API_TOKEN = {}", valid_token), + &format!("bot: {}", valid_token), + generate_sample_secret("telegram", &min_token), + generate_sample_secret("telegram", &max_token), + ]; + + let too_small_token = secrets::new_secret(&(numeric("4") + ":A" + alpha_numeric_extended_short("34"))); + let too_big_token = secrets::new_secret(&(numeric("17") + ":A" + alpha_numeric_extended_short("34"))); + let false_positives = vec![ + generate_sample_secret("telegram", &too_small_token), + generate_sample_secret("telegram", &too_big_token), + ]; + + validate(rule, &test_positives, Some(&false_positives)) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/travisci.rs b/src/secret-detect/cmd/generate/config/rules/travisci.rs new file mode 100644 index 0000000..9427f83 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/travisci.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn travis_ci_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Travis CI Access Token, potentially compromising continuous integration services and codebase security.".to_string(), + rule_id: "travisci-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["travis"], &alpha_numeric("22"), true)).unwrap(), + tags: vec![], + keywords: vec!["travis".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("travis", &secrets::new_secret(&alpha_numeric("22"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/trello.rs b/src/secret-detect/cmd/generate/config/rules/trello.rs new file mode 100644 index 0000000..7311a84 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/trello.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn trello_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Trello Access Token".to_string(), + rule_id: "trello-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["trello"], r"[a-zA-Z-0-9]{32}", true)).unwrap(), + tags: vec![], + keywords: vec!["trello".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("trello", &secrets::new_secret(r"[a-zA-Z-0-9]{32}")), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/twillio.rs b/src/secret-detect/cmd/generate/config/rules/twillio.rs new file mode 100644 index 0000000..59297bd --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/twillio.rs @@ -0,0 +1,28 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - hex(length: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn twilio() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Twilio API Key, posing a risk to communication services and sensitive customer interaction data.".to_string(), + rule_id: "twilio-api-key".to_string(), + regex: Regex::new(r"SK[0-9a-fA-F]{32}").unwrap(), + tags: vec![], + keywords: vec!["twilio".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + &format!("twilioAPIKey := \"SK{} +\"", secrets::new_secret(&hex("32"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/twitch.rs b/src/secret-detect/cmd/generate/config/rules/twitch.rs new file mode 100644 index 0000000..5b2bbe7 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/twitch.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn twitch_api_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Twitch API token, which could compromise streaming services and account integrations.".to_string(), + rule_id: "twitch-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["twitch"], &alpha_numeric("30"), true)).unwrap(), + tags: vec![], + keywords: vec!["twitch".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("twitch", &secrets::new_secret(&alpha_numeric("30"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/twitter.rs b/src/secret-detect/cmd/generate/config/rules/twitter.rs new file mode 100644 index 0000000..f7eedda --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/twitter.rs @@ -0,0 +1,113 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - alpha_numeric(length: &str) -> String +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn twitter_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Twitter API Key, which may compromise Twitter application integrations and user data security.".to_string(), + rule_id: "twitter-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["twitter"], &alpha_numeric("25"), true)).unwrap(), + tags: vec![], + keywords: vec!["twitter".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("twitter", &secrets::new_secret(&alpha_numeric("25"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn twitter_api_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Twitter API Secret, risking the security of Twitter app integrations and sensitive data access.".to_string(), + rule_id: "twitter-api-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["twitter"], &alpha_numeric("50"), true)).unwrap(), + tags: vec![], + keywords: vec!["twitter".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("twitter", &secrets::new_secret(&alpha_numeric("50"))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn twitter_bearer_token() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Twitter Bearer Token, potentially compromising API access and data retrieval from Twitter.".to_string(), + rule_id: "twitter-bearer-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["twitter"], r"A{22}[a-zA-Z0-9%]{80,100}", true)).unwrap(), + tags: vec![], + keywords: vec!["twitter".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("twitter", &secrets::new_secret(r"A{22}[a-zA-Z0-9%]{80,100}")), + ]; + + validate(rule, &test_positives, None) +} + +pub fn twitter_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Twitter Access Token, posing a risk of unauthorized account operations and social media data exposure.".to_string(), + rule_id: "twitter-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["twitter"], r"[0-9]{15,25}-[a-zA-Z0-9]{20,40}", true)).unwrap(), + tags: vec![], + keywords: vec!["twitter".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("twitter", &secrets::new_secret(r"[0-9]{15,25}-[a-zA-Z0-9]{20,40}")), + ]; + + validate(rule, &test_positives, None) +} + +pub fn twitter_access_secret() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Twitter Access Secret, potentially risking unauthorized Twitter integrations and data breaches.".to_string(), + rule_id: "twitter-access-secret".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["twitter"], &alpha_numeric("45"), true)).unwrap(), + tags: vec![], + keywords: vec!["twitter".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("twitter", &secrets::new_secret(&alpha_numeric("45"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/typeform.rs b/src/secret-detect/cmd/generate/config/rules/typeform.rs new file mode 100644 index 0000000..67e7f7a --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/typeform.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn typeform() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Typeform API token, which could lead to unauthorized survey management and data collection.".to_string(), + rule_id: "typeform-api-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["typeform"], r"tfp_[a-z0-9\-_\.=]{59}", true)).unwrap(), + tags: vec![], + keywords: vec!["tfp_".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("typeformAPIToken", &format!("tfp_{}", secrets::new_secret(&alpha_numeric_extended("59")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/vault.rs b/src/secret-detect/cmd/generate/config/rules/vault.rs new file mode 100644 index 0000000..d1a64d3 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/vault.rs @@ -0,0 +1,50 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_unique_token_regex(pattern: &str, case_insensitive: bool) -> String +// - alpha_numeric_extended_short(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn vault_service_token() -> Rule { + // Define rule + let rule = Rule { + description: "Identified a Vault Service Token, potentially compromising infrastructure security and access to sensitive credentials.".to_string(), + rule_id: "vault-service-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"hvs\.[a-z0-9_-]{90,100}", true)).unwrap(), + tags: vec![], + keywords: vec!["hvs".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("vault", &format!("hvs.{}", secrets::new_secret(&alpha_numeric_extended_short("90")))), + ]; + + validate(rule, &test_positives, None) +} + +pub fn vault_batch_token() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Vault Batch Token, risking unauthorized access to secret management services and sensitive data.".to_string(), + rule_id: "vault-batch-token".to_string(), + regex: Regex::new(&generate_unique_token_regex(r"hvb\.[a-z0-9_-]{138,212}", true)).unwrap(), + tags: vec![], + keywords: vec!["hvb".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("vault", &format!("hvb.{}", secrets::new_secret(&alpha_numeric_extended_short("138")))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/yandex.rs b/src/secret-detect/cmd/generate/config/rules/yandex.rs new file mode 100644 index 0000000..14eb1d1 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/yandex.rs @@ -0,0 +1,70 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn yandex_aws_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Uncovered a Yandex AWS Access Token, potentially compromising cloud resource access and data security on Yandex Cloud.".to_string(), + rule_id: "yandex-aws-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["yandex"], r"YC[a-zA-Z0-9_\-]{38}", true)).unwrap(), + tags: vec![], + keywords: vec!["yandex".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("yandex", &secrets::new_secret(r"YC[a-zA-Z0-9_\-]{38}")), + ]; + + validate(rule, &test_positives, None) +} + +pub fn yandex_api_key() -> Rule { + // Define rule + let rule = Rule { + description: "Discovered a Yandex API Key, which could lead to unauthorized access to Yandex services and data manipulation.".to_string(), + rule_id: "yandex-api-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["yandex"], r"AQVN[A-Za-z0-9_\-]{35,38}", true)).unwrap(), + tags: vec![], + keywords: vec!["yandex".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("yandex", &secrets::new_secret(r"AQVN[A-Za-z0-9_\-]{35,38}")), + ]; + + validate(rule, &test_positives, None) +} + +pub fn yandex_access_token() -> Rule { + // Define rule + let rule = Rule { + description: "Found a Yandex Access Token, posing a risk to Yandex service integrations and user data privacy.".to_string(), + rule_id: "yandex-access-token".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["yandex"], r"t1\.[A-Z0-9a-z_-]+[=]{0,2}\.[A-Z0-9a-z_-]{86}[=]{0,2}", true)).unwrap(), + tags: vec![], + keywords: vec!["yandex".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("yandex", &secrets::new_secret(r"t1\.[A-Z0-9a-z_-]+[=]{0,2}\.[A-Z0-9a-z_-]{86}[=]{0,2}")), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/config/rules/zendesk.rs b/src/secret-detect/cmd/generate/config/rules/zendesk.rs new file mode 100644 index 0000000..af6a402 --- /dev/null +++ b/src/secret-detect/cmd/generate/config/rules/zendesk.rs @@ -0,0 +1,29 @@ +use regex::Regex; + +use crate::config::{Allowlist, Rule}; + +// - generate_semi_generic_regex(prefixes: &[&str], suffix: &str, case_insensitive: bool) -> String +// - alpha_numeric(length: &str) -> String +// - generate_sample_secret(prefix: &str, secret: &str) -> String +// - validate(rule: Rule, test_positives: &[&str], test_negatives: Option<&[&str]>) -> Rule + +pub fn zendesk_secret_key() -> Rule { + // Define rule + let rule = Rule { + description: "Detected a Zendesk Secret Key, risking unauthorized access to customer support services and sensitive ticketing data.".to_string(), + rule_id: "zendesk-secret-key".to_string(), + regex: Regex::new(&generate_semi_generic_regex(&["zendesk"], &alpha_numeric("40"), true)).unwrap(), + tags: vec![], + keywords: vec!["zendesk".to_string()], + allowlist: Allowlist::default(), + entropy: None, + secret_group: None, + }; + + // Validate + let test_positives = vec![ + generate_sample_secret("zendesk", &secrets::new_secret(&alpha_numeric("40"))), + ]; + + validate(rule, &test_positives, None) +} \ No newline at end of file diff --git a/src/secret-detect/cmd/generate/secrets/regen.rs b/src/secret-detect/cmd/generate/secrets/regen.rs new file mode 100644 index 0000000..dd6d9b1 --- /dev/null +++ b/src/secret-detect/cmd/generate/secrets/regen.rs @@ -0,0 +1,28 @@ +use lazy_static::lazy_static; +use rand::prelude::*; +use regex::Regex; +use reggen::Generator; +use std::sync::Mutex; + +lazy_static! { + static ref RNG: Mutex = Mutex::new(rand::thread_rng()); +} + +/// Generates a secret string based on the provided regular expression. +/// +/// # Arguments +/// +/// * `regex` - A string slice containing the regular expression. +/// +/// # Returns +/// +/// A string containing the generated secret. +/// +/// # Panics +/// +/// Panics if the regular expression is invalid or if secret generation fails. +pub fn new_secret(regex: &str) -> String { + let mut rng = RNG.lock().unwrap(); + let generator = Generator::new(regex, &mut *rng).unwrap(); + generator.generate() +} \ No newline at end of file diff --git a/src/secret-detect/cmd/protect.rs b/src/secret-detect/cmd/protect.rs new file mode 100644 index 0000000..5111e7c --- /dev/null +++ b/src/secret-detect/cmd/protect.rs @@ -0,0 +1,93 @@ +use std::time::Instant; +use std::process::exit; + +use clap::{arg, ArgMatches, Command}; +use log::{error, info, LevelFilter}; +use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; + +use crate::config::Config; +use crate::detect::Detector; +use crate::report::{Finding, FindingSummary}; +use crate::sources::GitDiffCmd; + +// Function to initialize logging based on verbosity level +fn init_logging(verbose: u64) { + let log_level = match verbose { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + _ => LevelFilter::Trace, + }; + + let config = ConfigBuilder::new() + .set_target_level(log_level) + .set_thread_level(LevelFilter::Off) + .build(); + + TermLogger::init( + log_level, + config, + TerminalMode::Mixed, + ColorChoice::Auto, + ) + .unwrap(); +} + + +// Function to run the protect command +pub fn run_protect(matches: &ArgMatches) { + let start_time = Instant::now(); + + // Initialize logging + init_logging(matches.get_count("verbose") as u64); + + // Load the configuration + let config = match Config::load_from_file(matches.get_one::("config").unwrap()) { + Ok(cfg) => cfg, + Err(e) => { + error!("Error loading config: {}", e); + exit(1); + } + }; + + let source = matches.get_one::("source").unwrap(); + let staged = matches.get_flag("staged"); + let exit_code = matches.get_one::("exit-code").unwrap_or(&0); + + let detector = Detector::new(&config, source.clone()); + let git_cmd = match GitDiffCmd::new(source, staged) { + Ok(cmd) => cmd, + Err(e) => { + error!("Error creating GitDiffCmd: {}", e); + exit(1); + } + }; + + let findings = match detector.detect_git(&git_cmd) { + Ok(f) => f, + Err(e) => { + error!("Error during detection: {}", e); + Vec::new() + } + }; + + finding_summary_and_exit(&findings, &config, *exit_code, start_time); +} + +// Function to print finding summary and exit with appropriate code +fn finding_summary_and_exit(findings: &[Finding], config: &Config, exit_code: i32, start_time: Instant) { + let summary = FindingSummary::new(findings, config, start_time.elapsed()); + + if !findings.is_empty() { + summary.print(); + } + + if summary.has_secrets() { + info!( + "{} secrets detected in {}.", + summary.total_findings, summary.duration + ); + exit(exit_code); + } + + info!("No secrets detected in {}.", summary.duration); +} \ No newline at end of file diff --git a/src/secret-detect/cmd/root.rs b/src/secret-detect/cmd/root.rs new file mode 100644 index 0000000..29e208a --- /dev/null +++ b/src/secret-detect/cmd/root.rs @@ -0,0 +1,485 @@ +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::{self, Read, Write}; +use std::path::PathBuf; +use std::time::{Duration, Instant}; +use std::process::exit; + +use clap::{arg, ArgMatches, Command}; +use log::{error, info, warn, LevelFilter}; +use simplelog::{ColorChoice, CombinedLogger, ConfigBuilder, TermLogger, TerminalMode, WriteLogger}; + +use crate::config::{Config, Rule}; +use crate::detect::Detector; +use crate::report::{Finding, FindingSummary, write_report}; +use crate::sources::{ + DirectoryTargets, GitDiffCmd, GitLogCmd, PipeReader, +}; + +const BANNER: &str = r#" + +................................................................................ +................................................................................ +................................................................................ +............................,,,,,,,,,,,,,,,,,,,................................. +.........................,*#&@@@@@@@@@@@@@@@@@&#/,.............................. +.........................*#%@@&&&&&&&&&&&&&&&&&&#*.............................. +.........................*#%@@&&&&&&&&&&&&&&&&&&#*.............................. +.........................*#%@&&&&&&&&&&&&&&&&&&&#*.............................. +.........................*#%@&&&&&&&&&&&&&&&&&&&#*.............................. +.........,(#%##(/*,......*#%&&&&&&&&&&&&&&&&&&&&#*......,*/(##%#(,.............. +.......,*(&@&&&&&&&%%(/*,*#%&&&&&&&&&&&&&&&&&&&&#*,*/(#%&&&&&&&@&#/,............ +.......*%&&&&&&&&&&&&@@@@@@@&&&&&&&&&&&&&&&&&&&&&&&@@@&&&&&&&&&&&&%*............ +.....,*(&@&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&@&#/,.......... +.....*%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%*.......... +...,*(&@&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&@&#/,........ +...*#&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#*........ +...*%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%*........ +. ..,/#%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%#/,.. ..... + ...,*(#%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%(*,... ..... + ..,/(#%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%#(/*.. + .*(&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&(*. + .,/%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%(,. + .,/%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%/,. + .,/%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%/,. + .,/%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%/*. + .*(%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&%(, + ,*(%&&&&&&&&&&&&&&&&&&&&&%(*,,(%&&&&&&&&&&&&&&&&&&&&&%(/. + .*/#%&&&&&&&&&&&&&&&&%(*. .*(%&&&&&&&&&&&&&&&&%#(*.. + ..,/(%&&&&&&&&&&&&(*. ..*(%&&&&&&&&&&&%(/,.. + .,*(%&&&&&&&(*.. .,(%&&&&&&%(*,. + .*(#%%(*. .,(%%%(*. + + + +/$$ /$$ /$$ +| $$ | $$ | $$ +| $$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$| $$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ +| $$ /$$/ /$$__ $$| $$ | $$ /$$_____/| $$__ $$ |____ $$ /$$__ $$ /$$__ $$ +| $$$$$$/ | $$$$$$$$| $$ | $$| $$$$$$ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ +| $$_ $$ | $$_____/| $$ | $$ \____ $$| $$ | $$ /$$__ $$| $$ | $$| $$_____/ +| $$ \ $$| $$$$$$$| $$$$$$$ /$$$$$$$/| $$ | $$| $$$$$$$| $$$$$$$| $$$$$$$ +|__/ \__/ \_______/ \____ $$|_______/ |__/ |__/ \_______/ \_______/ \_______/ + /$$ | $$ + | $$$$$$/ keyshade secret scanner + \______/ + +"#; + +// Function to initialize logging based on verbosity level and color choice +fn init_logging(verbose: u64, no_color: bool) { + let log_level = match verbose { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + _ => LevelFilter::Trace, + }; + + let config = ConfigBuilder::new() + .set_target_level(log_level) + .set_thread_level(LevelFilter::Off) + .build(); + + let term_logger = TermLogger::new( + log_level, + config.clone(), + TerminalMode::Mixed, + if no_color { ColorChoice::Never } else { ColorChoice::Auto }, + ); + + // Combine the terminal logger with a file logger if the environment variable is set + if let Ok(log_file) = std::env::var("GITLEAKS_LOG_FILE") { + let file = File::create(log_file).expect("Failed to create log file"); + let file_logger = WriteLogger::new(log_level, config, file); + CombinedLogger::init(vec![term_logger.unwrap(), file_logger]).unwrap(); + } else { + TermLogger::init( + log_level, + config, + TerminalMode::Mixed, + if no_color { ColorChoice::Never } else { ColorChoice::Auto }, + ) + .unwrap(); + } +} + +// Function to run the detect command +pub fn run_detect(matches: &ArgMatches) { + let start_time = Instant::now(); + + // Initialize logging + let no_color = matches.get_flag("no-color"); + init_logging(matches.get_count("verbose") as u64, no_color); + + // Load the configuration + let config = match Config::load_from_file_or_default(matches.get_one::("config")) { + Ok(cfg) => cfg, + Err(e) => { + error!("Failed to load config: {}", e); + exit(1); + } + }; + + let source = matches.get_one::("source").unwrap_or("."); + let mut detector = Detector::new(config); + + // Override enabled rules if specified + if let Some(rule_ids) = matches.get_many::("enable-rule") { + detector.config.rules = override_enabled_rules(detector.config.rules.clone(), rule_ids); + } + + // Add baseline if specified + if let Some(baseline_path) = matches.get_one::("baseline-path") { + if let Err(e) = detector.add_baseline(baseline_path, source) { + error!("Failed to load baseline: {}", e); + } + } + + // Add .gitleaksignore if specified or found in default locations + let gitleaks_ignore_path = matches.get_one::("keyshade-ignore-path").unwrap_or("."); + add_gitleaks_ignore(&mut detector, gitleaks_ignore_path, source); + + detector.verbose = matches.get_flag("verbose"); + detector.redact = matches.get_one::("redact").cloned().unwrap_or(100); + detector.max_target_megabytes = matches.get_one::("max-target-megabytes").cloned().unwrap_or(0); + detector.ignore_gitleaks_allow = matches.get_flag("ignore-keyshade-allow"); + detector.follow_symlinks = matches.get_flag("follow-symlinks"); + + // Determine the scan type and execute + let no_git = matches.get_flag("no-git"); + let from_pipe = matches.get_flag("pipe"); + + let findings = if no_git { + match DirectoryTargets::new(source, detector.sema.clone(), detector.follow_symlinks) { + Ok(paths) => match detector.detect_files(paths) { + Ok(findings) => findings, + Err(e) => { + error!("Error during file scan: {}", e); + Vec::new() + } + }, + Err(e) => { + error!("Failed to get directory targets: {}", e); + exit(1); + } + } + } else if from_pipe { + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer).unwrap(); + match PipeReader::new(buffer.as_str()) { + Ok(reader) => match detector.detect_reader(reader, 10) { + Ok(findings) => findings, + Err(e) => { + error!("Error during pipe scan: {}", e); + exit(1); + } + }, + Err(e) => { + error!("Error creating pipe reader: {}", e); + exit(1); + } + } + } else { + let log_opts = matches.get_one::("log-opts").cloned().unwrap_or_default(); + match GitLogCmd::new(source, log_opts) { + Ok(git_cmd) => match detector.detect_git(git_cmd) { + Ok(findings) => findings, + Err(e) => { + error!("Error during git scan: {}", e); + Vec::new() + } + }, + Err(e) => { + error!("Failed to get git log command: {}", e); + exit(1); + } + } + }; + + finding_summary_and_exit( + findings, + matches, + detector.config, + start_time, + ); +} + +// Function to run the protect command +pub fn run_protect(matches: &ArgMatches) { + let start_time = Instant::now(); + + // Initialize logging + let no_color = matches.get_flag("no-color"); + init_logging(matches.get_count("verbose") as u64, no_color); + + // Load the configuration + let config = match Config::load_from_file_or_default(matches.get_one::("config")) { + Ok(cfg) => cfg, + Err(e) => { + error!("Failed to load config: {}", e); + exit(1); + } + }; + + let source = matches.get_one::("source").unwrap_or("."); + let detector = Detector::new(config); + let staged = matches.get_flag("staged"); + + let git_cmd = match GitDiffCmd::new(source, staged) { + Ok(cmd) => cmd, + Err(e) => { + error!("Failed to create git diff command: {}", e); + exit(1); + } + }; + + let findings = match detector.detect_git(git_cmd) { + Ok(findings) => findings, + Err(e) => { + error!("Error during git diff scan: {}", e); + Vec::new() + } + }; + + finding_summary_and_exit( + findings, + matches, + detector.config, + start_time, + ); +} + +// Function to handle finding summary and exit code +fn finding_summary_and_exit( + findings: Vec, + matches: &ArgMatches, + config: Config, + start_time: Instant, +) { + let elapsed = start_time.elapsed(); + let summary = FindingSummary::from_findings(&findings); + + if summary.leaks > 0 { + warn!( + "{} leak{} detected in {}. {}", + summary.leaks, + if summary.leaks == 1 { "" } else { "s" }, + format_duration(elapsed), + if summary.errors > 0 { + format!("({} error{} occurred during the scan)", summary.errors, if summary.errors == 1 { "" } else { "s" }) + } else { + "".to_string() + } + ); + } else { + info!( + "No leaks detected in {}. {}", + format_duration(elapsed), + if summary.errors > 0 { + format!("({} error{} occurred during the scan)", summary.errors, if summary.errors == 1 { "" } else { "s" }) + } else { + "".to_string() + } + ); + } + + // Write report if a path is specified + if let Some(report_path) = matches.get_one::("report-path") { + let report_format = matches.get_one::("report-format").unwrap_or("json"); + if let Err(e) = write_report(&findings, &config, report_format, report_path) { + error!("Failed to write report: {}", e); + } + } + + // Exit with appropriate code + let exit_code = matches.get_one::("exit-code").cloned().unwrap_or(1); + if summary.leaks > 0 { + exit(exit_code as i32); + } +} + +// Function to format duration into a human-readable string +fn format_duration(duration: Duration) -> String { + let secs = duration.as_secs(); + if secs < 60 { + return format!("{} seconds", secs); + } + let mins = secs / 60; + if mins < 60 { + return format!("{} minutes {} seconds", mins, secs % 60); + } + let hours = mins / 60; + return format!("{} hours {} minutes {} seconds", hours, mins % 60, secs % 60); +} + +// Helper function to check if a file exists +fn file_exists(file_path: &str) -> bool { + fs::metadata(file_path).is_ok() +} + +// Function to add .gitleaksignore files to the detector +fn add_gitleaks_ignore(detector: &mut Detector, gitleaks_ignore_path: &str, source: &str) { + if file_exists(gitleaks_ignore_path) { + if let Err(e) = detector.add_gitleaks_ignore(gitleaks_ignore_path) { + error!("Failed to add .gitleaksignore: {}", e); + } + } + + let default_paths = [ + filepath::join(gitleaks_ignore_path, ".gitleaksignore"), + filepath::join(source, ".gitleaksignore"), + ]; + + for path in default_paths { + if file_exists(&path) { + if let Err(e) = detector.add_gitleaks_ignore(&path) { + error!("Failed to add .gitleaksignore: {}", e); + } + } + } +} + +// Function to override enabled rules based on user input +fn override_enabled_rules( + mut rules: HashMap, + rule_ids: impl Iterator, +) -> HashMap { + let mut enabled_rules = HashMap::new(); + for rule_id in rule_ids { + if let Some(rule) = rules.remove(&rule_id) { + enabled_rules.insert(rule_id, rule); + } else { + error!("Requested rule {} not found in rules", rule_id); + } + } + enabled_rules +} + +// Main function to execute the CLI application +pub fn execute() { + let matches = Command::new("keyshade") + .version(clap::crate_version!()) + .about("keyshade scans code, past or present, for secrets") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("detect") + .about("Detect secrets in code") + .arg(arg!( "Path to source").default_value(".")) + .arg( + arg!(-c --config "Config file path") + .required(false) + ) + .arg( + arg!(-e --("exit-code") "Exit code when leaks have been encountered") + .default_value("1") + .validator(|s| s.parse::()) + ) + .arg(arg!(-r --("report-path") "Report file").required(false)) + .arg( + arg!(-f --("report-format") "Output format (json, csv, junit, sarif)") + .default_value("json") + .required(false) + ) + .arg(arg!(-b --("baseline-path") "Path to baseline with issues that can be ignored").required(false)) + .arg( + arg!(-l --("log-level") "Log level (trace, debug, info, warn, error, fatal)") + .default_value("info") + .required(false) + ) + .arg(arg!(-v --verbose "Show verbose output from scan").required(false)) + .arg(arg!(--"no-color" "Turn off color for verbose output").required(false)) + .arg( + arg!(--"max-target-megabytes" "Files larger than this will be skipped") + .default_value("0") + .validator(|s| s.parse::()) + .required(false) + ) + .arg(arg!(--"ignore-keyshade-allow" "Ignore keyshade:allow comments").required(false)) + .arg( + arg!(-u --redact "Redact secrets from logs and stdout. To redact only parts of the secret just apply a percent value from 0..100. For example --redact=20 (default 100%)") + .default_value("100") + .validator(|s| s.parse::()) + .required(false) + ) + .arg(arg!(--"no-git" "Treat git repo as a regular directory and scan those files").required(false)) + .arg(arg!(--pipe "Scan input from stdin").required(false)) + .arg(arg!(--"log-opts" "Git log options").required(false)) + .arg( + arg!(-R --("enable-rule") ... "Only enable specific rules by id") + .required(false) + ) + .arg( + arg!(-i --("keyshade-ignore-path") "Path to .gitleaksignore file or folder containing one") + .default_value(".") + .required(false) + ) + .arg(arg!(--"follow-symlinks" "Scan files that are symlinks to other files").required(false)), + ) + .subcommand( + Command::new("protect") + .about("Protect secrets in code") + .arg(arg!( "Path to source").default_value(".")) + .arg( + arg!(-c --config "Config file path") + .required(false) + ) + .arg( + arg!(-e --("exit-code") "Exit code when leaks have been encountered") + .default_value("1") + .validator(|s| s.parse::()) + ) + .arg(arg!(-r --("report-path") "Report file").required(false)) + .arg( + arg!(-f --("report-format") "Output format (json, csv, junit, sarif)") + .default_value("json") + .required(false) + ) + .arg(arg!(-b --("baseline-path") "Path to baseline with issues that can be ignored").required(false)) + .arg( + arg!(-l --("log-level") "Log level (trace, debug, info, warn, error, fatal)") + .default_value("info") + .required(false) + ) + .arg(arg!(-v --verbose "Show verbose output from scan").required(false)) + .arg(arg!(--"no-color" "Turn off color for verbose output").required(false)) + .arg( + arg!(--"max-target-megabytes" "Files larger than this will be skipped") + .default_value("0") + .validator(|s| s.parse::()) + .required(false) + ) + .arg(arg!(--"ignore-keyshade-allow" "Ignore keyshade:allow comments").required(false)) + .arg( + arg!(-u --redact "Redact secrets from logs and stdout. To redact only parts of the secret just apply a percent value from 0..100. For example --redact=20 (default 100%)") + .default_value("100") + .validator(|s| s.parse::()) + .required(false) + ) + .arg(arg!(--staged "Detect secrets in a staged state").required(false)) + .arg( + arg!(-i --("keyshade-ignore-path") "Path to .gitleaksignore file or folder containing one") + .default_value(".") + .required(false) + ) + .arg(arg!(--"follow-symlinks" "Scan files that are symlinks to other files").required(false)), + ) + .get_matches(); + + if let Some(banner) = std::env::var("GITLEAKS_BANNER").ok() { + if banner != "false" { + println!("{}", BANNER); + } + } else { + println!("{}", BANNER); + } + + match matches.subcommand() { + Some(("detect", matches)) => { + run_detect(matches); + } + Some(("protect", matches)) => { + run_protect(matches); + } + _ => unreachable!(), + } +} \ No newline at end of file diff --git a/src/secret-detect/cmd/version.rs b/src/secret-detect/cmd/version.rs new file mode 100644 index 0000000..6fd7afd --- /dev/null +++ b/src/secret-detect/cmd/version.rs @@ -0,0 +1,15 @@ +use clap::{ArgMatches, Command}; + +// Version string, to be set by the build process +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Function to run the version command +pub fn run_version(_matches: &ArgMatches) { + println!("{}", VERSION); +} + +// Function to create the version subcommand +pub fn build_version_command() -> Command<'static> { + Command::new("version") + .about("display keyshade secret scanner version") +} \ No newline at end of file diff --git a/src/secret-detect/config/allowlist.rs b/src/secret-detect/config/allowlist.rs new file mode 100644 index 0000000..7514418 --- /dev/null +++ b/src/secret-detect/config/allowlist.rs @@ -0,0 +1,118 @@ +use regex::Regex; +use std::collections::HashSet; + +pub struct Allowlist { + pub description: String, + pub regexes: Vec, + pub regex_target: Option, + pub paths: Vec, + pub commits: HashSet, + pub stop_words: HashSet, +} + +impl Allowlist { + pub fn commit_allowed(&self, c: &str) -> bool { + self.commits.contains(c) + } + + pub fn path_allowed(&self, path: &str) -> bool { + self.paths.iter().any(|r| r.is_match(path)) + } + + pub fn regex_allowed(&self, s: &str) -> bool { + match &self.regex_target { + Some(target) => self.regexes.iter().any(|r| r.is_match(s) && r.is_match(target)), + None => self.regexes.iter().any(|r| r.is_match(s)), + } + } + + pub fn contains_stop_word(&self, s: &str) -> bool { + let s_lower = s.to_lowercase(); + self.stop_words.iter().any(|stop_word| s_lower.contains(stop_word)) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn commit_allowed() { + let tests = [ + ( + Allowlist { + commits: vec!["commitA".to_string()], + }, + "commitA", + true, + ), + ( + Allowlist { + commits: vec!["commitB".to_string()], + }, + "commitA", + false, + ), + ( + Allowlist { + commits: vec!["commitB".to_string()], + }, + "", + false, + ), + ]; + + for (allowlist, commit, expected) in tests { + assert_eq!(allowlist.commit_allowed(commit), expected); + } + } + + #[test] + fn regex_allowed() { + let tests = [ + ( + Allowlist { + regexes: vec![Regex::new("matchthis").unwrap()], + }, + "a secret: matchthis, done", + true, + ), + ( + Allowlist { + regexes: vec![Regex::new("matchthis").unwrap()], + }, + "a secret", + false, + ), + ]; + + for (allowlist, secret, expected) in tests { + assert_eq!(allowlist.regex_allowed(secret), expected); + } + } + + #[test] + fn path_allowed() { + let tests = [ + ( + Allowlist { + paths: vec![Regex::new("path").unwrap()], + }, + "a path", + true, + ), + ( + Allowlist { + paths: vec![Regex::new("path").unwrap()], + }, + "a ???", + false, + ), + ]; + + for (allowlist, path, expected) in tests { + assert_eq!(allowlist.path_allowed(path), expected); + } + } +} diff --git a/src/secret-detect/config/config.rs b/src/secret-detect/config/config.rs new file mode 100644 index 0000000..203da79 --- /dev/null +++ b/src/secret-detect/config/config.rs @@ -0,0 +1,285 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::fmt; + +#[derive(Debug, Deserialize, Serialize)] +pub struct ViperConfig { + description: String, + extend: Extend, + rules: Vec, + allowlist: Allowlist, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Extend { + path: Option, + url: Option, + use_default: bool, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Rule { + description: String, + rule_id: String, + regex: Option, + path: Option, + secret_group: u32, + entropy: f64, + tags: Vec, + keywords: Vec, + allowlist: Allowlist, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Allowlist { + regex_target: Option, + regexes: Vec, + paths: Vec, + commits: Vec, + stop_words: Vec, +} + +#[derive(Debug)] +pub struct Config { + extend: Extend, + path: String, + description: String, + rules: HashMap, + allowlist: Allowlist, + keywords: Vec, + ordered_rules: Vec, +} + +impl Config { + fn translate(viper_config: ViperConfig) -> Result { + let mut keywords = Vec::new(); + let mut ordered_rules = Vec::new(); + let mut rules_map = HashMap::new(); + + for rule in viper_config.rules { + let allowlist_regexes = rule.allowlist.regexes.iter().map(|r| Regex::new(r.as_str())?) + .collect::, _>>()?; + let allowlist_paths = rule.allowlist.paths.iter().map(|r| Regex::new(r.as_str())?) + .collect::, _>>()?; + + let mut rule_keywords = rule.keywords.iter().map(|k| k.to_lowercase()).collect(); + keywords.append(&mut rule_keywords); + + let config_regex = rule.regex.as_ref().map(|r| Regex::new(r.as_str())?); + let config_path_regex = rule.path.as_ref().map(|r| Regex::new(r.as_str())?); + + let allowlist = Allowlist { + regex_target: rule.allowlist.regex_target, + regexes: allowlist_regexes, + paths: allowlist_paths, + commits: rule.allowlist.commits, + stop_words: rule.allowlist.stop_words, + }; + + if rule.regex.is_some() && rule.secret_group > rule.regex.unwrap().captures_len() { + return Err(format!( + "Invalid regex secret group {} for rule {}, max is {}", + rule.secret_group, rule.description, rule.regex.unwrap().captures_len() + )); + } + + rules_map.insert(rule.rule_id.clone(), Rule { + description: rule.description, + rule_id: rule.rule_id, + regex: config_regex, + path: config_path_regex, + secret_group: rule.secret_group, + entropy: rule.entropy, + tags: rule.tags, + keywords: rule_keywords, + allowlist, + }); + ordered_rules.push(rule.rule_id); + } + + let allowlist_regexes = viper_config.allowlist.regexes.iter().map(|r| Regex::new(r.as_str())?) + .collect::, _>>()?; + let allowlist_paths = viper_config.allowlist.paths.iter().map(|r| Regex::new(r.as_str())?) + .collect::, _>>()?; + + let mut config = Config { + extend: viper_config.extend, + path: String::new(), // Set later + description: viper_config.description, + rules: rules_map, + allowlist: Allowlist { + regex_target: viper_config.allowlist.regex_target, + regexes: allowlist_regexes, + paths: allowlist_paths, + commits: viper_config.allowlist.commits, + stop_words: viper_config.allowlist.stop_words, + }, + keywords, + ordered_rules, + }; + + if extend_depth != max_extend_depth { + if config.extend.path.is_some() && config.extend.use_default { + return Err("Using both extend.path and extend.useDefault is not allowed".to_string()); + } + + if config.extend.use_default { + config.extend_default()?; + } else if let Some(path) = &config.extend.path { + config.extend_path(path)?; + } + } + + Ok(config) + } + + fn get_ordered_rules(&self) -> Vec<&Rule> { + self.ordered_rules.iter().filter_map(|id| self.rules.get(id)).collect() + } + + fn extend_default(&mut self) -> Result<(), String> { + extend_depth += 1; + // Use serde to deserialize the embedded config + let default_config: Result = + serde_json::from_str(&DefaultConfig); + + match default_config { + Ok(viper_config) => { + let extension_config = Config::translate(viper_config)?; + log::debug!("Extending config with default config"); + self.extend(extension_config); + Ok(()) + } + Err(err) => Err(format!("Failed to load extended config: {}", err)), + } + } + + fn extend_path(&mut self, path: &str) -> Result<(), String> { + extend_depth += 1; + // Use serde to deserialize the config file + let extension_config: Result = + serde_json::from_str(&std::fs::read_to_string(path)?); + + match extension_config { + Ok(viper_config) => { + let extension_config = Config::translate(viper_config)?; + log::debug!("Extending config with {}", path); + self.extend(extension_config); + Ok(()) + } + Err(err) => Err(format!("Failed to load extended config: {}", err)), + } + } + + fn extend(&mut self, extension_config: Config) { + for (rule_id, rule) in extension_config.rules { + if !self.rules.contains_key(&rule_id) { + log::trace!("Adding {} to base config", rule_id); + self.rules.insert(rule_id.clone(), rule); + self.keywords.extend(rule.keywords.iter().cloned()); + self.ordered_rules.push(rule_id); + } + } + + // Append allowlists without merging + self.allowlist.commits.extend(extension_config.allowlist.commits.iter().cloned()); + self.allowlist.paths.extend(extension_config.allowlist.paths.iter().cloned()); + self.allowlist.regexes.extend(extension_config.allowlist.regexes.iter().cloned()); + + // Sort ordered rules for consistency + self.ordered_rules.sort(); + } +} + +#[derive(Debug)] +struct ConfigError { + message: String, +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl Error for ConfigError {} + +// Structure representing a rule +struct Rule { + description: String, + regex: Regex, + tags: Vec, + keywords: Vec, + rule_id: String, + allowlist: Allowlist, + entropy: Option, + secret_group: Option, +} + +// Structure representing an allowlist +struct Allowlist { + regexes: Vec, + commits: Vec, + paths: Vec, +} + +// Structure representing the configuration +struct Config { + rules: HashMap, +} + +// Function to convert Viper configuration to Rust configuration +fn translate(viper_config: &ViperConfig) -> Result { + let mut rules = HashMap::new(); + for (rule_id, rule_data) in &viper_config.rules { + let regex = Regex::new(&rule_data.regex).map_err(|e| ConfigError { + message: format!("Invalid regex for rule '{}': {}", rule_id, e), + })?; + + let allowlist = Allowlist { + regexes: rule_data + .allowlist + .regexes + .iter() + .map(|r| Regex::new(r).unwrap()) + .collect(), + commits: rule_data.allowlist.commits.clone(), + paths: rule_data + .allowlist + .paths + .iter() + .map(|r| Regex::new(r).unwrap()) + .collect(), + }; + + // Validate secret group + if let Some(group) = rule_data.secret_group { + if group > 3 { + return Err(ConfigError { + message: format!( + "{} invalid regex secret group {}, max regex secret group 3", + rule_data.description, group + ), + }); + } + } + + rules.insert( + rule_id.to_string(), + Rule { + description: rule_data.description.clone(), + regex, + tags: rule_data.tags.clone(), + keywords: rule_data.keywords.clone(), + rule_id: rule_id.to_string(), + allowlist, + entropy: rule_data.entropy, + secret_group: rule_data.secret_group, + }, + ); + } + + Ok(Config { rules }) +} diff --git a/src/secret-detect/config/mod.rs b/src/secret-detect/config/mod.rs new file mode 100644 index 0000000..c383923 --- /dev/null +++ b/src/secret-detect/config/mod.rs @@ -0,0 +1,4 @@ +pub mod allowlist; +pub mod config; +pub mod rule; +pub mod secret; \ No newline at end of file diff --git a/src/secret-detect/config/rule.rs b/src/secret-detect/config/rule.rs new file mode 100644 index 0000000..24baa5d --- /dev/null +++ b/src/secret-detect/config/rule.rs @@ -0,0 +1,14 @@ +use regex::Regex; + +#[derive(Debug)] +pub struct Rule { + pub description: String, + pub rule_id: String, + pub entropy: f64, + pub secret_group: u32, + pub regex: Option, + pub path: Option, + pub tags: Vec, + pub keywords: Vec, + pub allowlist: Allowlist, +} diff --git a/src/secret-detect/config/utils.rs b/src/secret-detect/config/utils.rs new file mode 100644 index 0000000..1d38f54 --- /dev/null +++ b/src/secret-detect/config/utils.rs @@ -0,0 +1,18 @@ +use regex::Regex; + +fn any_regex_match(f: &str, res: &[&Regex]) -> bool { + for re in res { + if re.is_match(f) { + return true; + } + } + false +} + +fn regex_matched(f: &str, re: &Option<&Regex>) -> bool { + if let Some(re) = re { + re.is_match(f) + } else { + false + } +} diff --git a/src/secret-detect/detect/baseline.rs b/src/secret-detect/detect/baseline.rs new file mode 100644 index 0000000..e4d40af --- /dev/null +++ b/src/secret-detect/detect/baseline.rs @@ -0,0 +1,39 @@ +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; + +use log::warn; +use serde_json::from_slice; + +use crate::report::Finding; + +pub fn is_new(finding: &Finding, baseline: &HashSet) -> bool { + !baseline.contains(finding) +} + +pub fn load_baseline(baseline_path: &Path) -> Result, HashSet, serde_json::Error> { + let bytes = fs::read(baseline_path)?; + let findings: Vec = from_slice(&bytes)?; + Ok(findings.into_iter().collect()) +} + +pub fn add_baseline(detector: &mut Detector, baseline_path: &str, source: &str) -> Result<(), Box> { + if !baseline_path.is_empty() { + let absolute_source = PathBuf::from(source).canonicalize()?; + let absolute_baseline = PathBuf::from(baseline_path).canonicalize()?; + + let relative_baseline = match absolute_baseline.strip_prefix(&absolute_source) { + Ok(path) => path.to_string_lossy().to_string(), + Err(_) => { + warn!("Baseline path is not within the source directory. Ignoring baseline."); + return Ok(()); + } + }; + + let baseline = load_baseline(&absolute_baseline)?; + detector.baseline = Some(baseline); + detector.baseline_path = Some(relative_baseline); + } + + Ok(()) +} \ No newline at end of file diff --git a/src/secret-detect/report/csv.rs b/src/secret-detect/report/csv.rs new file mode 100644 index 0000000..2899595 --- /dev/null +++ b/src/secret-detect/report/csv.rs @@ -0,0 +1,112 @@ +use std::fs::{self, File}; +use std::io::Write; + +use csv::Writer; +use tempfile::tempdir; + +use crate::report::Finding; + +// Function to write findings to a CSV file +pub fn write_csv(findings: &[Finding], writer: &mut impl Write) -> Result<(), csv::Error> { + if findings.is_empty() { + return Ok(()); + } + + let mut csv_writer = Writer::from_writer(writer); + + // Write CSV header + csv_writer.write_record(&[ + "RuleID", + "Commit", + "File", + "SymlinkFile", + "Secret", + "Match", + "StartLine", + "EndLine", + "StartColumn", + "EndColumn", + "Author", + "Message", + "Date", + "Email", + "Fingerprint", + "Tags", + ])?; + + // Write findings data + for finding in findings { + csv_writer.write_record(&[ + &finding.rule_id, + &finding.commit, + &finding.file, + &finding.symlink_file, + &finding.secret, + &finding.match_str, + &finding.start_line.to_string(), + &finding.end_line.to_string(), + &finding.start_column.to_string(), + &finding.end_column.to_string(), + &finding.author, + &finding.message, + &finding.date, + &finding.email, + &finding.fingerprint, + &finding.tags.join(" "), + ])?; + } + + // Flush the writer + csv_writer.flush()?; + Ok(()) +} + +#[test] +fn test_write_csv() { + let tests = vec![ + ( + "simple", + vec![Finding { + rule_id: "test-rule".to_string(), + match_str: "line containing secret".to_string(), + secret: "a secret".to_string(), + start_line: 1, + end_line: 2, + start_column: 1, + end_column: 2, + message: "opps".to_string(), + file: "auth.py".to_string(), + symlink_file: "".to_string(), + commit: "0000000000000000".to_string(), + author: "John Doe".to_string(), + email: "johndoe@gmail.com".to_string(), + date: "10-19-2003".to_string(), + fingerprint: "fingerprint".to_string(), + tags: vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()], + }], + "tests/fixtures/csv_simple.csv", + false, + ), + ("empty", vec![], "", true), + ]; + + for (test_name, findings, expected_path, want_empty) in tests { + let dir = tempdir().unwrap(); + let file_path = dir.path().join(format!("{}.csv", test_name)); + let mut file = File::create(&file_path).unwrap(); + + let result = write_csv(&findings, &mut file); + assert!(result.is_ok()); + + assert!(file_path.exists()); + + let got = fs::read_to_string(&file_path).unwrap(); + + if want_empty { + assert!(got.is_empty()); + } else { + let want = fs::read_to_string(expected_path).unwrap(); + assert_eq!(want, got); + } + } +} \ No newline at end of file diff --git a/src/secret-detect/report/finding.rs b/src/secret-detect/report/finding.rs new file mode 100644 index 0000000..105ecca --- /dev/null +++ b/src/secret-detect/report/finding.rs @@ -0,0 +1,156 @@ +use std::fmt; + +// Structure to represent a finding +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct Finding { + pub description: String, + pub start_line: usize, + pub end_line: usize, + pub start_column: usize, + pub end_column: usize, + pub line: String, + pub match_str: String, + pub secret: String, + pub file: String, + pub symlink_file: String, + pub commit: String, + pub entropy: f32, + pub author: String, + pub email: String, + pub date: String, + pub message: String, + pub tags: Vec, + pub rule_id: String, + pub fingerprint: String, +} + +impl Finding { + // Method to redact sensitive information from a finding + pub fn redact(&mut self, percent: u32) { + let secret = mask_secret(&self.secret, percent); + let redacted_secret = if percent >= 100 { + "REDACTED".to_string() + } else { + secret + }; + + self.line = self.line.replace(&self.secret, &redacted_secret); + self.match_str = self.match_str.replace(&self.secret, &redacted_secret); + self.secret = redacted_secret; + } +} + +// Function to mask a secret based on redaction percentage +fn mask_secret(secret: &str, percent: u32) -> String { + let percent = percent.min(100); + let length = secret.len() as f64; + if length <= 0.0 { + return secret.to_string(); + } + + let prc = (100 - percent) as f64; + let lth = (length * prc / 100.0).round() as usize; + + format!("{} +...", &secret[..lth]) +} + +#[test] +fn test_redact() { + let tests = vec![ + ( + true, + vec![Finding { + match_str: "line containing secret".to_string(), + secret: "secret".to_string(), + ..Default::default() + }], + ), + ]; + + for (redact, findings) in tests { + for mut finding in findings { + if redact { + finding.redact(100); + assert_eq!("REDACTED", finding.secret); + assert_eq!("line containing REDACTED", finding.match_str); + } + } + } +} + +#[test] +fn test_mask() { + let tests = vec![ + ( + "normal secret", + Finding { + match_str: "line containing secret".to_string(), + secret: "secret".to_string(), + ..Default::default() + }, + 75, + Finding { + match_str: "line containing se...".to_string(), + secret: "se...".to_string(), + ..Default::default() + }, + ), + ( + "empty secret", + Finding { + match_str: "line containing".to_string(), + secret: "".to_string(), + ..Default::default() + }, + 75, + Finding { + match_str: "line containing".to_string(), + secret: "".to_string(), + ..Default::default() + }, + ), + ( + "short secret", + Finding { + match_str: "line containing".to_string(), + secret: "ss".to_string(), + ..Default::default() + }, + 75, + Finding { + match_str: "line containing".to_string(), + secret: "...".to_string(), + ..Default::default() + }, + ), + ]; + + for (name, mut finding, percent, expected) in tests { + finding.redact(percent); + assert_eq!( + expected, finding, + "Test case '{}' failed. Expected: {:?}, Got: {:?}", + name, expected, finding + ); + } +} + +#[test] +fn test_mask_secret() { + let tests = vec![ + ("normal masking", "secret", 75, "se..."), + ("high masking", "secret", 90, "s..."), + ("low masking", "secret", 10, "secre..."), + ("invalid masking", "secret", 1000, "..."), + ]; + + for (name, secret, percent, expected) in tests { + let got = Finding::mask_secret(secret, percent); + assert_eq!( + expected, got, + "Test case '{}' failed. Expected: {}, Got: {}", + name, expected, got + ); + } +} \ No newline at end of file diff --git a/src/secret-detect/report/json.rs b/src/secret-detect/report/json.rs new file mode 100644 index 0000000..997f8c7 --- /dev/null +++ b/src/secret-detect/report/json.rs @@ -0,0 +1,70 @@ +use std::fs::{self, File}; +use std::io::Write; + +use serde_json::{ser::PrettyFormatter, to_writer_pretty}; +use tempfile::tempdir; + +use crate::report::Finding; + +// Function to write findings to a JSON file +pub fn write_json(findings: &[Finding], writer: &mut impl Write) -> Result<(), serde_json::Error> { + if findings.is_empty() { + // Serialize an empty array if no findings + to_writer_pretty(writer, &Vec::::new()) + } else { + // Serialize with indentation + let formatter = PrettyFormatter::with_indent(b" "); + to_writer_pretty(writer, findings) + } +} + +#[test] +fn test_write_json() { + let tests = vec![ + ( + "simple", + vec![Finding { + description: "".to_string(), + rule_id: "test-rule".to_string(), + match_str: "line containing secret".to_string(), + secret: "a secret".to_string(), + start_line: 1, + end_line: 2, + start_column: 1, + end_column: 2, + message: "opps".to_string(), + file: "auth.py".to_string(), + symlink_file: "".to_string(), + commit: "0000000000000000".to_string(), + author: "John Doe".to_string(), + email: "johndoe@gmail.com".to_string(), + date: "10-19-2003".to_string(), + tags: vec![], + ..Default::default() + }], + "tests/fixtures/json_simple.json", + false, + ), + ("empty", vec![], "tests/fixtures/empty.json", false), + ]; + + for (test_name, findings, expected_path, want_empty) in tests { + let dir = tempdir().unwrap(); + let file_path = dir.path().join(format!("{}.json", test_name)); + let mut file = File::create(&file_path).unwrap(); + + let result = write_json(&findings, &mut file); + assert!(result.is_ok()); + + assert!(file_path.exists()); + + let got = fs::read_to_string(&file_path).unwrap(); + + if want_empty { + assert!(got.is_empty()); + } else { + let want = fs::read_to_string(expected_path).unwrap(); + assert_eq!(want, got); + } + } +} \ No newline at end of file diff --git a/src/secret-detect/report/junit.rs b/src/secret-detect/report/junit.rs new file mode 100644 index 0000000..c929453 --- /dev/null +++ b/src/secret-detect/report/junit.rs @@ -0,0 +1,148 @@ +use std::fs::{self, File}; +use std::io::Write; + +use quick_xml::{ + events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}, + Writer, +}; +use serde_json::to_string_pretty; +use tempfile::tempdir; + +use crate::report::Finding; + +// Function to write findings to a JUnit XML file +pub fn write_junit(findings: &[Finding], writer: &mut impl Write) -> Result<(), quick_xml::Error> { + let mut xml_writer = Writer::new_with_indent(writer, b' ', 4); + + // Write XML declaration + xml_writer.write_event(Event::Decl(BytesDecl::new(b"1.0", Some(b"UTF-8"), None)))?; + + // Start "testsuites" element + let mut testsuites_start = BytesStart::owned_name(b"testsuites"); + xml_writer.write_event(Event::Start(testsuites_start))?; + + // Write "testsuite" element + let mut testsuite_start = BytesStart::owned_name(b"testsuite"); + testsuite_start.push_attribute(("failures", findings.len().to_string().as_str())); + testsuite_start.push_attribute(("name", "keyshade secret-detect")); + testsuite_start.push_attribute(("tests", findings.len().to_string().as_str())); + xml_writer.write_event(Event::Start(testsuite_start))?; + + // Write "testcase" elements for each finding + for finding in findings { + let mut testcase_start = BytesStart::owned_name(b"testcase"); + testcase_start.push_attribute(("classname", &finding.description)); + testcase_start.push_attribute(("file", &finding.file)); + testcase_start.push_attribute(("name", &get_message(finding))); + xml_writer.write_event(Event::Start(testcase_start))?; + + // Write "failure" element within "testcase" + let mut failure_start = BytesStart::owned_name(b"failure"); + failure_start.push_attribute(("message", &get_message(finding))); + failure_start.push_attribute(("type", &finding.description)); + xml_writer.write_event(Event::Start(failure_start))?; + + // Write finding data as CDATA within "failure" + let data = to_string_pretty(finding).unwrap(); + xml_writer.write_event(Event::CData(data.into()))?; + + // Close "failure" element + xml_writer.write_event(Event::End(BytesEnd::borrowed(b"failure")))?; + + // Close "testcase" element + xml_writer.write_event(Event::End(BytesEnd::borrowed(b"testcase")))?; + } + + // Close "testsuite" element + xml_writer.write_event(Event::End(BytesEnd::borrowed(b"testsuite")))?; + + // Close "testsuites" element + xml_writer.write_event(Event::End(BytesEnd::borrowed(b"testsuites")))?; + + Ok(()) +} + +// Helper function to generate the message for a finding +fn get_message(finding: &Finding) -> String { + if finding.commit.is_empty() { + format!( + "{} has detected a secret in file {}, line {}.", + finding.rule_id, finding.file, finding.start_line + ) + } else { + format!( + "{} has detected a secret in file {}, line {}, at commit {}.", + finding.rule_id, finding.file, finding.start_line, finding.commit + ) + } +} + +#[test] +fn test_write_junit() { + let tests = vec![ + ( + "simple", + vec![ + Finding { + description: "Test Rule".to_string(), + rule_id: "test-rule".to_string(), + match_str: "line containing secret".to_string(), + secret: "a secret".to_string(), + start_line: 1, + end_line: 2, + start_column: 1, + end_column: 2, + message: "opps".to_string(), + file: "auth.py".to_string(), + commit: "0000000000000000".to_string(), + author: "John Doe".to_string(), + email: "johndoe@gmail.com".to_string(), + date: "10-19-2003".to_string(), + tags: vec![], + ..Default::default() + }, + Finding { + description: "Test Rule".to_string(), + rule_id: "test-rule".to_string(), + match_str: "line containing secret".to_string(), + secret: "a secret".to_string(), + start_line: 2, + end_line: 3, + start_column: 1, + end_column: 2, + message: "".to_string(), + file: "auth.py".to_string(), + commit: "".to_string(), + author: "".to_string(), + email: "".to_string(), + date: "".to_string(), + tags: vec![], + ..Default::default() + }, + ], + "tests/fixtures/junit_simple.xml", + false, + ), + ("empty", vec![], "tests/fixtures/junit_empty.xml", false), + ]; + + for (test_name, findings, expected_path, want_empty) in tests { + let dir = tempdir().unwrap(); + let file_path = dir.path().join(format!("{}.xml", test_name)); + let mut file = File::create(&file_path).unwrap(); + + let result = write_junit(&findings, &mut file); + assert!(result.is_ok()); + + assert!(file_path.exists()); + + let got = fs::read_to_string(&file_path).unwrap(); + + if want_empty { + assert!(got.is_empty()); + } else { + let want = fs::read_to_string(expected_path).unwrap(); + assert_eq!(want, got); + } + } +} \ No newline at end of file diff --git a/src/secret-detect/report/report.rs b/src/secret-detect/report/report.rs new file mode 100644 index 0000000..0a39e08 --- /dev/null +++ b/src/secret-detect/report/report.rs @@ -0,0 +1,122 @@ +use std::{fs::{self,File}, io::Write, path::Path}; + +use tempfile::tempdir; + +use crate::config::Config; +use crate::report::{Finding, write_csv, write_json, write_junit, write_report}; + +pub const CWE: &str = "CWE-798"; +pub const CWE_DESCRIPTION: &str = "Use of Hard-coded Credentials"; + +// Function to write the report in the specified format +pub fn write_report( + findings: &[Finding], + config: &Config, + extension: &str, + report_path: &Path, +) -> Result<(), Box> { + let mut file = File::create(report_path)?; + let ext = extension.to_lowercase(); + + match ext.as_str() { + "json" | ".json" => write_json(findings, &mut file), + "csv" | ".csv" => write_csv(findings, &mut file), + "xml" | "junit" | ".xml" => write_junit(findings, &mut file), + "sarif" | ".sarif" => write_sarif(config, findings, &mut file), + _ => Err(format!("Unsupported report format: {}", extension).into()), + } +} + + +#[test] +fn test_write_report() { + let tests = vec![ + ( + "json", + vec![Finding { + rule_id: "test-rule".to_string(), + ..Default::default() + }], + false, + ), + ( + ".json", + vec![Finding { + rule_id: "test-rule".to_string(), + ..Default::default() + }], + false, + ), + ( + ".jsonj", + vec![Finding { + rule_id: "test-rule".to_string(), + ..Default::default() + }], + true, + ), + ( + ".csv", + vec![Finding { + rule_id: "test-rule".to_string(), + ..Default::default() + }], + false, + ), + ( + "csv", + vec![Finding { + rule_id: "test-rule".to_string(), + ..Default::default() + }], + false, + ), + ( + "CSV", + vec![Finding { + rule_id: "test-rule".to_string(), + ..Default::default() + }], + false, + ), + ( + ".xml", + vec![Finding { + rule_id: "test-rule".to_string(), + ..Default::default() + }], + false, + ), + ( + "junit", + vec![Finding { + rule_id: "test-rule".to_string(), + ..Default::default() + }], + false, + ), + ]; + + for (i, (extension, findings, want_empty)) in tests.iter().enumerate() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join(format!("{}{}", i, extension)); + + let result = write_report( + &findings, + &Config::default(), + extension, + &file_path, + ); + assert!(result.is_ok()); + + assert!(file_path.exists()); + + let got = fs::read_to_string(&file_path).unwrap(); + + if *want_empty { + assert!(got.is_empty()); + } else { + assert!(!got.is_empty()); + } + } +} \ No newline at end of file diff --git a/src/secret-detect/report/sarif.rs b/src/secret-detect/report/sarif.rs new file mode 100644 index 0000000..85856b3 --- /dev/null +++ b/src/secret-detect/report/sarif.rs @@ -0,0 +1,317 @@ +use std::fs::{self, File}; +use std::io::Write; + +use serde::Serialize; +use serde_json::{json, to_writer_pretty}; +use tempfile::tempdir; + +use crate::config::{Config, Rule}; +use crate::report::Finding; + +// Constants for SARIF report +const SCHEMA: &str = "https://json.schemastore.org/sarif-2.1.0.json"; +const VERSION: &str = "1.0.0"; +const DRIVER: &str = "keyshade secret-detect"; + +// Structs representing the SARIF report structure +#[derive(Serialize)] +pub struct Sarif { + #[serde(rename = "$schema")] + pub schema: String, + pub version: String, + pub runs: Vec +} + +#[derive(Serialize, Clone, Default)] +pub struct Runs { + pub tool: Tool, + pub results: Vec, +} + +#[derive(Serialize, Clone, Default)] +pub struct Tool { + pub driver: Driver, +} + +#[derive(Serialize, Clone, Default)] +pub struct Driver { + pub name: String, + pub semantic_version: String, + pub information_uri: String, + pub rules: Vec, +} + +#[derive(Serialize, Clone, Default)] +pub struct Rules { + pub id: String, + pub name: String, + pub short_description: ShortDescription, + pub full_description: Option, +} + +#[derive(Serialize, Clone, Default)] +pub struct ShortDescription { + pub text: String, +} + +#[derive(Serialize, Clone, Default)] +pub struct FullDescription { + pub text: String, +} + +#[derive(Serialize, Clone, Default)] +pub struct Message { + pub text: String, +} + +#[derive(Serialize, Clone, Default)] +pub struct ArtifactLocation { + pub uri: String, + pub uri_base_id: Option, + pub index: Option, +} + +#[derive(Serialize, Clone, Default)] +pub struct Region { + pub start_line: usize, + pub start_column: usize, + pub end_line: usize, + pub end_column: usize, + pub char_offset: Option, + pub char_length: Option, + pub byte_offset: Option, + pub byte_length: Option, + pub snippet: Snippet, +} + +#[derive(Serialize, Clone, Default)] +pub struct Snippet { + pub text: String, +} + +#[derive(Serialize, Clone, Default)] +pub struct PhysicalLocation { + pub artifact_location: ArtifactLocation, + pub region: Region, + pub context_region: Option, +} + +#[derive(Serialize, Clone, Default)] +pub struct Locations { + pub physical_location: PhysicalLocation, + pub logical_locations: Option>, + pub message: Option, + pub annotations: Option>, +} + +#[derive(Serialize, Clone, Default)] +pub struct LogicalLocations { + pub kind: Option, + pub name: Option, + pub fully_qualified_name: Option, + pub decorated_name: Option, + pub index: Option, + pub parent_index: Option, +} + +#[derive(Serialize, Clone, Default)] +pub struct Annotation { + pub location: Option, + pub message: Message, + pub properties: Option, +} + +#[derive(Serialize, Clone, Default)] +pub struct PartialFingerprints { + pub commit_sha: String, + pub email: String, + pub author: String, + pub date: String, + pub commit_message: String, +} + +#[derive(Serialize, Clone, Default)] +pub struct Properties { + pub tags: Vec, + #[serde(flatten)] + pub additional_properties: HashMap, +} + +#[derive(Serialize, Clone, Default)] +pub struct Results { + pub rule_id: String, + pub rule_index: Option, + pub message: Message, + pub locations: Vec, + pub partial_fingerprints: PartialFingerprints, + pub properties: Properties, + pub taxa: Option>, +} + +#[derive(Serialize, Clone, Default)] +pub struct Taxa { + pub tool_component: ToolComponent, +} + +#[derive(Serialize, Clone, Default)] +pub struct ToolComponent { + pub name: String, + pub index: Option, + pub guid: Option, + pub properties: Option, +} + + +// Function to write findings to a SARIF JSON file +pub fn write_sarif( + config: &Config, + findings: &[Finding], + writer: &mut impl Write, +) -> Result<(), serde_json::Error> { + // Build SARIF report structure + let sarif_report = Sarif { + schema: SCHEMA.to_string(), + version: VERSION.to_string(), + runs: vec![Runs { + tool: Tool { + driver: Driver { + name: DRIVER.to_string(), + semantic_version: env!("CARGO_PKG_VERSION").to_string(), // Get version from Cargo + information_uri: "https://github.com/zricethezav/gitleaks".to_string(), + rules: config.rules.values().cloned().map(get_rule).collect(), + }, + }, + results: findings.iter().map(get_result).collect(), + }], + }; + + // Write SARIF report to the writer + to_writer_pretty(writer, &sarif_report) +} + +// Helper function to convert a `Rule` to a `Rules` struct for SARIF +fn get_rule(rule: &Rule) -> Rules { + Rules { + id: rule.rule_id.clone(), + name: rule.description.clone(), + short_description: ShortDescription { + text: rule + .regex + .as_ref() + .map(|r| r.to_string()) + .unwrap_or_else(|| "Custom Rule".to_string()), // Placeholder for custom rules + }, + full_description: None, // Not used in this example + } +} + +// Helper function to convert a `Finding` to a `Results` struct for SARIF +fn get_result(finding: &Finding) -> Results { + Results { + message: Message { + text: get_message_text(finding), + }, + rule_id: finding.rule_id.clone(), + locations: vec![Locations { + physical_location: PhysicalLocation { + artifact_location: ArtifactLocation { + uri: if !finding.symlink_file.is_empty() { + finding.symlink_file.clone() + } else { + finding.file.clone() + }, + ..Default::default() + }, + region: Region { + start_line: finding.start_line, + start_column: finding.start_column, + end_line: finding.end_line, + end_column: finding.end_column, + snippet: Snippet { + text: finding.secret.clone(), + }, + ..Default::default() + }, + }, + ..Default::default() + }], + partial_fingerprints: PartialFingerprints { + commit_sha: finding.commit.clone(), + email: finding.email.clone(), + author: finding.author.clone(), + date: finding.date.clone(), + commit_message: finding.message.clone(), + }, + properties: Properties { + tags: finding.tags.clone(), + ..Default::default() + }, + ..Default::default() + } +} + +// Helper function to generate the message text for a SARIF result +fn get_message_text(finding: &Finding) -> String { + if finding.commit.is_empty() { + format!( + "{} has detected secret for file {}.", + finding.rule_id, finding.file + ) + } else { + format!( + "{} has detected secret for file {} at commit {}.", + finding.rule_id, finding.file, finding.commit + ) + } +} + + +#[test] +fn test_write_sarif() { + let tests = vec![( + "simple", + vec![Finding { + description: "A test rule".to_string(), + rule_id: "test-rule".to_string(), + match_str: "line containing secret".to_string(), + secret: "a secret".to_string(), + start_line: 1, + end_line: 2, + start_column: 1, + end_column: 2, + message: "opps".to_string(), + file: "auth.py".to_string(), + commit: "0000000000000000".to_string(), + author: "John Doe".to_string(), + email: "johndoe@gmail.com".to_string(), + date: "10-19-2003".to_string(), + tags: vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()], + ..Default::default() + }], + "tests/fixtures/sarif_simple.sarif", + false, + )]; + + for (config_name, findings, expected_path, want_empty) in tests { + let dir = tempdir().unwrap(); + let file_path = dir.path().join(format!("{}.json", config_name)); + let mut file = File::create(&file_path).unwrap(); + + // Load config + let config = Config::load_from_file(&format!("tests/fixtures/{}.toml", config_name)).unwrap(); + + let result = write_sarif(&config, &findings, &mut file); + assert!(result.is_ok()); + + assert!(file_path.exists()); + + let got = fs::read_to_string(&file_path).unwrap(); + + if want_empty { + assert!(got.is_empty()); + } else { + let want = fs::read_to_string(expected_path).unwrap(); + assert_eq!(want, got); + } + } +} \ No newline at end of file