mod ident_name;
mod string;
use std::fmt;
use oxc::ast::visit::walk;
use oxc::ast::{ast::*, Visit};
use oxc::span::{Atom, Span};
use crate::config::RuleId;
use crate::Config;
use ident_name::GetIdentifier as _;
#[derive(Debug)]
pub struct ApiKey<'a> {
pub span: Span,
pub rule_id: RuleId,
pub secret: &'a str,
pub key_name: Option<&'a str>,
}
fn atom_as_source_str<'a>(atom: &Atom<'a>) -> &'a str {
unsafe { std::mem::transmute(atom.as_str()) }
}
pub(super) struct ApiKeyVisitor<'c, 'a> {
config: &'c Config,
api_keys: Vec<ApiKey<'a>>,
current_identifier: Option<Atom<'a>>,
}
impl<'c, 'a> ApiKeyVisitor<'c, 'a> {
pub fn new(config: &'c Config) -> Self {
Self {
config,
api_keys: vec![],
current_identifier: None,
}
}
pub fn into_inner(self) -> Vec<ApiKey<'a>> {
self.api_keys
}
fn find_and_report_api_keys(&mut self, maybe_secret: &Atom<'a>, span: Span) {
let haystack = atom_as_source_str(maybe_secret);
let possible_found_secrets = self.config.check_values(haystack);
if let Some(identifier) = self.current_identifier.clone() {
let violations = possible_found_secrets
.filter(|(rule_id, _, _)| self.config.check_name(*rule_id, &identifier));
self.record_with_span(span, Some(atom_as_source_str(&identifier)), violations);
} else {
let violations = possible_found_secrets
.filter(|(rule_id, _, _)| self.config.get_name_criteria(*rule_id).is_none());
self.record_with_span(span, None, violations);
};
}
fn record_with_span(
&mut self,
span: Span,
identifier: Option<&'a str>,
violations: impl Iterator<Item = (RuleId, usize, &'a str)>,
) {
violations.for_each(|(rule_id, key_start, found_key)| {
let start = span.start + key_start as u32;
let len = found_key.len() as u32;
let span = Span::new(start, start + len);
self.api_keys.push(ApiKey {
rule_id,
span,
key_name: identifier,
secret: found_key,
});
});
}
}
impl<'c, 'a> Visit<'a> for ApiKeyVisitor<'c, 'a>
where
'c: 'a,
{
fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
let Some(init) = &declarator.init else { return };
let temp = self.current_identifier.take();
self.current_identifier = declarator.id.get_identifier_name();
walk::walk_expression(self, init);
self.current_identifier = temp;
}
fn visit_assignment_expression(&mut self, expr: &AssignmentExpression<'a>) {
let temp = self.current_identifier.take();
walk::walk_expression(self, &expr.right);
self.current_identifier = temp;
}
fn visit_property_definition(&mut self, def: &PropertyDefinition<'a>) {
let Some(value) = def.value.as_ref() else {
return;
};
let temp = self.current_identifier.take();
self.current_identifier = def.key.get_identifier_name();
walk::walk_expression(self, value);
self.current_identifier = temp;
}
fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
let temp = self.current_identifier.take();
walk::walk_call_expression(self, expr);
self.current_identifier = temp;
}
fn visit_string_literal(&mut self, lit: &StringLiteral<'a>) {
self.find_and_report_api_keys(&lit.value, lit.span);
}
fn visit_template_literal(&mut self, lit: &TemplateLiteral<'a>) {
if lit.is_no_substitution_template() {
let str_lit = lit.quasi().expect("TemplateLiteral.is_no_substitution_template() should have checked that at least one quasis exists.");
self.find_and_report_api_keys(&str_lit, lit.span)
} else {
walk::walk_template_literal(self, lit)
}
}
}
impl fmt::Debug for ApiKeyVisitor<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ApiKeyVisitor")
.field("semantic", &"<omitted>")
.field("config", &self.config)
.field("api_keys", &self.api_keys)
.field("current_identifier", &self.current_identifier)
.finish()
}
}