diff --git a/package-lock.json b/package-lock.json
index f1eaf9c8..f8b73bdd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -113,12 +113,12 @@
"@babel/plugin-syntax-import-assertions": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2",
- "@babel/runtime": "^7.28.6",
+ "@babel/runtime": "^7.29.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
- "@codemirror/view": "^6.39.17",
+ "@codemirror/view": "^6.40.0",
"autoprefixer": "^10.4.27",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
@@ -127,7 +127,7 @@
"colors": "^1.4.0",
"compression-webpack-plugin": "^11.1.0",
"copy-webpack-plugin": "^13.0.1",
- "core-js": "^3.48.0",
+ "core-js": "^3.49.0",
"cspell": "^8.19.4",
"css-loader": "7.1.4",
"eslint": "^9.39.4",
@@ -1755,9 +1755,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
- "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1897,13 +1897,13 @@
}
},
"node_modules/@codemirror/view": {
- "version": "6.39.17",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.17.tgz",
- "integrity": "sha512-Aim4lFqhbijnchl83RLfABWueSGs1oUCSv0mru91QdhpXQeNKprIdRO9LWA4cYkJvuYTKGJN7++9MXx8XW43ag==",
+ "version": "6.40.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz",
+ "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@codemirror/state": "^6.5.0",
+ "@codemirror/state": "^6.6.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
@@ -6820,9 +6820,9 @@
}
},
"node_modules/core-js": {
- "version": "3.48.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
- "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
+ "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -9634,9 +9634,9 @@
}
},
"node_modules/flatted": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
- "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
diff --git a/package.json b/package.json
index 2d40a586..2d4f9ff8 100644
--- a/package.json
+++ b/package.json
@@ -43,12 +43,12 @@
"@babel/plugin-syntax-import-assertions": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2",
- "@babel/runtime": "^7.28.6",
+ "@babel/runtime": "^7.29.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
- "@codemirror/view": "^6.39.17",
+ "@codemirror/view": "^6.40.0",
"autoprefixer": "^10.4.27",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
@@ -57,7 +57,7 @@
"colors": "^1.4.0",
"compression-webpack-plugin": "^11.1.0",
"copy-webpack-plugin": "^13.0.1",
- "core-js": "^3.48.0",
+ "core-js": "^3.49.0",
"cspell": "^8.19.4",
"css-loader": "7.1.4",
"eslint": "^9.39.4",
diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs
index ab8f83de..5be10868 100755
--- a/src/core/Chef.mjs
+++ b/src/core/Chef.mjs
@@ -55,8 +55,15 @@ class Chef {
progress = await recipe.execute(this.dish, progress);
} catch (err) {
log.error(err);
+
+ let displayStr;
+ if ("displayStr" in err) {
+ displayStr = err.displayStr;
+ } else {
+ displayStr = err.toString();
+ }
error = {
- displayStr: err.displayStr,
+ displayStr: displayStr,
};
progress = err.progress;
}
diff --git a/src/core/Ingredient.mjs b/src/core/Ingredient.mjs
old mode 100755
new mode 100644
index 319dfb15..0dd31707
--- a/src/core/Ingredient.mjs
+++ b/src/core/Ingredient.mjs
@@ -5,7 +5,8 @@
*/
import Utils from "./Utils.mjs";
-import {fromHex} from "./lib/Hex.mjs";
+import { fromHex } from "./lib/Hex.mjs";
+import OperationError from "./errors/OperationError.mjs";
/**
* The arguments to operations.
@@ -119,7 +120,9 @@ class Ingredient {
number = parseFloat(data);
if (isNaN(number)) {
const sample = Utils.truncate(data.toString(), 10);
- throw "Invalid ingredient value. Not a number: " + sample;
+ throw new OperationError(
+ "Invalid ingredient value. Not a number: " + sample,
+ );
}
return number;
default:
diff --git a/src/core/Operation.mjs b/src/core/Operation.mjs
index 24739d3f..09058766 100755
--- a/src/core/Operation.mjs
+++ b/src/core/Operation.mjs
@@ -5,6 +5,7 @@
*/
import Dish from "./Dish.mjs";
+import OperationError from "./errors/OperationError.mjs";
import Ingredient from "./Ingredient.mjs";
/**
@@ -223,7 +224,11 @@ class Operation {
*/
set ingValues(ingValues) {
ingValues.forEach((val, i) => {
- this._ingList[i].value = val;
+ try {
+ this._ingList[i].value = val;
+ } catch (err) {
+ throw new OperationError(`Failed to set value of ingredient '${this._ingList[i].name}': ${err}`);
+ }
});
}
diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs
index 7824d1e8..b4a10e03 100755
--- a/src/core/Recipe.mjs
+++ b/src/core/Recipe.mjs
@@ -70,11 +70,15 @@ class Recipe {
if (o instanceof Operation) {
return o;
} else {
- const op = new modules[o.module][o.name]();
- op.ingValues = o.ingValues;
- op.breakpoint = o.breakpoint;
- op.disabled = o.disabled;
- return op;
+ try {
+ const op = new modules[o.module][o.name]();
+ op.ingValues = o.ingValues;
+ op.breakpoint = o.breakpoint;
+ op.disabled = o.disabled;
+ return op;
+ } catch (err) {
+ throw new Error(`Failed to hydrate operation '${o.name}': ${err}`);
+ }
}
});
}
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index f6fe1790..f9e0d684 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -249,6 +249,7 @@
"DNS over HTTPS",
"Strip HTTP headers",
"Dechunk HTTP response",
+ "Parse Ethernet frame",
"Parse User Agent",
"Parse IP range",
"Parse IPv6 address",
diff --git a/src/core/operations/Jq.mjs b/src/core/operations/Jq.mjs
index c1e02b34..bc502957 100644
--- a/src/core/operations/Jq.mjs
+++ b/src/core/operations/Jq.mjs
@@ -30,7 +30,12 @@ class Jq extends Operation {
name: "Query",
type: "string",
value: ""
- }
+ },
+ {
+ name: "Raw",
+ type: "boolean",
+ value: false
+ },
];
}
@@ -40,7 +45,7 @@ class Jq extends Operation {
* @returns {string}
*/
run(input, args) {
- const [query] = args;
+ const [query, raw] = args;
let result;
try {
@@ -48,8 +53,11 @@ class Jq extends Operation {
} catch (err) {
throw new OperationError(`Invalid jq expression: ${err.message}`);
}
-
- return JSON.stringify(result);
+ if (raw && typeof result === "string") {
+ return result;
+ } else {
+ return JSON.stringify(result);
+ }
}
}
diff --git a/src/core/operations/ParseEthernetFrame.mjs b/src/core/operations/ParseEthernetFrame.mjs
new file mode 100644
index 00000000..9dac5d57
--- /dev/null
+++ b/src/core/operations/ParseEthernetFrame.mjs
@@ -0,0 +1,115 @@
+/**
+ * @author tedk [tedk@ted.do]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import {fromHex, toHex} from "../lib/Hex.mjs";
+
+/**
+ * Parse Ethernet frame operation
+ */
+class ParseEthernetFrame extends Operation {
+
+ /**
+ * ParseEthernetFrame constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Parse Ethernet frame";
+ this.module = "Default";
+ this.description = "Parses an Ethernet frame and either shows the deduced values (Source and destination MAC, VLANs) or returns the packet data.
Good for use in conjunction with the Parse IPv4, and Parse TCP/UDP recipes.";
+ this.infoURL = "https://en.wikipedia.org/wiki/Ethernet_frame#Frame_%E2%80%93_data_link_layer";
+ this.inputType = "string";
+ this.outputType = "html";
+ this.args = [
+ {
+ name: "Input type",
+ type: "option",
+ value: [
+ "Raw", "Hex"
+ ],
+ defaultIndex: 0,
+ },
+ {
+ name: "Return type",
+ type: "option",
+ value: [
+ "Text output", "Packet data", "Packet data (hex)",
+ ],
+ defaultIndex: 0,
+ }
+ ];
+ }
+
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {html}
+ */
+ run(input, args) {
+ const format = args[0];
+ const outputFormat = args[1];
+
+ if (format === "Hex") {
+ input = fromHex(input);
+ } else if (format === "Raw") {
+ input = new Uint8Array(Utils.strToArrayBuffer(input));
+ } else {
+ throw new OperationError("Invalid input format selected.");
+ }
+
+ const destinationMac = input.slice(0, 6);
+ const sourceMac = input.slice(6, 12);
+
+ let offset = 12;
+ const vlans = [];
+
+ while (offset < input.length) {
+ const ethType = Utils.byteArrayToChars(input.slice(offset, offset+2));
+ offset += 2;
+
+
+ if (ethType === "\x08\x00") {
+ break;
+ } else if (ethType === "\x81\x00" || ethType === "\x88\xA8") {
+ // Parse the VLAN tag:
+ // [0000] 0000 0000 0000
+ // ^^^ PRIO - Ignored
+ // ^ DEI - Ignored
+ // ^^^^ ^^^^ ^^^^ VLAN ID
+ const vlanTag = input.slice(offset+2, offset+4);
+ vlans.push((vlanTag[0] & 0b00001111) << 4 | vlanTag[1]);
+
+ offset += 2;
+ } else {
+ break;
+ }
+ }
+
+ const packetData = input.slice(offset);
+
+ if (outputFormat === "Packet data") {
+ return Utils.byteArrayToChars(packetData);
+ } else if (outputFormat === "Packet data (hex)") {
+ return toHex(packetData);
+ } else if (outputFormat === "Text output") {
+ let retval = `Source MAC: ${toHex(sourceMac, ":")}\nDestination MAC: ${toHex(destinationMac, ":")}\n`;
+ if (vlans.length > 0) {
+ retval += `VLAN: ${vlans.join(", ")}\n`;
+ }
+ retval += `Data:\n${toHex(packetData)}`;
+ return retval;
+ }
+
+ }
+
+
+}
+
+export default ParseEthernetFrame;
diff --git a/src/core/operations/ParseIPv4Header.mjs b/src/core/operations/ParseIPv4Header.mjs
index 84351cdc..4eed5d46 100644
--- a/src/core/operations/ParseIPv4Header.mjs
+++ b/src/core/operations/ParseIPv4Header.mjs
@@ -33,6 +33,12 @@ class ParseIPv4Header extends Operation {
"name": "Input format",
"type": "option",
"value": ["Hex", "Raw"]
+ },
+ {
+ "name": "Output format",
+ "type": "option",
+ "value": ["Table", "Data (hex)", "Data (raw)"],
+ defaultIndex: 0,
}
];
}
@@ -44,6 +50,8 @@ class ParseIPv4Header extends Operation {
*/
run(input, args) {
const format = args[0];
+ const outputFormat = args[1];
+
let output;
if (format === "Hex") {
@@ -98,7 +106,10 @@ class ParseIPv4Header extends Operation {
checksumResult = givenChecksum + " (incorrect, should be " + correctChecksum + ")";
}
- output = `
| Field | Value |
+ const data = input.slice(ihl * 4);
+
+ if (outputFormat === "Table") {
+ output = `| Field | Value |
| Version | ${version} |
| Internet Header Length (IHL) | ${ihl} (${ihl * 4} bytes) |
| Differentiated Services Code Point (DSCP) | ${dscp} |
@@ -116,13 +127,19 @@ class ParseIPv4Header extends Operation {
| Protocol | ${protocol}, ${protocolInfo.protocol} (${protocolInfo.keyword}) |
| Header checksum | ${checksumResult} |
| Source IP address | ${ipv4ToStr(srcIP)} |
-| Destination IP address | ${ipv4ToStr(dstIP)} |
`;
+| Destination IP address | ${ipv4ToStr(dstIP)} |
+| Data (hex) | ${toHex(data)} |
`;
- if (ihl > 5) {
- output += `| Options | ${toHex(options)} |
`;
+ if (ihl > 5) {
+ output += `| Options | ${toHex(options)} |
`;
+ }
+
+ return output + "
";
+ } else if (outputFormat === "Data (hex)") {
+ return toHex(data);
+ } else if (outputFormat === "Data (raw)") {
+ return Utils.byteArrayToChars(data);
}
-
- return output + "
";
}
}
diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs
index ae972a59..7cde638d 100755
--- a/src/web/Manager.mjs
+++ b/src/web/Manager.mjs
@@ -130,6 +130,7 @@ class Manager {
// Controls
document.getElementById("bake").addEventListener("click", this.controls.bakeClick.bind(this.controls));
document.getElementById("auto-bake").addEventListener("change", this.controls.autoBakeChange.bind(this.controls));
+ document.getElementById("auto-bake").addEventListener("keydown", this.controls.autoBakeKeyboardHandler.bind(this.controls));
document.getElementById("step").addEventListener("click", this.controls.stepClick.bind(this.controls));
document.getElementById("clr-recipe").addEventListener("click", this.controls.clearRecipeClick.bind(this.controls));
document.getElementById("save").addEventListener("click", this.controls.saveClick.bind(this.controls));
diff --git a/src/web/html/index.html b/src/web/html/index.html
index 38bf7ccc..2d4de4bd 100755
--- a/src/web/html/index.html
+++ b/src/web/html/index.html
@@ -145,7 +145,7 @@
-
+
@@ -584,22 +584,22 @@
-
-
+
FAQs
-
-
+
Report a bug
-
-
+
About
-
-
+
Keybindings
diff --git a/src/web/stylesheets/layout/_banner.css b/src/web/stylesheets/layout/_banner.css
index 59856958..1ef5d766 100755
--- a/src/web/stylesheets/layout/_banner.css
+++ b/src/web/stylesheets/layout/_banner.css
@@ -26,6 +26,36 @@
color: var(--banner-url-colour);
}
+#options:focus {
+ background-color: #eef3ec;
+ border: solid black 2px;
+ border-radius: 4px;
+}
+
+#support:focus {
+ background-color: #eef3ec;
+ border: solid black 2px;
+ border-radius: 4px;
+}
+
+#notice:focus {
+ background-color: #eef3ec;
+ border: solid black 2px;
+ border-radius: 4px;
+}
+
+#banner .col a:focus {
+ background-color: #eef3ec;
+ border: solid black 2px;
+ border-radius: 4px;
+}
+
+#notice-wrapper #notice:focus {
+ background-color: #eef3ec;
+ border: solid black 2px;
+ border-radius: 4px;
+}
+
#notice-wrapper {
text-align: center;
overflow: hidden;
diff --git a/src/web/stylesheets/layout/_modals.css b/src/web/stylesheets/layout/_modals.css
index 3cd0ce09..e15bc3e9 100755
--- a/src/web/stylesheets/layout/_modals.css
+++ b/src/web/stylesheets/layout/_modals.css
@@ -78,6 +78,11 @@
border-left: 2px solid var(--primary-border-colour);
}
+p a:focus {
+ color: #0a6ebd;
+ text-decoration: underline;
+}
+
.checkbox label input[type=checkbox]+.checkbox-decorator .check,
.checkbox label input[type=checkbox]+.checkbox-decorator .check::before {
border-color: var(--input-border-colour);
diff --git a/src/web/waiters/ControlsWaiter.mjs b/src/web/waiters/ControlsWaiter.mjs
index b57940a3..7f2bb683 100755
--- a/src/web/waiters/ControlsWaiter.mjs
+++ b/src/web/waiters/ControlsWaiter.mjs
@@ -57,6 +57,18 @@ class ControlsWaiter {
}
}
+ /**
+ * Checks or unchecks the Auto Bake checkbox with "Enter"
+ * @param {Event} ev
+ */
+ autoBakeKeyboardHandler(ev) {
+ const checkBox = document.getElementById("auto-bake");
+ ev.preventDefault();
+ if (ev.key === "Enter" || ev.key === " ") {
+ checkBox.checked = !checkBox.checked;
+ }
+ }
+
/**
* Handler to trigger baking.
@@ -387,6 +399,18 @@ class ControlsWaiter {
*/
supportButtonClick(e) {
e.preventDefault();
+ const faqs = document.getElementById("faqs");
+ const faqsAElement = faqs.getElementsByTagName("a");
+ for (let i = 0; i < faqsAElement.length; i++) {
+ faqsAElement[i].setAttribute("tabindex", "0");
+ faqsAElement[i].addEventListener("keydown", this.navigateFAQList, false);
+ }
+
+ const tabs = document.querySelectorAll('[role="tab"]');
+
+ for (let i = 0; i < tabs.length; i++) {
+ tabs[i].addEventListener("keydown", this.changeTabs, false);
+ }
const reportBugInfo = document.getElementById("report-bug-info");
const saveLink = this.generateStateUrl(true, true, null, null, "https://gchq.github.io/CyberChef/");
@@ -403,6 +427,64 @@ ${navigator.userAgent}
}
+ /**
+ * @param {Event} ev
+ */
+ changeTabs(ev) {
+ const tab = ev.target;
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ if (ev.key === "ArrowRight") {
+ const nextTab = tab.parentElement;
+ if (nextTab.nextElementSibling === null) {
+ tab.parentElement.parentElement.firstElementChild.firstElementChild.focus();
+ } else {
+ nextTab.nextElementSibling.firstElementChild.focus();
+ }
+
+ } else if (ev.key === "ArrowLeft") {
+ const prevTab = tab.parentElement;
+
+ if (prevTab.previousElementSibling === null) {
+ tab.parentElement.parentElement.lastElementChild.firstElementChild.focus();
+ } else {
+ prevTab.previousElementSibling.firstElementChild.focus();
+ }
+ } else if (ev.key === "Tab" && !ev.shiftKey && ev.target === document.getElementById("tab-1")) {
+ document.getElementById("faqs").querySelector("[class='btn btn-primary']").focus();
+ } else if (ev.key === "Tab" && !ev.shiftKey && ev.target === document.getElementById("tab-2")) {
+ document.getElementById("report-bug").querySelector("[class='btn btn-primary']").focus();
+ } else if (ev.key === "Tab" && !ev.shiftKey && ev.target === document.getElementById("tab-3")) {
+ document.getElementById("about").querySelector("[href]").focus();
+ } else if (ev.key === "Tab" && !ev.shiftKey && ev.target === document.getElementById("tab-4")) {
+ const button = document.getElementById("support-modal").getElementsByClassName("modal-footer");
+ const close = button[0].firstElementChild;
+ close.focus();
+ } else if (ev.key === "Enter" || ev.key === "Space" || ev.key === " ") {
+ tab.click();
+ }
+ }
+
+ /**
+ * @param {Event} ev
+ */
+ navigateFAQList(ev) {
+
+ const el = ev.target.nextElementSibling;
+ if (ev.key === "Enter" || ev.key === "Space" || ev.key === " ") {
+ ev.preventDefault();
+ const question = el.classList;
+ if (question !== undefined && question.value) {
+ if (!question.value.includes("show")) {
+ question.add("show");
+ } else if (question.contains("show")) {
+ question.remove("show");
+ }
+ }
+ }
+ }
+
/**
* Shows the stale indicator to show that the input or recipe has changed
* since the last bake.
diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js
index dde84f68..5ab55451 100644
--- a/tests/browser/02_ops.js
+++ b/tests/browser/02_ops.js
@@ -268,7 +268,7 @@ module.exports = {
testOpHtml(browser, "Parse colour code", "#000", ".colorpicker-preview", "rgb(0, 0, 0)");
testOpHtml(browser, "Parse DateTime", "01/12/2000 13:00:00", "", /Date: Friday 1st December 2000/);
// testOp(browser, "Parse IP range", "test input", "test_output");
- testOpHtml(browser, "Parse IPv4 header", "45 c0 00 c4 02 89 00 00 ff 11 1e 8c c0 a8 0c 01 c0 a8 0c 02", "tr:last-child td:last-child", "192.168.12.2");
+ testOpHtml(browser, "Parse IPv4 header", "45 c0 00 c4 02 89 00 00 ff 11 1e 8c c0 a8 0c 01 c0 a8 0c 02", "tr:nth-last-child(2) td:last-child", "192.168.12.2");
// testOp(browser, "Parse IPv6 address", "test input", "test_output");
// testOp(browser, "Parse ObjectID timestamp", "test input", "test_output");
// testOp(browser, "Parse QR Code", "test input", "test_output");
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index 9ef8eaa7..7ad76baf 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -122,6 +122,7 @@ import "./tests/NetBIOS.mjs";
import "./tests/NormaliseUnicode.mjs";
import "./tests/NTLM.mjs";
import "./tests/OTP.mjs";
+import "./tests/ParseEthernetFrame.mjs";
import "./tests/ParseIPRange.mjs";
import "./tests/ParseObjectIDTimestamp.mjs";
import "./tests/ParseQRCode.mjs";
diff --git a/tests/operations/tests/Jq.mjs b/tests/operations/tests/Jq.mjs
new file mode 100644
index 00000000..a2435450
--- /dev/null
+++ b/tests/operations/tests/Jq.mjs
@@ -0,0 +1,32 @@
+/**
+ * Jq tests.
+ *
+ * @author rtpt-romankarwacik [roman.karwacik@redteam-pentesting.de]
+ *
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Get raw JSON Property",
+ input: '{"data": "testString\\u0000"}',
+ expectedOutput: "testString\u0000",
+ recipeConfig: [
+ {
+ op: "Jq",
+ args: [".data", true],
+ },
+ ],
+ },
+ {
+ name: "Get JSON Property",
+ input: '{"data": "testString\\u0000"}',
+ expectedOutput: "\"testString\\u0000\"",
+ recipeConfig: [
+ {
+ op: "Jq",
+ args: [".data", false],
+ },
+ ],
+ },
+]);
diff --git a/tests/operations/tests/ParseEthernetFrame.mjs b/tests/operations/tests/ParseEthernetFrame.mjs
new file mode 100644
index 00000000..c849e207
--- /dev/null
+++ b/tests/operations/tests/ParseEthernetFrame.mjs
@@ -0,0 +1,45 @@
+/**
+ * Parse Ethernet frame tests.
+ *
+ * @author tedk [tedk@ted.do]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Parse plain Ethernet frame",
+ input: "000000000000ffffffffffff08004500",
+ expectedOutput: "Source MAC: ff:ff:ff:ff:ff:ff\nDestination MAC: 00:00:00:00:00:00\nData:\n45 00",
+ recipeConfig: [
+ {
+ "op": "Parse Ethernet frame",
+ "args": ["Hex", "Text output"]
+ }
+ ]
+ },
+ // Example PCAP data from: https://packetlife.net/captures/protocol/vlan/
+ {
+ name: "Parse Ethernet frame with one VLAN tag (802.1q)",
+ input: "01000ccdcdd00013c3dfae188100a0760165aaaa",
+ expectedOutput: "Source MAC: 00:13:c3:df:ae:18\nDestination MAC: 01:00:0c:cd:cd:d0\nVLAN: 117\nData:\naa aa",
+ recipeConfig: [
+ {
+ "op": "Parse Ethernet frame",
+ "args": ["Hex", "Text output"]
+ }
+ ]
+ },
+ {
+ name: "Parse Ethernet frame with two VLAN tags (802.1ad)",
+ input: "0019aa7de688002155c8f13c810000d18100001408004500",
+ expectedOutput: "Source MAC: 00:21:55:c8:f1:3c\nDestination MAC: 00:19:aa:7d:e6:88\nVLAN: 16, 128\nData:\n45 00",
+ recipeConfig: [
+ {
+ "op": "Parse Ethernet frame",
+ "args": ["Hex", "Text output"]
+ }
+ ]
+ }
+]);