├── .gitignore ├── Dockerfile ├── pipeline └── slackaudit.conf ├── readme.md └── scripts ├── auditor.py ├── config └── config.json.example └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | tmp.py 3 | *.DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | *.swp 7 | # Icon must end with two \r 8 | Icon 9 | config.json 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/logstash/logstash:7.1.0 2 | LABEL authors="maus" 3 | USER root 4 | ARG PYTHON_VERSION='3.7.3' 5 | # A sane person might ask you why I'm installing python3.7 in this container 6 | # tl;dr slack's latest lib requires python3.7 for asyncio support and it's easier to 7 | # use a elasticsearch->logstash container and build python over starting from python3.7 8 | # and building a 9 | RUN yum install -y \ 10 | wget \ 11 | gcc make \ 12 | libffi-devel \ 13 | zlib-dev openssl-devel sqlite-devel bzip2-devel 14 | RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz \ 15 | && tar xvf Python-${PYTHON_VERSION}.tgz \ 16 | && cd Python-${PYTHON_VERSION} \ 17 | && ./configure --prefix=/usr/local \ 18 | && make -j 8 \ 19 | && make -j 8 altinstall \ 20 | && rm -rf /usr/share/logstash/Python* 21 | 22 | USER logstash 23 | WORKDIR /usr/share/logstash 24 | RUN rm -f ./pipeline/logstash.conf 25 | RUN echo 'http.host: "127.0.0.1"' > ./config/logstash.yml 26 | ADD pipeline/ ./pipeline/ 27 | COPY scripts/*.py ./scripts/ 28 | COPY scripts/requirements.txt ./scripts/ 29 | COPY scripts/config/config.json ./scripts//config/ 30 | RUN pip3.7 install --user -r ./scripts/requirements.txt 31 | ENV SLACK_AUDIT_PATH=/usr/share/logstash/scripts/auditor.py 32 | -------------------------------------------------------------------------------- /pipeline/slackaudit.conf: -------------------------------------------------------------------------------- 1 | input { 2 | exec { 3 | interval => 1800 4 | command => "/usr/local/bin/python3.7 ${SLACK_AUDIT_PATH} login" 5 | codec => json 6 | tags => [ "login" ] 7 | } 8 | exec { 9 | interval => 1800 10 | command => "/usr/local/bin/python3.7 ${SLACK_AUDIT_PATH} integration" 11 | codec => json 12 | tags => [ "integration" ] 13 | } 14 | } 15 | 16 | filter { 17 | if "login" in [tags] { 18 | json { source => message } 19 | date { match => ["date_first", "YYYY-MM-dd HH:mm:ss"] } 20 | } 21 | 22 | if "integration" in [tags] { 23 | json { source => message } 24 | date { match => ["date", "YYYY-MM-dd HH:mm:ss"] } 25 | } 26 | 27 | if "_jsonparsefailure" in [tags] { drop {} } 28 | 29 | mutate { 30 | remove_field => ["@version", "host", "command", "message"] 31 | } 32 | } 33 | 34 | output { 35 | if "integration" in [tags] { 36 | stdout { 37 | codec => "rubydebug" 38 | } 39 | } 40 | if "login" in [tags] { 41 | stdout { 42 | codec => "rubydebug" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Slack API Auditor 2 | 3 | Provides a quick method of collecting Slack access logs and integration logs, then forwards them via Logstash. 4 | 5 | 6 | ### How this works 7 | The auditor is a Python script being executed by Logstash on a set interval. The script will reach out to the Slack API and gather [team.accessLogs](https://api.slack.com/methods/team.accessLogs) and [team.integrationLogs](https://api.slack.com/methods/team.integrationLogs) and output them to stdout, which Logstash collects. Logstash then tags with a proper timestamp and forwards to the service of your [choosing](https://www.elastic.co/guide/en/logstash/current/output-plugins.html). 8 | 9 | ### How to Deploy 10 | 11 | #### Via Dockerfile 12 | 1. `docker build -t slacklogger:latest .` 13 | 2. `docker run slacklogger:latest .` 14 | 15 | 16 | #### Run locally / build from source 17 | 1. Clone this repo and install the required dependencies, `pip install -r scripts/requirements.txt` 18 | 19 | 2. Create OAuth Token For Slack 20 | Follow the directions here [Creating oAuth Tokens for Slack Apps](https://api.slack.com/tutorials/slack-apps-and-postman), and generate a token with the "admin" scope. 21 | 22 | 3. [Install Logstash](https://www.elastic.co/guide/en/logstash/2.4/installing-logstash.html) (We tested/built on 2.4 -> 7.1) 23 | 24 | 4. Adjust Logstash config to point to Elasticsearch / splunk as an output instead of rubydebug. Although you might want to leave it there while you test. 25 | 26 | 5. set writeable filepaths / slack token in scripts/config/config.json 27 | 28 | 6. Run Logstash. 29 | 30 | 7. Logs. 31 | 32 | 33 | ### Caveats 34 | 35 | I've only tested this on teams that are using the paid-for Slack. I don't know if these methods are available to the free api. 36 | 37 | The Slack team.accesslog and team.integraiton log methods actually limit the results to a maximum value page of 100. So with 1000 events per page you can only grab the last 100,000 events. However you could work around this by grabbing the date of the last entry on the 100th page and pass that on to the before parameter and repeat the process. Really only usefull for backfilling events or if you have a tremendously high volume of events happening on 30 minute intervals. 38 | -------------------------------------------------------------------------------- /scripts/auditor.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from dateutil.parser import * 3 | import slack 4 | import dateutil 5 | import tzlocal 6 | import os 7 | import sys 8 | import time 9 | import json 10 | 11 | class SlackAuditor(object): 12 | """ 13 | SlackAuditor: 14 | This lib provides a set of abstractions to gather access logs and integration logs for a Slack Team. 15 | You could use a logstash config such as the one we provide in this project to send these events to ELK 16 | or Splunk or whatever. You need to have the enviornment variable SLACK_TOKEN set to your api token. 17 | """ 18 | def __init__(self): 19 | """ 20 | In order to create a proper token with required scope follow the guide here: 21 | https://api.slack.com/tutorials/slack-apps-and-postman you'll need the 'admin' scope 22 | """ 23 | with open(os.getenv( 24 | 'AUDIT_CONFIG_PATH', 25 | '/usr/share/logstash/scripts/config/config.json')) as config_data: 26 | self.config = json.load(config_data) 27 | self.integration_sincedb_path = '{}/integration_sincedb'.format(self.config['sincedb_path']) 28 | self.access_sincedb_path = '{}/access_sincedb'.format(self.config['sincedb_path']) 29 | self.sc = slack.WebClient(self.config['slack_token']) 30 | self.integration_sincedb = self._check_sincedb(self.integration_sincedb_path) 31 | self.access_sincedb = self._check_sincedb(self.access_sincedb_path) 32 | 33 | def _check_sincedb(self, sincedb_path): 34 | """ 35 | SinceDB is a logstash concept but we're just putting something primitive in here. 36 | """ 37 | if os.path.isfile(sincedb_path): 38 | sincedb = float(open(sincedb_path, 'r').read()) 39 | return datetime.utcfromtimestamp(sincedb) 40 | else: 41 | self._write_sincedb(sincedb_path) 42 | sincedb = float(open(sincedb_path, 'r').read()) 43 | return datetime.utcfromtimestamp(sincedb) 44 | 45 | def _write_sincedb(self, sincedb_path): 46 | sincedb = open(sincedb_path, 'w') 47 | sincedb.write(str(time.mktime(datetime.now().timetuple()))) 48 | 49 | def _unix_to_pretty_utc(self, date): 50 | """ 51 | Convert the Unix Timestamps to UTC / Pretty Format 52 | """ 53 | 54 | utc_time = datetime.utcfromtimestamp(date) 55 | return utc_time.strftime("%Y-%m-%d %H:%M:%S") 56 | 57 | def _check_max(self, pages): 58 | """ 59 | The Slack access logs api has a max page limit of 100 pages, despite taunting you with 60 | more logs / events. 61 | """ 62 | if pages > 100: 63 | return 100 64 | return pages 65 | 66 | def get_access_logs(self): 67 | """ 68 | This function will return all slack access logs formatted in a list of hashes. 69 | """ 70 | results = [] 71 | page = 1 72 | logs = self.sc.api_call("team.accessLogs", params={'count':'1000'}) 73 | results.extend(logs['logins']) 74 | max_pages = self._check_max(logs['paging']['pages']) 75 | while page < max_pages: 76 | page += 1 77 | logs = self.sc.api_call("team.accessLogs", params={'count':'1000', 'page':page}) 78 | results.extend(logs['logins']) 79 | return results 80 | 81 | def get_integration_logs(self): 82 | """ 83 | This function will return all Slack integration logs formatted as a list of hashes. 84 | """ 85 | results = [] 86 | page = 1 87 | logs = self.sc.api_call("team.integrationLogs", params={'count':'1000'}) 88 | results.extend(logs['logs']) 89 | max_pages = self._check_max(logs['paging']['pages']) 90 | while page < max_pages: 91 | page += 1 92 | logs = self.sc.api_call("team.integrationLogs",params={'count':'1000', 'page':page}) 93 | results.extend(logs['logs']) 94 | return results 95 | 96 | def get_latest_login_events(self): 97 | """ 98 | This gets all the login events and compares the datetime to what we have already indexed. 99 | Anything thats newer than the last timestamp stored in the /tmp/sincedb_login will be sorted 100 | and returned 101 | """ 102 | logs = self.get_access_logs() 103 | results = [] 104 | for log in logs: 105 | if datetime.utcfromtimestamp(log['date_first']) > self.access_sincedb: 106 | log['date_first'] = self._unix_to_pretty_utc(log['date_first']) 107 | log['date_last'] = self._unix_to_pretty_utc(log['date_last']) 108 | results.append(log) 109 | results.sort(key=lambda item:item['date_first'], reverse=False) 110 | self._write_sincedb(self.access_sincedb_path) 111 | return results 112 | 113 | def get_latest_integration_events(self): 114 | logs = self.get_integration_logs() 115 | results = [] 116 | for log in logs: 117 | if datetime.utcfromtimestamp(float(log['date'])) > self.integration_sincedb: 118 | log['date'] = self._unix_to_pretty_utc(float(log['date'])) 119 | results.append(log) 120 | results.sort(key=lambda item:item['date'], reverse=False) 121 | self._write_sincedb(self.integration_sincedb_path) 122 | return results 123 | 124 | if __name__ == "__main__": 125 | audit = SlackAuditor() 126 | 127 | if sys.argv[1] == 'login': 128 | print(json.dumps(audit.get_latest_login_events())) 129 | 130 | if sys.argv[1] == 'integration': 131 | print(json.dumps(audit.get_latest_integration_events())) 132 | -------------------------------------------------------------------------------- /scripts/config/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "slack_token": "", 3 | "sincedb_path": "/tmp" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | datetime 3 | slackclient 4 | tzlocal --------------------------------------------------------------------------------