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

[OSCD Initiative] add Gmail responder #891

Merged
merged 57 commits into from
Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
f17f067
Update URLhaus_analyzer.py
garanews Oct 5, 2020
ef5fa25
added initial files for gmail responder
Oct 6, 2020
f138f1e
implemented basic responder body
Oct 9, 2020
2b758ec
implemented blocking and unblocking of messages
Oct 9, 2020
b95cbba
added trashing cap and fixed some comments
Oct 9, 2020
aa77a4a
changed authenticate function
Oct 9, 2020
a4de4de
implemented initial thehive connection
Oct 15, 2020
26bdc4b
pseudo implemented blocking mechanisms
Oct 15, 2020
8282fdc
implemented service files
Oct 16, 2020
8a2621c
implemented blockdomain (with mock block action)
Oct 16, 2020
64c364e
implemented unblock of domain (dirty and fake data)
Oct 16, 2020
682849a
implemented blocksender (fake n dirty)
Oct 16, 2020
ecd92c7
added some comments and fixes
Oct 16, 2020
4723ef1
implemented blocking/unblocking
Oct 19, 2020
2d57010
added trash message function and deleted some code
Oct 20, 2020
bc5f4e9
implemeted error message if gmail auth fails
Oct 20, 2020
729f9a3
implemeted deletemessage service
Oct 20, 2020
d1866f5
implemeted helper functions for auth and observable requests
Oct 20, 2020
4570ebe
replaced variable to fit new helper functions
Oct 20, 2020
de99000
changed the hive auth
Oct 20, 2020
e9c7c92
added comment for dynamic call part
Oct 20, 2020
d16e0b3
implemeted helper function for tag and deleted useless comments
Oct 20, 2020
f2496ce
removed custom filter field of case
Oct 20, 2020
b5edcd4
implemented servicec account info for gmail auth
Oct 20, 2020
0ad4459
credentails get valid with the first request; patched gmail_auth
Oct 28, 2020
401c46e
implemented gmail auth
Oct 29, 2020
9b2941d
implemented thehive authentication
Oct 29, 2020
1ee3390
fixed naming convention
Oct 29, 2020
cce8357
implemented service file configurationitems
Oct 29, 2020
97248f7
added gsuite domain configurationitem
Oct 29, 2020
50b2c57
added gmail domain config
Oct 29, 2020
962cdb6
fixed private key to be compliant with PEM format
Oct 29, 2020
79609a1
changed to quote because it takes single string and urlencodes
Oct 29, 2020
f67f108
changed function descriptoin
Oct 29, 2020
a73bae0
added functionality for custom gsuite domains
Oct 29, 2020
4949512
fixed case observable creation
Nov 2, 2020
8407623
implemented two dimensional tags for saving the filters
Nov 2, 2020
99c0b63
removed some unessassary function overload
Nov 2, 2020
3d819c8
implemented bulk delete via query
Nov 2, 2020
58688d3
fixed general tag
Nov 2, 2020
7b7c32b
added docker file
Nov 4, 2020
eef66ba
added urllib to requirements.txt
Nov 4, 2020
127f29c
deleted testing file
Nov 4, 2020
a546548
untracked venv
Nov 4, 2020
1dc39fc
handle error if no messages found
Nov 4, 2020
be935fb
fixed git case-sensitivity issue
Nov 6, 2020
f79ce83
remove useless service configuration file
Nov 6, 2020
6b4ee03
implemented readme for responder explanation
Nov 6, 2020
a85366f
bumped version number and fixed service descriptoin
Nov 6, 2020
fc84e08
newline at the end!
Nov 6, 2020
5ec1563
extended author to OSCD twitter handle
Nov 7, 2020
6a56338
changed to delete API call acccording to discussion in pull request #891
Nov 12, 2020
baca6bb
changed requirements to urllib3
Feb 13, 2021
0c05251
pip freeze with compatible version requirements
strassi Feb 26, 2021
878e132
implemented thehive permission check
strassi Mar 4, 2021
605dc7f
fail in hive auth if permissions are wrong
strassi Mar 4, 2021
7f68242
added api key permission contrains to readme.md
strassi Mar 4, 2021
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
3 changes: 2 additions & 1 deletion analyzers/URLhaus/URLhaus_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def summary(self, raw):
namespace = "URLhaus"

if raw['query_status'] == 'no_results' \
or raw['query_status'] == 'ok' and raw['md5_hash'] == None and raw['sha256_hash'] == None:
or (raw['query_status'] == 'ok' and not raw.get('md5_hash', None) \
and not raw.get('sha256_hash', None)):
taxonomies.append(self.build_taxonomy(
'info',
namespace,
Expand Down
6 changes: 6 additions & 0 deletions responders/Gmail/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3

WORKDIR /worker
COPY . Gmail
RUN pip install --no-cache-dir -r Gmail/requirements.txt
ENTRYPOINT Gmail/Gmail.py
243 changes: 243 additions & 0 deletions responders/Gmail/Gmail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
#!/usr/bin/env python3
# encoding: utf-8

from cortexutils.responder import Responder
from google.oauth2 import service_account
from googleapiclient.discovery import build
import json
from thehive4py.api import TheHiveApi
from thehive4py.api import TheHiveException
from thehive4py.models import CaseObservable
from thehive4py.query import *
from urllib.parse import quote
from google.auth.exceptions import GoogleAuthError
from googleapiclient.errors import HttpError

class Gmail(Responder):
def __init__(self):
Responder.__init__(self)
self.service = self.get_param("config.service", None, "Service service missing")
self.__scopes = [
"https://mail.google.com/",
"https://www.googleapis.com/auth/gmail.settings.basic",
]
self.__thehive_url = self.get_param("config.thehive_url", None, "The Hive URL missing")
self.__thehive_api_key = self.get_param("config.thehive_api_key", None, "The Hive API key missing")
self.__gmail_domain = self.get_param("config.gmail_domain", None, "The Hive API key missing")
self.__gmail_service_account = {
"type": "service_account",
"project_id": self.get_param("config.gmail_project_id", None, "Project ID missing"),
"private_key_id": self.get_param("config.gmail_private_key_id", None, "Private Key ID missing"),
"private_key": self.get_param("config.gmail_private_key", None, "Private Key (PEM format) missing").replace("\\n", "\n"),
"client_email": self.get_param("config.gmail_client_email", None, "Client email missing"),
"client_id": self.get_param("config.gmail_client_id", None, "Client id missing"),
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/{}".format(
quote(self.get_param("config.gmail_client_email", None, "Client email missing"))
)
}

self.__gmail_service = None
self.__hive_service = None

def __not_found(self):
self.error("service named {} not found.".format(self.service))

def __get_gmail_subjects(self, caseId, query):
"""
Get all email addresses of a case for the correspondig gsuite domain

Returns: Array of Observable objects on success
"""
response = self.__hive_service.get_case_observables(caseId, query=query)
if response.status_code == 200:
return response.json()
else:
self.error("Failed to get valid response for query: {}".format(response.status_code, response.text))

def __get_filter_tag(self, tags):
"""
Get the correct tag for a dataType in a list of tags

Returns: tag string on success else None
"""
for tag in tags:
if "gmail_filter:{}".format(self.get_param("data.data")) in tag:
return tag
self.error("No valid filter tag found on observable. Tags: {}".format(tags))

def hive_check_permissions(self, user):
"""
Checks if the `user` does provide read,write permissions to the hive

Returns: True if read,write permissions are available, otherwise False
"""
roles = user["roles"]
if "read" in roles and "write" in roles:
return True
else:
return False

def hive_auth(self, url, api_key):
self.__hive_service = TheHiveApi(url, api_key)
try:
self.__hive_service.health()
except TheHiveException as e:
self.error("Responder needs TheHive connection but failed: {}".format(e))

user_object = self.__hive_service.get_current_user().json()
if not self.hive_check_permissions(user_object):
self.error("API key of `{}` is missing `read` or `write` role. Users current roles: {}".format(
user_object["name"],
user_object["roles"]
))

def gmail_impersonate(self, subject):
"""Peforms OAuth2 auth for a given service account, scope and a delegated subject

Args:
subject (str): email adress of the user, whos data shall be accessed (delegation)

Returns:
google.auth.service_account.Credentials if valid otherwise None
"""
credentials = service_account.Credentials.from_service_account_info(
info=self.__gmail_service_account,
scopes=self.__scopes,
subject=subject
)

if credentials.has_scopes(self.__scopes):
return build("gmail", "v1", credentials=credentials)
else:
self.error("Gmail service account creation failed. Aborting responder")

def trash_message(self, case_id, query):
"""Moves specified message into trash. this emails can be recovered if false-positive
"""
gmail_observables = self.__get_gmail_subjects(case_id, And(Eq("dataType", "mail"), EndsWith("data", self.__gmail_domain)))
for observable in gmail_observables:
resource = self.gmail_impersonate(observable["data"])
try:
response = resource.users().messages().list(userId=observable["data"], q=query).execute()
for message in response.get("messages", []):
resource.users().messages().delete(userId=observable["data"], id=message["id"]).execute()
observable["tags"].append("gmail_delete:{}".format(message["id"]))
except GoogleAuthError as e:
self.error("Gmail oauth failed: {}".format(e))
except HttpError as e:
self.error("Gmail api failed: {}".format(e))

for observable in gmail_observables:
self.__hive_service.update_case_observables(CaseObservable(**observable), fields=["tags"])
self.report({'message': "Deleted message"})

def block_messages(self, case_id, query):
"""Automatically labels matching emails according to query argument.
gmail search syntax can be used in query. https://support.google.com/mail/answer/7190?hl=en
"""
new_filter = {
"criteria": {
"query": query
},
"action": { # based on https://developers.google.com/gmail/api/guides/labels
"addLabelIds": ["TRASH"],
"removeLabelIds": ["INBOX"]
}
}

gmail_observables = self.__get_gmail_subjects(case_id, And(Eq("dataType", "mail"), EndsWith("data", self.__gmail_domain)))
for observable in gmail_observables:
resource = self.gmail_impersonate(observable["data"])
try:
gmail_filter = resource.users().settings().filters().create(userId=observable["data"], body=new_filter).execute()
except GoogleAuthError as e:
self.error("Gmail oauth failed: {}".format(e))
except HttpError as e:
self.error("Gmail api failed: {}".format(e))
observable["tags"].append("gmail_filter:{}:{}".format(self.get_param("data.data"), gmail_filter["id"]))

for observable in gmail_observables:
self.__hive_service.update_case_observables(CaseObservable(**observable), fields=["tags"])
self.report({'message': "Added filters"})

def unblock_messages(self, case_id):
"""Delete a previous created filter by filter ID
"""
gmail_observables = self.__get_gmail_subjects(case_id, query=
And(
Eq("dataType", "mail"), And(
EndsWith("data", self.__gmail_domain)
)
)
)
for observable in gmail_observables:
tag = self.__get_filter_tag(observable["tags"]) # a tag should look like gmail_filters:domain:1235123121
resource = self.gmail_impersonate(observable["data"])
try:
print("deleteing: {}".format(tag.split(":")[-1]))
resource.users().settings().filters().delete(userId=observable["data"], id=tag.split(":")[-1]).execute()
except GoogleAuthError as e:
self.error("Gmail oauth failed: {}".format(e))
except HttpError as e:
self.error("Gmail api failed: {}".format(e))
observable["tags"].remove(tag)

for observable in gmail_observables:
self.__hive_service.update_case_observables(CaseObservable(**observable), fields=["tags"])
self.report({'message': "Removed filters"})

def deletemessage(self, observable, dataType, caseId):
if dataType != "other":
self.error("{} needs gmail query of type 'other' but {} given".format(
self.get_param("config.service"), dataType
))
self.trash_message(caseId, observable)

def unblockdomain(self, observable, dataType, caseId):
if dataType != "domain":
self.error("{} needs data of type 'domain' but {} given".format(
self.get_param("config.service"), dataType
))
self.unblock_messages(caseId)

def unblocksender(self, observable, dataType, caseId):
if dataType != "mail":
self.error("{} needs data of type 'mail' but {} given".format(
self.get_param("config.service"), dataType
))
self.unblock_messages(caseId)

def blocksender(self, observable, dataType, caseId):
if dataType != "mail":
self.error("{} needs data of type 'mail' but {} given".format(
self.get_param("config.service"), dataType
))
self.block_messages(caseId, "from: {}".format(observable))

def blockdomain(self, observable, dataType, caseId):
if dataType != "domain":
self.error("{} needs data of type 'domain' but {} given".format(
self.get_param("config.service"), dataType
))
self.block_messages(caseId, "from: {}".format(observable))

def run(self):
Responder.run(self)

self.hive_auth(self.__thehive_url, self.__thehive_api_key)

dataType = self.get_param("data.dataType")
observable = self.get_param("data.data")
caseId = self.get_param("data._parent")

action = getattr(self, self.service, self.__not_found) # call respective func or fail with default
action(observable, dataType, caseId)

def operations(self, raw):
return [self.build_operation('AddTagToArtifact', tag='gmail:handled')]

if __name__ == '__main__':
Gmail().run()
76 changes: 76 additions & 0 deletions responders/Gmail/Gmail_BlockDomain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"name": "Gmail_BlockDomain",
"version": "1.0",
"author": "David Strassegger, @oscd_initiative",
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
"license": "MIT",
"description": "Move emails from a given domain to trash",
"dataTypeList": ["thehive:case_artifact"],
"command": "Gmail/Gmail.py",
"baseConfig": "Gmail",
"config": {
"service": "blockdomain",
"max_tlp": 2,
"check_tlp": false,
"max_pap": 2,
"check_pap": true
},
"configurationItems": [
{
"name": "thehive_url",
"description": "URL for thehive instance",
"type": "string",
"multi": false,
"required": true
},
{
"name": "thehive_api_key",
"description": "API key for TheHive instance",
"type": "string",
"multi": false,
"required": true
},
{
"name": "gmail_domain",
"description": "Gsuite Domain",
"type": "string",
"multi": false,
"required": true
},
{
"name": "gmail_project_id",
"description": "GCP Project ID",
"type": "string",
"multi": false,
"required": true
},
{
"name": "gmail_private_key_id",
"description": "Service account private key id",
"type": "string",
"multi": false,
"required": true
},
{
"name": "gmail_private_key",
"description": "Service Account private key (PEM Format)",
"type": "string",
"multi": false,
"required": true
},
{
"name": "gmail_client_email",
"description": "Service Account E-Mail address",
"type": "string",
"multi": false,
"required": true
},
{
"name": "gmail_client_id",
"description": "OAuth Client ID",
"type": "string",
"multi": false,
"required": true
}
]
}
Loading