LogoLogo
  • AttackForge Support
  • Release Notes
    • 2025
    • 2024
    • 2023
    • 2022
    • 2021
    • 2020
    • 2019
  • Core & Enterprise
    • Getting Started
      • How It Works
      • Requesting A Project
      • Creating & Updating Projects
      • Invite User To Project
      • View Project Team
      • Project Scope
      • Project Workspace
      • Project Notes
      • Project Pages
      • Test Cases
      • Creating Vulnerabilities
      • Updating Vulnerabilities
      • Review & QA
      • Attack Chains
      • Reporting
      • Retesting & Remediation
      • Notifications
      • Custom Fields & Forms
      • Advanced Filtering
      • Vulnerability SLAs
      • User Settings
      • Login Redirects
    • Modules
      • Dashboard
      • Analytics
      • Vulnerabilities
      • Projects
      • Scheduling
      • Portfolios
      • Groups
      • Attack Chains
      • Assets
      • Writeups
      • Test Suites
      • Report Templates
        • Overview
        • Tutorial
        • Tips & Tricks
        • Troubleshooting
        • Template - Report Templates
        • Template - Tags
        • Template - Options
        • Template - Functions
        • Template - Filters
        • Template - Styles
        • Template - Tables
        • Template - Charts
        • Template - Conditions
        • ReportGen CLI
        • ReportGen Library
      • Users
      • Administration
      • Flows
      • Self-Service RESTful API
        • GETTING STARTED
        • EXPORTING TO CSV
        • ADVANCED QUERY FILTER
        • ActivateUser
        • AddProjectMembershipAdministrators
        • AddTestcaseToTestsuite
        • AddTestcasesToTestsuite
        • AddUserToGroup
        • ApproveProjectRequestById
        • ArchivePortfolio
        • ArchiveProject
        • CancelProjectRetestRound
        • CloneProject
        • CompleteProjectRetestRound
        • CreateAssetInLibrary
        • CreateGroup
        • CreatePortfolio
        • CreateProject
        • CreateProjectNote
        • CreateProjectRequest
        • CreateProjectTestCase
        • CreateProjectWorkspaceNote
        • CreateRemediationNote
        • CreateScope
        • CreateTestcaseNote
        • CreateTestsuite
        • CreateUser
        • CreateUsers
        • CreateVulnerability
        • CreateVulnerabilityBulk
        • CreateVulnerabilityLibraryIssue
        • CreateVulnerabilityWithLibrary
        • DeactivateUser
        • DownloadProjectTestCaseFile
        • DownloadProjectTestCaseNoteFile
        • DownloadProjectTestCaseWorkspaceNoteFile
        • DownloadVulnerabilityEvidence
        • DownloadVulnerabilityLibraryFile
        • DownloadWorkspaceFile
        • GetApplicationAuditLogs
        • GetAssets
        • GetAssetsByGroup
        • GetAssetInLibrary
        • GetAssetsInLibrary
        • GetCustomFieldsConfig
        • GetFormConfig
        • GetGroup
        • GetGroups
        • GetMostCommonVulnerabilities
        • GetMostFailedTestcases
        • GetMostVulnerableAssets
        • GetPortfolio
        • GetPortfolios
        • GetPortfolioStream
        • GetProjectAuditLogs
        • GetProjectById
        • GetProjects
        • GetProjectsAndVulnerabilities
        • GetProjectsByGroup
        • GetProjectMembershipAdministrators
        • GetProjectNotes
        • GetProjectReport
        • GetProjectReportData
        • GetProjectRequests
        • GetProjectRequestById
        • GetProjectTestcasesById
        • GetProjectVulnerabilitiesById
        • GetProjectWorkspace
        • GetTestsuiteById
        • GetTestsuites
        • GetUserByEmail
        • GetUserById
        • GetUserByUsername
        • GetUserAuditLogs
        • GetUserGroups
        • GetUserLoginHistory
        • GetUserProjects
        • GetUsers
        • GetVulnerabilityById
        • GetVulnerabilities
        • GetVulnerabilitiesByAssetName
        • GetVulnerabilitiesByGroup
        • GetVulnerabilityLibraryIssues
        • GetVulnerabilityRevisionHistory
        • InviteUserToProject
        • InviteUsersToProjectTeam
        • RejectProjectRequestById
        • RegenerateAPIKey
        • RemoveProjectMembershipAdministrators
        • RemoveProjectTeamMembers
        • RequestNewProjectRetest
        • RestoreProject
        • SendEmail
        • SendDailyCommencementEmail
        • SendDailyCompletionEmail
        • UpdateAssetInLibrary
        • UpdateCustomFieldsConfig
        • UpdateExecSummaryNotes
        • UpdateFormConfig
        • UpdateGroup
        • UpdatePortfolio
        • UpdateProjectById
        • UpdateProjectMembershipAdministrators
        • UpdateProjectNote
        • UpdateProjectRequestById
        • UpdateProjectRetestRound
        • UpdateProjectWorkspaceNote
        • UpdateScope
        • UpdateTestcase
        • UpdateTestcaseOnTestsuite
        • UpdateTestsuite
        • UpdateUserAccessOnGroup
        • UpdateUserAccessOnProject
        • UpdateUser
        • UpdateVulnerabilityById
        • UpdateVulnerabilityLibraryIssue
        • UpdateVulnerabilitySLAs
        • UpdateVulnerabilityWithLibrary
        • UploadTestcaseFile
        • UploadVulnerabilityEvidence
        • UploadVulnerabilityLibraryFile
        • UploadWorkspaceFile
      • Self-Service Events API
        • GETTING STARTED
        • Project Created
        • Project Updated
        • Project Request Created
        • Project Request Updated
        • Project Retest Requested
        • Project Retest Completed
        • Project Retest Cancelled
        • Vulnerability Created
        • Vulnerability Updated
        • Vulnerability Evidence Created
        • Vulnerability Evidence Updated
        • Vulnerability Remediation Note Created
        • Vulnerability Remediation Note Updated
    • AFScript
    • Access Control Matrix
    • Raising Support Tickets
    • Security
  • Contact
Powered by GitBook

Check YouTube for more tutorials: https://youtube.com/@attackforge

On this page
  • Overview
  • Getting Access to Flows
  • Flow Overview
  • Run Overview
  • Sharing Flows with Teams
  • Triggers
  • Internal Events
  • External Events
  • Time-Based Events
  • Assigning Events
  • Secrets
  • Secrets in Headers
  • Secrets in Request/Response Scripts
  • Actions
  • Methods
  • URL
  • Headers
  • Request
  • Response
  • Request Script
  • Response Script
  • Code
  • Data
  • The Data Object
  • The Response Object
  • The Return Statement
  • Runs
  • Run Logs
  • Manually Run Flow
  • Re-Run
  • Importing/Exporting Flows
  • Transferring Flows
  • Examples
  • Create JIRA Issue
  • Update JIRA Issue
  • Create ServiceNow Incident
  • Create Azure DevOps Work Item
  • Prioritize Vulnerability with Threat Intelligence from VulnDB
  • Trigger an Automated Scan in Tenable
  • Create Slack Message
  • Create Teams Message
  • Send Vulnerability to PowerBI
  • Create Salesforce Opportunity
  • Create a Webhook
  • Send Custom Email
  1. Core & Enterprise
  2. Modules

Flows

PreviousAdministrationNextSelf-Service RESTful API

Last updated 3 days ago

Overview

The Flows module is a comprehensive, end-to-end automation engine powered by AFScript.

Flows can help you to automate AttackForge with nearly unlimited systems. You can streamline processes across your organization to save time and focus on what's important.

Some examples you can do with Flows:

  • Integrate your vulnerability data with ticketing tools like Atlassian JIRA, ServiceNow, Azure DevOps, BMC Helix and others.

  • Visualize your pentesting data in powerful tools like Power BI and Tableau

  • 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

  • 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

  • Create custom webhooks.

  • Send custom email notifications on events.

Getting Access to Flows

Flows is included in all AttackForge Enterprise plans, and in the AttackForge Core SME plan. For all others plans, Flows can be add-on from the Administration -> Subscriptions page.

To get started with building a Flow:

  • You must have access to at least one (1) Event; or

  • 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.

  • Event Trigger - the trigger which initiates a Run.

  • 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

  • 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.

Triggers

A Trigger is an action which initiates a Run. Triggers can be initiated from Events or manually initiated.

Internal Events

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

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

Time-Based Events are events which occur at a specified time for example each day at 9am, or on a specified frequency for example every hour. Time-Based Events are particularly useful when something needs to happen on a automated time basis.

For example, each day - find all vulnerabilities which have just exceeded their risk-acceptance date, change their status to open, create a ticket in an external system and notify the vulnerability owner(s) and security team by email and by chat message.

Time-Based Events are coming soon!

Assigning Events

A Flow can be assigned to only one Trigger.

Triggers can be assigned to a Flow when either creating or editing the Flow.

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:

  1. Select the Secret directly in the Headers

  2. Refer to the Secret in the Request Script or Response Script

Secrets in Headers

When creating or modifying Headers within the Action, you can select 'Secret' for the header type. This will then allow you to select from an existing Secret, or create a new Secret.

Secrets in Request/Response Scripts

When creating or modifying the Request Script or the Response Script, you can refer to secrets using the following syntax:

secrets.<KEY>

Where <KEY> is replaced with the Key associated with the Secret.

IMPORTANT: Make sure to select Use Secrets to ensure your secrets are used in your script.

Actions

Actions are either one activity, or a sequence of activities, which are executed in order during a Run.

For example, if the use case for your Flow is:

  • to create a JIRA Issue every time a Vulnerability is created

You may choose to include two (2) Actions in your Flow:

  • Action 1 - Create JIRA Issue

    • This involves formatting the vulnerability into the necessary JIRA Create Issue API format, and making a HTTPS request to the JIRA API to create the issue.

  • Action 2 - Update Vulnerability with JIRA Issue Key

    • This involves making a HTTPS request to the Update Vulnerability Self-Service API to set the JIRA Issue Key custom field.

The primary purpose of an Action is to make an update to a system. The system could be AttackForge (via the Self-Service APIs) or an external system.

Every Action is made up of a Request and a Response.

  • The Request is the HTTP request which is made by the Action.

  • The Response is the HTTP response from the server which received the HTTP request.

Every Action is made up of the following components:

  • 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.

  • Request Script - this is the script which will execute before the Request is made.

  • Response Script - this is the script which will execute after the Response is returned.

IMPORTANT: When more than one Action is included in a Flow, the output of an Action will become the input into the next Action.

Methods

Methods are the HTTP methods/verbs that will be used for the Request i.e. GET, POST, PUT, etc.

The following methods are supported:

  • GET

  • POST

  • PUT

  • PATCH

  • DELETE

Methods can be selected when editing the Action:

Methods can also be programatically set in your Request Script in the Return Statement:

return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': secrets.jira_auth
        },
        body: {
            fields: {
                summary: summary,
                description: description,
                priority: {
                    name: priority
                },
                issuetype: {
                    name: 'Bug'
                },
                labels: labels
            }
        }
    }
};

URL

The URL is the web address that will be used for the Request, for example https://acmecorp.atlassian.net/rest/api/2/issue

The URL can be entered in when editing the Action:

The URL can also be programatically set in your Request Script in the Return Statement. This is useful if your URL has a dynamic component which needs to be computed:

const url = 'https://demo.attackforge.com/api/ss/vulnerability/' + data.vulnerability_id;

return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
            'x-ssapi-key': secrets.af_auth
        },
        body: {
            project_id: project_id,
            custom_fields: [
                {
                    key: 'jira_issue_key',
                    value: jira_issue_key
                }
            ]
        }
    }
};

Headers

The Headers are the HTTP headers that will be sent when the Request is made.

The Headers can be manually entered in when editing the Action:

The Headers can also be programatically set in your Request Script in the Return Statement. This is useful if your Headers have a dynamic component which needs to be computed:

const customHeaderName = 'X_CUSTOM_HEADER_' + customHeader;
const customHeaderValue = customValue;

return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            customHeaderName: customHeaderValue,
            'Authorization': secrets.custom_auth
        },
        body: {
            super_secret_something: "..."
        }
    }
};

Request

The Request is a HTTP/HTTPS request to a web address.

The Request is made up of the following components:

  • URL

  • Method

  • Headers

  • Body - an optional HTTP body. This is typically required for POST, PUT and PATCH HTTP requests.

  • Request Script

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:

  • Response Object

  • Response Script

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:

  • Code

  • Data

  • Return Statement

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

  • Data

  • Return Statement

Code

Flows support AFScript - a powerful interpreted programming language created by AttackForge.

This makes it possible to write logic to help you handle all various use cases for how you want your Flows to work.

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

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

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

Data

Data is contextually relevent information for your Request Script and Response Script.

You can reference the information included within Data as follows:

data.<KEY>

Where <KEY> is replaced with the associated key on the Data Object.

For more information on Data, please see Data Object.

The Data Object

Data is contextually relevant information for your Request Script and Response Script.

The Data Object is an Object that holds the Data.

The first Action in your Flow will contain Data in the Request Script which is relevent to your Trigger Event. For example, if your Flow was assigned to the "vulnerability-created" Event, then your Data Object will contain all of the information relating to the vulnerability.

However from this point forward, you can control how you would like your Data Object to look for the Response Script and any subsequent Actions going forward.

In the following example, we can see that Data Object has vulnerability-related information due to the "vulnerability-created" Event.

You can refer to keys on the Data Object using the following syntax:

data.<KEY>

Using the example above, if you wanted to store the vulnerability Id in a constant, you could do the following:

const vuln_id = data.vulnerability_id;

Keeping with the example above, if you wanted to extract the project Id for the vulnerability, you could do the following:

let project_id = undefined;

if (data.vulnerability_projects) {
    for (let x = 0; x < data.vulnerability_projects.length; x++) {
        if (data.vulnerability_projects[x].id) {
            project_id = data.vulnerability_projects[x].id;
        }
    }
}

If you needed to pass this information to the next step of this Flow, which using the example above will be the Response Script on the first Action - you can include the "data" key in your Response Object and pass in an Object with key/value pairs as follows:

return {
    decision: { 
        status: 'continue'
    },
    data: {
        af_project_id: project_id,
        af_vuln_id: vuln_id,
        af_vuln: data
    }
}
   

Continuing with this example, the Response Script will now have the following Data Object:

data = {
    af_project_id: "...",
    af_vuln_id: "...",
    af_vuln: {
        "vulnerability_title": "...",
        ...
    }
}

When viewing the details of a Run - you can see what Data was passed as input and output into an Action.

If you would need to log the Data Object for visibility or debugging during execution of a Run you can do the following:

Logger.debug('Data:');
Logger.debug(JSON.stringify(data));

You can then view the details in the Run Logs

The Response Object

The Response Object is the HTTP information which is sent back from the server during the Response.

The Response Object is available in the Response Script.

The Response Object is made up of the following:

  • HTTP Response Status Code

  • HTTP Response Headers

  • HTTP Response Body

You can refer to keys on the Response Object using the following syntax:

response.<KEY>

An example of the Response Object:

response = {
    "statusCode": 200,
    "headers": {},
    "body": ""
}
  • The statusCode will be accessible as a Number.

  • The headers will be accessible as a Object.

  • The body will be accessible as a String.

If you are expecting the body to be returned as a JSON payload (which is common for RESTful APIs) - you must first parse the body into JSON format before you can access it using dot or bracket notation, see example below:

const body = JSON.parse(response.body);

When viewing the details of a Run - you can see the Response Status Code and Response Headers and Response Body:

If you would need to log the Response Object for visibility or debugging during execution of a Run you can do the following:

Logger.debug('Response:');
Logger.debug(JSON.stringify(response));

You can then view the details in the Run Logs

The Return Statement

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

The Return Statement is made up of the following components:

  • Decision

  • Request

  • Data

return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }, 
   request: { 
     url: 'https://www.attackforge.com/api',
     body: {}, 
     headers: {}, 
     method: 'GET', 
   }, 
   data: {} 
};

Decision

return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }
};

The decision is the action that your script will take. A decision is made up of the following components:

  • status - a supported status (see below)

  • message - an optional message to display in the logs

You can also include the decision as a string if you do not need to include a message:

return {
   decision: "continue" //also supports "next", "abort", "finish"
};

The following statuses are supported:

CONTINUE

Continue will instruct your script to continue with normal execution. For example, continuing in the Request Script will result in the Request being made. Continuing in the Response Script will result in executing the next Action.

Example using Continue:

return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }
};

NEXT

Next will instruct your Request Script or Response Script to move to the next Action. This is useful if a Request in your Flow is conditional i.e. it may or may not need to be made.

Example using Next:

return {
   decision: { 
     status: 'next', 
     message: 'Asset already exists. Move to create vulnerability', 
   }
};

ABORT

Abort will terminate your Flow as an error condition. This is useful in cases where your Flow can no longer proceed due to various reasons.

Example using Abort:

return {
   decision: { 
     status: 'abort', 
     message: 'Auth token not generated. Check if credentials are valid?', 
   }
};

FINISH

Finish will terminate your Flow as an success condition. This is how you would normally terminate a Flow.

Example using Finish:

return {
   decision: { 
     status: 'finish', 
     message: 'Vulnerability created!', 
   }
};

Request

The Request is the HTTP Request information that your Request Script will use. Request is made up of the following components:

  • url

  • body

  • headers

  • method

return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }, 
   request: { 
     url: 'https://www.attackforge.com/api',
     body: {}, 
     headers: {}, 
     method: 'GET', 
   }
};

URL

The URL is the URL that the Request will be sent to. This field is optional. If it is not specified, the URL set in the Action will prevail. If it is specified, it will take precedence over the URL set in the Action.

BODY

The Body is the payload body that will be sent in the Request. This field is optional. If it is not specified, no body will be sent in the Request.

Example with a JSON Body:

return {
    decision: { 
        status: 'continue'
    },
    request: {
        body: {
            fields: {
                project: {
                    key: jiraProjectKey
                },
                summary: summary,
                description: description,
                priority: {
                    name: priority
                },
                issuetype: {
                    name: 'Bug'
                },
                labels: labels
            }
        }
    }
};

HEADERS

The Headers are the HTTPS Headers that the Request will use. This field is optional. If it is not specified, the Headers set in the Action will prevail. If it is specified, it will take precedence over the Headers set in the Action.

Example Headers:

return {
    decision: { 
        status: 'continue',
    },
    request: {
        headers: {
            'Content-Type': 'application/json',
            'x-ssapi-key': secrets.af_auth,
        }
    }
};

METHOD

The Method is the HTTPS Method that the Request will be sent to. This field is optional. If it is not specified, the Method set in the Action will prevail. If it is specified, it will take precedence over the Method set in the Action.

Example Headers:

return {
    decision: { 
        status: 'continue',
    },
    request: {
        method: 'GET' //supports GET, POST, PUT, PATCH, DELETE
    }
};

Data

Data is an object that can be used to pass information between Request Script to Response Script, and from Response Script to the next Action.

The Data in the Request Script for the first Action of the Flow will be the Event information, for example "vulnerability-created" fields. From then onwards, you can override what the data will be in the Response Script and beyond.

Example with Data in the Request Script of the first Action in the Flow:

return {
    decision: { 
        status: 'continue',
    },
    data: {
        af_project_id: afProjectId,
        af_vuln: data
    }
};

Example with Data in the Response Script of the first Action in the Flow. This example will pass on the "af_project_id" and "af_vuln" that was passed in the Data from the Request Script on to the next Action in the Flow.

return {
    decision: { 
        status: 'continue',
    },
    data: {
        af_project_id: data.af_project_id,
        af_vuln: data.af_vuln
    }
};

Runs

Flow Runs is where you can view the history of each Run you have access to, across all of your Flows. It is a consolidated view for every Run. You can access this page from the Flows module.

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

Run Logs

When clicking on a Run, you will see an overview of the history for that Run, including the following information:

  • Run Status - Whether the Run was Completed or Failed.

  • Event - the related Event which triggered the Flow.

  • Started - the timestamp of when the Run started execution.

  • Duration - the duration (in milliseconds) for execution of the Run until completion or failure.

  • Actions - the Actions in-scope during the Run.

  • Data - the input and output of each Action.

  • HTTP - the URL, Method, Headers, Request Body, Response Status Code and Response Body for each Action in the Flow.

  • Logs - the logs for each Action.

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

Manually Run Flow

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

Re-Run

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

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

Importing/Exporting Flows

You can export your existing Flows and import Flows at any time. This utility can help to:

  • Share Flows with others, without giving them access to your Flows

  • Create a backup of your Flows

  • Bootstrap a new Flow based on a template

To export a Flow, click on the Export Flow button. The Flow will be exported with a .flow extension format.

IMPORTANT: Exported Flows do not contain the values of Secrets. However for compatibility and convenience, the Secret key/name will be included in the export.

To import a Flow, click on Import Flow:

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

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

Transferring Flows

Flows run under the context of an Owner. That means, when the event happens for the Owner, the Flow will trigger.

You can transfer ownership of Flows when required, so that the Flow can operate under the context of another user.

To transfer ownership of your Flow - open the Flow settings page and click on Transfer

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

The user will immediately receive ownership of the Flow, however it will be disabled until they review the Flow and chose to enable it.

Examples

The following examples can help you to automate and integrate with common security and enterprise tools. Each example includes a Flow which can be imported in to your own AttackForge to help you get started fast!

Create JIRA Issue

The purpose of this example is to create a JIRA Issue when a Vulnerability is created in AttackForge, and to update AttackForge to assign the JIRA Issue Key against the Vulnerability.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Vulnerability Created

  • Secrets:

    • af_auth - your AttackForge Self-Service API token.

    • jira_auth - your JIRA API token

Action 1 - Create JIRA Issue

  • Method: POST

  • URL: https://<YOUR-JIRA>/rest/api/2/issue

  • Headers:

    • Key = Accept; Type = Value; Value = application/json

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = jira_auth

  • Request Script:

let jiraProjectKey = '';
let afProjectId = '';

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    const project = data.vulnerability_projects[i];

    if (project.custom_fields) {
      for (let j = 0; j < project.custom_fields.length; j++) {
        const projectCustomField = project.custom_fields[j];

        if (projectCustomField.key === 'jira_project_key') {
          jiraProjectKey = projectCustomField.value;
          break;
        }
      }
    }

    if (project.id) {
      afProjectId = project.id;
    }

    if (afProjectId && jiraProjectKey) {
      break;
    }
  }
}

jiraProjectKey = String.replace(jiraProjectKey, m/"/g, '\"');
jiraProjectKey = String.replace(jiraProjectKey, m/\\"/g, '\"');

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody(jiraProjectKey)
  }
};

function buildRequestBody(jiraProjectKey) {
  let summary = '';

  if (data.vulnerability_title) {
    summary = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  summary = String.replace(summary, m/"/g, '\"');
  summary = String.replace(summary, m/\\"/g, '\"');

  let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';

  if (data.vulnerability_title) {
    description += 'h1. Vulnerability: ' + data.vulnerability_title + ' \n\n ';
  }

  if (data.vulnerability_description) {
    description += '*Description* \n ' + data.vulnerability_description + ' \n\n ';
  }

  let cvssScore;
  let cvssVector;
  let cvssVectorEscaped;

  if (data.vulnerability_priority && data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
        cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
      }
    }

    if (cvssScore && cvssVector && cvssVectorEscaped) {
      description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
    }
    else {
      description += '*Technical Severity* \n ||*Rating* \n |' + data.vulnerability_priority + ' \n\n ';
    }
  }

  if (data.vulnerability_attack_scenario && data.vulnerability_likelihood_of_exploitation) {
    description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vulnerability_attack_scenario + ' \n\n ';
  }

  if (data.vulnerability_affected_asset_name) {
    description += '*Affected Asset* \n\n * ' + data.vulnerability_affected_asset_name + ' \n\n ';
  }
  else if (data.vulnerability_affected_assets) {
    description += '*Affected Asset(s)*: \n';

    for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
      if (data.vulnerability_affected_assets[i].asset?.name) {
        description += '\n * ' + data.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data.vulnerability_steps_to_reproduce) {
    description += '*Steps to Reproduce* \n\n ' + data.vulnerability_steps_to_reproduce + ' \n\n ';
  }

  let notes;

  if (data.vulnerability_notes) {
    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (notes === undefined) {
        notes = data.vulnerability_notes[i].note;
      }
      else {
        notes += '\n\n' + data.vulnerability_notes[i].note;
      }
    }
  }

  if (notes) {
    description += '*Notes* \n\n ' + notes + ' \n\n ';
  }

  if (data.vulnerability_remediation_recommendation) {
    description += '*Recommendations* \n\n ' + data.vulnerability_remediation_recommendation + ' \n\n ';
  }

  const labels = [];
  const tags = [];

  if (data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      let newtag = '* ' + data.vulnerability_tags[i] + '\n';
      newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');

      Array.push(tags, newtag);

      let label = data.vulnerability_tags[i];
      label = String.replace(label, m/\s/g, '');

      Array.push(labels, label);
    }
  }

  Logger.debug(JSON.stringify(tags));

  if (tags.length > 0) {
    description = description + '*Tags* \n\n ';
    
    for (let i = 0; i < tags.length; i++) {
      description = description + tags[i];
    }    
  }

  description = String.replace(description, m/"/g, '\"');
  description = String.replace(description, m/\\"/g, '\"');

  let priority = 'Lowest';

  if (data.vulnerability_priority === 'Critical') {
    priority = 'Highest';
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 'High';
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 'Medium';
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 'Low';
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 'Lowest';
  }

  return {
    fields: {
      project: {
          key: jiraProjectKey
      },
      summary: summary,
      description: description,
      priority: {
          name: priority
      },
      issuetype: {
          name: 'Bug'
      },
      labels: labels
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.key) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing JIRA Issue Key (body.key)',
    }, 
  };
}
else if (!data?.af_project_id) {
  if (data) {
    Logger.error(JSON.stringify(data));
  }  

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    },
  };
}
else if (!data?.af_vuln_id) {
  if (data) {
    Logger.error(JSON.stringify(data));
  } 

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data.af_vuln_id,
      jira_issue_key: body.key
    } 
  };
}

Action 2 - Update AF Vuln with JIRA Issue Key

  • Method: PUT

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-Key; Type = Secret; Value = af_auth

  • Request Script:

if (data?.jira_issue_key && data?.af_vuln_id && data?.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.com/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
          project_id: data.af_project_id,
          custom_fields: [
            {
              key: 'jira_issue_key',
              value: data.jira_issue_key
            }
          ]
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'missing required fields'
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}

Update JIRA Issue

The purpose of this example is to update a JIRA Issue when a Vulnerability is updated in AttackForge.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

  • Event: Vulnerability Updated

  • Secrets:

    • jira_auth - your JIRA API token

Action 1 - Get JIRA Issue

  • Method: GET

  • URL: <defined in Request Script>

  • Headers:

    • Key = Accept; Type = Value; Value = application/json

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = jira_auth

  • Request Script:

if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted.',
      }
  };
}

let jiraIssueKey;

if (data.vulnerability_custom_fields) {
  for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
    if (data.vulnerability_custom_fields[i].key === 'jira_issue_key') {
      jiraIssueKey = data.vulnerability_custom_fields[i].value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'no JIRA Issue Key found',
      }
  };
}

if (jiraIssueKey) {
  return {
    data: {
      vuln: data
    },
    request: {
      url: 'https://attackforge.atlassian.net/rest/api/3/issue/' + jiraIssueKey,
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.key) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'finish',
      message: 'missing JIRA Issue Key (body.key) - Issue likely deleted',
    }
  };
}
else if (!data?.vuln) {
  if (data) {
    Logger.error(JSON.stringify(data));
  }

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.vuln',
    },
  };
}
else {
  return {
    data: {
      vuln: data?.vuln,
      jira_issue_key: body?.key
    } 
  };
}

Action 2 - Update JIRA Issue

  • Method: <defined in Request Script>

  • URL: <defined in Request Script>

  • Headers:

    • <defined in Request Script>

  • Request Script:

if (!data?.jira_issue_key) {
  return {
    decision: {
      status: 'finish',
      message: 'no JIRA Issue Key found',
    }
  };
}
else if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'missing data.vuln',
    }
  };
}
else {
  let url = 'https://attackforge.atlassian.net/rest/api/2/issue/' + data.jira_issue_key;

  return {
    request: {
      url: url,
      body: buildRequestBody(),
    }
  };
}

function buildRequestBody() {
  let summary = '';

  if (data.vuln.vulnerability_title) {
    summary = '[SECURITY][VULNERABILITY] ' + data.vuln.vulnerability_title;
  }

  summary = String.replace(summary, m/"/g, '\"');
  summary = String.replace(summary, m/\\"/g, '\"');

  let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';

  if (data?.vuln?.vulnerability_title) {
    description += 'h1. Vulnerability: ' + data.vuln.vulnerability_title + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_description) {
    description += '*Description* \n ' + data.vuln.vulnerability_description + ' \n\n ';
  }

  let cvssScore;
  let cvssVector;
  let cvssVectorEscaped;

  if (data?.vuln?.vulnerability_priority && data?.vuln?.vulnerability_tags) {
    for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
      const tag = data.vuln.vulnerability_tags[i];

      if (tag =~ m/CVSSv3.1 Base Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Base Score: ', '');
      }

      if (tag =~ m/CVSSv3.1 Temporal Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Temporal Score: ', '');
      }

      if (tag =~ m/CVSSv3.1 Environmental Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Environmental Score: ', '');
      }

      if (tag =~ m/CVSS: 3.1\//) {
        cvssVector = String.replace(tag, 'CVSS:3.1/', '');
        cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
      }
    }

    if (cvssScore && cvssVector && cvssVectorEscaped) {
      description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vuln.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
    }
    else {
      description += '*Technical Severity* \n ||*Rating* \n |' + data.vuln.vulnerability_priority + ' \n\n ';
    }
  }

  if (data?.vuln?.vulnerability_attack_scenario && data?.vuln?.vulnerability_likelihood_of_exploitation) {
    description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vuln.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vuln.vulnerability_attack_scenario + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_affected_asset_name) {
    description += '*Affected Asset* \n\n * ' + data.vuln.vulnerability_affected_asset_name + ' \n\n ';
  }
  else if (data?.vuln?.vulnerability_affected_assets) {
    description += '*Affected Asset(s)*: \n';

    for (let i = 0; i < data.vuln.vulnerability_affected_assets.length; i++) {
      if (data.vuln.vulnerability_affected_assets[i].asset?.name) {
        description += '\n * ' + data.vuln.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data?.vuln?.vulnerability_steps_to_reproduce) {
    description += '*Steps to Reproduce* \n\n ' + data.vuln.vulnerability_steps_to_reproduce + ' \n\n ';
  }

  let notes;

  if (data?.vuln?.vulnerability_notes) {
    for (let i = 0; i < data.vuln.vulnerability_notes.length; i++) {
      if (notes === undefined) {
        notes = data.vuln.vulnerability_notes[i].note;
      }
      else {
        notes += '\n\n' + data.vuln.vulnerability_notes[i].note;
      }
    }
  }

  if (notes) {
    description += '*Notes* \n\n ' + notes + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_remediation_recommendation) {
    description += '*Recommendations* \n\n ' + data.vuln.vulnerability_remediation_recommendation + ' \n\n ';
  }

  const labels = [];
  const tags = [];

  if (data?.vuln?.vulnerability_tags) {
    for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
      let newtag = '* ' + data.vuln.vulnerability_tags[i] + '\n';
      newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');

      Array.push(tags, newtag);

      let label = data.vuln.vulnerability_tags[i];
      label = String.replace(label, m/\s/g, '');

      Array.push(labels, label);
    }
  }

  if (tags.length > 0) {
    description += '*Tags* \n\n ';

    for (let i = 0; i < tags.length; i++) {
      description = description + tags[i];
    }
  }

  description = String.replace(description, m/ "/g, '\"');
  description = String.replace(description, m/\\"/g, '\"');

  let priority = 'Lowest';

  if (data?.vuln?.vulnerability_priority === 'Critical') {
    priority = 'Highest';
  }
  else if (data?.vuln?.vulnerability_priority === 'High') {
    priority = 'High';
  }
  else if (data?.vuln?.vulnerability_priority === 'Medium') {
    priority = 'Medium';
  }
  else if (data?.vuln?.vulnerability_priority === 'Low') {
    priority = 'Low';
  }
  else if (data?.vuln?.vulnerability_priority === 'Info') {
    priority = 'Lowest';
  }

  return {
    fields: {
      summary: summary,
      description: description,
      priority: {
        name: priority
      },
      issuetype: {
        name: 'Bug'
      },
      labels: labels
    }
  };
}
  • Response Script:

if (response.statusCode === 204) {
  return {
    decision: 'finish'
  };
}
else {
  Logger.info(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'JIRA Issue update failed',
    },
  };
}

Create ServiceNow Incident

The purpose of this example is to create a ServiceNow Incident when a Vulnerability is created in AttackForge, and to update AttackForge to assign the SNOW Incident Id against the Vulnerability.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Vulnerability Created

  • Secrets:

    • af_auth - your AttackForge Self-Service API token.

    • snow_auth - your SNOW API Key

Action 1 - Create SNOW Incident

  • Method: POST

  • URL: https://<YOUR-SNOW>/api/now/table/incident

  • Headers:

    • Key = Accept; Type = Value; Value = application/json

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = snow_auth

  • Request Script:

let afProjectId;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id) {
      afProjectId = data.vulnerability_projects[i].id;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is undefined'
    }
  };
}

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  const body = {};

  if (data.vulnerability_title) {
    body.short_description = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  let priority = 3;

  if (data.vulnerability_priority === 'Critical') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 2;
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 3;
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 3;
  }

  body.impact = priority;
  body.urgency = priority;
  body.priority = priority;
  body.severity = priority;

  let description = '';

  if (data.vulnerability_affected_asset_name) {
    description += 'Affected Asset: ' + data.vulnerability_affected_asset_name + '\n\n';
  }
  else if (data.vulnerability_affected_assets) {
    description += 'Affected Asset(s): ';

    for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
      if (data.vulnerability_affected_assets[i].asset?.name) {
        description += '\n* ' + data.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data.vulnerability_description) {
    description += 'Description: \n' + data.vulnerability_description + '\n\n';
  }

  if (data.vulnerability_likelihood_of_exploitation) {
    description += 'Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '\n\n';
  }

  let cvssScore;
  let cvssVector;

  if (data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
      }
    }
  }

  if (cvssScore && cvssVector) {
    description += 'CVSS Score: ' + cvssScore + '\nCVSS Vector: ' + cvssVector + '\n\n';
  }

  if (data.vulnerability_attack_scenario) {
    description += 'Attack Scenario (Technical Risk): \n' + data.vulnerability_attack_scenario + '\n\n';
  }

  if (data.vulnerability_remediation_recommendation) {
    description += 'Remediation Recommendation: \n' + data.vulnerability_remediation_recommendation + '\n\n';
  }

  if (data.vulnerability_steps_to_reproduce) {
    description += 'Steps to Reproduce: \n' + data.vulnerability_steps_to_reproduce + '\n\n';
  }

  if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
    description += 'Notes:';

    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (data.vulnerability_notes[i].note) {
        description += '\n' + data.vulnerability_notes[i].note;
      }
    }
  }

  if (data.vulnerability_tags && data.vulnerability_tags.length > 0) {
    description += 'Tags:';

    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      description += '\n* ' + data.vulnerability_tags[i];
    }
  }

  body.description = description;

  return body;
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.result?.sys_id) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing SNOW Incident SysId (body.result.sys_id)',
    },
  };
}
else if (!data?.af_project_id) {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    },
  };
}
else if (!data?.af_vuln_id) {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data?.af_vuln_id,
      snow_incident_number: body.result.number
    } 
  };
}

Action 2 - Update AF Vuln with SNOW Incident Id

  • Method: PUT

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-Key; Type = Secret; Value = af_auth

  • Request Script:

if (data.snow_incident_number && data.af_vuln_id && data.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
        project_id: data.af_project_id,
        custom_fields: [
          {
            key: 'snow_incident_number',
            value: data.snow_incident_number
          }
        ]
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'missing required fields'
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}

Create Azure DevOps Work Item

The purpose of this example is to create a Azure DevOps Work Item when a Vulnerability is created in AttackForge, and to update AttackForge to assign the Azure DevOps Work Item Id against the Vulnerability.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Vulnerability Created

  • Secrets:

    • af_auth - your AttackForge Self-Service API token.

    • ado_auth - your ADO Personal Access Token

Action 1 - Create ADO Work Item

  • Method: POST

  • URL: https://dev.azure.com/<YOUR-ADO-TENANT>/<YOUR-ADO-PROJECT>/_apis/wit/workitems/$Issue?api-version=6.1-preview.3

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json-patch+json

    • Key = Authorization; Type = Secret; Value = ado_auth

  • Request Script:

let afProjectId;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id) {
      afProjectId = data.vulnerability_projects[i].id;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is undefined',
    },
  };
}

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  const fields = [];

  let title = '';

  if (data.vulnerability_title) {
    title = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  Array.push(fields, {
    from: null,
    op: 'add',
    path: '/fields/System.Title',
    value: title
  });

  let priority = 4;

  if (data.vulnerability_priority === 'Critical') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 2;
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 3;
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 4;
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 4;
  }

  Array.push(fields, {
    from: null,
    op: 'add',
    path: '/fields/Microsoft.VSTS.Common.Priority',
    value: priority
  });

  if (data.vulnerability_priority) {
    Array.push(fields, {
      from: null,
      op: 'add',
      path: '/fields/System.Tags',
      value: data.vulnerability_priority + ', Security Vulnerability'
    });
  }

  let description = '';

  if (data.vulnerability_title) {
    description += '<h1>[SECURITY][VULNERABILITY] ' + data.vulnerability_title + '</h1><br/>';
  }

  if (data.vulnerability_description) {
    const sanitizedDescription = String.replace(data.vulnerability_description, m/\r\n|\n|\r/gi, '<br>');
    description += '<h2>Description</h2><p>' + sanitizedDescription + '</p>';
  }

  if (data.vulnerability_affected_asset_name) {
    description += '<h2>Affected Asset</h2><ul><li>' + data.vulnerability_affected_asset_name + '</li></ul>';
  }
  else if (data.vulnerability_affected_assets) {
      description += '<h2>Affected Asset(s)</h2><ul>';

      for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
        if (data.vulnerability_affected_assets[i].asset?.name) {
          description += '<li>' + data.vulnerability_affected_assets[i].asset.name + '</li>';
        }
      }

      description += '</ul>';
  }

  let cvssScore;
  let cvssVector;

  if (data.vulnerability_priority && data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
      }
    }

    if (cvssScore && cvssVector) {
      description +=  
        "<h2>Technical Severity</h2>" +
        "<table style='text-align: center; vertical-align: middle; font-size:15px;'>" +
          "<thead>" +
            "<td><b>Rating</b></td>" +
            "<td><b>CVSSv3.1 Score</b></td>" +
          "</thead>" +
          "<tbody>" +
            "<td>" + data.vulnerability_priority + "</td>" +
            "<td>" + cvssScore + "</td>" +
          "</tbody>" +
        "</table>" +
        "<p>CVSS 3.1 Vector String: <a href='https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=" + cvssVector + "'>" + cvssVector + "</a></p>";
    }
    else {
      description += 
        "<h2>Technical Severity</h2>" +
        "<table style='text-align: center; vertical-align: middle; font-size:15px;'>" +
          "<thead>" +
            "<td><b>Rating</b></td>" +
          "</thead>" +
          "<tbody>" +
            "<td>" + data.vulnerability_priority + "</td>" +
          "</tbody>" +
        "</table>";
    }
  }

  if (data.vulnerability_likelihood_of_exploitation && data.vulnerability_attack_scenario) {
    const sanitizedAttackScenario = String.replace(data.vulnerability_attack_scenario, m/\r\n|\n|\r/gi, '<br>');

    description += 
      "<h2>Attack Scenario (Technical Risk)</h2>" +
      "<p>Likelihood of Exploitation: " + data.vulnerability_likelihood_of_exploitation + "/10</p>" +
      "<p>" + sanitizedAttackScenario + "</p>";
  }

  if (data.vulnerability_remediation_recommendation) {
    const sanitizedRemediationRecommendation = String.replace(data.vulnerability_remediation_recommendation, m/\r\n|\n|\r/gi, '<br>');
    description += "<h2>Recommendations</h2><p>" + sanitizedRemediationRecommendation + "</p>";
  }

  if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
    description += "<h2>Notes</h2>";

    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (data.vulnerability_notes[i].note) {
        const sanitizedNote = String.replace(data.vulnerability_notes[i].note, m/\r\n|\n|\r/gi, '<br>');
        description += '<p>' + sanitizedNote + '</p>';
      }
    }
  }

  if (data.vulnerability_steps_to_reproduce) {
    const sanitizedPOC = String.replace(data.vulnerability_steps_to_reproduce, m/\r\n|\n|\r/gi, '<br>');
    description += "<h2>Steps to Reproduce</h2><p>" + sanitizedPOC + "</p>";
  }

  if (data.vulnerability_tags) {
    description += "<h2>Tags</h2><ul>";

    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      description += '<li>' + data.vulnerability_tags[i] + '</li>';
    }

    description += '</ul>';
  }

  Array.push(fields, {
      from: null,
      op: 'add',
      path: "/fields/System.Description",
      value: description
  });

  return fields;
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8; api-version=6.1-preview.3') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8; api-version=6.1-preview.3'
    }
  };
}

if (!body?.id) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing ADO Work Item Id (body.id)',
    },
  };
}
else if (!data?.af_project_id) {
  Logger.error(JSON.stringify(data ?? {}));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    }, 
  };
}
else if (!data?.af_vuln_id) {
  Logger.error(JSON.stringify(data ?? {}));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  Logger.debug('normal result');

  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data?.af_vuln_id,
      ado_work_item_id: body.id
    } 
  };
}

Action 2 - Update AF Vuln with ADO Work Item Id

  • Method: PUT

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-Key; Type = Secret; Value = af_auth

  • Request Script:

if (data.ado_work_item_id && data.af_vuln_id && data.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
        project_id: data.af_project_id,
        custom_fields: [
          {
            key: 'ado_work_item_id',
            value: JSON.stringify(data.ado_work_item_id)
          }
        ]
      }
    }
  };
}
else {
    return {
        decision: { 
            status: 'abort',
            message: 'missing required fields'
        }
    };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}

Prioritize Vulnerability with Threat Intelligence from VulnDB

The purpose of this example is to prioritize a vulnerability based on threat intelligence information harnessed from FlashPoint VulnDB and to apply a custom score/rating to the vulnerability.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Vulnerability Created

  • Secrets:

    • af_auth - your AttackForge Self-Service API token.

    • vulndb_client_id - your VulnDB Client Id

    • vulndb_client_secret - your VulnDB Client Secret

Action 1 - Get VulnDB OAuth Token

  • Method: POST

  • URL: https://vulndb.flashpoint.io/oauth/token

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

  • Request Script:

let cve;

if (data.vulnerability_custom_fields) {
    for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
      if (data.vulnerability_custom_fields[i].key === 'cve') {
        cve = data.vulnerability_custom_fields[i].value;
        break;
      }
    }
}

if (!cve) {
  return {
    decision: { 
      status: 'finish',
      message: 'No CVE found'
    }
  };
}

return {
  data: {
    cve: cve,
    vuln: data
  },
  request: {
    body: {
      client_id: secrets.vulndb_client_id,
      client_secret: secrets.vulndb_client_secret,
      grant_type: 'client_credentials'
    }
  }
};
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (body?.access_token) {
  return {
    data: {
      vulnDBToken: body.access_token,
      cve: data?.cve,
      vuln: data?.vuln
    } 
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      cause: 'VulnDB access token not found'
    }
  };
}

Action 2 - Get Threat Intel for Vuln from VulnDB

  • Method: <defined in Request Script>

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • <others defined in Request Script>

  • Request Script:

if (data?.vulnDBToken && data?.cve && data?.vuln) {
  return {
    data: {
      cve: data.cve,
      vuln: data.vuln
    },
    request: { 
      url: 'https://vulndb.flashpoint.io/api/v2/vulnerabilities/' + data.cve + '/find_by_cve_id',
      headers: {
        Authorization: 'Bearer ' + data.vulnDBToken
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'VulnDB access token not found'
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (!body?.results?[0]) {
  return {
    decision: { 
      status: 'abort',
      message: 'VulnDB CVE not found'
    }
  };
}

return {
  data: {
    cve: data?.cve,
    vulnDB: body.results[0],
    vuln: data?.vuln
  } 
};

Action 3 - Apply Threat Intel, Prioritize Vulnerability and Update Vulnerability

  • Method: <defined in Request Script>

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-KEY; Type = Secret; Value = af_auth

  • Request Script:

if (data?.vuln && data?.vulnDB) {
  const score = calculateScore(data.vuln, data.vulnDB);
  const priority = convertScoreToPriority(score);

  return {
      request: {
        url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.vuln.vulnerability_id,
        body: {
          priority: priority,
          custom_fields: [
            {
              key: 'threat_score',
              value: JSON.stringify(score)
            }
          ]
        }
      }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Missing required vuln data'
    }
  };
}

function calculateScore(vuln, vdb) {
  let score = 0;

  // Maximum score = 140
  // Higher the score = Higher Priority to fix!

  // [ THREAT CONTEXT ]

  const internetFacing = isInternetFacing(vuln);

  // Is the threat related to exposed/internet-facing services?
  if (internetFacing) {
    score += 10;
  }

  if (vdb.cvss_version_three_metrics) {
    for (let i = 0; i < vdb.cvss_version_three_metrics.length; i++) {
      const metrics = vdb.cvss_version_three_metrics[i];

      // Is the CVSS score 8 or higher?
      if (metrics.score !== undefined && metrics.score >= 8) {
        score += 5;
      }

      // Is the threat easily exploitable?
      if (metrics.attack_complexity === 'HIGH') {
        score += 1;
      }
      else if (metrics.attack_complexity === 'MEDIUM') {
        score += 5;
      }
      else if (metrics.attack_complexity === 'LOW') {
        score += 10;
      }

      // No user interaction required?
      if (metrics.user_interaction === 'NONE') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
    }
  }
  else if (vdb.cvss_metrics) {
    for (let i = 0; i < vdb.cvss_metrics.length; i++) {
      const metrics = vdb.cvss_metrics[i];

      // Is the CVSS score 8 or higher?
      if (metrics.score !== undefined && metrics.score >= 8) {
        score += 5;
      }

      // Is the threat easily exploitable?
      if (metrics.attack_complexity === 'HIGH') {
        score += 1;
      }
      else if (metrics.attack_complexity === 'MEDIUM') {
        score +=  5;
      }
      else if (metrics.attack_complexity === 'LOW') {
        score += 10;
      }
    }
  }

  if (vdb.classifications) {
    for (let i = 0; i < vdb.classifications.length; i++) {
      const classifications = vdb.classifications[i];

      // Is there a public exploit available for this threat?
      if (classifications.name === 'exploit_public') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // Does this threat require configuration changes?
      else if (classifications.name === 'solution_workaround') {
        score += 5;
      }
      // Does the threat grant unauthorized access?
      else if (classifications.name === 'location_remote') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // Disclosure in the wild?
      else if (classifications.name === 'disclosure_in_wild') {
        score += 5;
      }
      else if (classifications.name === 'disclosure_uncoordinated_disclosure') {
        score += 5;
      }
      // Wormable?
      else if (classifications.name === 'exploit_wormified') {
        if (isInternetFacing) {
          score += 15;
        }
        else {
          score += 10;
        }
      }
      // Virus / Malware?
      else if (classifications.name === 'exploit_virus_malware') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // PoC Public?
      else if (classifications.name === 'exploit_poc_public') {
        if (isInternetFacing) {
          score += 15;
        }
        else {
          score += 10;
        }
      }
    }
  }

  // Is a patch available for this threat?
  let patchExists = false;

  if (vdb.classifications) {
    for (let i = 0; i < vdb.classifications.length; i++) {
      const classifications = vdb.classifications[i];

      if (classifications.name === 'solution' || classifications.name === 'solution_upgrade') {
        patchExists = true;
        break;
      }
    }
  }
  else if (vdb.cvss_version_three_metrics) {
    for (let i = 0; i < vdb.cvss_version_three_metrics.length; i++) {
      const metrics = vdb.cvss_version_three_metrics[i];

      if (metrics.remediation_level === 'OFFICIAL_FIX' || metrics.remediation_level === 'TEMPORARY_FIX') {
        patchExists = true;
        break;
      }
    }
  }

  if (!patchExists) {
    score += 10;
  }

  // Social Risk Score (Vuln DB)
  if (vdb.social_risk_score === 'High') {
    score += 5;
  }
  else if (vdb.social_risk_score === 'Medium') {
    score += 3;
  }
  else if (vdb.social_risk_score === 'Low') {
    score += 1;
  }

  // What is the likelihood that this threat could be used in a ransomware attack?
  if (vdb.ransomware_likelihood === 'High') {
    if (isInternetFacing) {
      score += score + 10;
    }
    else {
      score += 5;
    }
  }
  else if (vdb.ransomware_likelihood === 'Medium') {
    if (isInternetFacing) {
      score += 5;
    }
    else {
      score += 3;
    }
  }
  else if (vdb.ransomware_likelihood === 'Low') {
    score += 1;
  }

  if (vdb.tags) {
    for (let i = 0; i < vdb.tags.length; i++) {
      // Is this a known CISA KEV vulnerability?
      if (vdb.tags[i] === 'cisa_kev') {
        score += 10;
        break;
      }
    }
  }

  return score;
}

function convertScoreToPriority(score) {
  if (score > 0 && score <= 30) {
    return 'Low';
  }
  else if (score > 30 && score <= 70) {
    return 'Medium';
  }
  else if (score > 70 && score <= 110) {
    return 'High';
  }
  else if (score > 110 && score <= 140) {
    return 'Critical';
  }
  else {
    return 'Info';
  }
}

function isInternetFacing(vuln) {
  if (vuln.vulnerability_affected_assets) {
    for (let i = 0; i < vuln.vulnerability_affected_assets.length; i++) {
      if (vuln.vulnerability_affected_assets[i].asset?.custom_fields) {
        const assetCustomFields = vuln.vulnerability_affected_assets[i].asset?.custom_fields;

        for (let j = 0; j < assetCustomFields.length; j++) {
          if (assetCustomFields[j].key === 'internet_facing' && assetCustomFields[j].value === 'Yes') {
            return true;
          }
        }
      }
    }
  }

  return false;
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'Vulnerability not updated'
    }
  };
}

Trigger an Automated Scan in Tenable

The purpose of this example is to schedule a vulnerability scan in Tenable when a Project is created in AttackForge.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

  • Event: Project Created

  • Secrets:

    • tenable_auth - your Tenable API Key

Action 1 - Schedule Vulnerability Scan in Tenable

  • Method: <insert>

  • URL: <insert>

  • Headers:

    • Key = Accept; Type = Value; Value = application/json

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = tenable_auth

  • Request Script:

<insert>
  • Response Script:

<insert>

Create Slack Message

The purpose of this example is to create a message in a Slack Channel when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

  • Event: Vulnerability Created

  • Secrets:

    • slack_auth - your Slack Bot Token

Action 1 - Post Slack Message

  • Method: POST

  • URL: https://slack.com/api/chat.postMessage

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = slack_auth

  • Request Script:

return {
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  let afProjectId;
  let afProjectName;
  let channel;

  if (data.vulnerability_projects) {
    for (let i = 0; i < data.vulnerability_projects.length; i++) {
      if (data.vulnerability_projects[i].name) {
        afProjectName = data.vulnerability_projects[i].name;
      }

      if (data.vulnerability_projects[i].id) {
        afProjectId = data.vulnerability_projects[i].id;
      }

      if (data.vulnerability_projects[i].custom_fields) {
        for (let j = 0; j < data.vulnerability_projects[i].custom_fields.length; j++) {
          if (data.vulnerability_projects[i].custom_fields[j].key === 'slack_channel') {
            channel = data.vulnerability_projects[i].custom_fields[j].value;
            break;
          }
        }
      }
    }
  }

  let text = '';

  if (data.vulnerability_priority) {
    text += '[' + data.vulnerability_priority + '] ';
  }

  text += 'Vulnerability ';

  if (data.vulnerability_title) {
    text += '[' + data.vulnerability_title + '] ';
  }

  text += 'discovered in Project';

  if (afProjectName) {
    text += ' [' + afProjectName + ']';
  }

  if (afProjectId) {
    text += ' - <https://afe1.attackforge.dev/projects/' + afProjectId + '/vulnerabilities/' + data.vulnerability_id + '|View Vulnerability>';
  }

  return {
    channel: channel,
    text: text
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (body?.ok === true) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'SLACK message not posted',
    },
  };   
}

Create Teams Message

The purpose of this example is to create a message in a Teams Channel when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

  • Event: Vulnerability Created

  • Secrets:

    • teams_auth - your Incoming Web Hook

Action 1 - Post Teams Message

  • Method: POST

  • URL: <YOUR-INCOMING-WEBHOOK>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Accept; Type = Value; Value = */*

  • Request Script:

let afProjectId;
let afProjectName;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id && data.vulnerability_projects[i].name) {
      afProjectId = data.vulnerability_projects[i].id;
      afProjectName = data.vulnerability_projects[i].name;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is falsy'
    }
  };
}

if (!afProjectName) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectName is falsy'
    }
  };
}

let text = '';

if (data.vulnerability_priority) {
  text += '[' + data.vulnerability_priority + '] ';
}

text += 'Vulnerability ';

if (data.vulnerability_title) {
  text += '[' + data.vulnerability_title + '] ';
}

text += 
  'discovered in Project' + 
  ' [' + afProjectName + ']' + 
  ' - [View Vulnerability](https://afe1.attackforge.dev/projects/' + afProjectId + '/vulnerabilities/' + data.vulnerability_id + ')';


return {
  request: {
    url: 'https://prod-26.australiasoutheast.logic.azure.com:443/workflows/e25ebc22ccf5438190dc46a087450e5c/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=' + secrets.sig,
    body: {
      type: 'message',
      attachments: [
        {
          contentType: 'application/vnd.microsoft.card.adaptive',
          content: {
            $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
            type: 'AdaptiveCard',
            version: '1.2',
            body: [
              {
                type: 'TextBlock',
                text: text
              }
            ]
          }
        }
      ]
    }
  }
};
  • Response Script:

if (response?.statusCode === 202) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'TEAMS message not posted',
    },
  };   
}

Send Vulnerability to PowerBI

The purpose of this example is to send data to PowerBI when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

  • Event: Vulnerability Created

  • Secrets:

    • powerbi_key - your Push Semantic Model - Push URL

Action 1 - Send Vulnerability to PowerBI

  • Method: POST

  • URL: <YOUR-PUSH-URL>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

  • Request Script:

return {
  request: {
    url: 'https://api.powerbi.com/beta/544b6efc-1149-4f8c-a17a-cad3c50cd5f8/datasets/d5ff54b4-2be2-4e02-baa2-9c6aa20a6c55/rows?experience=power-bi&key=' + secrets.powerbi_key,
    body: [
      {
        vuln_title: data.vulnerability_title,
        vuln_priority: data.vulnerability_priority
      }
    ]
  }
};
  • Response Script:

if (response?.statusCode === 200) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'PowerBI data not posted',
    },
  };   
}

Create Salesforce Opportunity

The purpose of this example is to create a Salesforce Opportunity when a Project is requested in AttackForge.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

  • Event: Project Requested

  • Secrets:

    • sf_client_id - your Salesforce OAuth Client Id

    • sf_client_secret - your Salesforce OAuth Client Secret

    • sf_password - Your Salesforce User Password

    • sf_username - Your SalesForce Username

Action 1 - Get Salesforce OAuth Token

  • Method: POST

  • URL: https://login.salesforce.com/services/oauth2/token

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded

  • Request Script:

return {
  request: {
    body: 'grant_type=password&client_id=' + secrets.sf_client_id + '&client_secret=' + secrets.sf_client_secret + '&username=' + secrets.sf_username + '&password=' + secrets.sf_password
  }, 
  data: {
    project_request: data
  } 
};
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (body?.access_token) {
  return {
    data: {
      project_request: data.project_request,
      sf_access_token: body.access_token
    }  
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce access token not found'
    }
  };
}

Action 2 - Create Salesforce Opportunity

  • Method: POST

  • URL: https://<YOUR-SALESFORCE>/services/data/v63.0/sobjects/Opportunity

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

  • Request Script:

if (data?.project_request && data?.sf_access_token) {
  let closeDate = '2030-12-31';
  
  if (data.project_request.project_request_end_date) {
    closeDate = String.slice(data.project_request.project_request_end_date, 0, 10);
  }

  return {
    request: {
      headers: {
        'Authorization': 'Bearer ' + data.sf_access_token
      },
      body: {
        'Name': data.project_request.project_request_name,
        'StageName': 'Needs Analysis',
        'CloseDate': closeDate
      }
    }
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce access token not found'
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (response.statusCode === 201 && body?.id && body.success === true) {
  return {
    decision: 'finish'
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce Opportunity not created'
    }
  };
}

Create a Webhook

The purpose of this example is to post data to a Webhook when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

  • Event: Vulnerability Created

  • Secrets: None

Action 1 - Send Data to Webhook

  • Method: POST

  • URL: <YOUR-WEBHOOK-ADDRESS>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = auth

  • Request Script:

// Not required for this webhook
  • Response Script:

// Not required for this webhook

Send Custom Email

The purpose of this example is to send an email to account managers when a Retest is requested.

This example Flow can be downloaded from our Flows GitHub Repository and imported into your AttackForge.

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Project Retest Requested

  • Secrets:

    • af_auth - your AttackForge Self-Service API token.

Action 1 - Send Email to Account Managers

  • Method: POST

  • URL: https://<YOUR-ATTACKFORGE>/api/ss/email

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-Key; Type = Value; Value = af_auth

  • Request Script:

return {
  request: { 
    body: {
      to: [
        "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

😄