feat(linter)!: remove unmaintained security plugin (#7773)

This commit is contained in:
Boshen 2024-12-10 14:29:22 +00:00
parent 9157a0ed0f
commit 39b9c5d01b
38 changed files with 19 additions and 1000 deletions

View file

@ -283,10 +283,6 @@ pub struct EnablePlugins {
/// Enable the node plugin and detect node usage problems
#[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)]
pub node_plugin: OverrideToggle,
/// Enable the security plugin and detect security problems
#[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)]
pub security_plugin: OverrideToggle,
}
/// Enables or disables a boolean option, or leaves it unset.
@ -362,7 +358,6 @@ impl EnablePlugins {
self.react_perf_plugin.inspect(|yes| plugins.set(LintPlugins::REACT_PERF, yes));
self.promise_plugin.inspect(|yes| plugins.set(LintPlugins::PROMISE, yes));
self.node_plugin.inspect(|yes| plugins.set(LintPlugins::NODE, yes));
self.security_plugin.inspect(|yes| plugins.set(LintPlugins::SECURITY, yes));
// Without this, jest plugins adapted to vitest will not be enabled.
if self.vitest_plugin.is_enabled() && self.jest_plugin.is_not_set() {

View file

@ -171,7 +171,7 @@ mod test {
serde_json::from_str(r#"{ "plugins": ["typescript", "unicorn"] }"#).unwrap();
assert_eq!(config.plugins, LintPlugins::TYPESCRIPT.union(LintPlugins::UNICORN));
let config: Oxlintrc =
serde_json::from_str(r#"{ "plugins": ["typescript", "unicorn", "react", "oxc", "import", "jsdoc", "jest", "vitest", "jsx-a11y", "nextjs", "react-perf", "promise", "node", "security"] }"#).unwrap();
serde_json::from_str(r#"{ "plugins": ["typescript", "unicorn", "react", "oxc", "import", "jsdoc", "jest", "vitest", "jsx-a11y", "nextjs", "react-perf", "promise", "node"] }"#).unwrap();
assert_eq!(config.plugins, LintPlugins::all());
let config: Oxlintrc =

View file

@ -38,8 +38,6 @@ bitflags! {
const PROMISE = 1 << 11;
/// `eslint-plugin-node`
const NODE = 1 << 12;
/// Custom security rules made by the Oxc team
const SECURITY = 1 << 13;
}
}
impl Default for LintPlugins {
@ -65,7 +63,6 @@ impl From<LintPluginOptions> for LintPlugins {
plugins.set(LintPlugins::REACT_PERF, options.react_perf);
plugins.set(LintPlugins::PROMISE, options.promise);
plugins.set(LintPlugins::NODE, options.node);
plugins.set(LintPlugins::SECURITY, options.security);
plugins
}
}
@ -115,7 +112,6 @@ impl From<&str> for LintPlugins {
"react-perf" | "react_perf" => LintPlugins::REACT_PERF,
"promise" => LintPlugins::PROMISE,
"node" => LintPlugins::NODE,
"security" | "oxc-security" => LintPlugins::SECURITY,
// "eslint" is not really a plugin, so it's 'empty'. This has the added benefit of
// making it the default value.
_ => LintPlugins::empty(),
@ -139,7 +135,6 @@ impl From<LintPlugins> for &'static str {
LintPlugins::REACT_PERF => "react-perf",
LintPlugins::PROMISE => "promise",
LintPlugins::NODE => "node",
LintPlugins::SECURITY => "security",
_ => "",
}
}
@ -245,7 +240,6 @@ pub struct LintPluginOptions {
pub react_perf: bool,
pub promise: bool,
pub node: bool,
pub security: bool,
}
impl Default for LintPluginOptions {
@ -264,7 +258,6 @@ impl Default for LintPluginOptions {
react_perf: false,
promise: false,
node: false,
security: false,
}
}
}
@ -287,7 +280,6 @@ impl LintPluginOptions {
react_perf: false,
promise: false,
node: false,
security: false,
}
}
@ -308,7 +300,6 @@ impl LintPluginOptions {
react_perf: true,
promise: true,
node: true,
security: true,
}
}
}
@ -332,7 +323,6 @@ impl<S: AsRef<str>> FromIterator<(S, bool)> for LintPluginOptions {
LintPlugins::REACT_PERF => options.react_perf = enabled,
LintPlugins::PROMISE => options.promise = enabled,
LintPlugins::NODE => options.node = enabled,
LintPlugins::SECURITY => options.security = enabled,
_ => {} // ignored
}
}
@ -365,7 +355,6 @@ mod test {
&& self.react_perf == other.react_perf
&& self.promise == other.promise
&& self.node == other.node
&& self.security == other.security
}
}
@ -405,7 +394,6 @@ mod test {
react_perf: false,
promise: false,
node: false,
security: false,
};
assert_eq!(plugins, expected);
}

View file

@ -350,5 +350,4 @@ const PLUGIN_PREFIXES: phf::Map<&'static str, &'static str> = phf::phf_map! {
"unicorn" => "eslint-plugin-unicorn",
"vitest" => "eslint-plugin-vitest",
"node" => "eslint-plugin-node",
"security" => "oxc-security",
};

View file

@ -443,10 +443,6 @@ mod oxc {
pub mod uninvoked_array_callback;
}
mod security {
pub mod api_keys;
}
mod nextjs {
pub mod google_font_display;
pub mod google_font_preconnect;
@ -850,7 +846,6 @@ oxc_macros::declare_all_lint_rules! {
react_perf::jsx_no_new_array_as_prop,
react_perf::jsx_no_new_function_as_prop,
react_perf::jsx_no_new_object_as_prop,
security::api_keys,
typescript::adjacent_overload_signatures,
typescript::array_type,
typescript::ban_ts_comment,

View file

@ -1,65 +0,0 @@
/// Calculates the Shannon entropy of a byte string.
///
/// Implementation borrowed from [Rosetta Code](https://rosettacode.org/wiki/Entropy#Rust).
///
/// see: [Entropy (Wikipedial)](https://en.wikipedia.org/wiki/Entropy_(information_theory))
#[allow(clippy::cast_precision_loss)]
pub(crate) fn entropy<S: AsRef<[u8]>>(string: S) -> f32 {
let mut histogram = [0u32; 256];
let bytes = string.as_ref();
// we don't care if this is truncated
let len = bytes.len() as f32;
for &b in bytes {
histogram[b as usize] += 1;
}
histogram
.iter()
.copied()
.filter(|&h| h != 0)
.map(|h| h as f32 / len) // we don't care if this is truncated
.map(|ratio| -ratio * ratio.log2())
.sum()
}
pub(crate) trait Entropy {
/// Calculates the Shannon entropy of a byte string.
///
/// Implementation borrowed from [Rosetta Code](https://rosettacode.org/wiki/Entropy#Rust).
///
/// see: [Entropy (Wikipedial)](https://en.wikipedia.org/wiki/Entropy_(information_theory))
fn entropy(&self) -> f32;
}
impl<S> Entropy for S
where
S: AsRef<[u8]>,
{
fn entropy(&self) -> f32 {
entropy(self)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_entropy() {
let test_cases = vec![
("hello world", "hello world".entropy()),
("hello world", b"hello world".entropy()),
("hello world", String::from("hello world").entropy()),
("hello world", 2.845_351_2),
];
for (input, expected) in test_cases {
let actual = entropy(input);
assert!(
(actual - expected).abs() < f32::EPSILON,
"expected entropy({input}) to be {expected}, got {actual}"
);
}
}
}

View file

@ -1,232 +0,0 @@
mod entropy;
#[allow(unused_imports, unused_variables)]
mod secret;
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::{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())
.with_error_code_num(format!("api-keys/{}", violation.rule_name()))
.with_label(violation.span())
.with_help(
"Use a secrets manager to store your API keys securely, then read them at runtime.",
)
}
declare_oxc_lint!(
/// ### What it does
///
/// Disallows hard-coded API keys and other credentials.
///
/// ### Why is this bad?
///
/// Hard-coding API keys and committing them to source control is a serious
/// security risk.
///
/// 1. If your code is leaked, attackers can use your API keys to access your
/// services and data.
/// 2. Accidental bundling of API keys can lead them to be exposed publicly
/// in your website, compriming your services.
/// 3. Any developer or contractor you hire will have access to your
/// services, even after they lose access to your codebase.
/// 4. Even after being deleted, they will be visible in your git repo's
/// commit history.
/// 5. Key rotation requires a code change and redeployment, and can
/// therefore not be handled by security teams or by automated systems.
/// 6. Many, many more reasons.
///
/// ```ts
/// const API_KEY = 'abcdef123456';
/// const data = await fetch('/api/some/endpoint', {
/// headers: {
/// 'Authorization': `Bearer ${API_KEY}`,
/// }
/// });
/// ```
///
/// ### What To Do Instead
///
/// :::warning
/// The Oxc team are not security experts. We do not endorse any particular
/// key management service or strategy. Do your research and choose the best
/// solution/architecture for your use case.
/// :::
///
/// One possible alternative is to store secrets in a secure secrets manager
/// (such as [AWS
/// KMS](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/kms/),
/// [HashiCorp Vault](https://github.com/nodevault/node-vault/tree/v0.10.2),
/// [Pangea](https://pangea.cloud/docs/sdk/js/vault#retrieve), etc.) and
/// request them when your application starts (e.g. a Docker container, an
/// EC2).
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```js
/// const AWS_ACCESS_KEY_ID = 'AKIA1234X678C123B567';
/// const OPENAI_API_KEY = 'sk_test_1234567890';
/// ```
///
/// Examples of **correct** code for this rule:
///
/// ```js
/// const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
/// const OPENAI_API_KEY = await getSecret('open-ai-api-key');
/// ```
ApiKeys,
correctness
);
#[derive(Debug, Default, Clone)]
pub struct ApiKeys(Box<ApiKeysInner>);
#[derive(Debug, Clone)]
pub struct ApiKeysInner {
/// Minimum length over all enabled secret rules.
/// This is a performance optimization to avoid checking each rule for every string.
min_len: NonZeroU32,
/// Minimum entropy over all enabled secret rules.
/// This is a performance optimization to avoid checking each rule for every string.
min_entropy: f32,
/// Credentials the user wants to check for.
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())
}
}
impl ApiKeysInner {
// TODO: allow configuring what rules are enabled/disabled
// TODO: allow custom patterns
pub fn new(rules: Vec<SecretsEnum>) -> Self {
let min_len = rules.iter().map(secrets::SecretsEnum::min_len).min().unwrap();
// can't use min() b/c f32 is not Ord
let min_entropy = rules.iter().map(secrets::SecretsEnum::min_entropy).fold(0.0, f32::min);
Self { min_len, min_entropy, rules }
}
}
impl Deref for ApiKeys {
type Target = ApiKeysInner;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ApiKeysInner {}
impl Rule for ApiKeys {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let string: &'a str = match node.kind() {
AstKind::StringLiteral(string) => string.value.as_str(),
AstKind::TemplateLiteral(string) => {
let Some(string) = string.quasi() else {
return;
};
string.as_str()
}
_ => return,
};
// skip strings that are below the length/entropy threshold of _all_ rules. Perf
// optimization, avoid O(n) len/entropy checks (for n rules)
if string.len() < self.min_len.get() as usize {
return;
}
let candidate = Secret::new(string, node.span(), None);
if candidate.entropy() < self.min_entropy {
return;
}
for rule in &self.rules {
// order here is important: they're in order of cheapest to most expensive
if candidate.len() < rule.min_len().get() as usize
|| candidate.entropy() < rule.min_entropy()
|| rule.max_len().is_some_and(|max_len| candidate.len() > max_len.get() as usize)
|| !rule.detect(&candidate)
{
continue;
}
// This clone allocs no memory and so is relatively cheap. rustc should optimize it
// away anyways.
let mut violation = SecretViolation::new(candidate.clone(), rule);
if rule.verify(&mut violation) {
ctx.diagnostic(api_keys(&violation));
return;
}
}
}
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)))
}
}

View file

@ -1,148 +0,0 @@
use std::{borrow::Cow, num::NonZeroU32, ops::Deref};
use oxc_span::{Atom, GetSpan, Span};
use super::{Entropy, SecretsEnum};
/// A credential discovered in source code.
///
/// Could be an API key, an auth token, or any other sensitive information.
#[allow(clippy::struct_field_names)]
#[derive(Debug, Clone)]
pub struct Secret<'a> {
secret: &'a str,
/// Secret span
span: Span,
/// TODO: find and pass identifiers once we have rules that need it
#[allow(dead_code)]
identifier: Option<Atom<'a>>,
entropy: f32,
}
/// A secret that was positively identified by a secret rule.
///
/// This gets used to construct the final diagnostic message.
#[derive(Debug, Clone)]
pub struct SecretViolation<'a> {
// NOTE: Rules get a &mut reference to a SecretViolation to verify the
// violation. It is important that the underlying secret is not modified.
secret: Secret<'a>,
rule_name: Cow<'a, str>, // really should be &'static
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) -> Cow<'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 {
DEFAULT_MIN_LEN
}
/// Secret candidates above this length will not be considered.
///
/// By default, no maximum length is enforced.
#[inline]
fn max_len(&self) -> Option<NonZeroU32> {
None
}
/// Min entropy a key must have to be considered a violation. Must be >= 0.
///
/// Defaults to 0.5
#[inline]
fn min_entropy(&self) -> f32 {
DEFAULT_MIN_ENTROPY
}
}
/// Detects hard-coded API keys and other credentials of a single kind or for a single SaaS
/// service.
pub trait SecretScanner: SecretScannerMeta {
/// Returns `true` if `candidate` is a leaked credential.
fn detect(&self, candidate: &Secret<'_>) -> bool;
/// `verify` lets secret rules modify diagnostic messages and/or perform additional
/// verification checks on secrets before they are reported. You may mutate state such as the
/// diagnostic message, but you _must not_ modify the secret itself as it is shared between
/// all rules.
///
/// Returns `true` to report the violation, or `false` to ignore it.
#[inline]
fn verify(&self, violation: &mut SecretViolation<'_>) -> bool {
true
}
}
impl<'a> Secret<'a> {
pub fn new(secret: &'a str, span: Span, identifier: Option<Atom<'a>>) -> Self {
let entropy = secret.entropy();
Self { secret, span, identifier, entropy }
}
}
impl Deref for Secret<'_> {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
self.secret
}
}
impl Entropy for Secret<'_> {
#[inline]
fn entropy(&self) -> f32 {
self.entropy
}
}
impl GetSpan for Secret<'_> {
#[inline]
fn span(&self) -> Span {
self.span
}
}
impl<'a> SecretViolation<'a> {
pub fn new(secret: Secret<'a>, rule: &SecretsEnum) -> Self {
Self { secret, rule_name: rule.rule_name(), message: rule.message() }
}
pub fn message(&self) -> &str {
&self.message
}
pub fn set_message<S: Into<Cow<'static, str>>>(&mut self, message: S) {
self.message = message.into();
}
pub fn rule_name(&self) -> &str {
&self.rule_name
}
}
impl GetSpan for SecretViolation<'_> {
#[inline]
fn span(&self) -> Span {
self.secret.span()
}
}
impl Deref for SecretViolation<'_> {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
&self.secret
}
}

View file

@ -1,71 +0,0 @@
mod aws_access_token;
mod custom;
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) -> Cow<'static, str> {
match self {
Self::AwsAccessKeyId(rule) => rule.rule_name(),
Self::Custom(rule) => rule.rule_name(),
}
}
pub fn message(&self) -> Cow<'static, str> {
match self {
Self::AwsAccessKeyId(rule) => rule.message(),
Self::Custom(rule) => rule.message(),
}
}
pub fn min_len(&self) -> NonZeroU32 {
match self {
Self::AwsAccessKeyId(rule) => rule.min_len(),
Self::Custom(rule) => rule.min_len(),
}
}
pub fn max_len(&self) -> Option<NonZeroU32> {
match self {
Self::AwsAccessKeyId(rule) => rule.max_len(),
Self::Custom(rule) => rule.max_len(),
}
}
pub fn min_entropy(&self) -> f32 {
match self {
Self::AwsAccessKeyId(rule) => rule.min_entropy(),
Self::Custom(rule) => rule.min_entropy(),
}
}
pub fn verify(&self, violation: &mut SecretViolation<'_>) -> bool {
match self {
Self::AwsAccessKeyId(rule) => rule.verify(violation),
Self::Custom(rule) => rule.verify(violation),
}
}
pub fn detect(&self, candidate: &Secret<'_>) -> bool {
match self {
Self::AwsAccessKeyId(rule) => rule.detect(candidate),
Self::Custom(rule) => rule.detect(candidate),
}
}
}
lazy_static::lazy_static! {
pub static ref ALL_RULES: Vec<SecretsEnum> = vec![
SecretsEnum::AwsAccessKeyId(aws_access_token::AwsAccessToken),
];
}

View file

@ -1,100 +0,0 @@
use std::num::NonZeroU32;
use oxc_macros::declare_oxc_secret;
use phf::{map::Map, phf_map};
use super::{Secret, SecretScanner, SecretViolation};
/// See: <https://swisskyrepo.github.io/InternalAllTheThings/cloud/aws/aws-access-token/#access-key-id-secret>
#[derive(Debug, Default, Clone)]
pub struct AwsAccessToken;
declare_oxc_secret! {
AwsAccessToken,
"Detected an AWS Access Key ID, which may lead to unauthorized access to AWS resources.",
entropy = 2.0,
min_len = 20,
max_len = 20,
}
impl SecretScanner for AwsAccessToken {
// '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA|<other-keys-below>)[A-Z0-9]{16}'''
fn detect(&self, candidate: &Secret<'_>) -> bool {
if !candidate.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()) {
return false;
}
let prefix = &candidate[..4];
AWS_TOKEN_PREFIXES.contains_key(prefix) || &prefix[0..3] == "A3T"
}
fn verify(&self, violation: &mut SecretViolation<'_>) -> bool {
let prefix = &violation[..4];
// Detect false positives on DNA sequences
if prefix == "ACCA" && violation.chars().all(|c| c.is_ascii_uppercase()) {
return false;
}
let name = AWS_TOKEN_PREFIXES.get(prefix).copied().unwrap_or("AWS access token");
let a_or_an = match name.chars().next().unwrap() {
'A' | 'E' | 'I' | 'O' | 'U' => "an",
_ => "a",
};
violation.set_message(format!(
"Detected {a_or_an} {name}, which may lead to unauthorized access to AWS resources."
));
true
}
}
/// List taken from:
/// <https://swisskyrepo.github.io/InternalAllTheThings/cloud/aws/aws-access-token/#access-key-id-secret>
static AWS_TOKEN_PREFIXES: Map<&'static str, &'static str> = phf_map! {
"ABIA" => "AWS STS service bearer token",
"ACCA" => "AWS Context-specific credential",
"AGPA" => "AWS User Group ID",
"AIDA" => "AWS IAM User ID",
"AIPA" => "Amazon EC2 instance profile",
"AKIA" => "AWS Access Key ID",
"ANPA" => "managed AWS Policy ID",
"ANVA" => "managed AWS Policy Version ID",
"APKA" => "AWS Public key",
"AROA" => "AWS Role",
"ASCA" => "AWS Certificate",
"ASIA" => "temporary (AWS STS) Access Key",
};
#[test]
fn test() {
use crate::{rules::SecurityApiKeys, tester::Tester, RuleMeta};
let pass = vec![
"let x = ''",
"let x = 'AKIA'",
"let not_a_key = 'abcdabcdabcdabcdabcd' ", // no prefix, has lowercase
"let not_a_key = 'AKIA ABCD1099FAM9KEY' ", // whitespace
"let not_a_key = 'AKIA-ABCD1099FAM9KEY' ", // special characters
"let not_a_key = 'AKIA_ABCD1099FAM9KEY' ", // special characters
"let not_a_key = 'AKIA%ABCD1099FAM9KEY' ", // special characters
"let not_a_key = 'AKIA$ABCD1099FAM9KEY' ", // special characters
"let not_a_key = 'AKIAAABcD1099FAM9KEY' ", // has lowercase
"let not_a_key = 'AKIAAABCD1099FAM9KEY9'", // too long
"let dna = 'ACCATGGCTACCGCTGTGCT' ", // DNA sequence
];
let fail = vec![
r#"let key = "AKIAAABCD1099FAM9KEY""#,
"let key = `AKIAAABCD1099FAM9KEY`", // no-expression template literal
"let key = 'ABIAAABCD1099FAM9KEY'",
"let key = 'ACCAAABCD1099FAM9KEY'",
"let key = 'AKIAAABCD1099FAM9KEY'",
"let key = 'AKIAAABCD1099FAM9KEY'",
"let key = 'AKIAAABCD1099FAM9KEY'",
];
Tester::new(SecurityApiKeys::NAME, SecurityApiKeys::CATEGORY, pass, fail)
.with_snapshot_suffix("aws_access_token")
.test_and_snapshot();
}

View file

@ -1,41 +0,0 @@
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)
}
}

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint(no-object-constructor): Disallow calls to the `Object` constructor without an argument
╭─[no_object_constructor.tsx:1:1]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint(no-unused-expressions): Disallow unused expressions
╭─[no_unused_expressions.tsx:1:1]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint(no-unused-vars): Function 'foox' is declared but never used.
╭─[no_unused_vars.tsx:1:10]

View file

@ -1,4 +1,5 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint(no-unused-vars): Variable 'a' is assigned a value but never used. Unused variables should start with a '_'.
╭─[no_unused_vars.tsx:1:5]

View file

@ -1,6 +1,5 @@
---
source: crates/oxc_linter/src/tester.rs
assertion_line: 353
snapshot_kind: text
---
⚠ eslint(prefer-spread): Require spread operators instead of .apply()

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-import(no-namespace): Usage of namespaced aka wildcard "*" imports prohibited
╭─[index.js:1:13]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-jsx-a11y(anchor-ambiguous-text): Unexpected ambagious anchor link text.
╭─[anchor_ambiguous_text.tsx:1:1]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ oxc(const-comparisons): Unexpected constant comparison
╭─[const_comparisons.tsx:1:1]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-react-hooks(exhaustive-deps): React Hook useCallback has a missing dependency: 'props.foo'
╭─[exhaustive_deps.tsx:4:14]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
╭─[jsx_no_script_url.tsx:1:4]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-react(no-array-index-key): Usage of Array index in keys is not allowed
╭─[no_array_index_key.tsx:2:20]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useConditionalHook" is called conditionally. React Hooks must be called in the exact same order in every component render.
╭─[rules_of_hooks.tsx:4:18]

View file

@ -1,52 +0,0 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ oxc-security(api-keys/aws-access-token): Detected an AWS Access Key ID, which may lead to unauthorized access to AWS resources.
╭─[api_keys.tsx:1:11]
1 │ let key = "AKIAAABCD1099FAM9KEY"
· ──────────────────────
╰────
help: Use a secrets manager to store your API keys securely, then read them at runtime.
⚠ oxc-security(api-keys/aws-access-token): Detected an AWS Access Key ID, which may lead to unauthorized access to AWS resources.
╭─[api_keys.tsx:1:11]
1 │ let key = `AKIAAABCD1099FAM9KEY`
· ──────────────────────
╰────
help: Use a secrets manager to store your API keys securely, then read them at runtime.
⚠ oxc-security(api-keys/aws-access-token): Detected an AWS STS service bearer token, which may lead to unauthorized access to AWS resources.
╭─[api_keys.tsx:1:11]
1 │ let key = 'ABIAAABCD1099FAM9KEY'
· ──────────────────────
╰────
help: Use a secrets manager to store your API keys securely, then read them at runtime.
⚠ oxc-security(api-keys/aws-access-token): Detected an AWS Context-specific credential, which may lead to unauthorized access to AWS resources.
╭─[api_keys.tsx:1:11]
1 │ let key = 'ACCAAABCD1099FAM9KEY'
· ──────────────────────
╰────
help: Use a secrets manager to store your API keys securely, then read them at runtime.
⚠ oxc-security(api-keys/aws-access-token): Detected an AWS Access Key ID, which may lead to unauthorized access to AWS resources.
╭─[api_keys.tsx:1:11]
1 │ let key = 'AKIAAABCD1099FAM9KEY'
· ──────────────────────
╰────
help: Use a secrets manager to store your API keys securely, then read them at runtime.
⚠ oxc-security(api-keys/aws-access-token): Detected an AWS Access Key ID, which may lead to unauthorized access to AWS resources.
╭─[api_keys.tsx:1:11]
1 │ let key = 'AKIAAABCD1099FAM9KEY'
· ──────────────────────
╰────
help: Use a secrets manager to store your API keys securely, then read them at runtime.
⚠ oxc-security(api-keys/aws-access-token): Detected an AWS Access Key ID, which may lead to unauthorized access to AWS resources.
╭─[api_keys.tsx:1:11]
1 │ let key = 'AKIAAABCD1099FAM9KEY'
· ──────────────────────
╰────
help: Use a secrets manager to store your API keys securely, then read them at runtime.

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ typescript-eslint(no-inferrable-types): Type can be trivially inferred from the initializer
╭─[no_inferrable_types.tsx:1:8]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ typescript-eslint(no-require-imports): Expected "import" statement instead of "require" call
╭─[no_require_imports.ts:1:11]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-unicorn(prefer-array-some): Prefer `.some(…)` over `.find(…)` or `.findLast(…)`.
╭─[prefer_array_some.tsx:1:9]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-unicorn(prefer-negative-index): Prefer negative index over .length - index when possible
╭─[prefer_negative_index.tsx:1:1]

View file

@ -1,5 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
snapshot_kind: text
---
⚠ eslint-plugin-unicorn(prefer-set-has): should be a `Set`, and use `.has()` to check existence or non-existence.
╭─[prefer_set_has.tsx:2:10]

View file

@ -1,155 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, Ident, LitFloat, LitInt, LitStr, Token};
use super::{
declare_oxc_lint::rule_name_converter,
util::{eat_comma, parse_assert},
};
pub struct SecretRuleMeta {
struct_name: Ident,
message: LitStr,
entropy: Option<LitFloat>,
min_len: Option<LitInt>,
max_len: Option<LitInt>,
}
impl Parse for SecretRuleMeta {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let struct_name = input.parse()?;
input.parse::<Token!(,)>()?;
let description = input.parse()?;
eat_comma(&input)?;
let mut rule = SecretRuleMeta {
struct_name,
message: description,
entropy: None,
min_len: None,
max_len: None,
};
while input.peek(Ident) {
let ident = input.parse::<Ident>()?;
#[allow(clippy::neg_cmp_op_on_partial_ord)]
match ident.to_string().as_str() {
"entropy" => {
input.parse::<Token!(=)>()?;
let entropy = input.parse::<LitFloat>()?;
parse_assert!(
entropy.base10_parse::<f32>()? >= 0.0,
entropy,
"Entropy must be greater than or equal to 0."
);
rule.entropy = Some(entropy);
}
"min_len" => {
input.parse::<Token!(=)>()?;
let min_len = input.parse::<LitInt>()?;
parse_assert!(
min_len.base10_parse::<u32>()? > 0,
min_len,
"Minimum length must be greater than or equal to 1."
);
rule.min_len = Some(min_len);
}
"max_len" => {
input.parse::<Token!(=)>()?;
let max_len = input.parse::<LitInt>()?;
parse_assert!(
max_len.base10_parse::<u32>()? > 0,
max_len,
"Maximum length cannot be zero."
);
rule.max_len = Some(max_len);
}
_ => parse_assert!(
false,
ident,
"Unexpected attribute. Only `entropy`, `min_len`, and `max_len` are allowed."
),
}
eat_comma(&input)?;
}
// Ignore the rest
input.parse::<proc_macro2::TokenStream>()?;
if let (Some(min), Some(max)) = (rule.min_len.as_ref(), &rule.max_len.as_ref()) {
let min = min.base10_parse::<u32>()?;
let max = max.base10_parse::<u32>()?;
parse_assert!(
min <= max,
max,
"Maximum length must be greater than or equal to minimum length."
);
}
Ok(rule)
}
}
pub fn declare_oxc_secret(meta: SecretRuleMeta) -> TokenStream {
let SecretRuleMeta {
//
struct_name,
message,
entropy,
min_len,
max_len,
} = meta;
let rule_name = rule_name_converter().convert(struct_name.to_string());
let min_len_fn = min_len.map(|min_len| {
quote! {
#[inline]
fn min_len(&self) -> NonZeroU32 {
// SAFETY: #min_len is a valid value for NonZeroU32
unsafe { NonZeroU32::new_unchecked(#min_len) }
}
}
});
let max_len_fn = max_len.map(|max_len| {
quote! {
#[inline]
fn max_len(&self) -> Option<NonZeroU32> {
Some(unsafe { NonZeroU32::new_unchecked(#max_len) })
}
}
});
let entropy_fn = entropy.map(|entropy| {
quote! {
#[inline]
fn min_entropy(&self) -> f32 {
#entropy
}
}
});
let output = quote! {
impl super::SecretScannerMeta for #struct_name {
#[inline]
fn rule_name(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(#rule_name)
}
#[inline]
fn message(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(#message)
}
#min_len_fn
#max_len_fn
#entropy_fn
}
};
TokenStream::from(output)
}

View file

@ -3,11 +3,8 @@
use proc_macro::TokenStream;
use syn::parse_macro_input;
#[macro_use]
mod util;
mod declare_all_lint_rules;
mod declare_oxc_lint;
mod declare_oxc_secret;
/// Macro used to declare an oxc lint rule
///
@ -110,65 +107,3 @@ pub fn declare_all_lint_rules(input: TokenStream) -> TokenStream {
let metadata = parse_macro_input!(input as declare_all_lint_rules::AllLintRulesMeta);
declare_all_lint_rules::declare_all_lint_rules(metadata)
}
/// Declare a secret scanner for `oxc-security/api-keys`.
///
/// Scanner definitions are composed of:
/// 1. The scanner struct name,
/// 2. A message displayed in diagnostics,
/// 3. A set of `key = value` config pairs.
///
/// # Pre-Verify Configuration
///
/// These configs filter secret candidates before `verify` is ever called. This
/// is for performance reasons, as many `verify` implementations use expensive
/// checks (such as regular expression matching). All configs are optional.
///
/// The following config key/value pairs are available:
///
/// ## `entropy` ([`f32`])
/// Minimum [Shannon
/// entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)) a
/// candidate must have to be considered a violation. Must be a positive,
/// non-zero `f32`. Defaults to `0.5`.
///
/// ## `min_len` ([`NonZeroU32`])
/// Minimum length a key candidate must have to be considered a violation. This
/// is a `u32` greater than 0. Defaults to `8`.
///
/// ## `max_len` ([`Option<NonZeroU32>`])
/// Maximum length a key candidate must have to be considered a violation. This
/// is a `u32` greater than 0. By default, no maximum is enforced ([`None`]).
///
/// # Example
/// ```
/// use oxc_macros::declare_oxc_secret;
/// use super::SecretScanner;
///
/// #[derive(Debug, Default, Clone)]
/// pub struct AwsAccessKeyId;
///
/// declare_oxc_secret! {
/// AwsAccessKeyId,
/// "Detected an AWS Access Key ID, which can be used to access AWS resources.",
/// entropy = 4.0,
/// min_len = 20,
/// max_len = 20,
/// }
///
/// impl SecretScanner for AwsAccessKeyId {
/// fn detect(&self, candidate: &Secret<'_>) -> bool {
/// // Look for AKIA, ASIA, or AIDA
/// ["AKIA", "ASIA", "AIDA"].iter().any(|prefix| candidate.starts_with(prefix)) &&
/// !candidate.cha
/// }
/// }
///
/// ```
///
/// [`NonZeroU32`]: std::num::NonZeroU32
#[proc_macro]
pub fn declare_oxc_secret(input: TokenStream) -> TokenStream {
let metadata = parse_macro_input!(input as declare_oxc_secret::SecretRuleMeta);
declare_oxc_secret::declare_oxc_secret(metadata)
}

View file

@ -1,34 +0,0 @@
use syn::{parse::ParseStream, Result, Token};
/// Checks if `cond` is `true`, returning [`Err(syn::Error)`] with `msg` if it's not.
/// ## Example
/// ```ignore
/// use syn::{parse::Parse, LitStr};
///
/// struct Foo(LitStr);
///
/// impl Parse for Foo {
/// fn parse(input: ParseStream) -> Result<Self> {
/// let s = input.parse::<LitStr>()?;
/// parse_assert!(s.value() == "foo", s, "Expected 'foo'");
/// Ok(Foo(s))
/// }
/// }
/// ```
macro_rules! parse_assert {
($cond:expr, $toks:expr, $msg:expr) => {
if !($cond) {
return Err(syn::Error::new_spanned($toks, $msg));
}
};
}
pub(crate) use parse_assert;
/// Consume a comma token if it's present, noop otherwise
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn eat_comma(input: &ParseStream) -> Result<()> {
if input.peek(Token!(,)) {
input.parse::<Token!(,)>()?;
}
Ok(())
}

View file

@ -199,9 +199,6 @@ new-promise-rule name:
new-vitest-rule name:
cargo run -p rulegen {{name}} vitest
new-security-rule name:
cargo run -p rulegen {{name}} security
[unix]
clone-submodule dir url sha:
cd {{dir}} || git init {{dir}}

View file

@ -597,7 +597,6 @@ pub enum RuleKind {
Node,
Promise,
Vitest,
Security,
}
impl RuleKind {
@ -616,7 +615,6 @@ impl RuleKind {
"n" => Self::Node,
"promise" => Self::Promise,
"vitest" => Self::Vitest,
"security" => Self::Security,
_ => Self::ESLint,
}
}
@ -639,7 +637,6 @@ impl Display for RuleKind {
Self::Node => write!(f, "eslint-plugin-n"),
Self::Promise => write!(f, "eslint-plugin-promise"),
Self::Vitest => write!(f, "eslint-plugin-vitest"),
Self::Security => write!(f, "security"),
}
}
}
@ -668,7 +665,7 @@ fn main() {
RuleKind::Node => format!("{NODE_TEST_PATH}/{kebab_rule_name}.js"),
RuleKind::Promise => format!("{PROMISE_TEST_PATH}/{kebab_rule_name}.js"),
RuleKind::Vitest => format!("{VITEST_TEST_PATH}/{kebab_rule_name}.test.ts"),
RuleKind::Oxc | RuleKind::Security => String::new(),
RuleKind::Oxc => String::new(),
};
let language = match rule_kind {
RuleKind::Typescript | RuleKind::Oxc => "ts",
@ -791,7 +788,6 @@ fn add_rules_entry(ctx: &Context, rule_kind: RuleKind) -> Result<(), Box<dyn std
RuleKind::Promise => "promise",
RuleKind::Vitest => "vitest",
RuleKind::Node => "node",
RuleKind::Security => "security",
};
let mod_def = format!("mod {mod_name}");
let Some(mod_start) = rules.find(&mod_def) else {

View file

@ -44,7 +44,6 @@ impl<'a> Template<'a> {
RuleKind::Node => Path::new("crates/oxc_linter/src/rules/node"),
RuleKind::Promise => Path::new("crates/oxc_linter/src/rules/promise"),
RuleKind::Vitest => Path::new("crates/oxc_linter/src/rules/vitest"),
RuleKind::Security => Path::new("crates/oxc_linter/src/rules/security"),
};
std::fs::create_dir_all(path)?;

View file

@ -70,8 +70,6 @@ Arguments:
Enable the promise plugin and detect promise usage problems
- **` --node-plugin`** &mdash;
Enable the node plugin and detect node usage problems
- **` --security-plugin`** &mdash;
Enable the security plugin and detect security problems

View file

@ -44,7 +44,6 @@ Enable Plugins
problems
--promise-plugin Enable the promise plugin and detect promise usage problems
--node-plugin Enable the node plugin and detect node usage problems
--security-plugin Enable the security plugin and detect security problems
Fix Problems
--fix Fix as many issues as possible. Only unfixed issues are reported in