use std::collections::HashSet;
const ERROR_TOKENS: &[&str] = &["catch", "except"];
const AUTH_STRONG: &[&str] = &["oauth", "jwt", "rbac", "passport", "authentic", "authoriz"];
const AUTH_TOKENS: &[&str] = &[
"auth",
"login",
"logout",
"signin",
"signout",
"permission",
"permissions",
"session",
"sessions",
"role",
"roles",
];
const MUTATION_VERBS: &[&str] = &[
"set", "apply", "assign", "create", "delete", "remove", "update", "add", "grant", "revoke",
"clear", "drop", "insert", "save", "store", "write", "build", "make", "register", "init",
"reset", "destroy", "unset", "put",
];
pub(super) fn line_has_error_handling(line: &str) -> bool {
let tokens = tokenize(line);
ERROR_TOKENS.iter().any(|needle| tokens.contains(*needle))
}
pub(super) fn line_has_auth(line: &str) -> bool {
let lower = line.to_ascii_lowercase();
if AUTH_STRONG.iter().any(|needle| lower.contains(needle)) {
return true;
}
let tokens = tokenize(line);
AUTH_TOKENS.iter().any(|needle| tokens.contains(*needle))
}
pub(super) fn callee_is_auth_check(callee: &str) -> bool {
let lower = callee.to_ascii_lowercase();
if AUTH_STRONG.iter().any(|needle| lower.contains(needle)) {
return true;
}
let tokens = ordered_tokens(callee);
if !tokens
.iter()
.any(|token| AUTH_TOKENS.contains(&token.as_str()))
{
return false;
}
!tokens
.first()
.is_some_and(|first| MUTATION_VERBS.contains(&first.as_str()))
}
fn ordered_tokens(text: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
for ch in text.chars() {
if !ch.is_ascii_alphanumeric() {
flush(&mut current, &mut tokens);
continue;
}
if ch.is_ascii_uppercase()
&& current
.chars()
.last()
.is_some_and(|last| last.is_ascii_lowercase() || last.is_ascii_digit())
{
flush(&mut current, &mut tokens);
}
current.push(ch.to_ascii_lowercase());
}
flush(&mut current, &mut tokens);
tokens
}
fn tokenize(text: &str) -> HashSet<String> {
ordered_tokens(text).into_iter().collect()
}
fn flush(current: &mut String, tokens: &mut Vec<String>) {
if !current.is_empty() {
tokens.push(std::mem::take(current));
}
}
#[cfg(test)]
mod tests {
use super::{callee_is_auth_check, line_has_auth, line_has_error_handling};
#[test]
fn callee_auth_check_matches_checks_not_args_or_mutations() {
assert!(callee_is_auth_check("checkPermission"));
assert!(callee_is_auth_check("isAuthenticated"));
assert!(callee_is_auth_check("req.isAuthenticated"));
assert!(callee_is_auth_check("requireAuth"));
assert!(callee_is_auth_check("verifyJWT"));
assert!(callee_is_auth_check("hasRole"));
assert!(!callee_is_auth_check("getUserProfile")); assert!(!callee_is_auth_check("log")); assert!(!callee_is_auth_check("applyRole")); assert!(!callee_is_auth_check("setSession"));
assert!(!callee_is_auth_check("grantPermission"));
assert!(!callee_is_auth_check("getAuthor"));
assert!(!callee_is_auth_check("authority"));
}
#[test]
fn error_handling_matches_real_catch_not_lookalikes() {
assert!(line_has_error_handling("} catch (e) {"));
assert!(line_has_error_handling(" except ValueError:"));
assert!(!line_has_error_handling("const catchy = makeCatcher();"));
assert!(!line_has_error_handling("// nothing to see here"));
}
#[test]
fn auth_matches_real_tokens_and_prefixes_not_author() {
assert!(line_has_auth("if (!isAuthenticated(req)) return 401;"));
assert!(line_has_auth("requireAuth(req, res);"));
assert!(line_has_auth("checkPermission(user, 'admin');"));
assert!(line_has_auth("const token = verifyJWT(header);"));
assert!(!line_has_auth("const author = post.author;"));
assert!(!line_has_auth("import { Authority } from './authority';"));
}
}