Pre-hooks
  • 26 Apr 2024
  • 5 Minutes to read
  • Dark
    Light
  • PDF

Pre-hooks

  • Dark
    Light
  • PDF

Article summary

Traceable provides the capability using which you can write pre-hooks. The pre-hooks can be used to carry out multiple tasks, for example, authentication before starting the tests, test setup, and so on. These pre-hooks can be configured to run at multiple levels. For example, you can configure pre-hook to run before each individual test plugin, or test plugin category, for the entire scan. For example, in the sample config.yaml file shown below, you can see that a pre-hook runs before the start of the plugin and one specific to a test. The bfla pre-hook shown in the sample config.yaml is for an individual Authorization plugin, while crap_login_prehook is for logging into an application. These pre-hooks are written in Python and used in Traceable CLI.

Note

Since pre-hooks are application-specific, use the examples as samples to create your custom pre-hooks.

# This is a sample user config. 
# Copy it to traceable config folder ~/.traceable/config.yaml to apply. 
# This config overrides data in default.yaml.
# Do not make edits to default.yaml as changes will be lost.
scan:
  plugins:
    # Enable prehooks for all plugins
    pre_hooks:
      - crapi_login_prehook
    post_hooks:
      - crapi_login_posthook
    authentication:
      unauthenticated_access:
        pre_hooks:
          # Disable crapi_login_prehook for unauthenticated_access
          - crapi_login_prehook:
              disabled: true
          # Disable crapi_login_posthook for unauthenticated_access
          - crapi_login_posthook:
              disabled: true
    authorization:
        bfla:
          pre_hooks:
            - crapi_creds_prehook
    custom:
      sample_plugin: {}

Make a note of the following that are used in the examples below:

  • Authentication hook method is invoked before sending every request.

  • You may or may not want to access the authentication endpoint before each request. You can control that behavior using scanctx and pluginctx.

  • scanctxis a dictionary scoped per scan. To reuse any value throughout the scan, add that value with the key of your choice and use it whenever it is present. Following is an example:

    Python

    if scanctx.get("token_key") == None:
                token = (make request to obtain token).getToken
                scanctx["token_key] = token
            token = scanctx.get("token_key")
  • Similar to scanctx, pluginctx is scoped per plugin. So, a unique dictionary is used per plugin. You can store values at key level. 

  • Hook can add key-value in headers, query parameters, cookies, or request body. You need to add these to the attributes map. For example,

    • You need to first fetch the attributes from the testcase (which essentially translates to parameter of next request) attributes = testcase.get_attributes()

    • To Add Cookie: attributes.set("mutated.http.request.cookie.%s" % (cookie.name), cookie.value)

    • To Add Header: attributes.set("mutated.http.request.header.authorization", "Bearer fnasfjasjfoijfoiefjkewwrw")    

    • To add Query parameter: attributes.set("mutated.http.request.query.authorization", "Bearer fnasfjasjfoijfoiefjkewwrw")     

    • To add body parameter: attributes.set("mutated.http.request.body.authorization", "Bearer fnasfjasjfoijfoiefjkewwrw")

The following two examples are two sample authentication pre-hooks for OWASP crAPI application. For more information, see crAPI.

crAPI credential pre-hook

The following is a sample pre-hook function in Python:

def sample_traceable_login_prehook(scanctx: ScanContext, pluginctx: PluginContext, testcase: TestCase, **kwargs) -> list[Assertion]:
    attributes = testcase.get_attributes()
    url = urllib.parse.urlparse(
        attributes.get_one("mutated.http.request.url", ""))
    logger.info("Running crapi_login_prehook for plugin %s" % pluginctx.get_plugin())
    if scanctx.get("crapi.role.user", None) is None:
        # Login:
        logger.debug("crapi_login_prehook: Token not found. Creating token")
        u = urllib.parse.urlunparse(
            url._replace(path="/identity/api/auth/login"))
        res = requests.post(u, json={
            "email": "test@example.com",
            "password": "Test!123"
        }, headers={
            "content-type": "application/json",
            "x-traceable-ast": "0,0,0"
        })
        if res.status_code != 200:
            logger.error("Failed to login to crapi, got status %d" %
                         res.status_code)
            return []

        res = res.json()
        scanctx.set("crapi.role.user", res["token"])
    else:
        logger.debug("Already logged in to CRAPI")
    attributes.set("mutated.auth.attribute", "mutated.http.request.header.authorization")
    attributes.set("mutated.role.user", "Bearer %s" % scanctx.get("crapi.role.user"))
    attributes.set("mutated.role.bolauser", "Bearer %s" % scanctx.get("crapi.role.bolauser"))
    logger.debug("Mutated role user: %s" % scanctx.get("crapi.role.user"))
    return []

Explanation

This function is a pre-hook that is used to manage authentication for the crAPI in a security scan context.

First, the function parses the URL from the mutated.http.request.url attribute in the attributes parameter, and then checks if a token for the user's role is already present in the scanctx object. If not, the function logs into crAPI by sending a POST request to the login endpoint with a test email and password, and retrieves a token. The retrieved token is then stored in the scanctx object.

If a token is already present, the function logs a message indicating that the user is already logged in. The function then sets two attributes in the attributes object: mutated.auth.attribute is set to mutated.http.request.header.authorization, and mutated.role.user is set to "Bearer" plus the token retrieved from scanctx.

scanctx is managed by Traceable CLI internally at run time.

CSRF token pre-hook

The following is a sample code to set header csrf_token to a random string in an attack request. A random string can be replaced with an actual token at runtime:

def set_csrf_token_prehook(scanctx: ScanContext, pluginctx: PluginContext, attributes: AttributeList) -> list[Assertion]:
    if not scanctx.get("csrf_token"):
        scanctx.set("csrf_token", "".join(random.sample(string.ascii_lowercase, k=10)))
    attributes.set("mutated.http.request.header.csrf_token", scanctx.get("csrf_token"))
    return []

Explanation

The function checks if a CSRF token is already present in the scanctx object by attempting to retrieve it using the get() method with the key "csrf_token". If a CSRF token is not present, the function generates a new CSRF token by randomly selecting 10 characters from the lowercase English alphabet using the random.sample() method and stores it in the scanctx object with the same key "csrf_token".

The function then sets the mutated.http.request.header.crf_token attribute in the attributes object to the CSRF token retrieved from scanctx using the get() method with the same key "csrf_token".


Configuring the pre-hook

The pre-hooks that you create need to be configured in Traceable CLI. Make sure that Traceable CLI is installed before proceeding. The important files and directories in Traceable CLI that are used for pre-hooks are:

  • hooks directory – Your pre-hooks and posthooks are stored in this directory.

  • config.yaml

  • default.yaml

The default.yaml file is provided by Traceable. Whatever is added to default.yaml is overwritten by config.yaml. It is a practical approach to have your working pre-hooks configured in config.yaml. The config.yaml file is given preference at run time. Refer to the config.yaml listed at the starting of the topic.


Was this article helpful?

What's Next