AFScript

What is AFScript?

AFScript is an interpreted programming language created by AttackForge.

It was built to help our customers to configure and personalize their AttackForge application to better match their requirements and needs.

Applications of AFScript could include:

  • Changing the logic in parts of the application to align with existing or intended workflows

  • Drive behaviour of forms and their respective fields

  • Create in-app automations

  • Apply pre-and-post data transformations

  • Build bespoke dashboards and analytics

AFScript is the next generation in empowerment for AttackForge customers. It comes off the back of the successes we’ve had with Hide Expressions (custom fields and sections, vulnerability SLAs, custom vulnerability parsing) and Filter Expressions (custom emails, APIs) which have provided customers with ways to make AttackForge their own.

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.

Logging

Logging is imperative when writing any script or code.

AFScript supports logging to help you debug your scripts and logic.

You can invoke a log message using the following syntax:

Logger.<type>(“message”);

You can also include multiple messages in the same log entry using a comma to separate each message:

Logger.<type>(“message1”,“message2”,“message3”);

Where <type> is one of the following log level types:

  1. fatal

  2. error

  3. warn

  4. info

  5. debug

  6. trace

Fatal

Logger.fatal(“a fatal error occurred in xyz”);

Error

Logger.error(“an error occurred in xyz”);

Warn

Logger.warn(“a warning for xyz”);

Info

Logger.info(“an informational message for xyz”);

Debug

Logger.debug(“a debug statement”);

Trace

Logger.trace(“function abc was called”);

Built-in Functions

Dates

FunctionDescriptionExample

datetime

The Date.datetime(“timeValue”, “modifiers”, “isostring”, “epoch”) function can be used to construct a date and time.

You can modify the date and time by passing modifiers.

The default response is the date & time in an ISO string format.

However, you can set the response to be in epoch format by passing in “epoch”.

Similarly, you can explicitly set the response to an ISO string format by passing in “isostring”.

timeValue - must be either:

  • “now”

  • “YYYY-MM-DD”

  • “YYYY-MM-DDTHH:MM”

  • “YYYY-MM-DDTHH:MM:SS”

  • “YYYY-MM-DDTHH:MM:SS:MMMZ”

  • time in milliseconds e.g. "1727246762913"

modifiers - must be either:

  • “+999 years”

  • “-999 years”

  • “+999 months”

  • “-999 months”

  • “+999 days”

  • “-999 days”

  • “+999 hours”

  • “-999 hours”

  • “+999 minutes”

  • “-999 minutes”

  • “start of year”

  • “start of month”

  • “start of day”

//Example 1:
Logger.debug(Date.datetime("2020-06-01"));
// Expected output: “2020-06-01T00:00:00.000Z”

//Example 2: 
Logger.debug(Date.datetime("now"));
// Expected output: “2024-09-09T13:24:05.274Z”

//Example 3:
Logger.debug(Date.datetime("now", "epoch"));
// Expected output: 1725888245274

//Example 4:
Logger.debug(Date.datetime("now", "-7 days"));
// Expected output: “2024-09-02T13:24:05.274Z”

//Example 5:
Logger.debug(Date.datetime("now", "-7 days", "+1 years"));
// Expected output: “2025-09-02T13:24:05.274Z”

Strings

FunctionDescriptionExample

decode

The String.decode() method takes a string and decodes it to a supplied format.

The following formats are supported:

Logger.debug(String.decode('dGhpcyBpcyBlbmNvZGVk', 'base64'));
// Expected output: this is encoded

Logger.debug(String.decode('dGhpcyBpcyBlbmNvZGVk', 'base64url'));
// Expected output: this is encoded
 
Logger.debug(String.decode('ORUGS4ZANFZSAZLOMNXWIZLE', 'base32'));
// Expected output: this is encoded

Logger.debug(String.decode('EHK6ISP0D5PI0PBECDNM8PB4', 'base32hex'));
// Expected output: this is encoded

Logger.debug(String.decode('7468697320697320656E636F646564', 'base16'));
// Expected output: this is encoded

Logger.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 following hashing algorithms are supported:

The following formats are supported:

Logger.debug(String.digest('hash this', 'SHA1'));
// Expected output: E1449548860671BCFED5A5D1E4701532A0308D20

Logger.debug(String.digest('hash this', 'SHA1', 'base64'));
// Expected output: 4USVSIYGcbz+1aXR5HAVMqAwjSA=

Logger.debug(String.digest('hash this', 'SHA224', 'base64'));
// Expected output: q6y5930Dpk8Y5BZSLgRFXYDoBfmzbWi10vCftQ====

ogger.debug(String.digest('hash this', 'SHA256', 'base64'));
// Expected output: GUZ3iLwM8ReQoHXqcYRSzs8OedtZ0ZZGcEdeX+LkphE=

ogger.debug(String.digest('hash this', 'SHA384', 'base64'));
// Expected output: Evu6KXz7wzv1U8QjEV20uNvBuWWoIGJ/O7YgG5EQFekQaqOd21lct9vFjtkLmxGP

ogger.debug(String.digest('hash this', 'SHA512', 'base64'));
// Expected output: zwboR4ZuVgz0iMrhfBmT6ntxTfAhXyFrBMKDQY62mymdjq99N4QzncNLtZcyfzobqo8a1GWL2IY5p4Ri1vgvcA==

ogger.debug(String.digest('hash this', 'MD5', 'base64'));
// Expected output: 6AxxXl1OiF9o16OFO1/Kcw==

ogger.debug(String.digest('hash this', 'RMD160', 'base64'));
// Expected output: 1UQ6FU8WfiwTMvbecs+0xqucjBc=

encode

The String.encode() method takes a string and encodes it to a supplied format.

The following formats are supported:

Logger.debug(String.encode('this is encoded', 'base64'));
// Expected output: dGhpcyBpcyBlbmNvZGVk

Logger.debug(String.encode('this is encoded', 'base64url'));
// Expected output: dGhpcyBpcyBlbmNvZGVk
 
Logger.debug(String.encode('this is encoded', 'base32'));
// Expected output: ORUGS4ZANFZSAZLOMNXWIZLE

Logger.debug(String.encode('this is encoded', 'base32hex'));
// Expected output: EHK6ISP0D5PI0PBECDNM8PB4

Logger.debug(String.encode('this is encoded', 'base16'));
// Expected output: 7468697320697320656E636F646564

Logger.debug(String.encode('this is encoded!', 'Z85'));
// Expected output: BzbxfazC)twO#0@wmYo{

The String.endsWith() method determines whether a string ends with the characters of this string, returning true or false as appropriate.

const str1 = 'Cats are the best!';
 
Logger.debug(String.endsWith(str1, 'best!'));
// Expected output: true

The String.length data property of a String value contains the length of the string in UTF-16 code units.

Logger.debug(String.length('The quick brown fox jumps over the lazy dog.'));
// Expected output: 44

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.

const str1 = 'Breaded Mushrooms';
 
Logger.debug(String.padEnd(str1, 25, '.'));
// Expected output: "Breaded Mushrooms........"

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.

const str1 = '5';
 
Logger.debug(String.padStart(str1, 2, '0'));
// Expected output: "05"

The String.slice() method extracts a section of this string and returns it as a new string, without modifying the original string.

const str1 = '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.

const str1 = 'Saturday night plans';
 
Logger.debug(String.startsWith(str1, 'Sat'));
// Expected output: true
 
Logger.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.

const str1 = 'Mozilla';
 
Logger.debug(String.substring(str1, 1, 3));
// Expected output: "oz"
 
Logger.debug(String.substring(str1, 2));
// Expected output: "zilla"

The String.toLowerCase() method returns this string converted to lowercase.

Logger.debug(String.toLowerCase('The quick brown fox jumps over the lazy dog.'));
// Expected output: the quick brown fox jumps over the lazy dog.

The String.toUpperCase() method returns this string converted to uppercase.

Logger.debug(String.toUpperCase('The quick brown fox jumps over the lazy dog.'));
// Expected output: THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.

The String.trim() method removes whitespace from both ends of this string and returns a new string, without modifying the original string.

To return a new string with whitespace trimmed from just one end, use trimStart() or trimEnd().

const str1 = '   Hello world!   ';
 
Logger.debug(str1);
// Expected output: "   Hello world!   ";
 
Logger.debug(String.trim(str1));
// Expected output: "Hello world!";

The String.trimEnd() method removes whitespace from the end of this string and returns a new string, without modifying the original string.

const str1 = '   Hello world!   ';
 
Logger.debug(str1);
// Expected output: "   Hello world!   ";
 
Logger.debug(String.trimEnd(str1));
// Expected output: "   Hello world!";

The String.trimStart() method removes whitespace from the beginning of this string and returns a new string, without modifying the original string.

const str1 = '   Hello world!   ';
 
Logger.debug(str1);
// Expected output: "   Hello world!   ";
 
Logger.debug(String.trimStart(str1));
// Expected output: "Hello world!   ";

Arrays

FunctionDescriptionExample

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.

const array1 = [5, 12, 8, 130, 44];
 
Logger.debug(Array.at(array1, 2));
// Expected output: 8
 
Logger.debug(Array.at(array1, -2));
// Expected output: 130

The Array.concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.

const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = Array.concat(array1, array2);
 
Logger.debug(JSON.stringify(array3));
// Expected output: “["a","b","c","d","e","f"]”

The Array.every() method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value.

function isBelowThreshold(currentValue) {
  return currentValue < 40;
}
 
const array1 = [1, 30, 39, 29, 10, 13];
 
Logger.debug(Array.every(array1, isBelowThreshold));
// Expected output: true

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().

const array1 = [5, 12, 8, 130, 44];
 
function isFound(number) {
   return number === 8;
}
 
const found = 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).

const array1 = [5, 12, 8, 130, 44];
 
function isLargeNumber(number) {
   return number > 13;
}
 
Logger.debug(Array.findIndex(array1, isLargeNumber));
// Expected output: 3

The Array.flat() method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth.

const arr1 = [0, 1, 2, [3, 4]];
 
Logger.debug(JSON.stringify(Array.flat(arr1)));
// Expected output: “[0,1,2,3,4]”
 
const arr2 = [0, 1, [2, [3, [4, 5]]]];
 
Logger.debug(JSON.stringify(Array.flat(arr2)));
// Expected output: “[0,1,2,[3,[4,5]]]”
 
Logger.debug(JSON.stringify(Array.flat(arr2, 2)));
// Expected output: “[0,1,2,3, Array [4,5]]”
 
Logger.debug(JSON.stringify(Array.flat(arr2, Infinity)));
// Expected output: “[0,1,2,3,4,5]”

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.

const arr1 = [1, 2, 1];
 
function callbackFunc(number) {
   return number === 2 ? [2, 2] : 1;
}
 
const result = Array.flatMap(arr1, callbackFunc);
 
Logger.debug(JSON.stringify(result));
// Expected output: “[1,2,2,1]”

The Array.includes() method determines whether an array includes a certain value among its entries, returning true or false as appropriate.

const array1 = [1, 2, 3];
 
Logger.debug(Array.includes(array1, 2));
// Expected output: true
 
const pets = ['cat', 'dog', 'bat'];
 
Logger.debug(Array.includes(pets, 'cat'));
// Expected output: true
 
Logger.debug(Array.includes(pets, 'at'));
// Expected output: false

The Array.indexOf() method returns the first index at which a given element can be found in the array, or -1 if it is not present.

const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];
 
Logger.debug(Array.indexOf(beasts, 'bison'));
// Expected output: 1
 
// Start from index 2
Logger.debug(Array.indexOf(beasts, 'bison', 2));
// Expected output: 4
 
Logger.debug(Array.indexOf(beasts, 'giraffe'));
// Expected output: -1

The Array.isArray() static method determines whether the passed value is an Array.

Logger.debug(Array.isArray([1, 3, 5]));
// Expected output: true
 
Logger.debug(Array.isArray('[]'));
// Expected output: false

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.

const elements = ['Fire', 'Air', 'Water'];
 
Logger.debug(Array.join(elements));
// Expected output: "Fire,Air,Water"
 
Logger.debug(Array.join(elements, ''));
// Expected output: "FireAirWater"
 
Logger.debug(Array.join(elements, '-'));
// Expected output: "Fire-Air-Water"

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.

const animals = ['Dodo', 'Tiger', 'Penguin', 'Dodo'];
 
Logger.debug(Array.lastIndexOf(animals, 'Dodo'));
// Expected output: 3
 
Logger.debug(Array.lastIndexOf(animals, 'Tiger'));
// Expected output: 1

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.

const clothing = ['shoes', 'shirts', 'socks', 'sweaters'];
 
Logger.debug(Array.length(clothing));
// Expected output: 4

The Array.map() method creates a new array populated with the results of calling a provided function on every element in the calling array.

const array1 = [1, 4, 9, 16];
 
function double(number) {
   return number * 2;
}
 
const map1 = Array.map(array1, double);
 
Logger.debug(JSON.stringify(map1));
// Expected output: “[2,8,18,32]”

The Array.pop() method removes the last element from an array and returns that element. This method changes the length of the array.

const plants = ['broccoli', 'cauliflower', 'cabbage', 'kale', 'tomato'];
 
Logger.debug(Array.pop(plants));
// Expected output: "tomato"
 
Logger.debug(JSON.stringify(plants));
// Expected output: “["broccoli","cauliflower","cabbage","kale"]”
 
Array.pop(plants);
 
Logger.debug(JSON.stringify(plants));
// Expected output: “["broccoli","cauliflower","cabbage"]”

The Array.push() method adds the specified elements to the end of an array and returns the new length of the array.

const animals = ['pigs', 'goats', 'sheep'];
 
const count = Array.push(animals, 'cows');
Logger.debug(count);
// Expected output: 4
Logger.debug(JSON.stringify(animals));
// Expected output: “["pigs","goats","sheep","cows"]”
 
Array.push(animals, 'chickens', 'cats', 'dogs');
Logger.debug(JSON.stringify(animals));
// Expected output: “["pigs","goats","sheep","cows","chickens","cats","dogs"]”

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

const array1 = [1, 2, 3, 4];
 
function reducer(accumulator, currentValue, index) {
   return accumulator + currentValue;
}
 
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = Array.reduce(array1, reducer,
 initialValue,
);
 
Logger.debug(sumWithInitial);
// Expected output: 10

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.

const array1 = [
  [0, 1],
  [2, 3],
  [4, 5],
];
 
function reducer(accumulator, currentValue) {
   return Array.concat(accumulator, currentValue);
}
 
const result = Array.reduceRight(array1, reducer);
 
Logger.debug(JSON.stringify(result));
// Expected output: “[4,5,2,3,0,1]”

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.

const array1 = ['one', 'two', 'three'];
Logger.debug(JSON.stringify(array1));
// Expected output: “["one","two","three"]”
 
const reversed = Array.reverse(array1);
Logger.debug(JSON.stringify(reversed));
// Expected output: “["three","two","one"]”

The Array.shift() method removes the first element from an array and returns that removed element. This method changes the length of the array.

const array1 = [1, 2, 3];
 
const firstElement = Array.shift(array1);
 
Logger.debug(JSON.stringify(array1));
// Expected output: “[2,3]”
 
Logger.debug(firstElement);
// Expected output: 1

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.

const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
 
Logger.debug(JSON.stringify(Array.slice(animals, 2)));
// Expected output: “["camel","duck","elephant"]”
 
Logger.debug((JSON.stringify(Array.slice(animals, 2, 4)));
// Expected output: “["camel","duck"]”
 
Logger.debug((JSON.stringify(Array.slice(animals, 1, 5)));
// Expected output: “["bison","camel","duck", "elephant"]”
 
Logger.debug((JSON.stringify(Array.slice(animals, -2)));
// Expected output: “["duck","elephant"]”
 
Logger.debug((JSON.stringify(Array.slice(animals, 2, -1)));
// Expected output: “["camel","duck"]”
 
Logger.debug((JSON.stringify(Array.slice(animals)));
// Expected output: “["ant","bison","camel", "duck", "elephant"]”

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.

const array = [1, 2, 3, 4, 5];
 
function isEven(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:
const arr1 = [5,2,1,9];
 
Array.sort(arr1);
 
Logger.debug(JSON.stringify(arr1));
// Expected output: “[1,2,5,9]”
 
//Example 2 – Sort descending:
const arr1 = [5,2,1,9];
 
function compareFn(a, b) {
   return b - a;
}
 
Array.sort(arr1, compareFn);
 
Logger.debug(JSON.stringify(arr1));
// Expected output: “[9,5,2,1]”
 
//Example 3:
const arr2 = ["b","d","c","a"];
 
function compareFn(a, b) {
   if (a > b) {
      return 1;
   }
   else if (a < b) {
      return -1;
   }
   else {
      return 0;
   }
}
 
Array.sort(arr2, compareFn);
 
Logger.debug(JSON.stringify(arr2));
// Expected output: “["a","b","c","d"]”
 
//Example 4:
const arr3 = [{v:"b"},{v:"d"},{v:"c"},{v:"a"}];
 
function compareFn(a, b) {
   if (a.v > b.v) {
      return 1;
   }
   else if (a.v < b.v) {
      return -1;
   }
   else {
      return 0;
   }
}
 
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().

const months = ['Jan', 'March', 'April', 'June'];
Array.splice(months, 1, 0, 'Feb');
// Inserts at index 1
Logger.debug(JSON.stringify(months));
// Expected output: “["Jan","Feb","March","April", "June"]”
 
Array.splice(months, 4, 1, 'May');
// Replaces 1 element at index 4
Logger.debug(JSON.stringify(months));
// Expected output: “["Jan","Feb","March","April","May"]”

The Array.unshift() method adds the specified elements to the beginning of an array and returns the new length of the array.

const array1 = [1, 2, 3];
 
Logger.debug(Array.unshift(array1, 4, 5));
// Expected output: 5
 
Logger.debug(JSON.stringify(array1));
// Expected output: “[4,5,1,2,3]”

JSON

FunctionDescriptionExample

The JSON.parse() static method parses a JSON string, constructing the JavaScript value or object described by the string.

const json = ‘{“result”:true, “count”:42}’;
const obj = JSON.parse(json);
 
Logger.debug(JSON.stringify(obj.count));
// Expected output: “42”
 
Logger.debug(JSON.stringify(obj.result));
// Expected output: “true”

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.

Logger.debug(JSON.stringify({ x: 5, y: 6 }));
// Expected output: “{"x":5,"y":6}”
 
Logger.debug(JSON.stringify(Date.datetime('2006-01-02T15:04:05.000Z'));
// Expected output: "2006-01-02T15:04:05.000Z"

Math

FunctionDescriptionExample

The Math.abs() static method returns the absolute value of a number.

function difference(a, b) {
  return Math.abs(a - b);
}
 
Logger.debug(difference(3, 5));
// Expected output: 2
 
Logger.debug(difference(5, 3));
// Expected output: 2
 
Logger.debug(difference(1.23456, 7.89012));
// Expected output: 6.6555599999999995

The Math.acos() static method returns the inverse cosine (in radians) of a number.

// Calculates angle of a right-angle triangle in radians
function calcAngle(adjacent, hypotenuse) {
  return Math.acos(adjacent / hypotenuse);
}
 
Logger.debug(calcAngle(8, 10));
// Expected output: 0.6435011087932843
 
Logger.debug(calcAngle(5, 3));
// Expected output: NaN

The Math.acosh() static method returns the inverse hyperbolic cosine of a number.

Logger.debug(Math.acosh(0.999999999999));
// Expected output: NaN
 
Logger.debug(Math.acosh(1));
// Expected output: 0
 
Logger.debug(Math.acosh(2));
// Expected output: 1.3169578969248166
 
Logger.debug(Math.acosh(2.5));
// Expected output: 1.566799236972411

The Math.asin() static method returns the inverse sine (in radians) of a number.

// Calculates angle of a right-angle triangle in radians
function calcAngle(opposite, hypotenuse) {
  return Math.asin(opposite / hypotenuse);
}
 
Logger.debug(calcAngle(6, 10));
// Expected output: 0.6435011087932844
 
Logger.debug(calcAngle(5, 3));
// Expected output: NaN

The Math.asinh() static method returns the inverse hyperbolic sine of a number.

Logger.debug(Math.asinh(1));
// Expected output: 0.881373587019543
 
Logger.debug(Math.asinh(0));
// Expected output: 0
 
Logger.debug(Math.asinh(-1));
// Expected output: -0.881373587019543
 
Logger.debug(Math.asinh(2));
// Expected output: 1.4436354751788103

The Math.atan() static method returns the inverse tangent (in radians) of a number.

// Calculates angle of a right-angle triangle in radians
function calcAngle(opposite, adjacent) {
  return Math.atan(opposite / adjacent);
}
 
Logger.debug(calcAngle(8, 10));
// Expected output: 0.6747409422235527
 
Logger.debug(calcAngle(5, 3));
// Expected output: 1.0303768265243125

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

function calcAngleDegrees(x, y) {
  return (Math.atan2(y, x) * 180) / Math.PI;
}
 
Logger.debug(calcAngleDegrees(5, 5));
// Expected output: 45
 
Logger.debug(calcAngleDegrees(10, 10));
// Expected output: 45
 
Logger.debug(calcAngleDegrees(0, 10));
// Expected output: 90

The Math.atanh() static method returns the inverse hyperbolic tangent of a number.

Logger.debug(Math.atanh(-1));
// Expected output: -Infinity

Logger.debug(Math.atanh(0));
// Expected output: 0

Logger.debug(Math.atanh(0.5));
// Expected output: 0.549306144334055 (approximately)

Logger.debug(Math.atanh(1));
// Expected output: Infinity

The Math.cbrt() static method returns the cube root of a number.

Logger.debug(Math.cbrt(-1));
// Expected output: -1
 
Logger.debug(Math.cbrt(1));
// Expected output: 1
 
Logger.debug(Math.cbrt(Infinity));
// Expected output: Infinity
 
Logger.debug(Math.cbrt(64));
// Expected output: 4

The Math.ceil() static method always rounds up and returns the smallest integer greater than or equal to a given number.

Logger.debug(Math.ceil(0.95));
// Expected output: 1
 
Logger.debug(Math.ceil(4));
// Expected output: 4
 
Logger.debug(Math.ceil(7.004));
// Expected output: 8
 
Logger.debug(Math.ceil(-7.004));
// Expected output: -7

The Math.clz32() static method returns the number of leading zero bits in the 32-bit binary representation of a number.

// 00000000000000000000000000000001
Logger.debug(Math.clz32(1));
// Expected output: 31
 
// 00000000000000000000000000000100
Logger.debug(Math.clz32(4));
// Expected output: 29
 
// 00000000000000000000001111101000
Logger.debug(Math.clz32(1000));
// Expected output: 22

The Math.cos() static method returns the cosine of a number in radians.

function getCircleX(radians, radius) {
  return Math.cos(radians) * radius;
}
 
Logger.debug(getCircleX(1, 10));
// Expected output: 5.403023058681398
 
Logger.debug(getCircleX(2, 10));
// Expected output: -4.161468365471424
 
Logger.debug(getCircleX(Math.PI, 10));
// Expected output: -10

The Math.cosh() static method returns the hyperbolic cosine of a number.

Logger.debug(Math.cosh(0));
// Expected output: 1
 
Logger.debug(Math.cosh(1));
// Expected output: 1.543080634815244 (approximately)
 
Logger.debug(Math.cosh(-1));
// Expected output: 1.543080634815244 (approximately)
 
Logger.debug(Math.cosh(2));
// Expected output: 3.7621956910836314

The Math.exp() static method returns e raised to the power of a number.

Logger.debug(Math.exp(0));
// Expected output: 1
 
Logger.debug(Math.exp(1));
// Expected output: 2.718281828459 (approximately)
 
Logger.debug(Math.exp(-1));
// Expected output: 0.36787944117144233
 
Logger.debug(Math.exp(2));
// Expected output: 7.38905609893065

The Math.expm1() static method returns e raised to the power of a number, subtracted by 1.

Logger.debug(Math.expm1(0));
// Expected output: 0
 
Logger.debug(Math.expm1(1));
// Expected output: 1.718281828459045
 
Logger.debug(Math.expm1(-1));
// Expected output: -0.6321205588285577
 
Logger.debug(Math.expm1(2));
// Expected output: 6.38905609893065

The Math.floor() static method always rounds down and returns the largest integer less than or equal to a given number.

Logger.debug(Math.floor(5.95));
// Expected output: 5
 
Logger.debug(Math.floor(5.05));
// Expected output: 5
 
Logger.debug(Math.floor(5));
// Expected output: 5
 
Logger.debug(Math.floor(-5.05));
// Expected output: -6

The Math.fround() static method returns the nearest 32-bit single precision float representation of a number.

Logger.debug(Math.fround(5.5));
// Expected output: 5.5
 
Logger.debug(Math.fround(5.05));
// Expected output: 5.050000190734863
 
Logger.debug(Math.fround(5));
// Expected output: 5
 
Logger.debug(Math.fround(-5.05));
// Expected output: -5.050000190734863

The Math.hypot() static method returns the square root of the sum of squares of its arguments.

Logger.debug(Math.hypot(3, 4));
// Expected output: 5
 
Logger.debug(Math.hypot(5, 12));
// Expected output: 13
 
Logger.debug(Math.hypot(3, 4, 5));
// Expected output: 7.0710678118654755
 
Logger.debug(Math.hypot(-5));
// Expected output: 5

The Math.imul() static method returns the result of the C-like 32-bit multiplication of the two parameters.

Logger.debug(Math.imul(3, 4));
// Expected output: 12
 
Logger.debug(Math.imul(-5, 12));
// Expected output: -60
 
Logger.debug(Math.imul(0xffffffff, 5));
// Expected output: -5
 
Logger.debug(Math.imul(0xfffffffe, 5));
// Expected output: -10

The Math.log() static method returns the natural logarithm (base e) of a number.

function getBaseLog(x, y) {
  return Math.log(y) / Math.log(x);
}
 
// 2 x 2 x 2 = 8
Logger.debug(getBaseLog(2, 8));
// Expected output: 3
 
// 5 x 5 x 5 x 5 = 625
Logger.debug(getBaseLog(5, 625));
// Expected output: 4

The Math.log10() static method returns the base 10 logarithm of a number.

Logger.debug(Math.log10(100000));
// Expected output: 5
 
Logger.debug(Math.log10(2));
// Expected output: 0.3010299956639812
 
Logger.debug(Math.log10(1));
// Expected output: 0
 
Logger.debug(Math.log10(0));
// Expected output: -Infinity

The Math.log1p() static method returns the natural logarithm (base e) of 1 + x, where x is the argument.

Logger.debug(Math.log1p(1));
// Expected output: 0.6931471805599453
 
Logger.debug(Math.log1p(0));
// Expected output: 0
 
Logger.debug(Math.log1p(-1));
// Expected output: -Infinity
 
Logger.debug(Math.log1p(-2));
// Expected output: NaN

The Math.log2() static method returns the base 2 logarithm of a number.

Logger.debug(Math.log2(3));
// Expected output: 1.584962500721156
 
Logger.debug(Math.log2(2));
// Expected output: 1
 
Logger.debug(Math.log2(1));
// Expected output: 0
 
Logger.debug(Math.log2(0));
// Expected output: -Infinity

The Math.max() static method returns the largest of the numbers given as input parameters, or -Infinity if there are no parameters.

Logger.debug(Math.max(1, 3, 2));
// Expected output: 3
 
Logger.debug(Math.max(-1, -3, -2));
// Expected output: -1

The Math.min() static method returns the smallest of the numbers given as input parameters, or Infinity if there are no parameters.

Logger.debug(Math.min(2, 3, 1));
// Expected output: 1
 
Logger.debug(Math.min(-2, -3, -1));
// Expected output: -3

The Math.pow() static method returns the value of a base raised to a power.

Logger.debug(Math.pow(7, 3));
// Expected output: 343
 
Logger.debug(Math.pow(4, 0.5));
// Expected output: 2
 
Logger.debug(Math.pow(7, -2));
// Expected output: 0.02040816326530612
// (1/49)
 
Logger.debug(Math.pow(-7, 0.5));
// Expected output: NaN

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.

function getRandomInt(max) {
  return Math.floor(Math.random() * max);
}
 
Logger.debug(getRandomInt(3));
// Expected output: 0, 1 or 2
 
Logger.debug(getRandomInt(1));
// Expected output: 0
 
Logger.debug(Math.random());
// Expected output: a number from 0 to <1

The Math.round() static method returns the value of a number rounded to the nearest integer.

Logger.debug(Math.round(0.9));
// Expected output: 1
 
Logger.debug(Math.round(5.95), Math.round(5.5), Math.round(5.05));
// Expected output: 6 6 5
 
Logger.debug(Math.round(-5.05), Math.round(-5.5), Math.round(-5.95));
// Expected output: -5 -5 -6

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.

Logger.debug(Math.sign(3));
// Expected output: 1
 
Logger.debug(Math.sign(-3));
// Expected output: -1
 
Logger.debug(Math.sign(0));
// Expected output: 0
 
Logger.debug(Math.sign('-3'));
// Expected output: -1

The Math.sin() static method returns the sine of a number in radians.

function getCircleY(radians, radius) {
  return Math.sin(radians) * radius;
}
 
Logger.debug(getCircleY(1, 10));
// Expected output: 8.414709848078965
 
Logger.debug(getCircleY(2, 10));
// Expected output: 9.092974268256818
 
Logger.debug(getCircleY(Math.PI, 10));
// Expected output: 1.2246467991473533e-15

The Math.sinh() static method returns the hyperbolic sine of a number.

Logger.debug(Math.sinh(0));
// Expected output: 0
 
Logger.debug(Math.sinh(1));
// Expected output: 1.1752011936438014
 
Logger.debug(Math.sinh(-1));
// Expected output: -1.1752011936438014
 
Logger.debug(Math.sinh(2));
// Expected output: 3.626860407847019

The Math.sqrt() static method returns the square root of a number.

function calcHypotenuse(a, b) {
  return Math.sqrt(a * a + b * b);
}
 
Logger.debug(calcHypotenuse(3, 4));
// Expected output: 5
 
Logger.debug(calcHypotenuse(5, 12));
// Expected output: 13
 
Logger.debug(calcHypotenuse(0, 0));
// Expected output: 0

The Math.tan() static method returns the tangent of a number in radians.

function getTanFromDegrees(degrees) {
  return Math.tan((degrees * Math.PI) / 180);
}
 
Logger.debug(getTanFromDegrees(0));
// Expected output: 0
 
Logger.debug(getTanFromDegrees(45));
// Expected output: 0.9999999999999999
 
Logger.debug(getTanFromDegrees(90));
// Expected output: 16331239353195370

The Math.tanh() static method returns the hyperbolic tangent of a number.

Logger.debug(Math.tanh(-1));
// Expected output: -0.7615941559557649
 
Logger.debug(Math.tanh(0));
// Expected output: 0
 
Logger.debug(Math.tanh(Infinity));
// Expected output: 1
 
Logger.debug(Math.tanh(1));
// Expected output: 0.7615941559557649

The Math.trunc() static method returns the integer part of a number by removing any fractional digits.

Logger.debug(Math.trunc(13.37));
// Expected output: 13
 
Logger.debug(Math.trunc(42.84));
// Expected output: 42
 
Logger.debug(Math.trunc(0.123));
// Expected output: 0
 
Logger.debug(Math.trunc(-0.123));
// Expected output: -0

Numbers

FunctionDescriptionExample

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.

Logger.debug(Number.isFinite(1 / 0));
// Expected output: false
 
Logger.debug(Number.isFinite(10 / 5));
// Expected output: true
 
Logger.debug(Number.isFinite(0 / 0));
// Expected output: false

The Number.isInteger() static method determines whether the passed value is an integer.

function fits(x, y) {
  if (Number.isInteger(y / x)) {
    return 'Fits!';
  }
  return 'Does NOT fit!';
}
 
Logger.debug(fits(5, 10));
// Expected output: "Fits!"
 
Logger.debug(fits(5, 11));
// Expected output: "Does NOT fit!"

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.

function typeOfNaN(x) {
  if (Number.isNaN(x)) {
    return 'Number NaN';
  }
}
 
Logger.debug(typeOfNaN('100F'));
// Expected output: "NaN"

The Number.isSafeInteger() static method determines whether the provided value is a number that is a safe integer.

function warn(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.

function circumference(r) {
  if (Number.isNaN(Number.parseFloat(r))) {
    return 0;
  }
  return Number.parseFloat(r) * 2.0 * Math.PI;
}
 
Logger.debug(circumference('4.567abcdefgh'));
// Expected output: 28.695307297889173
 
Logger.debug(circumference('abcdefgh'));
// Expected output: 0

The Number.parseInt() static method parses a string argument and returns an integer of the specified radix or base.

function roughScale(x, base) {
  const parsed = Number.parseInt(x, base);
  if (Number.isNaN(parsed)) {
    return 0;
  }
  return parsed * 100;
}
 
Logger.debug(roughScale(' 0xF', 16));
// Expected output: 1500
 
Logger.debug(roughScale('321', 2));
// Expected output: 0

Util

FunctionDescriptionExample

randomId

The Util.randomId() method returns a cryptographically secure random value, encoded using a supported encoding.

The first parameter must be a string specifying one of the following encodings:

The second parameter must be a number which specifies how many bytes the random value should be.

Logger.debug(Util.randomId('base64'));
// Expected output format: yC8tpqBMl2j3zxp7tsO3zdHsLcRCY1wyU44Djc5pCWI=
 
Logger.debug(Util.randomId('base64', 5));
// Expected output format: 1GRl4lI=

Logger.debug(Util.randomId('base64url'));
// Expected output format: lRnhwySDycxXBYz2C2q_MPyPKP51FExtyiJAwurz47Q=
 
Logger.debug(Util.randomId('base32'));
// Expected output format: C4GZRB7LHO4GXMV7HFR7263FVHVHPKQNQAVHWXUOHLZJJTOGOXWQ====

Logger.debug(Util.randomId('base32hex'));
// Expected output format: 72Q94N0LGT4FULVV87G0VCUJOOBLCPOJU0SO2TLN1B647501SIM0====

Logger.debug(Util.randomId('base16'));
// Expected output format: 31302C51572B49743F366A67294C785152267D3566555859643753446C35614C

Logger.debug(Util.randomId('Z85'));
// Expected output format: @YD$z&i[oD%i/M:@3CO(P<8[(OSer)[ge]cK[uia

uuidv4

The Util.uuidv4() method returns a Universally Unique IDentifier (UUID) also known as a GUID (Globally Unique IDentifier).

A UUID is 128 bits long, and can guarantee uniqueness across space and time.

Logger.debug(Util.uuidv4());
// Expected output format: b591fc9a-3c5d-43b1-9dfd-246a1e5d4941

Regular Expressions

You can test with Regular Expressions (RegExp) using the following syntax:

const myVariable = "WEB-APP-027458";

// Test if myVariable ends with six-digits
if (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 also easily access Built-in Functions.

Output

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
){
   const waitingCounter = project.total_not_tested_testcases;
   const initiatedCounter = project.total_in_progress_testcases;
   const completedCounter = project.total_tested_testcases + project.total_not_applicable_testcases;
   const past24hours = Date.datetime('now', '-1 days', 'epoch');
   const endDateTime = Date.datetime(project.end_date, 'epoch');
   const overrun = endDateTime < past24hours;
 
   let status;
 
   if (project.on_hold) {
      status = 'On Hold';
   }
   else if ((completedCounter < project.total_testcases && completedCounter > 0 && overrun) || (project.total_testcases > 0 && completedCounter === 0 && overrun)){
      status = 'Overrun';
   }
   else if (completedCounter === project.total_testcases && project.total_retest_vulnerabilities > 0){
      status = 'Retest';
   }
   else if (completedCounter === project.total_testcases) {
      status = 'Completed';
   }
   else if ((completedCounter < project.total_testcases && completedCounter > 0) || (completedCounter < project.total_testcases && initiatedCounter > 0)){
      status = 'Testing';
   }
   else if (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
){
   const waitingCounter = project.total_not_tested_testcases;
   const initiatedCounter = project.total_in_progress_testcases;
   const completedCounter = project.total_tested_testcases + project.total_not_applicable_testcases;
   const now = Date.datetime('now', 'epoch');
   const endDatePlus24Hours = Date.datetime(project.end_date, '1 days', 'epoch');
  // Check if project is completed. Allow for 24-hour grace period.
  const projectCompleted = now > endDatePlus24Hours || completedCounter === project.total_testcases;
 
   let status;
 
   if (project.on_hold) {
      status = 'On Hold';
   }
   else if (completedCounter === project.total_testcases && project.total_retest_vulnerabilities > 0){
      status = 'Retest';
   }
   else if (projectCompleted) {
      status = 'Completed';
   }
   else if ((completedCounter < project.total_testcases && completedCounter > 0) || (completedCounter < project.total_testcases && initiatedCounter > 0)){
      status = 'Testing';
   }
   else if (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
){
   const waitingCounter = project.total_not_tested_testcases;
   const initiatedCounter = project.total_in_progress_testcases;
   const completedCounter = project.total_tested_testcases + project.total_not_applicable_testcases;
   const now = Date.datetime('now', 'epoch');
   const endDatePlus24Hours = Date.datetime(project.end_date, '1 days', 'epoch');
  // Check if project is completed based on a project custom field "project_completed"
  const projectCompleted = (project.project_custom_fields?.project_completed === "Yes");
 
   let status;
 
   if (project.on_hold) {
      status = 'On Hold';
   }
   else if (completedCounter === project.total_testcases && project.total_retest_vulnerabilities > 0){
      status = 'Retest';
   }
   else if (projectCompleted) {
      status = 'Completed';
   }
   else if ((completedCounter < project.total_testcases && completedCounter > 0) || (completedCounter < project.total_testcases && initiatedCounter > 0)){
      status = 'Testing';
   }
   else if (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
){
   const waitingCounter = project.total_not_tested_testcases;
   const initiatedCounter = project.total_in_progress_testcases;
   const completedCounter = project.total_tested_testcases + project.total_not_applicable_testcases;
   const past24hours = Date.datetime('now', '-1 days', 'epoch');
   const endDateTime = Date.datetime(project.end_date, 'epoch');
   const overrun = endDateTime < past24hours;
   const isRetest = project.total_retests_completed < project.total_retests_requested;
 
   let status;
 
   if (project.on_hold) {
      status = 'On Hold';
   }
   else if ((completedCounter < project.total_testcases && completedCounter > 0 && overrun) || (project.total_testcases > 0 && completedCounter === 0 && overrun)){
      status = 'Overrun';
   }
   else if (isRetest){
      status = 'Retest';
   }
   else if (completedCounter === project.total_testcases) {
      status = 'Completed';
   }
   else if ((completedCounter < project.total_testcases && completedCounter > 0) || (completedCounter < project.total_testcases && initiatedCounter > 0)){
      status = 'Testing';
   }
   else if (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'.

const customerMap = {
    "ACME Corp.": "ACME",
    "Red Team": "REDTEAM",
    "Pentesters": "PEN",
    "Globex Corp.": "GLOBEX-CORP"
};

if (project?.project_custom_fields?.customer?.name
    && customerMap[project.project_custom_fields.customer.name]
) {
    let projectCode = customerMap[project.project_custom_fields.customer.name];

    return projectCode;
}
else {
    return project?.last_project_code;
}

Selecting the customer:

Suggested project code:

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

const customerMap = {
    "ACME Corp.": "ACME",
    "Red Team": "REDTEAM",
    "Pentesters": "PEN",
    "Globex Corp.": "GLOBEX-CORP"
};

const testingTypeMap = {
    "Web App": "WEBAPP",
    "API": "API",
    "Red Team": "REDTEAM",
    "Bug Bounty": "BB" 
};

if (project?.project_custom_fields?.customer?.name
    && customerMap[project.project_custom_fields.customer.name]
    && project.project_custom_fields.testing_types[0]
) {
    let projectCode = customerMap[project.project_custom_fields.customer.name];

    for (let x=0; x < project.project_custom_fields.testing_types.length; x++) {
        if (testingTypeMap[project.project_custom_fields.testing_types[x]]) {
            projectCode = projectCode + "-";
            projectCode = projectCode + testingTypeMap[project.project_custom_fields.testing_types[x]];
        }
    }

    return projectCode;
}
else {
    return project?.last_project_code;
}

Selecting the customer and testing types:

Suggested project code:

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

const customerMap = {
    "ACME Corp.": "ACME",
    "Red Team": "REDTEAM",
    "Pentesters": "PEN",
    "Globex Corp.": "GLOBEX-CORP"
};

const testingTypeMap = {
    "Web App": "WEBAPP",
    "API": "API",
    "Red Team": "REDTEAM",
    "Bug Bounty": "BB" 
};

if (project?.project_custom_fields?.customer?.name
    && customerMap[project.project_custom_fields.customer.name]
    && project.project_custom_fields.testing_types[0]
) {
    let projectCode = customerMap[project.project_custom_fields.customer.name];
    const randomNumber = Math.floor(Math.random() * 999999);

    for (let x=0; x < project.project_custom_fields.testing_types.length; x++) {
        if (testingTypeMap[project.project_custom_fields.testing_types[x]]) {
            projectCode = projectCode + "-";
            projectCode = projectCode + testingTypeMap[project.project_custom_fields.testing_types[x]];
        }
    }

    projectCode = projectCode + "-";
    projectCode = projectCode + randomNumber;

    return projectCode;
}
else {
    return project?.last_project_code;
}

Selecting the customer and testing types:

Suggested project code:

Last updated

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