bat-cli 0.12.1

Blockchain Auditor Toolkit (BAT)
use crate::batbelt::bat_dialoguer::BatDialoguer;
use crate::batbelt::command_line::execute_command;
use crate::batbelt::templates::finding_template::FindingTemplate;
use crate::batbelt::{
    path::{BatFile, BatFolder},
    BatEnumerator,
};
use colored::Colorize;

use crate::batbelt::git::git_commit::GitCommit;
use crate::batbelt::metadata::{BatMetadata, BatMetadataParser, SourceCodeMetadata};
use crate::batbelt::path::prettify_source_code_path;
use crate::commands::{BatCommandEnumerator, CommandResult};
use clap::Subcommand;
use error_stack::{Report, Result, ResultExt};
use inflector::Inflector;
use lazy_regex::regex;
use std::{
    fs::File,
    io::{self, BufRead},
    string::String,
};

use super::CommandError;

#[derive(
    Subcommand, Debug, strum_macros::Display, PartialEq, Clone, strum_macros::EnumIter, Default,
)]
pub enum FindingCommand {
    /// Creates a finding file
    #[default]
    Create,
    /// Finish a finding file by creating a commit
    Finish,
    /// Creates evidence from source code
    CreateEvidence,
    /// Update a finding file by creating a commit
    Update,
    /// Moves all the to-review findings to accepted
    AcceptAll,
    /// Moves a finding from to-review to rejected
    Reject,
}

impl BatEnumerator for FindingCommand {}

impl BatCommandEnumerator for FindingCommand {
    fn execute_command(&self) -> CommandResult<()> {
        unimplemented!()
    }

    fn check_metadata_is_initialized(&self) -> bool {
        true
    }

    fn check_correct_branch(&self) -> bool {
        true
    }
}

pub fn create_evidence() -> CommandResult<()> {
    let bat_metadata = BatMetadata::read_metadata().change_context(CommandError)?;
    let mut total_source_code = vec![];
    let SourceCodeMetadata {
        functions_source_code,
        structs_source_code,
        traits_source_code,
        enums_source_code,
    } = bat_metadata.source_code;

    let findings_bat_folder = BatFolder::FindingsFolderPath;
    let findings_dir_entry = findings_bat_folder
        .get_all_files_dir_entries(true, None, None)
        .change_context(CommandError)?;
    let findings_path_regex = regex!(r#"(findings/to-review)|(findings/accepted)"#);
    let filtered_finding = findings_dir_entry
        .into_iter()
        .filter(|finding| findings_path_regex.is_match(finding.path().to_str().unwrap()))
        .collect::<Vec<_>>();
    let filtered_findings_names = filtered_finding
        .clone()
        .into_iter()
        .map(|finding| finding.file_name().to_str().unwrap().to_string())
        .collect::<Vec<_>>();
    let prompt_text = format!("Select the {} to create evidence", "finding".bright_green());
    let selected_finding_index = BatDialoguer::select(prompt_text, filtered_findings_names, None)?;
    let _selected_finding = filtered_finding[selected_finding_index].clone();

    for meta in functions_source_code {
        total_source_code.push(meta.to_source_code_parser(None));
    }
    for meta in structs_source_code {
        total_source_code.push(meta.to_source_code_parser(None));
    }
    for meta in traits_source_code {
        total_source_code.push(meta.to_source_code_parser(None));
    }
    for meta in enums_source_code {
        total_source_code.push(meta.to_source_code_parser(None));
    }

    total_source_code.sort_by_key(|sc| sc.name.clone());

    let formatted_total_sc = total_source_code
        .clone()
        .into_iter()
        .map(|sc| {
            format!(
                "{}: [{}:{}]",
                sc.name,
                prettify_source_code_path(&sc.path).unwrap(),
                sc.start_line_index
            )
        })
        .collect::<Vec<_>>();
    let prompt_text = format!(
        "Select the source code to create the {}",
        "evidence".bright_yellow()
    );

    let selected_sc_index = BatDialoguer::select(prompt_text, formatted_total_sc, None)?;
    let selected_sc = total_source_code[selected_sc_index].clone();
    println!("selected_sc: {:#?}", selected_sc);

    Ok(())
}

pub fn reject() -> Result<(), CommandError> {
    prepare_all()?;
    let to_review_files_names = BatFolder::FindingsToReview
        .get_all_files_names(true, None, None)
        .change_context(CommandError)?;
    // get to-review files
    let selection = BatDialoguer::select(
        "Select the finding file to reject:".to_string(),
        to_review_files_names.clone(),
        None,
    )?;

    let rejected_file_name = to_review_files_names[selection].clone();
    BatFile::FindingToReview {
        file_name: rejected_file_name.clone(),
    }
    .move_file(
        &BatFile::FindingRejected {
            file_name: rejected_file_name.clone(),
        }
        .get_path(false)
        .change_context(CommandError)?,
    )
    .change_context(CommandError)?;

    GitCommit::RejectFinding {
        finding_name: rejected_file_name.clone(),
    }
    .create_commit(true)
    .change_context(CommandError)?;

    println!("{rejected_file_name} file moved to rejected");

    Ok(())
}

pub fn accept_all() -> Result<(), CommandError> {
    prepare_all()?;
    let accepted_path = BatFolder::FindingsAccepted
        .get_path(true)
        .change_context(CommandError)?;
    let findings_to_review_files = BatFolder::FindingsToReview
        .get_all_files_dir_entries(true, None, None)
        .change_context(CommandError)?;
    for to_review_file in findings_to_review_files {
        execute_command(
            "mv",
            &[
                to_review_file.path().to_str().unwrap(),
                &accepted_path.clone(),
            ],
            false,
        )?;
    }
    GitCommit::AcceptFindings
        .create_commit(true)
        .change_context(CommandError)?;
    println!(
        "All findings has been moved to the {} folder",
        "accepted".green()
    );
    Ok(())
}

pub fn start_finding() -> Result<(), CommandError> {
    let input_name =
        BatDialoguer::input("Finding name:".to_string()).change_context(CommandError)?;
    let finding_name = input_name.to_snake_case();
    validate_config_create_finding_file(finding_name.clone())?;
    copy_template_to_findings_to_review(finding_name.clone())?;

    GitCommit::StartFinding {
        finding_name: finding_name.clone(),
    }
    .create_commit(true)
    .change_context(CommandError)?;

    BatFile::FindingToReview {
        file_name: finding_name,
    }
    .open_in_editor(true, None)
    .change_context(CommandError)?;

    Ok(())
}

pub fn finish_finding() -> Result<(), CommandError> {
    let to_review_files = BatFolder::FindingsToReview
        .get_all_files_names(true, None, None)
        .change_context(CommandError)?;
    // get to-review files
    let prompt_text = "Select finding file to finish:";
    let selection = BatDialoguer::select(prompt_text.to_string(), to_review_files.clone(), None)
        .change_context(CommandError)?;

    let finding_name = &to_review_files[selection].clone();
    validate_finished_finding_file(finding_name.clone())?;
    GitCommit::FinishFinding {
        finding_name: finding_name.to_string(),
    }
    .create_commit(true)
    .change_context(CommandError)?;
    Ok(())
}

pub fn update_finding() -> Result<(), CommandError> {
    let to_review_files = BatFolder::FindingsToReview
        .get_all_files_names(true, None, None)
        .change_context(CommandError)?;

    let prompt_text = "Select finding file to update:";
    let selection = BatDialoguer::select(prompt_text.to_string(), to_review_files.clone(), None)
        .change_context(CommandError)?;

    let finding_name = to_review_files[selection].clone();
    GitCommit::UpdateFinding { finding_name }
        .create_commit(true)
        .change_context(CommandError)?;
    Ok(())
}

fn prepare_all() -> Result<(), CommandError> {
    let to_review_dir_entries = BatFolder::FindingsToReview
        .get_all_files_dir_entries(true, None, None)
        .change_context(CommandError)?;
    for to_review_file in to_review_dir_entries {
        let file = to_review_file;
        let file_name = file.file_name();
        let mut file_name_tokenized = file_name
            .to_str()
            .unwrap()
            .to_string()
            .split('-')
            .map(|token| token.to_string())
            .collect::<Vec<String>>();
        let severity_flags = ["1", "2", "3", "4"];
        let finding_name = if severity_flags.contains(&file_name_tokenized[0].as_str()) {
            file_name_tokenized.remove(0);
            file_name_tokenized.join("-")
        } else {
            file_name_tokenized.join("-")
        };
        let open_file = File::open(file.path()).unwrap();
        let file_lines = io::BufReader::new(open_file).lines().map(|l| l.unwrap());
        for line in file_lines {
            if line.contains("Severity:") {
                let file_severity = line
                    .replace("**Severity:**", "")
                    .replace(' ', "")
                    .to_lowercase();
                let severity = match file_severity.as_str() {
                    "high" => "1",
                    "medium" => "2",
                    "low" => "3",
                    "informational" => "4",
                    &_ => {
                        return Err(Report::new(CommandError).attach_printable(format!(
                            "severity: {:?} not recongnized in file {:?}",
                            file_severity,
                            file.path()
                        )));
                    }
                };
                let finding_file_name =
                    format!("{}-{}", severity, finding_name.replace(".md", "").as_str());
                // let to_path = utils::path::get_auditor_findings_to_review_path(Some())?;
                let to_path = BatFile::FindingToReview {
                    file_name: finding_file_name,
                }
                .get_path(false)
                .change_context(CommandError)?;
                execute_command(
                    "mv",
                    &[file.path().as_os_str().to_str().unwrap(), to_path.as_str()],
                    false,
                )?;
            }
        }
    }
    println!("All to-review findings severity tags updated");
    Ok(())
}

fn validate_config_create_finding_file(finding_name: String) -> Result<(), CommandError> {
    let bat_file = BatFile::FindingToReview {
        file_name: finding_name,
    };

    if bat_file.file_exists().change_context(CommandError)? {
        return Err(Report::new(CommandError).attach_printable(format!(
            "Finding file already exists: {:#?}",
            bat_file.get_path(false).change_context(CommandError)?
        )));
    }
    Ok(())
}

fn copy_template_to_findings_to_review(finding_name: String) -> Result<(), CommandError> {
    let prompt_text = "is the finding an informational?";
    let is_informational =
        BatDialoguer::select_yes_or_no(prompt_text.to_string()).change_context(CommandError)?;
    FindingTemplate::new_finding_file(&finding_name, is_informational)
        .change_context(CommandError)?;
    let finding_path = BatFile::FindingToReview {
        file_name: finding_name,
    }
    .get_path(false)
    .change_context(CommandError)?;
    println!("Finding file successfully created at: {}", finding_path);
    Ok(())
}

fn validate_finished_finding_file(file_name: String) -> Result<(), CommandError> {
    let bat_file = BatFile::FindingToReview {
        file_name: file_name.clone(),
    };
    let file_data = bat_file.read_content(true).change_context(CommandError)?;
    if file_data.contains("Fill the description") {
        bat_file
            .open_in_editor(true, None)
            .change_context(CommandError)?;
        return Err(Report::new(CommandError).attach_printable(format!(
            "Please complete the Description section of the {} file",
            file_name
        )));
    }
    if file_data.contains("Fill the impact") {
        bat_file
            .open_in_editor(true, None)
            .change_context(CommandError)?;
        return Err(Report::new(CommandError).attach_printable(format!(
            "Please complete the Impact section of the {} file",
            file_name
        )));
    }
    if file_data.contains("Add recommendations") {
        bat_file
            .open_in_editor(true, None)
            .change_context(CommandError)?;
        return Err(Report::new(CommandError).attach_printable(format!(
            "Please complete the Recommendations section of the {} file",
            file_name
        )));
    }
    Ok(())
}