Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement EchoTrail analyzer #1100

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions analyzers/EchoTrail/Dockerfile
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"]
37 changes: 37 additions & 0 deletions analyzers/EchoTrail/EchoTrail.json
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
}
]
}
5 changes: 5 additions & 0 deletions analyzers/EchoTrail/README
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.
Binary file added analyzers/EchoTrail/assets/echotrail_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions analyzers/EchoTrail/echotrail.py
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()
2 changes: 2 additions & 0 deletions analyzers/EchoTrail/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cortexutils
requests
177 changes: 177 additions & 0 deletions thehive-templates/EchoTrail_1_0/long.html
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>
6 changes: 6 additions & 0 deletions thehive-templates/EchoTrail_1_0/short.html
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>