Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): implement noSecrets #3823

Merged
merged 39 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
44dc010
feat(linter): implement noSecrets initial
SaadBazaz Sep 8, 2024
ab30aa2
chore: fix todo syntax, add more invalid test cases, add snaps
SaadBazaz Sep 8, 2024
6d343f2
chore: update shannon_entropy string
SaadBazaz Sep 8, 2024
81db935
refactor: fix tests, and actually get code to work
SaadBazaz Sep 8, 2024
227422f
docs: remove eslint inspired line
SaadBazaz Sep 8, 2024
35d4ac9
refactor: use lazylock for caching regexes
SaadBazaz Sep 8, 2024
b2a56be
chore: lint according to project req
SaadBazaz Sep 8, 2024
b3dfca7
refactor: turn sensitive patterns into a tuple, make regexes at compi…
SaadBazaz Sep 8, 2024
fa1addb
fix: merge conflict, regen
SaadBazaz Sep 8, 2024
dde2dc9
docs: update doc comments, show better error to user
SaadBazaz Sep 8, 2024
13b9cf5
chore: update test snaps
SaadBazaz Sep 8, 2024
0c0c396
chore: reorder declare lint rule to top
SaadBazaz Sep 8, 2024
2cfb7bd
chore: ignore strings less than 24 length
SaadBazaz Sep 8, 2024
c2c9449
chore: change hardcoded string length to 20
SaadBazaz Sep 8, 2024
86aa4bd
chore: use newer esm syntax
SaadBazaz Sep 9, 2024
93ac00f
feat: min_len for each sensitive pattern, with dynamic min length cal…
SaadBazaz Sep 9, 2024
4741c79
chore: remove parrot comments
SaadBazaz Sep 9, 2024
7de80aa
fix: broken regexes
SaadBazaz Sep 9, 2024
7e87766
chore: remove parrot comment
SaadBazaz Sep 9, 2024
d7a2398
chore: add todo
SaadBazaz Sep 9, 2024
6136902
chore: update formatting
SaadBazaz Sep 9, 2024
ce73c5e
feat: update regexes
SaadBazaz Sep 9, 2024
d4c21e4
refactor: purge multithreading
SaadBazaz Sep 9, 2024
108501c
refactor: use struct instead of tuple
SaadBazaz Sep 9, 2024
04c6207
chore(deps): update rust crate ignore to 0.4.23 (#3835)
renovate[bot] Sep 9, 2024
725853d
chore(deps): update rust crate bpaf to 0.9.13 (#3834)
renovate[bot] Sep 9, 2024
0956994
chore(deps): update codspeedhq/action action to v2.4.5 (#3831)
renovate[bot] Sep 9, 2024
6116ac0
chore(deps): update rust:1.80.1 docker digest to d22d893 (#3829)
renovate[bot] Sep 9, 2024
29b7cf0
chore(deps): update rust crate anyhow to 1.0.87 (#3832)
renovate[bot] Sep 9, 2024
4c47714
fix(linter): only emit diagnostics for grid area properties (#3838)
togami2864 Sep 9, 2024
e342b99
fix(linter): allow SVG elements with role="img" (#3837)
togami2864 Sep 9, 2024
3f8dfe1
chore: remove unnecessary string conversion
SaadBazaz Sep 9, 2024
2616e9e
chore: remove unused string conversion
SaadBazaz Sep 9, 2024
ae88cb5
Merge branch 'main' into feat/no-secrets
SaadBazaz Sep 9, 2024
a6a72d9
chore: update docstring to use const instead of var
SaadBazaz Sep 9, 2024
3642ad7
docs: update changelog, add disclaimer for users
SaadBazaz Sep 9, 2024
296d2ed
docs: update changelog
SaadBazaz Sep 9, 2024
2274dcd
refactor: check minlength as a test, create consts
SaadBazaz Sep 9, 2024
efbb8fd
docs: update changelog with rorrect link
SaadBazaz Sep 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 109 additions & 90 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ define_categories! {
"lint/nursery/noExportedImports": "https://biomejs.dev/linter/rules/no-exported-imports",
"lint/nursery/noImportantInKeyframe": "https://biomejs.dev/linter/rules/no-important-in-keyframe",
"lint/nursery/noInvalidDirectionInLinearGradient": "https://biomejs.dev/linter/rules/no-invalid-direction-in-linear-gradient",
"lint/nursery/noInvalidGridAreas": "https://biomejs.dev/linter/rules/use-consistent-grid-areas",
"lint/nursery/noInvalidPositionAtImportRule": "https://biomejs.dev/linter/rules/no-invalid-position-at-import-rule",
"lint/nursery/noIrregularWhitespace": "https://biomejs.dev/linter/rules/no-irregular-whitespace",
"lint/nursery/noLabelWithoutControl": "https://biomejs.dev/linter/rules/no-label-without-control",
Expand All @@ -137,6 +138,7 @@ define_categories! {
"lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props",
"lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports",
"lint/nursery/noRestrictedTypes": "https://biomejs.dev/linter/rules/no-restricted-types",
"lint/nursery/noSecrets": "https://biomejs.dev/linter/rules/no-secrets",
"lint/nursery/noShorthandPropertyOverrides": "https://biomejs.dev/linter/rules/no-shorthand-property-overrides",
"lint/nursery/noStaticElementInteractions": "https://biomejs.dev/linter/rules/no-static-element-interactions",
"lint/nursery/noSubstr": "https://biomejs.dev/linter/rules/no-substr",
Expand All @@ -159,7 +161,6 @@ define_categories! {
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
"lint/nursery/useConsistentBuiltinInstantiation": "https://biomejs.dev/linter/rules/use-consistent-new-builtin",
"lint/nursery/useConsistentCurlyBraces": "https://biomejs.dev/linter/rules/use-consistent-curly-braces",
"lint/nursery/noInvalidGridAreas": "https://biomejs.dev/linter/rules/use-consistent-grid-areas",
"lint/nursery/useConsistentMemberAccessibility": "https://biomejs.dev/linter/rules/use-consistent-member-accessibility",
"lint/nursery/useDateNow": "https://biomejs.dev/linter/rules/use-date-now",
"lint/nursery/useDefaultSwitchClause": "https://biomejs.dev/linter/rules/use-default-switch-clause",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod no_misplaced_assertion;
pub mod no_react_specific_props;
pub mod no_restricted_imports;
pub mod no_restricted_types;
pub mod no_secrets;
pub mod no_static_element_interactions;
pub mod no_substr;
pub mod no_undeclared_dependencies;
Expand Down Expand Up @@ -62,6 +63,7 @@ declare_lint_group! {
self :: no_react_specific_props :: NoReactSpecificProps ,
self :: no_restricted_imports :: NoRestrictedImports ,
self :: no_restricted_types :: NoRestrictedTypes ,
self :: no_secrets :: NoSecrets ,
self :: no_static_element_interactions :: NoStaticElementInteractions ,
self :: no_substr :: NoSubstr ,
self :: no_undeclared_dependencies :: NoUndeclaredDependencies ,
Expand Down
130 changes: 130 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_secrets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use biome_analyze::{context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, RuleSourceKind};
use biome_console::markup;

use biome_js_syntax::JsStringLiteralExpression;

use biome_rowan::AstNode;
use regex::Regex;
use std::sync::LazyLock;

// List of sensitive patterns
const SENSITIVE_PATTERNS: &[&str] = &[
SaadBazaz marked this conversation as resolved.
Show resolved Hide resolved
r"xox[p|b|o|a]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32}", // Slack Token
r"-----BEGIN RSA PRIVATE KEY-----", // RSA Private Key
SaadBazaz marked this conversation as resolved.
Show resolved Hide resolved
r"-----BEGIN OPENSSH PRIVATE KEY-----", // SSH (OPENSSH) Private Key
r"-----BEGIN DSA PRIVATE KEY-----", // SSH (DSA) Private Key
r"-----BEGIN EC PRIVATE KEY-----", // SSH (EC) Private Key
r"-----BEGIN PGP PRIVATE KEY BLOCK-----", // PGP Private Key Block
r#"[fF][aA][cC][eE][bB][oO][oO][kK].*['\"][0-9a-f]{32}['\"]"#, // Facebook OAuth
r#"[tT][wW][iI][tT][tT][eE][rR].*['\"][0-9a-zA-Z]{35,44}['\"]"#, // Twitter OAuth
r#"[gG][iI][tT][hH][uU][bB].*['\"][0-9a-zA-Z]{35,40}['\"]"#, // GitHub
r#""client_secret":"[a-zA-Z0-9-_]{24}""#, // Google OAuth
r"AKIA[0-9A-Z]{16}", // AWS API Key
r"[hH][eE][rR][oO][kK][uU].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", // Heroku API Key
r#"[sS][eE][cC][rR][eE][tT].*['\"][0-9a-zA-Z]{32,45}['\"]"#, // Generic Secret
r#"[aA][pP][iI][_]?[kK][eE][yY].*['\"][0-9a-zA-Z]{32,45}['\"]"#, // Generic API Key
r"https://hooks\.slack\.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}", // Slack Webhook
r#""type": "service_account""#, // Google (GCP) Service-account
r"SK[a-z0-9]{32}", // Twilio API Key
r#"[a-zA-Z]{3,10}://[^/\s:@]{3,20}:[^/\s:@]{3,20}@.{1,100}['"\s]"#, // Password in URL
];

// 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.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// var secret = "AKIA1234567890EXAMPLE";
SaadBazaz marked this conversation as resolved.
Show resolved Hide resolved
/// ```
///
/// ### Valid
///
/// ```js
/// var nonSecret = "hello world";
/// ```
pub NoSecrets {
version: "next",
name: "noSecrets",
language: "js",
recommended: false,
sources: &[RuleSource::Eslint("no-secrets/no-secrets")],
source_kind: RuleSourceKind::Inspired,
}
}

impl Rule for NoSecrets {
type Query = Ast<JsStringLiteralExpression>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let Some(token) = node.value_token().ok() else {
return None;
};
let text = token.text();

for pattern in SENSITIVE_PATTERNS {
let re = LazyLock::new(|| Regex::new(pattern).unwrap());
if re.is_match(&text) {
return Some(());
}
}

if is_high_entropy(&text) {
return Some(());
}

None
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! { "Potential secret found." }
).note(markup! { "This looks like a sensitive value such as an API key or token." }) // TODO: Give a more detailed response on the *type* of API Key/token (based on the SENSITIVE PATTERNS)
SaadBazaz marked this conversation as resolved.
Show resolved Hide resolved
)
}
}

fn is_high_entropy(text: &str) -> bool {
let entropy = calculate_shannon_entropy(text);
entropy > 4.5 // TODO: Make this optional, or controllable
}

/**
* From https://github.com/nickdeis/eslint-plugin-no-secrets/blob/master/utils.js#L93
* Calculates Shannon entropy to measure the randomness of data. High entropy values indicate potentially
* secret or sensitive information, as such data is typically more random and less predictable than regular text.
* Useful for detecting API keys, passwords, and other secrets within code or configuration files.
* @param {*} str
*/
fn calculate_shannon_entropy(data: &str) -> f64 {
let mut freq = [0usize; 256];
let mut len = 0usize;
for &byte in data.as_bytes() {
freq[byte as usize] += 1;
len += 1;
}
SaadBazaz marked this conversation as resolved.
Show resolved Hide resolved

let mut entropy = 0.0;
for count in freq.iter() {
if *count > 0 {
let p = *count as f64 / len as f64;
entropy -= p * p.log2();
}
}

entropy
}
1 change: 1 addition & 0 deletions crates/biome_js_analyze/src/options.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/noSecrets/invalid.js
SaadBazaz marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const apiKey = '12345';
const password = 'mySecretPassword';
var secret = "AKIA1234567890EXAMPLE"
const slackToken = "xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx";
const rsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890..."
const facebookToken = "facebook_app_id_12345abcde67890fghij12345";
const twitterApiKey = "twitter_api_key_1234567890abcdefghijklmnopqrstuvwxyz";
const githubToken = "github_pat_1234567890abcdefghijklmnopqrstuvwxyz";
const clientSecret = "abcdefghijklmnopqrstuvwxyz"
const herokuApiKey = "heroku_api_key_1234abcd-1234-1234-1234-1234abcd5678";
const genericSecret = "secret_1234567890abcdefghijklmnopqrstuvwxyz";
const genericApiKey = "api_key_1234567890abcdefghijklmnopqrstuvwxyz";
const slackKey = "https://hooks.slack.com/services/T12345678/B12345678/abcdefghijklmnopqrstuvwx"
const twilioApiKey = "SK1234567890abcdefghijklmnopqrstuv";
const dbUrl = "postgres://user:password123@example.com:5432/dbname";
Loading
Loading