//! This module implements a check for CWE-560: Use of umask() with chmod-style Argument.
//!
//! The program uses the system call umask(2) with arguments for chmod(2). For instance,
//! instead of a reasonable value like 0022 a value like 0666 is passed. This may result in wrong
//! read and/or write access to files and directories, which could be utilized to bypass
//! protection mechanisms.
//!
//! See <https://cwe.mitre.org/data/definitions/560.html> for a detailed description.
//!
//! ## How the check works
//!
//! This check looks for umask calls and checks if they have a reasonable value, i.e. smaller than
//! a certain value, currently set to 1000 and greater than a reasonable value for umask, currently set to 100.
//!
//! ## False Positives
//!
//! - A value deemed unreasonable by the check could theoretically be intended by the programmer.
//! But these cases should be very rare in real programs, so be sure to double check them!
//!
//! ## False Negatives
//!
//! - If the input to umask is not defined in the basic block before the call, the check will not see it.
//! However, a log message will be generated whenever the check is unable to determine the parameter value of umask.

use crate::abstract_domain::{BitvectorDomain, DataDomain};
use crate::analysis::pointer_inference::State;
use crate::intermediate_representation::*;
use crate::prelude::*;
use crate::utils::binary::RuntimeMemoryImage;
use crate::utils::log::{CweWarning, LogMessage};
use crate::utils::symbol_utils::{get_callsites, get_symbol_map};
use crate::CweModule;

pub static CWE_MODULE: CweModule = CweModule {
    name: "CWE560",
    version: "0.2",
    run: check_cwe,
};

pub static UPPER_BOUND_CORRECT_UMASK_ARG_VALUE: u64 = 100;
pub static UPPER_BOUND_CORRECT_CHMOD_ARG_VALUE: u64 = 1000;

/// Compute the parameter value of umask out of the basic block right before the umask call.
///
/// The function uses the same `State` struct as the pointer inference analysis for the computation.
fn get_umask_permission_arg(
    block: &Term<Blk>,
    umask_symbol: &ExternSymbol,
    project: &Project,
    global_memory: &RuntimeMemoryImage,
) -> Result<u64, Error> {
    let stack_register = &project.stack_pointer_register;
    let mut state = State::new(stack_register, block.tid.clone());

    for def in block.term.defs.iter() {
        match &def.term {
            Def::Store { address, value } => {
                let _ = state.handle_store(address, value, global_memory);
            }
            Def::Assign { var, value } => {
                let _ = state.handle_register_assign(var, value);
            }
            Def::Load { var, address } => {
                let _ = state.handle_load(var, address, global_memory);
            }
        }
    }

    let parameter = umask_symbol.get_unique_parameter()?;
    let param_value =
        state.eval_parameter_arg(parameter, &project.stack_pointer_register, global_memory)?;
    if let DataDomain::Value(BitvectorDomain::Value(umask_arg)) = param_value {
        Ok(umask_arg.try_to_u64()?)
    } else {
        Err(anyhow!("Parameter value unknown"))
    }
}

/// Is the given argument value considered to be a chmod-style argument?
fn is_chmod_style_arg(arg: u64) -> bool {
    arg > UPPER_BOUND_CORRECT_UMASK_ARG_VALUE && arg <= UPPER_BOUND_CORRECT_CHMOD_ARG_VALUE
}

/// Generate the CWE warning for a detected instance of the CWE.
fn generate_cwe_warning(sub: &Term<Sub>, jmp: &Term<Jmp>, permission_const: u64) -> CweWarning {
    CweWarning::new(CWE_MODULE.name, CWE_MODULE.version,
        format!("(Use of umask() with chmod-style Argument) Function {} calls umask with argument {:#o}", sub.term.name, permission_const))
        .tids(vec![format!("{}", jmp.tid)])
        .addresses(vec![jmp.tid.address.clone()])
        .other(vec![vec![
            "umask_arg".to_string(),
            format!("{:#o}", permission_const),
        ]])
}

/// Execute the CWE check.
///
/// For each call to umask we check whether the parameter value is a chmod-style parameter.
/// If yes, generate a CWE warning.
/// If the parameter value cannot be determined, generate a log message.
///
/// Only the basic block right before the umask call is evaluated when trying to determine the parameter value of umask.
pub fn check_cwe(
    analysis_results: &AnalysisResults,
    _cwe_params: &serde_json::Value,
) -> (Vec<LogMessage>, Vec<CweWarning>) {
    let project = analysis_results.project;
    let mut cwes = Vec::new();
    let mut log_messages = Vec::new();
    let umask_symbol_map = get_symbol_map(project, &["umask".to_string()]);
    if !umask_symbol_map.is_empty() {
        for sub in project.program.term.subs.iter() {
            for (block, jmp, umask_symbol) in get_callsites(sub, &umask_symbol_map) {
                match get_umask_permission_arg(
                    block,
                    umask_symbol,
                    project,
                    analysis_results.runtime_memory_image,
                ) {
                    Ok(permission_const) => {
                        if is_chmod_style_arg(permission_const) {
                            cwes.push(generate_cwe_warning(sub, jmp, permission_const));
                        }
                    }
                    Err(err) => {
                        let log = LogMessage::new_info(format!(
                            "Could not determine umask argument: {}",
                            err
                        ))
                        .location(jmp.tid.clone())
                        .source(CWE_MODULE.name);
                        log_messages.push(log);
                    }
                }
            }
        }
    }

    (log_messages, cwes)
}
