IAMInitADUser
Generates password, Set an AD user account with this password. Enable the account. Send mail to the user with the account information. This script is running `send-mail` command, make sure there is a matching Integration configured.
- Type
- python
- Pack
- Active_Directory_Query
Source
import uuid
import demistomock as demisto # noqa: F401
from CommonServerPython import * # noqa: F401
from pyzipper import WZ_AES, ZIP_DEFLATED, AESZipFile
DEFAULT_PWD_GENERATION_SCRIPT = "GeneratePassword"
TEXT_FILE_NAME = "AD_Password" # File name for the text file (within the zip file) to use
EMAIL_ZIP_NAME = "AD_Password" # File name to use for the zip file when attaching it to the email
def find_file_entry_id(file_name: str) -> str:
"""
Find the entry ID of a file in the context by its name.
Args:
file_name (str): The name of the file to find.
Returns:
str: The entry ID of the file.
"""
file_entries = demisto.context().get("File", [])
if isinstance(file_entries, dict): # In case of a single entry
file_entries = [file_entries]
for item in file_entries:
if item["Name"] == file_name:
return item["EntryID"]
raise DemistoException(f"Could not find file '{file_name}' in the context.")
def generate_password(
password_generation_script: str,
min_lcase: str,
max_lcase: str,
min_ucase: str,
max_ucase: str,
min_digits: str,
max_digits: str,
min_symbols: str,
max_symbols: str,
) -> str:
"""
Generate a random password using a script.
Args:
password_generation_script (str): The name of the script to use for generating the password.
min_lcase (str): The minimum number of lowercase characters to use in the password.
max_lcase (str): The maximum number of lowercase characters to use in the password.
min_ucase (str): The minimum number of uppercase characters to use in the password.
max_ucase (str): The maximum number of uppercase characters to use in the password.
min_digits (str): The minimum number of digits to use in the password.
max_digits (str): The maximum number of digits to use in the password.
min_symbols (str): The minimum number of symbols to use in the password.
max_symbols (str): The maximum number of symbols to use in the password.
Returns:
str: The generated password.
"""
# Generate a random password
if password_generation_script == DEFAULT_PWD_GENERATION_SCRIPT:
script_params = {
"min_lcase": min_lcase,
"max_lcase": max_lcase,
"min_ucase": min_ucase,
"max_ucase": max_ucase,
"min_digits": min_digits,
"max_digits": max_digits,
"min_symbols": min_symbols,
"max_symbols": max_symbols,
}
pwd_generation_script_output = demisto.executeCommand(password_generation_script, script_params)
else:
pwd_generation_script_output = demisto.executeCommand(password_generation_script, {})
if is_error(pwd_generation_script_output):
raise Exception(
f"An error occurred while trying to generate a new password for the user. "
f"Error is:\n{get_error(pwd_generation_script_output)}"
)
else:
password_output = pwd_generation_script_output[0]["Contents"]
if isinstance(password_output, dict):
return password_output["NEW_PASSWORD"]
elif isinstance(password_output, str):
return password_output
else:
raise Exception(
f"Could not parse the generated password from {password_generation_script} outputs. "
f"Please make sure the output of the script is a string, or a dictionary containing "
f"a key named NEW_PASSWORD."
)
def ad_update_user(username: str, password: str, password_generation_script: str) -> None:
"""
Update user's password in AD, and enable the user account in case it was disabled.
Args:
username (str): The username of the user to update.
password (str): The new password.
password_generation_script (str): The name of the script that generates the password (used for error handling).
Raises:
DemistoException: In case of a known potential error.
"""
ad_command_args = {"username": username, "password": password, "attribute-name": "pwdLastSet", "attribute-value": -1}
# Update user password
set_password_outputs = demisto.executeCommand("ad-set-new-password", ad_command_args)
if is_error(set_password_outputs):
error_message = get_error(set_password_outputs)
if "5003" in error_message:
raise Exception(
f"Could not set a new password for the user. "
f'Please make sure that "{password_generation_script}" script '
f"complies with your domain's password complexity policy."
)
raise Exception(error_message)
# Enable user (in case it was disabled)
enable_outputs = demisto.executeCommand("ad-enable-account", ad_command_args)
if is_error(enable_outputs):
raise Exception(f"Could not enable the user account:\n{get_error(enable_outputs)}")
update_outputs = demisto.executeCommand("ad-update-user", ad_command_args)
if is_error(update_outputs):
raise Exception(f"Could not update the user account:\n{get_error(update_outputs)}")
def send_email(
username: str,
user_email: str,
incident_id: str,
email_recipient: str,
email_subject: str,
email_body: str,
display_name: str | None = None,
error_message: str | None = None,
password: str | None = None,
zip_file_entry_id: str | None = None,
) -> list | dict:
"""
Email the user with the password (plain text or encrypted).
One of 'error_message', 'password' or 'zip_file_entry_id' must be provided.
Args:
username (str): The username of the user.
user_email (str): The email address of the user.
incident_id (str): The incident ID.
email_recipient (str): The email address of the recipient.
email_subject (str): The subject of the email.
email_body (str): The body of the email.
display_name (str | None, optional): The display name of the user.
error_message (str | None, optional): An error message to include in the email.
password (str | None, optional): The password to send in the email (plain text).
zip_file_entry_id (str | None, optional): The entry ID of the zip file to send in the email (encrypted).
Returns:
list | dict: The outputs of the send-mail command.
"""
if not any((error_message, password, zip_file_entry_id)):
raise ValueError("Either 'password' or 'zip_file_entry_id' must be provided.")
if error_message:
email_subject = '"IAM - Activate User In Active Directory" '
email_subject += f"incident '{incident_id}' " if incident_id else ""
email_subject += f"failed for user '{display_name or username}'"
email_body = (
"Hello,\n\n"
"This message was sent to inform you that an error occurred while trying "
f"to activate the user account of '{username}' in the active Directory.\n\n"
f"Error: {error_message}\n\nRegards,\nIAM Team"
)
else:
if not email_subject:
email_subject = f"[IAM] User '{display_name or username}' was successfully activated in Active Directory"
email_password = "Available in the attached zip file" if zip_file_entry_id else password
if not email_body:
email_body = "Hello,\n\nThe following account has been created in Active Directory:\n\n"
if display_name:
email_body += f"Name: {display_name}\n"
email_body += f"Username: {username}\nEmail: {user_email}\nPassword: {email_password}\n\nRegards,\nIAM Team"
else:
email_body += f"\nUsername: {username}\nEmail: {user_email}\nPassword: {email_password}"
send_email_args = {"to": email_recipient, "subject": email_subject, "body": email_body}
if zip_file_entry_id:
send_email_args |= {"attachIDs": zip_file_entry_id, "attachNames": f"{EMAIL_ZIP_NAME}.zip"}
return demisto.executeCommand("send-mail", send_email_args)
@polling_function(
name="IAMInitADUser",
interval=10,
requires_polling_arg=False,
)
def create_zip_with_password(args: dict, generated_password: str, zip_password: str) -> PollResult:
"""
Create a zip file with a password.
The function returns a zip file to the war room, and calls this script recursively using polling.
Args:
args (dict): The arguments passed to the script.
generated_password (str): The password to encrypt.
zip_password (str): The password to use for encrypting the zip file.
Returns:
PollResult: The polling result.
"""
text_file_name = f"{TEXT_FILE_NAME}.txt"
zip_file_name = f"{EMAIL_ZIP_NAME}_{uuid.uuid4()}.zip"
try:
with open(text_file_name, "w") as text_file:
text_file.write(generated_password)
demisto.debug(f"zipping {text_file_name=}")
with AESZipFile(zip_file_name, mode="w", compression=ZIP_DEFLATED, encryption=WZ_AES) as zf:
zf.pwd = bytes(zip_password, "utf-8")
zf.write(text_file_name)
with open(zip_file_name, "rb") as zip_file:
zip_content = zip_file.read()
except Exception as e:
raise DemistoException(f"Could not generate zip file. Error:\n{e!s}")
finally:
for file_name in (text_file_name, zip_file_name):
if os.path.exists(file_name):
os.remove(file_name)
return_results(fileResult(zip_file_name, zip_content))
return PollResult(
response=None,
continue_to_poll=True,
partial_result=CommandResults(readable_output=f"Encrypted zip file generated. File name: '{zip_file_name}'."),
args_for_next_run={**args, "zip_file_name": zip_file_name},
)
def main():
args = demisto.args()
password_generation_script = args.get("pwdGenerationScript", DEFAULT_PWD_GENERATION_SCRIPT)
username = args.get("sAMAccountName")
user_email = args.get("email")
display_name = args.get("displayname")
email_recipient = args.get("to_email")
incident_id = args.get("inc_id")
email_subject = args.get("email_subject")
email_body = args.get("email_body")
zip_password = args.get("ZipProtectWithPassword")
zip_file_name = args.get("zip_file_name")
generated_password: str | None = None
file_entry_id: str | None = None
context_outputs: dict[str, str] = {"success": "true"}
error_message: str | None = None
if not zip_password:
return_warning(
"It is highly recommended to run the script using the 'ZipProtectWithPassword' argument,"
"as sending a plain text password in an email is an insecure practice."
)
# If zip file is already generated and this is the second iteration, we skip this section
if not (zip_password and zip_file_name):
try:
generated_password = generate_password(
password_generation_script=password_generation_script,
min_lcase=args.get("min_lcase", "0"),
max_lcase=args.get("max_lcase", "10"),
min_ucase=args.get("min_ucase", "0"),
max_ucase=args.get("max_ucase", "10"),
min_digits=args.get("min_digits", "0"),
max_digits=args.get("max_digits", "10"),
min_symbols=args.get("min_symbols", "0"),
max_symbols=args.get("max_symbols", "10"),
)
ad_update_user(username=username, password=generated_password, password_generation_script=password_generation_script)
if zip_password:
# Rerun the script using polling (with a 'zip_file_name' value)
return_results(
create_zip_with_password(args=args, generated_password=generated_password, zip_password=zip_password)
)
return
except Exception as e:
context_outputs["success"] = "false"
error_message = str(e)
context_outputs["errorDetails"] = error_message
demisto.error(traceback.format_exc())
else:
file_entry_id = find_file_entry_id(zip_file_name)
try:
send_mail_outputs = send_email(
username=username,
user_email=user_email,
incident_id=incident_id,
email_recipient=email_recipient,
email_subject=email_subject,
email_body=email_body,
display_name=display_name,
error_message=error_message,
password=generated_password,
zip_file_entry_id=file_entry_id,
)
if is_error(send_mail_outputs):
raise Exception(f"An error occurred while trying to send mail:\n{get_error(send_mail_outputs)}")
context_outputs["sentMail"] = "true"
except Exception as e:
demisto.error(traceback.format_exc())
context_outputs["sentMail"] = "false"
context_outputs["sendMailError"] = str(e)
if context_outputs["success"] and context_outputs["sentMail"]:
readable_output = f"Successfully activated user {username}. An email with the user details was sent to {email_recipient}."
else:
readable_output = ""
if error_message:
readable_output += f"{error_message}"
if context_outputs.get("sendMailError"):
readable_output += "\n" if readable_output else "" # Add a new line separator if there is an existing error message
readable_output += context_outputs["sendMailError"]
result = CommandResults(outputs_prefix="IAM.InitADUser", outputs=context_outputs, readable_output=readable_output)
return_results(result)
if __name__ in ("__main__", "builtin", "builtins"):
main()