add: customize csv and display fmt from profile #165
This commit is contained in:
138
src/afterfact.rs
138
src/afterfact.rs
@@ -27,41 +27,6 @@ use std::process;
|
||||
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
|
||||
use terminal_size::Width;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct CsvFormat<'a> {
|
||||
timestamp: &'a str,
|
||||
computer: &'a str,
|
||||
channel: &'a str,
|
||||
event_i_d: &'a str,
|
||||
level: &'a str,
|
||||
mitre_attack: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
record_i_d: Option<&'a str>,
|
||||
rule_title: &'a str,
|
||||
details: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
record_information: Option<&'a str>,
|
||||
rule_file: &'a str,
|
||||
evtx_file: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct DisplayFormat<'a> {
|
||||
timestamp: &'a str,
|
||||
pub computer: &'a str,
|
||||
pub channel: &'a str,
|
||||
pub event_i_d: &'a str,
|
||||
pub level: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
record_i_d: Option<&'a str>,
|
||||
pub rule_title: &'a str,
|
||||
pub details: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub record_information: Option<&'a str>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref OUTPUT_COLOR: HashMap<String, Color> = set_output_color();
|
||||
}
|
||||
@@ -251,50 +216,13 @@ fn emit_csv<W: std::io::Write>(
|
||||
timestamps.push(_get_timestamp(time));
|
||||
for detect_info in detect_infos {
|
||||
detected_record_idset.insert(format!("{}_{}", time, detect_info.eventid));
|
||||
let level = detect_info.level.to_string();
|
||||
let time_str = format_time(time, false);
|
||||
if displayflag {
|
||||
let record_id = detect_info
|
||||
.record_id
|
||||
.as_ref()
|
||||
.map(|recinfo| _format_cellpos(recinfo, ColPos::Other));
|
||||
let recinfo = detect_info
|
||||
.record_information
|
||||
.as_ref()
|
||||
.map(|recinfo| _format_cellpos(recinfo, ColPos::Last));
|
||||
let ctr_char_exclude_details = detect_info
|
||||
.detail
|
||||
.chars()
|
||||
.filter(|&c| !c.is_control())
|
||||
.collect::<String>();
|
||||
|
||||
let details = if ctr_char_exclude_details.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
ctr_char_exclude_details
|
||||
};
|
||||
|
||||
let dispformat: _ = DisplayFormat {
|
||||
timestamp: &_format_cellpos(&time_str, ColPos::First),
|
||||
level: &_format_cellpos(
|
||||
level_abbr.get(&level).unwrap_or(&level),
|
||||
ColPos::Other,
|
||||
),
|
||||
computer: &_format_cellpos(&detect_info.computername, ColPos::Other),
|
||||
event_i_d: &_format_cellpos(&detect_info.eventid, ColPos::Other),
|
||||
channel: &_format_cellpos(&detect_info.channel, ColPos::Other),
|
||||
rule_title: &_format_cellpos(&detect_info.alert, ColPos::Other),
|
||||
details: &_format_cellpos(&details, ColPos::Other),
|
||||
record_information: recinfo.as_deref(),
|
||||
record_i_d: record_id.as_deref(),
|
||||
};
|
||||
|
||||
//ヘッダーのみを出力
|
||||
if plus_header {
|
||||
write_color_buffer(
|
||||
&disp_wtr,
|
||||
get_writable_color(None),
|
||||
&_get_serialized_disp_output(None),
|
||||
&_get_serialized_disp_output(PROFILES.as_ref().unwrap().clone(), true),
|
||||
true,
|
||||
)
|
||||
.ok();
|
||||
@@ -302,31 +230,18 @@ fn emit_csv<W: std::io::Write>(
|
||||
}
|
||||
write_color_buffer(
|
||||
&disp_wtr,
|
||||
get_writable_color(_get_output_color(&color_map, &detect_info.level)),
|
||||
&_get_serialized_disp_output(Some(dispformat)),
|
||||
get_writable_color(_get_output_color(&color_map, LEVEL_ABBR.get(&detect_info.level).unwrap_or(&String::default()))),
|
||||
&_get_serialized_disp_output(detect_info.ext_field.clone(), false),
|
||||
false,
|
||||
)
|
||||
.ok();
|
||||
} else {
|
||||
// csv output format
|
||||
wtr.serialize(CsvFormat {
|
||||
timestamp: &time_str,
|
||||
level: level_abbr.get(&level).unwrap_or(&level).trim(),
|
||||
computer: &detect_info.computername,
|
||||
event_i_d: &detect_info.eventid,
|
||||
channel: &detect_info.channel,
|
||||
mitre_attack: &detect_info.tag_info,
|
||||
rule_title: &detect_info.alert,
|
||||
details: &detect_info.detail,
|
||||
record_information: detect_info.record_information.as_deref(),
|
||||
evtx_file: &detect_info.filepath,
|
||||
rule_file: Path::new(&detect_info.rulepath)
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
record_i_d: detect_info.record_id.as_deref(),
|
||||
})?;
|
||||
if plus_header {
|
||||
wtr.write_record(detect_info.ext_field.keys())?;
|
||||
plus_header = false;
|
||||
}
|
||||
wtr.write_record(detect_info.ext_field.values())?;
|
||||
}
|
||||
let level_suffix = *configs::LEVELMAP
|
||||
.get(&detect_info.level.to_uppercase())
|
||||
@@ -470,24 +385,24 @@ enum ColPos {
|
||||
Other,
|
||||
}
|
||||
|
||||
fn _get_serialized_disp_output(dispformat: Option<DisplayFormat>) -> String {
|
||||
if dispformat.is_none() {
|
||||
let mut titles = vec![
|
||||
"Timestamp",
|
||||
"Computer",
|
||||
"Channel",
|
||||
"EventID",
|
||||
"Level",
|
||||
"RuleTitle",
|
||||
"Details",
|
||||
];
|
||||
if !*IS_HIDE_RECORD_ID {
|
||||
titles.insert(5, "RecordID");
|
||||
fn _get_serialized_disp_output(mut data: LinkedHashMap<String, String>, header: bool) -> String {
|
||||
let data_length = &data.len();
|
||||
let entries = data.entries();
|
||||
let mut ret:Vec<String> = vec![];
|
||||
if header {
|
||||
entries.for_each(|entry|{
|
||||
ret.push(entry.key().to_owned());
|
||||
});
|
||||
} else {
|
||||
entries.enumerate().for_each(|(i, entry)|{
|
||||
if i == 0 {
|
||||
ret.push(_format_cellpos(entry.get(), ColPos::First))
|
||||
} else if i == data_length - 1{
|
||||
ret.push(_format_cellpos(entry.get(), ColPos::Last))
|
||||
} else {
|
||||
ret.push(_format_cellpos(entry.get(), ColPos::Other))
|
||||
}
|
||||
if configs::CONFIG.read().unwrap().args.full_data {
|
||||
titles.push("RecordInformation");
|
||||
}
|
||||
return titles.join("|");
|
||||
});
|
||||
}
|
||||
let mut disp_serializer = csv::WriterBuilder::new()
|
||||
.double_quote(false)
|
||||
@@ -496,8 +411,7 @@ fn _get_serialized_disp_output(dispformat: Option<DisplayFormat>) -> String {
|
||||
.has_headers(false)
|
||||
.from_writer(vec![]);
|
||||
|
||||
disp_serializer.serialize(dispformat.unwrap()).ok();
|
||||
|
||||
disp_serializer.write_record(ret).ok();
|
||||
String::from_utf8(disp_serializer.into_inner().unwrap_or_default()).unwrap_or_default()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@ extern crate csv;
|
||||
|
||||
use crate::detections::configs;
|
||||
use crate::detections::utils::write_color_buffer;
|
||||
use crate::options::profile;
|
||||
use crate::options::profile::PROFILES;
|
||||
use linked_hash_map::LinkedHashMap;
|
||||
use termcolor::{BufferWriter, Color, ColorChoice};
|
||||
|
||||
use crate::detections::message::AlertMessage;
|
||||
use crate::detections::message::DetectInfo;
|
||||
use crate::detections::message::ERROR_LOG_STACK;
|
||||
use crate::detections::message::{CH_CONFIG, DEFAULT_DETAILS, IS_HIDE_RECORD_ID, TAGS_CONFIG};
|
||||
use crate::detections::message::{CH_CONFIG, DEFAULT_DETAILS, TAGS_CONFIG};
|
||||
use crate::detections::message::{
|
||||
LOGONSUMMARY_FLAG, PIVOT_KEYWORD_LIST_FLAG, QUIET_ERRORS_FLAG, STATISTICS_FLAG,
|
||||
};
|
||||
@@ -202,8 +205,12 @@ impl Detection {
|
||||
rule
|
||||
}
|
||||
|
||||
/// 条件に合致したレコードを表示するための関数
|
||||
/// 条件に合致したレコードを格納するための関数
|
||||
fn insert_message(rule: &RuleNode, record_info: &EvtxRecordInfo) {
|
||||
let profile_all_alias = if PROFILES.is_some() {
|
||||
PROFILES.as_ref().unwrap().values().cloned().collect::<Vec<_>>().join("|")
|
||||
}
|
||||
else{String::default()};
|
||||
let tag_info: Vec<String> = match TAGS_CONFIG.is_empty() {
|
||||
false => rule.yaml["tags"]
|
||||
.as_vec()
|
||||
@@ -229,7 +236,7 @@ impl Detection {
|
||||
.record_information
|
||||
.as_ref()
|
||||
.map(|recinfo| recinfo.to_string());
|
||||
let rec_id = if !*IS_HIDE_RECORD_ID {
|
||||
let rec_id = if !profile_all_alias.contains("%RecordID%") {
|
||||
Some(
|
||||
get_serde_number_to_string(&record_info.record["Event"]["System"]["EventRecordID"])
|
||||
.unwrap_or_default(),
|
||||
@@ -268,6 +275,7 @@ impl Detection {
|
||||
tag_info: tag_info.join(" | "),
|
||||
record_information: opt_record_info,
|
||||
record_id: rec_id,
|
||||
ext_field: PROFILES.as_ref().unwrap().to_owned(),
|
||||
};
|
||||
message::insert(
|
||||
&record_info.record,
|
||||
@@ -311,7 +319,8 @@ impl Detection {
|
||||
detail: output,
|
||||
record_information: rec_info,
|
||||
tag_info: tag_info.join(" : "),
|
||||
record_id: rec_id,
|
||||
record_id: Some("-".to_owned()),
|
||||
ext_field: PROFILES.as_ref().unwrap().to_owned(),
|
||||
};
|
||||
|
||||
message::insert_message(detect_info, agg_result.start_timedate)
|
||||
|
||||
@@ -20,6 +20,8 @@ use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use termcolor::{BufferWriter, ColorChoice};
|
||||
|
||||
use super::utils::format_time;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DetectInfo {
|
||||
pub filepath: String,
|
||||
@@ -33,6 +35,7 @@ pub struct DetectInfo {
|
||||
pub tag_info: String,
|
||||
pub record_information: Option<String>,
|
||||
pub record_id: Option<String>,
|
||||
pub ext_field: LinkedHashMap<String, String>,
|
||||
}
|
||||
|
||||
pub struct AlertMessage {}
|
||||
|
||||
@@ -21,7 +21,7 @@ use hayabusa::detections::pivot::PivotKeyword;
|
||||
use hayabusa::detections::pivot::PIVOT_KEYWORD;
|
||||
use hayabusa::detections::rule::{get_detection_keys, RuleNode};
|
||||
use hayabusa::omikuji::Omikuji;
|
||||
use hayabusa::options::{level_tuning::LevelTuning, update_rules::UpdateRules};
|
||||
use hayabusa::options::{level_tuning::LevelTuning, update_rules::UpdateRules, profile::PROFILES};
|
||||
use hayabusa::{afterfact::after_fact, detections::utils};
|
||||
use hayabusa::{detections::configs, timeline::timelines::Timeline};
|
||||
use hayabusa::{detections::utils::write_color_buffer, filter};
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::path::Path;
|
||||
use yaml_rust::{Yaml, YamlEmitter, YamlLoader};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PROFILES: Option<HashMap<String, String>> = load_profile(
|
||||
pub static ref PROFILES: Option<LinkedHashMap<String, String>> = load_profile(
|
||||
check_setting_path(
|
||||
&CURRENT_EXE_PATH.to_path_buf(),
|
||||
"config/default_profile.txt"
|
||||
@@ -46,7 +46,7 @@ fn read_profile_data(profile_path: &str) -> Result<Vec<Yaml>, String> {
|
||||
pub fn load_profile(
|
||||
default_profile_path: &str,
|
||||
profile_path: &str,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
) -> Option<LinkedHashMap<String, String>> {
|
||||
let conf = &configs::CONFIG.read().unwrap().args;
|
||||
let profile_all: Vec<Yaml> = if conf.profile.is_none() {
|
||||
match read_profile_data(default_profile_path) {
|
||||
@@ -71,7 +71,7 @@ pub fn load_profile(
|
||||
return None;
|
||||
}
|
||||
let profile_data = &profile_all[0];
|
||||
let mut ret: HashMap<String, String> = HashMap::new();
|
||||
let mut ret: LinkedHashMap<String, String> = LinkedHashMap::new();
|
||||
if let Some(profile_name) = &conf.profile {
|
||||
if !profile_data[profile_name.as_str()].is_badvalue() {
|
||||
profile_data[profile_name.as_str()].clone().as_hash().unwrap().into_iter().for_each(|(k, v)| {
|
||||
@@ -83,8 +83,10 @@ pub fn load_profile(
|
||||
None
|
||||
}
|
||||
} else {
|
||||
AlertMessage::alert("Not specified --profile").ok();
|
||||
None
|
||||
profile_all[0].clone().as_hash().unwrap().into_iter().for_each(|(k, v)| {
|
||||
ret.insert(k.as_str().unwrap().to_string(), v.as_str().unwrap().to_string());
|
||||
});
|
||||
Some(ret)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user