Fortanix DSM

Manage Secrets and Protect Confidential Data using Fortanix Data Security Manager.

Identity and Access Management · Fortanix DSM

Details

IDFortanix DSM
ProviderFortanix
CategoryIdentity and Access Management
From Version6.8.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Manage Secrets and Protect Confidential Data using Fortanix Data Security Manager (DSM)

Authorize Cortext XSOAR to Fortanix DSM

User/password or Client Certificate Auth Method

These fields accept the Username and Password parameters for a user or App. These credentials may also be used for mutual-TLS using a client key and certificate. The may be signed by a Trusted CA if Fortanix DSM is configured accordingly.

API KEY Auth Method

An easy and quick way to test the integration is to specify the Basic Authentication token parameter from the Fortanix DSM App’s API KEY.

Configure Fortanix DSM in Cortex

Parameter Description Required
Fortanix DSM server endpoint URL e.g., https://amer.smartkey.io True
Username / App UUID / Certificate If Certificate, specify PEM False
Password / App Secret / Private Key Private key cannot be encrypted False
API Key Alternative to Username and Password False
Trust any server certificate (insecure) Ignores TLS, not recommended False
Use system proxy settings Whether to use proxy settings from the Environment False
Group UUID to list secrets from Filter the secrets accessible to a single DSM Group False
Data protection key used for encryption and decryption Also configure the Cipher Mode False
Encryption and decryption mode e.g. FPE, GCM, CBC 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.

fortanix-list-secrets


List secrets from one or more specified group(s)

Base Command

fortanix-list-secrets

Input

Argument Name Description Required
group_id Group UUID to list secrets from (overrides integration settings). Optional
state Current state of the secret (default show all except deleted or destroyed). Possible values are: enabled, disabled, preactive, active, deactivated, compromised, deleted, destroyed. Optional
page Page offset to return (100 results at a time). Optional

Context Output

Path Type Description
Fortanix.Secret.Name string Secret Name
Fortanix.Secret.ID string Secret ID (Key ID or kid)
Fortanix.Secret.Group unknown Group ID

fortanix-get-secret-metadata


Get the secret metadata without exposing its value

Base Command

fortanix-get-secret-metadata

Input

Argument Name Description Required
name Name of the secret (mandatory, unless kid is specified). Optional
kid Secret UUID (unless name is unspecified). Optional

Context Output

Path Type Description
Fortanix.Secret unknown Secret metadata, if successful

fortanix-fetch-secret


Retrieve the secret value

Base Command

fortanix-fetch-secret

Input

Argument Name Description Required
kid Secret UUID (obtained from the list-secrets command). Required

Context Output

Path Type Description
Fortanix.Secret.Value unknown Sensitive value of the secret

fortanix-new-secret


Import a new secret

Base Command

fortanix-new-secret

Input

Argument Name Description Required
name Name of the secret. Required
value Sensitive value of the secret. Required
group_id Group UUID to import the secret into. Optional
metadata List of key-value pairs. Optional

Context Output

Path Type Description
Fortanix.Secret unknown Secret metadata, if successful

fortanix-rotate-secret


Update an existing secret, which will be rotated

Base Command

fortanix-rotate-secret

Input

Argument Name Description Required
name Name of the secret. Required
value Sensitive value of the secret. Required
metadata List of key-value pairs. Optional

Context Output

Path Type Description
Fortanix.Secret unknown Secret metadata, if successful

fortanix-delete-secret


Delete the secret

Base Command

fortanix-delete-secret

Input

Argument Name Description Required
kid Secret UUID. Required

Context Output

Path Type Description
Fortanix.Secret.Result unknown Deletion status

fortanix-invoke-plugin


Invoke a Fortanix Plugin that is executed in a Confidential Computing enclave

Base Command

fortanix-invoke-plugin

Input

Argument Name Description Required
pid Plugin UUID. Required
input Arbitrary user input based on the plugin. Optional

Context Output

Path Type Description
Fortanix.Plugin.Output unknown Plugin invocation output

fortanix-encrypt


Protects data using key configured in Fortanix DSM

Base Command

fortanix-encrypt

Input

Argument Name Description Required
data User data. Required
key Key name used for protection (overrides configured). Optional
mode Encryption mode (overrides configured). Possible values are: FPE, GCM, CBC. Optional

Context Output

Path Type Description
Fortanix.Data.Cipher unknown Encryption output

fortanix-decrypt


Reveal data using key configured in Fortanix DSM

Base Command

fortanix-decrypt

Input

Argument Name Description Required
cipher Protected ciphertext. Required
kid Key UUID for decryption (overrides configured). Optional
mode Decryption mode (overrides configured). Possible values are: FPE, GCM, CBC. Optional
iv Nonce or initialization vector (if any). Optional

Context Output

Path Type Description
Fortanix.Data.Plain unknown Decryption output

Configuration parameters

  • server — Fortanix DSM server endpoint (required)
  • credentials — Username / App UUID / Certificate
  • token — API Key
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • group_ids — Group UUID to list secrets from
  • protection_key — Data protection key used for encryption and decryption
  • protection_mode — Encryption and decryption mode

Commands (9)

  • fortanix-decrypt

    Reveal data using key configured in Fortanix DSM

  • fortanix-delete-secret

    Delete the secret

  • fortanix-encrypt

    Protects data using key configured in Fortanix DSM

  • fortanix-fetch-secret

    Retrieve the secret value

  • fortanix-get-secret-metadata

    Get the secret metadata without exposing its value

  • fortanix-invoke-plugin

    Invoke a Fortanix Plugin that is executed in a Confidential Computing enclave

  • fortanix-list-secrets

    List secrets from one or more specified group(s)

  • fortanix-new-secret

    Import a new secret

  • fortanix-rotate-secret

    Update an existing secret, which will be rotated

"""Fortanix-DSM Integration for Cortex XSOAR - Unit Tests file

This Unit Tests in this file validate the FortanixDSM Integration based
on pytest as mandated by Cortex XSOAR contribution requirements. They verify
that the integration is behaving as expected during CI/CD pipeline.

Test Execution
--------------

Unit tests can be checked in 3 ways:
- Using the command `lint` of demisto-sdk. The command will build a dedicated
  docker instance for your integration locally and use the docker instance to
  execute your tests in a dedicated docker instance.
- From the command line using `pytest -v` or `pytest -vv`
- From PyCharm

Example with demisto-sdk (from the content root directory):
demisto-sdk lint -i Packs/Fortanix-DSM/Integrations/FortanixDSM

Coverage
--------

There could be one or more unit tests per command function and is executed with
specific parameters, wherein the output is checked against an expected result.

Unit tests should be self contained and should not interact with external
resources like (API, devices, ...). To isolate the code from external resources
you need to mock the API of the external resource using pytest-mock:
https://github.com/pytest-dev/pytest-mock/

In the following code we configure requests-mock (a mock of Python requests)
before each test to simulate the API calls to the Fortanix DSM API. This way we
can have full control of the API behavior and focus only on testing the logic
inside the integration code.

We recommend to use outputs from the API calls and use them to compare the
results when possible. See the ``test_data`` directory that contains the data
we use for comparison, in order to reduce the complexity of the unit tests and
avoding to manually mock all the fields.

NOTE: we do not have to import or build a requests-mock instance explicitly.
requests-mock library uses a pytest specific mechanism to provide a
requests_mock instance to any function with an argument named requests_mock.

More Details
------------

More information about Unit Tests in Cortex XSOAR:
https://xsoar.pan.dev/docs/integrations/unit-testing

"""

import json

import pytest
from CommonServerPython import DemistoException


def util_load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


def test_integration(requests_mock):
    from FortanixDSM import Client, get_headers, test_module

    url = "https://fortanix.test"
    path = "sys/v1/session/auth"

    mock_response = {
        "token_type": "Bearer",
        "expires_in": 120,
        "entity_id": "some_account_id",
        "access_token": "some_bearer_token",
    }

    requests_mock.post(f"{url}/{path}", json=mock_response)

    client = Client(base_url=url, verify=False, headers=get_headers(None, None, "some_api_key"))

    response = test_module(client)
    assert response == "ok"

    requests_mock.post(f"{url}/{path}")
    with pytest.raises(DemistoException):
        response = test_module(client)
        assert not response


def test_list_secrets(requests_mock):
    """
    Tests fortanix-list-secrets command function.

        Given:
            - requests_mock instance to generate the appropriate list_secrets API
              response when the correct list_secrets API request is performed.
            - group_id argument

        When:
            - Running the 'list-secrets-command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, list_secrets_command

    url = "https://fortanix.test"
    path = "crypto/v1/keys"
    name = "secret 101"
    kid = "7a161a3f-8d53-42de-80cd-92fb017c5a12"
    gid = "07f85883-adaf-4a6c-a040-ffed46dfd349"
    params = f"obj_type=SECRET&sort=name%3Aasc&group_id={gid}"

    mock_response = [
        {"name": name, "kid": kid, "group_id": gid, "state": "Active", "enabled": True},
        {"name": name, "kid": kid, "group_id": gid, "state": "Active", "enabled": False},
    ]
    requests_mock.get(f"{url}/{path}?{params}", json=mock_response)

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"group_id": gid, "state": "Active", "page": 0}

    response = list_secrets_command(client, args)

    assert response.outputs_prefix == "Fortanix.Secret"
    """
    mocking a single secret response here, but;
    for a list of secrets, the response from the request
    needs to have each secret.state defined and the
    method response differs from the request response.
    """
    mock_response = [
        {
            "Name": name,
            "ID": kid,
            "Group": gid,
        },
        {
            "Name": name,
            "ID": kid,
            "Group": gid,
        },
    ]
    assert response.outputs == mock_response


def test_get_secret_metadata(requests_mock):
    """
    Tests fortanix-get-secret-metadata command function.

        Given:
            - requests_mock instance to generate the appropriate get_secret_metadata API
              response when the correct get_secret_metadata API request is performed.
            - kid argument

        When:
            - Running the 'get-secret-metadata-command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, list_secrets_command

    url = "https://fortanix.test"
    path = "crypto/v1/keys"
    name = "secret 101"
    kid = "7a161a3f-8d53-42de-80cd-92fb017c5a12"
    gid = "07f85883-adaf-4a6c-a040-ffed46dfd349"
    params = "obj_type=SECRET&sort=name%3Aasc"

    mock_response = {"name": name, "kid": kid, "group_id": gid, "obj_type": "SECRET"}

    requests_mock.get(f"{url}/{path}/{kid}?{params}", json=mock_response)

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"kid": kid}

    response = list_secrets_command(client, args)

    assert response.outputs_prefix == "Fortanix.Secret"
    assert response.outputs == mock_response

    args["page"] = 2
    args["state"] = "deleted"
    response = list_secrets_command(client, args, gid)

    assert response.outputs_prefix == "Fortanix.Secret"
    assert response.outputs == mock_response


def test_no_secrets(requests_mock):
    """
    Tests fortanix-get-secret-metadata command function.

        Given:
            - requests_mock instance to generate the appropriate get_secret_metadata API
              response when the correct get_secret_metadata API request is performed.
            - name argument

        When:
            - Running the 'get-secret-metadata-command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, list_secrets_command

    url = "https://fortanix.test"
    path = "crypto/v1/keys"
    name = "secret 101"
    params = "obj_type=SECRET&sort=name%3Aasc"

    requests_mock.get(f"{url}/{path}?{params}", json={})

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"name": name}

    response = list_secrets_command(client, args)

    assert response.outputs_prefix == "No secrets found"
    assert response.outputs == {}


def test_fetch_secret(requests_mock):
    """
    Tests fortanix-fetch-secret command function.

        Given:
            - requests_mock instance to generate the appropriate fetch_secret API
              response when the correct fetch_secret API request is performed.
            - name argument

        When:
            - Running the 'fetch-secret-command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, fetch_secret_command

    url = "https://fortanix.test"
    path = "crypto/v1/keys/export"
    kid = "7a161a3f-8d53-42de-80cd-92fb017c5a12"
    value = "SGVsbG8gV29ybGQ="

    mock_response = {"value": value}
    requests_mock.post(f"{url}/{path}", json=mock_response)

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"kid": kid}

    response = fetch_secret_command(client, args)

    assert response.outputs_prefix == "Fortanix.Secret.Value"
    mock_response = [{"Value": "Hello World"}]
    assert response.outputs == mock_response

    with pytest.raises(DemistoException):
        response = fetch_secret_command(client, {})
        assert not response


def test_new_secret(requests_mock):
    """
    Tests fortanix-new-secret command function.

        Given:
            - requests_mock instance to generate the appropriate import_secret API
              response when the correct import_secret API request is performed.
            - name argument
            - value argument

        When:
            - Running the 'new_secret_command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, import_secret_command

    url = "https://fortanix.test"
    path = "crypto/v1/keys"
    gid = "07f85883-adaf-4a6c-a040-ffed46dfd349"
    kid = "7a161a3f-8d53-42de-80cd-92fb017c5a12"
    aid = "0a9bcbbe-1919-4c57-89e4-b521f4d9ff28"
    name = "new secr3t"

    mock_response = {"kid": kid, "name": name, "acct_id": aid, "obj_type": "SECRET", "never_exportable": False, "state": "Active"}

    requests_mock.put(f"{url}/{path}", json=mock_response)

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {
        "name": name,
        "value": "rand0m",
        "metadata": {"key": "attribute"},
    }

    response = import_secret_command(client, args, gid)

    assert response.outputs_prefix == "Fortanix.Secret"
    assert response.outputs == mock_response

    with pytest.raises(DemistoException):
        response = import_secret_command(client, {"name": name})
        assert not response

        response = import_secret_command(client, {"value": "val"})
        assert not response

        requests_mock.put(f"{url}/{path}")
        response = import_secret_command(client, {"name": name, "value": "val"})
        assert not response


def test_rotate_secret(requests_mock):
    """
    Tests fortanix-rotate-secret command function.

        Given:
            - requests_mock instance to generate the appropriate import_secret API
              response when the correct import_secret API request is performed.
            - name argument
            - value argument

        When:
            - Running the 'rotate_secret_command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, import_secret_command

    url = "https://fortanix.test"
    path = "crypto/v1/keys/rekey"
    kid = "7a161a3f-8d53-42de-80cd-92fb017c5a12"
    aid = "0a9bcbbe-1919-4c57-89e4-b521f4d9ff28"
    name = "new secr3t"

    mock_response = {"kid": kid, "name": name, "acct_id": aid, "obj_type": "SECRET", "never_exportable": False, "state": "Active"}

    requests_mock.post(f"{url}/{path}", json=mock_response)

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"name": name, "value": "rand0m", "metadata": "key1=attr1,key2=attr2"}

    response = import_secret_command(client, args, None, True)

    assert response.outputs_prefix == "Fortanix.Secret"
    assert response.outputs == mock_response

    requests_mock.post(f"{url}/{path}")
    with pytest.raises(DemistoException):
        response = import_secret_command(client, args, None, True)
        assert not response


def test_delete_secret(requests_mock):
    """
    Tests fortanix-delete-secret command function.

        Given:
            - requests_mock instance to generate the appropriate delete_secret API
              response when the correct delete_secret API request is performed.
            - kid argument

        When:
            - Running the 'delete-secret-command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, delete_secret_command

    url = "https://fortanix.test"
    path = "crypto/v1/keys"
    kid = "7a161a3f-8d53-42de-80cd-92fb017c5a12"

    mock_response = [{"Result": "OK"}]

    requests_mock.delete(f"{url}/{path}/{kid}", json={})

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"kid": kid}

    response = delete_secret_command(client, args)

    assert response.outputs_prefix == "Fortanix.Secret.Result"
    assert response.outputs == mock_response

    with pytest.raises(DemistoException):
        response = delete_secret_command(client, {})
        assert not response

        requests_mock.delete(f"{url}/{path}/{kid}", json={})
        response = delete_secret_command(client, args)
        assert response.outputs == {}


def test_invoke_plugin(requests_mock):
    """
    Tests fortanix-invoke-plugin command function.

        Given:
            - requests_mock instance to generate the appropriate invoke-plugin API
              response when the correct invoke-plugin API request is performed.
            - pid argument
            - input argument

        When:
            - Running the 'invoke-plugin-command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, invoke_plugin_command

    url = "https://fortanix.test"
    path = "sys/v1/plugins"
    pid = "3599796b-7b18-49c3-aad8-9758af24fbf9"

    mock_response = {"plugin_output": "Plugin_value", "another_key": True, "numeric_key": 90210}

    requests_mock.post(f"{url}/{path}/{pid}", json=mock_response)

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"pid": pid, "input": "random"}

    response = invoke_plugin_command(client, args)

    assert response.outputs_prefix == "Fortanix.Plugin.Output"
    assert response.outputs == [mock_response]

    args["input"] = (
        "eyJraWQiOiAiN2ExNjFhM2YtOGQ1My00MmRlLTgwY"
        "2QtOTJmYjAxN2M1YTEyIiwgImNpcGhlciI6ICJ1Mk"
        "tNY0FVRjFqc2lmSmZoOTl1V3F3PT0iLCAiaXYiOiAi"
        "cjdIZUhkdUhTWjFJckNDNnM3TUcwdz09IiwgIm1vZG"
        "UiOiAiQ0JDIn0="
    )
    response = invoke_plugin_command(client, args)
    assert response.outputs_prefix == "Fortanix.Plugin.Output"
    assert response.outputs == [mock_response]

    with pytest.raises(DemistoException):
        response = invoke_plugin_command(client, {})
        assert not response


def test_encrypt(requests_mock):
    """
    Tests fortanix-encrypt command function.

        Given:
            - requests_mock instance to generate the appropriate encrypt API
              response when the correct encrypt API request is performed.
            - key argument
            - data argument
            - mode argument

        When:
            - Running the 'encrypt-command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, encrypt_command

    url = "https://fortanix.test"
    path = "crypto/v1/encrypt"
    kid = "7a161a3f-8d53-42de-80cd-92fb017c5a12"

    mock_response = {"kid": kid, "cipher": "u2KMcAUF1jsifJfh99uWqw==", "iv": "r7HeHduHSZ1IrCC6s7MG0w=="}

    requests_mock.post(f"{url}/{path}", json=mock_response)

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"key": "key_name", "data": "Hello World", "mode": "CBC"}

    response = encrypt_command(client, args)

    cipher = (
        "eyJraWQiOiAiN2ExNjFhM2YtOGQ1My00MmRlLTgwY2QtOTJmYjAxN2M1YTEyIi"
        "wgImNpcGhlciI6ICJ1MktNY0FVRjFqc2lmSmZoOTl1V3F3PT0iLCAiaXYiOiAi"
        "cjdIZUhkdUhTWjFJckNDNnM3TUcwdz09IiwgIm1vZGUiOiAiQ0JDIn0="
    )
    assert response.outputs_prefix == "Fortanix.Data.Cipher"
    assert response.outputs == [{"Cipher": cipher}]

    with pytest.raises(DemistoException):
        response = encrypt_command(client, {})
        assert not response


def test_decrypt(requests_mock):
    """
    Tests fortanix-decrypt command function.

        Given:
            - requests_mock instance to generate the appropriate decrypt API
              response when the correct decrypt API request is performed.
            - cipher argument

        When:
            - Running the 'decrypt-command'.

        Then:
            -  Checks the output of the command function with the expected output.

    """
    from FortanixDSM import Client, decrypt_command

    url = "https://fortanix.test"
    path = "crypto/v1/decrypt"
    kid = "7a161a3f-8d53-42de-80cd-92fb017c5a12"

    cipher = (
        "eyJraWQiOiAiN2ExNjFhM2YtOGQ1My00MmRlLTgwY2QtOTJmYjAxN2M1YTEyIi"
        "wgImNpcGhlciI6ICJ1MktNY0FVRjFqc2lmSmZoOTl1V3F3PT0iLCAiaXYiOiAi"
        "cjdIZUhkdUhTWjFJckNDNnM3TUcwdz09IiwgIm1vZGUiOiAiQ0JDIn0="
    )
    mock_response = {
        "kid": kid,
        "plain": "u2KMcAUF1jsifJfh99uWqw==",
    }

    requests_mock.post(f"{url}/{path}", json=mock_response)

    client = Client(base_url=url, verify=False, headers={"Authentication": "Basic some_api_key"})

    args = {"cipher": cipher}

    response = decrypt_command(client, args)

    assert response.outputs_prefix == "Fortanix.Data.Plain"
    assert response.outputs == [{"Plain": "u2KMcAUF1jsifJfh99uWqw=="}]

    with pytest.raises(DemistoException):
        response = decrypt_command(client, {})
        assert not response


@pytest.mark.parametrize("method", ["base64", "json", "metadata", "headers"])
def test_helpers(method):
    """
    Given:
        - A string represent a method name

    When:
        - Running the helper functions.

    Then:
        - Verify that the exceptions are raised
    """
    from FortanixDSM import get_headers, getMetadata, isBase64, isJSON

    empty = {}

    if method == "base64":
        res = isBase64(None)
        assert not res

    elif method == "json":
        res = isJSON(empty)
        assert not res

    elif method == "metadata":
        res = getMetadata(empty)
        assert not res

    elif method == "headers":
        res = get_headers("some_user", "some_passwd")
        assert res
        assert isinstance(res, dict)
        assert res["Authorization"]

        res = get_headers(None, None, None, "some_bearer")
        assert res
        assert isinstance(res, dict)
        assert res["Authorization"]