use super::{Rule, RuleCategory, RuleInfo, Severity, Violation};
use crate::query::{Query, QueryType};
pub struct TruncateDetected;
impl Rule for TruncateDetected {
fn info(&self) -> RuleInfo {
RuleInfo {
id: "SEC003",
name: "TRUNCATE statement detected",
severity: Severity::Error,
category: RuleCategory::Security
}
}
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation> {
if query.query_type != QueryType::Truncate {
return vec![];
}
let info = self.info();
let table_names = query.tables.join(", ");
vec![Violation {
rule_id: info.id,
rule_name: info.name,
message: format!(
"TRUNCATE removes all rows from table(s) '{}' without logging individual deletions",
table_names
),
severity: info.severity,
category: info.category,
suggestion: Some(
"Use DELETE with WHERE for safer data removal, or ensure backups exist"
.to_string()
),
query_index
}]
}
}
pub struct MissingWhereInUpdate;
impl Rule for MissingWhereInUpdate {
fn info(&self) -> RuleInfo {
RuleInfo {
id: "SEC001",
name: "UPDATE without WHERE",
severity: Severity::Error,
category: RuleCategory::Security
}
}
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation> {
if query.query_type != QueryType::Update {
return vec![];
}
if query.where_cols.is_empty() {
let info = self.info();
return vec![Violation {
rule_id: info.id,
rule_name: info.name,
message: "UPDATE statement without WHERE clause will affect all rows".to_string(),
severity: info.severity,
category: info.category,
suggestion: Some("Add WHERE clause to limit affected rows".to_string()),
query_index
}];
}
vec![]
}
}
pub struct DropDetected;
impl Rule for DropDetected {
fn info(&self) -> RuleInfo {
RuleInfo {
id: "SEC004",
name: "DROP statement detected",
severity: Severity::Error,
category: RuleCategory::Security
}
}
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation> {
if query.query_type != QueryType::Drop {
return vec![];
}
let info = self.info();
let object_type = query
.cte_names
.first()
.map(|s| s.as_str())
.unwrap_or("object");
let names = query.tables.join(", ");
vec![Violation {
rule_id: info.id,
rule_name: info.name,
message: format!(
"DROP {} '{}' permanently destroys data and schema",
object_type, names
),
severity: info.severity,
category: info.category,
suggestion: Some(
"Ensure this is intentional and backups exist before dropping".to_string()
),
query_index
}]
}
}
pub struct DynamicSqlExecution;
const DYNAMIC_SQL_OPENERS: [&str; 4] = ["EXEC(", "EXEC ", "EXECUTE", "PREPARE "];
impl Rule for DynamicSqlExecution {
fn info(&self) -> RuleInfo {
RuleInfo {
id: "SEC007",
name: "Dynamic SQL execution",
severity: Severity::Warning,
category: RuleCategory::Security
}
}
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation> {
let upper = query.raw.to_uppercase();
let trimmed = upper.trim_start();
if !DYNAMIC_SQL_OPENERS
.iter()
.any(|opener| trimmed.starts_with(opener))
{
return vec![];
}
let info = self.info();
vec![Violation {
rule_id: info.id,
rule_name: info.name,
message: "Dynamic SQL execution runs a string assembled at runtime".to_string(),
severity: info.severity,
category: info.category,
suggestion: Some(
"Validate every input that reaches the executed string and prefer parameterized execution (sp_executesql, prepared statements with bound parameters)"
.to_string()
),
query_index
}]
}
}
pub struct PrivilegeChange;
const DANGEROUS_GRANT_MARKERS: [&str; 4] = ["ALL PRIVILEGES", "ON *.*", "TO PUBLIC", "SUPERUSER"];
impl Rule for PrivilegeChange {
fn info(&self) -> RuleInfo {
RuleInfo {
id: "SEC005",
name: "GRANT/REVOKE privilege change",
severity: Severity::Warning,
category: RuleCategory::Security
}
}
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation> {
let upper = query.raw.to_uppercase();
let trimmed = upper.trim_start();
let is_grant = trimmed.starts_with("GRANT ");
if !is_grant && !trimmed.starts_with("REVOKE ") {
return vec![];
}
let dangerous = is_grant
&& DANGEROUS_GRANT_MARKERS
.iter()
.any(|marker| upper.contains(marker));
let info = self.info();
let (severity, message) = if dangerous {
(
Severity::Error,
"GRANT hands out broad or public privileges".to_string()
)
} else {
(
info.severity,
"Privilege change statement found in query set".to_string()
)
};
vec![Violation {
rule_id: info.id,
rule_name: info.name,
message,
severity,
category: info.category,
suggestion: Some(
"Keep GRANT/REVOKE in reviewed migrations and grant the narrowest privileges needed"
.to_string()
),
query_index
}]
}
}
pub struct HardcodedCredential;
const SENSITIVE_COLUMNS: [&str; 9] = [
"PASSWORD",
"PASSWD",
"PWD",
"SECRET",
"API_KEY",
"APIKEY",
"TOKEN",
"AUTH",
"CREDENTIAL"
];
fn sensitive_name_followed_by(upper: &str, name: &str, next: char) -> bool {
upper.match_indices(name).any(|(pos, _)| {
let after = &upper[pos + name.len()..];
let mut chars = after.chars();
match chars.next() {
Some(c) if c.is_ascii_alphanumeric() || c == '_' => return false,
None => return false,
_ => {}
}
after
.trim_start_matches(|c: char| c.is_whitespace())
.starts_with(next)
})
}
fn has_sensitive_assignment(upper: &str) -> bool {
SENSITIVE_COLUMNS.iter().any(|col| {
upper.match_indices(col).any(|(pos, _)| {
let after = &upper[pos + col.len()..];
let mut rest = after.trim_start();
match after.chars().next() {
Some(c) if c.is_ascii_alphanumeric() || c == '_' => return false,
None => return false,
_ => {}
}
if !rest.starts_with('=') {
return false;
}
rest = rest[1..].trim_start();
rest.starts_with('\'')
})
})
}
fn has_sensitive_insert(query: &Query, upper: &str) -> bool {
if query.query_type != QueryType::Insert {
return false;
}
let Some(values_pos) = upper.find(" VALUES") else {
return false;
};
let (columns_part, values_part) = upper.split_at(values_pos);
values_part.contains('\'')
&& SENSITIVE_COLUMNS.iter().any(|col| {
sensitive_name_followed_by(columns_part, col, ',')
|| sensitive_name_followed_by(columns_part, col, ')')
})
}
impl Rule for HardcodedCredential {
fn info(&self) -> RuleInfo {
RuleInfo {
id: "SEC008",
name: "Hardcoded credential detected",
severity: Severity::Error,
category: RuleCategory::Security
}
}
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation> {
let upper = query.raw.to_uppercase();
let ddl_credential = upper.contains("IDENTIFIED BY '")
|| upper.contains("WITH PASSWORD '")
|| upper.contains("SET PASSWORD");
if !ddl_credential
&& !has_sensitive_assignment(&upper)
&& !has_sensitive_insert(query, &upper)
{
return vec![];
}
let info = self.info();
vec![Violation {
rule_id: info.id,
rule_name: info.name,
message: "Possible hardcoded credential in SQL statement".to_string(),
severity: info.severity,
category: info.category,
suggestion: Some(
"Use environment variables, a secret manager, or parameterized values instead of plaintext secrets"
.to_string()
),
query_index
}]
}
}
pub struct InjectionTautology;
fn is_literal_token(tok: &str) -> bool {
(tok.starts_with('\'') && tok.ends_with('\'') && tok.len() >= 2)
|| (!tok.is_empty() && tok.chars().all(|c| c.is_ascii_digit()))
}
fn has_or_tautology(upper: &str) -> bool {
let mut search_from = 0;
while let Some(pos) = upper[search_from..].find(" OR ") {
let rest = &upper[search_from + pos + 4..];
let mut toks = rest.split_whitespace();
if let (Some(a), Some(op), Some(b)) = (toks.next(), toks.next(), toks.next())
&& op == "="
&& a == b
&& is_literal_token(a)
{
return true;
}
search_from += pos + 4;
}
false
}
impl Rule for InjectionTautology {
fn info(&self) -> RuleInfo {
RuleInfo {
id: "SEC006",
name: "Potential SQL injection pattern",
severity: Severity::Error,
category: RuleCategory::Security
}
}
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation> {
let upper = query.raw.to_uppercase();
if !has_or_tautology(&upper) {
return vec![];
}
let info = self.info();
vec![Violation {
rule_id: info.id,
rule_name: info.name,
message: "Query contains an always-true OR tautology, a classic SQL injection pattern"
.to_string(),
severity: info.severity,
category: info.category,
suggestion: Some(
"If this query is built in application code, replace string concatenation with parameterized queries"
.to_string()
),
query_index
}]
}
}
pub struct MissingWhereInDelete;
impl Rule for MissingWhereInDelete {
fn info(&self) -> RuleInfo {
RuleInfo {
id: "SEC002",
name: "DELETE without WHERE",
severity: Severity::Error,
category: RuleCategory::Security
}
}
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation> {
if query.query_type != QueryType::Delete {
return vec![];
}
if query.where_cols.is_empty() {
let info = self.info();
return vec![Violation {
rule_id: info.id,
rule_name: info.name,
message: "DELETE statement without WHERE clause will remove all rows".to_string(),
severity: info.severity,
category: info.category,
suggestion: Some("Add WHERE clause to limit deleted rows".to_string()),
query_index
}];
}
vec![]
}
}