Browse Source

Did some linting.

master
Bob 8 years ago
parent
commit
0e96d429aa
  1. 4
      .flake8
  2. 2
      README.md
  3. 122
      belay.py

4
.flake8

@ -0,0 +1,4 @@
[flake8]
exclude = .git
# 79 character limit is impractical with a 4 space indent...
ignore = E501

2
README.md

@ -1,6 +1,6 @@
# belay # belay
A simple python utility for checking up on your Slack organization. A simple python utility for checking up on your Slack organization. It's sort of like AWS Config Audit, but for a Slack team.
## Setup ## Setup

122
belay.py

@ -1,16 +1,17 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys
import os import os
import sys
import yaml import yaml
import logging import logging
import argparse import argparse
from slackclient import SlackClient from slackclient import SlackClient
program_version="1.0" program_version = "1.0"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def load_config(conf_path=None, team=None): def load_config(conf_path=None, team=None):
# Try the usual places to store an API token # Try the usual places to store an API token
# First token wins # First token wins
@ -22,14 +23,14 @@ def load_config(conf_path=None, team=None):
# 1. Specified config file # 1. Specified config file
if conf_path: if conf_path:
logger.debug("Skipping standard config locations because config path passed in on the command line.") logger.debug("Skipping standard locations because config path passed on the command line.")
# 2. ~/.config/slack/config.yml # 2. ~/.config/slack/config.yml
elif os.path.isfile(os.path.join(homedir,".config","belay","config.yml")): elif os.path.isfile(os.path.join(homedir, ".config", "belay", "config.yml")):
conf_path = os.path.join(homedir,".config","belay","config.yml") conf_path = os.path.join(homedir, ".config", "belay", "config.yml")
logger.info("Found config file in Home Directory: %s", conf_path) logger.info("Found config file in Home Directory: %s", conf_path)
# 3. ./config.yml # 3. ./config.yml
elif os.path.isfile(os.path.join(scriptdir,"config.yml")): elif os.path.isfile(os.path.join(scriptdir, "config.yml")):
conf_path = os.path.join(scriptdir,"config.yml") conf_path = os.path.join(scriptdir, "config.yml")
logger.info("Found config file in current script directory: %s", conf_path) logger.info("Found config file in current script directory: %s", conf_path)
# OK, now actually pull values from the configs # OK, now actually pull values from the configs
if conf_path: if conf_path:
@ -40,9 +41,12 @@ def load_config(conf_path=None, team=None):
if "teams" in config_data: if "teams" in config_data:
logger.debug("This is a multi-team config file. Script is looking for team: %s", team) logger.debug("This is a multi-team config file. Script is looking for team: %s", team)
if not team: if not team:
raise ValueError("No team specified with multi-team config. Please indicate which team you'd like to use.") raise ValueError("No team specified with multi-team config. " +
"Please indicate which team you'd like to use.")
elif team not in config_data["teams"]: elif team not in config_data["teams"]:
raise ValueError("Specified team '"+team+"' not present in config file. Valid choices are: "+", ".join(config_data["teams"].keys())) raise ValueError("Specified team '" + team +
"' not present in config file. Valid choices are: " +
", ".join(config_data["teams"].keys()))
else: else:
logger.debug("Team '%s' found. Selecting only data for that team.", team) logger.debug("Team '%s' found. Selecting only data for that team.", team)
config_data = config_data["teams"][team] config_data = config_data["teams"][team]
@ -62,9 +66,12 @@ def load_config(conf_path=None, team=None):
if "api_token" not in config_data: if "api_token" not in config_data:
raise RuntimeError("No API token not found.") raise RuntimeError("No API token not found.")
if "bot_token" not in config_data: if "bot_token" not in config_data:
logger.warn("No bot token provided. Assuming API token is legacy token. Use of legacy tokens is a potential security issue and we strongly recommend switching to the new token format.") logger.warn("No bot token provided. Assuming API token is legacy token. " +
"Use of legacy tokens is a potential security issue and we strongly " +
"recommend switching to the new token format.")
return config_data return config_data
def belay(config): def belay(config):
slack_api = SlackClient(config["api_token"]) slack_api = SlackClient(config["api_token"])
api_test = slack_api.api_call("auth.test") api_test = slack_api.api_call("auth.test")
@ -90,7 +97,8 @@ def belay(config):
integration_issues = check_integrations(slack_api, config) integration_issues = check_integrations(slack_api, config)
if integration_issues: if integration_issues:
logger.info("Found the following integration issues: %s", integration_issues) logger.info("Found the following integration issues: %s", integration_issues)
notify_problems(integration_issues, config, slack_bot, "Problem Integrations:", "integration_name", "date") notify_problems(integration_issues, config, slack_bot,
"Problem Integrations:", "integration_name", "date")
else: else:
logger.info("No integration issues found.") logger.info("No integration issues found.")
user_issues = check_users(slack_api, config) user_issues = check_users(slack_api, config)
@ -100,23 +108,27 @@ def belay(config):
else: else:
logger.info("No user issues found.") logger.info("No user issues found.")
def check_integrations(api_client, config): def check_integrations(api_client, config):
if "skip_integrations" in config and config["skip_integrations"]: if "skip_integrations" in config and config["skip_integrations"]:
return {} return {}
result = api_client.api_call("team.integrationLogs") result = api_client.api_call("team.integrationLogs")
logger.debug("Integration log results: %s", result) logger.debug("Integration log results: %s", result)
if not result["ok"]: if not result["ok"]:
raise RuntimeError("API Call encountered an error while getting initial integrations: "+unicode(result)) raise RuntimeError("API Call encountered an error while getting initial integrations: " +
unicode(result))
iLogs = result["logs"] iLogs = result["logs"]
if result["paging"]["pages"] > 1: if result["paging"]["pages"] > 1:
logger.info("Multiple pages of integration logs exist. Pulling remaining %s pages...", result["paging"]["pages"] - 1) logger.info("Multiple pages of integration logs exist. Pulling remaining %s pages...",
result["paging"]["pages"] - 1)
while result["paging"]["page"] < result["paging"]["pages"]: while result["paging"]["page"] < result["paging"]["pages"]:
nextPage = result["paging"]["page"] + 1 nextPage = result["paging"]["page"] + 1
logger.info("Pulling page %s.", nextPage) logger.info("Pulling page %s.", nextPage)
result = api_client.api_call("team.integrationLogs", page=nextPage) result = api_client.api_call("team.integrationLogs", page=nextPage)
logger.debug("Page %s: %s", nextPage, result) logger.debug("Page %s: %s", nextPage, result)
if not result["ok"]: if not result["ok"]:
raise RuntimeError("API Call encountered an error while getting additional integrations: "+unicode(result)) raise RuntimeError("API Call encountered an error while getting more integrations: " +
unicode(result))
iLogs.extend(result["logs"]) iLogs.extend(result["logs"])
integrations = {} integrations = {}
if "integration_whitelist" in config: if "integration_whitelist" in config:
@ -150,7 +162,9 @@ def check_integrations(api_client, config):
logger.warn("Unknown integration type: %s", log) logger.warn("Unknown integration type: %s", log)
continue continue
if int_id not in integrations: if int_id not in integrations:
integrations[int_id] = {"integration_name": int_name, "app_id": int_id, "status": status, "scopes": scopes, "user_id": log["user_id"], "user_name": log["user_name"], "date": log["date"]} integrations[int_id] = {"integration_name": int_name, "app_id": int_id,
"status": status, "scopes": scopes, "user_id": log["user_id"],
"user_name": log["user_name"], "date": log["date"]}
if "reason" in log: if "reason" in log:
integrations[int_id]["reason"] = log["reason"] integrations[int_id]["reason"] = log["reason"]
if "channel" in log: if "channel" in log:
@ -197,21 +211,26 @@ def check_integrations(api_client, config):
problem_integrations.append(integration) problem_integrations.append(integration)
return problem_integrations return problem_integrations
def check_users(api_client, config): def check_users(api_client, config):
result = api_client.api_call("users.list", presence=False, limit=100) result = api_client.api_call("users.list", presence=False, limit=100)
logger.debug("User list results: %s", result) logger.debug("User list results: %s", result)
if not result["ok"]: if not result["ok"]:
raise RuntimeError("API Call encountered an error while getting initial user list: "+unicode(result)) raise RuntimeError("API Call encountered an error while getting initial user list: " +
unicode(result))
users = result["members"] users = result["members"]
while "next_cursor" in result["response_metadata"] and result["response_metadata"]["next_cursor"]: while "next_cursor" in result["response_metadata"] and result["response_metadata"]["next_cursor"]:
logger.info("Further pages of users exist. Pulling next page...") logger.info("Further pages of users exist. Pulling next page...")
result = api_client.api_call("users.list", presence=False, limit=100, cursor=result["response_metadata"]["next_cursor"]) result = api_client.api_call("users.list", presence=False, limit=100,
cursor=result["response_metadata"]["next_cursor"])
logger.debug("User list results: %s", result) logger.debug("User list results: %s", result)
if not result["ok"]: if not result["ok"]:
raise RuntimeError("API Call encountered an error while getting additional user list: "+unicode(result)) raise RuntimeError("API Call encountered an error while getting additional users: " +
unicode(result))
users.extend(result["members"]) users.extend(result["members"])
problem_users = [] problem_users = []
retained_keys = ["real_name", "id", "team_id", "name", "problems", "has_2fa", "two_factor_type", "updated", "is_owner", "is_admin"] retained_keys = ["real_name", "id", "team_id", "name", "problems",
"has_2fa", "two_factor_type", "updated", "is_owner", "is_admin"]
if "user_whitelist" in config: if "user_whitelist" in config:
user_whitelist = config["user_whitelist"] user_whitelist = config["user_whitelist"]
else: else:
@ -236,13 +255,13 @@ def check_users(api_client, config):
if user["has_2fa"] and user["two_factor_type"] == "sms" and "sms" not in issue_whitelist: if user["has_2fa"] and user["two_factor_type"] == "sms" and "sms" not in issue_whitelist:
user["problems"] = ["User is using less-secure SMS-based 2FA"] user["problems"] = ["User is using less-secure SMS-based 2FA"]
if "problems" in user: if "problems" in user:
problem_user = {k:v for k,v in user.iteritems() if k in retained_keys} problem_user = {k: v for k, v in user.iteritems() if k in retained_keys}
problem_users.append(problem_user) problem_users.append(problem_user)
return problem_users return problem_users
def notify_problems(problems, config, slack_bot, heading="Problems:",
def notify_problems(problems, config, slack_bot, heading="Problems:", item_name="name", sort_field=None): item_name="name", sort_field=None):
indent = "\t" indent = "\t"
if "output_channel" in config: if "output_channel" in config:
indent = "\t\t" indent = "\t\t"
@ -250,34 +269,45 @@ def notify_problems(problems, config, slack_bot, heading="Problems:", item_name=
problems = sorted(problems, key=lambda k: k[sort_field]) problems = sorted(problems, key=lambda k: k[sort_field])
prob_strings = [] prob_strings = []
for problem in problems: for problem in problems:
fmt_problem = problem[item_name]+": "+", ".join(problem["problems"]) fmt_problem = problem[item_name] + ": " + ", ".join(problem["problems"])
for item in sorted(problem.items()): for item in sorted(problem.items()):
if not hasattr(item[1], "strip") and (hasattr(item[1], "__getitem__") or hasattr(item[1], "__iter__")): if not hasattr(item[1], "strip") and \
(hasattr(item[1], "__getitem__") or hasattr(item[1], "__iter__")):
val = ", ".join(item[1]) val = ", ".join(item[1])
else: else:
val = unicode(item[1]) val = unicode(item[1])
fmt_problem= fmt_problem+"\n"+indent+item[0]+": "+val fmt_problem = fmt_problem + "\n" + indent + item[0] + ": " + val
prob_strings.append(fmt_problem) prob_strings.append(fmt_problem)
formatted_problems = "\n\n".join(prob_strings) formatted_problems = "\n\n".join(prob_strings)
if "output_channel" in config: if "output_channel" in config:
if len(formatted_problems) < 3000: if len(formatted_problems) < 3000:
attachment = [{ attachment = [{
"fallback": heading+"\n"+formatted_problems, "fallback": heading + "\n" + formatted_problems,
"title": heading, "title": heading,
"text": formatted_problems, "text": formatted_problems,
"color": "#ffe600" "color": "#ffe600"
}] }]
result = slack_bot.api_call("chat.postMessage", channel=config["output_channel"], attachments=attachment, as_user=True) result = slack_bot.api_call("chat.postMessage",
channel=config["output_channel"],
attachments=attachment, as_user=True)
else: else:
# For bigger files we use a snippet. This has a 1MB limit. If you have more problems... well add that to the list. # For bigger files we use a snippet. This has a 1MB limit.
result = slack_bot.api_call("files.upload", content=formatted_problems, title=heading, filetype="text") # If you have more problems... well add this to the list.
result = slack_bot.api_call("files.upload",
content=formatted_problems,
title=heading, filetype="text")
logger.info("Message too large. Uploaded as file: %s", result) logger.info("Message too large. Uploaded as file: %s", result)
if not result["ok"]: if not result["ok"]:
raise RuntimeError("API Call encountered an error while uploading file: "+unicode(result)) raise RuntimeError("API Call encountered an error while uploading file: " +
result = slack_bot.api_call("chat.postMessage", channel=config["output_channel"], text="*"+heading+"*\n\nToo many issues to post directly.\nSee <"+result["file"]["url_private"]+"|the uploaded file> for more information.", unfurl_links=True, as_user=True) unicode(result))
msg = "*" + heading + "*\n\nToo many " + "issues to post directly.\nSee <" + \
result["file"]["url_private"] + "|the uploaded file> for details."
result = slack_bot.api_call("chat.postMessage", channel=config["output_channel"],
text=msg, unfurl_links=True, as_user=True)
logger.info("Message posted. Result: %s", result) logger.info("Message posted. Result: %s", result)
if not result["ok"]: if not result["ok"]:
raise RuntimeError("API Call encountered an error while posting message: "+unicode(result)) raise RuntimeError("API Call encountered an error while posting message: " +
unicode(result))
else: else:
print heading print heading
print "" print ""
@ -285,12 +315,20 @@ def notify_problems(problems, config, slack_bot, heading="Problems:", item_name=
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(prog="Belay", description="Check the security of your Slack.") parser = argparse.ArgumentParser(prog="Belay",
parser.add_argument("-c", "--config", default=None, help="Non-standard location of a configuration file.") description="Secure your Slack.")
parser.add_argument("-f", "--file", default=None, help="File to redirect log output into.") parser.add_argument("-c", "--config", default=None,
parser.add_argument("-t", "--team", default=None, help="Team to check for multi-team configs.") help="Non-standard location of a configuration file.")
parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase log verbosity level. (Default level: WARN, use twice for DEBUG)") parser.add_argument("-f", "--file", default=None,
parser.add_argument("-V", "--version", action="version", version="%(prog)s "+program_version, help="Display version information and exit.") help="File to redirect log output into.")
parser.add_argument("-t", "--team", default=None,
help="Team to check for multi-team configs.")
parser.add_argument("-v", "--verbose", action="count", default=0,
help="Increase log verbosity level. (Default" +
" level: WARN, use twice for DEBUG)")
parser.add_argument("-V", "--version", action="version",
version="%(prog)s " + program_version,
help="Display version information and exit.")
args = parser.parse_args() args = parser.parse_args()
loglevel = max(10, 30 - (args.verbose * 10)) loglevel = max(10, 30 - (args.verbose * 10))
logformat = '%(asctime)s %(levelname)s: %(message)s' logformat = '%(asctime)s %(levelname)s: %(message)s'
@ -300,10 +338,10 @@ if __name__ == '__main__':
logging.basicConfig(level=loglevel, format=logformat) logging.basicConfig(level=loglevel, format=logformat)
logger.info("Belaying...") logger.info("Belaying...")
# try: try:
config = load_config(args.config, args.team) config = load_config(args.config, args.team)
belay(config) belay(config)
# except Exception as e: except Exception as e:
# sys.exit(e) sys.exit(e)
# finally: finally:
logging.shutdown() logging.shutdown()

Loading…
Cancel
Save