use crate::models::{ComparisonSide, FactorCategory, ScoreFactor, VariableSource};
use crate::simd::{self, patterns::prebuilt};
#[derive(Debug, Clone)]
pub struct RhsConfig {
pub command_like_names: Vec<String>,
pub auth_like_names: Vec<String>,
pub input_like_names: Vec<String>,
pub external_input_functions: Vec<String>,
pub command_score_mod: i32,
pub auth_score_mod: i32,
pub input_score_mod: i32,
pub external_input_score_mod: i32,
}
impl Default for RhsConfig {
fn default() -> Self {
Self {
command_like_names: vec![
"cmd".into(),
"command".into(),
"action".into(),
"op".into(),
"operation".into(),
"verb".into(),
"method".into(),
"mode".into(),
"type".into(),
"kind".into(),
"variant".into(),
"arg".into(),
"args".into(),
"argument".into(),
"argv".into(),
"flag".into(),
"flags".into(),
"option".into(),
"opt".into(),
"route".into(),
"path".into(),
"endpoint".into(),
"uri".into(),
"url".into(),
"resource".into(),
"message_type".into(),
"msg_type".into(),
"event_type".into(),
"packet_type".into(),
"frame_type".into(),
"interval".into(),
"qos".into(),
"cert".into(),
"dir".into(),
"format".into(),
"level".into(),
"timeout".into(),
"port".into(),
"host".into(),
],
auth_like_names: vec![
"token".into(),
"auth_token".into(),
"access_token".into(),
"bearer".into(),
"jwt".into(),
"session_token".into(),
"password".into(),
"passwd".into(),
"pwd".into(),
"pass".into(),
"secret".into(),
"credential".into(),
"credentials".into(),
"key".into(),
"api_key".into(),
"apikey".into(),
"secret_key".into(),
"private_key".into(),
"signing_key".into(),
"auth".into(),
"authorization".into(),
"authenticate".into(),
"hash".into(),
"digest".into(),
"signature".into(),
],
input_like_names: vec![
"input".into(),
"user_input".into(),
"request".into(),
"req".into(),
"payload".into(),
"body".into(),
"data".into(),
"content".into(),
"query".into(),
"params".into(),
"form".into(),
],
external_input_functions: vec![
"header".into(),
"get_header".into(),
"headers".into(),
"query".into(),
"query_param".into(),
"param".into(),
"form".into(),
"body".into(),
"json".into(),
"env".into(),
"var".into(),
"env_var".into(),
"getenv".into(),
"read".into(),
"read_to_string".into(),
"read_line".into(),
"recv".into(),
"receive".into(),
"stdin".into(),
"readline".into(),
],
command_score_mod: -30,
auth_score_mod: 25,
input_score_mod: 15,
external_input_score_mod: 20,
}
}
}
pub struct RhsAnalyzer {
config: RhsConfig,
}
impl RhsAnalyzer {
pub fn new(config: RhsConfig) -> Self {
Self { config }
}
pub fn analyze(&self, variable: &ComparisonSide) -> Vec<ScoreFactor> {
let mut factors = Vec::new();
match variable {
ComparisonSide::Variable { name, source } => {
factors.extend(self.analyze_variable_name(name));
if let Some(src) = source {
factors.extend(self.analyze_variable_source(src));
}
}
ComparisonSide::FieldAccess { base, field } => {
factors.extend(self.analyze_variable_name(field));
factors.extend(self.analyze_variable_name(base));
}
ComparisonSide::MethodCall {
receiver,
method,
args,
} => {
factors.extend(self.analyze_method_call(receiver, method, args));
}
ComparisonSide::FunctionCall { path, args } => {
factors.extend(self.analyze_function_call(path, args));
}
_ => {}
}
factors
}
fn analyze_variable_name(&self, name: &str) -> Vec<ScoreFactor> {
let mut factors = Vec::new();
if prebuilt::command_like().is_match(name) {
factors.push(
ScoreFactor::new(
"rhs_command_like",
FactorCategory::RightHandSide,
self.config.command_score_mod,
"Variable name suggests routing/command handling",
)
.with_evidence(format!("Variable: {}", name)),
);
}
if prebuilt::auth_like().is_match(name) {
factors.push(
ScoreFactor::new(
"rhs_auth_like",
FactorCategory::RightHandSide,
self.config.auth_score_mod,
"Variable name suggests authentication data",
)
.with_evidence(format!("Variable: {}", name)),
);
}
if prebuilt::input_like().is_match(name) {
factors.push(
ScoreFactor::new(
"rhs_input_like",
FactorCategory::RightHandSide,
self.config.input_score_mod,
"Variable name suggests user/external input",
)
.with_evidence(format!("Variable: {}", name)),
);
}
factors
}
fn analyze_variable_source(&self, source: &VariableSource) -> Vec<ScoreFactor> {
let mut factors = Vec::new();
let (score, reason) = match source {
VariableSource::Parameter => (10, "Value comes from function parameter"),
VariableSource::Environment => (20, "Value comes from environment variable"),
VariableSource::Header => (25, "Value comes from HTTP header"),
VariableSource::QueryParam => (20, "Value comes from query parameter"),
VariableSource::RequestBody => (20, "Value comes from request body"),
VariableSource::FileRead => (15, "Value comes from file read"),
VariableSource::Stdin => (25, "Value comes from user input"),
VariableSource::Database => (15, "Value comes from database"),
VariableSource::Unknown => (0, "Unknown source"),
};
if score > 0 {
factors.push(
ScoreFactor::new("rhs_source", FactorCategory::RightHandSide, score, reason)
.with_evidence(format!("Source: {:?}", source)),
);
}
factors
}
fn analyze_method_call(
&self,
receiver: &str,
method: &str,
_args: &[String],
) -> Vec<ScoreFactor> {
let mut factors = Vec::new();
let lower_method = simd::to_ascii_lowercase(method);
if self
.config
.external_input_functions
.iter()
.any(|f| lower_method.contains(f))
{
factors.push(
ScoreFactor::new(
"rhs_external_input",
FactorCategory::RightHandSide,
self.config.external_input_score_mod,
"Value comes from external input method",
)
.with_evidence(format!("{}.{}()", receiver, method)),
);
}
factors.extend(self.analyze_variable_name(receiver));
factors
}
fn analyze_function_call(&self, path: &str, _args: &[String]) -> Vec<ScoreFactor> {
let mut factors = Vec::new();
let lower_path = simd::to_ascii_lowercase(path);
if self
.config
.external_input_functions
.iter()
.any(|f| lower_path.contains(f))
{
factors.push(
ScoreFactor::new(
"rhs_external_function",
FactorCategory::RightHandSide,
self.config.external_input_score_mod,
"Value comes from external input function",
)
.with_evidence(format!("Function: {}", path)),
);
}
if lower_path.contains("env::var") || lower_path.contains("std::env") {
factors.push(ScoreFactor::new(
"rhs_env_var",
FactorCategory::RightHandSide,
15,
"Value comes from environment variable",
));
}
factors
}
pub fn looks_like_routing(&self, variable: &ComparisonSide) -> bool {
let name = match variable {
ComparisonSide::Variable { name, .. } => Some(name.as_str()),
ComparisonSide::FieldAccess { field, .. } => Some(field.as_str()),
_ => None,
};
if let Some(n) = name {
prebuilt::command_like().is_match(n)
} else {
false
}
}
pub fn looks_like_auth(&self, variable: &ComparisonSide) -> bool {
let name = match variable {
ComparisonSide::Variable { name, .. } => Some(name.as_str()),
ComparisonSide::FieldAccess { field, .. } => Some(field.as_str()),
_ => None,
};
if let Some(n) = name {
prebuilt::auth_like().is_match(n)
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_variable_reduces_score() {
let analyzer = RhsAnalyzer::new(RhsConfig::default());
let var = ComparisonSide::Variable {
name: "command".into(),
source: None,
};
let factors = analyzer.analyze(&var);
let total: i32 = factors.iter().map(|f| f.contribution).sum();
assert!(total < 0, "command variable should reduce score");
}
#[test]
fn test_token_variable_increases_score() {
let analyzer = RhsAnalyzer::new(RhsConfig::default());
let var = ComparisonSide::Variable {
name: "auth_token".into(),
source: None,
};
let factors = analyzer.analyze(&var);
let total: i32 = factors.iter().map(|f| f.contribution).sum();
assert!(total > 0, "auth_token variable should increase score");
}
#[test]
fn test_header_method_increases_score() {
let analyzer = RhsAnalyzer::new(RhsConfig::default());
let var = ComparisonSide::MethodCall {
receiver: "request".into(),
method: "header".into(),
args: vec!["Authorization".into()],
};
let factors = analyzer.analyze(&var);
let total: i32 = factors.iter().map(|f| f.contribution).sum();
assert!(total > 0, "header() call should increase score");
}
}