Flows
Last updated
Last updated
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.
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 access to at least one (1) Event; or
You must be granted access to another person's existing Flow
A Flow is comprised of the following:
Name - the name of the Flow.
Event Trigger - the trigger which initiates a Run.
Secrets - any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.
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.
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
Click on Add Access
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:
Manually run the Flow
Edit the Flow, including changing Name, Event Trigger, Actions and Secrets
Enable/Disable the Flow
View Run history and individual Runs, including Re-run
Export the Flow
You can remove a users' access to your Flows from the Settings
.
A Trigger is an action which initiates a Run. Triggers can be initiated from Events or manually initiated.
Internal Events 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
External Events 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.
External Events are coming soon!
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!
A Flow can be assigned to only one Trigger.
Triggers can be assigned to a Flow when either creating or editing the Flow.
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
Refer to the Secret in the Request Script or Response Script
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.
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 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 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
}
}
}
};
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
}
]
}
}
};
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: "..."
}
}
};
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.
The Response is the HTTP server response to a Request.
The Response is made of the of the following components:
The Request Script is the script which will execute before the Request is made.
The Request Script is made up of the following components:
The Response Script is the script which will execute after the Response is returned.
The Request Script is made up of the following components:
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 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.
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 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 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: {}
};
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!',
}
};
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 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
}
};
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.
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.
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.
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.
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.
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.
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!
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'
},
};
}
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',
},
};
}
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'
},
};
}
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'
},
};
}
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'
}
};
}
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>
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',
},
};
}
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',
},
};
}
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',
},
};
}
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'
}
};
}
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
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: [
"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>'
},
},
};
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'
}
};
}
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