diff --git a/.travis.yml b/.travis.yml index f733e25..a5db62d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,12 @@ language: python python: - "2.7" sudo: false + +services: + - docker + install: pip install -r requirements.txt script: + - docker build . - flake8 --ignore F401 securitybot/ - PYTHONPATH=$(pwd) py.test -v tests/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a4096de --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:2.7 + +COPY . /securitybot + +ENV PYTHONPATH $PYTHONPATH:/securitybot + +RUN apt-get update +RUN apt-get install -y mysql-client +RUN pip install -r /securitybot/requirements.txt +RUN useradd -N -s '/bin/false' -e '' securitybot + +USER securitybot + +WORKDIR /securitybot + +ENTRYPOINT ["/securitybot/docker_entrypoint.sh"] diff --git a/README.md b/README.md index 440aa64..da2b00c 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,46 @@ If the following were all generated successfully, Securitybot should be up and r To test it, message the bot user it's assigned to and say `hi`. To test the process of dealing with an alert, message `test` to test the bot. +#### Environment variable +To prevent having to modify the scripts, you can set the following environment variables to configure the bot: + +|Variable|Setting| +|--------|-------| +|SLACK_API_TOKEN|Slack API Token| +|REPORTING_CHANNEL|Slack Channel ID| +|DUO_INTEGRATION_KEY|Duo Integration Key| +|DUO_SECRET_KEY|Duo Secret Key| +|DUO_ENDPOINT|Duo Endpoint| +|DB_HOST|MySQL Hostname| +|DB_USER|MySQL Username| +|DB_PASS|MySQL Password| +|DB_NAME|MySQL Database name| + +#### Docker + +Dockerfile is included to generate a Docker Image to run the bot. Entrypoint script will wait for database startup and initialize database if it does not already exist. Entrypoint takes one of two arguments: + +* bot: Starts the main bot +* frontend: starts the frontend and API server + +Run configuration will be based on the environment variables above. + +Example: + +Bot: +``` +docker build --tag securitybot +docker run -e DB_NAME=securitybot -e DB_USER=root -e DB_HOST=127.0.0.1 -e DB_PASS=password -e SLACK_API_TOKEN= -e REPORTING_CHANNEL=security-notifications -e DUO_INTEGRATION_KEY= -e DUO_SECRET_KEY= -e DUO_ENDPOINT= securitybot bot +``` + +Frontend: +``` +docker build --tag securitybot +docker run -p 8888:8888 -e DB_NAME=securitybot -e DB_USER=root -e DB_HOST=127.0.0.1 -e DB_PASS=password securitybot frontend +``` + +A docker-compose file is provided for ease of use. docker-compose up will start database, bot and frontend. Frontend and API server will be available on port 8888. Database content will be stored in persistent volume `mysql-securitybot`. Slack-related, Duo related and database password environment variables must be set for docker-compose stack to run properly. + ## Architecture Securitybot was designed to be as modular as possible. This means that it's possible to easily swap out chat systems, 2FA providers, and alerting data sources. diff --git a/config/commands.yaml b/config/commands.yaml index 5df825d..ef01d13 100644 --- a/config/commands.yaml +++ b/config/commands.yaml @@ -57,4 +57,13 @@ test: # Generates a test alert for a user success_msg: 'Sending you a testing alert. Be patient...' failure_msg: 'I was unable to generate a testing alert. Sorry. :sadpanda:' +insightlet: # Generates insightlet by name + info: Generates insightlet by name + usage: + - "'insightlet (widget name)'" + fn: insightlet + success_msg: 'Done!' + failure_msg: 'I can not generate insightlet for you. Sorry. :sadpanda:' + + ... diff --git a/config/messages.yaml b/config/messages.yaml index 09eb5f8..f86a7a7 100644 --- a/config/messages.yaml +++ b/config/messages.yaml @@ -20,17 +20,27 @@ bye: > # Message sent when alerting a user that a security action occured # Takes alert description and alert reason as formatting parameters 0 and 1 rsp alert: > - There's an alert, `{0}`, that's associated with your username. - Here's some more information on the alert: + { + "color": "danger", + "text" : "Following alert is associated with you" + } - {1} # Message sent when asking for user response on whether they're responsible action_prompt: > - Did you do this? - Respond with either "yes" or "no" followed by an explanation in one message. + { + "title": "Did you do this?", + "color": "#3AA3E3", + "fields": [ + { + "value": "Respond with either 'yes' or 'no' followed by an explanation in one message.", + "short": false + } + ] + } + #Respond with either "yes" or "no" followed by an explanation in one message. # Message sent if the user is not responsible escalated: > @@ -73,11 +83,18 @@ no_response: > # Message sent to report a user didn't do something. report: > - `{username}` reports they didn't do `{title}` (`{description}`): + {{ + "color": "danger", + "text" : "`{username}` reports `{username}` didn't do following `{title}`", + "fields": [ + {{ + "title": "Comment", + "value": "{comment}", + "short": false + }} + ] + }} - {comment} - - URL: {url} # Sent when a message is unrecognized bad_command: > @@ -101,4 +118,14 @@ ignore_time: > ignore_no_time: > You must provide a non-zero amount of time. + +insightlet: > + [ + {{ + "title": "{title}", + "title_link": "{url}", + "image_url": "{insightlet}", + "color": "#3AA3E3" + }} + ] ... diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a9d76a3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3' +services: + db: + image: mysql:5.7.22 + environment: + - MYSQL_ROOT_PASSWORD=${DB_PASS} +# volumes: +# - mysql-securitybot:/var/lib/mysql + ports: + - "3306" + bot: + build: . + image: securitybot + command: bot + environment: + - SLACK_API_TOKEN=${SLACK_API_TOKEN} + - REPORTING_CHANNEL=${REPORTING_CHANNEL} + - DUO_INTEGRATION_KEY=${DUO_INTEGRATION_KEY} + - DUO_SECRET_KEY=${DUO_SECRET_KEY} + - DUO_ENDPOINT=${DUO_ENDPOINT} + - DB_HOST=db + - DB_USER=root + - DB_PASS=${DB_PASS} + - DB_NAME=securitybot + - SLOW_RESPONSE_TIME=${SLOW_RESPONSE_TIME} + - ANALYTICS_TOKEN=${ANALYTICS_TOKEN} + frontend: + build: . + image: securitybot + command: frontend + ports: + - "8888:8888" + environment: + - DB_HOST=db + - DB_USER=root + - DB_PASS=${DB_PASS} + - DB_NAME=securitybot +volumes: + mysql-securitybot: diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh new file mode 100755 index 0000000..b622d6b --- /dev/null +++ b/docker_entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + + +end=$((SECONDS + 120)) + +# Wait max of 2 minutes for database to come up +until mysql --user $DB_USER --password=$DB_PASS --host=$DB_HOST -e"quit" || [ $SECONDS -ge $end ]; do echo "Waiting for DB"; sleep 2; done; + +# Check if database is initialized +if ! mysql --user $DB_USER --password=$DB_PASS --host=$DB_HOST -e"use $DB_NAME"; then + echo "Creating database" + mysql --user $DB_USER --password=$DB_PASS --host=$DB_HOST -e"create database $DB_NAME" + echo "Populating database" + python /securitybot/util/db_up.py +fi + + +if [ "$1" = 'bot' ]; then + echo "Starting bot" + shift + exec python /securitybot/main.py "$@" +elif [ "$1" = 'frontend' ]; then + echo "Starting frontend" + shift + exec python /securitybot/frontend/securitybot_frontend.py "$@" +fi + +exec "$@" diff --git a/frontend/securitybot_api.py b/frontend/securitybot_api.py index f484227..224e5c2 100644 --- a/frontend/securitybot_api.py +++ b/frontend/securitybot_api.py @@ -296,7 +296,7 @@ def blacklist(**kwargs): return response # Custom alert creation -def create_alert(ldap, title, description, reason): +def create_alert(ldap, title, description, reason, attachments=None): # type: (str, str, str, str) -> Dict[str, Any] ''' Creates a new alert. @@ -307,10 +307,16 @@ def create_alert(ldap, title, description, reason): reason: The reason for creating the alert Content: Empty. + :param user: + :param alert_time: + :param location: + :param target: + :param source: + :param risk: ''' response = build_response() try: - create_new_alert(title, ldap, description, reason) + create_new_alert(title, ldap, description, reason, url='N/A', key=None, attachments=attachments) except SQLEngineException: response['error'] = 'Invalid parameters' return response diff --git a/frontend/securitybot_frontend.py b/frontend/securitybot_frontend.py index 692f865..ec253e3 100644 --- a/frontend/securitybot_frontend.py +++ b/frontend/securitybot_frontend.py @@ -13,6 +13,7 @@ import tornado.ioloop import tornado.netutil import tornado.web +import json # Securitybot includes import securitybot_api as api @@ -73,18 +74,38 @@ class NewAlertHandler(tornado.web.RequestHandler): def post(self): response = api.build_response() args = {} - for name in ['title', 'ldap', 'description', 'reason']: + for name in ['title', 'ldap', 'description', 'reason', 'attachments']: args[name] = self.get_argument(name, default=None) if args[name] is None: response['error'] += 'ERROR: {} must be specified!\n'.format(name) if all(v is not None for v in args.values()): - self.write(api.create_alert(args['ldap'], - args['title'], - args['description'], - args['reason'])) + self.write(api.create_alert(args['ldap'], args['title'], args['description'], args['reason'], + args['attachments'])) else: self.write(response) +''' +prompt -> description +actions -> reason +''' +class NewJSONAlertHandler(tornado.web.RequestHandler): + def post(self): + response = api.build_response() + data = json.loads(self.request.body) + args = {} + for name in ['title', 'user', 'prompt', 'actions', 'attachments']: + args[name] = data.get(name, None) + if args[name] is None: + response['error'] += 'ERROR: {} must be specified!\n'.format(name) + if all(v is not None for v in args.values()): + self.write(api.create_alert(args['user'], args['title'], args['prompt'], args['actions'], + args['attachments'])) + else: + self.write(response) + + + + class IndexHandler(tornado.web.RequestHandler): def get(self): self.render() @@ -106,6 +127,7 @@ def __init__(self, port): (r'/api/ignored', IgnoredHandler), (r'/api/blacklist', BlacklistHandler), (r'/api/create', NewAlertHandler), + (r'/api/new', NewJSONAlertHandler), ], xsrf_cookie=True, static_path=static_path, diff --git a/frontend/static/js/main.js b/frontend/static/js/main.js index 04712c9..0511310 100644 --- a/frontend/static/js/main.js +++ b/frontend/static/js/main.js @@ -202,6 +202,8 @@ function submitCustom(form) { data["ldap"] = form.customLdap.value; data["description"] = form.customDescription.value; data["reason"] = form.customReason.value; + data["attachments"] = form.customAttachments.value; + // Check for empty title or ldap let hasError = false; @@ -218,6 +220,7 @@ function submitCustom(form) { form.customLdap.parentElement.parentElement.classList.remove("has-error"); } + if (hasError) { presentAlert("alert-danger", "Please fill in the fields highlighted in red."); } else { diff --git a/frontend/templates/index.html b/frontend/templates/index.html index 720d775..155ac87 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -343,6 +343,13 @@

Send custom alert

+ +
+ +
+ +
+
diff --git a/main.py b/main.py index 9d81a72..99c9ccd 100644 --- a/main.py +++ b/main.py @@ -4,17 +4,20 @@ from securitybot.bot import SecurityBot from securitybot.chat.slack import Slack from securitybot.tasker.sql_tasker import SQLTasker -from securitybot.auth.duo import DuoAuth +from securitybot.auth.default import DefaultAuth from securitybot.sql import init_sql +from os import getenv + import duo_client CONFIG = {} -SLACK_KEY = 'slack_api_token' -DUO_INTEGRATION = 'duo_integration_key' -DUO_SECRET = 'duo_secret_key' -DUO_ENDPOINT = 'duo_endpoint' -REPORTING_CHANNEL = 'some_slack_channel_id' -ICON_URL = 'https://dl.dropboxusercontent.com/s/t01pwfrqzbz3gzu/securitybot.png' +SLACK_KEY = getenv('SLACK_API_TOKEN', 'slack_api_token') +DUO_INTEGRATION = getenv('DUO_INTEGRATION_KEY', 'duo_integration_key') +DUO_SECRET = getenv('DUO_SECRET_KEY', 'duo_secret_key') +DUO_ENDPOINT = getenv('DUO_ENDPOINT', 'duo_endpoint') +REPORTING_CHANNEL = getenv('REPORTING_CHANNEL', 'some_slack_channel_id') +BOT_NAME = getenv('BOT_NAME', 'SecurityBot') +BOT_ICON_URL = getenv('BOT_ICON_URL', 'https://colerisetemp2.files.wordpress.com/2009/11/dock-icon-flat.png') def init(): # Setup logging @@ -28,17 +31,17 @@ def main(): init_sql() # Create components needed for Securitybot - duo_api = duo_client.Auth( - ikey=DUO_INTEGRATION, - skey=DUO_SECRET, - host=DUO_ENDPOINT - ) - duo_builder = lambda name: DuoAuth(duo_api, name) - - chat = Slack('securitybot', SLACK_KEY, ICON_URL) + # duo_api = duo_client.Auth( + # ikey=DUO_INTEGRATION, + # skey=DUO_SECRET, + # host=DUO_ENDPOINT + # ) + # duo_builder = lambda name: DuoAuth(duo_api, name) + default_builder = lambda name: DefaultAuth(name) + chat = Slack(BOT_NAME, SLACK_KEY, BOT_ICON_URL) tasker = SQLTasker() - sb = SecurityBot(chat, tasker, duo_builder, REPORTING_CHANNEL, 'config/bot.yaml') + sb = SecurityBot(chat, tasker, default_builder, REPORTING_CHANNEL, 'config/bot.yaml') sb.run() if __name__ == '__main__': diff --git a/plugins/splunk/apps/securitybot_alerts/bin/send_bot_alerts.py b/plugins/splunk/apps/securitybot_alerts/bin/send_bot_alerts.py index 943648e..af598f0 100755 --- a/plugins/splunk/apps/securitybot_alerts/bin/send_bot_alerts.py +++ b/plugins/splunk/apps/securitybot_alerts/bin/send_bot_alerts.py @@ -28,7 +28,7 @@ def create_securitybot_task(search_name, hash, username, description, reason, ur '''.format(rows, hash)) # Insert that into the database as a new alert - create_new_alert(search_name, username, description, reason, url, hash) + create_new_alert(search_name, username, description, reason, url, user=hash) class CollisionException(Exception): pass diff --git a/securitybot/analytics/__init__.py b/securitybot/analytics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/securitybot/analytics/analytics.py b/securitybot/analytics/analytics.py new file mode 100644 index 0000000..09afca9 --- /dev/null +++ b/securitybot/analytics/analytics.py @@ -0,0 +1,50 @@ +from urllib2 import Request, urlopen, quote +import json +from os import getenv + +BASE_URL = 'https://data-platform-pod0-dev.centrify.io' +TOKEN = getenv('ANALYTICS_TOKEN') +DASHBOARD_URL = BASE_URL + '/analytics/ui/#/dashboards' + +def list_insightlets(): + + url = BASE_URL + "/analytics/services/v1.0/preference/dashboards/library/widgets/summaries" + widgets = [] + req = Request(url) + req.add_header("authorization", "Bearer " + TOKEN) + resp = urlopen(req).read() + jsons = json.loads(resp) + for i in range(len(jsons)): + widgets.append(jsons[i]['parcel_name']) + return widgets + +def get_insightlet(name): + # type: () -> str + url = BASE_URL + "/report?token={token}&uri=&uri=/analytics/ui/%23/widgets?parcel_name={name}&embedded=true&type=screenshot&height=600&width=800&delay=1000" + return url.format(token=TOKEN, name=name) + #img_name = "/tmp/" + name + ".jpg" + #urllib.request.urlretrieve(url, img_name) + +def get_insightlet_id(id): + # type: () -> Tuple[str, str] + i = int(id) + widgets = list_insightlets() + name = quote(widgets[i]) + url = BASE_URL + "/report?token={token}&uri=/analytics/ui/%23/widgets?parcel_name={name}%26embedded=true&type=screenshot&height=600&width=800&delay=2500" + return (widgets[i], url.format(token=TOKEN, name=name)) + + +def list_insightlet_message(): + # type: () -> str + widgets = list_insightlets() + md = '' + for i in range(len(widgets)): + md += str(i) + ". " + widgets[i] + '\n' + return "```" + md + "```\n" + +def main(): + print list_insightlet_message() + + +if __name__ == '__main__': + main() diff --git a/securitybot/auth/default.py b/securitybot/auth/default.py new file mode 100644 index 0000000..4aee799 --- /dev/null +++ b/securitybot/auth/default.py @@ -0,0 +1,58 @@ +''' +Default Authentication. +''' +__author__ = 'Yanlin Wang' +__email__ = 'yanlin.wang@centrify.com' + +import logging +from datetime import datetime +from urllib import urlencode +from securitybot.auth.auth import Auth, AUTH_STATES, AUTH_TIME + +from typing import Any + +class DefaultAuth(Auth): + def __init__(self, username): + # type: (Any, str) -> None + ''' + Args: + duo_api (duo_client.Auth): An Auth API client from Duo. + username (str): The username of the person authorized through + this object. + ''' + self.username = username + self.txid = None # type: str + self.auth_time = datetime.min + self.state = AUTH_STATES.NONE + + def can_auth(self): + # type: () -> bool + # Use Duo preauth to look for a device with Push + # TODO: This won't work for anyone who's set to auto-allow, but + # I don't believe we have anyone like that... + logging.debug('Checking auth capabilities for {}'.format(self.username)) + + return True + + def auth(self, reason=None): + # type: (str) -> None + pushinfo = 'from=Securitybot' + if reason: + pushinfo += '&' + pushinfo += urlencode({'reason': reason}) + + self.txid = 'txid' + self.state = AUTH_STATES.AUTHORIZED + + def _recently_authed(self): + # type: () -> bool + return (datetime.now() - self.auth_time) < AUTH_TIME + + def auth_status(self): + # type: () -> int + return AUTH_STATES.AUTHORIZED + + def reset(self): + # type: () -> None + self.txid = None + self.state = AUTH_STATES.NONE diff --git a/securitybot/bot.py b/securitybot/bot.py index 4f3ed90..5714c8e 100644 --- a/securitybot/bot.py +++ b/securitybot/bot.py @@ -13,6 +13,7 @@ import shlex import yaml import string +import json import securitybot.commands as bot_commands from securitybot.blacklist.sql_blacklist import SQLBlacklist @@ -43,7 +44,7 @@ def clean_input(text): # smart quote, as happens with auto-formatting. text = (text.replace(u'\u2018', '\'') .replace(u'\u2019', '\'') - .replace(u'\u201c','"') + .replace(u'\u201c', '"') .replace(u'\u201d', '"')) # Undo autoformatting of dashes text = (text.replace(u'\u2013', '--') @@ -325,12 +326,22 @@ def alert_user(self, user, task): task (Task): A task to alert on. ''' # Format the reason to be indented - reason = '\n'.join(['>' + s for s in task.reason.split('\n')]) - - message = self.messages['alert'].format(task.description, reason) - message += '\n' - message += self.messages['action_prompt'] - self.chat.message_user(user, message) + #reason = '\n'.join(['>' + s for s in task.reason.split('\n')]) + + #message = self.messages['alert'].format(task.description, reason) + #message += '\n' + #message += self.messages['action_prompt'] + message ='' + message_attachement = self.messages['alert'] + message_attachement += ',\n' + message_attachement += task.attachments + message_attachement += ',\n' + message_attachement += self.messages['action_prompt'] + message_attachement = '[ \n' + message_attachement + '\n ]' + #logging.info(message_attachement) + mjson = json.loads(message_attachement) + + self.chat.message_user(user, message, mjson) # User creation and lookup methods @@ -346,6 +357,7 @@ def _populate_users(self): user = User(member, self.auth_builder(member['name']), self) self.users[member['id']] = user self.users_by_name[member['name']] = user + #logging.info(member['name']) logging.info('Gathered info on {} users.'.format(len(self.users))) def user_lookup(self, id): diff --git a/securitybot/chat/slack.py b/securitybot/chat/slack.py index 6d9510c..f790ecf 100644 --- a/securitybot/chat/slack.py +++ b/securitybot/chat/slack.py @@ -6,7 +6,6 @@ import logging from slackclient import SlackClient -import json from securitybot.user import User from securitybot.chat.chat import Chat, ChatException @@ -103,7 +102,8 @@ def get_messages(self): messages = [e for e in events if e['type'] == 'message'] return [m for m in messages if 'user' in m and m['channel'].startswith('D')] - def send_message(self, channel, message): + + def send_message(self, channel, message, msg_attachments=[]): # type: (Any, str) -> None ''' Sends some message to a desired channel. @@ -114,12 +114,14 @@ def send_message(self, channel, message): text=message, username=self._username, as_user=False, - icon_url=self._icon_url) + icon_url=self._icon_url, + attachments=msg_attachments) + - def message_user(self, user, message): + def message_user(self, user, message,msg_attachments=[]): # type: (User, str) -> None ''' Sends some message to a desired user, using a User object and a string message. ''' channel = self._api_call('im.open', user=user['id'])['channel']['id'] - self.send_message(channel, message) + self.send_message(channel, message, msg_attachments) diff --git a/securitybot/commands.py b/securitybot/commands.py index 099f052..4beb36f 100644 --- a/securitybot/commands.py +++ b/securitybot/commands.py @@ -7,11 +7,12 @@ if the command doesn't have success/failure messages. ''' import re - -from datetime import timedelta - +import logging +from datetime import datetime, timedelta +import json import securitybot.ignored_alerts as ignored_alerts from securitybot.util import create_new_alert +from securitybot.analytics.analytics import list_insightlet_message, get_insightlet_id, DASHBOARD_URL def hi(bot, user, args): '''Says hello to a user.''' @@ -94,6 +95,24 @@ def ignore(bot, user, args): def test(bot, user, args): '''Creates a new test alert in Maniphest for a user.''' - create_new_alert('testing_alert', user['name'], 'Testing alert', 'Testing Securitybot') + create_new_alert('testing_alert', user['name'], 'Testing alert', 'Testing Securitybot', user=user['name'], + alert_time=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), location='Santa Clara', + target='127.0.0.1', source='127.0.0.1', risk='High') return True + +def insightlet(bot, user, args): + if len(args)<1: + # list the insights + msg = list_insightlet_message() + msg += "Please find the insightlet id you want to run\n" + msg += "```insightlet id```" + bot.chat.message_user(user, msg) + return True + if len(args) == 1: + name, insightlet_url = get_insightlet_id(args[0]) + logging.info ("insight_url = " + insightlet_url) + msg_att = bot.messages['insightlet'].format(title=name, url=DASHBOARD_URL, insightlet=insightlet_url) + msg_att_json = json.loads(msg_att) + bot.chat.message_user(user, '', msg_att_json) + return True diff --git a/securitybot/sql.py b/securitybot/sql.py index 43ebd7d..16c4f7c 100644 --- a/securitybot/sql.py +++ b/securitybot/sql.py @@ -4,8 +4,15 @@ import MySQLdb import logging +from os import getenv from typing import Any, Sequence +DB_HOST = getenv('DB_HOST', 'localhost') +DB_USER = getenv('DB_USER', 'root') +DB_PASS = getenv('DB_PASS', '') +DB_NAME = getenv('DB_NAME', 'securitybot') + + class SQLEngine(object): # Whether the singleton has been instantiated _host = None # type: str @@ -71,6 +78,7 @@ def execute(query, params=None): SQLEngine._conn.commit() except (AttributeError, MySQLdb.OperationalError): # Recover from lost connection + logging.exception('Got exception on sql exec') logging.warn('Recovering from lost MySQL connection.') SQLEngine._create_engine(SQLEngine._host, SQLEngine._user, @@ -90,4 +98,4 @@ class SQLEngineException(Exception): def init_sql(): # type: () -> None '''Initializes SQL.''' - SQLEngine('localhost', 'root', '', 'securitybot') + SQLEngine(DB_HOST, DB_USER, DB_PASS, DB_NAME) diff --git a/securitybot/tasker/sql_tasker.py b/securitybot/tasker/sql_tasker.py index e5db55f..3af1b74 100644 --- a/securitybot/tasker/sql_tasker.py +++ b/securitybot/tasker/sql_tasker.py @@ -14,6 +14,7 @@ reason, description, url, + attachments, performed, comment, authenticated, @@ -67,14 +68,14 @@ def get_pending_tasks(self): class SQLTask(Task): def __init__(self, hsh, title, username, reason, description, url, - performed, comment, authenticated, status): + attachments, performed, comment, authenticated, status): # type: (str, str, str, str, str, str, bool, str, bool, int) -> None ''' Args: hsh (str): SHA256 primary key hash. ''' super(SQLTask, self).__init__(title, username, reason, description, url, - performed, comment, authenticated, status) + attachments, performed, comment, authenticated, status) self.hash = hsh def _set_status(self, status): diff --git a/securitybot/tasker/tasker.py b/securitybot/tasker/tasker.py index 4167489..f74d56a 100644 --- a/securitybot/tasker/tasker.py +++ b/securitybot/tasker/tasker.py @@ -50,7 +50,7 @@ def get_pending_tasks(self): class Task(object): __metaclass__ = ABCMeta - def __init__(self, title, username, reason, description, url, performed, comment, + def __init__(self, title, username, reason, description, url, attachments, performed, comment, authenticated, status): # type: (str, str, str, str, str, bool, str, bool, int) -> None ''' @@ -75,6 +75,7 @@ def __init__(self, title, username, reason, description, url, performed, comment self.reason = reason self.description = description self.url = url + self.attachments = attachments self.performed = performed self.comment = comment self.authenticated = authenticated diff --git a/securitybot/user.py b/securitybot/user.py index f7e47cf..818b064 100644 --- a/securitybot/user.py +++ b/securitybot/user.py @@ -6,6 +6,7 @@ __email__ = 'abertsch@dropbox.com' import logging +import json import pytz from datetime import datetime, timedelta import securitybot.ignored_alerts as ignored_alerts @@ -13,10 +14,12 @@ from securitybot.auth.auth import AUTH_STATES from securitybot.state_machine import StateMachine from securitybot.util import tuple_builder, get_expiration_time +from os import getenv from typing import Any, Dict, List ESCALATION_TIME = timedelta(hours=2) +SLOW_RESPONSE_TIME = timedelta(minutes=int(getenv("SLOW_RESPONSE_TIME", '1'))) BACKOFF_TIME = timedelta(hours=21) class User(object): @@ -53,6 +56,8 @@ def __init__(self, user, auth, parent): # Task auto-escalation time self._escalation_time = datetime.max.replace(tzinfo=pytz.utc) + self._start = datetime.now(tz=pytz.utc) + # Build state hierarchy states = ['need_task', 'action_performed_check', @@ -65,7 +70,8 @@ def __init__(self, user, auth, parent): { 'source': 'need_task', 'dest': 'action_performed_check', - 'condition': self._has_tasks + 'condition': self._has_tasks, + 'action': self._start_timer, }, # Finish task if user says action was performed and recently authorized { @@ -197,7 +203,8 @@ def _did_not_perform_action(self): def _slow_response_time(self): # type: () -> bool '''Returns true if the user has taken a long time to respond.''' - return datetime.now(tz=pytz.utc) > self._escalation_time + now = datetime.now(tz=pytz.utc) + return now - self._start > SLOW_RESPONSE_TIME def _allows_authorization(self): # type: () -> bool @@ -216,6 +223,9 @@ def _auth_completed(self): # State actions + def _start_timer(self): + self._start = datetime.now(tz=pytz.utc) + def _auto_escalate(self): # type: () -> None '''Marks the current task as needing verification and moves on.''' @@ -226,6 +236,7 @@ def _auto_escalate(self): self.pending_task.set_verifying() self._escalation_time = datetime.max.replace(tzinfo=pytz.utc) self.send_message('no_response') + self._act_on_not_performed() def _act_on_not_performed(self): # type: () -> None @@ -242,14 +253,25 @@ def _act_on_not_performed(self): comment = self._last_message.text else: comment = 'No comment provided.' - comment = '\n'.join('> ' + s for s in comment.split('\n')) + # comment = '\n'.join('> ' + s for s in comment.split('\n')) + msg ='' + msg_att = self.parent.messages['report'].format(username=self['name'], + title=self.pending_task.title, + comment=comment) + msg_att += ',\n' + msg_att += self.pending_task.attachments + # msg_att += '"' + self.pending_task.attachments +'"' + msg_att += ',\n' + msg_att += self.pending_task.reason + # msg_att += '"' + self.pending_task.reason + '"' + msg_att = '[ \n' + msg_att + '\n ]' + + logging.info(msg_att) + msg_att_json = json.loads(msg_att) self.parent.chat.send_message( self.parent.reporting_channel, - self.parent.messages['report'].format(username=self['name'], - title=self.pending_task.title, - description=self.pending_task.description, - comment=comment, - url=self.pending_task.url)) + msg, + msg_att_json) # Exit actions diff --git a/securitybot/util.py b/securitybot/util.py index 33ccca3..084e13b 100644 --- a/securitybot/util.py +++ b/securitybot/util.py @@ -69,27 +69,34 @@ def get_expiration_time(start, time): end = next_day + delta return end -def create_new_alert(title, ldap, description, reason, url='N/A', key=None): +def create_new_alert(title, ldap, description, reason, url='N/A', key=None, attachments=None): # type: (str, str, str, str, str, str) -> None ''' Creates a new alert in the SQL DB with an optionally random hash. + :param key: + :param alert_time: + :param location: + :param target: + :param source: + :param risk: ''' + # Generate random key if none provided if key is None: key = binascii.hexlify(os.urandom(32)) # Insert that into the database as a new alert SQLEngine.execute(''' - INSERT INTO alerts (hash, ldap, title, description, reason, url, event_time) - VALUES (UNHEX(%s), %s, %s, %s, %s, %s, NOW()) + INSERT INTO alerts (hash, ldap, title, description, reason, url, event_time, attachments) + VALUES (UNHEX(%s), %s, %s, %s, %s, %s, NOW(), %s) ''', - (key, ldap, title, description, reason, url)) + (key, ldap, title, description, reason, url, attachments)) SQLEngine.execute(''' INSERT INTO user_responses (hash, comment, performed, authenticated) VALUES (UNHEX(%s), '', false, false) ''', - (key,)) + (key,)) SQLEngine.execute('INSERT INTO alert_status (hash, status) VALUES (UNHEX(%s), 0)', (key,)) diff --git a/util/db_up.py b/util/db_up.py index fd1fdd1..9e3fd3e 100644 --- a/util/db_up.py +++ b/util/db_up.py @@ -1,16 +1,18 @@ #!/usr/bin/env python import MySQLdb import sys +from os import getenv # DB CONFIG GOES HERE -host = 'localhost' -user = 'root' -passwd= '' +host = getenv('DB_HOST', 'localhost') +user = getenv('DB_USER', 'root') +passwd=getenv('DB_PASS', '') +dbname=getenv('DB_NAME', 'securitybot') db = MySQLdb.connect(host=host, user=user, passwd=passwd, - db='securitybot') + db=dbname) cur = db.cursor() @@ -57,6 +59,7 @@ reason TEXT NOT NULL, url VARCHAR(511) NOT NULL, event_time DATETIME NOT NULL, + attachments TEXT, PRIMARY KEY ( hash ) ) '''