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.

python · 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()