mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter)!: remove unmaintained security plugin (#7773)
This commit is contained in:
parent
9157a0ed0f
commit
39b9c5d01b
38 changed files with 19 additions and 1000 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
];
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
3
justfile
3
justfile
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -70,8 +70,6 @@ Arguments:
|
|||
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
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue