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): support user-configurable secrets for oxc-security/api-keys #5938

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 60 additions & 5 deletions crates/oxc_linter/src/rules/security/api_keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ mod secrets;

use std::{num::NonZeroU32, ops::Deref};

use regex::Regex;
use serde::Deserialize;
use serde_json::Value;

use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::GetSpan;

use entropy::Entropy;
use secret::{Secret, SecretScanner, SecretScannerMeta, SecretViolation};
use secrets::{SecretsEnum, ALL_RULES};
use oxc_span::{CompactStr, GetSpan};

use crate::{context::LintContext, rule::Rule, AstNode};
use entropy::Entropy;
use secret::{
Secret, SecretScanner, SecretScannerMeta, SecretViolation, DEFAULT_MIN_ENTROPY, DEFAULT_MIN_LEN,
};
use secrets::{CustomSecret, SecretsEnum, ALL_RULES};

fn api_keys(violation: &SecretViolation) -> OxcDiagnostic {
OxcDiagnostic::warn(violation.message().to_owned())
Expand Down Expand Up @@ -105,6 +110,31 @@ pub struct ApiKeysInner {
rules: Vec<SecretsEnum>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeysConfig {
#[serde(default)]
custom_patterns: Vec<CustomPattern>,
}

#[derive(Debug, Deserialize)]
struct CustomPattern {
// required fields
#[serde(rename = "ruleName")]
rule_name: CompactStr,
pattern: String,

// optional fields
#[serde(default)]
message: Option<CompactStr>,
#[serde(default)]
entropy: Option<f32>,
#[serde(default, rename = "minLength")]
min_len: Option<NonZeroU32>,
#[serde(default, rename = "maxLength")]
max_len: Option<NonZeroU32>,
}

impl Default for ApiKeysInner {
fn default() -> Self {
Self::new(ALL_RULES.clone())
Expand Down Expand Up @@ -174,4 +204,29 @@ impl Rule for ApiKeys {
}
}
}

fn from_configuration(value: Value) -> Self {
let Some(obj) = value.get(0) else {
return Self::default();
};
let config = serde_json::from_value::<ApiKeysConfig>(obj.clone())
.expect("Invalid configuration for 'oxc-security/api-keys'");

// TODO: Check if this is worth optimizing, then do so if needed.
let mut rules = ALL_RULES.clone();
rules.extend(config.custom_patterns.into_iter().map(|pattern| {
let regex = Regex::new(&pattern.pattern)
.expect("Invalid custom API key regex in 'oxc-security/api-keys'");
SecretsEnum::Custom(CustomSecret {
rule_name: pattern.rule_name,
message: pattern.message.unwrap_or("Detected a hard-coded secret.".into()),
entropy: pattern.entropy.unwrap_or(DEFAULT_MIN_ENTROPY),
min_len: pattern.min_len.unwrap_or(DEFAULT_MIN_LEN),
max_len: pattern.max_len,
pattern: regex,
})
}));

Self(Box::new(ApiKeysInner::new(rules)))
}
}
19 changes: 9 additions & 10 deletions crates/oxc_linter/src/rules/security/api_keys/secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,23 @@ pub struct SecretViolation<'a> {
message: Cow<'a, str>, // really should be &'static
}

// SAFETY: 8 is a valid value for NonZeroU32
pub(super) const DEFAULT_MIN_LEN: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(8) };
pub(super) const DEFAULT_MIN_ENTROPY: f32 = 0.5;

/// Metadata trait separated out of [`SecretScanner`]. The easiest way to implement this is with
/// the [`oxc_macros::declare_oxc_secret!`] macro.
pub trait SecretScannerMeta {
/// Human-readable unique identifier describing what service this rule finds api keys for.
/// Must be kebab-case.
fn rule_name(&self) -> &'static str;
fn rule_name(&self) -> Cow<'static, str>;

fn message(&self) -> &'static str;
fn message(&self) -> Cow<'static, str>;

/// Min str length a key candidate must have to be considered a violation. Must be >= 1.
#[inline]
fn min_len(&self) -> NonZeroU32 {
// SAFETY: 8 is a valid value for NonZeroU32
unsafe { NonZeroU32::new_unchecked(8) }
DEFAULT_MIN_LEN
}

/// Secret candidates above this length will not be considered.
Expand All @@ -60,7 +63,7 @@ pub trait SecretScannerMeta {
/// Defaults to 0.5
#[inline]
fn min_entropy(&self) -> f32 {
0.5
DEFAULT_MIN_ENTROPY
}
}

Expand Down Expand Up @@ -113,11 +116,7 @@ impl GetSpan for Secret<'_> {

impl<'a> SecretViolation<'a> {
pub fn new(secret: Secret<'a>, rule: &SecretsEnum) -> Self {
Self {
secret,
rule_name: Cow::Borrowed(rule.rule_name()),
message: Cow::Borrowed(rule.message()),
}
Self { secret, rule_name: rule.rule_name(), message: rule.message() }
}

pub fn message(&self) -> &str {
Expand Down
31 changes: 21 additions & 10 deletions crates/oxc_linter/src/rules/security/api_keys/secrets.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,65 @@
mod aws_access_token;
mod custom;

use std::num::NonZeroU32;
use std::{borrow::Cow, num::NonZeroU32};

use super::{Secret, SecretScanner, SecretScannerMeta, SecretViolation};

pub use custom::CustomSecret;

#[derive(Debug, Clone)]
pub enum SecretsEnum {
AwsAccessKeyId(aws_access_token::AwsAccessToken),
Custom(custom::CustomSecret),
}

impl SecretsEnum {
pub fn rule_name(&self) -> &'static str {
pub fn rule_name(&self) -> Cow<'static, str> {
match self {
SecretsEnum::AwsAccessKeyId(rule) => rule.rule_name(),
Self::AwsAccessKeyId(rule) => rule.rule_name(),
Self::Custom(rule) => rule.rule_name(),
}
}

pub fn message(&self) -> &'static str {
pub fn message(&self) -> Cow<'static, str> {
match self {
SecretsEnum::AwsAccessKeyId(rule) => rule.message(),
Self::AwsAccessKeyId(rule) => rule.message(),
Self::Custom(rule) => rule.message(),
}
}

pub fn min_len(&self) -> NonZeroU32 {
match self {
SecretsEnum::AwsAccessKeyId(rule) => rule.min_len(),
Self::AwsAccessKeyId(rule) => rule.min_len(),
Self::Custom(rule) => rule.min_len(),
}
}

pub fn max_len(&self) -> Option<NonZeroU32> {
match self {
SecretsEnum::AwsAccessKeyId(rule) => rule.max_len(),
Self::AwsAccessKeyId(rule) => rule.max_len(),
Self::Custom(rule) => rule.max_len(),
}
}

pub fn min_entropy(&self) -> f32 {
match self {
SecretsEnum::AwsAccessKeyId(rule) => rule.min_entropy(),
Self::AwsAccessKeyId(rule) => rule.min_entropy(),
Self::Custom(rule) => rule.min_entropy(),
}
}

pub fn verify(&self, violation: &mut SecretViolation<'_>) -> bool {
match self {
SecretsEnum::AwsAccessKeyId(rule) => rule.verify(violation),
Self::AwsAccessKeyId(rule) => rule.verify(violation),
Self::Custom(rule) => rule.verify(violation),
}
}

pub fn detect(&self, candidate: &Secret<'_>) -> bool {
match self {
SecretsEnum::AwsAccessKeyId(rule) => rule.detect(candidate),
Self::AwsAccessKeyId(rule) => rule.detect(candidate),
Self::Custom(rule) => rule.detect(candidate),
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions crates/oxc_linter/src/rules/security/api_keys/secrets/custom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::{borrow::Cow, num::NonZeroU32};

use regex::Regex;

use oxc_span::CompactStr;

use super::{Secret, SecretScanner, SecretScannerMeta};

#[derive(Debug, Clone)]
pub struct CustomSecret {
pub(crate) rule_name: CompactStr,
pub(crate) message: CompactStr,
pub(crate) entropy: f32,
pub(crate) min_len: NonZeroU32,
pub(crate) max_len: Option<NonZeroU32>,
pub(crate) pattern: Regex,
}

impl SecretScannerMeta for CustomSecret {
fn rule_name(&self) -> Cow<'static, str> {
self.rule_name.clone().into()
}
fn message(&self) -> Cow<'static, str> {
self.message.clone().into()
}
fn min_len(&self) -> NonZeroU32 {
self.min_len
}
fn max_len(&self) -> Option<NonZeroU32> {
self.max_len
}
fn min_entropy(&self) -> f32 {
self.entropy
}
}

impl SecretScanner for CustomSecret {
fn detect(&self, candidate: &Secret<'_>) -> bool {
self.pattern.is_match(candidate)
}
}
8 changes: 4 additions & 4 deletions crates/oxc_macros/src/declare_oxc_secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,13 @@ pub fn declare_oxc_secret(meta: SecretRuleMeta) -> TokenStream {
let output = quote! {
impl super::SecretScannerMeta for #struct_name {
#[inline]
fn rule_name(&self) -> &'static str {
#rule_name
fn rule_name(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(#rule_name)
}

#[inline]
fn message(&self) -> &'static str {
#message
fn message(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(#message)
}

#min_len_fn
Expand Down
Loading