Skip to content

Commit

Permalink
Merge pull request #1100 from joeslazaro-cdw/jl/echotrail-analyzer
Browse files Browse the repository at this point in the history
Implement EchoTrail analyzer
  • Loading branch information
jeromeleonard authored Oct 10, 2022
2 parents 67f1628 + f6417d1 commit dfe4d0b
Show file tree
Hide file tree
Showing 9 changed files with 345 additions and 0 deletions.
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>

0 comments on commit dfe4d0b

Please sign in to comment.