- 25 Apr 2024
- 19 Minutes to read
- PDF
Test and Custom Plugins
- Updated on 25 Apr 2024
- 19 Minutes to read
- PDF
Traceable provides some plugins by default, known as Test Plugins. These plugins are pre-designed to assess and validate the security of your APIs against the most common vulnerabilities. Test plugins are part of the scan policy, and you can use them to perform security checks, identify vulnerabilities, and potential security weaknesses.
In addition to these out-of-the-box plugins, Traceable provides you the option to create custom plugins and upload them to the Test Plugins page. These plugins enable you to implement specific security rules or logic tailored according to your requirements, such as enforcing particular security policies, etc. You must configure these custom plugins in the config.yaml
file. The following section explains the various components of a custom plugin along with the description of the parameters and operators that are required to create this plugin.
Custom Plugins
Default Location — TRACEABLE_HOME
CLI — $HOME/.traceable
Docker — /app/userdata
File Directory | Default Location | Description |
---|---|---|
config.yaml |
| Contains the configuration for AST, such as pre-hooks, post-hooks, and custom plugin definitions. |
custom |
| Contains the custom plugin implementations. |
hooks |
| Contains the pre and post-hooks. |
testsuite (per API) |
| Contains the JSON files that represent test suites. Each file is a suite of tests generated at the moment for a specific API. |
Configuring Custom Plugins
The following section in the config.yaml
file defines the custom plugins to load during the test runs:
custom:
sample_plugin: {}
In the above code snippet, the plugin name is sample_plugin which should match the name of the plugin defined in the custom plugin code placed in the $TRACEABLE_HOME/plugins/custom
directory.
Writing a Custom Plugin
The custom plugin is written in Python and should define some of the mandatory fields for identifying the test case to be performed and the name of the plugin. The following is a sample code snippet for writing a custom plugin:
from traceable.ast.testsuite.assertion import Assertion
from traceable.ast.testsuite.mutation import Mutation
from traceable.ast.testsuite.plugin import Plugin
from traceable.config import logger
from traceable.ast.context import ScanContext
import re
class CustomSamplePlugin(Plugin):
# Required fields - See table below for description of the fields.
###############################################################################
# This is the category of the plugin and is used to group plugins
category = "custom" # Can't be changed
name = "SamplePlugin" # Name of the plugin
title = "Custom Sample Plugin" # Title of the plugin used to display in the vulnerability
# Description about what the plugin does used to display in the vulnerability
description = "This is a description text for vulnerability generated by sample plugin"
# Mitigation for the vulnerability this plugin finds
mitigation = "This is a mitigation text for vulnerability generated by sample plugin"
# Severity of the vulnerability this plugin finds
severity = "CRITICAL"
# CVSS vector string for the vulnerability this plugin finds. Score is calculated automatically
# https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=AV:A/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L
cvssVectorString = "AV:A/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L"
# Optional Fields - See table below for description of the fields.
###############################################################################
displayName = "Broken Object Level Authorization Plugin" # Display name for plugin in UI
affectedEntityType = "API" # API/SERVICE/CUSTOM
impact = "This is a impact text for vulnerability generated by sample plugin"
references = "This is a references text for vulnerability generated by sample plugin"
estimatedFixTime = 1.0
estimatedBountyValue = 100
tags = {
"author": "Tester",
}
def __init__(self, scanctx: ScanContext):
super().__init__(scanctx)
# This will be called per testsuite generated per API call
def run(self):
# Logic to run the plugin only once per api per scan. Comment the following to run for each invocation
if self.scanctx.get(self.name + self.api_name, False):
return
else:
self.scanctx[self.name + self.api_name] = True
logger.info("Running sample custom plugin")
# Your custom generator here
for role in ["sampleuser", "guestuser"]:
attribues_suseptible_to_sample = self.attributes.get("mutated.http.request.*(?i:id|title|path\.param).*", regex=True)
for attr in attribues_suseptible_to_sample:
test = self.create_test_case(vector="Accessing resource using other user on %s" % role, onattribute=attr.key)
# set test attributes. By default the attributes are inherited from the parent plugin if not set
# test.set_attributes(self.attributes)
# TODO
# Your custom evaluation logic here
#
# Mutation function signature:: create(operator: str, key: str, value: any = "") - See table below for details around this function.
test.mutations = [
# Set the sampleuser/guestuser creds in prehooks to be available for all plugins or you can set it here
Mutation.create("MUTATION_SET", "mutated.http.request.header.authorization", "${mutated.role.%s}" % role),
Mutation.create("MUTATION_SET", attr.key, attr.value),
]
# Assertion function signature:: create(key: str, operator: str, mutated: str, original: str, **kwargs) - See table below for details around this function.
# Assertion function signature:: create_logical_assertion(operator: str = "OR|AND", assertions: List[Assertion]) - See table below for details around this function.
assertions_or_group = Assertion.create_logical_assertion("LOGICAL_OPERATOR_OR", [
Assertion.create("mutated.http.response.body", "MATCH_OPERATOR_KEYS_EQUALS", "${mutated.http.response.body}", "${original.http.response.body}"),
Assertion.create("mutated.http.response.body", "MATCH_OPERATOR_FUZZY_EQUALS", "${mutated.http.response.body}", "${original.http.response.body}", data={"level": "5", "threshold": "0.9"}),
Assertion.create("mutated.http.response.body", "MATCH_OPERATOR_VALUES_SEARCH", "${mutated.http.response.body}", attr.value, data={"level": "5"}),
])
test.assertions = [
assertions_or_group,
Assertion.create("mutated.http.response.code", "MATCH_OPERATOR_EQUALS", "${mutated.http.response.code}", "${original.http.response.code}"),
]
test_result = test.run(self.attributes)
# Upload the test result to the server
self.upload_result(test_result)
# OR you can use the following to force the result
# // ok = True/False (True = success, False = failure/vulnerability)
# ok = True
# self.submit_test_results(test, ok)
The following sections specify the description of various fields present in the above code, along with the operators that you can use in them:
Required Fields
Field | Description |
---|---|
category | For a custom plugin, the value of this field should always be “custom”. |
name | The name of the custom plugin. The name you specify here should match the one in the For example, in the above code:
Similarly, in the config.yaml file:
|
title | The plugin title you want to display in the vulnerability. |
description | The description of the vulnerability identified using the plugin. |
mitigation | The steps for mitigating the vulnerability that the plugin finds. |
severity | The severity of the vulnerability that the plugin finds. The severity can be either UNSPECIFIED, CRITICAL, HIGH, MEDIUM, or LOW. |
cvss_vector_string | The string specifies the attributes of the vulnerability, along with the attack vector, complexity, privileges required, and impact. These values are used to calculate the severity score. For more information, see CVSS Vector Strings. |
Optional Fields
Field | Description |
---|---|
displayName | The name of the plugin should be visible in the Traceable platform user interface. |
affectedEntityType | This field should have the value “CUSTOM”. |
impact | A description of the impact of the vulnerability that the plugin finds. |
references | The list of references for the vulnerability generated by the plugin. |
estimated_fix_time | The estimated fix time for the vulnerability that the plugin finds. |
estimated_bounty_value | The estimated bounty value in case the vulnerability is exploited. |
tags | The tags associated with the plugin. |
Attributes Convention
Attributes in a custom plugin are the configurable parameters that determine how the plugin should operate and process API requests and responses.
While the attributes starting with original.
correspond to the requests Traceable is receiving from you or that are being generated from other sources, the attributes starting with mutated.
represent the modified traffic from Traceable.
All attributes are stored in the form of their fully qualified name. For example, the authorization header is represented as original.http.request.header.authorization
in the original request and mutated.http.request.header.authorization
in the mutated request.
The following is the common list of attributes that you can use while writing a custom plugin:
Common List of Attributes
(original/mutated).http.request.method: "POST"
(original/mutated).http.request.url: "http://example.com?arg=val1&arg2=val2"
(original/mutated).net.host.scheme: https
(original/mutated).net.host.name: "example.com"
(original/mutated).net.host.port: 443
(original/mutated).http.request.header.*: <All header keys are in lowercase>
(original/mutated).http.request.header.user-agent: "curl/1.1"
(original/mutated).http.request.header.accept: "application/json"
(original/mutated).http.request.header.content-type: "application/json"
(original/mutated).http.request.cookie.PHPSESSID: "064d213baaef028e724b06042a69561dceb256f3119721b846234d72dcc35134"
(original/mutated).http.request.query.param.limit: 100
(original/mutated).http.request.body: '{"username": "user1", "password": "p4s1d135DDg4"}'
(original/mutated).http.request.body.username: "user1"
(original/mutated).http.request.body.password: "user1"
(original/mutated).http.response.code'
(original/mutated).http.response.duration
(original/mutated).http.response.header.*: <All header keys are in lowercase>
(original/mutated).http.request.header.content-type: "application/json"
(original/mutated).http.response.cookie.PHPSESSID: "064d213baaef028e724b06042a69561dceb256f3119721b846234d72dcc35134"
(original/mutated).http.response.body: '{"token": "jshdlahflafnfbdfbkfnalfja", "msg": "Success"}'
(original/mutated).http.response.token: "jshdlahflafnfbdfbkfnalfja"
(original/mutated).http.response.msg: "Success"
Attribute Operators
The following are the supported operators and their descriptions that you can use while writing a custom plugin:
Operator | Description |
---|---|
attributes.add_attributes(attributeList, prefix: str = "") | This operator is used to add attributes from another attribute list. It adds new values without modifying the existing ones. |
attributes.set_attributes(attributeList, prefix: str = "") | This operator is used to set attributes from another attribute list. It modifies any existing values or creates them if they don’t exist. |
attributes.get_one(key, , default: any = None, use_prefix: bool = False, regex: bool = False) | This operator is used to fetch one value from the attribute. |
attributes.get(key, : str, default: any = None, use_prefix: bool = False, regex: bool = False) | This operator is used to fetch values from the attribute. |
attributes.get_one_attr(key, , default: any = None, use_prefix: bool = False, regex: bool = False) | This operator is used to fetch one attribute. |
attributes.get_original(key, , default: any = None, use_prefix: bool = False, regex: bool = False) | This operator is used to fetch the original value from the attribute. |
attributes.items(self) | This operator is used to fetch the attribute list iterator. |
attributes.add(key, : str, value: any, original_and_mutated: bool = False, is_dirty: bool = False, force: bool = False) | This operator is used to add attributes to the list. If original_and_mutated is True, both original and mutated keys are added; otherwise, only one copy without a prefix is added. |
attributes.modify(key, : str, value: any, first_only: bool = False, regex: bool = False) | This operator is used to update all attributes with the specified key. If first_only is True, only the first attribute is updated. If the key is not found, the instruction is ignored. |
attributes.set(key, : str, value: any, original_and_mutated: bool = False, regex: bool = False, is_dirty: bool = False, force: bool = True) | This operator is used to set attributes to the list. If original_and_mutated is True, both original and mutated keys are modified or are created if they don’t exist; otherwise, only one copy without a prefix is modified or created. |
attributes.expand_attributes(self, data, regex: bool = False, user_prefix: bool = False) | This operator is used to expand templated variables to fetch the attribute value. |
attributes.expand(self, data, regex: bool = False, use_prefix: bool = False) | This operator is used to expand templated variables to fetch the attribute value. For example, if the input is mutated.${authorization_header} and the authorization_header is request.header.jwt, then the output is mutated.request.header.jwt. |
attributes.delete(key, value, regex: bool = False) | This operator is used to delete attributes from the attribute list. |
Mutation Function
The mutation function adds, modifies, and deletes specific parameters within API requests.
Syntax — Mutation.create(operator: str, key: str, value: any = ””)
The function contains the following parameters:
operator (String) — The type of operation to be performed.
key (String) — The key or attribute to be mutated.
value (Any) — The new value to replace the existing value. This parameter can contain any value, for example, none or an empty string (““).
Mutation Operators
The following are the supported operators that you can use along with the mutation function while writing a custom plugin:
Operator | Description |
---|---|
MUTATION_ADD | This operator adds a new key attribute with the specified value. If the key attribute already exists, it duplicates it. Example:
This adds two query params in the url resulting in ?id=100&id=50. This is typically used to test parameter pollution. |
MUTATION_DELETE | This operator deletes an existing key attribute. Example:
This removes the query param id from the API request. |
MUTATION_MODIFY | This operator modifies a key attribute if it exists; otherwise, it does nothing. Example:
This updates the query param id with value 100 if it exists; otherwise, it ignores the instruction. |
MUTATION_SET | This operator sets the value of an existing key attribute or adds it if it does not exist. Example:
This sets the query param id to 100 if it already exists; otherwise, it creates a query param id with the same value. |
MUTATION_REGEX_DELETE | This operator deletes all attributes that match the key/regular expression with value. Example:
This deletes all query params ending with the string “amount”. |
MUTATION_REGEX_MODIFY | This operator modifies all attributes that match the key/regular expression with value. Example:
This modifies the query param ending with the string “amount” with value 100, if it exists; otherwise, it ignores the instruction. |
MUTATION_REGEX_SET | This operator sets all attributes that match the key/regular expression with value. Example:
This sets the query param ending with the string “amount” with a value of 100, if it exists; otherwise, it ignores the instruction. |
Logical Assertion Function
The logical assertion function is used to apply conditional operators (AND/OR) to a list of assertions.
Syntax — create_logical_assertion(operator: str = "OR|AND", assertions: List[Assertion]): VULN(True)/NOT_VULN(False)
The function contains the following parameters:
operator (String) — The logical operator used to combine multiple assertions. The default operator is OR; however, it can be set to AND.
assertions (List[Assertion]) — A list of assertion objects to be combined using the logical operator.
Vulnerable Logical Assertion Operators
The following are the supported operators that you can use along with the logical assertion function while writing a custom plugin:
Operator | Description |
---|---|
LOGICAL_OPERATOR_AND | All assertion items in the assertion list need to be VULN(True) for the return value to be VULN(True). |
LOGICAL_OPERATOR_OR | Any assertion item in the assertion list needs to be VULN(True) for the return value to be VULN(True). |
Assertion Create Function
Assertions are used to evaluate the scans’ responses for vulnerabilities.
Syntax — Assertion.create(key: str, operator: str, mutated: str, original: str, **kwargs)
The function contains the following parameters:
key (String) — Used for display purposes as part of visualization on the UI. It is not used for any processing.
operator (String) — The match operator used for comparison.
mutated (String) — The mutated value for comparison.
original (String) — The original value for comparison.
**kwargs (optional) — Additional parameters used to provide metadata for the assertion. These parameters can vary depending on the match operator used.
Assertion Operators
The following are the supported operators that you can use along with the assertion create function while writing a custom plugin:
Operator | Description |
---|---|
MATCH_OPERATOR_EQUALS | This operator checks whether the mutated value matches the original value. If it does, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the response code received from the mutated request matches the response code received from the original request. |
MATCH_OPERATOR_NOT_EQUALS | This operator checks whether the mutated value does not match the original value. If it does not, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if param1 received in the body of the response received for the mutated request does not match param1 in the body of the response received from the original request. |
MATCH_OPERATOR_MATCHES_REGEX | This operator checks whether the mutated value matches the regex. If it does, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the response code received for the mutated request matches the specified regular expression. |
MATCH_OPERATOR_NOT_MATCHES_REGEX | This operator checks whether the mutated value does not match the regex. If it does not, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the response code received for the mutated request does not match the specified regular expression. |
MATCH_OPERATOR_GREATER_THAN | This operator checks whether the mutated value is greater than the original value. If it is, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the time taken to receive a response to the mutated request takes longer than 100 ms. |
MATCH_OPERATOR_GREATER_THAN_EQUALS | This operator checks whether the mutated value is greater than or equal to the original value. If it is, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if param1 in the body of the response received for the mutated request has a value greater than or equal to 100. |
MATCH_OPERATOR_LESS_THAN | This operator checks whether the mutated value is less than the original value. If it is, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if param1 in the body of the response received for the mutated request has a value less than 100. |
MATCH_OPERATOR_LESS_THAN_EQUALS | This operator checks whether the mutated value is less than or equal to the original value. If it is, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if param1 in the body of the response received for the mutated request has a value less than or equal to 100. |
MATCH_OPERATOR_CONTAINS | This operator checks whether the mutated value contains the original value. If it does, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if param1 in the body of the response received for the mutated request contains the key word “Unauthorized”. |
MATCH_OPERATOR_NOT_CONTAINS | This operator checks whether the mutated value does not contain the original value. If it does not, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. It also supports various python data structures, such as list, set, and string. Example:
This evaluates to True if the body of the response received for the mutated request does not contain the key word “Bad request”. |
MATCH_OPERATOR_IN | This operator checks whether the mutated value is present in the original value. If it is, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the message in the body of the response received for the mutated request includes either “Failed” or “Errored”. |
MATCH_OPERATOR_NOT_IN | This operator checks whether the mutated value is not present in the original value. If it is not, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the message in the body of the response received for the mutated request does not include either “Failed” or “Errored”. |
MATCH_OPERATOR_KEYS_EQUALS | This operator checks whether the mutated and original values are JSON/URL-encoded parseable and have the same keys. If they do, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the keys in the body of the response received for the mutated request has the same set of keys in the body of the response received for the original request - up to a depth of 5 for the JSON encoded tree. |
MATCH_OPERATOR_KEYS_NOT_EQUALS | This operator checks whether the mutated and original values are JSON/URL-encoded parseable and have the same keys. If they do, the assertion evaluates to False/Not Vulnerable; otherwise, it evaluates to True/Vulnerable. Example:
This evaluates to True if the keys in the body of the response received for the mutated request does not have the same set of keys in the body of the response received for the original request - up to a depth of 5 for the JSON encoded tree. |
MATCH_OPERATOR_FUZZY_EQUALS | This operator checks whether the mutated and original values match up to the specified threshold. If they do, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the body of the response received for the mutated request matches (at least up to the threshold value of 80%) with the response received for the original request - up to a depth of 5 for the JSON encoded tree. |
MATCH_OPERATOR_FUZZY_NOT_EQUALS | This operator checks whether the mutated and original values do not match up to the specified threshold. If they do not, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the body of the response received for the mutated request does not match (at least up to the threshold value of 80%) with the response received for the original request - up to a depth of 5 for the JSON encoded tree. |
MATCH_OPERATOR_FUZZY_KEYS_EQUALS | This operator checks whether the mutated and original values are JSON/URL-encoded parseable and the keys match up to the specified threshold. If they do, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the keys in the body of the response received for the mutated request matches (at least up to the threshold value of 80%) with the keys in the body of the response received for the original request - up to a depth of 5 for the JSON encoded tree. |
MATCH_OPERATOR_FUZZY_KEYS_NOT_EQUALS | This operator checks whether the mutated and original values are JSON/URL-encoded parseable and the keys match up to the specified threshold. If they do, the assertion evaluates to False/Not Vulnerable; otherwise, it evaluates to True/Vulnerable. Example:
This evaluates to True if the keys in the body of the response received for the mutated request does not match (at least up to the threshold value of 80%) with the keys in the body of the response received for the original request - up to a depth of 5 for the JSON encoded tree. |
MATCH_OPERATOR_VALUES_DIFFERENCE | This operator checks if the absolute difference between the mutated and original value is a certain amount. Example:
This evaluates to True if the query param id of the mutated request differs from the response parameter id by a value of 100. |
MATCH_OPERATOR_VALUES_SEARCH | This operator searches for the original value in the mutated value. The mutated value can be a dictionary or a URL-encoded payload. The level specified in this operator signifies the max depth till which Traceable should look at in the tree structure like JSON. If the original value is found in the mutated one, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the body of the response received for the mutated request contains the value 15 - up to the depth 5 for the JSON encoded tree. |
MATCH_OPERATOR_VALUES_NOT_SEARCH | This operator searches for the original value in the mutated value. The mutated value can be a dictionary or a URL-encoded payload. The level specified in this operator signifies the max depth till which Traceable should look at in the tree structure like JSON. If the original value is not found in the mutated one, the assertion evaluates to True/Vulnerable; otherwise, it evaluates to False/Not Vulnerable. Example:
This evaluates to True if the body of the response received for the mutated request does not contain the query param id of the original request - up to a depth of 5 for the JSON encoded tree. |
MATCH_OPERATOR_REGEXLOOKUP_EQUALS | This operator works in two steps:
Example:
This evaluates to True if all parameters that match the specified regex in response to the mutated request contains the value 15. |
MATCH_OPERATOR_REGEXLOOKUP_NOT_EQUALS | This operator works in two steps:
Example:
This evaluates to True if all parameters that match the specified regex in response to the mutated request does not contain the value 15. |
MATCH_OPERATOR_REGEXLOOKUP_MATCHES_REGEX | This operator works in two steps:
Example:
This evaluates to True if all parameters that match the specified regex in response to the mutated request contains a value of that matches the regex ("141[a-z0-9]{1,10}411"). |
MATCH_OPERATOR_REGEXLOOKUP_NOT_MATCHES_REGEX | This operator works in two steps:
Example:
This evaluates to True if all parameters that match the specified regex in response to the mutated request does not contain a value of that matches the regex ("141[a-z0-9]{1,10}411"). |
MATCH_OPERATOR_ALWAYS_TRUE | This operator always returns True/Vulnerable. It is mostly useful when you are writing plugins and want to test dummy assertions. Example:
|
MATCH_OPERATOR_ALWAYS_FALSE | This operator always returns False/Not Vulnerable. It is mostly useful when you are writing plugins and want to test dummy assertions. Example:
|
MATCH_OPERATOR_COLLABORATOR_LOOKUP_CONTAINS | This operator is used for SSRF-based scans, where if the application reaches out to Traceable collaborator, it looks out for the presence of test id in the payload received by the collaborator. Example:
This evaluates to True if the payload (from the application) received by the collaborator contains the test id. |
MATCH_OPERATOR_RAW | This operator executes raw lambda code supplying it with mutated and original values. If lambda returns True, it is marked as Vulnerable, else Not Vulnerable. Example:
|
Uploading a Custom Plugin
You can upload a plugin on the Traceable platform to add it to the Testing Plugins page. To do so, navigate to Testing → Test Plugins, and complete the following steps:
In the page’s top right corner, click on Upload Plugin.
In the Upload Custom Plugin window, complete the following:
Specify the plugin Name.
Specify your custom plugin code in the code block or click on Import from file and select the code file according to your requirements.
Click on Save.