1. Home
  2. Docs
  3. Developer’s Guide
  4. Callbacks

Callbacks

Callbacks allow users to execute arbitrary Python code during test runtime to perform operations not available via the user interface. Callbacks can be extremely useful in several ways, such as:

  • Implement advanced response processing rules.
  • Incorporating arbitrary health checks into the test flow.
  • Restore target to a known good state before delivering the next test case.
  • Utilizing binary instrumentation frameworks (e.g., QDBI and Frida). For example, instrument client applications to connect to GUARDARA when it is simulating a server.
  • Perform other, external automation during the test flow.

Creating Callbacks

Callbacks can be assigned to the Callback actions within Test Flow templates. Similarly to other assets, Callbacks can be managed via the Templates page. To create a new Callback, click File > New Callback on the main menu of the Templates page.

The default, skeleton callback method to be implemented by users is shown below.

def callback(context):
    pass

Context

The context variable of a Callback is an object that exposes several internal methods of the Engine. These are discussed below.

Runtime

Method Description
context.runtime.is_analysis() Returns True if the callback is executed during the automated issue analysis phase of the testing process, False otherwise.
context.runtime.log(severity, message) The method allows to write messages to the test’s Activity Log. The severity supports the following values: info, warning and error. The message is expected to be an arbitrary string (Python str).
context.runtime.performance() Returns a dictionary (Python dict) containing performance metrics about the test.

Session

Method Description
context.session.get(key) Obtain the value of a session variable.
context.session.set(key, type, value) Set the value of an existing session variable or create a new session variable.

When fetching a session variable, the value of the key variable should be the name of the session variable. The method returns a Python dict with two keys: raw and rendered. The raw is the raw value of the session variable without any transforms. The rendered value is the value after all transforms are applied by the Engine.

When updating or creating a session variable, the key should be the name of the session variable, the type should be either raw or rendered and the value is the value to be set.

Driver

The driver methods allow sending and receiving data from the target. The following table summarizes the driver methods exposed via the callback context.

Method Description
context.send_next(data) Allows to override what data to send during the next send action. The data is expected to be a Python string (str).
context.driver.connect(props) Allows connecting to the target using the configured driver.
context.driver.disconnect(props) Allows disconnecting from the target using the configured driver.
context.driver.send(data, props) Allows sending data to the target via the configured driver.
context.driver.recv(props) Allows receiving data from the target via the configured driver.

The props variable allows to pass configuration options to the driver method. These are the same options generated by the Flow Designer for actions.

History

The history contains information about messages previously sent to the target. In addition, the instance of the mutator of the previously sent and the next action is also made accessible. The following table summarizes the history methods exposed via the callback context.

Method Description
context.history.fetch() Returns all items from the history.
context.history.get_last() Returns the last item from the history.
context.history.get_last_mutator() Returns the last mutator instance.
context.history.get_next_mutator() Returns the next mutator instance.

History Items

The previously sent data is a list of objects. The format of these objects is shown below.

{
    "iteration":   counter,
    "timestamp":   timestamp,
    "action_id":   action_id,
    "action_type": action,
    "unit":        unit_id,
    "length":      nr_bytes_sent,
    "data":        data_sent
}

The table below discusses the above attributes in more detail.

Property Type Description
iteration integer The iteration counter. One iteration is one round of executing all the actions.
timestamp integer Timestamp in the format: int(time.time() * 1000).
action_id string The UUID of the action that sent the data.
action_type string The type of the action, such as send or event.
unit string The UUID of the Message the data was generated based on.
length integer The length of the data sent.
data list The data as a list of integers representing the individual bytes.

Mutator

The mutator instances obtained via get_last_mutator() and get_next_mutator() expose the following methods.

Method Description
completed() Returns whether all mutations completed (True) or not (False).
next() Set current mutating Field to completed and moves to the next Field.
mutate() Triggers one mutation of the currently mutating Field. This method does not return anything.
render() Returns the rendered Message data. The data returned is a list of rendered Group fields on the root level of the Message. Each item in the list is a string. To get the final data to be sent, the list has to be joined together, e.g.: b"".join(list_items).

The mutator instances also expose the Root Block via the unit property for example:

mutator = context.get_last_mutator()
root_block = mutator.unit

The root block instance exposes the following methods.

Method Description
mutating() Returns the instances of mutating Fields as a list.
next() Set current mutating Field to completed and moves to the next Field.
mutate() Triggers one mutation of the currently mutating Field. This method does not return anything.
render() Returns the rendered Message data. The data returned is a list of rendered Group fields on the root level of the Message. Each item in the list is a string. To get the final data to be sent, the list has to be joined together, e.g.: b"".join(list_items).
search(name) Get Field instance by name.

An example of getting the rendered value of a Field representing the HTTP Protocol is shown below.

def callback(context):
    m = context.history.get_last_mutator()
    mutating = m.unit.search("Http.Protocol")
    print(mutating.render())

Exceptions

Callbacks can raise exceptions just like any Python code. Additionally, there are a set of Engine-specific exceptions available via the guardara.sdk.exceptions module, these are listed below.

Exception Description
NextIterationException When raised, the Engine ignore any remaining actions and continue from the next iteration.
TerminateException When raised, the Engine terminates the test. The exception message is added to the test’s Activity Log.
TimeoutException Raised in case communication with the target has timed out.
MonitorException Usually raised by monitors. When raised it will result in a new Finding within the report. The string passed to the exception when instantiated will be included under the “Monitor Details” tab of the Finding.
PatternMatchFailConditionException Raised when the response from the target matches a user-provided regular expression. When raised, The Engine stops test execution.
PatternMatchFailConditionWithReportException Same as above but results in the generation of an issue entry within the test report.
PatternMatchNextConditionException Raised when the response from the target matches a user-provided regular expression. When raised, the Engine ignore any remaining actions and moves to the next Field.
PatternMatchNextConditionWithReportException Same as above but results in the generation of an issue entry within the test report.
PatternMatchSkipConditionException Raised when the response from the target matches a user-provided regular expression. When raised, the engine ignores any remaining actions and moves to the next iteration.
PatternMatchSkipConditionWithReportException Same as above but results in the generation of an issue entry within the test report.
PatternMatchPauseConditionException Raised when the response from the target matches a user-provided regular expression. When raised, the Engine pauses test execution.
PatternMatchPauseConditionWithReportException Same as above but results in the generation of an issue entry within the test report.
PatternMatchReportConditionException Raised when the response from the target matches a user-provided regular expression. When raised, the Engine creates an issue entry within the report that includes the exception message.

For example, the NextIterationException exception can be imported as shown below.

from guardara.sdk.exceptions.NextIterationException import NextIterationException

Pattern Match Exceptions

The exceptions whose name starts with PatternMatch should be instantiated with a JSON object, for example:

PatternMatchFailConditionException({
    "rule_name": "",
    "message": ""
})

The rule_name allows setting an arbitrary text to identify the source of the exception. For example, when using standard response processing rules in GUARDARA, the rule_name is set to the name of the response processing rule that triggers raising the exception.

The message is an arbitrary text to be included as the monitor information for the finding within the report.

Example

The following example is a Callback that:

  1. Checks if the next mutation produced by the mutator of the next Send action would generate a value that is longer than MAX_MESSAGE_LENGTH.
  2. If it is:
    2.1. Longer: it will skip mutations until the value generated by the mutator is shorter
    2.2. Shorter: it will override the value to be sent with the next Send action
next_mutator = context.history.get_next_mutator()
# Mutate one as we are interested in the next mutation value.
next_mutator.mutate()
output = "".join(next_mutator.render())
if len(output) < MAX_MESSAGE_LENGTH:
    # Please note, its expected that the overrided value is a string. The 
    # driver should be able to handle both `str` and `list` type in the  
    # `send(..)` method.
    context.send_next(output)
    return
# If the generated value is longer, we keep mutating until we hit a value
# that is of required length.
while len(output) > MAX_MESSAGE_LENGTH: 
    next_mutator.mutate()
    output = b"".join(next_mutator.render())
# Let's overwrite what we are about to send in the next Send actions.
context.send_next(output)
Was this article helpful to you? Yes No