HashiCorp Vault

Manage Secrets and Protect Sensitive Data through HashiCorp Vault.

Authentication & Identity Management · HashiCorp Vault

Details

IDHashiCorp Vault
ProviderIBM
CategoryAuthentication & Identity Management
From Version5.0.0
Docker Imagedemisto/vendors-sdk:1.0.0.10120494
Supported ModulesAgentix XSIAM

README

Secure, store, and tightly control access to tokens, passwords, certificates, and encryption keys for protecting secrets and other sensitive data using HashiCorp Vault. This integration fetches credentials. For more information, see Managing Credentials.

This integration was integrated and tested with version 1.12.2 of HashiCorp Vault.

Authentication

The integration supports the following auth methods:

Userpass Auth Method

You are required to fill in only the Username / Role ID parameter with the username and Password / Secret ID parameter with the password. For more details, see the HashiCorp Vault documentation.

Token Auth Method

You are required to fill in only the Authentication token parameter. For more details, see the HashiCorp Vault documentation.

AppRole Auth Method

You are required to fill in only the Username / Role ID parameter with the role ID and Password / Secret ID parameter with the secret ID, and check the Use AppRole Auth Method checkbox. For more details, see the HashiCorp Vault documentation.

Configure HashiCorp Vault in Cortex

Parameter Description Required
HashiCorp server URL (e.g., https://192.168.0.1:8200) The server URL True
Use AppRole Auth Method Set as true if you are using the AppRole method for authentication. False
Username / Role ID The username for the Hashicorp vault. False
Password / Secret ID The password for the Hashicorp vault. False
Cache the authentication token enables caching of the authentication token. False
Authentication token A token for authentication for the Hashicorp vault. (Use instead of password and username.) False
Vault enterprise namespace The namespace used for the vault by the user. False
Trust any certificate (not secure) Mark as true to make unverified HTTP requests. False
Use system proxy settings Mark as true to use proxy settings. False
Fetches credentials Mark as true to fetch credentials to the Cortex XSOAR credentials vault. False
CSV list of secrets engine types to fetch secrets from Possible values are KV, Cubbyhole, AWS. False
Concat username to credential object name Should be used in case there are several secrets under the same folder in order to make the credential object unique. False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

hashicorp-list-secrets-engines


List all secrets engines that exist in HashiCorp Vault.

Base Command

hashicorp-list-secrets-engines

Input

There are no input arguments.

Context Output

Path Type Description
HashiCorp.Engine.Type string Secrets engine type.
HashiCorp.Engine.Path string Secrets engine path in HashiCorp.
HashiCorp.Engine.Description string Secrets engine description.
HashiCorp.Engine.Accessor string Secrets engine accessor.

Command example

!hashicorp-list-secrets-engines

hashicorp-list-secrets


List secrets (names) for a specified KV engine.

Base Command

hashicorp-list-secrets

Input

Argument Name Description Required
engine Engine path, e.g.,”secret/”. Use the list-secrets-engines command to retrieve the engine path. Required
version The version of the KV engine. Possible values are: 1, 2. Default is 1. Optional

Context Output

Path Type Description
HashiCorp.Secret.Path string Secret path

Command example

!hashicorp-list-secrets

hashicorp-get-secret-metadata


Returns information about a specified secret in a specified KV V2 engine.

Base Command

hashicorp-get-secret-metadata

Input

Argument Name Description Required
engine_path KV Engine path, e.g., “kv/”. Required
secret_path Secret path, e.g., “secret”. Required

Context Output

Path Type Description
HashiCorp.Secret.Created date Secret creation time.
HashiCorp.Secret.Version.Destroyed boolean Is the version destroyed.
HashiCorp.Secret.Version.Created number Version creation time.
HashiCorp.Secret.Version.Deleted date Version deletion time.
HashiCorp.Secret.Updated date Secret last updated time.
HashiCorp.Secret.Engine string Secret engine type.
HashiCorp.Secret.CurrentVersion number Secret current version.
HashiCorp.Secret.Path string Secret path.
HashiCorp.Secret.CustomMetadata unknown The custom metadata saved in the secret.

Command example

!hashicorp-get-secret-metadata engine_path=secret secret_path=test

hashicorp-delete-secret


Deletes the data under a specified secret given the secret path. Performs a soft delete that allows you to run the hashicorp-undelete-secret command if necessary (for KV V2 engine).

Base Command

hashicorp-delete-secret

Input

Argument Name Description Required
secret_path Secret path, e.g., “secret”. Required
engine_path Engine path, e.g.,”secret/”. Required
versions CSV list of secret versions to delete. Required

Context Output

There is no context output for this command.

Command example

!hashicorp-delete-secret engine_path=secret secret_path=test versions=2

hashicorp-undelete-secret


Undeletes (restores) a secret on HashiCorp (for KV V2 engine).

Base Command

hashicorp-undelete-secret

Input

Argument Name Description Required
secret_path Secret path, e.g., “secret”. Required
engine_path Engine path, e.g.,”secret/”. Required
versions CSV list of secret versions to undelete (restore). Required

Context Output

There is no context output for this command.

Command example

!hashicorp-undelete-secret engine_path=secret secret_path=test versions=2

hashicorp-destroy-secret


Permanently deletes a secret (for KV V2 engine).

Base Command

hashicorp-destroy-secret

Input

Argument Name Description Required
secret_path Secret path, .e.g., “secret”. Required
engine_path Engine path, e.g.,”secret/”. Required
versions CSV list of secret versions to permanently delete. Required

Context Output

There is no context output for this command.

Command example

!hashicorp-destroy-secret engine_path=secret secret_path=test versions=2

hashicorp-disable-engine


When a secrets engine is no longer needed, it can be disabled. All secrets under the engine are revoked and the corresponding vault data and configurations are removed.

Base Command

hashicorp-disable-engine

Input

Argument Name Description Required
path Path of the secrets engine to disable. Required

Context Output

There is no context output for this command.

Command example

!hashicorp-disable-engine path=secret

hashicorp-enable-engine

***!hashicorp-disable-engine path=secret
Enables a new secrets engine at the specified path.

Base Command

hashicorp-enable-engine

Input

Argument Name Description Required
path The path where the secrets engine will be mounted. Required
type Type of backend, e.g., “aws”. Required
description Friendly description of the mount. Optional
default_lease_ttl The default lease duration, specified as a string duration, e.g., “5s” or “30m”. Optional
max_lease_ttl The maximum lease duration, specified as a string duration, e.g., “5s” or “30m”. Optional
force_no_cache Whether to disable caching. Optional
audit_non_hmac_request_keys CSV list of keys that will not be HMAC’d by audit devices in the request data object. Optional
audit_non_hmac_response_keys CSV list of keys that will not be HMAC’d by audit devices in the response data object. Optional
listing_visibility Whether to show this mount in the UI-specific listing endpoint. Default is hidden. Possible values are: unauth, hidden. Optional
passthrough_request_headers CSV list of headers to add to allow list and pass from the request to the backend. Optional
kv_version KV version to mount. Set to “2” for mount KV V2. Possible values are: 1, 2. Optional
local Specifies if the secrets engine is a local mount only. Local mounts are not replicated, nor (if a secondary) removed by replication. Supported only in Vault Enterprise. Optional
seal_wrap Enable seal wrapping for the mount. Supported only in Vault Enterprise. Optional

Context Output

There is no context output for this command.

Command example

!hashicorp-enable-engine path=secret type=AWS

hashicorp-list-policies


Lists all configured policies.

Base Command

hashicorp-list-policies

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
HashiCorp.Policy.Name string Policy name.

Command example

hashicorp-list-policies

hashicorp-get-policy


Get information for a policy.

Base Command

hashicorp-get-policy

Input

Argument Name Description Required
name Policy name. Required

Context Output

Path Type Description
HashiCorp.Policy.Name string Policy name.
HashiCorp.Policy.Rule.Path string Policy rule path.
HashiCorp.Policy.Rule.Capabilities unknown Policy rule capabilities.

Command example

!hashicorp-get-policy name=secret

hashicorp-seal-vault


If you suspect your data has been compromised, you can seal your vault to prevent access to your secrets.

Base Command

hashicorp-seal-vault

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

Command example

!hashicorp-seal-vault

hashicorp-unseal-vault


Use a single master key share to unseal the vault. If the master key shares threshold is met, the key will attempt to unseal the vault. Otherwise, this API must be called until the threshold is met.

Base Command

hashicorp-unseal-vault

Input

Argument Name Description Required
key Single master key . Optional
reset Reset the unseal project. Possible values are: true. Optional

Context Output

There is no context output for this command.

Command example

!hashicorp-unseal-vault

hashicorp-configure-engine


Configure a secrets engine to fetch secrets from.

Base Command

hashicorp-configure-engine

Input

Argument Name Description Required
path The engine path, e.g., “secret/”. Required
folder Specific folder to fetch secrets from, e.g., “secret-folder/”. (Supported only for engine type KV2.) Optional
type The engine type, e.g., “KV”. Possible values are: KV, Cubbyhole, AWS. Required
version The engine version (for KV engines). Possible values are: 1, 2. Optional
aws_roles_list A comma-delimited list of roles names to generate credentials for. If not mentioned, we will generate credentials for all roles in the path.(used for only for AWS). Optional
aws_method A parameter to indicate which type of request we would like to use to generate credentials(used for only for AWS). Optional

Context Output

There is no context output for this command.

Command example

!hashicorp-configure-engine type=type version=2 path=path ttl=3600

Human Readable Output

Engine configured successfully

hashicorp-reset-configuration


Reset the engine configuration.

Base Command

hashicorp-reset-configuration

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

Command example

!hashicorp-reset-configuration

Human Readable Output

Successfully reset the engine configuration.

hashicorp-create-token


Create a new authentication token.

Base Command

hashicorp-create-token

Input

Argument Name Description Required
role_name The name of the token role. Optional
policies CSV list of policies for the token. This must be a subset of the policies belonging to the token making the request, unless root. If policies are not specified, all policies of the calling token are applied to the new token. Optional
meta A map of string-to-string valued metadata. This is passed through to the audit devices. Optional
no_parent If true and set by a root caller, the token will not have the parent token of the caller. This creates a token with no parent. Possible values are: true, false. Optional
no_default_policy If true the default policy will not be included in this token’s policy set. Possible values are: true, false. Optional
renewable If set to false, the token cannot be renewed past its initial TTL. If set to true, the token can be renewed up to the system/mount maximum TTL. Possible values are: true, false. Optional
ttl The TTL (lease duration) period of the token, provided as “10m” or “1h”, where hour is the largest suffix. If not provided, the token is valid for the default lease TTL, or indefinitely if the root policy is used. Optional
explicit_max_ttl If set, the token will have an explicit max TTL applied to it. The maximum token TTL cannot be changed later, and unlike with normal tokens, updates to the system/mount max TTL value will have no effect at renewal time. The token can never be renewed or used past the value set at issue time. Optional
display_name The display name of the token. Optional
num_uses The maximum number of times the token can be used. Supply this argument to create a one-time-token, or limited use token. The value of 0 has no limit to the number of uses. Optional
period If specified, the token will be periodic. It will not have a maximum TTL (unless an “explicit-max-ttl” is also set), but every renewal will use the given period. Requires a root/sudo token to use. Optional

Context Output

Path Type Description
HashiCorp.Auth.Token string Authentication token.
HashiCorp.Auth.Policy unknown Authentication policies.
HashiCorp.Auth.LeaseDuration number Authentication lease duration in seconds, 0 if indefinitely.

Command example

!hashicorp-create-token display_name=token explicit_max_ttl=3600 renewable=false

hashicorp-generate-role-secret


Generates and issues a new SecretID on an existing AppRole.

Base Command

hashicorp-generate-role-secret

Input

Argument Name Description Required
role_name The name of the AppRole. Required
meta_data Metadata to be tied to the SecretID. Optional
cidr_list Comma separated string or list of CIDR blocks enforcing secret IDs to be used from specific set of IP addresses. Optional
token_bound_cidrs Comma-separated string or list of CIDR blocks. Optional
num_uses Number of times this SecretID can be used, after which the SecretID expires. A value of zero will allow unlimited uses. Optional
ttl_seconds Duration in seconds after which this SecretID expires. A value of zero will allow the SecretID to not expire. Optional

Context Output

There is no context output for this command.

Command example

!hashicorp-generate-role-secret role_name=my-role

Human Readable Output

SecretID:123

hashicorp-get-role-id


Retrieves the AppRole ID for a specified role.

Base Command

hashicorp-get-role-id

Input

Argument Name Description Required
role_name The name of the AppRole. Required

Context Output

Path Type Description
HashiCorp.AppRole.Id string AppRole ID.
HashiCorp.AppRole.Name string AppRole Name.

Command example

!hashicorp-get-role-id role_name=my-role

Human Readable Output

Id Name
role_id role_name

Additional Information

  • In order to fetch credentials from HashiCorp Vault, the relevant secrets engines must be configured with the integration so it can pull the data from them. To configure an engine with the integration, use the configure-engine command.
  • The default fetch rate for fetch-credentials is 10 minutes. This is configurable with the server parameter vault.module.cache.expire

Known Limitations

Currently the integration is able to fetch credentials from the following engines:

  • K/V Versions 1,2
  • Cubbyhole
  • AWS

The following commands are limited to the K/V V2 engine

  • hashicorp-list-secrets
  • hashicorp-get-secret-metadata
  • hashicorp-delete-secret
  • hashicorp-undelete-secret
  • hashicorp-destroy-secret

Configuration parameters

  • server — HashiCorp server URL (e.g., https://192.168.0.1:8200) (required)
  • use_approle — Use AppRole Auth Method
  • credentials — Username / Role ID
  • cache_token — Cache the authentication token
  • token — Authentication token
  • credentials_token
  • namespace — Vault enterprise namespace
  • unsecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetchCredentials — Fetches credentials
  • engines — CSV list of secrets engine types to fetch secrets from
  • concat_username_to_cred_name — Concat username to credential object name

Commands (17)

  • hashicorp-configure-engine

    Configure a secrets engine to fetch secrets from.

  • hashicorp-create-token

    Creates a new authentication token.

  • hashicorp-delete-secret

    Deletes the data under a specified secret given the secret path. Performs a soft delete that allows you to run the hashicorp-undelete-secret command if necessary (for KV V2 engine).

  • hashicorp-destroy-secret

    Permanently deletes a secret (for KV V2 engine).

  • hashicorp-disable-engine

    When a secrets engine is no longer needed, it can be disabled. All secrets under the engine are revoked and the corresponding vault data and configurations are removed.

  • hashicorp-enable-engine

    Enables a new secrets engine at the specified path.

  • hashicorp-generate-role-secret

    Generates and issues a new SecretID on an existing AppRole.

  • hashicorp-get-policy

    Get information for a policy.

  • hashicorp-get-role-id

    Retrieves the AppRole ID for a specified role.

  • hashicorp-get-secret-metadata

    Returns information about a specified secret in a specified KV V2 engine.

  • hashicorp-list-policies

    Lists all configured policies.

  • hashicorp-list-secrets

    List secrets (names) for a specified KV engine.

  • hashicorp-list-secrets-engines

    List all secrets engines that exist in HashiCorp Vault.

  • hashicorp-reset-configuration

    Reset the engines configuration.

  • hashicorp-seal-vault

    If you suspect your data has been compromised, you can seal your vault to prevent access to your secrets.

  • hashicorp-undelete-secret

    Undeletes (restores) a secret on HashiCorp (for KV V2 engine).

  • hashicorp-unseal-vault

    Use a single master key share to unseal the vault. If the master key shares threshold is met, the vault will attempt to unseal the vault. Otherwise, this API must be called until the threshold is met.

import demistomock as demisto  # noqa: F401
import hcl
from CommonServerPython import *  # noqa: F401

""" GLOBAL VARIABLES """

CREDENTIALS = demisto.params().get("credentials", {})
USERNAME = None
PASSWORD = None
# Used to make sure we generate a new token before the old one expires, in seconds, only relevant to AWS
AWS_TOKEN_OVERLAP_TIME = 600
if CREDENTIALS:
    USERNAME = CREDENTIALS.get("identifier")
    PASSWORD = CREDENTIALS.get("password")
VERIFY_SSL = not demisto.params().get("unsecure", False)
TOKEN = demisto.params().get("credentials_token", {}).get("password") or demisto.params().get("token")
CACHE_TOKEN = argToBoolean(demisto.params().get("cache_token", "false") or "false")
NAMESPACE = demisto.params().get("namespace")
USE_APPROLE_AUTH_METHOD = argToBoolean(demisto.params().get("use_approle", "false") or "false")
BASE_URL = demisto.params().get("server", "")
SERVER_URL = BASE_URL + "/v1"

DEFAULT_STATUS_CODES = {429, 472, 473}
TIME_BUFFER = 5

""" HELPER FUNCTIONS """


def get_headers():
    headers = {"Content-Type": "application/json", "X-Vault-Request": "true"}

    if TOKEN:  # pragma: no cover
        headers["X-Vault-Token"] = TOKEN

    if NAMESPACE:  # pragma: no cover
        headers["X-Vault-Namespace"] = NAMESPACE

    return headers


def login():  # pragma: no cover
    if USE_APPROLE_AUTH_METHOD:
        path = "auth/approle/login"  # type: ignore
        body = {"role_id": USERNAME, "secret_id": PASSWORD}
    else:
        path = "auth/userpass/login/" + USERNAME  # type: ignore
        body = {"password": PASSWORD}

    integration_context = get_integration_context()
    url = urljoin(SERVER_URL, path)
    payload = json.dumps(body)
    headers = get_headers()
    res = requests.request("POST", url, headers=headers, data=payload, verify=VERIFY_SSL, allow_redirects=True)
    if (res.status_code < 200 or res.status_code >= 300) and res.status_code not in DEFAULT_STATUS_CODES:
        try:
            error_body = res.json()
            if "errors" in error_body and isinstance(error_body["errors"], list):
                error_body = ";".join(error_body["errors"]) if len(error_body["errors"]) > 0 else "None"
        except Exception as ex:
            demisto.error(f"Error in login (parsing error msg): {ex}")
            error_body = res.content
        return_error(f"Login failed. Status code: {str(res.status_code)}, details: {error_body}")

    auth_res = res.json()
    if not auth_res or "auth" not in auth_res or "client_token" not in auth_res["auth"]:
        return_error("Could not authenticate user")

    integration_context.update(
        {
            "lease_expiration": int(time.time()) + int(auth_res["auth"]["lease_duration"]),
            "auth_token": auth_res["auth"]["client_token"],
        }
    )
    set_integration_context(integration_context)
    return auth_res["auth"]["client_token"]


def send_request(path, method="get", body=None, params=None, headers=None):
    body = body if body is not None else {}
    params = params if params is not None else {}

    url = urljoin(SERVER_URL, path)

    headers = headers if headers is not None else get_headers()
    res = requests.request(method, url, headers=headers, data=json.dumps(body), params=params, verify=VERIFY_SSL)
    if res.status_code < 200 or res.status_code >= 300:
        try:
            error_body = res.json()
            if "errors" in error_body and isinstance(error_body["errors"], list):
                error_body = ";".join(error_body["errors"]) if len(error_body["errors"]) > 0 else "None"
        except Exception as ex:
            demisto.error(f"Error in send_request (parsing error msg): {ex}")
            error_body = res.content
        return_error(f"Request failed. Status code: {str(res.status_code)}, details: {error_body}")
    if res.content:
        return res.json()
    return ""


""" FUNCTIONS """


def generate_role_secret_command():
    """
    Generate a secret ID for a specified AppRole in the authentication system.
    Args:
        args (dict): A dictionary containing the following keys:
            - 'role_name' (required): The name of the AppRole for which the secret ID is generated.
            - 'meta_data': Metadata associated with the secret ID.
            - 'cidr_list': Comma-separated list of CIDR blocks from which requests using the secret ID are allowed.
            - 'token_bound_cidrs': Comma-separated list of CIDR blocks to restrict tokens issued with this secret ID.
            - 'num_uses': Number of times the secret ID can be used before it expires.
            - 'ttl_seconds': Time duration in seconds for which the secret ID remains valid.
    Returns:
        CommandResults: The command results object containing the response from the Vault server as readable output.
    """
    args = demisto.args()
    role_name = args.get("role_name")
    meta_data = args.get("meta_data")
    cidr_list = argToList(args.get("cidr_list", ""))
    token_bound_cidrs = argToList(args.get("token_bound_cidrs", ""))
    num_uses = arg_to_number(args.get("num_uses", ""))
    ttl_seconds = arg_to_number(args.get("ttl_seconds", ""))

    path = f"/auth/approle/role/{role_name}/secret-id"
    body = {
        "metadata": meta_data,
        "cidr_list": cidr_list,
        "token_bound_cidrs": token_bound_cidrs,
        "ttl": ttl_seconds,
        "num_uses": num_uses,
    }
    body = remove_empty_elements(body)

    response = send_request(path=path, method="post", body=body)
    return_results(CommandResults(readable_output=response))


def get_role_id_command():
    """
    Retrieve the Role ID associated with a specified AppRole from the authentication system.
    Args:
        args (dict): A dictionary containing the following keys:
            - 'role_name' (required): The name of the AppRole for which the Role ID is retrieved.
    Returns:
        CommandResults: The command results object containing the retrieved Role ID and role name as outputs.
    """
    args = demisto.args()
    role_name = args.get("role_name")
    path = f"/auth/approle/role/{role_name}/role-id"
    response = send_request(path=path, method="get", body={"role_name": role_name})
    role_id = response.get("data", {}).get("role_id", "") if response else ""

    if not role_id:
        raise DemistoException(f"Role ID not found for AppRole '{role_name}'. Please check the role name and try again.")

    return_results(CommandResults(outputs_prefix="HashiCorp.AppRole", outputs={"Id": role_id, "Name": role_name}))


def list_secrets_engines_command():  # pragma: no cover
    res = list_secrets_engines()

    if not res:
        return_error("No engines found")

    mapped_engines = [
        {"Path": k, "Type": v.get("type"), "Description": v.get("description"), "Accessor": v.get("accessor")}
        for k, v in res.get("data", {}).items()
    ]

    headers = ["Path", "Type", "Description", "Accessor"]

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": res,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown("HashiCorp Vault Secrets Engines", mapped_engines, headers=headers, removeNull=True),
            "EntryContext": {"HashiCorp.Engine(val.Path===obj.Path)": createContext(mapped_engines, removeNull=True)},
        }
    )


def list_secrets_engines():
    path = "sys/mounts"

    return send_request(path)


def list_secrets_command():  # pragma: no cover
    engine = demisto.args()["engine"]
    version = demisto.args().get("version")

    res = list_secrets(engine, version)

    if not res or "data" not in res:
        return_error("Secrets not found")

    mapped_secrets = [{"Path": k} for k in res["data"].get("keys", [])]

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": res,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(
                "HashiCorp Vault Secrets in engine path: " + engine, mapped_secrets, removeNull=True
            ),
            "EntryContext": {"HashiCorp.Secret(val.Path===obj.Path)": createContext(mapped_secrets)},
        }
    )


def list_secrets(engine_path, version, folder=None):
    path = engine_path

    if version == "2":
        path = urljoin(path, "metadata")
        if folder:
            path += os.path.join("/", folder)

    params = {"list": "true"}

    return send_request(path, "get", params=params)


def get_secret_metadata_command():  # pragma: no cover
    engine_path = demisto.args()["engine_path"]
    secret_path = demisto.args()["secret_path"]

    res = get_secret_metadata(engine_path, secret_path)

    if not res or "data" not in res:
        return_error("Secret not found")

    data = res["data"]

    secret_headers = ["Engine", "Created", "Updated", "CurrentVersion"]
    version_headers = ["Number", "Created", "Deleted", "Destroyed"]

    mapped_secret = {
        "Path": secret_path,
        "Engine": engine_path,
        "Created": data.get("created_time"),
        "Updated": data.get("updated_time"),
        "CurrentVersion": data.get("current_version"),
        "CustomMetadata": data.get("custom_metadata"),
    }

    mapped_versions = [
        {"Number": k, "Created": v["created_time"], "Deleted": v["deletion_time"], "Destroyed": v["destroyed"]}
        for k, v in data.get("versions", {}).items()
    ]

    hr = tableToMarkdown("Secret metadata", mapped_secret, headers=secret_headers, removeNull=True)
    if mapped_versions:
        hr += tableToMarkdown("Versions", mapped_versions, headers=version_headers, removeNull=True)
        mapped_secret["Version"] = mapped_versions

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": res,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": hr,
            "EntryContext": {"HashiCorp.Secret(val.Path===obj.Path)": createContext(mapped_secret, removeNull=True)},
        }
    )


def get_secret_metadata(engine_path, secret_path):
    path = engine_path + "/metadata/" + secret_path

    return send_request(path, "get")


def delete_secret_command():  # pragma: no cover
    engine_path = demisto.args()["engine_path"]
    secret_path = demisto.args()["secret_path"]
    versions = argToList(demisto.args()["versions"])

    delete_secret(engine_path, secret_path, versions)

    demisto.results("Secret versions deleted successfully")


def delete_secret(engine_path, secret_path, versions):
    path = urljoin(engine_path, urljoin("delete/", secret_path))

    body = {"versions": versions}

    return send_request(path, "post", body=body)


def undelete_secret_command():  # pragma: no cover
    engine_path = demisto.args()["engine_path"]
    secret_path = demisto.args()["secret_path"]
    versions = argToList(demisto.args()["versions"])

    undelete_secret(engine_path, secret_path, versions)

    demisto.results("Secret versions undeleted successfully")


def undelete_secret(engine_path, secret_path, versions):
    path = urljoin(engine_path, urljoin("undelete/", secret_path))

    body = {"versions": versions}

    return send_request(path, "post", body=body)


def destroy_secret_command():  # pragma: no cover
    engine_path = demisto.args()["engine_path"]
    secret_path = demisto.args()["secret_path"]
    versions = argToList(demisto.args()["versions"])

    destroy_secret(engine_path, secret_path, versions)

    demisto.results("Secret versions destroyed successfully")


def destroy_secret(engine_path, secret_path, versions):
    path = urljoin(engine_path, urljoin("destroy/", secret_path))

    body = {"versions": versions}

    return send_request(path, "post", body=body)


def list_policies_command():  # pragma: no cover
    res = list_policies()

    if not res or "policies" not in res:
        return_error("No policies found")

    mapped_policies = [{"Name": i} for i in res["policies"]]

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": res,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown("HashiCorp Vault Policies", mapped_policies, removeNull=True),
            "EntryContext": {"HashiCorp.Policy(val.Name===obj.Name)": createContext(mapped_policies, removeNull=True)},
        }
    )


def list_policies():
    path = "/sys/policy"

    return send_request(path, "get")


def get_policy_command():  # pragma: no cover
    name = demisto.args()["name"]

    res = get_policy(name)

    if not res or "rules" not in res:
        return_error("Policy not found")

    rules = hcl.loads(res["rules"])

    mapped_rules = [{"Path": k, "Capabilities": v["capabilities"]} for k, v in rules.get("path", {}).items()]

    mapped_policy = {"Name": res["name"], "Rule": mapped_rules}

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": res,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown("HashiCorp Vault Policy - " + name, mapped_rules, removeNull=True),
            "EntryContext": {"HashiCorp.Policy(val.Name===obj.Name)": createContext(mapped_policy, removeNull=True)},
        }
    )


def get_policy(policy_name):
    path = "sys/policy/" + policy_name

    return send_request(path, "get")


def disable_engine_command():  # pragma: no cover
    path = demisto.args()["path"]

    disable_engine(path)

    demisto.results("Engine disabled successfully")


def disable_engine(engine_path):
    path = "sys/mounts/" + engine_path

    return send_request(path, "delete")


def enable_engine_command():  # pragma: no cover
    path = demisto.args()["path"]
    engine_type = demisto.args()["type"]
    description = demisto.args().get("description")
    default_lease_ttl = demisto.args().get("default_lease_ttl")
    max_lease_ttl = demisto.args().get("max_lease_ttl")
    force_no_cache = demisto.args().get("force_no_cache")
    audit_non_hmac_request_keys = argToList(demisto.args().get("audit_non_hmac_request_keys", []))
    audit_non_hmac_response_keys = argToList(demisto.args().get("audit_non_hmac_response_keys", []))
    listing_visibility = demisto.args().get("listing_visibility")
    passthrough_request_headers = argToList(demisto.args().get("passthrough_request_headers", []))
    kv_version = demisto.args().get("kv_version")
    local = demisto.args().get("local")
    seal_wrap = demisto.args().get("seal_wrap")

    enable_engine(
        path,
        engine_type,
        description,
        default_lease_ttl,
        max_lease_ttl,
        force_no_cache,
        audit_non_hmac_request_keys,
        audit_non_hmac_response_keys,
        listing_visibility,
        passthrough_request_headers,
        kv_version,
        local,
        seal_wrap,
    )

    demisto.results("Engine enabled successfully")


def enable_engine(
    path,
    engine_type,
    description,
    default_lease_ttl,
    max_lease_ttl,
    force_no_cache,
    audit_non_hmac_request_keys,
    audit_non_hmac_response_keys,
    listing_visibility,
    passthrough_request_headers,
    kv_version,
    local,
    seal_wrap,
):  # pragma: no cover
    path = "sys/mounts/" + path

    body = {"type": engine_type, "config": {}}
    if description:
        body["description"] = description

    if default_lease_ttl:
        body["config"]["default_lease_ttl"] = default_lease_ttl
    if max_lease_ttl:
        body["config"]["max_lease_ttl"] = max_lease_ttl
    if force_no_cache:
        body["config"]["force_no_cache"] = force_no_cache
    if audit_non_hmac_request_keys:
        body["config"]["audit_non_hmac_request_keys"] = audit_non_hmac_request_keys
    if audit_non_hmac_response_keys:
        body["config"]["audit_non_hmac_response_keys"] = audit_non_hmac_response_keys
    if listing_visibility:
        body["config"]["listing_visibility"] = listing_visibility
    if passthrough_request_headers:
        body["config"]["passthrough_request_headers"] = passthrough_request_headers
    if kv_version:
        body["options"] = {"version": kv_version}
    if local:
        body["local"] = local
    if seal_wrap:
        body["seal_wrap"] = seal_wrap

    if not body["config"]:
        del body["config"]

    return send_request(path, "post", body=body)


def seal_vault_command():  # pragma: no cover
    seal_vault()

    demisto.results("Vault sealed successfully")


def seal_vault():
    path = "sys/seal"

    return send_request(path, "put")


def unseal_vault_command():  # pragma: no cover
    reset = demisto.args().get("reset")
    key = demisto.args().get("key")

    if not key and not reset:
        return_error("Either key or reset must be provided")

    res = unseal_vault(key, reset)

    if not res:
        return_error("Could not retrieve unseal state")

    mapped_unseal = {
        "Sealed": res.get("sealed"),
        "Threshold": res.get("t"),
        "Shares": res.get("n"),
        "Progress": res.get("progress"),
    }

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": res,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown("HashiCorp Vault Unseal", mapped_unseal, removeNull=True),
        }
    )


def unseal_vault(key, reset):
    path = "sys/unseal"
    body = {}
    if reset:
        body["reset"] = reset
    elif key:
        body["key"] = key

    return send_request(path, "put", body=body)


def create_token_command():  # pragma: no cover
    role_name = demisto.args().get("role_name")
    policies = argToList(demisto.args().get("policies", []))
    meta = demisto.args().get("meta")
    no_parent = demisto.args().get("no_parent")
    no_default_policy = demisto.args().get("no_default_policy")
    renewable = demisto.args().get("renewable")
    ttl = demisto.args().get("ttl")
    explicit_max_ttl = demisto.args().get("explicit_max_ttl")
    display_name = demisto.args().get("display_name")
    num_uses = demisto.args().get("num_uses")
    period = demisto.args().get("period")

    res = create_token(
        role_name, policies, meta, no_parent, no_default_policy, renewable, ttl, explicit_max_ttl, display_name, num_uses, period
    )

    if not res or "auth" not in res:
        return_error("Could not get authentication token")

    auth = res["auth"]

    mapped_auth = {"Token": auth.get("client_token"), "Policy": auth.get("policies"), "LeaseDuration": auth.get("lease_duration")}

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": res,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown("Token successfully created", mapped_auth, removeNull=True),
            "EntryContext": {"HashiCorp.Auth(val.Token===obj.Token)": createContext(mapped_auth, removeNull=True)},
        }
    )


def create_token(
    role_name, policies, meta, no_parent, no_default_policy, renewable, ttl, explicit_max_ttl, display_name, num_uses, period
):  # pragma: no cover
    path = "auth/token/create"
    body = {}
    if role_name:
        body["role_name"] = role_name
    if policies:
        body["policies"] = policies
    if meta:
        body["meta"] = meta
    if no_parent:
        body["no_parent"] = no_parent
    if no_default_policy:
        body["no_default_policy"] = no_default_policy
    if renewable:
        body["renewable"] = renewable
    if ttl:
        body["ttl"] = ttl
    if explicit_max_ttl:
        body["explicit_max_ttl"] = explicit_max_ttl
    if display_name:
        body["display_name"] = display_name
    if num_uses:
        body["num_uses"] = num_uses
    if period:
        body["period"] = period

    return send_request(path, "post", body=body)


def configure_engine_command():  # pragma: no cover
    engine_path = demisto.args()["path"]
    engine_type = demisto.args()["type"]
    version = demisto.args().get("version")
    folder = demisto.args().get("folder")
    aws_roles_list = demisto.args().get("aws_roles_list")
    aws_method = demisto.args().get("aws_method")
    ttl = demisto.args().get("ttl")

    configure_engine(
        engine_path, engine_type, version, folder=folder, aws_roles_list=aws_roles_list, aws_method=aws_method, ttl=ttl
    )

    demisto.results("Engine configured successfully")


def reset_config_command():  # pragma: no cover
    set_integration_context({"configs": []})

    demisto.results("Successfully reset the engines configuration")


def configure_engine(
    engine_path, engine_type, version, folder=None, ttl="3600", aws_roles_list=None, aws_method=None
):  # pragma: no cover
    engine_conf = {"type": engine_type, "path": engine_path, "ttl": ttl}
    if version:
        engine_conf["version"] = str(version)
    if folder:
        engine_conf["folder"] = folder
    if aws_roles_list:
        engine_conf["aws_roles_list"] = aws_roles_list
    if aws_method:
        engine_conf["aws_method"] = aws_method

    ENGINE_CONFIGS.append(engine_conf)  # pylint: disable=E0606

    set_integration_context({"configs": ENGINE_CONFIGS})  # pylint: disable=E0606


def fetch_credentials():  # pragma: no cover
    credentials = []
    engines_to_fetch_from = []
    engines = argToList(demisto.params().get("engines", []))
    identifier = demisto.args().get("identifier")
    concat_username_to_cred_name = argToBoolean(demisto.params().get("concat_username_to_cred_name") or "false")
    if len(engines) == 0:
        return_error("No secrets engines specified")
    for engine_type in engines:
        engines_to_fetch = list(filter(lambda e: e["type"] == engine_type, ENGINE_CONFIGS))
        engines_to_fetch_from += engines_to_fetch
    if len(engines_to_fetch_from) == 0:
        return_error("Engine type not configured, Use the configure-engine command to configure a secrets engine.")

    for engine in engines_to_fetch_from:
        if engine["type"] == "KV":
            if "version" not in engine:
                return_error("Version not configured for KV engine, re-configure the engine")
            if engine["version"] == "1":
                credentials += get_kv1_secrets(engine["path"], concat_username_to_cred_name)
            elif engine["version"] == "2":
                credentials += get_kv2_secrets(engine["path"], concat_username_to_cred_name, engine.get("folder"))
        elif engine["type"] == "Cubbyhole":
            credentials += get_ch_secrets(engine["path"], concat_username_to_cred_name)

        elif engine["type"] == "AWS":
            aws_roles_list = []
            if engine.get("aws_roles_list"):
                aws_roles_list = engine.get("aws_roles_list").split(",")
            credentials += get_aws_secrets(engine["path"], concat_username_to_cred_name, aws_roles_list, engine.get("aws_method"))

    if identifier:
        credentials = list(filter(lambda c: c.get("name", "") == identifier, credentials))

    demisto.credentials(credentials)


def get_kv1_secrets(engine_path, concat_username_to_cred_name=False):  # pragma: no cover
    path = engine_path
    params = {"list": "true"}

    res = send_request(path, "get", params=params)

    secrets = []

    if not res or "data" not in res:
        return []

    for secret in res["data"].get("keys", []):
        secret_data = get_kv1_secret(engine_path, secret)
        for k, v in secret_data.get("data", {}).items():
            if concat_username_to_cred_name:
                name = f"{secret}_{k}"
            else:
                name = secret
            secrets.append({"user": k, "password": v, "name": name})

    return secrets


def get_kv1_secret(engine_path, secret):
    path = engine_path + secret

    return send_request(path, "get")


def get_kv2_secrets(engine_path, concat_username_to_cred_name=False, folder=None):  # pragma: no cover
    secrets = []
    res = list_secrets(engine_path, "2", folder)
    if not res or "data" not in res:
        return []

    for secret in res["data"].get("keys", []):
        if str(secret).endswith("/") and secret.replace("/", "") != folder:
            demisto.debug(f"Could not get secrets from path: {secret}")
            continue

        secret_data = get_kv2_secret(engine_path, secret, folder)
        secret_info = secret_data.get("data", {}).get("data", {})
        for k in secret_data.get("data", {}).get("data", {}):
            if concat_username_to_cred_name:
                name = f"{secret}_{k}"
            else:
                name = secret
            secrets.append({"user": k, "password": secret_info[k], "name": name})

    return secrets


def get_kv2_secret(engine_path, secret, folder=None):
    path = urljoin(engine_path, "data/")
    if folder:
        path += os.path.join(folder)
    path += secret

    return send_request(path, "get")


def get_ch_secrets(engine_path, concat_username_to_cred_name=False):  # pragma: no cover
    path = engine_path

    params = {"list": "true"}

    res = send_request(path, "get", params=params)

    secrets = []

    if not res or "data" not in res:
        return []

    for secret in res["data"].get("keys", []):
        secret_data = get_ch_secret(engine_path, secret)
        for k, v in secret_data.get("data", {}).items():
            if concat_username_to_cred_name:
                name = f"{secret}_{k}"
            else:
                name = secret
            secrets.append({"user": k, "password": v, "name": name})

    return secrets


def get_aws_secrets(engine_path, concat_username_to_cred_name, aws_roles_list, aws_method):
    secrets = []
    roles_list_url = engine_path + "/roles"
    demisto.debug(f"roles_list_url: {roles_list_url}")
    params = {"list": "true"}
    res = send_request(roles_list_url, "get", params=params)
    if not res or "data" not in res:
        return []
    for role in res["data"].get("keys", []):
        if aws_roles_list and role not in aws_roles_list:
            continue
        role_url = urljoin(engine_path, urljoin("/roles/", role))
        demisto.debug(f"role_url: {role_url}")
        role_data = send_request(role_url, "get")
        if not role_data or "data" not in role_data:
            return []
        credential_type = role_data["data"].get("credential_type")
        if aws_method:
            if aws_method == "POST":
                credential_type = "sts"
            else:
                credential_type = "iam_user"
        if credential_type != "iam_user":
            method = "POST"
            credential_type = "sts"
        else:
            method = "GET"
            credential_type = "creds"
        generate_credentials_url = urljoin(engine_path + "/", urljoin(credential_type, "/" + role))
        demisto.debug(f"generate_credentials_url: {generate_credentials_url}")
        body = {}
        if "role_arns" in role_data["data"]:
            body["role_arns"] = role_data["data"].get("role_arns", [])
        aws_credentials = send_request(generate_credentials_url, method, body=body)
        if not aws_credentials or "data" not in aws_credentials:
            return []
        access_key = aws_credentials["data"].get("access_key")
        secret_key = aws_credentials["data"].get("secret_key")
        if aws_credentials["data"].get("security_token"):
            secret_key = secret_key + "@@@" + aws_credentials["data"].get("security_token")
        if concat_username_to_cred_name:
            role = f"{role}_{access_key}"
        secrets.append({"user": access_key, "password": secret_key, "name": role})

    return secrets


def get_ch_secret(engine_path, secret):
    path = engine_path + secret

    return send_request(path, "get")


""" EXECUTION CODE """

if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    handle_proxy()
    integration_context = get_integration_context()

    demisto.debug("Executing command: " + demisto.command())
    if USERNAME and PASSWORD:
        if TOKEN:
            return_error("You can only specify one login method, please choose username and password or authentication token")
        else:
            if not CACHE_TOKEN:
                TOKEN = login()
            else:
                lease_expiration = integration_context.get("lease_expiration")
                if not lease_expiration:
                    demisto.debug("No existing lease; creating a new one.")
                    TOKEN = login()
                elif lease_expiration <= int(time.time()) - TIME_BUFFER:
                    demisto.debug("Existing lease expired; creating a new one.")
                    TOKEN = login()
                else:
                    demisto.debug("Existing lease still available; reusing it.")
                    TOKEN = integration_context.get("auth_token")
    elif not TOKEN:
        return_error("Either an authentication token or user credentials must be provided")

    if not integration_context or "configs" not in integration_context:
        integration_context["configs"] = []

    ENGINE_CONFIGS = integration_context["configs"]

    try:
        command = demisto.command()
        if command == "test-module":
            demisto.results("ok")
        elif command == "fetch-credentials":
            fetch_credentials()
        elif command == "hashicorp-list-secrets-engines":
            list_secrets_engines_command()
        elif command == "hashicorp-list-secrets":
            list_secrets_command()
        elif command == "hashicorp-list-policies":
            list_policies_command()
        elif command == "hashicorp-get-policy":
            get_policy_command()
        elif command == "hashicorp-get-secret-metadata":
            get_secret_metadata_command()
        elif command == "hashicorp-delete-secret":
            delete_secret_command()
        elif command == "hashicorp-undelete-secret":
            undelete_secret_command()
        elif command == "hashicorp-destroy-secret":
            destroy_secret_command()
        elif command == "hashicorp-disable-engine":
            disable_engine_command()
        elif command == "hashicorp-enable-engine":
            enable_engine_command()
        elif command == "hashicorp-seal-vault":
            seal_vault_command()
        elif command == "hashicorp-unseal-vault":
            unseal_vault_command()
        elif command == "hashicorp-create-token":
            create_token_command()
        elif command == "hashicorp-configure-engine":
            configure_engine_command()
        elif command == "hashicorp-reset-configuration":
            reset_config_command()
        elif command == "hashicorp-generate-role-secret":
            generate_role_secret_command()
        elif command == "hashicorp-get-role-id":
            get_role_id_command()

    except Exception as e:
        demisto.debug(f"An error occurred: {e}")
        return_error(f"An error occurred: {e}")