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:
Trigger automated scanning activities in your security toolset like Rapid7 , Tenable and Qualys .
Create messages on collaboration platforms like Slack and Teams .
Prioritize vulnerabilities with threat-intelligence like VulnDB
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 be granted access to another person's existing Flow
Flow Overview
A Flow is comprised of the following:
Name - the name of the Flow.
Actions - a sequence of steps which are executed in order during a Run .
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 an Event - 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 Event Trigger . For example, if the Event 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 cannot be changed (for the time being).
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
Insert the user's email address or look up and select the user
Once a user has been given access to a Flow, they will be able to do the following:
You can remove a users' access to your Flows from the Settings
.
Triggers
A Trigger is an action which initiates a Run . Triggers can be initiated from Events or manually initiated.
The following Triggers are currently supported:
Vulnerability Remediation Note Created
Vulnerability Remediation Note Updated
Vulnerability Evidence Created
Vulnerability Evidence Updated
A Flow can be assigned to only one Trigger.
Triggers can be assigned to a Flow when either creating or editing the Flow.
COMING SOON! Ability to create your own custom Triggers which are invoked from custom URLs. This will allow you to execute a Flow on-demand whenever you would need it to run, and also allow for creating custom webhook receivers for your security toolsets and systems to communicate back to AttackForge.
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.
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 and enter a Key and a Value. Note the Key must be letters, numbers and underscores only.
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.
COMING SOON: You will be able to create User Secrets which belong to the user and are managed in one place. This makes is easy to rotate passwords and credentials without having to update the Flows.
There are two (2) ways in which you can refer to your Secrets in your Flow:
Select the Secret directly in the 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:
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
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:
Method - this is the HTTP method i.e. GET, POST, PUT, etc. that will be used for the Request
URL - this is the URL that will be used for the Request .
Verify Certificate - this determines whether to verify if the TLS certificate is valid for the URL.
Headers - these are the headers that will be sent when the Request is made.
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:
Methods can be selected when editing the Action :
Methods can also be programatically set in your Request Script in the Return Statement :
Copy 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:
Copy 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
}
]
}
}
};
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:
Copy 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:
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:
Using the example above, if you wanted to store the vulnerability Id in a constant, you could do the following:
Copy 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:
Copy 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:
Copy 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:
Copy 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:
Copy 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:
An example of the Response Object:
Copy 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:
Copy 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:
Copy 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:
Copy 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
Copy 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:
Copy 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:
Copy 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:
Copy 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:
Copy 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:
Copy 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:
Copy 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:
Copy 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:
Copy 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:
Copy 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:
Copy 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.
Copy 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 .
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.
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.
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!
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
Action 1 - Create JIRA Issue
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
Copy 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
}
};
}
Copy 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
URL : <defined in Request Script>
Headers :
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-Key; Type = Secret; Value = af_auth
Copy 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'
}
};
}
Copy 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
Action 1 - Get JIRA Issue
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
Copy 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,
}
};
}
Copy 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>
Copy 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
}
};
}
Copy 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
Action 1 - Create SNOW Incident
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
Copy 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;
}
Copy 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
URL : <defined in Request Script>
Headers :
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-Key; Type = Secret; Value = af_auth
Copy 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'
}
};
}
Copy 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
Action 1 - Create ADO Work Item
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
Copy 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;
}
Copy 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
URL : <defined in Request Script>
Headers :
Key = Content-Type; Type = Value; Value = application/json
Key = X-SSAPI-Key; Type = Secret; Value = af_auth
Copy 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'
}
};
}
Copy 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 :
vulndb_client_id - your VulnDB Client Id
vulndb_client_secret - your VulnDB Client Secret
Action 1 - Get VulnDB OAuth Token
URL : https://vulndb.flashpoint.io/oauth/token
Headers :
Key = Content-Type; Type = Value; Value = application/json
Copy 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'
}
}
};
Copy 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>
Copy 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'
}
};
}
Copy 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
Copy 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;
}
Copy 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'
}
};
}
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
Action 1 - Schedule Vulnerability Scan in Tenable
Headers :
Key = Accept; Type = Value; Value = application/json
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = tenable_auth
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
Action 1 - Post Slack Message
Headers :
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = slack_auth
Copy 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
};
}
Copy 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
Action 1 - Post Teams Message
URL : <YOUR-INCOMING-WEBHOOK>
Headers :
Key = Content-Type; Type = Value; Value = application/json
Key = Accept; Type = Value; Value = */*
Copy 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
}
]
}
}
]
}
}
};
Copy 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
Action 1 - Send Vulnerability to PowerBI
Headers :
Key = Content-Type; Type = Value; Value = application/json
Copy 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
}
]
}
};
Copy 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
Action 1 - Get Salesforce OAuth Token
URL : https://login.salesforce.com/services/oauth2/token
Headers :
Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
Copy 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
}
};
Copy 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
URL : https://<YOUR-SALESFORCE>/services/data/v63.0/sobjects/Opportunity
Headers :
Key = Content-Type; Type = Value; Value = application/json
Copy 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'
}
};
}
Copy 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
Action 1 - Send Data to Webhook
URL : <YOUR-WEBHOOK-ADDRESS>
Headers :
Key = Content-Type; Type = Value; Value = application/json
Key = Authorization; Type = Secret; Value = auth
Copy // Not required for this webhook
Copy // 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
Action 1 - Send Email to Account Managers
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
Copy return {
request: {
body: {
to: [
"admin@attackforge.com",
{
"user_id": "67b45e6de6da84559d9860fc"
},
],
cc: [
"support@attackforge.com"
],
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>'
},
},
};
Copy 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'
}
};
}