diff --git a/scripts/__load__.zeek b/scripts/__load__.zeek index 907b921..2e318f3 100644 --- a/scripts/__load__.zeek +++ b/scripts/__load__.zeek @@ -1,2 +1,7 @@ @load ./extract_cve -@load ./enrich +@load ./main +@load ./notice + +@ifdef (Suricata::Info) + @load ./suricata +@endif diff --git a/scripts/enrich.zeek b/scripts/enrich.zeek deleted file mode 100644 index 1199d07..0000000 --- a/scripts/enrich.zeek +++ /dev/null @@ -1,123 +0,0 @@ -@load Corelight/Suricata - -module CVEEnrichment; - -type Idx: record { - ip: addr; -}; - -type Val: record { - cve_list: string_set; - ## The ID of the known CVE on the vulnerable host. - cve: string &log &optional; - ## The hostname of the vulnerable host. - hostname: string &log &optional; - ## The unique identifier, assigned by the CVE information source, of the vulnerable host. - host_uid: string &log &optional; - ## The machine domain of the vulnerable host. - machine_domain: string &log &optional; - ## The Operating System version of the vulnerable host. - os_version: string &log &optional; - ## The source of the CVE information. - source: string &log &optional; - ## The criticality of the host. - criticality: string &log &optional; -}; - -global cve_data: table[addr] of Val = table(); - -event zeek_init() - { - Input::add_table([ $source="cve_data.tsv", $name="cve_data", $idx=Idx, - $val=Val, $destination=cve_data, $mode=Input::REREAD ]); - } - -# Enrich the Suricata_corelight log. -redef record Suricata::Info += { - orig_vulnerable_host: Val &log &optional; - resp_vulnerable_host: Val &log &optional; -}; - -event Suricata::connection_alert(c: connection, - msg: Corelight::Suricata::SuricataMsg) - { - if ( ! msg?$alert || ( ! msg$alert?$signature && ! msg$alert?$metadata ) ) - return; - - local orig = c$id$orig_h; - local resp = c$id$resp_h; - - local cve = ""; - - # Look for CVE in metadata. - if ( msg$alert?$metadata ) - { - cve = extract_cve_metadata(msg$alert$metadata); - } - - # Look for CVE in signature name, if it's not in metadata. - if ( cve == "" && msg$alert?$signature ) - { - cve = extract_cve_sig(msg$alert$signature); - } - - if ( cve == "" ) - return; - - # Some CVE's have "_" and some have "-". We normalize them to "-". - cve = subst_string(cve, "_", "-"); - cve = subst_string(cve, "cve", "CVE"); - - if ( orig in cve_data && cve in cve_data[orig]$cve_list ) - { - cve_data[orig]$cve = cve; - c$suricata_alert$orig_vulnerable_host = cve_data[orig]; - } - - if ( resp in cve_data && cve in cve_data[resp]$cve_list ) - { - cve_data[resp]$cve = cve; - c$suricata_alert$resp_vulnerable_host = cve_data[resp]; - } - } - -# Enrich the Notice log. -redef record Notice::Info += { - orig_vulnerable_host: Val &log &optional; - resp_vulnerable_host: Val &log &optional; -}; - -hook Notice::notice(n: Notice::Info) - { - if ( ! n?$msg && ! n?$note ) - return; - - if ( ! n?$src && ! n?$dst ) - return; - - local cve: string; - - # Look for CVE in msg. - if ( n?$msg ) - cve = extract_cve_sig(n$msg); - # Look for CVE in note if not in msg. - if ( cve == "" && n?$note ) - cve = extract_cve_sig(fmt("%s", n$note)); - - # Some CVE's have "_" and some have "-". We normalize them to "-". - cve = subst_string(cve, "_", "-"); - - if ( cve == "" ) - return; - - if ( n?$src && n$src in cve_data && cve in cve_data[n$src]$cve_list ) - { - cve_data[n$src]$cve = cve; - n$orig_vulnerable_host = cve_data[n$src]; - } - if ( n?$dst && n$dst in cve_data && cve in cve_data[n$dst]$cve_list ) - { - cve_data[n$dst]$cve = cve; - n$resp_vulnerable_host = cve_data[n$dst]; - } - } diff --git a/scripts/extract_cve.zeek b/scripts/extract_cve.zeek index ed4ffa1..34486e3 100644 --- a/scripts/extract_cve.zeek +++ b/scripts/extract_cve.zeek @@ -1,25 +1,35 @@ module CVEEnrichment; -export { - global extract_cve_sig: function(s: string): string; - global extract_cve_metadata: function(s: vector of string): string; -} +# Shared CVE pattern: matches CVE-YYYY-NN... (2 or more digits in last part) +# It could be just the numbers, for example: 2003-0605. However, the reference +# section of a rule would proceed the CVE name with "cve,". +# The last section of the numbers could be 3 or more digits. +# It could include "cve" or "CVE" as a prefix. +# Each section could be separated by "_", "-", or ",". For example, +# the reference section could have "cve,2003-0605". +# The metadata section could have "cve CVE-2003-0605". +# There's no standard "required" format for how CVE numbers are added to rules. +const cve_pattern: pattern = /(?i:CVE)[-_,][0-9]{4}[-_,][0-9]{2,}/; -function extract_cve_sig(s: string): string - { - # The string can contain "CVE" or "cve" and "-" or "_". - if ( /(?i:CVE)(-|_)/ !in s ) - return ""; - return find_last(s, /(?i:CVE)(-|_)[0-9]+(-|_)[0-9]+/); - } +# Extract all CVEs from a string +function extract_all_cves(s: string): vector of string + { + local cves: vector of string = vector(); + if ( /(?i:CVE)/ !in s ) + return cves; -# works with zeek 5.1.0 and later -function extract_cve_metadata(s: vector of string): string - { - for ( _, val in s ) - { - if ( /cve/ in val ) - return lstrip(val, "cve:"); - } - return ""; - } + cves = find_all_ordered(s, cve_pattern); + return cves; + } + +# Extract all CVEs from a vector of strings +function extract_all_cves_from_vector(v: vector of string): vector of string + { + local all_cves: vector of string = vector(); + for ( _, val in v ) + { + if ( /(?i:CVE)/ in val ) + all_cves += find_all_ordered(val, cve_pattern); + } + return all_cves; + } diff --git a/scripts/main.zeek b/scripts/main.zeek new file mode 100644 index 0000000..959e8e3 --- /dev/null +++ b/scripts/main.zeek @@ -0,0 +1,31 @@ +module CVEEnrichment; + +type Idx: record { + ip: addr; +}; + +type Val: record { + cve_list: string_set; + ## The ID of the known CVE on the vulnerable host. + cve: set[string] &log &optional; + ## The hostname of the vulnerable host. + hostname: string &log &optional; + ## The unique identifier, assigned by the CVE information source, of the vulnerable host. + host_uid: string &log &optional; + ## The machine domain of the vulnerable host. + machine_domain: string &log &optional; + ## The Operating System version of the vulnerable host. + os_version: string &log &optional; + ## The source of the CVE information. + source: string &log &optional; + ## The criticality of the host. + criticality: string &log &optional; +}; + +global cve_data: table[addr] of Val = table(); + +event zeek_init() + { + Input::add_table([ $source="cve_data.tsv", $name="cve_data", $idx=Idx, + $val=Val, $destination=cve_data, $mode=Input::REREAD ]); + } diff --git a/scripts/notice.zeek b/scripts/notice.zeek new file mode 100644 index 0000000..b31ee66 --- /dev/null +++ b/scripts/notice.zeek @@ -0,0 +1,53 @@ +module CVEEnrichment; + +# Enrich the Notice log. +redef record Notice::Info += { + orig_vulnerable_host: Val &log &optional; + resp_vulnerable_host: Val &log &optional; +}; + +hook Notice::notice(n: Notice::Info) + { + if ( !n?$msg && !n?$note ) + return; + if ( !n?$src && !n?$dst ) + return; + + local cves: vector of string = vector(); + + # Look for CVE in msg. + if ( n?$msg ) + cves += extract_all_cves(n$msg); + + # Look for CVE in note if not in msg. + if ( n?$note ) + # convert enum to string + cves += extract_all_cves(fmt("%s", n$note)); + + # return if no CVE data found + if ( |cves| == 0 ) + return; + + for ( _, raw_cve in cves ) + { + # Some CVE's have "_" and some have "-". We normalize them to "-". + local cve = subst_string(raw_cve, "_", "-"); + cve = subst_string(cve, ",", "-"); + cve = subst_string(cve, "cve", "CVE"); + + if ( n?$src && n$src in cve_data && cve in cve_data[n$src]$cve_list ) + { + if ( ! cve_data[n$src]?$cve ) + cve_data[n$src]$cve = set(); + add cve_data[n$src]$cve[cve]; + n$orig_vulnerable_host = cve_data[n$src]; + } + if ( n?$dst && n$dst in cve_data && cve in cve_data[n$dst]$cve_list ) + { + if ( ! cve_data[n$dst]?$cve ) + cve_data[n$dst]$cve = set(); + add cve_data[n$dst]$cve[cve]; + n$resp_vulnerable_host = cve_data[n$dst]; + } + } + } diff --git a/scripts/suricata.zeek b/scripts/suricata.zeek new file mode 100644 index 0000000..a49f3d6 --- /dev/null +++ b/scripts/suricata.zeek @@ -0,0 +1,70 @@ +module CVEEnrichment; + +# Enrich the Suricata_corelight log. +redef record Suricata::Info += { + orig_vulnerable_host: Val &log &optional; + resp_vulnerable_host: Val &log &optional; +}; + +event Suricata::connection_alert(c: connection, + msg: Corelight::Suricata::SuricataMsg) + { + if ( ! msg?$alert || ( ! msg$alert?$signature && ! msg$alert?$metadata ) ) + return; + + local orig = c$id$orig_h; + local resp = c$id$resp_h; + + local cves: vector of string = vector(); + + # Look for CVE in rule, if rule is present. + if ( msg$alert?$rule ) + { + cves = extract_all_cves(msg$alert$rule); + } + else + { + # Look for CVEs in references. + if ( msg$alert?$references ) + cves += extract_all_cves_from_vector(msg$alert$references); + + # Look for CVEs in metadata and add them to the previously found CVEs. + if ( msg$alert?$metadata ) + cves += extract_all_cves_from_vector(msg$alert$metadata); + + # Look for CVEs in signature name and add them to the previously found CVEs. + if ( msg$alert?$signature ) + cves += extract_all_cves(msg$alert$signature); + } + + + # return if no CVE data found + if ( |cves| == 0 ) + return; + + for ( _, raw_cve in cves ) + { + # Some CVE's have "_" and some have "-". We normalize them to "-". + local cve = subst_string(raw_cve, "_", "-"); + cve = subst_string(cve, ",", "-"); + cve = subst_string(cve, "cve", "CVE"); + + if ( orig in cve_data && cve in cve_data[orig]$cve_list ) + { + if ( ! cve_data[orig]?$cve ) + cve_data[orig]$cve = set(); + add cve_data[orig]$cve[cve]; + c$suricata_alert$orig_vulnerable_host = cve_data[orig]; + c$suricata_alert$alert$severity = 1; + } + + if ( resp in cve_data && cve in cve_data[resp]$cve_list ) + { + if ( ! cve_data[resp]?$cve ) + cve_data[resp]$cve = set(); + add cve_data[resp]$cve[cve]; + c$suricata_alert$resp_vulnerable_host = cve_data[resp]; + c$suricata_alert$alert$severity = 1; + } + } + } diff --git a/testing/Baseline/notice-enrichment.log b/testing/Baseline/notice-enrichment.log new file mode 100644 index 0000000..4139a41 --- /dev/null +++ b/testing/Baseline/notice-enrichment.log @@ -0,0 +1 @@ +{"_path":"notice","note":"Notice::Misc","msg":"CVE-1234-56789 and cve_1234_00001 seen in message",...,"orig_vulnerable_host":{...}} diff --git a/testing/Scripts/test_notice.zeek b/testing/Scripts/test_notice.zeek new file mode 100644 index 0000000..7951e9d --- /dev/null +++ b/testing/Scripts/test_notice.zeek @@ -0,0 +1,20 @@ +@load base/frameworks/notice +@load ./../../extract_cve +@load ./../../main +@load ./../../notice + +module TestNotice; + +redef Input::read_files += { + fmt("%s/../inputs/test-cve-data.tsv", get_script_path()) +}; + +event zeek_init() &priority=5 +{ + NOTICE([ + $note = Notice::Misc, + $msg = "CVE-1234-56789 and cve_1234_00001 seen in message", + $src = 1.2.3.4, + $dst = 9.8.7.6, + ]); +} diff --git a/testing/inputs/test-cve-data.tsv b/testing/inputs/test-cve-data.tsv new file mode 100644 index 0000000..6341901 --- /dev/null +++ b/testing/inputs/test-cve-data.tsv @@ -0,0 +1,2 @@ +#fields ip hostname uid cid criticality machine_domain os_version source cve_list +1.2.3.4 test-host - - Medium - Linux test_source CVE-1234-56789,CVE-1234-00001 diff --git a/testing/tests/extract-cve.zeek b/testing/tests/extract-cve.zeek index 859da3b..2f82940 100644 --- a/testing/tests/extract-cve.zeek +++ b/testing/tests/extract-cve.zeek @@ -1,26 +1,45 @@ # @TEST-EXEC: zeek ../../../scripts/extract_cve.zeek %INPUT > out # @TEST-EXEC: btest-diff out -global cases: string_vec = vector("hello", "", "CVE-NOPE", "", - "ET EXPLOIT Zoho ManageEngine Desktop Central RCE Inbound (CVE-2020-10189)", - "CVE-2020-10189"); +@load ./../../extract_cve event zeek_init() - { - local n = 0; - while ( n < |cases| ) - { - local i = cases[n]; - local ex = cases[n + 1]; - local got = CVEEnrichment::extract_cve_sig(i); - if ( got == ex ) - { - print fmt("OK extract_cve(%s) == '%s'", i, ex); - } - else - { - print fmt("FAIL extract_cve(%s) != '%s', got '%s'", i, ex, got); - } - n += 2; - } - } + { + local cases: table[int] of string = { + [0] = "CVE-2023-1234", + [1] = "exploit CVE_2022_9876 in the wild", + [2] = "something with CVE-1999-0001 and CVE_2005_123456", + [3] = "CVE,2021-44444", + [4] = "cve CVE-2020-0002 and cve_2021_0003", + [5] = "no CVE here", + [6] = "multiple: CVE-2000-1111, CVE-2000-2222, CVE-2000-3333", + [7] = "cve-2001-01 cve_2001_0001 cve-2001_999999" + }; + + local expected: table[int] of vector of string = { + [0] = vector("CVE-2023-1234"), + [1] = vector("CVE_2022_9876"), + [2] = vector("CVE-1999-0001", "CVE_2005_123456"), + [3] = vector("CVE,2021-44444"), + [4] = vector("CVE-2020-0002", "cve_2021_0003"), + [5] = vector(), + [6] = vector("CVE-2000-1111", "CVE-2000-2222", "CVE-2000-3333"), + [7] = vector("cve-2001-01", "cve_2001_0001", "cve-2001_999999") + }; + + for ( i in cases ) + { + local input = cases[i]; + local result = extract_all_cves(input); + local want = expected[i]; + + if ( result == want ) + print fmt("PASS [%d]: %s", i, input); + else + { + print fmt("FAIL [%d]: %s", i, input); + print fmt(" Expected: %s", want); + print fmt(" Got : %s", result); + } + } + }