Flows
Overview

The Flows module is a comprehensive, end-to-end automation engine powered by AFScript.
Flows can help you to automate AttackForge with nearly unlimited systems. You can streamline processes across your organization to save time and focus on what's important.
Some examples you can do with Flows:
Integrate your vulnerability data with ticketing tools like Atlassian JIRA, ServiceNow, Azure DevOps, BMC Helix and others.
Help make better risk decisions by sending your vulnerability data to GRC platforms like RSA Archer, MetricStream, OneTrust and LogicGate.
Create workflow automations by chaining together AttackForge Self-Service APIs
Prioritize vulnerabilities with threat-intelligence like VulnDB
Create custom webhooks.
Send custom email notifications on events.

Getting Access to Flows
Flows is included in all AttackForge Enterprise plans, and in the AttackForge Core SME plan. For all others plans, Flows can be add-on from the Administration -> Subscriptions
page.
To get started with building a Flow:
You must have Create access to Event Triggers or HTTP Triggers.
As an Administrator, go to Users > (Select User) > Access > Flows
and enable Create access to the desired triggers.

Event Trigger Access Level
None - user is unable to create or import flows with the Event Trigger type. Any existing Event Trigger flows for which they are the owner of will also not run.
Run - user is able to trigger flows with the Event Trigger type they are the owner of, so long as the flow is enabled and they have access to the matching event type.
Create - user is able to create or import flows with the Event Trigger type. Any existing Event Trigger flows for which they are the owner of will run so long as the flow is enabled and the user has access to the matching event type.
HTTP Trigger - Authentication (None) Access Level
HTTP Triggers can be configured with no authentication. This means that the flow will not check for authentication before the flow runs.
None - user is unable to create or import flows with the non-authenticated HTTP Trigger type. Any existing non-authenticated HTTP Trigger flows for which they are the owner of will also not run.
Run - user is able to trigger flows with the non-authenticated HTTP Trigger type they are the owner of, so long as the flow is enabled.
Create - user is able to create or import flows with the non-authenticated HTTP Trigger type. Any existing non-authenticated HTTP Trigger flows for which they are the owner of will run so long as the flow is enabled.
HTTP Trigger - Authentication (User API Key) Access Level
HTTP Triggers can be configured with authentication. This means that the flow will check for authentication before the flow runs.
None - user is unable to create or import flows with the authenticated HTTP Trigger type. Any existing authenticated HTTP Trigger flows for which they are the owner of will also not run.
Run - user is able to trigger flows with the authenticated HTTP Trigger type they are the owner of, so long as the flow is enabled and an authorized User Key is supplied in the nominated header.
Create - user is able to create or import flows with the authenticated HTTP Trigger type. Any existing authenticated HTTP Trigger flows for which they are the owner of will run so long as the flow is enabled and an authorized User Key is supplied in the nominated header.
IMPORTANT: Administrators have implicit Create on all Triggers. This is non revokable.
Flow Overview
A Flow is comprised of the following:
Name - the name of the Flow.
Secrets - any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.

Run Overview
A Run refers to a single execution of a Flow, meaning when a set of actions defined in your Flow is triggered and carried out from start to finish, that is considered one "Run" of the flow; essentially, it's a single instance of your Flow being executed.
Key points about a Run:
Triggered by a Trigger - A Run is initiated by a Trigger, like a new vulnerability or an update to a project, or a manual action.
Trackable status - You can monitor the status of a Run, including whether it succeeded, failed, or is currently running.
Provides details - Each Run has details like start time, duration, and the specific Actions taken within the Flow.
IMPORTANT: A normal Run will only be executed in the context of the Flow Owner and related Trigger. For example, if the Trigger was "vulnerability-created" - the Run will only initiate for the vulnerability for which the Flow Owner has access to the vulnerability. Test runs can be manually executed with test data at any time.


Sharing Flows with Teams
When a Flow is created, it belongs to the user who created the Flow (the Flow Owner). Flow Owners can be transferred however a Flow can only ever run under the context of a single user.
Only Flow Owners are allowed to share their Flows with other users.
To share your Flow:
Open your Flow and click on the
Settings
button

Click on
Add Access

Insert the user's email address, or if permitted, look up and select the user. Assign an access level to the flow.

Access Levels
None - the user explicitly does not have access to the flow.
Info - the user is able to view basic information about the flow, including:
Name of the flow
Current status, last run, trigger, enabled/disabled
Flow owner
Readme
If HTTP Trigger - HTTP Method, Trigger URL, Trigger Id
View - the user is able to access read-only information for the flow, including:
Name of the flow
Current status, last run, trigger, enabled/disabled
Flow owner
Readme
If HTTP Trigger - HTTP Method, Trigger URL, Trigger Id
View all Runs
View all Actions
View Run details and logs
Export the flow
Edit - the user is able to modify the flow, including:
Name of the flow
Current status, last run, trigger, enabled/disabled
Flow owner
Readme
If HTTP Trigger - HTTP Method, Trigger URL, Trigger Id
View all Runs
View all Actions
View Run details and logs
Edit the flow
Save and Run the flow
Export the flow
Disable the flow
Trigger Access
Trigger access is used for controlling authentication to triggering a Flow Run using authenticated HTTP Triggers based on the User Key supplied in the nominated header.
No - user is not able to trigger the flow.
Yes - user is able to trigger the flow provided they enter their valid User Key into the flows' nominated authentication header.
IMPORTANT: Administrators have implicit permissions to view access for all flows.
Triggers
A Trigger is an action which initiates a Run. Triggers can be initiated from Events or manually initiated.

Internal Events
Internal Events (or Event Triggers) are events which occur when something inside AttackForge changes.
The following Internal Events are currently supported:
Project Created
Project Updated
Project Request Created
Project Request Updated
Project Retest Requested
Project Retest Completed
Project Retest Cancelled
Vulnerability Created
Vulnerability Updated
Vulnerability Remediation Note Created
Vulnerability Remediation Note Updated
Vulnerability Evidence Created
Vulnerability Evidence Updated
Access to events can be granted by Administrators in Users > (Select User) > Access > Events (Flows / Self Service API)
External Events
External Events (or HTTP Triggers) are events which occur when something outside of AttackForge changes.
For example, if an update happens in an external system - that system can use Webhooks to send the message to AttackForge in real-time.
External Events can also be called from within any other Flow, creating possibilities for modularisation of your flows.
Time-Based Events
Time-Based Events are events which occur at a specified time for example each day at 9am, or on a specified frequency for example every hour. Time-Based Events are particularly useful when something needs to happen on a automated time basis.
For example, each day - find all vulnerabilities which have just exceeded their risk-acceptance date, change their status to open, create a ticket in an external system and notify the vulnerability owner(s) and security team by email and by chat message.
Time-Based Events are coming soon!
Assigning Events
A Flow can be assigned to only one Trigger.
Triggers can be assigned to a Flow when either creating or editing the Flow.



HTTP Trigger - Authentication
External Events (HTTP Triggers) can be either authenticated or non-authenticated.
Authentication is a way to protect the flow by ensuring that the user who is sending data to the Trigger URL is authorized to do so. Authentication is recommended where it is practical to implement on the system sending data to AttackForge, via custom headers.
An authenticated HTTP Trigger flow will check for authentication before the flow runs.
A non-authenticated HTTP Trigger flow will not check for authentication before the flow runs.
Authentication is configured as a custom header. You have control over what the header name should be.
A user interacting with an authenticated HTTP Trigger flow must include their User Key in the specified header. They must also be granted access to the Flow with Trigger access, unless they are the Flow Owner whom has implicit access to trigger their flows.

HTTP Trigger URL
After creating a HTTP Trigger flow, you will receive the Trigger URL and Trigger Id. The Trigger URL is the URL which your external systems and scripts will use to interact and send data to your flow.
The Trigger URL and Trigger ID are unique for every flow.

IMPORTANT: For non-authenticated HTTP Trigger flows, protect your Trigger URL and Trigger ID as if it was a secret. Without authentication, anybody who knows the URL and HTTP Method can attempt to trigger your flow.
You can rotate your Trigger URL and Trigger ID by clicking on the regenerate button:

Trigger Configuration
Triggers have different configuration options which are supported.

Redact user API Key - this option can be used to prevent anybody working on an authenticated HTTP Trigger flow from gaining access to the User Key supplied in the authentication header, such as through logging.
Redact all headers except specified - this option can be used to specify a whitelist of headers which are allowed to be passed into the flow. This option is useful to prevent anybody working on an authenticated HTTP Trigger flow from gaining access to headers which they should not have access to, for example session tokens from external systems.
Redact specified headers - this option can be used to specify a blacklist of headers which will not be passed into the flow. This option is useful to prevent anybody working on an authenticated HTTP Trigger flow from gaining access to headers which they should not have access to, for example session tokens from external systems.
Secrets
Secrets are any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.
You can create Secrets which belong to the Flow. Only users with access to the Flow would be able to view the associated Secrets.
You can also reference Secrets which belong to users. User Secrets make it easy to rotate passwords and credentials without having to update flows.
To create a Secret, start by clicking on the Secrets
button when creating or editing a Flow.

From here, you can see and manage all of the existing Secrets associated to the Flow.
Click on Add Secret
to create a new Secret.

Note the Key must be letters, numbers and underscores only.
You can create a Local Secret:

Or select from one of your User Secrets:

You can also view, manage and create secrets in the Request Script and in the Response Script:

NOTE: Secrets are stored encrypted in the database.
There are two (2) ways in which you can refer to your Secrets in your Flow:
Select the Secret directly in the Headers
Refer to the Secret in the Request Script or Response Script
Secrets in Headers
When creating or modifying Headers within the Action, you can select 'Secret' for the header type. This will then allow you to select from an existing Secret, or create a new Secret.


Secrets in Request/Response Scripts
When creating or modifying the Request Script or the Response Script, you can refer to secrets using the following syntax:
secrets.<KEY>
Where <KEY>
is replaced with the Key associated with the Secret.
IMPORTANT: Make sure to select
Use Secrets
to ensure your secrets are used in your script.

Actions
Actions are either one activity, or a sequence of activities, which are executed in order during a Run.
For example, if the use case for your Flow is:
to create a JIRA Issue every time a Vulnerability is created
You may choose to include two (2) Actions in your Flow:
Action 1 - Create JIRA Issue
This involves formatting the vulnerability into the necessary JIRA Create Issue API format, and making a HTTPS request to the JIRA API to create the issue.
Action 2 - Update Vulnerability with JIRA Issue Key
This involves making a HTTPS request to the Update Vulnerability Self-Service API to set the JIRA Issue Key custom field.
The primary purpose of an Action is to make an update to a system. The system could be AttackForge (via the Self-Service APIs) or an external system.
Every Action is made up of a Request and a Response.
The Request is the HTTP request which is made by the Action.
The Response is the HTTP response from the server which received the HTTP request.
Every Action is made up of the following components:
Verify Certificate - this determines whether to verify if the TLS certificate is valid for the URL.
Request Script - this is the script which will execute before the Request is made.
Response Script - this is the script which will execute after the Response is returned.
IMPORTANT: When more than one Action is included in a Flow, the output of an Action will become the input into the next Action.

Methods
Methods are the HTTP methods/verbs that will be used for the Request i.e. GET, POST, PUT, etc.
The following methods are supported:
GET
POST
PUT
PATCH
DELETE
Methods can be selected when editing the Action:

Methods can also be programatically set in your Request Script in the Return Statement:
return {
decision: {
status: 'continue'
},
request: {
url: url,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': secrets.jira_auth
},
body: {
fields: {
summary: summary,
description: description,
priority: {
name: priority
},
issuetype: {
name: 'Bug'
},
labels: labels
}
}
}
};
URL
The URL is the web address that will be used for the Request, for example https://acmecorp.atlassian.net/rest/api/2/issue
The URL can be entered in when editing the Action:

The URL can also be programatically set in your Request Script in the Return Statement. This is useful if your URL has a dynamic component which needs to be computed:
const url = 'https://demo.attackforge.com/api/ss/vulnerability/' + data.vulnerability_id;
return {
decision: {
status: 'continue'
},
request: {
url: url,
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-ssapi-key': secrets.af_auth
},
body: {
project_id: project_id,
custom_fields: [
{
key: 'jira_issue_key',
value: jira_issue_key
}
]
}
}
};
Headers
The Headers are the HTTP headers that will be sent when the Request is made.
The Headers can be manually entered in when editing the Action:

The Headers can also be programatically set in your Request Script in the Return Statement. This is useful if your Headers have a dynamic component which needs to be computed:
const customHeaderName = 'X_CUSTOM_HEADER_' + customHeader;
const customHeaderValue = customValue;
return {
decision: {
status: 'continue'
},
request: {
url: url,
method: 'POST',
headers: {
'Content-Type': 'application/json',
customHeaderName: customHeaderValue,
'Authorization': secrets.custom_auth
},
body: {
super_secret_something: "..."
}
}
};
Request
The Request is a HTTP/HTTPS request to a web address.
The Request is made up of the following components:
Body - an optional HTTP body. This is typically required for POST, PUT and PATCH HTTP requests.
IMPORTANT: Requests can be made over both HTTP and HTTPS.
Response
The Response is the HTTP server response to a Request.
The Response is made of the of the following components:
Request Script
The Request Script is the script which will execute before the Request is made.
The Request Script is made up of the following components:
Response Script
The Response Script is the script which will execute after the Response is returned.
The Request Script is made up of the following components:
Code
Flows support AFScript - a powerful interpreted programming language created by AttackForge.
This makes it possible to write logic to help you handle all various use cases for how you want your Flows to work.

You can take advantage of Logging in AFScript to help you to debug and test your code.

You can test and debug your code using the Run
option:

If your code fails after running it, you will see an error message with the relevant stack trace:

Data
Data is contextually relevent information for your Request Script and Response Script.
You can reference the information included within Data as follows:
data.<KEY>
Where <KEY>
is replaced with the associated key on the Data Object.
For more information on Data, please see Data Object.
The Data Object
Data is contextually relevant information for your Request Script and Response Script.
The Data Object is an Object that holds the Data.
The first Action in your Flow will contain Data in the Request Script which is relevent to your Trigger Event. For example, if your Flow was assigned to the "vulnerability-created" Event, then your Data Object will contain all of the information relating to the vulnerability.
However from this point forward, you can control how you would like your Data Object to look for the Response Script and any subsequent Actions going forward.
In the following example, we can see that Data Object has vulnerability-related information due to the "vulnerability-created" Event.

You can refer to keys on the Data Object using the following syntax:
data.<KEY>
Using the example above, if you wanted to store the vulnerability Id in a constant, you could do the following:
const vuln_id = data.vulnerability_id;
Keeping with the example above, if you wanted to extract the project Id for the vulnerability, you could do the following:
let project_id = undefined;
if (data.vulnerability_projects) {
for (let x = 0; x < data.vulnerability_projects.length; x++) {
if (data.vulnerability_projects[x].id) {
project_id = data.vulnerability_projects[x].id;
}
}
}
If you needed to pass this information to the next step of this Flow, which using the example above will be the Response Script on the first Action - you can include the "data" key in your Response Object and pass in an Object with key/value pairs as follows:
return {
decision: {
status: 'continue'
},
data: {
af_project_id: project_id,
af_vuln_id: vuln_id,
af_vuln: data
}
}
Continuing with this example, the Response Script will now have the following Data Object:
data = {
af_project_id: "...",
af_vuln_id: "...",
af_vuln: {
"vulnerability_title": "...",
...
}
}
When viewing the details of a Run - you can see what Data was passed as input and output into an Action.


If you would need to log the Data Object for visibility or debugging during execution of a Run you can do the following:
Logger.debug('Data:');
Logger.debug(JSON.stringify(data));
You can then view the details in the Run Logs

The Response Object
The Response Object is the HTTP information which is sent back from the server during the Response.
The Response Object is available in the Response Script.
The Response Object is made up of the following:

You can refer to keys on the Response Object using the following syntax:
response.<KEY>
An example of the Response Object:
response = {
"statusCode": 200,
"headers": {},
"body": ""
}
The statusCode will be accessible as a Number.
The headers will be accessible as a Object.
The body will be accessible as a String.
If you are expecting the body to be returned as a JSON payload (which is common for RESTful APIs) - you must first parse the body into JSON format before you can access it using dot or bracket notation, see example below:
const body = JSON.parse(response.body);
When viewing the details of a Run - you can see the Response Status Code and Response Headers and Response Body:

If you would need to log the Response Object for visibility or debugging during execution of a Run you can do the following:
Logger.debug('Response:');
Logger.debug(JSON.stringify(response));
You can then view the details in the Run Logs

The Return Statement
The Return Statement is the action to take for your Request Script and Response Script.

The Return Statement is made up of the following components:
return {
decision: {
status: 'continue',
message: 'Payload is valid, proceed to submit request',
},
request: {
url: 'https://www.attackforge.com/api',
body: {},
headers: {},
method: 'GET',
},
data: {}
};
Decision
return {
decision: {
status: 'continue',
message: 'Payload is valid, proceed to submit request',
}
};
The decision is the action that your script will take. A decision is made up of the following components:
status - a supported status (see below)
message - an optional message to display in the logs
You can also include the decision as a string if you do not need to include a message:
return {
decision: "continue" //also supports "next", "abort", "finish"
};
The following statuses are supported:
CONTINUE
Continue will instruct your script to continue with normal execution. For example, continuing in the Request Script will result in the Request being made. Continuing in the Response Script will result in executing the next Action.
Example using Continue:
return {
decision: {
status: 'continue',
message: 'Payload is valid, proceed to submit request',
}
};
NEXT
Next will instruct your Request Script or Response Script to move to the next Action. This is useful if a Request in your Flow is conditional i.e. it may or may not need to be made.
Example using Next:
return {
decision: {
status: 'next',
message: 'Asset already exists. Move to create vulnerability',
}
};
ABORT
Abort will terminate your Flow as an error condition. This is useful in cases where your Flow can no longer proceed due to various reasons.
Example using Abort:
return {
decision: {
status: 'abort',
message: 'Auth token not generated. Check if credentials are valid?',
}
};
FINISH
Finish will terminate your Flow as an success condition. This is how you would normally terminate a Flow.
Example using Finish:
return {
decision: {
status: 'finish',
message: 'Vulnerability created!',
}
};
Request
The Request is the HTTP Request information that your Request Script will use. Request is made up of the following components:
url
body
headers
method
return {
decision: {
status: 'continue',
message: 'Payload is valid, proceed to submit request',
},
request: {
url: 'https://www.attackforge.com/api',
body: {},
headers: {},
method: 'GET',
}
};
URL
The URL is the URL that the Request will be sent to. This field is optional. If it is not specified, the URL set in the Action will prevail. If it is specified, it will take precedence over the URL set in the Action.
BODY
The Body is the payload body that will be sent in the Request. This field is optional. If it is not specified, no body will be sent in the Request.
Example with a JSON Body:
return {
decision: {
status: 'continue'
},
request: {
body: {
fields: {
project: {
key: jiraProjectKey
},
summary: summary,
description: description,
priority: {
name: priority
},
issuetype: {
name: 'Bug'
},
labels: labels
}
}
}
};
HEADERS
The Headers are the HTTPS Headers that the Request will use. This field is optional. If it is not specified, the Headers set in the Action will prevail. If it is specified, it will take precedence over the Headers set in the Action.
Example Headers:
return {
decision: {
status: 'continue',
},
request: {
headers: {
'Content-Type': 'application/json',
'x-ssapi-key': secrets.af_auth,
}
}
};
METHOD
The Method is the HTTPS Method that the Request will be sent to. This field is optional. If it is not specified, the Method set in the Action will prevail. If it is specified, it will take precedence over the Method set in the Action.
Example Headers:
return {
decision: {
status: 'continue',
},
request: {
method: 'GET' //supports GET, POST, PUT, PATCH, DELETE
}
};
Data
Data is an object that can be used to pass information between Request Script to Response Script, and from Response Script to the next Action.
The Data in the Request Script for the first Action of the Flow will be the Event information, for example "vulnerability-created" fields. From then onwards, you can override what the data will be in the Response Script and beyond.
Example with Data in the Request Script of the first Action in the Flow:
return {
decision: {
status: 'continue',
},
data: {
af_project_id: afProjectId,
af_vuln: data
}
};
Example with Data in the Response Script of the first Action in the Flow. This example will pass on the "af_project_id" and "af_vuln" that was passed in the Data from the Request Script on to the next Action in the Flow.
return {
decision: {
status: 'continue',
},
data: {
af_project_id: data.af_project_id,
af_vuln: data.af_vuln
}
};
Runs
Flow Runs is where you can view the history of each Run you have access to, across all of your Flows. It is a consolidated view for every Run. You can access this page from the Flows module.

You can also view Runs for a specific Flow by clicking on Flows and then clicking on the name of a Flow.


Run Logs
When clicking on a Run, you will see an overview of the history for that Run, including the following information:
Run Status - Whether the Run was Completed or Failed.
Event - the related Event which triggered the Flow.
Started - the timestamp of when the Run started execution.
Duration - the duration (in milliseconds) for execution of the Run until completion or failure.
Data - the input and output of each Action.
HTTP - the URL, Method, Headers, Request Body, Response Status Code and Response Body for each Action in the Flow.
Logs - the logs for each Action.



If you include Logging in your Request Script or Response Script, you will be able to see the logs here during execution of a Flow.

Manually Run Flow
You can manually run a Flow at any time. This is useful for testing your Flow. When you manually run a Flow, the input into the first Action will be test data. You can modify the test data to match your testing needs.



Re-Run
You can manually re-run a Flow at any time. When you Re-Run a Flow, it will execute with exactly the same input data into the first Action.

After a Flow has Re-Run, you will notice the Event Trigger will show that it was Re-Run.

Flow Readme
Every flow can have a readme which helps to detail key information such as:
How the flow works
How to operate the flow
Links to external documentation
Contact information for key persons

You can create and edit the readme when creating or editing the flow:

Importing/Exporting Flows
You can export your existing Flows and import Flows at any time. This utility can help to:
Share Flows with others, without giving them access to your Flows
Create a backup of your Flows
Bootstrap a new Flow based on a template
To export a Flow, click on the Export Flow
button. The Flow will be exported with a .flow
extension format.
IMPORTANT: Exported Flows do not contain the values of Secrets. However for compatibility and convenience, the Secret key/name will be included in the export.


To import a Flow, click on Import Flow
:

Select the Flow you would like to import (.flow file):

You will have an opportunity to review and adjust the Flow prior to saving.

Transferring Flows
Flows run under the context of an Owner. That means, when the event happens for the Owner, the Flow will trigger.
You can transfer ownership of Flows when required, so that the Flow can operate under the context of another user.
To transfer ownership of your Flow - open the Flow settings page and click on Transfer

Select the user or enter in their email address and click Transfer

The user will immediately receive ownership of the Flow, however it will be disabled until they review the Flow and chose to enable it.
Examples
The following examples can help you to automate and integrate with common security and enterprise tools. Each example includes a Flow which can be imported in to your own AttackForge to help you get started fast!
NOTE: These are just some common examples so far. We are constantly adding more Flow examples as we continue to roll out Flows. Remember - Flows can interact with any HTTP interface, including AttackForge Self-Service API and any other external systems! Unleash your imagination and creativity 😄
Create JIRA Issue
The purpose of this example is to create a JIRA Issue when a Vulnerability is created in AttackForge, and to update AttackForge to assign the JIRA Issue Key against the Vulnerability.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows
Event: Vulnerability Created
Secrets:
af_auth - your AttackForge Self-Service API token.
jira_auth - your JIRA API token
Action 1 - Create JIRA Issue
Method: POST
URL: https://<YOUR-JIRA>/rest/api/2/issue
Headers:
Key = Accept; Type = Value; Value = application/json
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = jira_auth
Request Script:
let jiraProjectKey = '';
let afProjectId = '';
if (data.vulnerability_projects) {
for (let i = 0; i < data.vulnerability_projects.length; i++) {
const project = data.vulnerability_projects[i];
if (project.custom_fields) {
for (let j = 0; j < project.custom_fields.length; j++) {
const projectCustomField = project.custom_fields[j];
if (projectCustomField.key === 'jira_project_key') {
jiraProjectKey = projectCustomField.value;
break;
}
}
}
if (project.id) {
afProjectId = project.id;
}
if (afProjectId && jiraProjectKey) {
break;
}
}
}
jiraProjectKey = String.replace(jiraProjectKey, m/"/g, '\"');
jiraProjectKey = String.replace(jiraProjectKey, m/\\"/g, '\"');
return {
data: {
af_project_id: afProjectId,
af_vuln_id: data?.vulnerability_id
},
request: {
body: buildRequestBody(jiraProjectKey)
}
};
function buildRequestBody(jiraProjectKey) {
let summary = '';
if (data.vulnerability_title) {
summary = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
}
summary = String.replace(summary, m/"/g, '\"');
summary = String.replace(summary, m/\\"/g, '\"');
let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';
if (data.vulnerability_title) {
description += 'h1. Vulnerability: ' + data.vulnerability_title + ' \n\n ';
}
if (data.vulnerability_description) {
description += '*Description* \n ' + data.vulnerability_description + ' \n\n ';
}
let cvssScore;
let cvssVector;
let cvssVectorEscaped;
if (data.vulnerability_priority && data.vulnerability_tags) {
for (let i = 0; i < data.vulnerability_tags.length; i++) {
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
}
}
if (cvssScore && cvssVector && cvssVectorEscaped) {
description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
}
else {
description += '*Technical Severity* \n ||*Rating* \n |' + data.vulnerability_priority + ' \n\n ';
}
}
if (data.vulnerability_attack_scenario && data.vulnerability_likelihood_of_exploitation) {
description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vulnerability_attack_scenario + ' \n\n ';
}
if (data.vulnerability_affected_asset_name) {
description += '*Affected Asset* \n\n * ' + data.vulnerability_affected_asset_name + ' \n\n ';
}
else if (data.vulnerability_affected_assets) {
description += '*Affected Asset(s)*: \n';
for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
if (data.vulnerability_affected_assets[i].asset?.name) {
description += '\n * ' + data.vulnerability_affected_assets[i].asset.name;
}
}
description += '\n\n';
}
if (data.vulnerability_steps_to_reproduce) {
description += '*Steps to Reproduce* \n\n ' + data.vulnerability_steps_to_reproduce + ' \n\n ';
}
let notes;
if (data.vulnerability_notes) {
for (let i = 0; i < data.vulnerability_notes.length; i++) {
if (notes === undefined) {
notes = data.vulnerability_notes[i].note;
}
else {
notes += '\n\n' + data.vulnerability_notes[i].note;
}
}
}
if (notes) {
description += '*Notes* \n\n ' + notes + ' \n\n ';
}
if (data.vulnerability_remediation_recommendation) {
description += '*Recommendations* \n\n ' + data.vulnerability_remediation_recommendation + ' \n\n ';
}
const labels = [];
const tags = [];
if (data.vulnerability_tags) {
for (let i = 0; i < data.vulnerability_tags.length; i++) {
let newtag = '* ' + data.vulnerability_tags[i] + '\n';
newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');
Array.push(tags, newtag);
let label = data.vulnerability_tags[i];
label = String.replace(label, m/\s/g, '');
Array.push(labels, label);
}
}
Logger.debug(JSON.stringify(tags));
if (tags.length > 0) {
description = description + '*Tags* \n\n ';
for (let i = 0; i < tags.length; i++) {
description = description + tags[i];
}
}
description = String.replace(description, m/"/g, '\"');
description = String.replace(description, m/\\"/g, '\"');
let priority = 'Lowest';
if (data.vulnerability_priority === 'Critical') {
priority = 'Highest';
}
else if (data.vulnerability_priority === 'High') {
priority = 'High';
}
else if (data.vulnerability_priority === 'Medium') {
priority = 'Medium';
}
else if (data.vulnerability_priority === 'Low') {
priority = 'Low';
}
else if (data.vulnerability_priority === 'Info') {
priority = 'Lowest';
}
return {
fields: {
project: {
key: jiraProjectKey
},
summary: summary,
description: description,
priority: {
name: priority
},
issuetype: {
name: 'Bug'
},
labels: labels
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json;charset=UTF-8'
}
};
}
if (!body?.key) {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'missing JIRA Issue Key (body.key)',
},
};
}
else if (!data?.af_project_id) {
if (data) {
Logger.error(JSON.stringify(data));
}
return {
decision: {
status: 'abort',
message: 'missing data.af_project_id',
},
};
}
else if (!data?.af_vuln_id) {
if (data) {
Logger.error(JSON.stringify(data));
}
return {
decision: {
status: 'abort',
message: 'missing data.af_vuln_id',
},
};
}
else {
return {
data: {
af_project_id: data?.af_project_id,
af_vuln_id: data.af_vuln_id,
jira_issue_key: body.key
}
};
}
Action 2 - Update AF Vuln with JIRA Issue Key
Method: PUT
URL: <defined in Request Script>
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-Key; Type = Secret; Value = af_auth
Request Script:
if (data?.jira_issue_key && data?.af_vuln_id && data?.af_project_id) {
return {
request: {
url: 'https://demo.attackforge.com/api/ss/vulnerability/' + data.af_vuln_id,
body: {
project_id: data.af_project_id,
custom_fields: [
{
key: 'jira_issue_key',
value: data.jira_issue_key
}
]
}
}
};
}
else {
return {
decision: {
status: 'abort',
message: 'missing required fields'
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json'
}
};
}
if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
return {
decision: 'finish'
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'vulnerability not updated'
},
};
}
Update JIRA Issue
The purpose of this example is to update a JIRA Issue when a Vulnerability is updated in AttackForge.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Event: Vulnerability Updated
Secrets:
jira_auth - your JIRA API token
Action 1 - Get JIRA Issue
Method: GET
URL: <defined in Request Script>
Headers:
Key = Accept; Type = Value; Value = application/json
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = jira_auth
Request Script:
if (data.vulnerability_is_deleted === true) {
return {
decision: {
status: 'finish',
message: 'Vulnerability is deleted.',
}
};
}
let jiraIssueKey;
if (data.vulnerability_custom_fields) {
for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
if (data.vulnerability_custom_fields[i].key === 'jira_issue_key') {
jiraIssueKey = data.vulnerability_custom_fields[i].value;
break;
}
}
}
if (!jiraIssueKey) {
return {
decision: {
status: 'finish',
message: 'no JIRA Issue Key found',
}
};
}
if (jiraIssueKey) {
return {
data: {
vuln: data
},
request: {
url: 'https://attackforge.atlassian.net/rest/api/3/issue/' + jiraIssueKey,
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json;charset=UTF-8'
}
};
}
if (!body?.key) {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'finish',
message: 'missing JIRA Issue Key (body.key) - Issue likely deleted',
}
};
}
else if (!data?.vuln) {
if (data) {
Logger.error(JSON.stringify(data));
}
return {
decision: {
status: 'abort',
message: 'missing data.vuln',
},
};
}
else {
return {
data: {
vuln: data?.vuln,
jira_issue_key: body?.key
}
};
}
Action 2 - Update JIRA Issue
Method: <defined in Request Script>
URL: <defined in Request Script>
Headers:
<defined in Request Script>
Request Script:
if (!data?.jira_issue_key) {
return {
decision: {
status: 'finish',
message: 'no JIRA Issue Key found',
}
};
}
else if (!data?.vuln) {
return {
decision: {
status: 'abort',
message: 'missing data.vuln',
}
};
}
else {
let url = 'https://attackforge.atlassian.net/rest/api/2/issue/' + data.jira_issue_key;
return {
request: {
url: url,
body: buildRequestBody(),
}
};
}
function buildRequestBody() {
let summary = '';
if (data.vuln.vulnerability_title) {
summary = '[SECURITY][VULNERABILITY] ' + data.vuln.vulnerability_title;
}
summary = String.replace(summary, m/"/g, '\"');
summary = String.replace(summary, m/\\"/g, '\"');
let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';
if (data?.vuln?.vulnerability_title) {
description += 'h1. Vulnerability: ' + data.vuln.vulnerability_title + ' \n\n ';
}
if (data?.vuln?.vulnerability_description) {
description += '*Description* \n ' + data.vuln.vulnerability_description + ' \n\n ';
}
let cvssScore;
let cvssVector;
let cvssVectorEscaped;
if (data?.vuln?.vulnerability_priority && data?.vuln?.vulnerability_tags) {
for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
const tag = data.vuln.vulnerability_tags[i];
if (tag =~ m/CVSSv3.1 Base Score:/) {
cvssScore = String.replace(tag, 'CVSSv3.1 Base Score: ', '');
}
if (tag =~ m/CVSSv3.1 Temporal Score:/) {
cvssScore = String.replace(tag, 'CVSSv3.1 Temporal Score: ', '');
}
if (tag =~ m/CVSSv3.1 Environmental Score:/) {
cvssScore = String.replace(tag, 'CVSSv3.1 Environmental Score: ', '');
}
if (tag =~ m/CVSS: 3.1\//) {
cvssVector = String.replace(tag, 'CVSS:3.1/', '');
cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
}
}
if (cvssScore && cvssVector && cvssVectorEscaped) {
description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vuln.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
}
else {
description += '*Technical Severity* \n ||*Rating* \n |' + data.vuln.vulnerability_priority + ' \n\n ';
}
}
if (data?.vuln?.vulnerability_attack_scenario && data?.vuln?.vulnerability_likelihood_of_exploitation) {
description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vuln.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vuln.vulnerability_attack_scenario + ' \n\n ';
}
if (data?.vuln?.vulnerability_affected_asset_name) {
description += '*Affected Asset* \n\n * ' + data.vuln.vulnerability_affected_asset_name + ' \n\n ';
}
else if (data?.vuln?.vulnerability_affected_assets) {
description += '*Affected Asset(s)*: \n';
for (let i = 0; i < data.vuln.vulnerability_affected_assets.length; i++) {
if (data.vuln.vulnerability_affected_assets[i].asset?.name) {
description += '\n * ' + data.vuln.vulnerability_affected_assets[i].asset.name;
}
}
description += '\n\n';
}
if (data?.vuln?.vulnerability_steps_to_reproduce) {
description += '*Steps to Reproduce* \n\n ' + data.vuln.vulnerability_steps_to_reproduce + ' \n\n ';
}
let notes;
if (data?.vuln?.vulnerability_notes) {
for (let i = 0; i < data.vuln.vulnerability_notes.length; i++) {
if (notes === undefined) {
notes = data.vuln.vulnerability_notes[i].note;
}
else {
notes += '\n\n' + data.vuln.vulnerability_notes[i].note;
}
}
}
if (notes) {
description += '*Notes* \n\n ' + notes + ' \n\n ';
}
if (data?.vuln?.vulnerability_remediation_recommendation) {
description += '*Recommendations* \n\n ' + data.vuln.vulnerability_remediation_recommendation + ' \n\n ';
}
const labels = [];
const tags = [];
if (data?.vuln?.vulnerability_tags) {
for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
let newtag = '* ' + data.vuln.vulnerability_tags[i] + '\n';
newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');
Array.push(tags, newtag);
let label = data.vuln.vulnerability_tags[i];
label = String.replace(label, m/\s/g, '');
Array.push(labels, label);
}
}
if (tags.length > 0) {
description += '*Tags* \n\n ';
for (let i = 0; i < tags.length; i++) {
description = description + tags[i];
}
}
description = String.replace(description, m/ "/g, '\"');
description = String.replace(description, m/\\"/g, '\"');
let priority = 'Lowest';
if (data?.vuln?.vulnerability_priority === 'Critical') {
priority = 'Highest';
}
else if (data?.vuln?.vulnerability_priority === 'High') {
priority = 'High';
}
else if (data?.vuln?.vulnerability_priority === 'Medium') {
priority = 'Medium';
}
else if (data?.vuln?.vulnerability_priority === 'Low') {
priority = 'Low';
}
else if (data?.vuln?.vulnerability_priority === 'Info') {
priority = 'Lowest';
}
return {
fields: {
summary: summary,
description: description,
priority: {
name: priority
},
issuetype: {
name: 'Bug'
},
labels: labels
}
};
}
Response Script:
if (response.statusCode === 204) {
return {
decision: 'finish'
};
}
else {
Logger.info(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'JIRA Issue update failed',
},
};
}
Create ServiceNow Incident
The purpose of this example is to create a ServiceNow Incident when a Vulnerability is created in AttackForge, and to update AttackForge to assign the SNOW Incident Id against the Vulnerability.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows
Event: Vulnerability Created
Secrets:
af_auth - your AttackForge Self-Service API token.
snow_auth - your SNOW API Key
Action 1 - Create SNOW Incident
Method: POST
URL: https://<YOUR-SNOW>/api/now/table/incident
Headers:
Key = Accept; Type = Value; Value = application/json
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = snow_auth
Request Script:
let afProjectId;
if (data.vulnerability_projects) {
for (let i = 0; i < data.vulnerability_projects.length; i++) {
if (data.vulnerability_projects[i].id) {
afProjectId = data.vulnerability_projects[i].id;
break;
}
}
}
if (!afProjectId) {
return {
decision: {
status: 'abort',
message: 'afProjectId is undefined'
}
};
}
return {
data: {
af_project_id: afProjectId,
af_vuln_id: data?.vulnerability_id
},
request: {
body: buildRequestBody()
}
};
function buildRequestBody() {
const body = {};
if (data.vulnerability_title) {
body.short_description = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
}
let priority = 3;
if (data.vulnerability_priority === 'Critical') {
priority = 1;
}
else if (data.vulnerability_priority === 'High') {
priority = 1;
}
else if (data.vulnerability_priority === 'Medium') {
priority = 2;
}
else if (data.vulnerability_priority === 'Low') {
priority = 3;
}
else if (data.vulnerability_priority === 'Info') {
priority = 3;
}
body.impact = priority;
body.urgency = priority;
body.priority = priority;
body.severity = priority;
let description = '';
if (data.vulnerability_affected_asset_name) {
description += 'Affected Asset: ' + data.vulnerability_affected_asset_name + '\n\n';
}
else if (data.vulnerability_affected_assets) {
description += 'Affected Asset(s): ';
for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
if (data.vulnerability_affected_assets[i].asset?.name) {
description += '\n* ' + data.vulnerability_affected_assets[i].asset.name;
}
}
description += '\n\n';
}
if (data.vulnerability_description) {
description += 'Description: \n' + data.vulnerability_description + '\n\n';
}
if (data.vulnerability_likelihood_of_exploitation) {
description += 'Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '\n\n';
}
let cvssScore;
let cvssVector;
if (data.vulnerability_tags) {
for (let i = 0; i < data.vulnerability_tags.length; i++) {
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
}
}
}
if (cvssScore && cvssVector) {
description += 'CVSS Score: ' + cvssScore + '\nCVSS Vector: ' + cvssVector + '\n\n';
}
if (data.vulnerability_attack_scenario) {
description += 'Attack Scenario (Technical Risk): \n' + data.vulnerability_attack_scenario + '\n\n';
}
if (data.vulnerability_remediation_recommendation) {
description += 'Remediation Recommendation: \n' + data.vulnerability_remediation_recommendation + '\n\n';
}
if (data.vulnerability_steps_to_reproduce) {
description += 'Steps to Reproduce: \n' + data.vulnerability_steps_to_reproduce + '\n\n';
}
if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
description += 'Notes:';
for (let i = 0; i < data.vulnerability_notes.length; i++) {
if (data.vulnerability_notes[i].note) {
description += '\n' + data.vulnerability_notes[i].note;
}
}
}
if (data.vulnerability_tags && data.vulnerability_tags.length > 0) {
description += 'Tags:';
for (let i = 0; i < data.vulnerability_tags.length; i++) {
description += '\n* ' + data.vulnerability_tags[i];
}
}
body.description = description;
return body;
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json;charset=UTF-8'
}
};
}
if (!body?.result?.sys_id) {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'missing SNOW Incident SysId (body.result.sys_id)',
},
};
}
else if (!data?.af_project_id) {
Logger.error(JSON.stringify(data));
return {
decision: {
status: 'abort',
message: 'missing data.af_project_id',
},
};
}
else if (!data?.af_vuln_id) {
Logger.error(JSON.stringify(data));
return {
decision: {
status: 'abort',
message: 'missing data.af_vuln_id',
},
};
}
else {
return {
data: {
af_project_id: data?.af_project_id,
af_vuln_id: data?.af_vuln_id,
snow_incident_number: body.result.number
}
};
}
Action 2 - Update AF Vuln with SNOW Incident Id
Method: PUT
URL: <defined in Request Script>
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-Key; Type = Secret; Value = af_auth
Request Script:
if (data.snow_incident_number && data.af_vuln_id && data.af_project_id) {
return {
request: {
url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
body: {
project_id: data.af_project_id,
custom_fields: [
{
key: 'snow_incident_number',
value: data.snow_incident_number
}
]
}
}
};
}
else {
return {
decision: {
status: 'abort',
message: 'missing required fields'
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json'
}
};
}
if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
return {
decision: 'finish'
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'vulnerability not updated'
},
};
}
Create Azure DevOps Work Item
The purpose of this example is to create a Azure DevOps Work Item when a Vulnerability is created in AttackForge, and to update AttackForge to assign the Azure DevOps Work Item Id against the Vulnerability.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows
Event: Vulnerability Created
Secrets:
af_auth - your AttackForge Self-Service API token.
ado_auth - your ADO Personal Access Token
Action 1 - Create ADO Work Item
Method: POST
URL: https://dev.azure.com/<YOUR-ADO-TENANT>/<YOUR-ADO-PROJECT>/_apis/wit/workitems/$Issue?api-version=6.1-preview.3
Headers:
Key = Content-Type; Type = Value; Value = application/json-patch+json
Key = Authorization; Type = Secret; Value = ado_auth
Request Script:
let afProjectId;
if (data.vulnerability_projects) {
for (let i = 0; i < data.vulnerability_projects.length; i++) {
if (data.vulnerability_projects[i].id) {
afProjectId = data.vulnerability_projects[i].id;
break;
}
}
}
if (!afProjectId) {
return {
decision: {
status: 'abort',
message: 'afProjectId is undefined',
},
};
}
return {
data: {
af_project_id: afProjectId,
af_vuln_id: data?.vulnerability_id
},
request: {
body: buildRequestBody()
}
};
function buildRequestBody() {
const fields = [];
let title = '';
if (data.vulnerability_title) {
title = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
}
Array.push(fields, {
from: null,
op: 'add',
path: '/fields/System.Title',
value: title
});
let priority = 4;
if (data.vulnerability_priority === 'Critical') {
priority = 1;
}
else if (data.vulnerability_priority === 'High') {
priority = 2;
}
else if (data.vulnerability_priority === 'Medium') {
priority = 3;
}
else if (data.vulnerability_priority === 'Low') {
priority = 4;
}
else if (data.vulnerability_priority === 'Info') {
priority = 4;
}
Array.push(fields, {
from: null,
op: 'add',
path: '/fields/Microsoft.VSTS.Common.Priority',
value: priority
});
if (data.vulnerability_priority) {
Array.push(fields, {
from: null,
op: 'add',
path: '/fields/System.Tags',
value: data.vulnerability_priority + ', Security Vulnerability'
});
}
let description = '';
if (data.vulnerability_title) {
description += '<h1>[SECURITY][VULNERABILITY] ' + data.vulnerability_title + '</h1><br/>';
}
if (data.vulnerability_description) {
const sanitizedDescription = String.replace(data.vulnerability_description, m/\r\n|\n|\r/gi, '<br>');
description += '<h2>Description</h2><p>' + sanitizedDescription + '</p>';
}
if (data.vulnerability_affected_asset_name) {
description += '<h2>Affected Asset</h2><ul><li>' + data.vulnerability_affected_asset_name + '</li></ul>';
}
else if (data.vulnerability_affected_assets) {
description += '<h2>Affected Asset(s)</h2><ul>';
for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
if (data.vulnerability_affected_assets[i].asset?.name) {
description += '<li>' + data.vulnerability_affected_assets[i].asset.name + '</li>';
}
}
description += '</ul>';
}
let cvssScore;
let cvssVector;
if (data.vulnerability_priority && data.vulnerability_tags) {
for (let i = 0; i < data.vulnerability_tags.length; i++) {
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
}
if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
}
}
if (cvssScore && cvssVector) {
description +=
"<h2>Technical Severity</h2>" +
"<table style='text-align: center; vertical-align: middle; font-size:15px;'>" +
"<thead>" +
"<td><b>Rating</b></td>" +
"<td><b>CVSSv3.1 Score</b></td>" +
"</thead>" +
"<tbody>" +
"<td>" + data.vulnerability_priority + "</td>" +
"<td>" + cvssScore + "</td>" +
"</tbody>" +
"</table>" +
"<p>CVSS 3.1 Vector String: <a href='https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=" + cvssVector + "'>" + cvssVector + "</a></p>";
}
else {
description +=
"<h2>Technical Severity</h2>" +
"<table style='text-align: center; vertical-align: middle; font-size:15px;'>" +
"<thead>" +
"<td><b>Rating</b></td>" +
"</thead>" +
"<tbody>" +
"<td>" + data.vulnerability_priority + "</td>" +
"</tbody>" +
"</table>";
}
}
if (data.vulnerability_likelihood_of_exploitation && data.vulnerability_attack_scenario) {
const sanitizedAttackScenario = String.replace(data.vulnerability_attack_scenario, m/\r\n|\n|\r/gi, '<br>');
description +=
"<h2>Attack Scenario (Technical Risk)</h2>" +
"<p>Likelihood of Exploitation: " + data.vulnerability_likelihood_of_exploitation + "/10</p>" +
"<p>" + sanitizedAttackScenario + "</p>";
}
if (data.vulnerability_remediation_recommendation) {
const sanitizedRemediationRecommendation = String.replace(data.vulnerability_remediation_recommendation, m/\r\n|\n|\r/gi, '<br>');
description += "<h2>Recommendations</h2><p>" + sanitizedRemediationRecommendation + "</p>";
}
if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
description += "<h2>Notes</h2>";
for (let i = 0; i < data.vulnerability_notes.length; i++) {
if (data.vulnerability_notes[i].note) {
const sanitizedNote = String.replace(data.vulnerability_notes[i].note, m/\r\n|\n|\r/gi, '<br>');
description += '<p>' + sanitizedNote + '</p>';
}
}
}
if (data.vulnerability_steps_to_reproduce) {
const sanitizedPOC = String.replace(data.vulnerability_steps_to_reproduce, m/\r\n|\n|\r/gi, '<br>');
description += "<h2>Steps to Reproduce</h2><p>" + sanitizedPOC + "</p>";
}
if (data.vulnerability_tags) {
description += "<h2>Tags</h2><ul>";
for (let i = 0; i < data.vulnerability_tags.length; i++) {
description += '<li>' + data.vulnerability_tags[i] + '</li>';
}
description += '</ul>';
}
Array.push(fields, {
from: null,
op: 'add',
path: "/fields/System.Description",
value: description
});
return fields;
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json; charset=utf-8; api-version=6.1-preview.3') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json; charset=utf-8; api-version=6.1-preview.3'
}
};
}
if (!body?.id) {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'missing ADO Work Item Id (body.id)',
},
};
}
else if (!data?.af_project_id) {
Logger.error(JSON.stringify(data ?? {}));
return {
decision: {
status: 'abort',
message: 'missing data.af_project_id',
},
};
}
else if (!data?.af_vuln_id) {
Logger.error(JSON.stringify(data ?? {}));
return {
decision: {
status: 'abort',
message: 'missing data.af_vuln_id',
},
};
}
else {
Logger.debug('normal result');
return {
data: {
af_project_id: data?.af_project_id,
af_vuln_id: data?.af_vuln_id,
ado_work_item_id: body.id
}
};
}
Action 2 - Update AF Vuln with ADO Work Item Id
Method: PUT
URL: <defined in Request Script>
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-Key; Type = Secret; Value = af_auth
Request Script:
if (data.ado_work_item_id && data.af_vuln_id && data.af_project_id) {
return {
request: {
url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
body: {
project_id: data.af_project_id,
custom_fields: [
{
key: 'ado_work_item_id',
value: JSON.stringify(data.ado_work_item_id)
}
]
}
}
};
}
else {
return {
decision: {
status: 'abort',
message: 'missing required fields'
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json'
}
};
}
if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
return {
decision: 'finish'
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'vulnerability not updated'
},
};
}
Prioritize Vulnerability with Threat Intelligence from VulnDB
The purpose of this example is to prioritize a vulnerability based on threat intelligence information harnessed from FlashPoint VulnDB and to apply a custom score/rating to the vulnerability.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows
Event: Vulnerability Created
Secrets:
af_auth - your AttackForge Self-Service API token.
vulndb_client_id - your VulnDB Client Id
vulndb_client_secret - your VulnDB Client Secret
Action 1 - Get VulnDB OAuth Token
Method: POST
URL: https://vulndb.flashpoint.io/oauth/token
Headers:
Key = Content-Type; Type = Value; Value = application/json
Request Script:
let cve;
if (data.vulnerability_custom_fields) {
for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
if (data.vulnerability_custom_fields[i].key === 'cve') {
cve = data.vulnerability_custom_fields[i].value;
break;
}
}
}
if (!cve) {
return {
decision: {
status: 'finish',
message: 'No CVE found'
}
};
}
return {
data: {
cve: cve,
vuln: data
},
request: {
body: {
client_id: secrets.vulndb_client_id,
client_secret: secrets.vulndb_client_secret,
grant_type: 'client_credentials'
}
}
};
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json; charset=utf-8'
}
};
}
if (body?.access_token) {
return {
data: {
vulnDBToken: body.access_token,
cve: data?.cve,
vuln: data?.vuln
}
};
}
else {
return {
decision: {
status: 'abort',
cause: 'VulnDB access token not found'
}
};
}
Action 2 - Get Threat Intel for Vuln from VulnDB
Method: <defined in Request Script>
URL: <defined in Request Script>
Headers:
Key = Content-Type; Type = Value; Value = application/json
<others defined in Request Script>
Request Script:
if (data?.vulnDBToken && data?.cve && data?.vuln) {
return {
data: {
cve: data.cve,
vuln: data.vuln
},
request: {
url: 'https://vulndb.flashpoint.io/api/v2/vulnerabilities/' + data.cve + '/find_by_cve_id',
headers: {
Authorization: 'Bearer ' + data.vulnDBToken
}
}
};
}
else {
return {
decision: {
status: 'abort',
message: 'VulnDB access token not found'
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json; charset=utf-8'
}
};
}
if (!body?.results?[0]) {
return {
decision: {
status: 'abort',
message: 'VulnDB CVE not found'
}
};
}
return {
data: {
cve: data?.cve,
vulnDB: body.results[0],
vuln: data?.vuln
}
};
Action 3 - Apply Threat Intel, Prioritize Vulnerability and Update Vulnerability
Method: <defined in Request Script>
URL: <defined in Request Script>
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-KEY; Type = Secret; Value = af_auth
Request Script:
if (data?.vuln && data?.vulnDB) {
const score = calculateScore(data.vuln, data.vulnDB);
const priority = convertScoreToPriority(score);
return {
request: {
url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.vuln.vulnerability_id,
body: {
priority: priority,
custom_fields: [
{
key: 'threat_score',
value: JSON.stringify(score)
}
]
}
}
};
}
else {
return {
decision: {
status: 'abort',
message: 'Missing required vuln data'
}
};
}
function calculateScore(vuln, vdb) {
let score = 0;
// Maximum score = 140
// Higher the score = Higher Priority to fix!
// [ THREAT CONTEXT ]
const internetFacing = isInternetFacing(vuln);
// Is the threat related to exposed/internet-facing services?
if (internetFacing) {
score += 10;
}
if (vdb.cvss_version_three_metrics) {
for (let i = 0; i < vdb.cvss_version_three_metrics.length; i++) {
const metrics = vdb.cvss_version_three_metrics[i];
// Is the CVSS score 8 or higher?
if (metrics.score !== undefined && metrics.score >= 8) {
score += 5;
}
// Is the threat easily exploitable?
if (metrics.attack_complexity === 'HIGH') {
score += 1;
}
else if (metrics.attack_complexity === 'MEDIUM') {
score += 5;
}
else if (metrics.attack_complexity === 'LOW') {
score += 10;
}
// No user interaction required?
if (metrics.user_interaction === 'NONE') {
if (isInternetFacing) {
score += 10;
}
else {
score += 5;
}
}
}
}
else if (vdb.cvss_metrics) {
for (let i = 0; i < vdb.cvss_metrics.length; i++) {
const metrics = vdb.cvss_metrics[i];
// Is the CVSS score 8 or higher?
if (metrics.score !== undefined && metrics.score >= 8) {
score += 5;
}
// Is the threat easily exploitable?
if (metrics.attack_complexity === 'HIGH') {
score += 1;
}
else if (metrics.attack_complexity === 'MEDIUM') {
score += 5;
}
else if (metrics.attack_complexity === 'LOW') {
score += 10;
}
}
}
if (vdb.classifications) {
for (let i = 0; i < vdb.classifications.length; i++) {
const classifications = vdb.classifications[i];
// Is there a public exploit available for this threat?
if (classifications.name === 'exploit_public') {
if (isInternetFacing) {
score += 10;
}
else {
score += 5;
}
}
// Does this threat require configuration changes?
else if (classifications.name === 'solution_workaround') {
score += 5;
}
// Does the threat grant unauthorized access?
else if (classifications.name === 'location_remote') {
if (isInternetFacing) {
score += 10;
}
else {
score += 5;
}
}
// Disclosure in the wild?
else if (classifications.name === 'disclosure_in_wild') {
score += 5;
}
else if (classifications.name === 'disclosure_uncoordinated_disclosure') {
score += 5;
}
// Wormable?
else if (classifications.name === 'exploit_wormified') {
if (isInternetFacing) {
score += 15;
}
else {
score += 10;
}
}
// Virus / Malware?
else if (classifications.name === 'exploit_virus_malware') {
if (isInternetFacing) {
score += 10;
}
else {
score += 5;
}
}
// PoC Public?
else if (classifications.name === 'exploit_poc_public') {
if (isInternetFacing) {
score += 15;
}
else {
score += 10;
}
}
}
}
// Is a patch available for this threat?
let patchExists = false;
if (vdb.classifications) {
for (let i = 0; i < vdb.classifications.length; i++) {
const classifications = vdb.classifications[i];
if (classifications.name === 'solution' || classifications.name === 'solution_upgrade') {
patchExists = true;
break;
}
}
}
else if (vdb.cvss_version_three_metrics) {
for (let i = 0; i < vdb.cvss_version_three_metrics.length; i++) {
const metrics = vdb.cvss_version_three_metrics[i];
if (metrics.remediation_level === 'OFFICIAL_FIX' || metrics.remediation_level === 'TEMPORARY_FIX') {
patchExists = true;
break;
}
}
}
if (!patchExists) {
score += 10;
}
// Social Risk Score (Vuln DB)
if (vdb.social_risk_score === 'High') {
score += 5;
}
else if (vdb.social_risk_score === 'Medium') {
score += 3;
}
else if (vdb.social_risk_score === 'Low') {
score += 1;
}
// What is the likelihood that this threat could be used in a ransomware attack?
if (vdb.ransomware_likelihood === 'High') {
if (isInternetFacing) {
score += score + 10;
}
else {
score += 5;
}
}
else if (vdb.ransomware_likelihood === 'Medium') {
if (isInternetFacing) {
score += 5;
}
else {
score += 3;
}
}
else if (vdb.ransomware_likelihood === 'Low') {
score += 1;
}
if (vdb.tags) {
for (let i = 0; i < vdb.tags.length; i++) {
// Is this a known CISA KEV vulnerability?
if (vdb.tags[i] === 'cisa_kev') {
score += 10;
break;
}
}
}
return score;
}
function convertScoreToPriority(score) {
if (score > 0 && score <= 30) {
return 'Low';
}
else if (score > 30 && score <= 70) {
return 'Medium';
}
else if (score > 70 && score <= 110) {
return 'High';
}
else if (score > 110 && score <= 140) {
return 'Critical';
}
else {
return 'Info';
}
}
function isInternetFacing(vuln) {
if (vuln.vulnerability_affected_assets) {
for (let i = 0; i < vuln.vulnerability_affected_assets.length; i++) {
if (vuln.vulnerability_affected_assets[i].asset?.custom_fields) {
const assetCustomFields = vuln.vulnerability_affected_assets[i].asset?.custom_fields;
for (let j = 0; j < assetCustomFields.length; j++) {
if (assetCustomFields[j].key === 'internet_facing' && assetCustomFields[j].value === 'Yes') {
return true;
}
}
}
}
}
return false;
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json'
}
};
}
if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
return {
decision: 'finish'
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'Vulnerability not updated'
}
};
}
Create Vuln from HackerOne Report
The purpose of this example is to create a new vulnerability immediately when a new report is submitted on HackerOne.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
HTTP Trigger
Method: POST
Authentication: None
Secrets:
h1_webhook_secret - your HackerOne Webhook Secret
hackerone_api_key - your HackerOne API Key
x_user_key - your AttackForge Self-Service API token.
Action 1 - Get HackerOne Report
Method: GET
URL: https://api.hackerone.com/v1/reports/{id}
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = hackerone_api_key
Request Script:
// Validate HMAC
const h1Secret = secrets.h1_webhook_secret;
const h1Signature = String.replace(data.headers['X-H1-Signature'], "SHA256=", '');
const payloadHMAC = String.toLowerCase(String.hmac(data.body, h1Secret, "SHA256", "base16"));
if (h1Signature !== payloadHMAC) {
Logger.info('h1Signature: ' + h1Signature);
Logger.info('hmac: ' + payloadHMAC);
return {
decision: {
status: 'abort',
message: 'Invalid HMAC',
}
};
}
if (data.jsonBody?.data?.report?.id) {
return {
decision: {
status: 'continue',
message: 'Fetching HackerOne Report',
},
request: {
url: 'https://api.hackerone.com/v1/reports/' + data.jsonBody.data.report.id
}
};
}
else {
return {
decision: {
status: 'abort',
message: 'Missing "data.report.id"',
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json; charset=utf-8'
}
};
}
if (response.statusCode === 200 && body?.data) {
return {
decision: {
status: 'continue',
message: 'Retrieved HackerOne Report'
},
data: {
report: body.data
}
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'Failed retrieving HackerOne Report'
},
};
}
Action 2 - Get CWE Details
Method: GET
URL: https://cwe-api.mitre.org/api/v1/cwe/weakness/{id}
Headers:
Key = Content-Type; Type = Value; Value = application/json
Request Script:
let cweId = undefined;
if (data.report?.relationships?.weakness?.data?.attributes?.external_id) {
const cweField = data.report.relationships.weakness.data.attributes.external_id;
if (String.startsWith(cweField, 'cwe-')) {
cweId = String.replace(cweField, "cwe-", "");
}
}
if (cweId) {
return {
decision: {
status: 'continue',
message: 'Fetching CWE [ ' + cweId + ' ] details',
},
request: {
url: 'https://cwe-api.mitre.org/api/v1/cwe/weakness/' + cweId
},
data: {
report: data.report
}
};
}
else {
return {
decision: {
status: 'next',
message: 'No CWE found. Skip fetching CWE details.',
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json; charset=UTF-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json; charset=UTF-8'
}
};
}
if (response.statusCode === 200 && body?.Weaknesses?[0]) {
return {
decision: {
status: 'continue',
message: 'Retrieved CWE details'
},
data: {
report: data.report,
cwe: body?.Weaknesses[0]
}
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'Failed retrieving CWE details'
},
};
}
Action 3 - Create Vulnerability
Method: POST
URL: https://acme.attackforge.com/api/ss/vulnerability
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-KEY; Type = Secret; Value = x_user_key
Request Script:
if (data.report) {
const report = data.report;
const cwe = data.cwe;
return {
decision: {
status: 'continue',
message: 'Creating Vulnerability',
},
request: {
body: buildRequestBody(report, cwe)
},
data: {}
};
}
else {
return {
decision: {
status: 'abort',
message: 'Missing HackerOne Report',
}
};
}
function buildRequestBody (report, cwe) {
const vuln = {
projectId: "685500711d6a44e61f90db4e",
title: "New Submission",
affected_asset_name: "AttackForge-TEST H1B",
priority: "Info",
likelihood_of_exploitation: 1,
description: "TBD",
attack_scenario: "TBD",
remediation_recommendation: "TBD",
steps_to_reproduce: "TBD",
tags: [
'HackerOne',
'H1'
],
is_visible: true,
custom_fields: []
};
if (cwe) {
if (cwe.ID) {
const name = cwe.ID;
Array.push(vuln.tags, 'CWE-' + cwe.ID);
}
if (cwe.Name) {
const name = cwe.Name;
vuln.title = name;
}
if (cwe.Description) {
let description = cwe.Description;
vuln.description = formatVulnInfo(description);
}
if (cwe.ExtendedDescription) {
let extendedDescription = cwe.ExtendedDescription;
vuln.description = vuln.description + ' ' + formatVulnInfo(extendedDescription);
}
if (cwe.BackgroundDetails) {
const backgroundDetails = cwe.BackgroundDetails;
vuln.attack_scenario = "";
if (Array.isArray(backgroundDetails)) {
for (let x = 0; x < backgroundDetails.length; x++) {
const attackScenario = backgroundDetails[x];
vuln.attack_scenario = vuln.attack_scenario + formatVulnInfo(attackScenario);
}
}
else {
vuln.attack_scenario = vuln.attack_scenario + formatVulnInfo(backgroundDetails);
}
}
if (cwe.PotentialMitigations) {
const potentialMitigations = cwe.PotentialMitigations;
vuln.remediation_recommendation = "";
if (Array.isArray(potentialMitigations)) {
for (let x = 0; x < potentialMitigations.length; x++) {
const mitigation = potentialMitigations[x];
if (mitigation.Description) {
vuln.remediation_recommendation = vuln.remediation_recommendation
+ formatVulnInfo(mitigation.Description);
}
}
}
else {
if (potentialMitigations.Description) {
vuln.remediation_recommendation = vuln.remediation_recommendation
+ formatVulnInfo(potentialMitigations.Description);
}
}
}
if (report.attributes?.title && report.attributes.vulnerability_information) {
vuln.steps_to_reproduce = '<p>'
+ report.attributes.title + '</p><p>'
+ formatVulnInfo(report.attributes.vulnerability_information)
+ '</p>';
}
}
else {
if (report.attributes?.title) {
vuln.title = report.attributes.title;
}
if (report.attributes?.vulnerability_information) {
vuln.description = formatVulnInfo(report.attributes.vulnerability_information);
}
}
if (report.id) {
Array.push(vuln.custom_fields, {
key: "hackerone_report_id",
value: report.id
});
if (report.relationships?.program?.data?.attributes?.handle) {
const handle = report.relationships.program.data.attributes.handle;
Array.push(vuln.custom_fields, {
key: "hackerone_report_url",
value: 'https://hackerone.com/bugs?report_id=' + report.id + '&subject=' + handle
});
}
}
if (report.relationships?.severity?.data?.attributes) {
const severity = report.relationships.severity.data.attributes;
if (severity.rating === 'critical') {
vuln.priority = 'Critical';
}
else if (severity.rating === 'high') {
vuln.priority = 'High';
}
else if (severity.rating === 'medium') {
vuln.priority = 'Medium';
}
else if (severity.rating === 'low') {
vuln.priority = 'Low';
}
if (severity.score && severity.cvss_vector_string) {
vuln.likelihood_of_exploitation = Number.parseInt(Math.ceil(severity.score));
Array.push(vuln.tags, 'CVSSv3.1 Base Score: ' + severity.score);
Array.push(vuln.tags, severity.cvss_vector_string);
}
}
return vuln;
};
function formatVulnInfo (value) {
if (String.startsWith(value, "\n\n")) {
value = String.substring(value, 2);
}
else if (String.startsWith(value, "\n")) {
value = String.substring(value, 1);
}
value = String.replaceAll(value, "\n\n", "<br/>");
value = String.replaceAll(value, "\n", "<br/>");
return value;
};
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json'
}
};
}
if (response.statusCode === 200 && body?.vulnerability?.vulnerability_id) {
return {
decision: {
status: 'finish',
message: 'Created Vulnerability'
}
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'Failed creating Vulnerability'
},
};
}
Trigger an Automated Scan in Tenable

The purpose of this example is to schedule a vulnerability scan in Tenable when a Project is created in AttackForge.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Event: Project Created
Secrets:
tenable_auth - your Tenable API Key
Action 1 - Schedule Vulnerability Scan in Tenable
Method: <insert>
URL: <insert>
Headers:
Key = Accept; Type = Value; Value = application/json
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = tenable_auth
Request Script:
<insert>
Response Script:
<insert>
Create Slack Message
The purpose of this example is to create a message in a Slack Channel when a Vulnerability is created in AttackForge.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Event: Vulnerability Created
Secrets:
slack_auth - your Slack Bot Token
Action 1 - Post Slack Message
Method: POST
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = slack_auth
Request Script:
return {
request: {
body: buildRequestBody()
}
};
function buildRequestBody() {
let afProjectId;
let afProjectName;
let channel;
if (data.vulnerability_projects) {
for (let i = 0; i < data.vulnerability_projects.length; i++) {
if (data.vulnerability_projects[i].name) {
afProjectName = data.vulnerability_projects[i].name;
}
if (data.vulnerability_projects[i].id) {
afProjectId = data.vulnerability_projects[i].id;
}
if (data.vulnerability_projects[i].custom_fields) {
for (let j = 0; j < data.vulnerability_projects[i].custom_fields.length; j++) {
if (data.vulnerability_projects[i].custom_fields[j].key === 'slack_channel') {
channel = data.vulnerability_projects[i].custom_fields[j].value;
break;
}
}
}
}
}
let text = '';
if (data.vulnerability_priority) {
text += '[' + data.vulnerability_priority + '] ';
}
text += 'Vulnerability ';
if (data.vulnerability_title) {
text += '[' + data.vulnerability_title + '] ';
}
text += 'discovered in Project';
if (afProjectName) {
text += ' [' + afProjectName + ']';
}
if (afProjectId) {
text += ' - <https://afe1.attackforge.dev/projects/' + afProjectId + '/vulnerabilities/' + data.vulnerability_id + '|View Vulnerability>';
}
return {
channel: channel,
text: text
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json; charset=utf-8'
}
};
}
if (body?.ok === true) {
return {
decision: 'continue'
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'SLACK message not posted',
},
};
}
Create Teams Message
The purpose of this example is to create a message in a Teams Channel when a Vulnerability is created in AttackForge.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Event: Vulnerability Created
Secrets:
teams_auth - your Incoming Web Hook
Action 1 - Post Teams Message
Method: POST
URL: <YOUR-INCOMING-WEBHOOK>
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = Accept; Type = Value; Value = */*
Request Script:
let afProjectId;
let afProjectName;
if (data.vulnerability_projects) {
for (let i = 0; i < data.vulnerability_projects.length; i++) {
if (data.vulnerability_projects[i].id && data.vulnerability_projects[i].name) {
afProjectId = data.vulnerability_projects[i].id;
afProjectName = data.vulnerability_projects[i].name;
break;
}
}
}
if (!afProjectId) {
return {
decision: {
status: 'abort',
message: 'afProjectId is falsy'
}
};
}
if (!afProjectName) {
return {
decision: {
status: 'abort',
message: 'afProjectName is falsy'
}
};
}
let text = '';
if (data.vulnerability_priority) {
text += '[' + data.vulnerability_priority + '] ';
}
text += 'Vulnerability ';
if (data.vulnerability_title) {
text += '[' + data.vulnerability_title + '] ';
}
text +=
'discovered in Project' +
' [' + afProjectName + ']' +
' - [View Vulnerability](https://afe1.attackforge.dev/projects/' + afProjectId + '/vulnerabilities/' + data.vulnerability_id + ')';
return {
request: {
url: 'https://prod-26.australiasoutheast.logic.azure.com:443/workflows/e25ebc22ccf5438190dc46a087450e5c/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=' + secrets.sig,
body: {
type: 'message',
attachments: [
{
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
type: 'AdaptiveCard',
version: '1.2',
body: [
{
type: 'TextBlock',
text: text
}
]
}
}
]
}
}
};
Response Script:
if (response?.statusCode === 202) {
return {
decision: 'continue'
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'TEAMS message not posted',
},
};
}
Send Vulnerability to PowerBI
The purpose of this example is to send data to PowerBI when a Vulnerability is created in AttackForge.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Event: Vulnerability Created
Secrets:
powerbi_key - your Push Semantic Model - Push URL
Action 1 - Send Vulnerability to PowerBI
Method: POST
URL: <YOUR-PUSH-URL>
Headers:
Key = Content-Type; Type = Value; Value = application/json
Request Script:
return {
request: {
url: 'https://api.powerbi.com/beta/544b6efc-1149-4f8c-a17a-cad3c50cd5f8/datasets/d5ff54b4-2be2-4e02-baa2-9c6aa20a6c55/rows?experience=power-bi&key=' + secrets.powerbi_key,
body: [
{
vuln_title: data.vulnerability_title,
vuln_priority: data.vulnerability_priority
}
]
}
};
Response Script:
if (response?.statusCode === 200) {
return {
decision: 'continue'
};
}
else {
Logger.error(JSON.stringify(response));
return {
decision: {
status: 'abort',
message: 'PowerBI data not posted',
},
};
}
Create Salesforce Opportunity
The purpose of this example is to create a Salesforce Opportunity when a Project is requested in AttackForge.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Event: Project Requested
Secrets:
sf_client_id - your Salesforce OAuth Client Id
sf_client_secret - your Salesforce OAuth Client Secret
sf_password - Your Salesforce User Password
sf_username - Your SalesForce Username
Action 1 - Get Salesforce OAuth Token
Method: POST
URL: https://login.salesforce.com/services/oauth2/token
Headers:
Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
Request Script:
return {
request: {
body: 'grant_type=password&client_id=' + secrets.sf_client_id + '&client_secret=' + secrets.sf_client_secret + '&username=' + secrets.sf_username + '&password=' + secrets.sf_password
},
data: {
project_request: data
}
};
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json;charset=UTF-8'
}
};
}
if (body?.access_token) {
return {
data: {
project_request: data.project_request,
sf_access_token: body.access_token
}
};
}
else {
return {
decision: {
status: 'abort',
message: 'Salesforce access token not found'
}
};
}
Action 2 - Create Salesforce Opportunity
Method: POST
URL: https://<YOUR-SALESFORCE>/services/data/v63.0/sobjects/Opportunity
Headers:
Key = Content-Type; Type = Value; Value = application/json
Request Script:
if (data?.project_request && data?.sf_access_token) {
let closeDate = '2030-12-31';
if (data.project_request.project_request_end_date) {
closeDate = String.slice(data.project_request.project_request_end_date, 0, 10);
}
return {
request: {
headers: {
'Authorization': 'Bearer ' + data.sf_access_token
},
body: {
'Name': data.project_request.project_request_name,
'StageName': 'Needs Analysis',
'CloseDate': closeDate
}
}
};
}
else {
return {
decision: {
status: 'abort',
message: 'Salesforce access token not found'
}
};
}
Response Script:
let body;
if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
body = JSON.parse(response.body);
}
else {
return {
decision: {
status: 'abort',
message: 'Content-Type is expected to be application/json;charset=UTF-8'
}
};
}
if (response.statusCode === 201 && body?.id && body.success === true) {
return {
decision: 'finish'
};
}
else {
return {
decision: {
status: 'abort',
message: 'Salesforce Opportunity not created'
}
};
}
Create a Webhook
The purpose of this example is to post data to a Webhook when a Vulnerability is created in AttackForge.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Event: Vulnerability Created
Secrets: None
Action 1 - Send Data to Webhook
Method: POST
URL: <YOUR-WEBHOOK-ADDRESS>
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = auth
Request Script:
// Not required for this webhook
Response Script:
// Not required for this webhook
Send Custom Email
The purpose of this example is to send an email to account managers when a Retest is requested.
This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.
Initial Set Up
Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows
Event: Project Retest Requested
Secrets:
af_auth - your AttackForge Self-Service API token.
Action 1 - Send Email to Account Managers
Method: POST
URL: https://<YOUR-ATTACKFORGE>/api/ss/email
Headers:
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-Key; Type = Value; Value = af_auth
Request Script:
return {
request: {
body: {
to: [
"[email protected]",
{
"user_id": "67b45e6de6da84559d9860fc"
},
],
cc: [
"[email protected]"
],
subject: "New Project Requested - Please Follow Up",
text: data.project_request_name + ' just requested to start on ' + data.project_request_start_date,
html: '<p>[<b>' + data.project_request_name + '</b>] just requested to start on [<b>' + data.project_request_start_date + '</b>]</p>'
},
},
};
Response Script:
const body = JSON.parse(response.body);
if (body?.status === 'Accepted') {
return {
decision: {
status: 'finish',
message: 'Email sent!'
},
};
}
else {
return {
decision: {
status: 'abort',
message: 'Email not sent'
}
};
}
Last updated