use std::sync::OnceLock;
use regex::Regex;
use crate::impl_rule;
use crate::rules::common::{
confidence_for_hops, csharp_hardcoded_secret_re, get_source_line, is_secret_value_long_enough,
make_finding, walk_tree,
};
use crate::rules::csharp_taint;
use crate::{Finding, Language, Severity};
fn cs_cors_star_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r#"WithOrigins\s*\(\s*"\*"\s*\)"#).expect("static C# CORS regex should compile")
})
}
fn is_string_literal(node: tree_sitter::Node) -> bool {
matches!(
node.kind(),
"string_literal"
| "verbatim_string_literal"
| "interpolated_string_expression"
| "string_literal_expression"
)
}
fn is_tainting_string_concat(
node: tree_sitter::Node,
root: tree_sitter::Node,
source: &str,
) -> bool {
if node.kind() == "binary_expression" {
if let Some(op) = node.child_by_field_name("operator") {
if &source[op.byte_range()] == "+" {
let mut operands = Vec::new();
collect_concat_operands(node, source, &mut operands);
let has_non_literal = operands
.iter()
.any(|operand| !is_literal_operand(*operand, root, source));
if has_non_literal {
return true;
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if is_tainting_string_concat(child, root, source) {
return true;
}
}
false
}
fn collect_concat_operands<'a>(
node: tree_sitter::Node<'a>,
source: &str,
out: &mut Vec<tree_sitter::Node<'a>>,
) {
if node.kind() == "binary_expression" {
if let Some(op) = node.child_by_field_name("operator") {
if &source[op.byte_range()] == "+" {
if let (Some(left), Some(right)) = (
node.child_by_field_name("left"),
node.child_by_field_name("right"),
) {
collect_concat_operands(left, source, out);
collect_concat_operands(right, source, out);
return;
}
}
}
}
out.push(node);
}
fn is_literal_operand(node: tree_sitter::Node, root: tree_sitter::Node, source: &str) -> bool {
let mut n = node;
while n.kind() == "parenthesized_expression" {
match n.named_child(0) {
Some(inner) => n = inner,
None => break,
}
}
if is_string_literal(n) {
return true;
}
if n.kind() == "identifier" {
let name = &source[n.byte_range()];
return identifier_is_const_string(name, n, root, source);
}
false
}
fn collect_declarators_named<'a>(
node: tree_sitter::Node<'a>,
name: &str,
source: &'a str,
out: &mut Vec<tree_sitter::Node<'a>>,
) {
if node.kind() == "variable_declarator" {
if let Some(name_node) = node.child_by_field_name("name") {
if &source[name_node.byte_range()] == name {
out.push(node);
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_declarators_named(child, name, source, out);
}
}
fn enclosing_scope<'a>(node: tree_sitter::Node<'a>) -> tree_sitter::Node<'a> {
let mut cur = node;
while let Some(parent) = cur.parent() {
if matches!(
parent.kind(),
"block"
| "method_declaration"
| "constructor_declaration"
| "accessor_declaration"
| "local_function_statement"
) {
return parent;
}
cur = parent;
}
cur
}
fn resolve_single_declarator<'a>(
name: &str,
use_site: tree_sitter::Node<'a>,
root: tree_sitter::Node<'a>,
source: &'a str,
) -> Option<tree_sitter::Node<'a>> {
let scope = enclosing_scope(use_site);
let mut local: Vec<tree_sitter::Node> = Vec::new();
collect_declarators_named(scope, name, source, &mut local);
if local.len() == 1 {
return Some(local[0]);
}
if !local.is_empty() {
return None;
}
let mut global: Vec<tree_sitter::Node> = Vec::new();
collect_declarators_named(root, name, source, &mut global);
if global.len() == 1 {
Some(global[0])
} else {
None
}
}
fn identifier_is_const_string(
name: &str,
use_site: tree_sitter::Node,
root: tree_sitter::Node,
source: &str,
) -> bool {
resolve_single_declarator(name, use_site, root, source)
.and_then(declarator_initializer)
.map(is_string_literal)
.unwrap_or(false)
}
fn declarator_initializer(decl: tree_sitter::Node) -> Option<tree_sitter::Node> {
let mut init: Option<tree_sitter::Node> = None;
let mut cursor = decl.walk();
for child in decl.children(&mut cursor) {
match child.kind() {
"equals_value_clause" => {
init = child.named_child(0);
}
"identifier" | "=" => {}
_ => {
if init.is_none() && child.is_named() {
init = Some(child);
}
}
}
}
init
}
fn is_sanitizer_call_text(text: &str) -> bool {
let t = text.trim();
if !t.contains('(') {
return false;
}
let callee = t.split('(').next().unwrap_or(t);
let method = callee.rsplit('.').next().unwrap_or(callee).trim();
let lower = method.to_ascii_lowercase();
lower.starts_with("validate") || lower.starts_with("sanitize") || lower.starts_with("allowlist")
}
fn is_safe_path_call_text(text: &str) -> bool {
let t = text.trim_start();
t.starts_with("Path.Combine") || t.starts_with("Path.GetFullPath") || t.starts_with("Path.Join")
}
fn argument_identifier_name<'a>(arg: tree_sitter::Node<'a>, source: &'a str) -> Option<&'a str> {
let mut n = arg;
if n.kind() == "argument" {
n = n.named_child(0)?;
}
while n.kind() == "parenthesized_expression" {
n = n.named_child(0)?;
}
if n.kind() == "identifier" {
Some(&source[n.byte_range()])
} else {
None
}
}
fn identifier_is_safe(
name: &str,
use_site: tree_sitter::Node,
root: tree_sitter::Node,
source: &str,
) -> bool {
let decl = match resolve_single_declarator(name, use_site, root, source) {
Some(d) => d,
None => return false,
};
let init = match declarator_initializer(decl) {
Some(i) => i,
None => return false,
};
if is_string_literal(init) {
return true;
}
let init_text = &source[init.byte_range()];
is_safe_path_call_text(init_text) || is_sanitizer_call_text(init_text)
}
fn sink_argument_is_safe(arg: tree_sitter::Node, root: tree_sitter::Node, source: &str) -> bool {
let mut inner = arg;
if inner.kind() == "argument" {
if let Some(c) = inner.named_child(0) {
inner = c;
}
}
if is_string_literal(inner) {
return true;
}
let arg_text = &source[inner.byte_range()];
if arg_text.starts_with('"') || arg_text.starts_with("@\"") || arg_text.starts_with("$\"") {
return true;
}
if is_safe_path_call_text(arg_text) || is_sanitizer_call_text(arg_text) {
return true;
}
if let Some(name) = argument_identifier_name(arg, source) {
if identifier_is_safe(name, arg, root, source) {
return true;
}
}
false
}
pub struct NoSqlInjection;
impl_rule! {
NoSqlInjection,
id = "cs/no-sql-injection",
severity = Severity::Critical,
cwe = Some("CWE-89"),
description = "Potential SQL injection via string concatenation in database call",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
let sql_methods = [
"ExecuteReader",
"ExecuteNonQuery",
"ExecuteScalar",
"FromSqlRaw",
];
let root = tree.root_node();
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "invocation_expression" {
let node_text = &src[node.byte_range()];
let has_sql_method = sql_methods.iter().any(|m| node_text.contains(m));
if has_sql_method {
if let Some(args) = node.child_by_field_name("arguments") {
if is_tainting_string_concat(args, root, src) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"SQL query built with string concatenation — use parameterized queries",
node,
src,
));
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "argument_list" && is_tainting_string_concat(child, root, src) {
if node.child_by_field_name("arguments").is_none() {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"SQL query built with string concatenation — use parameterized queries",
node,
src,
));
}
}
}
}
}
});
findings
}
}
pub struct NoCommandInjection;
impl_rule! {
NoCommandInjection,
id = "cs/no-command-injection",
severity = Severity::Critical,
cwe = Some("CWE-78"),
description = "Potential command injection via Process.Start with dynamic argument",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "invocation_expression" {
let node_text = &src[node.byte_range()];
if node_text.contains("Process.Start") {
let root = tree.root_node();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "argument_list" {
let mut arg_cursor = child.walk();
for arg in child.named_children(&mut arg_cursor) {
if !sink_argument_is_safe(arg, root, src) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"Process.Start called with dynamic argument — risk of command injection",
node,
src,
));
return;
}
}
}
}
}
}
});
findings
}
}
pub struct NoUnsafeDeserialization;
impl_rule! {
NoUnsafeDeserialization,
id = "cs/no-unsafe-deserialization",
severity = Severity::Critical,
cwe = Some("CWE-502"),
description = "Use of unsafe deserialization API",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
let unsafe_patterns = ["BinaryFormatter", "JavaScriptSerializer"];
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "invocation_expression" {
let node_text = &src[node.byte_range()];
if (node_text.contains("BinaryFormatter") && node_text.contains("Deserialize"))
|| (node_text.contains("JavaScriptSerializer")
&& node_text.contains("Deserialize"))
{
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"Unsafe deserialization — BinaryFormatter/JavaScriptSerializer can execute arbitrary code",
node,
src,
));
}
}
if node.kind() == "object_creation_expression" {
let node_text = &src[node.byte_range()];
for pattern in &unsafe_patterns {
if node_text.contains(pattern) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
&format!(
"new {}() — this type is inherently unsafe for deserialization",
pattern
),
node,
src,
));
}
}
}
});
findings
}
}
pub struct NoSsrf;
impl_rule! {
NoSsrf,
id = "cs/no-ssrf",
severity = Severity::High,
cwe = Some("CWE-918"),
description = "Potential SSRF via HTTP request with dynamic URL",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
let ssrf_methods = ["GetAsync", "PostAsync", "SendAsync", "GetStringAsync"];
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "invocation_expression" {
let node_text = &src[node.byte_range()];
let has_http_method = ssrf_methods.iter().any(|m| node_text.contains(m));
let has_webrequest = node_text.contains("WebRequest.Create");
if has_http_method || has_webrequest {
let root = tree.root_node();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "argument_list" {
if let Some(first_arg) = child.named_child(0) {
if !sink_argument_is_safe(first_arg, root, src) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"HTTP request with dynamic URL — validate and allowlist target hosts to prevent SSRF",
node,
src,
));
return;
}
}
}
}
}
}
});
findings
}
}
pub struct NoPathTraversal;
impl_rule! {
NoPathTraversal,
id = "cs/no-path-traversal",
severity = Severity::High,
cwe = Some("CWE-22"),
description = "Potential path traversal via dynamic file path",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
let file_methods = [
"File.ReadAllText",
"File.ReadAllBytes",
"File.Open",
"File.OpenRead",
"File.WriteAllText",
"File.Delete",
"File.Exists",
];
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "invocation_expression" {
let node_text = &src[node.byte_range()];
let has_file_method = file_methods.iter().any(|m| node_text.contains(m));
let has_stream_reader =
node_text.contains("StreamReader") && node.kind() == "invocation_expression";
if has_file_method {
let root = tree.root_node();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "argument_list" {
if let Some(first_arg) = child.named_child(0) {
if !sink_argument_is_safe(first_arg, root, src) {
let mut f = make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"File operation with dynamic path — validate and sanitize to prevent path traversal",
node,
src,
);
f.fix_suggestion = Some("Validate file paths with Path.GetFullPath() and ensure they don't escape the intended directory".to_string());
findings.push(f);
return;
}
}
}
}
}
let _ = has_stream_reader;
}
if node.kind() == "object_creation_expression" {
let node_text = &src[node.byte_range()];
let is_stream_reader = node_text.contains("StreamReader");
let is_file_stream = node_text.contains("FileStream");
if is_stream_reader || is_file_stream {
let root = tree.root_node();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "argument_list" {
if let Some(first_arg) = child.named_child(0) {
if !sink_argument_is_safe(first_arg, root, src) {
{
let type_name = if is_stream_reader {
"StreamReader"
} else {
"FileStream"
};
let mut f = make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
&format!(
"new {} with dynamic path — validate and sanitize to prevent path traversal",
type_name
),
node,
src,
);
f.fix_suggestion = Some("Validate file paths with Path.GetFullPath() and ensure they don't escape the intended directory".to_string());
findings.push(f);
return;
}
}
}
}
}
}
}
});
findings
}
}
pub struct NoWeakCrypto;
impl_rule! {
NoWeakCrypto,
id = "cs/no-weak-crypto",
severity = Severity::Medium,
cwe = Some("CWE-327"),
description = "Use of weak cryptographic algorithm",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
let weak_algos = [
("MD5", "MD5.Create"),
("SHA1", "SHA1.Create"),
("DES", "DES.Create"),
("DES", "DESCryptoServiceProvider"),
("RC2", "RC2.Create"),
("RC2", "RC2CryptoServiceProvider"),
];
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "invocation_expression" || node.kind() == "object_creation_expression"
{
let node_text = &src[node.byte_range()];
for (algo, pattern) in &weak_algos {
if node_text.contains(pattern) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
&format!("{} is cryptographically weak — use AES or SHA-256+", algo),
node,
src,
));
return;
}
}
}
});
findings
}
}
pub struct NoHardcodedSecret;
impl_rule! {
NoHardcodedSecret,
id = "cs/no-hardcoded-secret",
severity = Severity::High,
cwe = Some("CWE-798"),
description = "Hardcoded secret or credential detected",
language = Language::CSharp,
fn check_with_context(_self, source, tree, ctx) {
let mut findings = Vec::new();
let secret_pattern = csharp_hardcoded_secret_re();
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "variable_declarator" {
if let Some(name_node) = node.child_by_field_name("name") {
let name = &src[name_node.byte_range()];
if secret_pattern.is_match(name) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "string_literal"
|| child.kind() == "verbatim_string_literal"
|| child.kind() == "interpolated_string_expression"
{
let val = &src[child.byte_range()];
let trimmed = val.trim_matches(|c| c == '"' || c == '@');
let trimmed = trimmed.trim_matches('"');
if is_secret_value_long_enough(trimmed, ctx.secret_thresholds) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
&format!(
"Hardcoded secret in '{}' — use environment variables or a secret manager",
name
),
node,
src,
));
return;
}
}
}
}
}
}
if node.kind() == "assignment_expression" {
if let Some(left) = node.child_by_field_name("left") {
let left_text = &src[left.byte_range()];
if secret_pattern.is_match(left_text) {
if let Some(right) = node.child_by_field_name("right") {
if is_string_literal(right) || right.kind() == "string_literal" {
let val = &src[right.byte_range()];
let trimmed = val.trim_matches(|c| c == '"' || c == '@');
let trimmed = trimmed.trim_matches('"');
if is_secret_value_long_enough(trimmed, ctx.secret_thresholds) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
&format!(
"Hardcoded secret in '{}' — use environment variables or a secret manager",
left_text.trim()
),
node,
src,
));
}
}
}
}
}
}
});
findings
}
}
pub struct NoXxe;
impl_rule! {
NoXxe,
id = "cs/no-xxe",
severity = Severity::High,
cwe = Some("CWE-611"),
description = "Potential XXE vulnerability in XML parsing",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
let resolver_nulled = {
source.contains("XmlResolver")
&& source
.split("XmlResolver")
.skip(1)
.any(|after| {
let trimmed = after.trim_start();
let rest = trimmed.strip_prefix('=').map(|r| r.trim_start());
matches!(rest, Some(r) if r.starts_with("null"))
})
};
let has_dtd_prohibit = source.contains("DtdProcessing.Prohibit")
|| source.contains("ProhibitDtd = true")
|| resolver_nulled;
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "invocation_expression" {
let node_text = &src[node.byte_range()];
if ((node_text.contains("XmlDocument") && node_text.contains("Load"))
|| (node_text.contains("XmlReader") && node_text.contains("Create"))
|| node_text.contains("XmlTextReader"))
&& !has_dtd_prohibit
{
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"XML parsing without DtdProcessing.Prohibit — vulnerable to XXE attacks",
node,
src,
));
}
}
if node.kind() == "object_creation_expression" {
let node_text = &src[node.byte_range()];
if (node_text.contains("XmlDocument") || node_text.contains("XmlTextReader"))
&& !has_dtd_prohibit
{
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"XML parser created without disabling DTD processing — vulnerable to XXE attacks",
node,
src,
));
}
}
});
findings
}
}
pub struct NoLdapInjection;
impl_rule! {
NoLdapInjection,
id = "cs/no-ldap-injection",
severity = Severity::High,
cwe = Some("CWE-90"),
description = "Potential LDAP injection via string concatenation in search filter",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
let root = tree.root_node();
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "assignment_expression" {
if let Some(left) = node.child_by_field_name("left") {
let left_text = &src[left.byte_range()];
if left_text.contains("Filter")
&& (left_text.contains("DirectorySearcher")
|| left_text.contains("searcher")
|| left_text.ends_with(".Filter"))
{
if let Some(right) = node.child_by_field_name("right") {
if is_tainting_string_concat(right, root, src) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"LDAP filter built with string concatenation — use parameterized filters to prevent LDAP injection",
node,
src,
));
}
}
}
}
}
if node.kind() == "object_creation_expression" {
let node_text = &src[node.byte_range()];
if node_text.contains("DirectorySearcher") {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "argument_list" && is_tainting_string_concat(child, root, src) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"DirectorySearcher created with concatenated filter — use parameterized filters to prevent LDAP injection",
node,
src,
));
return;
}
}
}
}
});
findings
}
}
pub struct NoCorsStar;
impl_rule! {
NoCorsStar,
id = "cs/no-cors-star",
severity = Severity::Medium,
cwe = Some("CWE-942"),
description = "Overly permissive CORS configuration",
language = Language::CSharp,
fn check(_self, source, tree) {
let mut findings = Vec::new();
let cors_star = cs_cors_star_re();
walk_tree(tree.root_node(), source, &mut |node, src| {
if node.kind() == "invocation_expression" {
let func_child = node.child(0);
let func_text = func_child.map(|c| &src[c.byte_range()]).unwrap_or("");
if func_text.ends_with("AllowAnyOrigin") || func_text.ends_with(".AllowAnyOrigin") {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"AllowAnyOrigin() permits requests from any domain — restrict CORS origins",
node,
src,
));
} else if func_text.ends_with("WithOrigins") {
let node_text = &src[node.byte_range()];
if cors_star.is_match(node_text) {
findings.push(make_finding(
_self.id(),
_self.severity(),
_self.cwe(),
"WithOrigins(\"*\") permits requests from any domain — restrict CORS origins",
node,
src,
));
}
}
}
});
findings
}
}
struct CSharpTaintRuleMeta<'a> {
rule_id: &'a str,
severity: Severity,
cwe: Option<&'a str>,
fix_suggestion: Option<&'a str>,
format_description: fn(&str, &str) -> String,
}
fn csharp_taint_sql_injection_desc(src: &str, sink: &str) -> String {
format!("{src} reaches {sink} — untrusted ASP.NET input can inject SQL")
}
fn csharp_taint_command_injection_desc(src: &str, sink: &str) -> String {
format!("{src} reaches {sink} — untrusted ASP.NET input can inject OS commands")
}
fn csharp_taint_xss_desc(src: &str, sink: &str) -> String {
format!("{src} reaches {sink} — untrusted input reaches an HTML output sink (XSS)")
}
fn csharp_taint_open_redirect_desc(src: &str, sink: &str) -> String {
format!("{src} reaches {sink} — untrusted input can drive an open redirect")
}
fn csharp_taint_xxe_desc(src: &str, sink: &str) -> String {
format!("{src} reaches {sink} — untrusted input reaches an XML parser sink (XXE)")
}
fn csharp_taint_unsafe_load_desc(src: &str, sink: &str) -> String {
format!("{src} reaches {sink} — untrusted input can load arbitrary code or types (unsafe load)")
}
fn csharp_taint_meta(rule_id: &str) -> Option<CSharpTaintRuleMeta<'static>> {
match rule_id {
"csharp/taint-sql-injection" => Some(CSharpTaintRuleMeta {
rule_id: "csharp/taint-sql-injection",
severity: Severity::Critical,
cwe: Some("CWE-89"),
fix_suggestion: Some(
"Use parameterized queries (SqlCommand.Parameters.AddWithValue) instead of concatenating request input into SQL",
),
format_description: csharp_taint_sql_injection_desc,
}),
"csharp/taint-command-injection" => Some(CSharpTaintRuleMeta {
rule_id: "csharp/taint-command-injection",
severity: Severity::Critical,
cwe: Some("CWE-78"),
fix_suggestion: Some(
"Avoid invoking shell commands with request-controlled data; pass fixed executable names and validated argument arrays",
),
format_description: csharp_taint_command_injection_desc,
}),
"csharp/taint-xss" => Some(CSharpTaintRuleMeta {
rule_id: "csharp/taint-xss",
severity: Severity::High,
cwe: Some("CWE-79"),
fix_suggestion: Some(
"HTML-encode untrusted values before writing them to the response (HttpUtility.HtmlEncode)",
),
format_description: csharp_taint_xss_desc,
}),
"csharp/taint-open-redirect" => Some(CSharpTaintRuleMeta {
rule_id: "csharp/taint-open-redirect",
severity: Severity::Medium,
cwe: Some("CWE-601"),
fix_suggestion: Some(
"Validate redirect targets against an allowlist of permitted destinations",
),
format_description: csharp_taint_open_redirect_desc,
}),
"csharp/taint-xxe" => Some(CSharpTaintRuleMeta {
rule_id: "csharp/taint-xxe",
severity: Severity::High,
cwe: Some("CWE-611"),
fix_suggestion: Some(
"Resolve untrusted XML only through a parser configured with DTD/entity processing disabled",
),
format_description: csharp_taint_xxe_desc,
}),
"csharp/taint-unsafe-load" => Some(CSharpTaintRuleMeta {
rule_id: "csharp/taint-unsafe-load",
severity: Severity::Critical,
cwe: Some("CWE-502"),
fix_suggestion: Some(
"Do not load assemblies or activate types from request-controlled input; bind against a fixed, validated type set",
),
format_description: csharp_taint_unsafe_load_desc,
}),
_ => None,
}
}
fn map_csharp_taint_finding(
meta: &CSharpTaintRuleMeta<'_>,
source: &str,
finding: csharp_taint::TaintFinding,
) -> Finding {
Finding {
rule_id: meta.rule_id.to_string(),
severity: meta.severity,
cwe: meta.cwe.map(|s| s.to_string()),
description: (meta.format_description)(
&finding.source_description,
&finding.sink_description,
),
file: String::new(),
line: finding.sink_line,
column: finding.sink_column,
end_line: finding.sink_end_line,
end_column: finding.sink_end_column,
snippet: get_source_line(source, finding.sink_start_byte),
source_line: Some(finding.source_line),
source_description: Some(finding.source_description),
sink_line: Some(finding.sink_line),
sink_description: Some(finding.sink_description),
fix_suggestion: meta.fix_suggestion.map(|s| s.to_string()),
sink_start_byte: Some(finding.sink_start_byte),
sink_end_byte: Some(finding.sink_end_byte),
confidence: confidence_for_hops(finding.hops),
taint_hops: Some(finding.hops),
tags: vec![],
crypto_algorithm: None,
cnsa2_deadline: None,
dep_name: None,
dep_version: None,
dep_ecosystem: None,
dep_purl: None,
dep_vulnerability_id: None,
dep_fixed_version: None,
dep_source: None,
dep_vulnerability_severity: None,
dep_path: vec![],
crypto_material: None,
}
}
pub fn run_csharp_taint_batched(
source: &str,
tree: &tree_sitter::Tree,
ctx: &crate::rules::FileContext<'_>,
enabled_rule_ids: &std::collections::HashSet<&str>,
) -> Vec<Finding> {
let mut findings = Vec::new();
let rule_specs = csharp_taint::csharp_taint_rule_specs();
for (rule_id, spec) in &rule_specs {
if !enabled_rule_ids.contains(rule_id) {
continue;
}
let Some(meta) = csharp_taint_meta(rule_id) else {
continue;
};
let raw = csharp_taint::analyze_tree(tree.root_node(), source, spec, None);
for finding in raw {
findings.push(map_csharp_taint_finding(&meta, source, finding));
}
}
if let (Some(summaries), Some(paths)) = (
ctx.cross_file_summaries,
ctx.csharp_same_package_paths.as_ref(),
) {
let allowed: std::collections::HashSet<String> =
enabled_rule_ids.iter().map(|id| id.to_string()).collect();
let enabled_specs: Vec<(&str, csharp_taint::TaintSpec)> = rule_specs
.iter()
.filter(|(id, _)| enabled_rule_ids.contains(id))
.map(|(id, spec)| (*id, spec.clone()))
.collect();
let cross = csharp_taint::CrossFileInfo {
same_package_paths: paths,
summaries,
allowed_rule_ids: &allowed,
};
let raw = csharp_taint::extract_cross_file_findings(
tree.root_node(),
source,
&enabled_specs,
&cross,
);
for finding in raw {
let Some(rule_id) = finding.rule_id_hint.as_deref() else {
continue;
};
let Some(meta) = csharp_taint_meta(rule_id) else {
continue;
};
findings.push(map_csharp_taint_finding(&meta, source, finding));
}
}
findings
}
fn run_csharp_taint_single(
rule_id: &str,
source: &str,
tree: &tree_sitter::Tree,
spec: &csharp_taint::TaintSpec,
) -> Vec<Finding> {
let Some(meta) = csharp_taint_meta(rule_id) else {
return Vec::new();
};
let raw = csharp_taint::analyze_tree(tree.root_node(), source, spec, None);
raw.into_iter()
.map(|finding| map_csharp_taint_finding(&meta, source, finding))
.collect()
}
pub struct TaintSqlInjection;
impl_rule! {
TaintSqlInjection,
id = "csharp/taint-sql-injection",
severity = Severity::Critical,
cwe = Some("CWE-89"),
description = "Untrusted ASP.NET request input reaches a SQL query sink",
language = Language::CSharp,
fn check(_self, source, tree) {
let spec = csharp_taint::csharp_taint_rule_specs()
.into_iter()
.find(|(id, _)| *id == _self.id())
.map(|(_, spec)| spec)
.unwrap_or_default();
run_csharp_taint_single(_self.id(), source, tree, &spec)
}
}
pub struct TaintCommandInjection;
impl_rule! {
TaintCommandInjection,
id = "csharp/taint-command-injection",
severity = Severity::Critical,
cwe = Some("CWE-78"),
description = "Untrusted ASP.NET request input reaches a command execution sink",
language = Language::CSharp,
fn check(_self, source, tree) {
let spec = csharp_taint::csharp_taint_rule_specs()
.into_iter()
.find(|(id, _)| *id == _self.id())
.map(|(_, spec)| spec)
.unwrap_or_default();
run_csharp_taint_single(_self.id(), source, tree, &spec)
}
}
pub struct TaintXss;
impl_rule! {
TaintXss,
id = "csharp/taint-xss",
severity = Severity::High,
cwe = Some("CWE-79"),
description = "Untrusted ASP.NET request input reaches an HTML output sink",
language = Language::CSharp,
fn check(_self, source, tree) {
let spec = csharp_taint::csharp_taint_rule_specs()
.into_iter()
.find(|(id, _)| *id == _self.id())
.map(|(_, spec)| spec)
.unwrap_or_default();
run_csharp_taint_single(_self.id(), source, tree, &spec)
}
}
pub struct TaintOpenRedirect;
impl_rule! {
TaintOpenRedirect,
id = "csharp/taint-open-redirect",
severity = Severity::Medium,
cwe = Some("CWE-601"),
description = "Untrusted ASP.NET request input reaches a redirect sink",
language = Language::CSharp,
fn check(_self, source, tree) {
let spec = csharp_taint::csharp_taint_rule_specs()
.into_iter()
.find(|(id, _)| *id == _self.id())
.map(|(_, spec)| spec)
.unwrap_or_default();
run_csharp_taint_single(_self.id(), source, tree, &spec)
}
}
pub struct TaintXxe;
impl_rule! {
TaintXxe,
id = "csharp/taint-xxe",
severity = Severity::High,
cwe = Some("CWE-611"),
description = "Untrusted ASP.NET request input reaches an XML parser sink",
language = Language::CSharp,
fn check(_self, source, tree) {
let spec = csharp_taint::csharp_taint_rule_specs()
.into_iter()
.find(|(id, _)| *id == _self.id())
.map(|(_, spec)| spec)
.unwrap_or_default();
run_csharp_taint_single(_self.id(), source, tree, &spec)
}
}
pub struct TaintUnsafeLoad;
impl_rule! {
TaintUnsafeLoad,
id = "csharp/taint-unsafe-load",
severity = Severity::Critical,
cwe = Some("CWE-502"),
description = "Untrusted ASP.NET request input reaches an assembly/type load sink",
language = Language::CSharp,
fn check(_self, source, tree) {
let spec = csharp_taint::csharp_taint_rule_specs()
.into_iter()
.find(|(id, _)| *id == _self.id())
.map(|(_, spec)| spec)
.unwrap_or_default();
run_csharp_taint_single(_self.id(), source, tree, &spec)
}
}