We built AFScript to provide a safe and secure path for our customers to apply their own code to their AttackForge application, in a performant manner and without creating any security holes.
The language was built to look and feel like JavaScript to make it familiar and easy to use for security teams, pentesters and software engineers.
Importantly, the language itself is not executable. This makes it safe to use in a secure way. AttackForge will interpret AFScript and derive actions to take, without executing arbitrary code. This important distinction is why we had to build our own programming language instead of going with many of the existing languages already available.
How does AFScript work?
For the most part, write AFScript the same way you would write JavaScript code.
If you are not familiar with JavaScript, we recommend checking the JavaScript basics guide by Mozilla.
We built AFScript to resemble the primary syntax of JavaScript. You can define and use variables, create loops, create functions, call functions inside functions, etc.
To make the language easier to use, we have included built-in Functions which resemble common JavaScript built-in objects like Math, String and others.
We’ve also added our own functions and syntax which was inspired by various other programming languages, such as Date.datetime.
What can’t AFScript do?
For mostly security reasons, there are some limitations in AFScript that could otherwise be found in JavaScript. This is by design, to prevent accidental or intentional security holes.
The following is a non-exhaustive list of limitations. We will continue to update this list over time as the language evolves.
Logger.debug(String.decode('dGhpcyBpcyBlbmNvZGVk','base64'));// Expected output: this is encodedLogger.debug(String.decode('dGhpcyBpcyBlbmNvZGVk','base64url'));// Expected output: this is encodedLogger.debug(String.decode('ORUGS4ZANFZSAZLOMNXWIZLE','base32'));// Expected output: this is encodedLogger.debug(String.decode('EHK6ISP0D5PI0PBECDNM8PB4','base32hex'));// Expected output: this is encodedLogger.debug(String.decode('7468697320697320656E636F646564','base16'));// Expected output: this is encodedLogger.debug(String.decode('BzbxfazC)twO#0@wmYo{'));// Expected output: this is encoded!
digest
The String.digest() method takes a string and a supplied hashing algorithm and encodes it to a supplied format.
The String.padEnd() method pads this string with a given string (repeated, if needed) so that the resulting string reaches a given length. The padding is applied from the end of this string.
The String.padStart() method pads this string with another string (multiple times, if needed) until the resulting string reaches the given length. The padding is applied from the start of this string.
The String.slice() method extracts a section of this string and returns it as a new string, without modifying the original string.
conststr1='The quick brown fox jumps over the lazy dog.';Logger.debug(String.slice(str1,31));// Expected output: "the lazy dog."Logger.debug(String.slice(str1,4,19));// Expected output: "quick brown fox"
The String.startsWith() method determines whether this string begins with the characters of a specified string, returning true or false as appropriate.
conststr1='Saturday night plans';Logger.debug(String.startsWith(str1,'Sat'));// Expected output: trueLogger.debug(String.startsWith(str1,'Sat',3));// Expected output: false
The String.substring() method returns the part of this string from the start index up to and excluding the end index, or to the end of the string if no end index is supplied.
The Array.at() method takes an integer value and returns the item at that index, allowing for positive and negative integers. Negative integers count back from the last item in the array.
The Array.find() method returns the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned.
If you need the index of the found element in the array, use findIndex().
If you need to find the index of a value, use indexOf(). (It's similar to findIndex(), but checks each element for equality with the value instead of using a testing function.)
If you need to find if a value exists in an array, use includes(). Again, it checks each element for equality with the value instead of using a testing function.
If you need to find if any element satisfies the provided testing function, use some().
constarray1= [5,12,8,130,44];functionisFound(number) {return number ===8;}constfound=Array.find(array1, isFound);Logger.debug(found);// Expected output: 8
The Array.findIndex() method returns the index of the first element in an array that satisfies the provided testing function. If no elements satisfy the testing function, -1 is returned.
See also the find() method, which returns the first element that satisfies the testing function (rather than its index).
constarray1= [5,12,8,130,44];functionisLargeNumber(number) {return number >13;}Logger.debug(Array.findIndex(array1, isLargeNumber));// Expected output: 3
The Array.flatMap() method returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
The Array.join() method creates and returns a new string by concatenating all of the elements in this array, separated by commas or a specified separator string. If the array has only one item, then that item will be returned without using the separator.
The Array.lastIndexOf() method returns the last index at which a given element can be found in the array, or -1 if it is not present. The array is searched backwards, starting at fromIndex.
The Array.length data property represents the number of elements in that array. The value is an unsigned, 32-bit integer that is always numerically greater than the highest index in the array.
The Array.reduce() method executes a user-supplied "reducer" callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the array is a single value.
The first time that the callback is run there is no "return value of the previous calculation". If supplied, an initial value may be used in its place. Otherwise the array element at index 0 is used as the initial value and iteration starts from the next element (index 1 instead of index 0).
The Array.reduceRight() method applies a function against an accumulator and each value of the array (from right-to-left) to reduce it to a single value.
The Array.reverse() method reverses an array in place and returns the reference to the same array, the first array element now becoming the last, and the last array element becoming the first. In other words, elements order in the array will be turned towards the direction opposite to that previously stated.
The Array.slice() method returns a shallow copy of a portion of an array into a new array object selected from start to end (end not included) where start and end represent the index of items in that array. The original array will not be modified.
The Array.some() method tests whether at least one element in the array passes the test implemented by the provided function. It returns true if, in the array, it finds an element for which the provided function returns true; otherwise it returns false. It doesn't modify the array.
constarray= [1,2,3,4,5];functionisEven(number) {return number %2===0;}Logger.debug(Array.some(array, isEven));// Expected output: true
The Array.sort() method sorts the elements of an array in place and returns the reference to the same array, now sorted. The default sort order is ascending, built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.
//Example 1 – Sort ascending:constarr1= [5,2,1,9];Array.sort(arr1);Logger.debug(JSON.stringify(arr1));// Expected output: “[1,2,5,9]”//Example 2 – Sort descending:constarr1= [5,2,1,9];functioncompareFn(a, b) {return b - a;}Array.sort(arr1, compareFn);Logger.debug(JSON.stringify(arr1));// Expected output: “[9,5,2,1]”//Example 3:constarr2= ["b","d","c","a"];functioncompareFn(a, b) {if (a > b) {return1; }elseif (a < b) {return-1; }else {return0; }}Array.sort(arr2, compareFn);Logger.debug(JSON.stringify(arr2));// Expected output: “["a","b","c","d"]”//Example 4:constarr3= [{v:"b"},{v:"d"},{v:"c"},{v:"a"}];functioncompareFn(a, b) {if (a.v >b.v) {return1; }elseif (a.v <b.v) {return-1; }else {return0; }}Array.sort(arr3, compareFn);Logger.debug(JSON.stringify(arr3));// Expected output: “[{"v":"a"},{"v":"b"},{"v":"c"},{"v":"d"}]”
The Array.splice() method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place.
To access part of an array without modifying it, see slice().
constmonths= ['Jan','March','April','June'];Array.splice(months,1,0,'Feb');// Inserts at index 1Logger.debug(JSON.stringify(months));// Expected output: “["Jan","Feb","March","April", "June"]”Array.splice(months,4,1,'May');// Replaces 1 element at index 4Logger.debug(JSON.stringify(months));// Expected output: “["Jan","Feb","March","April","May"]”
The JSON.stringify() static method converts a JavaScript value to a JSON string, optionally replacing values if a replacer function is specified or optionally including only the specified properties if a replacer array is specified.
The Math.acos() static method returns the inverse cosine (in radians) of a number.
// Calculates angle of a right-angle triangle in radiansfunctioncalcAngle(adjacent, hypotenuse) {returnMath.acos(adjacent / hypotenuse);}Logger.debug(calcAngle(8,10));// Expected output: 0.6435011087932843Logger.debug(calcAngle(5,3));// Expected output: NaN
The Math.asin() static method returns the inverse sine (in radians) of a number.
// Calculates angle of a right-angle triangle in radiansfunctioncalcAngle(opposite, hypotenuse) {returnMath.asin(opposite / hypotenuse);}Logger.debug(calcAngle(6,10));// Expected output: 0.6435011087932844Logger.debug(calcAngle(5,3));// Expected output: NaN
The Math.atan2() static method returns the angle in the plane (in radians) between the positive x-axis and the ray from (0, 0) to the point (x, y), for Math.atan2(y, x).
The Math.random() static method returns a floating-point, pseudo-random number that's greater than or equal to 0 and less than 1, with approximately uniform distribution over that range — which you can then scale to your desired range. The implementation selects the initial seed to the random number generation algorithm; it cannot be chosen or reset by the user.
functiongetRandomInt(max) {returnMath.floor(Math.random() * max);}Logger.debug(getRandomInt(3));// Expected output: 0, 1 or 2Logger.debug(getRandomInt(1));// Expected output: 0Logger.debug(Math.random());// Expected output: a number from 0 to <1
The Math.sign() static method returns 1 or -1, indicating the sign of the number passed as argument. If the input is 0 or -0, it will be returned as-is.
The Number.isFinite() static method determines whether the passed value is a finite number — that is, it checks that a given value is a number, and the number is neither positive Infinity, negative Infinity, nor NaN.
The Number.isNaN() static method determines whether the passed value is the number value NaN, and returns false if the input is not of the Number type. It is a more robust version of the original, global isNaN() function.
The Number.isSafeInteger() static method determines whether the provided value is a number that is a safe integer.
functionwarn(x) {if (Number.isSafeInteger(x)) {return'Precision safe.'; }return'Precision may be lost!';}Logger.debug(warn(Math.pow(2,53)));// Expected output: "Precision may be lost!"Logger.debug(warn(Math.pow(2,53) -1));// Expected output: "Precision safe."
The Number.parseFloat() static method parses an argument and returns a floating point number. If a number cannot be parsed from the argument, it returns NaN.
constmyVariable="WEB-APP-027458";// Test if myVariable ends with six-digitsif (myVariable =~ m/\d{6}$/) {return"myVariable has a numbered code applied";}
Context
When interacting with AFScript, each supported use will have a context which is automatically injected and made available for you to use.
The context is made up of data which is contextually relevant for the purpose for which you are applying AFScript.
For example, when updating the logic for project status calculations – the context will include data about the project, which can be used in your logic to derive the intended behaviour and result.
When building scripts and debugging, you can update the context to ensure the robustness of your logic and code.
Code Editing
AttackForge has a built-in lightweight code editor to help you to use AFScript.
The editor supports autocomplete which also maps to the context.
You can test the output of your script and logic. This helps to provide assurance that your script is working as expected prior to rolling it into the application.
The output can be viewed at the bottom of the page.
Clicking Run will run your code and show the output.
Clicking Preserve log will preserve logging from multiple runs in the output.
TIP: you can use Logging to help you debug your code.
Supported Use Cases
Project Status Calculations
You can use AFScript to change the logic for how project status is calculated.
To get started, click on Administration -> Projects -> Status -> Configure
The default calculations for project status are included below.
IMPORTANT: Project statuses relying on Date.datetime() will be automatically updated every 5 minutes - it is not a live calculation. This is to ensure performance, especially when re-calculating all projects and comparing to 'now'.
if (Number.isInteger(project.total_not_tested_testcases)&&Number.isInteger(project.total_in_progress_testcases)&&Number.isInteger(project.total_tested_testcases)&&Number.isInteger(project.total_testcases)&&Number.isInteger(project.total_retest_vulnerabilities)&&Number.isInteger(project.total_not_applicable_testcases)&&project.end_date !==undefined){constwaitingCounter=project.total_not_tested_testcases;constinitiatedCounter=project.total_in_progress_testcases;constcompletedCounter=project.total_tested_testcases +project.total_not_applicable_testcases;constpast24hours=Date.datetime('now','-1 days','epoch');constendDateTime=Date.datetime(project.end_date,'epoch');constoverrun= endDateTime < past24hours;let status;if (project.on_hold) { status ='On Hold'; }elseif ((completedCounter <project.total_testcases && completedCounter >0&& overrun) || (project.total_testcases >0&& completedCounter ===0&& overrun)){ status ='Overrun'; }elseif (completedCounter ===project.total_testcases &&project.total_retest_vulnerabilities >0){ status ='Retest'; }elseif (completedCounter ===project.total_testcases) { status ='Completed'; }elseif ((completedCounter <project.total_testcases && completedCounter >0) || (completedCounter <project.total_testcases && initiatedCounter >0)){ status ='Testing'; }elseif (waitingCounter ===project.total_testcases) { status ='Waiting to Start'; }return status;}
The return value must be one of the following values:
"On Hold"
"Overrun"
"Retest"
"Completed"
"Testing"
"Waiting to Start"
Your code does not need to handle all statuses above if you do not intend to use all statuses.
Example 1: Project Completed based on Project End Date
This example will automatically show the project as completed if now (the time you are viewing the project status in the application) is any time after the project end date.
NOTE: Overrun status has been removed as it logically does not apply under this example use.
IMPORTANT: Project statuses relying on Date.datetime() will be automatically updated every 5 minutes - it is not a live calculation. This is to ensure performance, especially when re-calculating all projects and comparing to 'now'.
if (Number.isInteger(project.total_not_tested_testcases)&&Number.isInteger(project.total_in_progress_testcases)&&Number.isInteger(project.total_tested_testcases)&&Number.isInteger(project.total_testcases)&&Number.isInteger(project.total_retest_vulnerabilities)&&Number.isInteger(project.total_not_applicable_testcases)&&project.end_date !==undefined){constwaitingCounter=project.total_not_tested_testcases;constinitiatedCounter=project.total_in_progress_testcases;constcompletedCounter=project.total_tested_testcases +project.total_not_applicable_testcases;constnow=Date.datetime('now','epoch');constendDatePlus24Hours=Date.datetime(project.end_date,'1 days','epoch');// Check if project is completed. Allow for 24-hour grace period.constprojectCompleted= now > endDatePlus24Hours || completedCounter ===project.total_testcases;let status;if (project.on_hold) { status ='On Hold'; }elseif (completedCounter ===project.total_testcases &&project.total_retest_vulnerabilities >0){ status ='Retest'; }elseif (projectCompleted) { status ='Completed'; }elseif ((completedCounter <project.total_testcases && completedCounter >0) || (completedCounter <project.total_testcases && initiatedCounter >0)){ status ='Testing'; }elseif (waitingCounter ===project.total_testcases) { status ='Waiting to Start'; }return status;}
Example 2: Project Completed based on Custom Field
This example will automatically show the project as completed if a project custom field ‘project_completed’ has a value of ‘Yes’.
NOTE: This example requires configuration of a project custom field. For more information on how to do this, see Custom Fields & Forms.
NOTE: Overrun status has been removed as it logically does not apply under this example use.
IMPORTANT: Project statuses relying on Date.datetime() will be automatically updated every 5 minutes - it is not a live calculation. This is to ensure performance, especially when re-calculating all projects and comparing to 'now'.
if (Number.isInteger(project.total_not_tested_testcases)&&Number.isInteger(project.total_in_progress_testcases)&&Number.isInteger(project.total_tested_testcases)&&Number.isInteger(project.total_testcases)&&Number.isInteger(project.total_retest_vulnerabilities)&&Number.isInteger(project.total_not_applicable_testcases)&&project.end_date !==undefined){constwaitingCounter=project.total_not_tested_testcases;constinitiatedCounter=project.total_in_progress_testcases;constcompletedCounter=project.total_tested_testcases +project.total_not_applicable_testcases;constnow=Date.datetime('now','epoch');constendDatePlus24Hours=Date.datetime(project.end_date,'1 days','epoch');// Check if project is completed based on a project custom field "project_completed"constprojectCompleted= (project.project_custom_fields?.project_completed ==="Yes");let status;if (project.on_hold) { status ='On Hold'; }elseif (completedCounter ===project.total_testcases &&project.total_retest_vulnerabilities >0){ status ='Retest'; }elseif (projectCompleted) { status ='Completed'; }elseif ((completedCounter <project.total_testcases && completedCounter >0) || (completedCounter <project.total_testcases && initiatedCounter >0)){ status ='Testing'; }elseif (waitingCounter ===project.total_testcases) { status ='Waiting to Start'; }return status;}
Example 3: Project Retest based on Retesting Rounds
This example will automatically show the project as retest if there is at least one outstanding retesting round.
IMPORTANT: Project statuses relying on Date.datetime() will be automatically updated every 5 minutes - it is not a live calculation. This is to ensure performance, especially when re-calculating all projects and comparing to 'now'.
if (Number.isInteger(project.total_not_tested_testcases)&&Number.isInteger(project.total_in_progress_testcases)&&Number.isInteger(project.total_tested_testcases)&&Number.isInteger(project.total_testcases)&&Number.isInteger(project.total_retest_vulnerabilities)&&Number.isInteger(project.total_not_applicable_testcases)&&project.end_date !==undefined){constwaitingCounter=project.total_not_tested_testcases;constinitiatedCounter=project.total_in_progress_testcases;constcompletedCounter=project.total_tested_testcases +project.total_not_applicable_testcases;constpast24hours=Date.datetime('now','-1 days','epoch');constendDateTime=Date.datetime(project.end_date,'epoch');constoverrun= endDateTime < past24hours;constisRetest=project.total_retests_completed <project.total_retests_requested;let status;if (project.on_hold) { status ='On Hold'; }elseif ((completedCounter <project.total_testcases && completedCounter >0&& overrun) || (project.total_testcases >0&& completedCounter ===0&& overrun)){ status ='Overrun'; }elseif (isRetest){ status ='Retest'; }elseif (completedCounter ===project.total_testcases) { status ='Completed'; }elseif ((completedCounter <project.total_testcases && completedCounter >0) || (completedCounter <project.total_testcases && initiatedCounter >0)){ status ='Testing'; }elseif (waitingCounter ===project.total_testcases) { status ='Waiting to Start'; }return status;}
Suggested Values
You can use AFScript to suggest values for fields.
Suggestions can help to guide users into completing forms, based on your own logic.
For example:
Suggest a project code or vulnerability code on a project
Suggest a custom score for a vulnerability
Suggest a budget for a project/test, based on how scoping questions have been answered
Suggest missing evidence for a vulnerability
Suggest execution flows for a test case
!IMPORTANT: Suggested values are only supported on the Project form at present. This feature is expected to be widely supported in Q4, 2025.
Suggestion Formats
Input fields
Must return a string, for example:
return"This is some text";
Text Area fields
Must return a string, for example:
return"This is some text.\nThis is some more text.";
Rich-Text fields
Must return a string. Can be HTML, for example:
return"<h1>This is some heading</h1>";
Select fields
Must return a string, for example:
return"Yes";
Multi-Select fields
Must return a string array, for example:
return ["Yes","Maybe"];
Date fields
Must return a string in ISO 8601 UTC format (YYYY-MM-DDThh:mm:ssZ), for example:
return"2024-09-18T21:12:16.478Z";
Table fields
Must return an array of objects. Each object must include the key for the column field and appropriate values, for example:
return [ {"name":"Bruce Wayne","role":"Defender of Gotham" }];
List fields
Must return a string array, for example:
return ["Tag 1","Tag 2"];
User Select fields
Must return a string array with each string as an Object Id, for example:
return ["63cb153fedc40abef76bf991"];
User Multi-Select fields
Must return a string array with each string as an Object Id, for example:
return ["63cb153fedc40abef76bf991"];
Group Select fields
Must return a string array with each string as an Object Id, for example:
return ["63cb153fedc40abef76bf991"];
Group Multi-Select fields
Must return a string array with each string as an Object Id, for example:
return ["63cb153fedc40abef76bf991"];
Project Code and Vulnerability Code
You can suggest a custom project code and a vulnerability code prefix when creating or editing a project.
To get started with Project Code, click on Administration -> Projects -> Fields (Code) -> Suggested Value (Configure)
To get started with Vulnerability Code, click on Administration -> Projects -> Fields (Vulnerability Code)
Example 1: Suggest A Code Based on the Customer
This example will suggest a code which is made up of the customer name (or a short name for the customer you can create a mapping for).
Example input: ACME Corp.
Example project code: ACME
PREREQUSITIES:
You must have a SELECT type project custom field which is used to select the customer on the project. This example uses a custom field with a key 'customer'.
Example 2: Suggest A Code Based on the Customer and Testing Types
This example will suggest a code which is made up of the customer name (or a short name for the customer you can create a mapping for) as well as the testing types assigned to the project.
Example input: ACME Corp. + Web App
Example project code: ACME-WEBAPP
PREREQUSITIES:
You must have a SELECT type project custom field which is used to select the customer on the project. This example uses a custom field with a key 'customer'.
You must have a MULTI-SELECT type project custom field which is used to select the testing types assigned to the project. This example uses a custom field with a key 'testing_types'.
Example 3: Suggest A Code Based on the Customer, Testing Types and a Generated Random Number
This example will suggest a code which is made up of the customer name (or a short name for the customer you can create a mapping for) as well as the testing types assigned to the project, and a random number.
Example input: ACME Corp. + Web App
Example project code: ACME-WEBAPP-192834
PREREQUSITIES:
You must have a SELECT type project custom field which is used to select the customer on the project. This example uses a custom field with a key 'customer'.
You must have a MULTI-SELECT type project custom field which is used to select the testing types assigned to the project. This example uses a custom field with a key 'testing_types'.