Skip to content

Commit

Permalink
Merge pull request #23 from misje/dev
Browse files Browse the repository at this point in the history
Implement a number of small features and improvements
  • Loading branch information
misje authored Feb 4, 2024
2 parents ec94f0b + 18132ed commit 99bc463
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 43 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ wazuh-opencti operates on
- SHA256 hashes (typically from files)
- IP addresses (IPv4/IPv6)
- Domain names (like DNS queries)
- Hostnames (like DNS queries)
- URLs (found in arguments in audited commands)

and inspects events from sysmon, syscheck, suricata and osquery. The script can
easily be extended to match other types of events as well.
Expand All @@ -26,6 +28,7 @@ The integration will only inspect events whose *rule.groups* matches
- syscheck\_file
- osquery
- osquery\_file
- audit\_command

The logic is as follows:

Expand Down Expand Up @@ -77,7 +80,7 @@ Add an entry like the following to an `<ossec_config>` block:
```xml
<integration>
<name>custom-opencti</name>
<group>sysmon_eid1_detections,sysmon_eid3_detections,sysmon_eid7_detections,sysmon_eid22_detections,syscheck_file,osquery_file,ids,sysmon_process-anomalies</group>
<group>sysmon_eid1_detections,sysmon_eid3_detections,sysmon_eid7_detections,sysmon_eid22_detections,syscheck_file,osquery_file,ids,sysmon_process-anomalies,audit_command</group>
<alert_format>json</alert_format>
<api_key>REPLACE-ME-WITH-A-VALID-TOKEN</api_key>
<hook_url>https://my.opencti.location/graphql</hook_url>
Expand Down Expand Up @@ -170,7 +173,7 @@ in your setup):
<rule id="100213" level="12">
<if_sid>100210</if_sid>
<field name="opencti.event_type">observable_with_indicator</field>
<description>OpenCTI: IoC found in threat intel: $(opencti.indicator.observable_value)</description>
<description>OpenCTI: IoC found in threat intel: $(opencti.observable_value)</description>
<options>no_full_log</options>
<group>opencti,opencti_alert,</group>
</rule>
Expand Down Expand Up @@ -234,8 +237,9 @@ integration doesn't fail and return an exit value of 1.
sysmon\_event\_24, sysmon\_eid24\_detections | win.eventdata.hashes |
sysmon\_event\_25, sysmon\_eid25\_detections | win.eventdata.hashes |
sysmon\_process-anomalies | win.eventdata.hashes |
ids | dest\_ip, src\_ip, dns.question.name, dns.question.answers |
ids | dest\_ip, destip, src\_ip, srcip, dns.question.name, dns.question.answers |
osquery, osquery\_file | osquery.columns.sha256 |
audit\_command | execve.a0, execve.a1, … |

## Customisation

Expand Down
136 changes: 96 additions & 40 deletions custom-opencti.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ def relationship_with_indicators(node):
related.append(dict(
id=relationship['node']['related']['id'],
type=relationship['node']['type'],
relationship=relationship['node']['relationship_type'],
value=relationship['node']['related']['value'],
# Create a list of the individual node objects in indicator edges:
indicator = modify_indicator(next(iter(sort_indicators(list(map(lambda x:x['node'], relationship['node']['related']['indicators']['edges'])))), None)),
Expand All @@ -232,13 +233,14 @@ def add_context(source_event, event):
# from official VirusTotal integration):
event['opencti']['source'] = {}
event['opencti']['source']['alert_id'] = source_event['id']
event['opencti']['source']['rule_id'] = source_event['rule']['id']
if 'syscheck' in source_event:
event['opencti']['source']['file'] = source_event['syscheck']['path']
event['opencti']['source']['md5'] = source_event['syscheck']['md5_after']
event['opencti']['source']['sha1'] = source_event['syscheck']['sha1_after']
event['opencti']['source']['sha256'] = source_event['syscheck']['sha256_after']
if 'data' in source_event:
for key in ['in_iface', 'src_ip', 'src_mac', 'src_port', 'dest_ip', 'dst_mac', 'dest_port', 'proto', 'app_proto']:
for key in ['in_iface', 'srcintf', 'src_ip', 'srcip', 'src_mac', 'srcmac', 'src_port', 'srcport', 'dest_ip', 'dstip', 'dst_mac', 'dstmac', 'dest_port', 'dstport', 'dstintf', 'proto', 'app_proto']:
if key in source_event['data']:
event['opencti']['source'][key] = source_event['data'][key]
if packetbeat_dns(source_event):
Expand All @@ -255,6 +257,11 @@ def add_context(source_event, event):
for key in ['queryName', 'queryResults', 'image']:
if key in source_event['data']['win']['eventdata']:
event['opencti']['source'][key] = source_event['data']['win']['eventdata'][key]
if 'audit' in source_event['data'] and 'execve' in source_event['data']['audit']:
event['opencti']['source']['execve'] = ' '.join(source_event['data']['audit']['execve'][key] for key in sorted(source_event['data']['audit']['execve'].keys()))
for key in ['success', 'key', 'uid', 'gid', 'euid', 'egid', 'exe', 'exit', 'pid']:
if key in source_event['data']['audit']:
event['opencti']['source'][key] = source_event['data']['audit'][key]

def send_event(msg, agent = None):
if not agent or agent['id'] == '000':
Expand Down Expand Up @@ -282,6 +289,10 @@ def ind_ip_pattern(string):
else:
return f"[ipv4-addr:value = '{string}']"

# Return the value of the first key argument that exists in within:
def oneof(*keys, within):
return next((within[key] for key in keys if key in within), None)

def query_opencti(alert, url, token):
# The OpenCTI graphql query is filtering on a key and a list of values. By
# default, this key is "value", unless set to "hashes.SHA256":
Expand Down Expand Up @@ -319,20 +330,20 @@ def query_opencti(alert, url, token):
if packetbeat_dns(alert):
addrs = filter_packetbeat_dns(alert['data']['dns']['answers']) if 'answers' in alert['data']['dns'] else []
filter_values = [alert['data']['dns']['question']['name']] + addrs
ind_filter = [f"[domain-name:value = '{filter_values[0]}']"] + list(map(lambda a: ind_ip_pattern(a), addrs))
ind_filter = [f"[domain-name:value = '{filter_values[0]}']", f"[hostname:value = '{filter_values[0]}']"] + list(map(lambda a: ind_ip_pattern(a), addrs))
else:
# Look up either dest or source IP, whichever is public:
filter_values = [next(filter(lambda x: ipaddress.ip_address(x).is_global, [alert['data']['dest_ip'], alert['data']['src_ip']]), None)]
filter_values = [next(filter(lambda x: x and ipaddress.ip_address(x).is_global, [oneof('dest_ip', 'dstip', within=alert['data']), oneof('src_ip', 'srcip', within=alert['data'])]), None)]
ind_filter = [ind_ip_pattern(filter_values[0])] if filter_values else None
if not filter_values:
if not all(filter_values):
sys.exit()
# Look up domain names in DNS queries (sysmon event 22), along with the
# results (if they're IPv4/IPv6 addresses (A/AAAA records)):
elif any(True for _ in filter(sysmon_event22_regex.match, groups)):
query = alert['data']['win']['eventdata']['queryName']
results = format_dns_results(alert['data']['win']['eventdata']['queryResults'])
filter_values = [query] + results
ind_filter = [f"[domain-name:value = '{filter_values[0]}']"] + list(map(lambda a: ind_ip_pattern(a), results))
ind_filter = [f"[domain-name:value = '{filter_values[0]}']", f"[hostname:value = '{filter_values[0]}']"] + list(map(lambda a: ind_ip_pattern(a), results))
# Look up sha256 hashes for files added to the system or files that have been modified:
elif 'syscheck_file' in groups and any(x in groups for x in ['syscheck_entry_added', 'syscheck_entry_modified']):
filter_key = 'hashes.SHA256'
Expand All @@ -344,6 +355,12 @@ def query_opencti(alert, url, token):
filter_key = 'hashes.SHA256'
filter_values = [alert['data']['osquery']['columns']['sha256']]
ind_filter = [f"[file:hashes.'SHA-256' = '{filter_values[0]}']"]
elif 'audit_command' in groups:
# Extract any command line arguments that looks vaguely like a URL (starts with 'http'):
filter_values = [val for val in alert['data']['audit']['execve'].values() if val.startswith('http')]
ind_filter = list(map(lambda x: f"[url:value = 'x']", filter_values))
if not filter_values:
sys.exit()
# Nothing to do:
else:
sys.exit()
Expand Down Expand Up @@ -372,6 +389,7 @@ def query_opencti(alert, url, token):
fragment Object on StixCoreObject {
id
type: entity_type
created_at
updated_at
createdBy {
Expand All @@ -394,7 +412,6 @@ def query_opencti(alert, url, token):
externalReferences {
edges {
node {
source_name
url
}
}
Expand Down Expand Up @@ -442,6 +459,32 @@ def query_opencti(alert, url, token):
globalCount
}
fragment NameRelation on StixObjectOrStixRelationshipOrCreator {
... on DomainName {
id
value
...Indicators
}
... on Hostname {
id
value
...Indicators
}
}
fragment AddrRelation on StixObjectOrStixRelationshipOrCreator {
... on IPv4Addr {
id
value
...Indicators
}
... on IPv6Addr {
id
value
...Indicators
}
}
query IoCs($obs: FilterGroup, $ind: FilterGroup) {
indicators(filters: $ind, first: 10) {
edges {
Expand All @@ -453,80 +496,93 @@ def query_opencti(alert, url, token):
...PageInfo
}
}
stixCyberObservables(filters: $obs, first: 10) {
edges {
node {
...Object
entity_type
observable_value
x_opencti_description
x_opencti_score
...Indicators
... on DomainName {
value
stixCoreRelationships(toTypes: ["IPv4-Addr", "IPv6-Addr", "Domain-Name"]) {
stixCoreRelationships(
toTypes: ["IPv4-Addr", "IPv6-Addr", "Domain-Name", "Hostname"]
) {
edges {
node {
type: toType
relationship_type
related: to {
... on IPv4Addr {
id
value
...Indicators
}
... on IPv6Addr {
id
value
...Indicators
}
... on DomainName {
id
value
...Indicators
}
...AddrRelation
...NameRelation
}
}
}
}
}
... on Hostname {
value
stixCoreRelationships(
toTypes: ["IPv4-Addr", "IPv6-Addr", "Domain-Name", "Hostname"]
) {
edges {
node {
type: toType
relationship_type
related: to {
...AddrRelation
...NameRelation
}
}
}
}
}
... on Url {
value
stixCoreRelationships(
toTypes: ["IPv4-Addr", "IPv6-Addr", "Domain-Name", "Hostname"]
) {
edges {
node {
type: toType
relationship_type
related: to {
...AddrRelation
...NameRelation
}
}
}
}
}
... on IPv4Addr {
value
stixCoreRelationships(fromTypes: ["Domain-Name"]) {
stixCoreRelationships(fromTypes: ["Domain-Name", "Hostname"]) {
edges {
node {
type: fromType
relationship_type
related: from {
... on DomainName {
id
value
...Indicators
}
...NameRelation
}
}
}
}
}
... on IPv6Addr {
value
stixCoreRelationships(fromTypes: ["Domain-Name"]) {
stixCoreRelationships(fromTypes: ["Domain-Name", "Hostname"]) {
edges {
node {
type: fromType
relationship_type
related: from {
... on DomainName {
id
value
...Indicators
}
...NameRelation
}
}
}
}
}
... on Hostname {
value
}
... on StixFile {
extensions
size
Expand Down Expand Up @@ -595,7 +651,7 @@ def query_opencti(alert, url, token):
'indicator_link': indicator_link(indicator),
'query_key': filter_key,
'query_values': ';'.join(ind_filter),
'event_type': 'indicator_pattern_match' if indicator['pattern'] == ind_filter else 'indicator_partial_pattern_match',
'event_type': 'indicator_pattern_match' if indicator['pattern'] in ind_filter else 'indicator_partial_pattern_match',
}}
add_context(alert, new_alert)
new_alerts.append(remove_empties(new_alert))
Expand Down

0 comments on commit 99bc463

Please sign in to comment.