-
Notifications
You must be signed in to change notification settings - Fork 385
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1100 from joeslazaro-cdw/jl/echotrail-analyzer
Implement EchoTrail analyzer
- Loading branch information
Showing
9 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
FROM python:3.10-alpine | ||
WORKDIR /worker | ||
COPY requirements.txt EchoTrail/ | ||
RUN pip3 install --no-cache-dir -r EchoTrail/requirements.txt | ||
COPY . EchoTrail/ | ||
ENTRYPOINT ["python3", "EchoTrail/echotrail.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "EchoTrail", | ||
"version": "1.0", | ||
"author": "Joe Lazaro", | ||
"url": "https://github.com/TheHive-Project/Cortex-Analyzers", | ||
"license": "AGPL-V3", | ||
"description": "EchoTrail Insights takes a Windows filename or hash and provides several unique pieces of analytical context including prevalence & rank scores, process ancestry, behavioral analysis, and security analysis.", | ||
"dataTypeList": [ | ||
"hash", | ||
"filename" | ||
], | ||
"command": "EchoTrail/echotrail.py", | ||
"baseConfig": "EchoTrail", | ||
"registration_required": true, | ||
"subscription_required": false, | ||
"free_subscription": true, | ||
"service_homepage": "https://www.echotrail.io/", | ||
"service_logo": { | ||
"path": "assets/echotrail_logo.png", | ||
"caption": "logo" | ||
}, | ||
"screenshots": [ | ||
{ | ||
"path": "assets/echotrail_filename_report.png", | ||
"caption": "Sample long form report on a filename from a Windows system" | ||
} | ||
], | ||
"configurationItems": [ | ||
{ | ||
"name": "key", | ||
"description": "API key", | ||
"type": "string", | ||
"multi": false, | ||
"required": true | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
EchoTrail Insights data helps security analysts understand the big picture of how processes typically behave on Windows endpoints by taking a Windows filename or hash and providing several unique pieces of analytical context including prevalence & rank scores, process ancestry, behavioral analysis, and security analysis. This enables analysts to move faster, and with more efficiency, allowing them to make more informed decisions. | ||
|
||
See https://www.echotrail.io/products/insights/ for details on the features of the report. | ||
|
||
This analyzer will accept a "hash" or "filename" observable and query the EchoTrail API for any known information about that file. A nicely formatted report is then shown in TheHive. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
#!/usr/bin/env python3 | ||
# encoding: utf-8 | ||
|
||
import hashlib | ||
from typing import Tuple, TypedDict | ||
|
||
import requests | ||
from cortexutils.analyzer import Analyzer | ||
|
||
ItemPrevalence = Tuple[str, str] # The second is a stringified float | ||
|
||
|
||
class InsightResult(TypedDict): | ||
# Note: Some of these fields may be optional and not actually present | ||
rank: int | ||
host_prev: float | ||
eps: float | ||
description: str | ||
intel: str | ||
paths: list[ItemPrevalence] | ||
parents: list[ItemPrevalence] | ||
children: list[ItemPrevalence] | ||
grandparents: list[ItemPrevalence] | ||
hashes: list[ItemPrevalence] | ||
network: list[ItemPrevalence] | ||
|
||
|
||
class EchoTrailAnalyzer(Analyzer): | ||
@staticmethod | ||
def get_file_hash( | ||
file_path: str, | ||
blocksize: int = 8192, | ||
algorithm=hashlib.sha256): | ||
file_hash = algorithm() | ||
with open(file_path, 'rb') as f: | ||
for chunk in iter(lambda: f.read(blocksize), b""): | ||
file_hash.update(chunk) | ||
return file_hash.hexdigest() | ||
|
||
def __init__(self): | ||
Analyzer.__init__(self) | ||
self.api_key = self.get_param( | ||
'config.key', None, "Missing API Key") | ||
self.api_root = "https://api.echotrail.io/v1/private" | ||
|
||
self.session = requests.Session() | ||
self.session.verify = True | ||
self.session.proxies = self.get_param('config.proxy', None) | ||
self.session.headers.update({ | ||
'Accept': 'application/json', | ||
'X-Api-key': self.api_key | ||
}) | ||
|
||
def _check_for_api_errors(self, response: requests.Response, | ||
error_prefix="", good_status_code=200): | ||
"""Check for API a failure response and exit with error if needed""" | ||
if response.status_code != good_status_code: | ||
message = None | ||
try: | ||
response_dict = response.json() | ||
if 'message' in response_dict: | ||
message = "{} {}".format( | ||
error_prefix, response_dict['message']) | ||
except requests.exceptions.JSONDecodeError: | ||
pass | ||
|
||
if message is None: | ||
message = "{} HTTP {} {}".format( | ||
error_prefix, response.status_code, response.text) | ||
self.error(message) | ||
|
||
def get_insights(self, search_term: str) -> InsightResult: | ||
url = f"{self.api_root}/insights/{search_term}" | ||
try: | ||
response = self.session.get(url) | ||
self._check_for_api_errors(response) | ||
return response.json() | ||
except requests.RequestException as e: | ||
self.error('Error while trying to get insights: ' + str(e)) | ||
|
||
def summary(self, full_report: dict): | ||
"""Build taxonomies from the report data to give an IOC count""" | ||
taxonomies = [] | ||
namespace = "EchoTrail" | ||
keys = ["rank", "host_prev", "eps"] | ||
level = "info" | ||
for k in keys: | ||
if k not in full_report: | ||
continue | ||
taxonomies.append( | ||
self.build_taxonomy(level, namespace, k, full_report[k])) | ||
return {"taxonomies": taxonomies} | ||
|
||
def run(self): | ||
data = self.get_param('data', None, 'Missing data field') | ||
if self.data_type == "hash": | ||
if len(data) != 32 and len(data) != 64: | ||
self.error( | ||
f"The input hash has an invalid length ({len(data)})." | ||
" It should be 32 (MD5) or 64 (SHA-256) characters.") | ||
|
||
result = self.get_insights(data) | ||
if len(result) == 1 and 'message' in result: | ||
result['matched'] = False | ||
else: | ||
result['matched'] = True | ||
|
||
self.report(result) | ||
|
||
|
||
if __name__ == '__main__': | ||
EchoTrailAnalyzer().run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
cortexutils | ||
requests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
<style> | ||
section.report td, | ||
section.report th { | ||
border: 1px solid rgb(141, 159, 68); | ||
padding: 8px; | ||
} | ||
|
||
section.report th, | ||
section.report td.header { | ||
background-color: rgb(209, 224, 181); | ||
font-weight: bold; | ||
font-size: larger; | ||
} | ||
|
||
section.report .report-list-container { | ||
display: flex; | ||
flex-wrap: wrap; | ||
margin-top: 0.5em; | ||
} | ||
section.report .report-list { | ||
margin-right: 0.5em; | ||
margin-bottom: 1em; | ||
} | ||
section.report tr.striped-row:nth-child(even) { | ||
background-color: #f2f2f2; | ||
} | ||
</style> | ||
<section class="report"> | ||
<!-- General error --> | ||
<div class="panel panel-danger" ng-if="!success"> | ||
<div class="panel-heading"> | ||
<strong>{{(artifact.data || artifact.attachment.name) | fang}}</strong> | ||
</div> | ||
<div class="panel-body"> | ||
<dl class="dl-horizontal" ng-if="content.errorMessage"> | ||
<dt><i class="fa fa-warning"></i> MCAP:</dt> | ||
<dd class="wrap">{{content.errorMessage}}</dd> | ||
</dl> | ||
</div> | ||
</div> | ||
|
||
<!-- Success --> | ||
<div class="panel panel-primary" ng-if="success"> | ||
<div class="panel-heading">EchoTrail Report</div> | ||
|
||
<div class="panel-body" ng-if="!(content.matched)"> | ||
<p class="lead">{{content.message}}</p> | ||
</div> | ||
|
||
<div class="panel-body" ng-if="content.matched"> | ||
<dl class="dl-horizontal"> | ||
<dt>Execution Rank</dt> | ||
<dd>{{content.rank}}</dd> | ||
<dt>Host Prevalence</dt> | ||
<dd>{{content.host_prev}}%</dd> | ||
<dt>EchoTrail Prev. Score</dt> | ||
<dd>{{content.eps}} (100 = most common, 0 = least common)</dd> | ||
<dt>Description</dt> | ||
<dd> | ||
<p>{{content.description}}</p> | ||
<p>{{content.intel}}</p> | ||
</dd> | ||
</dl> | ||
<hr /> | ||
|
||
<div class="report-list-container"> | ||
<div class="report-list" ng-if="content.paths.length > 0"> | ||
<table> | ||
<thead> | ||
<tr> | ||
<th>Path</th> | ||
<th>Prevalence</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr class="striped-row" ng-repeat="item in content.paths"> | ||
<td>{{item[0]}}</td> | ||
<td>{{item[1]}}%</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
<!-- end of paths listing --> | ||
|
||
<div class="report-list" ng-if="content.parents.length > 0"> | ||
<table> | ||
<thead> | ||
<tr> | ||
<th>Parent Processes</th> | ||
<th>Prevalence</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr class="striped-row" ng-repeat="item in content.parents"> | ||
<td>{{item[0]}}</td> | ||
<td>{{item[1]}}%</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
<!-- end of parents listing --> | ||
|
||
<div class="report-list" ng-if="content.children.length > 0"> | ||
<table> | ||
<thead> | ||
<tr> | ||
<th>Child Processes</th> | ||
<th>Prevalence</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr class="striped-row" ng-repeat="item in content.children"> | ||
<td>{{item[0]}}</td> | ||
<td>{{item[1]}}%</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
<!-- end of children listing --> | ||
|
||
<div class="report-list" ng-if="content.grandparents.length > 0"> | ||
<table> | ||
<thead> | ||
<tr> | ||
<th>Grandparent Processes</th> | ||
<th>Prevalence</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr class="striped-row" ng-repeat="item in content.grandparents"> | ||
<td>{{item[0]}}</td> | ||
<td>{{item[1]}}%</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
<!-- end of grandparents listing --> | ||
|
||
<div class="report-list" ng-if="content.hashes.length > 0"> | ||
<table> | ||
<thead> | ||
<tr> | ||
<th>Hashes</th> | ||
<th>Prevalence</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr class="striped-row" ng-repeat="item in content.hashes"> | ||
<td>{{item[0]}}</td> | ||
<td>{{item[1]}}%</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
<!-- end of hashes listing --> | ||
|
||
<div class="report-list" ng-if="content.network.length > 0"> | ||
<table> | ||
<thead> | ||
<tr> | ||
<th>Network Ports</th> | ||
<th>Prevalence</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr class="striped-row" ng-repeat="item in content.network"> | ||
<td>{{item[0]}}</td> | ||
<td>{{item[1]}}%</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</div> | ||
<!-- end of network listing --> | ||
</div> | ||
</div> | ||
</div> | ||
</section> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<span class="label" ng-repeat="t in content.taxonomies" | ||
ng-class="{'info': 'label-info', 'safe': 'label-success', | ||
'suspicious': 'label-warning', | ||
'malicious':'label-danger'}[t.level]"> | ||
{{t.namespace}}:{{t.predicate}}={{t.value}} | ||
</span> |