under constructing
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/target
|
||||
/samples
|
||||
*.test
|
||||
/.vscode/
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1192,6 +1192,7 @@ dependencies = [
|
||||
"evtx",
|
||||
"flate2",
|
||||
"lazy_static",
|
||||
"linked-hash-map",
|
||||
"quick-xml 0.17.2",
|
||||
"regex",
|
||||
"serde",
|
||||
|
||||
@@ -20,6 +20,7 @@ flate2 = "1.0"
|
||||
lazy_static = "1.4.0"
|
||||
chrono = "0.4.19"
|
||||
yaml-rust = "0.4"
|
||||
linked-hash-map = "0.5.3"
|
||||
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "x86_64-w64-mingw32-gcc"
|
||||
|
||||
12
config/eventkey_alias.txt
Normal file
12
config/eventkey_alias.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
alias,event_key
|
||||
EventID,Event.System.EventId
|
||||
Channel,Event.System.Channel
|
||||
CommandLine,Event.EventData.CommandLine
|
||||
Signed,Event.EventData.Signed
|
||||
ProcessName,Event.EventData.ProcessName
|
||||
AccessMask,Event.EventData.AccessMask
|
||||
TargetUserName,Event.EventData.TargetUserName
|
||||
param1,Event.EventData.param1
|
||||
param2,Event.EventData.param2
|
||||
ServiceName,Event.EventData.ServiceName
|
||||
ImagePath,Event.EventData.ImagePath
|
||||
20
rules/deep_blue_cli/powershell/4103.yml
Normal file
20
rules/deep_blue_cli/powershell/4103.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
title: PowerShell Execution Pipeline
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
EventLog: PowerShell
|
||||
EventID: 4103
|
||||
ContextInfo:
|
||||
- Host Application
|
||||
- ホスト アプリケーション
|
||||
condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'command=%CommandLine%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
19
rules/deep_blue_cli/powershell/4104.yml
Normal file
19
rules/deep_blue_cli/powershell/4104.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
title: PowerShell Execution Remote Command
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
EventLog: PowerShell
|
||||
EventID: 4104
|
||||
Path: ''
|
||||
ScriptBlockText: '.'
|
||||
condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'command=%CommandLine%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
@@ -1,4 +0,0 @@
|
||||
[rule]
|
||||
severity = "high"
|
||||
name = "4103"
|
||||
message = "Execute Pipeline"
|
||||
@@ -1,4 +0,0 @@
|
||||
[rule]
|
||||
severity = "high"
|
||||
name = "4104"
|
||||
message = "Excute Remote Command"
|
||||
@@ -2,12 +2,12 @@ use clap::{App, AppSettings, Arg, ArgMatches};
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::sync::Once;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SingletonReader {
|
||||
pub regex: Vec<Vec<String>>,
|
||||
pub whitelist: Vec<Vec<String>>,
|
||||
pub args: ArgMatches<'static>,
|
||||
pub event_key_alias_config: EventKeyAliasConfig,
|
||||
}
|
||||
|
||||
pub fn singleton() -> Box<SingletonReader> {
|
||||
@@ -17,9 +17,8 @@ pub fn singleton() -> Box<SingletonReader> {
|
||||
unsafe {
|
||||
ONCE.call_once(|| {
|
||||
let singleton = SingletonReader {
|
||||
regex: read_csv("regexes.txt"),
|
||||
whitelist: read_csv("whitelist.txt"),
|
||||
args: build_app().get_matches(),
|
||||
event_key_alias_config: load_eventkey_alias(),
|
||||
};
|
||||
|
||||
SINGLETON = Some(Box::new(singleton));
|
||||
@@ -56,6 +55,41 @@ fn build_app() -> clap::App<'static, 'static> {
|
||||
.arg(Arg::from_usage("--credits 'Zachary Mathis, Akira Nishikawa'"))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EventKeyAliasConfig {
|
||||
key_to_eventkey: HashMap<String,String>,
|
||||
}
|
||||
|
||||
impl EventKeyAliasConfig {
|
||||
pub fn new() -> EventKeyAliasConfig {
|
||||
return EventKeyAliasConfig{ key_to_eventkey: HashMap::new() };
|
||||
}
|
||||
|
||||
pub fn get_event_key(&self, alias: String ) -> Option<&String> {
|
||||
return self.key_to_eventkey.get(&alias);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_eventkey_alias() -> EventKeyAliasConfig {
|
||||
let config = EventKeyAliasConfig::new();
|
||||
|
||||
read_csv("config/eventkey_alias.txt").into_iter().for_each( | line| {
|
||||
if line.len() != 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let alias = line[0];
|
||||
let event_key = line[1];
|
||||
if alias.len() == 0 || event_key.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
config.key_to_eventkey.insert(alias, event_key);
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
fn read_csv(filename: &str) -> Vec<Vec<String>> {
|
||||
let mut f = File::open(filename).expect("file not found!!!");
|
||||
let mut contents: String = String::new();
|
||||
|
||||
@@ -1,74 +1,78 @@
|
||||
extern crate csv;
|
||||
extern crate quick_xml;
|
||||
extern crate chrono;
|
||||
|
||||
use crate::detections::application;
|
||||
use crate::detections::applocker;
|
||||
use crate::detections::common;
|
||||
use crate::detections::powershell;
|
||||
use crate::detections::security;
|
||||
use crate::detections::sysmon;
|
||||
use crate::detections::system;
|
||||
use crate::models::event;
|
||||
use crate::detections::rule;
|
||||
use crate::detections::rule::RuleNode;
|
||||
use crate::detections::print::{Message};
|
||||
use crate::yaml::ParseYaml;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use evtx::EvtxParser;
|
||||
use quick_xml::de::DeError;
|
||||
use std::collections::BTreeMap;
|
||||
use serde_json::{Error, Value};
|
||||
|
||||
const DIRPATH_RULES: &str = "rules";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Detection {
|
||||
timeline_list: BTreeMap<String, String>,
|
||||
|
||||
}
|
||||
|
||||
impl Detection {
|
||||
pub fn new() -> Detection {
|
||||
Detection {
|
||||
timeline_list: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, mut parser: EvtxParser<std::fs::File>) -> Result<(), DeError> {
|
||||
let mut common: common::Common = common::Common::new();
|
||||
let mut security = security::Security::new();
|
||||
let mut system = system::System::new();
|
||||
let mut application = application::Application::new();
|
||||
let mut applocker = applocker::AppLocker::new();
|
||||
let mut sysmon = sysmon::Sysmon::new();
|
||||
let mut powershell = powershell::PowerShell::new();
|
||||
|
||||
for record in parser.records() {
|
||||
match record {
|
||||
Ok(r) => {
|
||||
let event: event::Evtx = quick_xml::de::from_str(&r.data)?;
|
||||
let event_id = event.system.event_id.to_string();
|
||||
let channel = event.system.channel.to_string();
|
||||
let event_data = event.parse_event_data();
|
||||
|
||||
&common.detection(&event.system, &event_data);
|
||||
if channel == "Security" {
|
||||
&security.detection(event_id, &event.system, &event.user_data, event_data);
|
||||
} else if channel == "System" {
|
||||
&system.detection(event_id, &event.system, event_data);
|
||||
} else if channel == "Application" {
|
||||
&application.detection(event_id, &event.system, event_data);
|
||||
} else if channel == "Microsoft-Windows-PowerShell/Operational" {
|
||||
&powershell.detection(event_id, &event.system, event_data);
|
||||
} else if channel == "Microsoft-Windows-Sysmon/Operational" {
|
||||
&sysmon.detection(event_id, &event.system, event_data);
|
||||
} else if channel == "Microsoft-Windows-AppLocker/EXE and DLL" {
|
||||
&applocker.detection(event_id, &event.system, event_data);
|
||||
} else {
|
||||
//&other.detection();
|
||||
}
|
||||
pub fn start(&mut self, mut parser: EvtxParser<std::fs::File>) {
|
||||
// from .etvx to json
|
||||
let event_records: Vec<Value> = parser
|
||||
.records_json()
|
||||
.filter_map(|result_record| {
|
||||
if result_record.is_err() {
|
||||
eprintln!("{}", result_record.unwrap_err());
|
||||
return Option::None;
|
||||
}
|
||||
Err(e) => eprintln!("{}", e),
|
||||
}
|
||||
|
||||
//// refer https://rust-lang-nursery.github.io/rust-cookbook/encoding/complex.html
|
||||
let result_json: Result<Value, Error> =
|
||||
serde_json::from_str(&result_record.unwrap().data);
|
||||
if result_json.is_err() {
|
||||
eprintln!("{}", result_json.unwrap_err());
|
||||
return Option::None;
|
||||
}
|
||||
return result_json.ok();
|
||||
})
|
||||
.collect();
|
||||
|
||||
event_records.iter().for_each(|event_rec| {
|
||||
println!("{}", event_rec["Event"]);
|
||||
});
|
||||
|
||||
// load rule files
|
||||
let mut rulefile_loader = ParseYaml::new();
|
||||
let resutl_readdir = rulefile_loader.read_dir(DIRPATH_RULES);
|
||||
if resutl_readdir.is_err() {
|
||||
eprintln!("{}", resutl_readdir.unwrap_err());
|
||||
return;
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
// 表示
|
||||
////////////////////////////
|
||||
common.disp();
|
||||
security.disp();
|
||||
// parse rule files
|
||||
let rules: Vec<RuleNode> = rulefile_loader
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|rule_file| rule::parse_rule(rule_file))
|
||||
.collect();
|
||||
|
||||
return Ok(());
|
||||
// selection rule files and collect log
|
||||
let mut message = Message::new();
|
||||
rules.iter().for_each(|rule| {
|
||||
&event_records
|
||||
.iter()
|
||||
.filter(|event_record| rule.detection.select(event_record))
|
||||
.for_each(|event_record| message.insert(Utc.ymd(1996, 2, 27).and_hms(1, 5, 1), event_record.to_string()));
|
||||
});
|
||||
|
||||
// output message
|
||||
message.debug();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod configs;
|
||||
pub mod detection;
|
||||
mod powershell;
|
||||
pub mod print;
|
||||
mod rule;
|
||||
mod security;
|
||||
mod sysmon;
|
||||
mod system;
|
||||
|
||||
204
src/detections/rule.rs
Normal file
204
src/detections/rule.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use serde_json::Value;
|
||||
use yaml_rust::Yaml;
|
||||
use crate::detections::configs;
|
||||
|
||||
pub fn parse_rule(yaml: Yaml) -> RuleNode {
|
||||
let selection = parse_selection(&yaml);
|
||||
return RuleNode {
|
||||
yaml: yaml,
|
||||
detection: DetectionNode {
|
||||
selection: selection,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_selection(yaml: &Yaml) -> Option<Box<dyn SelectionNode>> {
|
||||
let selection_yaml = &yaml["detection"]["selection"];
|
||||
return Option::Some(parse_selection_recursively(vec![], &selection_yaml));
|
||||
}
|
||||
|
||||
fn parse_selection_recursively(mut key_list: Vec<String>, yaml: &Yaml) -> Box<dyn SelectionNode> {
|
||||
if yaml.as_hash().is_some() {
|
||||
let yaml_hash = yaml.as_hash().unwrap();
|
||||
let mut and_node = AndSelectionNode::new();
|
||||
|
||||
yaml_hash.keys().for_each(|hash_key| {
|
||||
let child_yaml = yaml_hash.get(hash_key).unwrap();
|
||||
let mut child_key_list = key_list.clone();
|
||||
child_key_list.push(hash_key.as_str().unwrap().to_string());
|
||||
let child_node = parse_selection_recursively(child_key_list, child_yaml);
|
||||
and_node.child_nodes.push(child_node);
|
||||
});
|
||||
return Box::new(and_node);
|
||||
} else if yaml.as_vec().is_some() {
|
||||
let mut or_node = OrSelectionNode::new();
|
||||
yaml.as_vec().unwrap().iter().for_each(|child_yaml| {
|
||||
let child_node = parse_selection_recursively(key_list.clone(), child_yaml);
|
||||
or_node.child_nodes.push(child_node);
|
||||
});
|
||||
|
||||
return Box::new(or_node);
|
||||
} else {
|
||||
return Box::new(FieldSelectionNode::new(key_list, yaml.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////// RuleNode
|
||||
pub struct RuleNode {
|
||||
pub yaml: Yaml,
|
||||
pub detection: DetectionNode,
|
||||
}
|
||||
|
||||
//////////////// Detection Node
|
||||
pub struct DetectionNode {
|
||||
selection: Option<Box<dyn SelectionNode>>,
|
||||
}
|
||||
|
||||
impl DetectionNode {
|
||||
pub fn select(&self, event_record: &Value) -> bool {
|
||||
if self.selection.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self.selection.as_ref().unwrap().select(event_record);
|
||||
}
|
||||
}
|
||||
|
||||
//////////// Selection Node
|
||||
trait SelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool;
|
||||
}
|
||||
|
||||
///////////////// AndSelectionNode
|
||||
struct AndSelectionNode {
|
||||
pub child_nodes: Vec<Box<dyn SelectionNode>>,
|
||||
}
|
||||
|
||||
impl AndSelectionNode {
|
||||
pub fn new() -> AndSelectionNode {
|
||||
return AndSelectionNode {
|
||||
child_nodes: vec![],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for AndSelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool {
|
||||
return self.child_nodes.iter().all(|child_node| {
|
||||
return child_node.as_ref().select(event_record);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
////////// OrSelectionNode
|
||||
struct OrSelectionNode {
|
||||
pub child_nodes: Vec<Box<dyn SelectionNode>>,
|
||||
}
|
||||
|
||||
impl OrSelectionNode {
|
||||
pub fn new() -> OrSelectionNode {
|
||||
return OrSelectionNode {
|
||||
child_nodes: vec![],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for OrSelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool {
|
||||
return self.child_nodes.iter().any(|child_node| {
|
||||
return child_node.as_ref().select(event_record);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
////////////// Field Selection Node
|
||||
struct FieldSelectionNode {
|
||||
key_list: Vec<String>,
|
||||
select_value: Yaml,
|
||||
}
|
||||
|
||||
impl FieldSelectionNode {
|
||||
fn new(key_list: Vec<String>, value_yaml: Yaml) -> FieldSelectionNode {
|
||||
return FieldSelectionNode {
|
||||
key_list: key_list,
|
||||
select_value: value_yaml,
|
||||
};
|
||||
}
|
||||
|
||||
// JSON形式のEventJSONから値を取得する関数 aliasも考慮されている。
|
||||
// TODO Messageを出力する際も利用するので、共通して使えるようにrefactoringする。
|
||||
fn get_event_value<'a>(&self, event_value: &'a Value) -> Option<&'a Value> {
|
||||
if self.key_list.is_empty() {
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
let key: &str = &self.key_list[0];
|
||||
if key.len() == 0 {
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
let event_key = match configs::singleton().event_key_alias_config.get_event_key(key.to_string()) {
|
||||
Some(alias_event_key) => { alias_event_key }
|
||||
None => { key }
|
||||
};
|
||||
|
||||
let mut ret: &Value = event_value;
|
||||
for key in event_key.split(".") {
|
||||
if ret.is_object() == false {
|
||||
return Option::None;
|
||||
}
|
||||
ret = &ret[key];
|
||||
}
|
||||
|
||||
return Option::Some(ret);
|
||||
}
|
||||
|
||||
// TODO Matcherのインスタンスが都度生成されないようにする。
|
||||
fn get_matchers(&self) -> Vec<Box<dyn FieldSelectionMatcher>> {
|
||||
return vec![Box::new(ValueMatcher {})];
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for FieldSelectionNode {
|
||||
fn select(&self, event_record: &Value) -> bool {
|
||||
let matchers = self.get_matchers();
|
||||
let matcher = matchers
|
||||
.into_iter()
|
||||
.find(|matcher| matcher.is_target_key(&self.key_list));
|
||||
if matcher.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let event_value = self.get_event_value(event_record);
|
||||
return matcher
|
||||
.unwrap()
|
||||
.is_match(&self.key_list, &self.select_value, event_value);
|
||||
}
|
||||
}
|
||||
|
||||
trait FieldSelectionMatcher {
|
||||
fn is_target_key(&self, key_list: &Vec<String>) -> bool;
|
||||
fn is_match(
|
||||
&self,
|
||||
key_list: &Vec<String>,
|
||||
select_value: &Yaml,
|
||||
event_value: Option<&Value>,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
struct ValueMatcher {}
|
||||
|
||||
impl FieldSelectionMatcher for ValueMatcher {
|
||||
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
|
||||
return key_list.is_empty();
|
||||
}
|
||||
|
||||
fn is_match(
|
||||
&self,
|
||||
key_list: &Vec<String>,
|
||||
select_value: &Yaml,
|
||||
event_value: Option<&Value>,
|
||||
) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,12 @@ use std::path::{Path, PathBuf};
|
||||
use yaml_rust::YamlLoader;
|
||||
|
||||
pub struct ParseYaml {
|
||||
pub rules: Vec<yaml_rust::Yaml>,
|
||||
pub files: Vec<yaml_rust::Yaml>,
|
||||
}
|
||||
|
||||
impl ParseYaml {
|
||||
pub fn new() -> ParseYaml {
|
||||
ParseYaml { rules: Vec::new() }
|
||||
ParseYaml { files: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn read_file(&self, path: PathBuf) -> Result<String, String> {
|
||||
@@ -39,7 +39,7 @@ impl ParseYaml {
|
||||
let docs = YamlLoader::load_from_str(&s).unwrap();
|
||||
for i in docs {
|
||||
if i["enabled"].as_bool().unwrap() {
|
||||
&self.rules.push(i);
|
||||
&self.files.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ mod tests {
|
||||
fn test_read_yaml() {
|
||||
let mut yaml = yaml::ParseYaml::new();
|
||||
&yaml.read_dir("test_files/rules/yaml/".to_string());
|
||||
for rule in yaml.rules {
|
||||
for rule in yaml.files {
|
||||
if rule["title"].as_str().unwrap() == "Sysmon Check command lines" {
|
||||
assert_eq!(
|
||||
"*",
|
||||
|
||||
Reference in New Issue
Block a user