use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils;
use std::collections::{HashMap, HashSet};
use tree_sitter::Node;
pub struct Fio05C;
impl CertRule for Fio05C {
fn rule_id(&self) -> &'static str {
"FIO05-C"
}
fn description(&self) -> &'static str {
"Identify files using multiple file attributes"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"FIO05-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
if node.kind() == "translation_unit" {
let mut analyzer = FileReopenAnalyzer::new();
analyzer.analyze_scope(node, source, &mut violations);
return violations;
}
if node.kind() == "function_definition" {
let mut analyzer = FileReopenAnalyzer::new();
analyzer.analyze_scope(node, source, &mut violations);
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
violations.extend(self.check(&child, source));
}
}
violations
}
}
struct FileReopenAnalyzer {
file_operations: HashMap<String, Vec<FileOperation>>,
fstat_calls: HashSet<String>,
has_attribute_comparison: bool,
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct FileOperation {
op_type: OpType,
filename: String,
fd_var: Option<String>, line: usize,
column: usize,
node_id: usize,
is_read_mode: bool, }
#[derive(Clone, Debug, PartialEq)]
enum OpType {
Open,
Close,
Fstat,
}
impl FileReopenAnalyzer {
fn new() -> Self {
Self {
file_operations: HashMap::new(),
fstat_calls: HashSet::new(),
has_attribute_comparison: false,
}
}
fn analyze_scope(
&mut self,
scope_node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
) {
if scope_node.kind() == "function_definition" {
if let Some(body) = scope_node.child_by_field_name("body") {
self.collect_operations(&body, source);
}
} else {
self.collect_operations(scope_node, source);
}
self.detect_reopen_violations(violations);
}
fn collect_operations(&mut self, node: &Node, source: &str) {
match node.kind() {
"call_expression" => {
self.process_call(node, source);
}
"assignment_expression" => {
self.process_assignment(node, source);
}
"declaration" => {
self.process_declaration(node, source);
}
"binary_expression" => {
self.check_attribute_comparison(node, source);
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.collect_operations(&child, source);
}
}
}
fn process_call(&mut self, node: &Node, source: &str) {
if let Some(function) = node.child_by_field_name("function") {
let func_name = ast_utils::get_node_text_owned(&function, source);
match func_name.as_str() {
"fopen" | "open" => {
self.process_open_call(node, source, &func_name);
}
"fclose" | "close" => {
self.process_close_call(node, source, &func_name);
}
"fstat" | "stat" | "lstat" => {
self.process_fstat_call(node, source, &func_name);
}
_ => {}
}
}
}
fn process_assignment(&mut self, node: &Node, source: &str) {
if let Some(right) = node.child_by_field_name("right") {
if right.kind() == "call_expression" {
if let Some(function) = right.child_by_field_name("function") {
let func_name = ast_utils::get_node_text_owned(&function, source);
if func_name == "fopen" || func_name == "open" {
if let Some(left) = node.child_by_field_name("left") {
let var_name = ast_utils::get_node_text_owned(&left, source);
self.process_open_call_with_var(
&right,
source,
&func_name,
Some(var_name),
);
}
}
}
}
}
}
fn process_declaration(&mut self, node: &Node, source: &str) {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "init_declarator" {
if let Some(declarator) = child.child_by_field_name("declarator") {
let var_name = self.extract_identifier_name(&declarator, source);
if let Some(value) = child.child_by_field_name("value") {
if value.kind() == "call_expression" {
if let Some(function) = value.child_by_field_name("function") {
let func_name =
ast_utils::get_node_text_owned(&function, source);
if func_name == "fopen" || func_name == "open" {
self.process_open_call_with_var(
&value,
source,
&func_name,
Some(var_name),
);
}
}
}
}
}
}
}
}
}
fn extract_identifier_name(&self, declarator: &Node, source: &str) -> String {
match declarator.kind() {
"identifier" => ast_utils::get_node_text_owned(declarator, source),
"pointer_declarator" | "array_declarator" | "function_declarator" => {
for i in 0..declarator.child_count() {
if let Some(child) = declarator.child(i) {
if child.kind() == "identifier" {
return ast_utils::get_node_text_owned(&child, source);
}
let name = self.extract_identifier_name(&child, source);
if !name.is_empty() {
return name;
}
}
}
String::new()
}
_ => String::new(),
}
}
fn process_open_call(&mut self, node: &Node, source: &str, func_name: &str) {
self.process_open_call_with_var(node, source, func_name, None);
}
fn process_open_call_with_var(
&mut self,
node: &Node,
source: &str,
func_name: &str,
fd_var: Option<String>,
) {
if let Some(arguments) = node.child_by_field_name("arguments") {
if let Some(filename_node) = self.get_first_argument(&arguments) {
let filename = ast_utils::get_node_text_owned(&filename_node, source);
let is_read_mode = self.is_read_mode(&arguments, source, func_name);
let op = FileOperation {
op_type: OpType::Open,
filename: filename.clone(),
fd_var,
line: node.start_position().row + 1,
column: node.start_position().column + 1,
node_id: node.id(),
is_read_mode,
};
self.file_operations.entry(filename).or_default().push(op);
}
}
}
fn is_read_mode(&self, arguments: &Node, source: &str, func_name: &str) -> bool {
let args: Vec<_> = (0..arguments.child_count())
.filter_map(|i| arguments.child(i))
.filter(|n| !matches!(n.kind(), "," | "(" | ")"))
.collect();
if func_name == "fopen" && args.len() >= 2 {
let mode = ast_utils::get_node_text_owned(&args[1], source);
return mode.contains("\"r") || mode.contains("'r");
}
if func_name == "open" && args.len() >= 2 {
let flags = ast_utils::get_node_text_owned(&args[1], source);
let has_rdonly = flags.contains("O_RDONLY");
let has_rdwr = flags.contains("O_RDWR");
let has_creat = flags.contains("O_CREAT");
return (has_rdonly || has_rdwr) && !has_creat;
}
false
}
fn process_close_call(&mut self, node: &Node, source: &str, _func_name: &str) {
if let Some(arguments) = node.child_by_field_name("arguments") {
if let Some(fd_node) = self.get_first_argument(&arguments) {
let fd_var = ast_utils::get_node_text_owned(&fd_node, source);
for (filename, ops) in &mut self.file_operations {
if let Some(_last_open) = ops.iter().rev().find(|op| {
op.op_type == OpType::Open
&& op.fd_var.as_ref().map(|v| v == &fd_var).unwrap_or(false)
}) {
let close_op = FileOperation {
op_type: OpType::Close,
filename: filename.clone(),
fd_var: Some(fd_var.clone()),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
node_id: node.id(),
is_read_mode: false,
};
ops.push(close_op);
break;
}
}
}
}
}
fn process_fstat_call(&mut self, node: &Node, source: &str, _func_name: &str) {
if let Some(arguments) = node.child_by_field_name("arguments") {
if let Some(fd_node) = self.get_first_argument(&arguments) {
let fd_var = ast_utils::get_node_text_owned(&fd_node, source);
self.fstat_calls.insert(fd_var.clone());
for (filename, ops) in &mut self.file_operations {
if let Some(_last_open) = ops.iter().rev().find(|op| {
op.op_type == OpType::Open
&& op.fd_var.as_ref().map(|v| v == &fd_var).unwrap_or(false)
}) {
let fstat_op = FileOperation {
op_type: OpType::Fstat,
filename: filename.clone(),
fd_var: Some(fd_var),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
node_id: node.id(),
is_read_mode: false,
};
ops.push(fstat_op);
break;
}
}
}
}
}
fn check_attribute_comparison(&mut self, node: &Node, source: &str) {
let text = ast_utils::get_node_text_owned(node, source);
if text.contains("st_dev")
|| text.contains("st_ino")
|| text.contains("st_mode")
|| text.contains("st_uid")
|| text.contains("st_gid")
{
self.has_attribute_comparison = true;
}
}
fn get_first_argument<'a>(&self, arguments: &'a Node) -> Option<Node<'a>> {
for i in 0..arguments.child_count() {
if let Some(arg) = arguments.child(i) {
if !matches!(arg.kind(), "," | "(" | ")") {
return Some(arg);
}
}
}
None
}
fn detect_reopen_violations(&self, violations: &mut Vec<RuleViolation>) {
for (filename, ops) in &self.file_operations {
self.check_reopen_pattern(filename, ops, violations);
self.check_unvalidated_open(filename, ops, violations);
}
}
fn check_unvalidated_open(
&self,
filename: &str,
ops: &[FileOperation],
violations: &mut Vec<RuleViolation>,
) {
let trimmed = filename.trim();
if trimmed.starts_with('"') || trimmed.starts_with('\'') {
return;
}
for op in ops {
if op.op_type == OpType::Open && op.is_read_mode {
let has_fstat = ops.iter().any(|o| {
o.op_type == OpType::Fstat
&& o.fd_var.is_some()
&& op.fd_var.is_some()
&& o.fd_var == op.fd_var
});
if !has_fstat && !self.has_attribute_comparison {
violations.push(RuleViolation {
rule_id: "FIO05-C".to_string(),
severity: Severity::Medium,
message: format!(
"File opened for reading using variable filename '{}' without verifying file attributes (fstat + st_uid/st_gid check)",
filename
),
file_path: String::new(),
line: op.line,
column: op.column,
suggestion: Some(
"Use fstat() to check file ownership (st_uid, st_gid) before reading untrusted files"
.to_string(),
),
..Default::default()
});
return; }
}
}
}
fn check_reopen_pattern(
&self,
filename: &str,
ops: &[FileOperation],
violations: &mut Vec<RuleViolation>,
) {
let mut i = 0;
while i < ops.len() {
if ops[i].op_type == OpType::Open {
let first_open = &ops[i];
let mut j = i + 1;
while j < ops.len() {
if ops[j].op_type == OpType::Close {
let close_op = &ops[j];
let is_matching_close = first_open.fd_var.is_some()
&& close_op.fd_var.is_some()
&& first_open.fd_var == close_op.fd_var;
if is_matching_close {
let mut k = j + 1;
while k < ops.len() {
if ops[k].op_type == OpType::Open && ops[k].filename == *filename {
let has_fstat_between = self.has_fstat_between(i, k, ops);
if !has_fstat_between && !self.has_attribute_comparison {
violations.push(RuleViolation {
rule_id: "FIO05-C".to_string(),
severity: Severity::Medium,
message: format!(
"File reopened using filename '{}' without verifying file attributes (st_dev, st_ino)",
filename
),
file_path: String::new(),
line: ops[k].line,
column: ops[k].column,
suggestion: Some(
"Use fstat() to check st_dev and st_ino before and after reopening, or avoid closing and reopening the file".to_string()
),
..Default::default()
});
}
i = k;
break;
}
k += 1;
}
break;
}
}
j += 1;
}
}
i += 1;
}
}
fn has_fstat_between(&self, start_idx: usize, end_idx: usize, ops: &[FileOperation]) -> bool {
for op in ops
.iter()
.take(end_idx.min(ops.len() - 1) + 1)
.skip(start_idx)
{
if op.op_type == OpType::Fstat {
return true;
}
}
false
}
}