Skip to content

Commit

Permalink
feat(wip): add temp option for entropy_threshold, doesn't work right now
Browse files Browse the repository at this point in the history
  • Loading branch information
SaadBazaz committed Oct 2, 2024
1 parent 02fb62d commit 70901ff
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 13 deletions.
6 changes: 5 additions & 1 deletion crates/biome_analyze/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ impl std::fmt::Display for RuleSource {
Self::EslintBarrelFiles(_) => write!(f, "eslint-plugin-barrel-files"),
Self::EslintN(_) => write!(f, "eslint-plugin-n"),
Self::Stylelint(_) => write!(f, "Stylelint"),
Self::EslintNoSecrets(_) => write!(f, "eslint-plugin-no-secrets"),
}
}
}
Expand Down Expand Up @@ -209,7 +210,8 @@ impl RuleSource {
| Self::EslintMysticatea(rule_name)
| Self::EslintBarrelFiles(rule_name)
| Self::EslintN(rule_name)
| Self::Stylelint(rule_name) => rule_name,
| Self::Stylelint(rule_name)
| Self::EslintNoSecrets(rule_name) => rule_name
}
}

Expand All @@ -234,6 +236,7 @@ impl RuleSource {
Self::EslintBarrelFiles(rule_name) => format!("barrel-files/{rule_name}"),
Self::EslintN(rule_name) => format!("n/{rule_name}"),
Self::Stylelint(rule_name) => format!("stylelint/{rule_name}"),
Self::EslintNoSecrets(rule_name) => format!("no-secrets/{rule_name}"),
}
}

Expand All @@ -259,6 +262,7 @@ impl RuleSource {
Self::EslintBarrelFiles(rule_name) => format!("https://github.com/thepassle/eslint-plugin-barrel-files/blob/main/docs/rules/{rule_name}.md"),
Self::EslintN(rule_name) => format!("https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/{rule_name}.md"),
Self::Stylelint(rule_name) => format!("https://github.com/stylelint/stylelint/blob/main/lib/rules/{rule_name}/README.md"),
Self::EslintNoSecrets(_rule_name) => format!("https://github.com/nickdeis/eslint-plugin-no-secrets/README.md"),
}
}

Expand Down
107 changes: 96 additions & 11 deletions crates/biome_js_analyze/src/lint/nursery/no_secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,27 @@ use regex::Regex;

use std::sync::LazyLock;

use biome_deserialize_macros::Deserializable;
use serde::{Deserialize, Serialize};

// TODO: Try to get this to work in JavaScript comments as well
declare_lint_rule! {
/// Disallow usage of sensitive data such as API keys and tokens.
///
/// This rule checks for high-entropy strings and matches common patterns
/// for secrets, such as AWS keys, Slack tokens, and private keys.
/// for secrets, including AWS keys, Slack tokens, and private keys.
/// It aims to help users identify immediate potential secret leaks in their codebase,
/// especially for those who may not be aware of the risks associated with
/// sensitive data exposure.
///
/// While this rule is helpful, it's not infallible. Always review your code carefully and consider implementing additional security measures like automated secret scanning in your CI/CD and git pipeline, such as GitGuardian or GitHub protections.
/// While this rule is beneficial for catching the most egregious cases,
/// it is not infallible and may yield false positives. Therefore, always
/// review your code carefully and consider implementing additional security
/// measures, such as automated secret scanning in your CI/CD and git pipeline.
/// Some recommended tools for more comprehensive secret detection include:
/// - [Gitleaks](https://github.com/gitleaks/gitleaks/): A mature secret scanning tool.
/// - [Trufflehog](https://github.com/trufflesecurity/trufflehog): A tool for finding secrets in git history.
/// - [Sensleak](https://github.com/crates-pro/sensleak-rs): A Rust-based solution for secret detection.
///
/// ## Examples
///
Expand All @@ -32,6 +45,11 @@ declare_lint_rule! {
/// ```js
/// const nonSecret = "hello world";
/// ```
///
/// ## Disclaimer
/// This rule is intended to catch obvious secret leaks, but for more robust detection
/// across different languages and scenarios, we encourage users to explore the dedicated
/// tools mentioned above.
pub NoSecrets {
version: "1.9.0",
name: "noSecrets",
Expand All @@ -46,7 +64,7 @@ impl Rule for NoSecrets {
type Query = Ast<JsStringLiteralExpression>;
type State = &'static str;
type Signals = Option<Self::State>;
type Options = ();
type Options = NoSecretsOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
Expand All @@ -57,11 +75,17 @@ impl Rule for NoSecrets {
return None;
}

let hasSpaces = text.contains(' ');

for sensitive_pattern in SENSITIVE_PATTERNS.iter() {
if text.len() < sensitive_pattern.min_len {
continue;
}

if hasSpaces && !sensitive_pattern.allows_spaces {
continue;
}

let matched = match &sensitive_pattern.pattern {
Pattern::Regex(re) => re.is_match(text),
Pattern::Contains(substring) => text.contains(substring),
Expand All @@ -72,8 +96,8 @@ impl Rule for NoSecrets {
}
}

if is_high_entropy(text) {
Some("The string has a high entropy value")
if !hasSpaces {
return detect_secret(ctx, text)
} else {
None
}
Expand All @@ -98,10 +122,17 @@ impl Rule for NoSecrets {
}
}

const HIGH_ENTROPY_THRESHOLD: f64 = 4.5;
#[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NoSecretsOptions {
/// Set entropy threshold (default is 4.5).
entropy_threshold: f64, // @TODO: Doesn't work currently.
}

const DEFAULT_HIGH_ENTROPY_THRESHOLD: f64 = 4.5;

// Workaround: Since I couldn't figure out how to declare them inline,
// declare the LazyLock patterns separately
// Known sensitive patterns start here
static SLACK_TOKEN_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"xox[baprs]-([0-9a-zA-Z]{10,48})?").unwrap());

Expand Down Expand Up @@ -155,96 +186,150 @@ struct SensitivePattern {
pattern: Pattern,
comment: &'static str,
min_len: usize,
allows_spaces: bool,
}

static SENSITIVE_PATTERNS: &[SensitivePattern] = &[
SensitivePattern {
pattern: Pattern::Regex(&SLACK_TOKEN_REGEX),
comment: "Slack Token",
min_len: 32,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&SLACK_WEBHOOK_REGEX),
comment: "Slack Webhook",
min_len: 24,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&GITHUB_TOKEN_REGEX),
comment: "GitHub",
min_len: 35,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&TWITTER_OAUTH_REGEX),
comment: "Twitter OAuth",
min_len: 35,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&FACEBOOK_OAUTH_REGEX),
comment: "Facebook OAuth",
min_len: 32,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&GOOGLE_OAUTH_REGEX),
comment: "Google OAuth",
min_len: 24,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&AWS_API_KEY_REGEX),
comment: "AWS API Key",
min_len: 16,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&HEROKU_API_KEY_REGEX),
comment: "Heroku API Key",
min_len: 12,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&PASSWORD_IN_URL_REGEX),
comment: "Password in URL",
min_len: 14,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Regex(&GOOGLE_SERVICE_ACCOUNT_REGEX),
comment: "Google (GCP) Service-account",
min_len: 14,
allows_spaces: true,
},
SensitivePattern {
pattern: Pattern::Regex(&TWILIO_API_KEY_REGEX),
comment: "Twilio API Key",
min_len: 32,
allows_spaces: false,
},
SensitivePattern {
pattern: Pattern::Contains("-----BEGIN RSA PRIVATE KEY-----"),
comment: "RSA Private Key",
min_len: 64,
allows_spaces: true,
},
SensitivePattern {
pattern: Pattern::Contains("-----BEGIN OPENSSH PRIVATE KEY-----"),
comment: "SSH (OPENSSH) Private Key",
min_len: 64,
allows_spaces: true,
},
SensitivePattern {
pattern: Pattern::Contains("-----BEGIN DSA PRIVATE KEY-----"),
comment: "SSH (DSA) Private Key",
min_len: 64,
allows_spaces: true,
},
SensitivePattern {
pattern: Pattern::Contains("-----BEGIN EC PRIVATE KEY-----"),
comment: "SSH (EC) Private Key",
min_len: 64,
allows_spaces: true,
},
SensitivePattern {
pattern: Pattern::Contains("-----BEGIN PGP PRIVATE KEY BLOCK-----"),
comment: "PGP Private Key Block",
min_len: 64,
allows_spaces: true,
},
];

const MIN_PATTERN_LEN: usize = 12;

fn is_high_entropy(text: &str) -> bool {
let entropy = calculate_shannon_entropy(text);
entropy > HIGH_ENTROPY_THRESHOLD // TODO: Make this optional, or controllable
// Known safe patterns start here
static BASE64_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[A-Za-z0-9+/]{40,}={0,2}$").unwrap());
static URL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^https?://[a-zA-Z0-9.-]+(/[a-zA-Z0-9./_-]*)?$").unwrap());
static UNIX_PATH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(/[^/\0]+)+/?$").unwrap());
static WINDOWS_PATH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z]:\\(?:[^\\\0]+\\?)*$").unwrap());

// Since list is smaller, heuristics may not be needed as were in sensitive patterns.
static KNOWN_SAFE_PATTERNS: &[&LazyLock<Regex>] = &[
&BASE64_REGEX,
&URL_REGEX,
&UNIX_PATH_REGEX,
&WINDOWS_PATH_REGEX,
];


fn detect_secret(ctx: &RuleContext, data: &str) -> std::option::Option<&str> {
if is_known_safe_pattern(data) {
return None;
}

let entropy_threshold = ctx.options().entropy_threshold.unwrap_or(DEFAULT_HIGH_ENTROPY_THRESHOLD);
let entropy = calculate_shannon_entropy(data);

if entropy > entropy_threshold {
Some(format!(
"Detected high entropy string: {:.2} (Threshold: {:.2})",
entropy, entropy_threshold
))
} else {
None
}
}

fn is_known_safe_pattern(data: &str) -> bool {
for pattern in KNOWN_SAFE_PATTERNS {
if pattern.is_match(data) {
return true;
}
}
false
}

/// Inspired by https://github.com/nickdeis/eslint-plugin-no-secrets/blob/master/utils.js#L93
Expand Down
14 changes: 13 additions & 1 deletion crates/biome_js_analyze/tests/specs/nursery/noSecrets/valid.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,16 @@ const facebookAndAwsString = {
};
const IsoString = {
key: 'ISO-27001 information , GDPR'
};
};

// Postgres json path query
const isNumeric = '@.scoreDisplayMode == "numeric" || @.scoreDisplayMode == "metricSavings"'
const tailwindClassNames = 'whitespace-nowrap bg-base-4 px-1 text-[0.65rem] group-hover:w-auto group-hover:overflow-visible'
const tailwindConfigOptions = {
theme: {
animation: {
slideDown: 'slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1)',
}
}
}
export const url = 'https://www.nytimes.com/2024/03/05/arts/design/pritzker-prize-riken-yamamoto-architecture.html'

0 comments on commit 70901ff

Please sign in to comment.