Merge pull request #36 from YamatoSecurity/feature/message_display
Feature/message display
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
/target
|
||||
/samples
|
||||
*.test
|
||||
*.test
|
||||
/.vscode/
|
||||
.DS_Store
|
||||
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"
|
||||
|
||||
19
config/eventkey_alias.txt
Normal file
19
config/eventkey_alias.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
alias,event_key
|
||||
EventID,Event.System.EventID
|
||||
Channel,Event.System.Channel
|
||||
CommandLine,Event.EventData.CommandLine
|
||||
ParentProcessName,Event.EventData.ParentProcessName
|
||||
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
|
||||
ContextInfo,Event.EventData.ContextInfo
|
||||
Path,Event.EventData.Path
|
||||
ScriptBlockText,Event.EventData.ScriptBlockText#Name
|
||||
MemberName,Event.EventData.SubjectUserName
|
||||
MemberSid,Event.EventData.SubjectUserSid
|
||||
TargetSid,Event.EventData.TargetSid
|
||||
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:
|
||||
Channel: 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:
|
||||
Channel: PowerShell
|
||||
EventID: 4104
|
||||
Path: null
|
||||
ScriptBlockText: null
|
||||
# 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"
|
||||
17
rules/deep_blue_cli/security/1102.yml
Normal file
17
rules/deep_blue_cli/security/1102.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
title: The Audit log file was cleared
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 1102
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'Audit Log Clear¥n The Audit log was cleared.¥m%user_data.log_file_cleared%%user_data.subject_user_name%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
17
rules/deep_blue_cli/security/4673.yml
Normal file
17
rules/deep_blue_cli/security/4673.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
title: Sensitive Privilede Use (Mimikatz)
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4673
|
||||
# condition: selection | count(EventID) > 4
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'Sensitive Privilege Use Exceeds Threshold¥n Potentially indicative of Mimikatz, multiple sensitive priviledge calls have been made.¥nUserName:SubjectUserName% Domain Name:%DomainName%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
19
rules/deep_blue_cli/security/4674.yml
Normal file
19
rules/deep_blue_cli/security/4674.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
title: An Operation was attempted on a privileged object
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4674
|
||||
ProcessName: '(?i)C:\WINDOWS\SYSTEM32\SERVICE.EXE' # (?i) means case insesitive for Rust Regex
|
||||
AccessMask: '%%1539'
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'Possible Hidden Service Attempt¥nUser requested to modify the Dynamic Access Control (DAC) permissions of a service, possibly to hide it from view.¥nUser: %SubjectUserName%¥nTarget service:%ObjectName¥nDesired Access:WRITE_DAC'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
18
rules/deep_blue_cli/security/4688.yml
Normal file
18
rules/deep_blue_cli/security/4688.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
title: Command Line Logging
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4688
|
||||
CommandLine: '.+'
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'CommandLine:%CommandLine% ParentProcessName:%ParentProcessName%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
17
rules/deep_blue_cli/security/4720.yml
Normal file
17
rules/deep_blue_cli/security/4720.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
title: A user account was created.
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4720
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'New User Created UserName:%TargetUserName% SID:%TargetSid%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
18
rules/deep_blue_cli/security/4728.yml
Normal file
18
rules/deep_blue_cli/security/4728.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
title: A member was added to a security-enabled global group.
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4728
|
||||
TargetUserName: Administrators
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'user added to global Administrators UserName: %MemberName% SID: %MemberSid%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
18
rules/deep_blue_cli/security/4732.yml
Normal file
18
rules/deep_blue_cli/security/4732.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
title: A member was added to a security-enabled local group.
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4732
|
||||
TargetUserName: Administrators
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'user added to local Administrators UserName: %MemberName% SID: %MemberSid%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
18
rules/deep_blue_cli/security/4756.yml
Normal file
18
rules/deep_blue_cli/security/4756.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
title: A member was added to a security-enabled universal group.
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4756
|
||||
TargetUserName: Administrators
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'user added to universal Administrators UserName: %MemberName% SID: %MemberSid%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
17
rules/deep_blue_cli/security/_4625.yml
Normal file
17
rules/deep_blue_cli/security/_4625.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
title: An account failed to log on
|
||||
description: hogehoge
|
||||
enabled: false
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4625
|
||||
# condition: selection | count(TargetUserName) > 3
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'High number of logon failures for one account UserName:%event_data.SubjectUserName% Total logon faiures:%count%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
17
rules/deep_blue_cli/security/_4648.yml
Normal file
17
rules/deep_blue_cli/security/_4648.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
title: An account failed to log on
|
||||
description: hogehoge
|
||||
enabled: false
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4648
|
||||
# condition: selection | count(TargetUserName) > 3
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: High
|
||||
output: 'Distributed Account Explicit Credential Use (Password Spray Attack)¥n The use of multiple user account access attempts with explicit credentials is ¥nan indicator of a password spray attack.¥nTarget Usernames:%TargetUserName$¥nAccessing Username: %SubjectUserName%¥nAccessing Host Name: %SubjectDomainName%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
19
rules/deep_blue_cli/security/_4672.yml
Normal file
19
rules/deep_blue_cli/security/_4672.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
title: Command Line Logging
|
||||
description: hogehoge
|
||||
enabled: false
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Security
|
||||
EventID: 4672
|
||||
PrivilegeList:
|
||||
contain: SeDebugPrivilege
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'CommandLine:%CommandLine% ParentProcessName:%ParentProcessName%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
19
rules/deep_blue_cli/sysmon/1.yml
Normal file
19
rules/deep_blue_cli/sysmon/1.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
title: Sysmon Check command lines
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Sysmon
|
||||
EventID: 1
|
||||
CommandLine: '.+'
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'CommandLine=%CommandLine%¥nParentImage=%ParentImage%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
|
||||
18
rules/deep_blue_cli/sysmon/7.yml
Normal file
18
rules/deep_blue_cli/sysmon/7.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
title: Check for unsigned EXEs/DLLs
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: Sysmon
|
||||
EventID: 7
|
||||
Signed: "false" # Compare by string
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'Message: Unsigned Image(DLL)¥n Result : Loaded by: %event_data.Image%¥nCommand : %event_data.ImageLoaded%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
17
rules/deep_blue_cli/system/104.yml
Normal file
17
rules/deep_blue_cli/system/104.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
title: The System log file was cleared
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: System
|
||||
EventID: 104
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: medium
|
||||
output: 'System Log Clear¥nThe System log was cleared.'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
19
rules/deep_blue_cli/system/7030.yml
Normal file
19
rules/deep_blue_cli/system/7030.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
title: This service may not function properly
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: System
|
||||
EventID: 7030
|
||||
param1:
|
||||
regexes: ./regexes.txt
|
||||
# condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'Interactive service warning¥nService name: %ServiceName%¥nMalware (and some third party software) trigger this warning'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
19
rules/deep_blue_cli/system/7036.yml
Normal file
19
rules/deep_blue_cli/system/7036.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
title: The ... service entered the stopped|running state
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: System
|
||||
EventID: 7036
|
||||
param1:
|
||||
regexes: ./regexes.txt
|
||||
condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'Suspicious Service Name¥nService name: %ServiceName%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
21
rules/deep_blue_cli/system/7040.yml
Normal file
21
rules/deep_blue_cli/system/7040.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
title: The start type of the Windows Event Log service was changed from auto start to disabled
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: System
|
||||
EventID: 7040
|
||||
param1: 'Windows Event Log'
|
||||
param2:
|
||||
- "disabled"
|
||||
- "auto start"
|
||||
condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'Service name : %param1%¥nMessage : Event Log Service Stopped¥nResults: Selective event log manipulation may follow this event.'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
22
rules/deep_blue_cli/system/7045.yml
Normal file
22
rules/deep_blue_cli/system/7045.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
title: A service was installed in the system
|
||||
description: hogehoge
|
||||
enabled: true
|
||||
author: Yea
|
||||
logsource:
|
||||
product: windows
|
||||
detection:
|
||||
selection:
|
||||
Channel: System
|
||||
EventID: 7045
|
||||
ServiceName:
|
||||
regexes: ./regexes.txt
|
||||
ImagePath:
|
||||
min_length: 1000
|
||||
whitelist: ./whitelist.txt
|
||||
condition: selection
|
||||
falsepositives:
|
||||
- unknown
|
||||
level: low
|
||||
output: 'New Service Created¥n%ImagePath¥nService name: %ServiceName%'
|
||||
creation_date: 2020/11/8
|
||||
uodated_date: 2020/11/8
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::detections::configs;
|
||||
use crate::detections::print;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use std::error::Error;
|
||||
use std::process;
|
||||
@@ -37,27 +37,50 @@ fn emit_csv(path: &str) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use std::fs::{read_to_string, remove_file};
|
||||
use std::io::Read;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_emit_csv() {
|
||||
{
|
||||
let mut messages = print::MESSAGES.lock().unwrap();
|
||||
let poke = Utc.ymd(1996, 2, 27).and_hms(1, 5, 1);
|
||||
messages.insert(poke, "pokepoke".to_string());
|
||||
}
|
||||
use crate::afterfact::emit_csv;
|
||||
use crate::detections::print;
|
||||
use serde_json::Value;
|
||||
use std::fs::{read_to_string, remove_file};
|
||||
|
||||
let expect = "Time,Message
|
||||
#[test]
|
||||
fn test_emit_csv() {
|
||||
{
|
||||
let mut messages = print::MESSAGES.lock().unwrap();
|
||||
let json_str = r##"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"CommandLine": "hoge"
|
||||
},
|
||||
"System": {
|
||||
"TimeCreated": {
|
||||
"#attributes":{
|
||||
"SystemTime": "1996-02-27T01:05:01Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"##;
|
||||
let event_record: Value = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
messages.insert(&event_record, "pokepoke".to_string());
|
||||
}
|
||||
|
||||
let expect = "Time,Message
|
||||
1996-02-27T01:05:01Z,pokepoke
|
||||
";
|
||||
|
||||
assert!(emit_csv(&"./test_emit_csv.csv".to_string()).is_ok());
|
||||
assert!(emit_csv(&"./test_emit_csv.csv".to_string()).is_ok());
|
||||
|
||||
match read_to_string("./test_emit_csv.csv") {
|
||||
Err(_) => panic!("Failed to open file"),
|
||||
Ok(s) => assert_eq!(s, expect),
|
||||
};
|
||||
match read_to_string("./test_emit_csv.csv") {
|
||||
Err(_) => panic!("Failed to open file"),
|
||||
Ok(s) => assert_eq!(s, expect),
|
||||
};
|
||||
|
||||
assert!(remove_file("./test_emit_csv.csv").is_ok());
|
||||
assert!(remove_file("./test_emit_csv.csv").is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
extern crate regex;
|
||||
|
||||
use crate::models::event;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct Application {}
|
||||
|
||||
impl Application {
|
||||
pub fn new() -> Application {
|
||||
Application {}
|
||||
}
|
||||
|
||||
pub fn detection(
|
||||
&mut self,
|
||||
event_id: String,
|
||||
system: &event::System,
|
||||
event_data: HashMap<String, String>,
|
||||
) {
|
||||
self.emet(&event_id, system);
|
||||
}
|
||||
|
||||
fn emet(&mut self, event_id: &String, system: &event::System) {
|
||||
if event_id != "2" {
|
||||
return;
|
||||
}
|
||||
|
||||
match &system.provider.name {
|
||||
Some(name) => {
|
||||
if name != "EMET" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
None => return,
|
||||
}
|
||||
match &system.message {
|
||||
Some(message) => {
|
||||
let message_split: Vec<&str> = message.split("\n").collect();
|
||||
if !message_split.is_empty() && message_split.len() >= 5 {
|
||||
let text = message_split[0];
|
||||
let application = message_split[3];
|
||||
let re = Regex::new(r"^Application: ").unwrap();
|
||||
let command = re.replace_all(application, "");
|
||||
let username = message_split[4];
|
||||
|
||||
println!("Message EMET Block");
|
||||
println!("Command : {}", command);
|
||||
println!("Results : {}", text);
|
||||
println!("Results : {}", username);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("Warning: EMET Message field is blank. Install EMET locally to see full details of this alert");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
extern crate regex;
|
||||
|
||||
use crate::models::event;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct AppLocker {}
|
||||
|
||||
impl AppLocker {
|
||||
pub fn new() -> AppLocker {
|
||||
AppLocker {}
|
||||
}
|
||||
|
||||
pub fn detection(
|
||||
&mut self,
|
||||
event_id: String,
|
||||
_system: &event::System,
|
||||
_event_data: HashMap<String, String>,
|
||||
) {
|
||||
self.applocker_log_warning(&event_id, &_system);
|
||||
self.applocker_log_block(&event_id, &_system);
|
||||
}
|
||||
|
||||
fn applocker_log_warning(&mut self, event_id: &String, system: &event::System) {
|
||||
if event_id != "8003" {
|
||||
return;
|
||||
}
|
||||
|
||||
let re = Regex::new(r" was .*$").unwrap();
|
||||
let default = "".to_string();
|
||||
let message = &system.message.as_ref().unwrap_or(&default);
|
||||
let command = re.replace_all(&message, "");
|
||||
|
||||
println!("Message Applocker Warning");
|
||||
println!("Command : {}", command);
|
||||
println!("Results : {}", message);
|
||||
}
|
||||
|
||||
fn applocker_log_block(&mut self, event_id: &String, system: &event::System) {
|
||||
if event_id != "8004" {
|
||||
return;
|
||||
}
|
||||
|
||||
let re = Regex::new(r" was .*$").unwrap();
|
||||
let default = "".to_string();
|
||||
let message = &system.message.as_ref().unwrap_or(&default);
|
||||
let command = re.replace_all(&message, "");
|
||||
|
||||
println!("Message Applocker Block");
|
||||
println!("Command : {}", command);
|
||||
println!("Results : {}", message);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
use crate::models::event;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Common {
|
||||
record_id: u64,
|
||||
date: String,
|
||||
record_id_list: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Common {
|
||||
pub fn new() -> Common {
|
||||
Common {
|
||||
record_id: 0,
|
||||
date: "".to_string(),
|
||||
record_id_list: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disp(&self) {
|
||||
for (record_id, date) in self.record_id_list.iter() {
|
||||
println!("date:{:?} record-id: {:?}", date, record_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detection(&mut self, system: &event::System, event_data: &HashMap<String, String>) {
|
||||
self.check_record_id(system);
|
||||
}
|
||||
|
||||
//
|
||||
// Record IDがシーケンスになっているかチェック
|
||||
//
|
||||
fn check_record_id(&mut self, system: &event::System) {
|
||||
let event_record_id: u64 = system.event_record_id.parse().unwrap();
|
||||
if self.record_id > 0 && event_record_id - self.record_id > 1 {
|
||||
self.record_id_list.insert(
|
||||
self.record_id.to_string() + " - " + &system.event_record_id.to_string(),
|
||||
self.date.to_string() + " - " + &system.time_created.system_time.to_string(),
|
||||
);
|
||||
}
|
||||
self.record_id = event_record_id;
|
||||
self.date = system.time_created.system_time.to_string();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
use crate::detections::utils;
|
||||
use clap::{App, AppSettings, Arg, ArgMatches};
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Once;
|
||||
|
||||
#[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 +15,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,25 +53,44 @@ fn build_app() -> clap::App<'static, 'static> {
|
||||
.arg(Arg::from_usage("--credits 'Zachary Mathis, Akira Nishikawa'"))
|
||||
}
|
||||
|
||||
fn read_csv(filename: &str) -> Vec<Vec<String>> {
|
||||
let mut f = File::open(filename).expect("file not found!!!");
|
||||
let mut contents: String = String::new();
|
||||
let mut ret = vec![];
|
||||
if f.read_to_string(&mut contents).is_err() {
|
||||
return ret;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventKeyAliasConfig {
|
||||
key_to_eventkey: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl EventKeyAliasConfig {
|
||||
pub fn new() -> EventKeyAliasConfig {
|
||||
return EventKeyAliasConfig {
|
||||
key_to_eventkey: HashMap::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut rdr = csv::Reader::from_reader(contents.as_bytes());
|
||||
rdr.records().for_each(|r| {
|
||||
if r.is_err() {
|
||||
pub fn get_event_key(&self, alias: String) -> Option<&String> {
|
||||
return self.key_to_eventkey.get(&alias);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_eventkey_alias() -> EventKeyAliasConfig {
|
||||
let mut config = EventKeyAliasConfig::new();
|
||||
|
||||
let read_result = utils::read_csv("config/eventkey_alias.txt");
|
||||
// eventkey_aliasが読み込めなかったらエラーで終了とする。
|
||||
read_result.unwrap().into_iter().for_each(|line| {
|
||||
if line.len() != 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let line = r.unwrap();
|
||||
let mut v = vec![];
|
||||
line.iter().for_each(|s| v.push(s.to_string()));
|
||||
ret.push(v);
|
||||
let empty = &"".to_string();
|
||||
let alias = line.get(0).unwrap_or(empty);
|
||||
let event_key = line.get(1).unwrap_or(empty);
|
||||
if alias.len() == 0 || event_key.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
config
|
||||
.key_to_eventkey
|
||||
.insert(alias.to_owned(), event_key.to_owned());
|
||||
});
|
||||
|
||||
return ret;
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1,74 +1,101 @@
|
||||
extern crate chrono;
|
||||
extern crate csv;
|
||||
extern crate quick_xml;
|
||||
|
||||
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::print::Message;
|
||||
use crate::detections::rule;
|
||||
use crate::detections::rule::RuleNode;
|
||||
use crate::yaml::ParseYaml;
|
||||
|
||||
use chrono::{DateTime, FixedOffset, TimeZone, Utc};
|
||||
use evtx::EvtxParser;
|
||||
use quick_xml::de::DeError;
|
||||
use std::collections::BTreeMap;
|
||||
use serde_json::{Error, Value};
|
||||
|
||||
const DIRPATH_RULES: &str = "rules";
|
||||
|
||||
// TODO テストケースかかなきゃ...
|
||||
#[derive(Debug)]
|
||||
pub struct Detection {
|
||||
timeline_list: BTreeMap<String, String>,
|
||||
}
|
||||
pub struct Detection {}
|
||||
|
||||
impl Detection {
|
||||
pub fn new() -> Detection {
|
||||
Detection {
|
||||
timeline_list: BTreeMap::new(),
|
||||
}
|
||||
Detection {}
|
||||
}
|
||||
|
||||
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>) {
|
||||
// serialize from .etvx to jsons
|
||||
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),
|
||||
}
|
||||
|
||||
//// 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();
|
||||
|
||||
// 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 mut selection_rules: Vec<RuleNode> = rulefile_loader
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|rule_file| rule::parse_rule(rule_file))
|
||||
.filter_map(|mut rule| {
|
||||
let err_msgs_result = rule.init();
|
||||
if err_msgs_result.is_ok() {
|
||||
return Option::Some(rule);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
// ruleファイルの初期化失敗時のエラーを表示する部分
|
||||
err_msgs_result.err().iter().for_each(|err_msgs| {
|
||||
// TODO 本当はファイルパスを出力したい
|
||||
// ParseYamlの変更が必要なので、一旦yamlのタイトルを表示。
|
||||
|
||||
// TODO エラーの出力方法を統一したい。
|
||||
// エラー出力用のクラスを作成してもいいかも
|
||||
println!(
|
||||
"[ERROR] Failed to parse Rule file. (Error Rule Title : {})",
|
||||
rule.yaml["title"].as_str().unwrap_or("")
|
||||
);
|
||||
err_msgs.iter().for_each(|err_msg| println!("{}", err_msg));
|
||||
println!("");
|
||||
});
|
||||
|
||||
return Option::None;
|
||||
})
|
||||
.collect();
|
||||
|
||||
// selection rule files and collect message
|
||||
let mut message = Message::new();
|
||||
selection_rules.iter_mut().for_each(|rule| {
|
||||
event_records.iter().for_each(|event_record| {
|
||||
if !rule.select(event_record) {
|
||||
return;
|
||||
}
|
||||
|
||||
message.insert(
|
||||
event_record,
|
||||
rule.yaml["output"].as_str().unwrap_or("").to_string(),
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
// output message
|
||||
message.print();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
mod application;
|
||||
mod applocker;
|
||||
mod common;
|
||||
pub mod configs;
|
||||
pub mod detection;
|
||||
mod powershell;
|
||||
pub mod print;
|
||||
mod security;
|
||||
mod sysmon;
|
||||
mod system;
|
||||
mod rule;
|
||||
mod utils;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
use crate::detections::utils;
|
||||
use crate::models::event;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct PowerShell {}
|
||||
|
||||
impl PowerShell {
|
||||
pub fn new() -> PowerShell {
|
||||
PowerShell {}
|
||||
}
|
||||
|
||||
pub fn detection(
|
||||
&mut self,
|
||||
event_id: String,
|
||||
_system: &event::System,
|
||||
event_data: HashMap<String, String>,
|
||||
) {
|
||||
self.execute_pipeline(&event_id, &event_data);
|
||||
self.execute_remote_command(&event_id, &event_data);
|
||||
}
|
||||
|
||||
fn execute_pipeline(&mut self, event_id: &String, event_data: &HashMap<String, String>) {
|
||||
if event_id != "4103" {
|
||||
return;
|
||||
}
|
||||
|
||||
let default = String::from("");
|
||||
let commandline = event_data.get("ContextInfo").unwrap_or(&default);
|
||||
|
||||
if commandline.contains("Host Application")
|
||||
|| commandline.contains("ホスト アプリケーション")
|
||||
{
|
||||
let rm_before =
|
||||
Regex::new("(?ms)^.*(ホスト アプリケーション|Host Application) = ").unwrap();
|
||||
let rm_after = Regex::new("(?ms)\n.*$").unwrap();
|
||||
|
||||
let temp_command_with_extra = rm_before.replace_all(commandline, "");
|
||||
let command = rm_after.replace_all(&temp_command_with_extra, "");
|
||||
|
||||
if command != "" {
|
||||
utils::check_command(4103, &command, 1000, 0, &default, &default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_remote_command(&mut self, event_id: &String, event_data: &HashMap<String, String>) {
|
||||
if event_id != "4104" {
|
||||
return;
|
||||
}
|
||||
|
||||
let default = String::from("");
|
||||
let path = event_data.get("Path").unwrap().to_string();
|
||||
if path == "".to_string() {
|
||||
let commandline = event_data.get("ScriptBlockText").unwrap_or(&default);
|
||||
if commandline.to_string() != default {
|
||||
utils::check_command(4104, &commandline, 1000, 0, &default, &default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
extern crate chrono;
|
||||
extern crate lazy_static;
|
||||
use crate::detections::configs;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -21,7 +25,15 @@ impl Message {
|
||||
}
|
||||
|
||||
/// メッセージを設定
|
||||
pub fn insert(&mut self, time: DateTime<Utc>, message: String) {
|
||||
pub fn insert(&mut self, event_record: &Value, output: String) {
|
||||
if output.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = &self.parse_message(event_record, output);
|
||||
let default_time = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0);
|
||||
let time = Message::get_event_time(event_record).unwrap_or(default_time);
|
||||
|
||||
match self.map.get_mut(&time) {
|
||||
Some(v) => {
|
||||
v.push(message.to_string());
|
||||
@@ -33,6 +45,44 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_message(&mut self, event_record: &Value, output: String) -> String {
|
||||
let mut return_message: String = output;
|
||||
let mut hash_map: HashMap<String, String> = HashMap::new();
|
||||
let re = Regex::new(r"%[a-zA-Z0-9-_]+%").unwrap();
|
||||
for caps in re.captures_iter(&return_message) {
|
||||
let full_target_str = &caps[0];
|
||||
let target_length = full_target_str.chars().count() - 2; // The meaning of 2 is two percent
|
||||
let target_str = full_target_str
|
||||
.chars()
|
||||
.skip(1)
|
||||
.take(target_length)
|
||||
.collect::<String>();
|
||||
|
||||
if let Some(array_str) = configs::singleton()
|
||||
.event_key_alias_config
|
||||
.get_event_key(target_str.to_string())
|
||||
{
|
||||
let split: Vec<&str> = array_str.split(".").collect();
|
||||
let mut tmp_event_record: &Value = event_record.into();
|
||||
for s in split {
|
||||
if let Some(record) = tmp_event_record.get(s) {
|
||||
tmp_event_record = record;
|
||||
}
|
||||
}
|
||||
hash_map.insert(
|
||||
full_target_str.to_string(),
|
||||
tmp_event_record.as_str().unwrap_or("").to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in &hash_map {
|
||||
return_message = return_message.replace(k, v);
|
||||
}
|
||||
|
||||
return_message
|
||||
}
|
||||
|
||||
/// メッセージを返す
|
||||
pub fn get(&self, time: DateTime<Utc>) -> Vec<String> {
|
||||
match self.map.get(&time) {
|
||||
@@ -46,22 +96,122 @@ impl Message {
|
||||
println!("{:?}", self.map);
|
||||
}
|
||||
|
||||
/// 最後に表示を行う
|
||||
pub fn print(&self) {
|
||||
for (key, values) in self.map.iter() {
|
||||
for value in values.iter() {
|
||||
println!("{} : {}", key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> &BTreeMap<DateTime<Utc>, Vec<String>> {
|
||||
&self.map
|
||||
}
|
||||
|
||||
fn get_event_time(event_record: &Value) -> Option<DateTime<Utc>> {
|
||||
let system_time =
|
||||
&event_record["Event"]["System"]["TimeCreated"]["#attributes"]["SystemTime"];
|
||||
let system_time_str = system_time.as_str().unwrap_or("");
|
||||
if system_time_str.is_empty() {
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
let rfc3339_time = DateTime::parse_from_rfc3339(system_time_str);
|
||||
if rfc3339_time.is_err() {
|
||||
return Option::None;
|
||||
}
|
||||
let datetime = Utc
|
||||
.from_local_datetime(&rfc3339_time.unwrap().naive_utc())
|
||||
.single();
|
||||
if datetime.is_none() {
|
||||
return Option::None;
|
||||
} else {
|
||||
return Option::Some(datetime.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_and_append_message() {
|
||||
let mut message = Message::new();
|
||||
let poke = Utc.ymd(1996, 2, 27).and_hms(1, 5, 1);
|
||||
let taka = Utc.ymd(2000, 1, 21).and_hms(9, 6, 1);
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::detections::print::Message;
|
||||
use serde_json::Value;
|
||||
|
||||
message.insert(poke, "TEST".to_string());
|
||||
message.insert(poke, "TEST2".to_string());
|
||||
message.insert(taka, "TEST3".to_string());
|
||||
#[test]
|
||||
fn test_create_and_append_message() {
|
||||
let mut message = Message::new();
|
||||
let json_str_1 = r##"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"CommandLine": "hoge"
|
||||
},
|
||||
"System": {
|
||||
"TimeCreated": {
|
||||
"#attributes":{
|
||||
"SystemTime": "1996-02-27T01:05:01Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"##;
|
||||
let event_record_1: Value = serde_json::from_str(json_str_1).unwrap();
|
||||
message.insert(&event_record_1, "CommandLine1: %CommandLine%".to_string());
|
||||
|
||||
let display = format!("{}", format_args!("{:?}", message));
|
||||
let expect = "Message { map: {1996-02-27T01:05:01Z: [\"TEST\", \"TEST2\"], 2000-01-21T09:06:01Z: [\"TEST3\"]} }";
|
||||
assert_eq!(display, expect);
|
||||
let json_str_2 = r##"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"CommandLine": "hoge"
|
||||
},
|
||||
"System": {
|
||||
"TimeCreated": {
|
||||
"#attributes":{
|
||||
"SystemTime": "1996-02-27T01:05:01Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"##;
|
||||
let event_record_2: Value = serde_json::from_str(json_str_2).unwrap();
|
||||
message.insert(&event_record_2, "CommandLine2: %CommandLine%".to_string());
|
||||
|
||||
let json_str_3 = r##"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"CommandLine": "hoge"
|
||||
},
|
||||
"System": {
|
||||
"TimeCreated": {
|
||||
"#attributes":{
|
||||
"SystemTime": "2000-01-21T09:06:01Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"##;
|
||||
let event_record_3: Value = serde_json::from_str(json_str_3).unwrap();
|
||||
message.insert(&event_record_3, "CommandLine3: %CommandLine%".to_string());
|
||||
|
||||
let json_str_4 = r##"
|
||||
{
|
||||
"Event": {
|
||||
"EventData": {
|
||||
"CommandLine": "hoge"
|
||||
}
|
||||
}
|
||||
}
|
||||
"##;
|
||||
let event_record_4: Value = serde_json::from_str(json_str_4).unwrap();
|
||||
message.insert(&event_record_4, "CommandLine4: %CommandLine%".to_string());
|
||||
|
||||
let display = format!("{}", format_args!("{:?}", message));
|
||||
println!("display::::{}", display);
|
||||
let expect = "Message { map: {1970-01-01T00:00:00Z: [\"CommandLine4: hoge\"], 1996-02-27T01:05:01Z: [\"CommandLine1: hoge\", \"CommandLine2: hoge\"], 2000-01-21T09:06:01Z: [\"CommandLine3: hoge\"]} }";
|
||||
assert_eq!(display, expect);
|
||||
}
|
||||
}
|
||||
|
||||
582
src/detections/rule.rs
Normal file
582
src/detections/rule.rs
Normal file
@@ -0,0 +1,582 @@
|
||||
extern crate regex;
|
||||
|
||||
use crate::detections::utils;
|
||||
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
// TODO テストケースかかなきゃ...
|
||||
pub fn parse_rule(yaml: Yaml) -> RuleNode {
|
||||
let detection = parse_detection(&yaml);
|
||||
|
||||
return RuleNode {
|
||||
yaml: yaml,
|
||||
detection: detection,
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_detection(yaml: &Yaml) -> Option<DetectionNode> {
|
||||
if yaml["detection"].is_badvalue() {
|
||||
return Option::None;
|
||||
} else {
|
||||
let node = DetectionNode {
|
||||
selection: parse_selection(&yaml),
|
||||
};
|
||||
return Option::Some(node);
|
||||
}
|
||||
}
|
||||
|
||||
fn concat_selection_key(key_list: &Vec<String>) -> String {
|
||||
return key_list
|
||||
.iter()
|
||||
.fold("detection -> selection".to_string(), |mut acc, cur| {
|
||||
acc = acc + " -> " + cur;
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_selection(yaml: &Yaml) -> Option<Box<dyn SelectionNode>> {
|
||||
// TODO detection-selectionが存在しない場合のチェック
|
||||
let selection_yaml = &yaml["detection"]["selection"];
|
||||
if selection_yaml.is_badvalue() {
|
||||
return Option::None;
|
||||
}
|
||||
return Option::Some(parse_selection_recursively(vec![], &selection_yaml));
|
||||
}
|
||||
|
||||
fn parse_selection_recursively(key_list: Vec<String>, yaml: &Yaml) -> Box<dyn SelectionNode> {
|
||||
if yaml.as_hash().is_some() {
|
||||
// 連想配列はAND条件と解釈する
|
||||
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() {
|
||||
// 配列はOR条件と解釈する。
|
||||
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(LeafSelectionNode::new(key_list, yaml.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Ruleファイルを表すノード
|
||||
pub struct RuleNode {
|
||||
pub yaml: Yaml,
|
||||
detection: Option<DetectionNode>,
|
||||
}
|
||||
|
||||
impl RuleNode {
|
||||
pub fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
let mut errmsgs: Vec<String> = vec![];
|
||||
|
||||
// field check
|
||||
if self.yaml["output"].as_str().unwrap_or("").is_empty() {
|
||||
errmsgs.push("Cannot find required key. key:output".to_string());
|
||||
}
|
||||
|
||||
// detection node initialization
|
||||
self.detection.as_mut().and_then(|detection| {
|
||||
let detection_result = detection.init();
|
||||
if detection_result.is_err() {
|
||||
errmsgs.extend(detection_result.unwrap_err());
|
||||
}
|
||||
return Option::Some(detection);
|
||||
});
|
||||
|
||||
if errmsgs.is_empty() {
|
||||
return Result::Ok(());
|
||||
} else {
|
||||
return Result::Err(errmsgs);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(&mut self, event_record: &Value) -> bool {
|
||||
let selection = self
|
||||
.detection
|
||||
.as_mut()
|
||||
.and_then(|detect_node| detect_node.selection.as_mut());
|
||||
if selection.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selection.unwrap().select(event_record);
|
||||
}
|
||||
}
|
||||
|
||||
// Ruleファイルのdetectionを表すノード
|
||||
struct DetectionNode {
|
||||
pub selection: Option<Box<dyn SelectionNode>>,
|
||||
}
|
||||
|
||||
impl DetectionNode {
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
if self.selection.is_none() {
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
return self.selection.as_mut().unwrap().init();
|
||||
}
|
||||
}
|
||||
|
||||
// Ruleファイルの detection- selection配下のノードはこのtraitを実装する。
|
||||
trait SelectionNode {
|
||||
fn select(&mut self, event_record: &Value) -> bool;
|
||||
fn init(&mut self) -> Result<(), Vec<String>>;
|
||||
}
|
||||
|
||||
// detection - selection配下でAND条件を表すノード
|
||||
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(&mut self, event_record: &Value) -> bool {
|
||||
return self.child_nodes.iter_mut().all(|child_node| {
|
||||
return child_node.select(event_record);
|
||||
});
|
||||
}
|
||||
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
let err_msgs = self
|
||||
.child_nodes
|
||||
.iter_mut()
|
||||
.map(|child_node| {
|
||||
let res = child_node.init();
|
||||
if res.is_err() {
|
||||
return res.unwrap_err();
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
})
|
||||
.fold(
|
||||
vec![],
|
||||
|mut acc: Vec<String>, cur: Vec<String>| -> Vec<String> {
|
||||
acc.extend(cur.into_iter());
|
||||
return acc;
|
||||
},
|
||||
);
|
||||
|
||||
if err_msgs.is_empty() {
|
||||
return Result::Ok(());
|
||||
} else {
|
||||
return Result::Err(err_msgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detection - selection配下でOr条件を表すノード
|
||||
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(&mut self, event_record: &Value) -> bool {
|
||||
return self.child_nodes.iter_mut().any(|child_node| {
|
||||
return child_node.select(event_record);
|
||||
});
|
||||
}
|
||||
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
let err_msgs = self
|
||||
.child_nodes
|
||||
.iter_mut()
|
||||
.map(|child_node| {
|
||||
let res = child_node.init();
|
||||
if res.is_err() {
|
||||
return res.unwrap_err();
|
||||
} else {
|
||||
return vec![];
|
||||
}
|
||||
})
|
||||
.fold(
|
||||
vec![],
|
||||
|mut acc: Vec<String>, cur: Vec<String>| -> Vec<String> {
|
||||
acc.extend(cur.into_iter());
|
||||
return acc;
|
||||
},
|
||||
);
|
||||
|
||||
if err_msgs.is_empty() {
|
||||
return Result::Ok(());
|
||||
} else {
|
||||
return Result::Err(err_msgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detection - selection配下の末端ノード
|
||||
struct LeafSelectionNode {
|
||||
key_list: Vec<String>,
|
||||
select_value: Yaml,
|
||||
matcher: Option<Box<dyn LeafMatcher>>,
|
||||
}
|
||||
|
||||
impl LeafSelectionNode {
|
||||
fn new(key_list: Vec<String>, value_yaml: Yaml) -> LeafSelectionNode {
|
||||
return LeafSelectionNode {
|
||||
key_list: key_list,
|
||||
select_value: value_yaml,
|
||||
matcher: Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
// JSON形式のEventJSONから値を取得する関数 aliasも考慮されている。
|
||||
fn get_event_value<'a>(&self, event_value: &'a Value) -> Option<&'a Value> {
|
||||
if self.key_list.is_empty() {
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
return utils::get_event_value(&self.key_list[0].to_string(), event_value);
|
||||
}
|
||||
|
||||
// LeafMatcherの一覧を取得する。
|
||||
fn get_matchers(&self) -> Vec<Box<dyn LeafMatcher>> {
|
||||
return vec![
|
||||
Box::new(RegexMatcher::new()),
|
||||
Box::new(MinlengthMatcher::new()),
|
||||
Box::new(RegexesFileMatcher::new()),
|
||||
Box::new(WhitelistFileMatcher::new()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionNode for LeafSelectionNode {
|
||||
fn select(&mut self, event_record: &Value) -> bool {
|
||||
if self.matcher.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let event_value = self.get_event_value(event_record);
|
||||
return self.matcher.as_mut().unwrap().is_match(event_value);
|
||||
}
|
||||
|
||||
fn init(&mut self) -> Result<(), Vec<String>> {
|
||||
let matchers = self.get_matchers();
|
||||
let mut match_key_list = self.key_list.clone();
|
||||
match_key_list.remove(0);
|
||||
self.matcher = matchers
|
||||
.into_iter()
|
||||
.find(|matcher| matcher.is_target_key(&match_key_list));
|
||||
// 一致するmatcherが見つからないエラー
|
||||
if self.matcher.is_none() {
|
||||
return Result::Err(vec![format!(
|
||||
"Found unknown key. key:{}",
|
||||
concat_selection_key(&match_key_list)
|
||||
)]);
|
||||
}
|
||||
|
||||
if self.select_value.is_badvalue() {
|
||||
return Result::Err(vec![format!(
|
||||
"Cannot parse yaml file. key:{}",
|
||||
concat_selection_key(&match_key_list)
|
||||
)]);
|
||||
}
|
||||
|
||||
return self
|
||||
.matcher
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.init(&match_key_list, &self.select_value);
|
||||
}
|
||||
}
|
||||
|
||||
// 末端ノードがEventLogの値を比較するロジックを表す。
|
||||
// 正規条件のマッチや文字数制限など、比較ロジック毎にこのtraitを実装したクラスが存在する。
|
||||
//
|
||||
// 新規にLeafMatcherを実装するクラスを作成した場合、
|
||||
// LeafSelectionNodeのget_matchersクラスの戻り値の配列に新規作成したクラスのインスタンスを追加する。
|
||||
trait LeafMatcher {
|
||||
fn is_target_key(&self, key_list: &Vec<String>) -> bool;
|
||||
|
||||
fn is_match(&mut self, event_value: Option<&Value>) -> bool;
|
||||
|
||||
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>>;
|
||||
}
|
||||
|
||||
// 正規表現で比較するロジックを表すクラス
|
||||
struct RegexMatcher {
|
||||
re: Option<Regex>,
|
||||
}
|
||||
|
||||
impl RegexMatcher {
|
||||
fn new() -> RegexMatcher {
|
||||
return RegexMatcher {
|
||||
re: Option::None, // empty
|
||||
};
|
||||
}
|
||||
fn is_regex_fullmatch(&self, re: &Regex, value: String) -> bool {
|
||||
return re.find_iter(&value).any(|match_obj| {
|
||||
return match_obj.as_str().to_string() == value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl LeafMatcher for RegexMatcher {
|
||||
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
|
||||
if key_list.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if key_list.len() == 1 {
|
||||
return key_list.get(0).unwrap_or(&"".to_string()) == &"regex".to_string();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
|
||||
if select_value.is_null() {
|
||||
self.re = Option::None;
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
// stringで比較する。
|
||||
let yaml_value = match select_value {
|
||||
Yaml::Boolean(b) => Option::Some(b.to_string()),
|
||||
Yaml::Integer(i) => Option::Some(i.to_string()),
|
||||
Yaml::Real(r) => Option::Some(r.to_string()),
|
||||
Yaml::String(s) => Option::Some(s.to_owned()),
|
||||
_ => Option::None,
|
||||
};
|
||||
// ここには来ないはず
|
||||
if yaml_value.is_none() {
|
||||
let errmsg = format!(
|
||||
"unknown error occured. [key:{}]",
|
||||
concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
|
||||
// 指定された正規表現が間違っていて、パースに失敗した場合
|
||||
let yaml_str = yaml_value.unwrap();
|
||||
let re_result = Regex::new(&yaml_str);
|
||||
if re_result.is_err() {
|
||||
let errmsg = format!(
|
||||
"cannot parse regex. [regex:{}, key:{}]",
|
||||
yaml_str,
|
||||
concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
self.re = re_result.ok();
|
||||
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
fn is_match(&mut self, event_value: Option<&Value>) -> bool {
|
||||
// unwrap_orの引数に""ではなく" "を指定しているのは、
|
||||
// event_valueが文字列じゃない場合にis_event_value_nullの値がfalseになるように、len() == 0とならない値を指定している。
|
||||
let is_event_value_null = event_value.is_none()
|
||||
|| event_value.unwrap().is_null()
|
||||
|| event_value.unwrap().as_str().unwrap_or(" ").len() == 0;
|
||||
|
||||
// yamlにnullが設定されていた場合
|
||||
if self.re.is_none() {
|
||||
return is_event_value_null;
|
||||
}
|
||||
|
||||
return match event_value.unwrap_or(&Value::Null) {
|
||||
Value::Bool(b) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), b.to_string()),
|
||||
Value::String(s) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), s.to_owned()),
|
||||
Value::Number(n) => self.is_regex_fullmatch(self.re.as_ref().unwrap(), n.to_string()),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 指定された文字数以上であることをチェックするクラス。
|
||||
struct MinlengthMatcher {
|
||||
min_len: i64,
|
||||
}
|
||||
|
||||
impl MinlengthMatcher {
|
||||
fn new() -> MinlengthMatcher {
|
||||
return MinlengthMatcher { min_len: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
impl LeafMatcher for MinlengthMatcher {
|
||||
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
|
||||
if key_list.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
return key_list.get(0).unwrap() == "min_length";
|
||||
}
|
||||
|
||||
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
|
||||
let min_length = select_value.as_i64();
|
||||
if min_length.is_none() {
|
||||
let errmsg = format!(
|
||||
"min_length value should be Integer. [key:{}]",
|
||||
concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
|
||||
self.min_len = min_length.unwrap();
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
fn is_match(&mut self, event_value: Option<&Value>) -> bool {
|
||||
return match event_value.unwrap_or(&Value::Null) {
|
||||
Value::String(s) => s.len() as i64 >= self.min_len,
|
||||
Value::Number(n) => n.to_string().len() as i64 >= self.min_len,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 正規表現のリストが記載されたファイルを読み取って、比較するロジックを表すクラス
|
||||
// DeepBlueCLIのcheck_cmdメソッドの一部に同様の処理が実装されていた。
|
||||
struct RegexesFileMatcher {
|
||||
regexes_csv_content: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl RegexesFileMatcher {
|
||||
fn new() -> RegexesFileMatcher {
|
||||
return RegexesFileMatcher {
|
||||
regexes_csv_content: vec![],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl LeafMatcher for RegexesFileMatcher {
|
||||
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
|
||||
if key_list.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
return key_list.get(0).unwrap() == "regexes";
|
||||
}
|
||||
|
||||
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
|
||||
let value = match select_value {
|
||||
Yaml::String(s) => Option::Some(s.to_owned()),
|
||||
Yaml::Integer(i) => Option::Some(i.to_string()),
|
||||
Yaml::Real(r) => Option::Some(r.to_owned()),
|
||||
_ => Option::None,
|
||||
};
|
||||
if value.is_none() {
|
||||
let errmsg = format!(
|
||||
"regexes value should be String. [key:{}]",
|
||||
concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
|
||||
let csv_content = utils::read_csv(&value.unwrap());
|
||||
if csv_content.is_err() {
|
||||
let errmsg = format!(
|
||||
"cannot read regexes file. [key:{}]",
|
||||
concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
self.regexes_csv_content = csv_content.unwrap();
|
||||
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
fn is_match(&mut self, event_value: Option<&Value>) -> bool {
|
||||
return match event_value.unwrap_or(&Value::Null) {
|
||||
Value::String(s) => !utils::check_regex(s, 0, &self.regexes_csv_content).is_empty(),
|
||||
Value::Number(n) => {
|
||||
!utils::check_regex(&n.to_string(), 0, &self.regexes_csv_content).is_empty()
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ファイルに列挙された文字列に一致する場合に検知するロジックを表す
|
||||
// DeepBlueCLIのcheck_cmdメソッドの一部に同様の処理が実装されていた。
|
||||
struct WhitelistFileMatcher {
|
||||
whitelist_csv_content: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl WhitelistFileMatcher {
|
||||
fn new() -> WhitelistFileMatcher {
|
||||
return WhitelistFileMatcher {
|
||||
whitelist_csv_content: vec![],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl LeafMatcher for WhitelistFileMatcher {
|
||||
fn is_target_key(&self, key_list: &Vec<String>) -> bool {
|
||||
if key_list.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
return key_list.get(0).unwrap() == "whitelist";
|
||||
}
|
||||
|
||||
fn init(&mut self, key_list: &Vec<String>, select_value: &Yaml) -> Result<(), Vec<String>> {
|
||||
let value = match select_value {
|
||||
Yaml::String(s) => Option::Some(s.to_owned()),
|
||||
Yaml::Integer(i) => Option::Some(i.to_string()),
|
||||
Yaml::Real(r) => Option::Some(r.to_owned()),
|
||||
_ => Option::None,
|
||||
};
|
||||
if value.is_none() {
|
||||
let errmsg = format!(
|
||||
"whitelist value should be String. [key:{}]",
|
||||
concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
|
||||
let csv_content = utils::read_csv(&value.unwrap());
|
||||
if csv_content.is_err() {
|
||||
let errmsg = format!(
|
||||
"cannot read whitelist file. [key:{}]",
|
||||
concat_selection_key(key_list)
|
||||
);
|
||||
return Result::Err(vec![errmsg]);
|
||||
}
|
||||
self.whitelist_csv_content = csv_content.unwrap();
|
||||
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
fn is_match(&mut self, event_value: Option<&Value>) -> bool {
|
||||
return match event_value.unwrap_or(&Value::Null) {
|
||||
Value::String(s) => utils::check_whitelist(s, &self.whitelist_csv_content),
|
||||
Value::Number(n) => utils::check_whitelist(&n.to_string(), &self.whitelist_csv_content),
|
||||
Value::Bool(b) => utils::check_whitelist(&b.to_string(), &self.whitelist_csv_content),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +0,0 @@
|
||||
use crate::detections::utils::check_command;
|
||||
use crate::models::event;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct Sysmon {
|
||||
checkunsigned: u16,
|
||||
}
|
||||
|
||||
impl Sysmon {
|
||||
pub fn new() -> Sysmon {
|
||||
Sysmon { checkunsigned: 0 }
|
||||
}
|
||||
|
||||
pub fn detection(
|
||||
&mut self,
|
||||
event_id: String,
|
||||
_system: &event::System,
|
||||
event_data: HashMap<String, String>,
|
||||
) {
|
||||
self.check_command_lines(&event_id, &event_data);
|
||||
self.check_for_unsigned_files(&event_id, &event_data);
|
||||
}
|
||||
|
||||
fn check_command_lines(&mut self, event_id: &String, event_data: &HashMap<String, String>) {
|
||||
if event_id != "1" {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_command_line) = event_data.get("CommandLine") {
|
||||
let default = "".to_string();
|
||||
let _creater = event_data.get("ParentImage").unwrap_or(&default);
|
||||
|
||||
check_command(1, _command_line, 1000, 0, "", _creater);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_unsigned_files(
|
||||
&mut self,
|
||||
event_id: &String,
|
||||
event_data: &HashMap<String, String>,
|
||||
) {
|
||||
if event_id != "7" {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.checkunsigned == 1 {
|
||||
let default = "".to_string();
|
||||
let _signed = event_data.get("Signed").unwrap_or(&default);
|
||||
if _signed == "false" {
|
||||
let _image = event_data.get("Image").unwrap_or(&default);
|
||||
let _command_line = event_data.get("ImageLoaded").unwrap_or(&default);
|
||||
|
||||
println!("Message : Unsigned Image (DLL)");
|
||||
println!("Result : Loaded by: {}", _image);
|
||||
println!("Command : {}", _command_line);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
use crate::detections::utils;
|
||||
use crate::models::event;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct System {}
|
||||
|
||||
impl System {
|
||||
pub fn new() -> System {
|
||||
System {}
|
||||
}
|
||||
|
||||
pub fn detection(
|
||||
&mut self,
|
||||
event_id: String,
|
||||
system: &event::System,
|
||||
event_data: HashMap<String, String>,
|
||||
) {
|
||||
self.system_log_clear(&event_id);
|
||||
self.windows_event_log(&event_id, &event_data);
|
||||
self.new_service_created(&event_id, &event_data);
|
||||
self.interactive_service_warning(&event_id, &event_data);
|
||||
self.suspicious_service_name(&event_id, &event_data);
|
||||
}
|
||||
|
||||
fn new_service_created(&mut self, event_id: &String, event_data: &HashMap<String, String>) {
|
||||
if event_id != "7045" {
|
||||
return;
|
||||
}
|
||||
|
||||
let default = String::from("");
|
||||
let servicename = &event_data.get("ServiceName").unwrap_or(&default);
|
||||
let commandline = &event_data.get("ImagePath").unwrap_or(&default);
|
||||
let text = utils::check_regex(&servicename, 1);
|
||||
if !text.is_empty() {
|
||||
println!("Message : New Service Created");
|
||||
println!("Command : {}", commandline);
|
||||
println!("Results : Service name: {}", servicename);
|
||||
println!("Results : {}", text);
|
||||
}
|
||||
if !commandline.is_empty() {
|
||||
utils::check_command(7045, &commandline, 1000, 0, &servicename, &"");
|
||||
}
|
||||
}
|
||||
|
||||
fn interactive_service_warning(
|
||||
&mut self,
|
||||
event_id: &String,
|
||||
event_data: &HashMap<String, String>,
|
||||
) {
|
||||
if event_id != "7030" {
|
||||
return;
|
||||
}
|
||||
|
||||
let default = String::from("");
|
||||
let servicename = &event_data.get("param1").unwrap_or(&default);
|
||||
println!("Message : Interactive service warning");
|
||||
println!("Results : Service name: {}", servicename);
|
||||
println!("Results : Malware (and some third party software) trigger this warning");
|
||||
println!("{}", utils::check_regex(&servicename, 1));
|
||||
}
|
||||
|
||||
fn suspicious_service_name(&mut self, event_id: &String, event_data: &HashMap<String, String>) {
|
||||
if event_id != "7036" {
|
||||
return;
|
||||
}
|
||||
|
||||
let default = String::from("");
|
||||
let servicename = &event_data.get("param1").unwrap_or(&default);
|
||||
let text = utils::check_regex(&servicename, 1);
|
||||
if !text.is_empty() {
|
||||
println!("Message : Suspicious Service Name");
|
||||
println!("Results : Service name: {}", servicename);
|
||||
println!("Results : {}", text);
|
||||
}
|
||||
}
|
||||
|
||||
fn system_log_clear(&mut self, event_id: &String) {
|
||||
if event_id != "104" {
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Message : System Log Clear");
|
||||
println!("Results : The System log was cleared.");
|
||||
}
|
||||
|
||||
fn windows_event_log(&mut self, event_id: &String, event_data: &HashMap<String, String>) {
|
||||
if event_id != "7040" {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(_param1) = event_data.get("param1") {
|
||||
if _param1 == "Windows Event Log" {
|
||||
println!("Service name : {}", _param1);
|
||||
if let Some(_param2) = event_data.get("param2") {
|
||||
if _param2 == "disabled" {
|
||||
println!("Message : Event Log Service Stopped");
|
||||
println!(
|
||||
"Results : Selective event log manipulation may follow this event."
|
||||
);
|
||||
} else if _param2 == "auto start" {
|
||||
println!("Message : Event Log Service Started");
|
||||
println!(
|
||||
"Results : Selective event log manipulation may precede this event."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,133 +3,22 @@ extern crate csv;
|
||||
extern crate regex;
|
||||
|
||||
use crate::detections::configs;
|
||||
use flate2::read::GzDecoder;
|
||||
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::str;
|
||||
use std::string::String;
|
||||
|
||||
pub fn check_command(
|
||||
event_id: usize,
|
||||
commandline: &str,
|
||||
minlength: usize,
|
||||
servicecmd: usize,
|
||||
servicename: &str,
|
||||
creator: &str,
|
||||
) {
|
||||
let mut text = "".to_string();
|
||||
let mut base64 = "".to_string();
|
||||
|
||||
let empty = "".to_string();
|
||||
for line in configs::singleton().whitelist {
|
||||
let r_str = line.get(0).unwrap_or(&empty);
|
||||
if r_str.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let r = Regex::new(r_str);
|
||||
if r.is_ok() && r.unwrap().is_match(commandline) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if commandline.len() > minlength {
|
||||
text.push_str("Long Command Line: greater than ");
|
||||
text.push_str(&minlength.to_string());
|
||||
text.push_str("bytes\n");
|
||||
}
|
||||
text.push_str(&check_obfu(commandline));
|
||||
text.push_str(&check_regex(commandline, 0));
|
||||
text.push_str(&check_creator(commandline, creator));
|
||||
if Regex::new(r"\-enc.*[A-Za-z0-9/+=]{100}")
|
||||
.unwrap()
|
||||
.is_match(commandline)
|
||||
{
|
||||
let re = Regex::new(r"^.* \-Enc(odedCommand)? ").unwrap();
|
||||
base64.push_str(&re.replace_all(commandline, ""));
|
||||
} else if Regex::new(r":FromBase64String\(")
|
||||
.unwrap()
|
||||
.is_match(commandline)
|
||||
{
|
||||
let re = Regex::new(r"^.*:FromBase64String\('*").unwrap();
|
||||
base64.push_str(&re.replace_all(commandline, ""));
|
||||
let re = Regex::new(r"'.*$").unwrap();
|
||||
base64.push_str(&re.replace_all(&base64.to_string(), ""));
|
||||
}
|
||||
if let Ok(decoded) = base64::decode(&base64) {
|
||||
if !base64.is_empty() {
|
||||
if Regex::new(r"Compression.GzipStream.*Decompress")
|
||||
.unwrap()
|
||||
.is_match(commandline)
|
||||
{
|
||||
let mut d = GzDecoder::new(decoded.as_slice());
|
||||
let mut uncompressed = String::new();
|
||||
d.read_to_string(&mut uncompressed).unwrap();
|
||||
println!("Decoded : {}", uncompressed);
|
||||
text.push_str("Base64-encoded and compressed function\n");
|
||||
} else {
|
||||
println!("Decoded : {}", str::from_utf8(decoded.as_slice()).unwrap());
|
||||
text.push_str("Base64-encoded function\n");
|
||||
text.push_str(&check_obfu(str::from_utf8(decoded.as_slice()).unwrap()));
|
||||
text.push_str(&check_regex(str::from_utf8(decoded.as_slice()).unwrap(), 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !text.is_empty() {
|
||||
println!("EventID : {}", event_id);
|
||||
if servicecmd != 0 {
|
||||
println!("Message : Suspicious Service Command");
|
||||
println!("Results : Service name: {}\n", servicename);
|
||||
} else {
|
||||
println!("Message : Suspicious Command Line");
|
||||
}
|
||||
println!("command : {}", commandline);
|
||||
println!("result : {}", text);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_obfu(string: &str) -> std::string::String {
|
||||
let mut obfutext = "".to_string();
|
||||
let lowercasestring = string.to_lowercase();
|
||||
let length = lowercasestring.len() as f64;
|
||||
let mut minpercent = 0.65;
|
||||
let maxbinary = 0.50;
|
||||
|
||||
let mut re = Regex::new(r"[a-z0-9/¥;:|.]").unwrap();
|
||||
let noalphastring = re.replace_all(&lowercasestring, "");
|
||||
|
||||
re = Regex::new(r"[01]").unwrap();
|
||||
let nobinarystring = re.replace_all(&lowercasestring, "");
|
||||
|
||||
if length > 0.0 {
|
||||
let mut percent = (length - noalphastring.len() as f64) / length;
|
||||
if ((length / 100.0) as f64) < minpercent {
|
||||
minpercent = length / 100.0;
|
||||
}
|
||||
|
||||
if percent < minpercent {
|
||||
obfutext.push_str("Possible command obfuscation: only ");
|
||||
let percent = (percent * 100.0) as usize;
|
||||
obfutext.push_str(&percent.to_string());
|
||||
obfutext.push_str("% alphanumeric and common symbols\n");
|
||||
}
|
||||
|
||||
percent = ((nobinarystring.len().wrapping_sub(length as usize) as f64) / length) / length;
|
||||
let binarypercent = 1.0 - percent;
|
||||
if binarypercent > maxbinary {
|
||||
obfutext.push_str("Possible command obfuscation: ");
|
||||
let binarypercent = (binarypercent * 100.0) as usize;
|
||||
obfutext.push_str(&binarypercent.to_string());
|
||||
obfutext.push_str("% zeroes and ones (possible numeric or binary encoding)\n");
|
||||
}
|
||||
}
|
||||
return obfutext;
|
||||
}
|
||||
|
||||
pub fn check_regex(string: &str, r#type: usize) -> std::string::String {
|
||||
pub fn check_regex(
|
||||
string: &str,
|
||||
r#type: usize,
|
||||
regex_list: &Vec<Vec<String>>,
|
||||
) -> std::string::String {
|
||||
let empty = "".to_string();
|
||||
let mut regextext = "".to_string();
|
||||
for line in configs::singleton().regex {
|
||||
for line in regex_list {
|
||||
let type_str = line.get(0).unwrap_or(&empty);
|
||||
if type_str != &r#type.to_string() {
|
||||
continue;
|
||||
@@ -157,22 +46,67 @@ pub fn check_regex(string: &str, r#type: usize) -> std::string::String {
|
||||
return regextext;
|
||||
}
|
||||
|
||||
fn check_creator(command: &str, creator: &str) -> std::string::String {
|
||||
let mut creatortext = "".to_string();
|
||||
if !creator.is_empty() {
|
||||
if command == "powershell" {
|
||||
if creator == "PSEXESVC" {
|
||||
creatortext.push_str("PowerShell launched via PsExec: ");
|
||||
creatortext.push_str(creator);
|
||||
creatortext.push_str("\n");
|
||||
} else if creator == "WmiPrvSE" {
|
||||
creatortext.push_str("PowerShell launched via WMI: ");
|
||||
creatortext.push_str(creator);
|
||||
creatortext.push_str("\n");
|
||||
}
|
||||
pub fn check_whitelist(target: &str, whitelist: &Vec<Vec<String>>) -> bool {
|
||||
let empty = "".to_string();
|
||||
for line in whitelist {
|
||||
let r_str = line.get(0).unwrap_or(&empty);
|
||||
if r_str.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let r = Regex::new(r_str);
|
||||
if r.is_ok() && r.unwrap().is_match(target) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return creatortext;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn read_csv(filename: &str) -> Result<Vec<Vec<String>>, String> {
|
||||
let mut f = File::open(filename).expect("file not found!!!");
|
||||
let mut contents: String = String::new();
|
||||
let mut ret = vec![];
|
||||
let read_res = f.read_to_string(&mut contents);
|
||||
if f.read_to_string(&mut contents).is_err() {
|
||||
return Result::Err(read_res.unwrap_err().to_string());
|
||||
}
|
||||
|
||||
let mut rdr = csv::Reader::from_reader(contents.as_bytes());
|
||||
rdr.records().for_each(|r| {
|
||||
if r.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let line = r.unwrap();
|
||||
let mut v = vec![];
|
||||
line.iter().for_each(|s| v.push(s.to_string()));
|
||||
ret.push(v);
|
||||
});
|
||||
|
||||
return Result::Ok(ret);
|
||||
}
|
||||
|
||||
pub fn get_event_value<'a>(key: &String, event_value: &'a Value) -> Option<&'a Value> {
|
||||
if key.len() == 0 {
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
let alias_config = configs::singleton().event_key_alias_config;
|
||||
let event_key = match 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);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -180,36 +114,21 @@ mod tests {
|
||||
use crate::detections::utils;
|
||||
#[test]
|
||||
fn test_check_regex() {
|
||||
let regextext = utils::check_regex("\\cvtres.exe", 0);
|
||||
let regexes = utils::read_csv("regexes.txt").unwrap();
|
||||
let regextext = utils::check_regex("\\cvtres.exe", 0, ®exes);
|
||||
assert!(regextext == "Resource File To COFF Object Conversion Utility cvtres.exe\n");
|
||||
|
||||
let regextext = utils::check_regex("\\hogehoge.exe", 0, ®exes);
|
||||
assert!(regextext == "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_creator() {
|
||||
let mut creatortext = utils::check_creator("powershell", "PSEXESVC");
|
||||
assert!(creatortext == "PowerShell launched via PsExec: PSEXESVC\n");
|
||||
creatortext = utils::check_creator("powershell", "WmiPrvSE");
|
||||
assert!(creatortext == "PowerShell launched via WMI: WmiPrvSE\n");
|
||||
}
|
||||
fn test_check_whitelist() {
|
||||
let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"";
|
||||
let whitelist = utils::read_csv("whitelist.txt").unwrap();
|
||||
assert!(true == utils::check_whitelist(commandline, &whitelist));
|
||||
|
||||
#[test]
|
||||
fn test_check_obfu() {
|
||||
let obfutext = utils::check_obfu("string");
|
||||
assert!(obfutext == "Possible command obfuscation: 100% zeroes and ones (possible numeric or binary encoding)\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_command() {
|
||||
utils::check_command(1, "dir", 100, 100, "dir", "dir");
|
||||
|
||||
//test return with whitelist.
|
||||
utils::check_command(
|
||||
1,
|
||||
"\"C:\\Program Files\\Google\\Update\\GoogleUpdate.exe\"",
|
||||
100,
|
||||
100,
|
||||
"dir",
|
||||
"dir",
|
||||
);
|
||||
let commandline = "\"C:\\Program Files\\Google\\Update\\GoogleUpdate2.exe\"";
|
||||
assert!(false == utils::check_whitelist(commandline, &whitelist));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod afterfact;
|
||||
pub mod detections;
|
||||
pub mod models;
|
||||
pub mod omikuji;
|
||||
pub mod yaml;
|
||||
|
||||
@@ -11,12 +11,7 @@ use yamato_event_analyzer::detections::detection;
|
||||
use yamato_event_analyzer::omikuji::Omikuji;
|
||||
|
||||
fn main() -> Result<(), DeError> {
|
||||
let filepath: String = configs::singleton()
|
||||
.args
|
||||
.value_of("filepath")
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if filepath != "" {
|
||||
if let Some(filepath) = configs::singleton().args.value_of("filepath") {
|
||||
parse_file(&filepath);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
extern crate serde;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Data {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: Option<String>,
|
||||
#[serde(rename = "$value")]
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct TimeCreated {
|
||||
#[serde(rename = "SystemTime")]
|
||||
pub system_time: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct Execution {
|
||||
#[serde(rename = "ProcessID")]
|
||||
process_id: i32,
|
||||
#[serde(rename = "ThreadID")]
|
||||
thread_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Provider {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: Option<String>,
|
||||
#[serde(rename = "Guid")]
|
||||
guid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct System {
|
||||
#[serde(rename = "Provider")]
|
||||
pub provider: Provider,
|
||||
#[serde(rename = "EventID")]
|
||||
pub event_id: String,
|
||||
#[serde(rename = "Version")]
|
||||
version: Option<String>,
|
||||
#[serde(rename = "Level")]
|
||||
level: String,
|
||||
#[serde(rename = "Task")]
|
||||
task: String,
|
||||
#[serde(rename = "Opcode")]
|
||||
opcode: Option<String>,
|
||||
#[serde(rename = "Keywords")]
|
||||
keywords: String,
|
||||
#[serde(rename = "TimeCreated")]
|
||||
pub time_created: TimeCreated,
|
||||
#[serde(rename = "EventRecordID")]
|
||||
pub event_record_id: String,
|
||||
#[serde(rename = "Correlation")]
|
||||
correlation: Option<String>,
|
||||
#[serde(rename = "Execution")]
|
||||
execution: Option<Execution>,
|
||||
#[serde(rename = "Channel")]
|
||||
pub channel: String, // Security, System, Application ...etc
|
||||
#[serde(rename = "Computer")]
|
||||
computer: String,
|
||||
#[serde(rename = "Security")]
|
||||
security: String,
|
||||
#[serde(rename = "Message")]
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct EventData {
|
||||
#[serde(rename = "Data")]
|
||||
pub data: Option<Vec<Data>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct UserData {
|
||||
#[serde(rename = "LogFileCleared")]
|
||||
pub log_file_cleared: Option<LogFileCleared>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct LogFileCleared {
|
||||
#[serde(rename = "SubjectUserSid")]
|
||||
pub subject_user_sid: Option<String>,
|
||||
#[serde(rename = "SubjectUserName")]
|
||||
pub subject_user_name: Option<String>,
|
||||
#[serde(rename = "SubjectDomainName")]
|
||||
pub subject_domain_name: Option<String>,
|
||||
#[serde(rename = "SubjectLogonId")]
|
||||
pub subject_logon_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Evtx {
|
||||
#[serde(rename = "System")]
|
||||
pub system: System,
|
||||
#[serde(rename = "EventData")]
|
||||
pub event_data: Option<EventData>,
|
||||
#[serde(rename = "UserData")]
|
||||
pub user_data: Option<UserData>,
|
||||
}
|
||||
|
||||
impl Evtx {
|
||||
//
|
||||
// 文字列データを取得する
|
||||
//
|
||||
fn get_string(v: &Data) -> String {
|
||||
let mut ret = "".to_string();
|
||||
if let Some(text) = &v.text {
|
||||
ret = text.to_string();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
//
|
||||
// EventDataをHashMapとして取得する
|
||||
//
|
||||
pub fn parse_event_data(&self) -> HashMap<String, String> {
|
||||
let mut values = HashMap::new();
|
||||
|
||||
if let Some(event_data) = &self.event_data {
|
||||
if let Some(data) = &event_data.data {
|
||||
for v in data.iter() {
|
||||
if let Some(name) = &v.name {
|
||||
values.insert(name.to_string(), Evtx::get_string(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod event;
|
||||
@@ -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