From 85513daea4c04c01c9bd6a3965973fe1cc207aef Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 24 Apr 2026 12:46:38 +0100
Subject: [PATCH 001/114] chore (deps): bump @codemirror/search from 6.6.0 to
6.7.0 (#2331)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index a21038ee..5907637d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -115,7 +115,7 @@
"@babel/runtime": "^7.29.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.3",
- "@codemirror/search": "^6.6.0",
+ "@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.41.0",
"autoprefixer": "^10.5.0",
@@ -1868,9 +1868,9 @@
}
},
"node_modules/@codemirror/search": {
- "version": "6.6.0",
- "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz",
- "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz",
+ "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 783a0980..28f4e617 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
"@babel/runtime": "^7.29.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.3",
- "@codemirror/search": "^6.6.0",
+ "@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.41.0",
"autoprefixer": "^10.5.0",
From e0774ed9408a3079de5c1cfac59590e0b5fd205c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:13:57 +0100
Subject: [PATCH 002/114] chore (deps): bump the patch-updates group with 6
updates (#2330)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 48 +++++++++++++++++++++++------------------------
package.json | 12 ++++++------
2 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 5907637d..2ad8f7e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,7 @@
"@astronautlabs/amf": "^0.0.6",
"@blu3r4y/lzma": "^2.3.3",
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
- "@xmldom/xmldom": "^0.8.12",
+ "@xmldom/xmldom": "^0.8.13",
"argon2-browser": "^1.18.0",
"arrive": "^2.5.3",
"assert": "^2.1.0",
@@ -38,7 +38,7 @@
"d3": "7.9.0",
"d3-hexbin": "^0.2.2",
"diff": "^5.2.2",
- "dompurify": "^3.4.0",
+ "dompurify": "^3.4.1",
"es6-promisify": "^7.0.0",
"escodegen": "^2.1.0",
"esprima": "^4.0.1",
@@ -62,7 +62,7 @@
"jsonpath-plus": "^10.4.0",
"jsonwebtoken": "9.0.3",
"jsqr": "^1.4.0",
- "jsrsasign": "^11.1.2",
+ "jsrsasign": "^11.1.3",
"kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
@@ -117,7 +117,7 @@
"@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.5.4",
- "@codemirror/view": "^6.41.0",
+ "@codemirror/view": "^6.41.1",
"autoprefixer": "^10.5.0",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
@@ -143,7 +143,7 @@
"grunt-exec": "~3.0.0",
"grunt-webpack": "^6.0.0",
"grunt-zip": "^1.0.0",
- "html-webpack-plugin": "^5.6.6",
+ "html-webpack-plugin": "^5.6.7",
"imports-loader": "^5.0.0",
"mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
@@ -154,7 +154,7 @@
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
- "terser": "^5.46.1",
+ "terser": "^5.46.2",
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
@@ -1890,9 +1890,9 @@
}
},
"node_modules/@codemirror/view": {
- "version": "6.41.0",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz",
- "integrity": "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==",
+ "version": "6.41.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.1.tgz",
+ "integrity": "sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4524,9 +4524,9 @@
}
},
"node_modules/@xmldom/xmldom": {
- "version": "0.8.12",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
- "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==",
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -8296,9 +8296,9 @@
}
},
"node_modules/dompurify": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
- "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz",
+ "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -10927,9 +10927,9 @@
}
},
"node_modules/html-webpack-plugin": {
- "version": "5.6.6",
- "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz",
- "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==",
+ "version": "5.6.7",
+ "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz",
+ "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12412,9 +12412,9 @@
"license": "Apache-2.0"
},
"node_modules/jsrsasign": {
- "version": "11.1.2",
- "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.2.tgz",
- "integrity": "sha512-GJuqiU/Grs6BaBBXMAZM9kxhsBrksZE0pF3qIfpkopMd7OMJ9zZmE/+CpV//97srfEyyyq1Ec0ELQtSlW/gPTA==",
+ "version": "11.1.3",
+ "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.3.tgz",
+ "integrity": "sha512-nPnK5D/4lv0Dwr7TlzrKtAd8JlLZwFTqTUUB3NQCbtdobcRcohGFxjbPySDVh74iWUudcCsapYT6OxoyhJLhhA==",
"license": "MIT"
},
"node_modules/jszip": {
@@ -17062,9 +17062,9 @@
"license": "MIT"
},
"node_modules/terser": {
- "version": "5.46.1",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
- "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
+ "version": "5.46.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz",
+ "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
diff --git a/package.json b/package.json
index 28f4e617..2d7b8ae8 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.5.4",
- "@codemirror/view": "^6.41.0",
+ "@codemirror/view": "^6.41.1",
"autoprefixer": "^10.5.0",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
@@ -74,7 +74,7 @@
"grunt-exec": "~3.0.0",
"grunt-webpack": "^6.0.0",
"grunt-zip": "^1.0.0",
- "html-webpack-plugin": "^5.6.6",
+ "html-webpack-plugin": "^5.6.7",
"imports-loader": "^5.0.0",
"mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
@@ -85,7 +85,7 @@
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
- "terser": "^5.46.1",
+ "terser": "^5.46.2",
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
@@ -97,7 +97,7 @@
"@astronautlabs/amf": "^0.0.6",
"@blu3r4y/lzma": "^2.3.3",
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
- "@xmldom/xmldom": "^0.8.12",
+ "@xmldom/xmldom": "^0.8.13",
"argon2-browser": "^1.18.0",
"arrive": "^2.5.3",
"assert": "^2.1.0",
@@ -121,7 +121,7 @@
"d3": "7.9.0",
"d3-hexbin": "^0.2.2",
"diff": "^5.2.2",
- "dompurify": "^3.4.0",
+ "dompurify": "^3.4.1",
"es6-promisify": "^7.0.0",
"escodegen": "^2.1.0",
"esprima": "^4.0.1",
@@ -145,7 +145,7 @@
"jsonpath-plus": "^10.4.0",
"jsonwebtoken": "9.0.3",
"jsqr": "^1.4.0",
- "jsrsasign": "^11.1.2",
+ "jsrsasign": "^11.1.3",
"kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
From 4080df0bbd7fd69b02b8d9e47326503a4fbadb26 Mon Sep 17 00:00:00 2001
From: ko80240 <275699905+ko80240@users.noreply.github.com>
Date: Fri, 24 Apr 2026 15:14:35 +0100
Subject: [PATCH 003/114] Added metadata extraction for UUID strings. (#2322)
---
src/core/operations/AnalyseUUID.mjs | 115 +++++++++++++++++++++++--
tests/node/tests/operations.mjs | 5 +-
tests/operations/index.mjs | 1 +
tests/operations/tests/AnalyseUUID.mjs | 66 ++++++++++++++
4 files changed, 178 insertions(+), 9 deletions(-)
create mode 100644 tests/operations/tests/AnalyseUUID.mjs
diff --git a/src/core/operations/AnalyseUUID.mjs b/src/core/operations/AnalyseUUID.mjs
index b3506017..6f960c8a 100644
--- a/src/core/operations/AnalyseUUID.mjs
+++ b/src/core/operations/AnalyseUUID.mjs
@@ -1,5 +1,6 @@
/**
* @author n1474335 [n1474335@gmail.com]
+ * @author ko80240 [csk.dev@proton.me]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
@@ -8,6 +9,7 @@ import * as uuid from "uuid";
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
+import { toHex } from "../lib/Hex.mjs";
/**
* Analyse UUID operation
@@ -22,27 +24,128 @@ class AnalyseUUID extends Operation {
this.name = "Analyse UUID";
this.module = "Crypto";
- this.description = "Tries to determine information about a given UUID and suggests which version may have been used to generate it";
+ this.description = "Operation for extracting metadata and detecting the version of a given UUID.";
this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier";
this.inputType = "string";
this.outputType = "string";
- this.args = [];
+ this.args = [
+ {
+ name: "Include Metadata",
+ type: "boolean",
+ value: true
+ }
+ ];
}
/**
- * @param {string} input
+ * @param {string} input - Expects a valid UUID string
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
+ input = input.trim();
+
+ let uuidVersion, uuidBytes;
try {
- const uuidVersion = uuid.version(input);
- return "UUID version: " + uuidVersion;
+ uuidVersion = uuid.version(input); // Re-using the uuid library to extract version
+ uuidBytes = uuid.parse(input); // Re-using the uuid library to parse bytes
} catch (error) {
throw new OperationError("Invalid UUID");
}
- }
+ const [includeMetadata] = args;
+ const dv = new DataView(uuidBytes.buffer, uuidBytes.byteOffset, uuidBytes.byteLength); // Dataview helps handle the multi-byte ints
+ const uuidInteger = (dv.getBigUint64(0) << 64n) | dv.getBigUint64(8);
+
+ const sections = [`Version:\n${uuidVersion}`];
+
+ if (includeMetadata) {
+ const parser = UUID_PARSERS[uuidVersion];
+ const decoded = parser?.(uuidBytes, dv);
+ sections.push(formatDecoded(decoded));
+ }
+
+ sections.push(`UUID Integer:\n${uuidInteger}`);
+
+ return sections.filter(Boolean).join("\n\n");
+ }
}
export default AnalyseUUID;
+
+/**
+ * Metadata can be extracted for versions 1, 6, and 7.
+ * Enum-like frozen mapping of UUID version to parser function.
+ */
+const UUID_PARSERS = Object.freeze({
+ 1: parsev1v6,
+ 6: parsev1v6,
+ 7: parsev7,
+});
+
+/**
+ * Versions 1 and 6. Note 6 is a re-order of 1.
+ * Version 1 == layout: timeLow(32) | timeMid(16) | timeHi(12)
+ * Version 6 == layout: timeHi(32) | timeMid(16) | timeLow(12)
+ */
+function parsev1v6(uuidBytes, dv) {
+ const isV1 = (uuidBytes[6] >> 4) === 1;
+
+ const timeStamp =
+ isV1 ? (
+ (BigInt(dv.getUint16(6) & 0x0fff) << 48n) | // mask off version bits
+ (BigInt(dv.getUint16(4)) << 32n) |
+ BigInt(dv.getUint32(0))
+ ) : (
+ (BigInt(dv.getUint32(0)) << 28n) |
+ (BigInt(dv.getUint16(4)) << 12n) |
+ (BigInt(dv.getUint16(6) & 0x0fff))
+ );
+
+ // Convert to Unix time
+ const milliseconds =
+ Number(
+ (timeStamp - 122192928000000000n) / 10000n
+ );
+
+ return {
+ timestamp: milliseconds,
+ isoTimestamp: new Date(milliseconds).toISOString(),
+ clock: ((uuidBytes[8] & 0x3f) << 8) | uuidBytes[9],
+ node: toHex(uuidBytes.slice(10), ":").toUpperCase()
+ };
+}
+
+/** Version 7 */
+function parsev7(uuidBytes, dv) {
+ const milliseconds = Number((BigInt(dv.getUint32(0)) << 16n) | BigInt(dv.getUint16(4)));
+
+ return {
+ timestamp: milliseconds,
+ isoTimestamp: new Date(milliseconds).toISOString(),
+ randA: ((uuidBytes[6] & 0x0f) << 8) | uuidBytes[7],
+ randB: toHex(uuidBytes.slice(8), "").toUpperCase()
+ };
+}
+
+/**
+ * Formats metadata
+ *
+ * @param {Object|undefined} decoded
+ * @returns {string}
+ */
+function formatDecoded(decoded) {
+ if (!decoded) return "No metadata available. Only versions 1, 6, 7 are supported.";
+
+ return Object.entries({
+ "Timestamp": decoded.timestamp,
+ "Timestamp (ISO)": decoded.isoTimestamp,
+ "Node": decoded.node,
+ "Clock": decoded.clock,
+ "Rand A": decoded.randA,
+ "Rand B": decoded.randB
+ })
+ .filter(([, value]) => value !== undefined)
+ .map(([label, value]) => `${label}:\n${value}`)
+ .join("\n\n");
+}
diff --git a/tests/node/tests/operations.mjs b/tests/node/tests/operations.mjs
index 41eddd82..a97f5ccb 100644
--- a/tests/node/tests/operations.mjs
+++ b/tests/node/tests/operations.mjs
@@ -589,8 +589,7 @@ Password: 282760`;
...[1, 3, 4, 5, 6, 7].map(version => it(`Analyze UUID v${version}`, () => {
const uuid = chef.generateUUID("", { "version": `v${version}` }).toString();
const result = chef.analyseUUID(uuid).toString();
- const expected = `UUID version: ${version}`;
- assert.strictEqual(result, expected);
+ assert.ok(result.startsWith(`Version:\n${version}\n`), `Expected output to start with "Version:\\n${version}\\n", got: ${result}`);
})),
it("Generate UUID using defaults", () => {
@@ -598,7 +597,7 @@ Password: 282760`;
assert.ok(uuid);
const analysis = chef.analyseUUID(uuid).toString();
- assert.strictEqual(analysis, "UUID version: 4");
+ assert.ok(analysis.startsWith("Version:\n4\n"), `Expected output to start with "Version:\\n4\\n", got: ${analysis}`);
}),
it("Gzip, Gunzip", () => {
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index d18fbffe..420d0129 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -16,6 +16,7 @@ import { setLongTestFailure, logTestReport } from "../lib/utils.mjs";
import TestRegister from "../lib/TestRegister.mjs";
import "./tests/A1Z26CipherDecode.mjs";
import "./tests/AESKeyWrap.mjs";
+import "./tests/AnalyseUUID.mjs";
import "./tests/AlternatingCaps.mjs";
import "./tests/AvroToJSON.mjs";
import "./tests/BaconCipher.mjs";
diff --git a/tests/operations/tests/AnalyseUUID.mjs b/tests/operations/tests/AnalyseUUID.mjs
new file mode 100644
index 00000000..89118421
--- /dev/null
+++ b/tests/operations/tests/AnalyseUUID.mjs
@@ -0,0 +1,66 @@
+/**
+ * Analyse UUID tests
+ *
+ * @author ko80240 [csk.dev@proton.me]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ "name": "Analyse UUID: v1 UUID extracts timestamp, clock, and node",
+ "input": "cefa1760-28ee-11f1-9f95-1fb76af3e239",
+ "expectedOutput": "Version:\n1\n\nTimestamp:\n1774514156502\n\nTimestamp (ISO):\n2026-03-26T08:35:56.502Z\n\nNode:\n1F:B7:6A:F3:E2:39\n\nClock:\n8085\n\nUUID Integer:\n275119515460318071558429785403790975545",
+ "recipeConfig": [
+ {
+ "op": "Analyse UUID",
+ "args": [true]
+ }
+ ]
+ },
+ {
+ "name": "Analyse UUID: v7 UUID extracts timestamp, randA, and randB",
+ "input": "019d294a-af64-7728-9524-26da08f50708",
+ "expectedOutput": "Version:\n7\n\nTimestamp:\n1774514253668\n\nTimestamp (ISO):\n2026-03-26T08:37:33.668Z\n\nRand A:\n1832\n\nRand B:\n952426DA08F50708\n\nUUID Integer:\n2145256098533991595556290452700595976",
+ "recipeConfig": [
+ {
+ "op": "Analyse UUID",
+ "args": [true]
+ }
+ ]
+ },
+ {
+ "name": "Analyse UUID: v4 UUID should show no metadata - not possible",
+ "input": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
+ "expectedOutput": "Version:\n4\n\nNo metadata available. Only versions 1, 6, 7 are supported.\n\nUUID Integer:\n324969006592305634633390616021200786553",
+ "recipeConfig": [
+ {
+ "op": "Analyse UUID",
+ "args": [true]
+ }
+ ]
+ },
+ {
+ "name": "Analyse UUID: if the 'Include Metadata' option is false it should return not metadata",
+ "input": "cefa1760-28ee-11f1-9f95-1fb76af3e239",
+ "expectedOutput": "Version:\n1\n\nUUID Integer:\n275119515460318071558429785403790975545",
+ "recipeConfig": [
+ {
+ "op": "Analyse UUID",
+ "args": [false]
+ }
+ ]
+ },
+ {
+ "name": "Analyse UUID: invalid UUID should return error message",
+ "input": "not-a-uuid",
+ "expectedOutput": "Invalid UUID",
+ "recipeConfig": [
+ {
+ "op": "Analyse UUID",
+ "args": [true]
+ }
+ ]
+ }
+]);
From 5a1b121041ec98e9d5cbd61a92f6883f824fa266 Mon Sep 17 00:00:00 2001
From: Matt C
Date: Fri, 24 Apr 2026 11:19:36 -0400
Subject: [PATCH 004/114] Accessibility - Add support for screenreaders in
operations search (#1862)
---
src/web/HTMLOperation.mjs | 10 +++++--
src/web/html/index.html | 2 +-
src/web/waiters/OperationsWaiter.mjs | 43 +++++++++++++++++++---------
3 files changed, 39 insertions(+), 16 deletions(-)
diff --git a/src/web/HTMLOperation.mjs b/src/web/HTMLOperation.mjs
index 30cfd1d9..725f0b5f 100755
--- a/src/web/HTMLOperation.mjs
+++ b/src/web/HTMLOperation.mjs
@@ -43,17 +43,23 @@ class HTMLOperation {
/**
* Renders the operation in HTML as a stub operation with no ingredients.
*
+ * @param {boolean} removeIcon - show icon for removing operation
+ * @param {string} elementId - element ID for aria usage
* @returns {string}
*/
- toStubHtml(removeIcon) {
+ toStubHtml(removeIcon = false, elementId = null) {
let html = "${titleFromWikiLink(this.infoURL)}` : "";
html += ` data-container='body' data-toggle='popover' data-placement='right'
data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover'
- data-boundary='viewport'`;
+ data-boundary='viewport' role='button'`;
}
html += ">" + this.name;
diff --git a/src/web/html/index.html b/src/web/html/index.html
index 2d4de4bd..335f8c44 100755
--- a/src/web/html/index.html
+++ b/src/web/html/index.html
@@ -173,7 +173,7 @@
Operations
-
+
diff --git a/src/web/waiters/OperationsWaiter.mjs b/src/web/waiters/OperationsWaiter.mjs
index 45a40c82..6c2de064 100755
--- a/src/web/waiters/OperationsWaiter.mjs
+++ b/src/web/waiters/OperationsWaiter.mjs
@@ -28,17 +28,16 @@ class OperationsWaiter {
this.removeIntent = false;
}
-
/**
* Handler for search events.
* Finds operations which match the given search term and displays them under the search box.
*
- * @param {event} e
+ * @param {KeyboardEvent | ClipboardEvent | Event} e
*/
searchOperations(e) {
let ops, selected;
- if (e.type === "search" || e.keyCode === 13) { // Search or Return
+ if ((e.type === "search" && e.target.value !== "") || e.keyCode === 13) { // Search (non-empty) or Return
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
@@ -49,27 +48,43 @@ class OperationsWaiter {
}
}
+ /**
+ * Sets up the operation element with the correct attributes when selected
+ * @param {HTMLElement} element
+ */
+ const _selectOperation = (element) => {
+ element.classList.add("selected-op");
+ element.scrollIntoView({block: "nearest"});
+ $(element).popover("show");
+ e.target.setAttribute("aria-activedescendant", element.id);
+ };
+
+ /**
+ * Sets up the operation element with the correct attributes when deselected
+ * @param {HTMLElement} element
+ */
+ const _deselectOperation = (element) => {
+ element.classList.remove("selected-op");
+ $(element).popover("hide");
+ };
+
if (e.keyCode === 40) { // Down
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
selected = this.getSelectedOp(ops);
- if (selected > -1) {
- ops[selected].classList.remove("selected-op");
- }
+ if (selected > -1) _deselectOperation(ops[selected]);
if (selected === ops.length-1) selected = -1;
- ops[selected+1].classList.add("selected-op");
+ _selectOperation(ops[selected+1]);
}
} else if (e.keyCode === 38) { // Up
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
selected = this.getSelectedOp(ops);
- if (selected > -1) {
- ops[selected].classList.remove("selected-op");
- }
+ if (selected > -1) _deselectOperation(ops[selected]);
if (selected === 0) selected = ops.length;
- ops[selected-1].classList.add("selected-op");
+ _selectOperation(ops[selected-1]);
}
} else {
const searchResultsEl = document.getElementById("search-results");
@@ -83,11 +98,13 @@ class OperationsWaiter {
searchResultsEl.removeChild(searchResultsEl.firstChild);
}
+ document.querySelector("#search").removeAttribute("aria-activedescendant");
+
$("#categories .show").collapse("hide");
if (str) {
const matchedOps = this.filterOperations(str, true);
const matchedOpsHtml = matchedOps
- .map(v => v.toStubHtml())
+ .map((operation, idx) => operation.toStubHtml(false, `search-result-${idx}`))
.join("");
searchResultsEl.innerHTML = matchedOpsHtml;
@@ -103,7 +120,7 @@ class OperationsWaiter {
* @param {string} searchStr
* @param {boolean} highlight - Whether or not to highlight the matching string in the operation
* name and description
- * @returns {string[]}
+ * @returns {HTMLOperation[]}
*/
filterOperations(inStr, highlight) {
const matchedOps = [];
From 97f35095caaf617241fae7e77149aa383f332c49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Heinrichs?=
Date: Sat, 25 Apr 2026 03:43:22 -0400
Subject: [PATCH 005/114] Feature md link blanks (#660)
Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> (added tests)
---
src/core/operations/RenderMarkdown.mjs | 39 +++++++-
tests/operations/index.mjs | 1 +
tests/operations/tests/RenderMarkdown.mjs | 110 ++++++++++++++++++++++
3 files changed, 146 insertions(+), 4 deletions(-)
create mode 100644 tests/operations/tests/RenderMarkdown.mjs
diff --git a/src/core/operations/RenderMarkdown.mjs b/src/core/operations/RenderMarkdown.mjs
index c656bf5b..20966cca 100644
--- a/src/core/operations/RenderMarkdown.mjs
+++ b/src/core/operations/RenderMarkdown.mjs
@@ -35,6 +35,11 @@ class RenderMarkdown extends Operation {
name: "Enable syntax highlighting",
type: "boolean",
value: true
+ },
+ {
+ name: "Open links in new tab.",
+ type: "boolean",
+ value: false
}
];
}
@@ -45,7 +50,7 @@ class RenderMarkdown extends Operation {
* @returns {html}
*/
run(input, args) {
- const [convertLinks, enableHighlighting] = args,
+ const [convertLinks, enableHighlighting, openLinksBlank] = args,
md = new MarkdownIt({
linkify: convertLinks,
html: false, // Explicitly disable HTML rendering
@@ -58,12 +63,38 @@ class RenderMarkdown extends Operation {
return "";
}
- }),
- rendered = md.render(input);
-
+ });
+ if (openLinksBlank) {
+ this.makeLinksOpenInNewTab(md);
+ }
+ const rendered = md.render(input);
return `${rendered}
`;
}
+ /**
+ * Adds target="_blank" to links.
+ * @param {MarkdownIt} md
+ */
+ makeLinksOpenInNewTab(md) {
+ // Adapted from: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
+ // Remember old renderer, if overridden, or proxy to default renderer
+ const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
+ return self.renderToken(tokens, idx, options);
+ };
+
+ // eslint-disable-next-line camelcase
+ md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
+ const token = tokens[idx];
+ if (token.attrIndex("target") >= 0) {
+ // Target attribute already set, do not replace.
+ return;
+ }
+ token.attrPush(["target", "_blank"]); // add new attribute
+
+ // pass token to default renderer.
+ return defaultRender(tokens, idx, options, env, self);
+ };
+ }
}
export default RenderMarkdown;
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index 420d0129..f40d840e 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -145,6 +145,7 @@ import "./tests/RAKE.mjs";
import "./tests/Regex.mjs";
import "./tests/Register.mjs";
import "./tests/RegularExpression.mjs";
+import "./tests/RenderMarkdown.mjs";
import "./tests/RisonEncodeDecode.mjs";
import "./tests/Rotate.mjs";
import "./tests/RSA.mjs";
diff --git a/tests/operations/tests/RenderMarkdown.mjs b/tests/operations/tests/RenderMarkdown.mjs
new file mode 100644
index 00000000..47781e5e
--- /dev/null
+++ b/tests/operations/tests/RenderMarkdown.mjs
@@ -0,0 +1,110 @@
+/**
+ * RenderMarkdown tests.
+ *
+ * @copyright Crown Copyright 2026
+ * @license Apache-2.0
+ */
+
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Render Markdown: Nothing",
+ input: "",
+ expectedOutput: '',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Render Markdown: Basic Text",
+ input: "Hello World!",
+ expectedOutput: '',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Render Markdown: Simple Markdown",
+ input: "# Hello World!",
+ expectedOutput: 'Hello World!
\n
',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Render Markdown: URL (not expanded)",
+ input: "https://gchq.github.io/CyberChef/",
+ expectedOutput: 'https://gchq.github.io/CyberChef/
\n
',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": [false, false, false]
+ }
+ ]
+ },
+ {
+ name: "Render Markdown: URL (expanded)",
+ input: "https://gchq.github.io/CyberChef/",
+ expectedOutput: '',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": [true, false, false]
+ }
+ ]
+ },
+ {
+ name: "Render Markdown: Link (not expanded)",
+ input: "[CyberChef](https://gchq.github.io/CyberChef/)",
+ expectedOutput: '',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": [false, false, false]
+ }
+ ]
+ },
+ {
+ name: "Render Markdown: Link (expanded)",
+ input: "[CyberChef](https://gchq.github.io/CyberChef/)",
+ expectedOutput: '',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": [true, false, false]
+ }
+ ]
+ },
+ {
+ name: "Render Markdown: Link (open in new window)",
+ input: "[CyberChef](https://gchq.github.io/CyberChef/)",
+ expectedOutput: '',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": [true, false, true]
+ }
+ ]
+ },
+ {
+ name: "Render Markdown: URL (open in new window)",
+ input: "https://gchq.github.io/CyberChef/",
+ expectedOutput: '',
+ recipeConfig: [
+ {
+ "op": "Render Markdown",
+ "args": [true, false, true]
+ }
+ ]
+ },
+]);
From ba09f5f1d587cec9cdec9f8a1ec3e69aa4e20bc9 Mon Sep 17 00:00:00 2001
From: j83305 <63656067+j83305@users.noreply.github.com>
Date: Sat, 25 Apr 2026 08:52:26 +0100
Subject: [PATCH 006/114] [#927] added parity bit operation (#1036)
---
src/core/config/Categories.json | 3 +-
src/core/lib/ParityBit.mjs | 50 +++++++++
src/core/operations/ParityBit.mjs | 128 +++++++++++++++++++++++
tests/operations/index.mjs | 1 +
tests/operations/tests/ParityBit.mjs | 147 +++++++++++++++++++++++++++
5 files changed, 328 insertions(+), 1 deletion(-)
create mode 100644 src/core/lib/ParityBit.mjs
create mode 100644 src/core/operations/ParityBit.mjs
create mode 100644 tests/operations/tests/ParityBit.mjs
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index a2bd2d08..ab1dafb0 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -465,7 +465,8 @@
"Luhn Checksum",
"CRC Checksum",
"TCP/IP Checksum",
- "XOR Checksum"
+ "XOR Checksum",
+ "Parity Bit"
]
},
{
diff --git a/src/core/lib/ParityBit.mjs b/src/core/lib/ParityBit.mjs
new file mode 100644
index 00000000..9675b46c
--- /dev/null
+++ b/src/core/lib/ParityBit.mjs
@@ -0,0 +1,50 @@
+/**
+ * Parity Bit functions.
+ *
+ * @author j83305 [awz22@protonmail.com]
+ * @copyright Crown Copyright 2020
+ * @license Apache-2.0
+ *
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * Function to take the user input and encode using the given arguments
+ * @param input string of binary
+ * @param args array
+ */
+export function calculateParityBit(input, args) {
+ let count1s = 0;
+ for (let i = 0; i < input.length; i++) {
+ const character = input.charAt(i);
+ if (character === "1") {
+ count1s++;
+ } else if (character !== args[3] && character !== "0" && character !== " ") {
+ throw new OperationError("unexpected character encountered: \"" + character + "\"");
+ }
+ }
+ let parityBit = "1";
+ const flipflop = args[0] === "Even Parity" ? 0 : 1;
+ if (count1s % 2 === flipflop) {
+ parityBit = "0";
+ }
+ if (args[1] === "End") {
+ return input + parityBit;
+ } else {
+ return parityBit + input;
+ }
+}
+
+/**
+ * just removes the parity bit to return the original data
+ * @param input string of binary, encoded
+ * @param args array
+ */
+export function decodeParityBit(input, args) {
+ if (args[1] === "End") {
+ return input.slice(0, -1);
+ } else {
+ return input.slice(1);
+ }
+}
diff --git a/src/core/operations/ParityBit.mjs b/src/core/operations/ParityBit.mjs
new file mode 100644
index 00000000..c5ac1d1e
--- /dev/null
+++ b/src/core/operations/ParityBit.mjs
@@ -0,0 +1,128 @@
+/**
+ * @author j83305 [awz22@protonmail.com]
+ * @copyright Crown Copyright 2020
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import { calculateParityBit, decodeParityBit } from "../lib/ParityBit.mjs";
+
+/**
+ * Parity Bit operation
+ */
+class ParityBit extends Operation {
+
+ /**
+ * ParityBit constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Parity Bit";
+ this.module = "Default";
+ this.description = "A parity bit, or check bit, is the simplest form of error detection. It is a bit which is added to a string of bits and represents if the number of 1's in the binary string is an even number or odd number.
If a delimiter is specified, the parity bit calculation will be performed on each 'block' of the input data, where the blocks are created by slicing the input at each occurence of the delimiter character";
+ this.infoURL = "https://wikipedia.org/wiki/Parity_bit";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Mode",
+ type: "option",
+ value: [
+ "Even Parity",
+ "Odd Parity"
+ ]
+ },
+ {
+ name: "Postion",
+ type: "option",
+ value: [
+ "Start",
+ "End"
+ ]
+ },
+ {
+ name: "Encode or Decode",
+ type: "option",
+ value: [
+ "Encode",
+ "Decode"
+ ]
+ },
+ {
+ name: "Delimiter",
+ type: "shortString",
+ value: ""
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ if (input.length === 0) {
+ return input;
+ }
+ /**
+ * determines weather to use the encode or decode method based off args[2]
+ * @param input input to be encoded or decoded
+ * @param args array
+ */
+ const method = (input, args) => args[2] === "Encode" ? calculateParityBit(input, args) : decodeParityBit(input, args);
+ if (args[3].length > 0) {
+ const byteStrings = input.split(args[3]);
+ for (let byteStringsArrayIndex = 0; byteStringsArrayIndex < byteStrings.length; byteStringsArrayIndex++) {
+ byteStrings[byteStringsArrayIndex] = method(byteStrings[byteStringsArrayIndex], args);
+ }
+ return byteStrings.join(args[3]);
+ }
+ return method(input, args);
+ }
+
+ /**
+ * Highlight Parity Bit
+ *
+ * @param {Object[]} pos
+ * @param {number} pos[].start
+ * @param {number} pos[].end
+ * @param {Object[]} args
+ * @returns {Object[]} pos
+ */
+ highlight(pos, args) {
+ if (args[3].length === 0) {
+ if (args[1] === "Prepend") {
+ pos[0].start += 1;
+ pos[0].end += 1;
+ }
+ return pos;
+ }
+ // need to be able to read input to do the highlighting when there is a delimiter
+ }
+
+ /**
+ * Highlight Parity Bit in reverse
+ *
+ * @param {Object[]} pos
+ * @param {number} pos[].start
+ * @param {number} pos[].end
+ * @param {Object[]} args
+ * @returns {Object[]} pos
+ */
+ highlightReverse(pos, args) {
+ if (args[3].length === 0) {
+ if (args[1] === "Prepend") {
+ if (pos[0].start > 0) {
+ pos[0].start -= 1;
+ }
+ pos[0].end -= 1;
+ }
+ return pos;
+ }
+ }
+
+}
+
+export default ParityBit;
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index f40d840e..19637e34 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -135,6 +135,7 @@ import "./tests/ParseUDP.mjs";
import "./tests/PEMtoHex.mjs";
import "./tests/PGP.mjs";
import "./tests/PHP.mjs";
+import "./tests/ParityBit.mjs";
import "./tests/PHPSerialize.mjs";
import "./tests/PowerSet.mjs";
import "./tests/Protobuf.mjs";
diff --git a/tests/operations/tests/ParityBit.mjs b/tests/operations/tests/ParityBit.mjs
new file mode 100644
index 00000000..bc42b9f8
--- /dev/null
+++ b/tests/operations/tests/ParityBit.mjs
@@ -0,0 +1,147 @@
+/**
+ * Parity Bit tests
+ *
+ * @author j83305 [awz22@protonmail.com]
+ * @copyright Crown Copyright 2020
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Parity bit encode in even parity, 1 block of binary of arbitrary length, prepend, even number of 1s",
+ input: "01010101 10101010",
+ expectedOutput: "001010101 10101010",
+ recipeConfig: [
+ {
+ "op": "Parity Bit",
+ "args": [
+ "Even Parity",
+ "Start",
+ "Encode",
+ ""
+ ]
+ }
+ ]
+ },
+ {
+ name: "Parity bit encode in even parity, 1 block of binary of arbitrary length, prepend, odd number of 1s",
+ input: "01010101 10101011",
+ expectedOutput: "101010101 10101011",
+ recipeConfig: [
+ {
+ "op": "Parity Bit",
+ "args": [
+ "Even Parity",
+ "Start",
+ "Encode",
+ ""
+ ]
+ }
+ ]
+ },
+ {
+ name: "Parity bit encode in even parity, 1 block of binary of arbitrary length, append, odd number of 1s",
+ input: "01010101 10101011",
+ expectedOutput: "01010101 101010111",
+ recipeConfig: [
+ {
+ "op": "Parity Bit",
+ "args": [
+ "Even Parity",
+ "End",
+ "Encode",
+ ""
+ ]
+ }
+ ]
+ },
+ {
+ name: "Parity bit encode in odd parity, 1 block of binary of arbitrary length, prepend, even number of 1s",
+ input: "01010101 10101010",
+ expectedOutput: "101010101 10101010",
+ recipeConfig: [
+ {
+ "op": "Parity Bit",
+ "args": [
+ "Odd Parity",
+ "Start",
+ "Encode",
+ ""
+ ]
+ }
+ ]
+ },
+ {
+ name: "Parity bit encode in odd parity, 1 block of binary of arbitrary length, prepend, odd number of 1s",
+ input: "01010101 10101011",
+ expectedOutput: "001010101 10101011",
+ recipeConfig: [
+ {
+ "op": "Parity Bit",
+ "args": [
+ "Odd Parity",
+ "Start",
+ "Encode",
+ ""
+ ]
+ }
+ ]
+ },
+ {
+ name: "Parity bit encode in odd parity, 1 block of binary of arbitrary length, append, odd number of 1s",
+ input: "01010101 10101011",
+ expectedOutput: "01010101 101010110",
+ recipeConfig: [
+ {
+ "op": "Parity Bit",
+ "args": [
+ "Odd Parity",
+ "End",
+ "Encode",
+ ""
+ ]
+ }
+ ]
+ },
+ {
+ name: "Parity bit encode in even parity, binary for 'hello world!', prepend to each byte",
+ input: "hello world!",
+ expectedOutput: "101101000 001100101 001101100 001101100 001101111 100100000 001110111 001101111 001110010 001101100 101100100 000100001",
+ recipeConfig: [
+ {
+ "op": "To Binary",
+ "args": ["Space"]
+ },
+ {
+ "op": "Parity Bit",
+ "args": [
+ "Even Parity",
+ "Start",
+ "Encode",
+ " "
+ ]
+ }
+ ]
+ },
+ {
+ name: "Parity bit encode in odd parity, binary for 'hello world!', append to each byte",
+ input: "hello world!",
+ expectedOutput: "011010000 011001011 011011001 011011001 011011111 001000000 011101111 011011111 011100101 011011001 011001000 001000011",
+ recipeConfig: [
+ {
+ "op": "To Binary",
+ "args": ["Space"]
+ },
+ {
+ "op": "Parity Bit",
+ "args": [
+ "Odd Parity",
+ "End",
+ "Encode",
+ " "
+ ]
+ }
+ ]
+ },
+]);
From 534032f4fe842062c0a64117763552b43a23a612 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Mon, 27 Apr 2026 12:35:10 +0100
Subject: [PATCH 007/114] Fix, and link, Fernet tests (#2335)
---
tests/operations/index.mjs | 1 +
tests/operations/tests/Fernet.mjs | 21 ++++++++++++++++++---
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index 19637e34..bb81d32e 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -72,6 +72,7 @@ import "./tests/ExtractAudioMetadata.mjs";
import "./tests/ExtractEmailAddresses.mjs";
import "./tests/ExtractHashes.mjs";
import "./tests/ExtractIPAddresses.mjs";
+import "./tests/Fernet.mjs";
import "./tests/Float.mjs";
import "./tests/FileTree.mjs";
import "./tests/FletcherChecksum.mjs";
diff --git a/tests/operations/tests/Fernet.mjs b/tests/operations/tests/Fernet.mjs
index ee9ba2f1..7a17a675 100644
--- a/tests/operations/tests/Fernet.mjs
+++ b/tests/operations/tests/Fernet.mjs
@@ -5,7 +5,7 @@
* @copyright Karsten Silkenbäumer 2019
* @license Apache-2.0
*/
-import TestRegister from "../TestRegister";
+import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
@@ -47,7 +47,7 @@ TestRegister.addTests([
{
name: "Fernet Encrypt: no input",
input: "",
- expectedMatch: /^gAAAAABce-[\w-]+={0,2}$/,
+ expectedMatch: /^gAAA[\w-]+={0,2}$/,
recipeConfig: [
{
op: "Fernet Encrypt",
@@ -69,12 +69,27 @@ TestRegister.addTests([
{
name: "Fernet Encrypt: valid arguments",
input: "This is a secret message.\n",
- expectedMatch: /^gAAAAABce-[\w-]+={0,2}$/,
+ expectedMatch: /^gAAA[\w-]+={0,2}$/,
recipeConfig: [
{
op: "Fernet Encrypt",
args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="]
}
],
+ },
+ {
+ name: "Fernet Encrypt/Decrypt: round trip",
+ input: "This is a secret message.\n",
+ expectedOutput: "This is a secret message.\n",
+ recipeConfig: [
+ {
+ op: "Fernet Encrypt",
+ args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="]
+ },
+ {
+ op: "Fernet Decrypt",
+ args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="]
+ },
+ ],
}
]);
From 09bb086853c16190f5f8adcec74435e02b50c8ce Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Mon, 27 Apr 2026 12:44:08 +0100
Subject: [PATCH 008/114] Update CONTRIBUTING.md (#2333)
---
CONTRIBUTING.md | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index abb37d42..0cee9a7b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,10 +6,13 @@ There are lots of opportunities to contribute to CyberChef. If you want ideas, t
Before your contributions can be accepted, you must:
- - Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef)
+ - Fork the CyberChef repo
+ - Create a new branch within your fork for the changes
- Push your changes to your fork.
+ - Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef)
- Submit a pull request.
+Please note that we will ***reject*** pull requests from the master branch of your fork owing to the mess it makes of our own working repositories and the extra work entailed.
## Coding conventions
From 9088561acf75fce6961efda2ed276f3f15b9bf04 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Mon, 27 Apr 2026 13:17:39 +0100
Subject: [PATCH 009/114] Bump v10.24.0 (#2338)
---
CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f9d7969..0a8a0b79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,30 @@ All major and minor version changes will be documented in this file. Details of
## Details
+### [10.24.0] - 2026-04-27
+- Update CONTRIBUTING.md [@GCHQDeveloper581] | [#2333]
+- Fix, and link, Fernet tests [@GCHQDeveloper581] | [#2335]
+- [#927] added parity bit operation [@j83305] | [#1036]
+- Feature md link blanks [@BjoernAkAManf] [@GCHQDeveloper581] | [#660]
+- Accessibility - Add support for screenreaders in operations search [@mattnotmitt] | [#1862]
+- Added metadata extraction for UUID strings. [@ko80240] | [#2322]
+- chore (deps): bump the patch-updates group with 6 updates | [#2330]
+- chore (deps): bump @codemirror/search from 6.6.0 to 6.7.0 | [#2331]
+- (Feature) Improve CI [@GCHQDeveloper581] | [#2328]
+- Update dependabot.yml [@GCHQDeveloper581] | [#2326]
+- chore (deps): bump lodash, grunt-legacy-log and grunt-legacy-util | [#2327]
+- chore (deps): bump the patch-updates group with 6 updates [@GCHQDeveloper581] | [#2323]
+- chore (deps): bump autoprefixer from 10.4.27 to 10.5.0 | [#2324]
+- chore (deps): bump dompurify from 3.3.3 to 3.4.0 | [#2321]
+- chore (deps): bump follow-redirects from 1.15.11 to 1.16.0 | [#2320]
+- Regular Expression operation email address regex: Support IPv4 domains [@C85297] [@GCHQDeveloper581] | [#2167]
+- Rewriting fixCryptoApiImports and fixSnackbarMarkup to js to make it OS agnostic [@BigYellowHammer] | [#2298]
+- chore (deps): bump basic-ftp from 5.2.1 to 5.2.2 | [#2317]
+- chore (deps): bump axios from 1.13.6 to 1.15.0 | [#2316]
+- chore (deps): bump webpack from 5.105.4 to 5.106.0 | [#2315]
+- chore (deps): bump basic-ftp from 5.2.0 to 5.2.1 | [#2313]
+- Update vulnerable dependencies [@GCHQDeveloper581] | [#2311]
+
### [10.23.0] - 2026-04-06
- Properly escape HTML entities in sampleDelim to avoid XSS issue [@GCHQDeveloper581] | [#2307]
- chore (deps): bump lodash from 4.17.23 to 4.18.1 | [#2304]
@@ -596,6 +620,7 @@ All major and minor version changes will be documented in this file. Details of
## [4.0.0] - 2016-11-28
- Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306)
+[10.24.0]: https://github.com/gchq/CyberChef/releases/tag/v10.24.0
[10.23.0]: https://github.com/gchq/CyberChef/releases/tag/v10.23.0
[10.22.0]: https://github.com/gchq/CyberChef/releases/tag/v10.22.0
[10.21.0]: https://github.com/gchq/CyberChef/releases/tag/v10.21.0
@@ -872,6 +897,10 @@ All major and minor version changes will be documented in this file. Details of
[@aby-jo]: https://github.com/aby-jo
[@atsiv1]: https://github.com/atsiv1
[@fjh1997]: https://github.com/fjh1997
+[@j83305]: https://github.com/j83305
+[@BjoernAkAManf]: https://github.com/BjoernAkAManf
+[@ko80240]: https://github.com/ko80240
+[@BigYellowHammer]: https://github.com/BigYellowHammer
[8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7
@@ -1141,4 +1170,26 @@ All major and minor version changes will be documented in this file. Details of
[#2194]: https://github.com/gchq/CyberChef/pull/2194
[#2193]: https://github.com/gchq/CyberChef/pull/2193
[#2192]: https://github.com/gchq/CyberChef/pull/2192
+[#2333]: https://github.com/gchq/CyberChef/pull/2333
+[#2335]: https://github.com/gchq/CyberChef/pull/2335
+[#1036]: https://github.com/gchq/CyberChef/pull/1036
+[#660]: https://github.com/gchq/CyberChef/pull/660
+[#1862]: https://github.com/gchq/CyberChef/pull/1862
+[#2322]: https://github.com/gchq/CyberChef/pull/2322
+[#2330]: https://github.com/gchq/CyberChef/pull/2330
+[#2331]: https://github.com/gchq/CyberChef/pull/2331
+[#2328]: https://github.com/gchq/CyberChef/pull/2328
+[#2326]: https://github.com/gchq/CyberChef/pull/2326
+[#2327]: https://github.com/gchq/CyberChef/pull/2327
+[#2323]: https://github.com/gchq/CyberChef/pull/2323
+[#2324]: https://github.com/gchq/CyberChef/pull/2324
+[#2321]: https://github.com/gchq/CyberChef/pull/2321
+[#2320]: https://github.com/gchq/CyberChef/pull/2320
+[#2167]: https://github.com/gchq/CyberChef/pull/2167
+[#2298]: https://github.com/gchq/CyberChef/pull/2298
+[#2317]: https://github.com/gchq/CyberChef/pull/2317
+[#2316]: https://github.com/gchq/CyberChef/pull/2316
+[#2315]: https://github.com/gchq/CyberChef/pull/2315
+[#2313]: https://github.com/gchq/CyberChef/pull/2313
+[#2311]: https://github.com/gchq/CyberChef/pull/2311
diff --git a/package-lock.json b/package-lock.json
index 2ad8f7e4..66cec577 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cyberchef",
- "version": "10.23.0",
+ "version": "10.24.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cyberchef",
- "version": "10.23.0",
+ "version": "10.24.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
diff --git a/package.json b/package.json
index 2d7b8ae8..c6412cb7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cyberchef",
- "version": "10.23.0",
+ "version": "10.24.0",
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
"author": "n1474335 ",
"homepage": "https://gchq.github.io/CyberChef",
From 9e5f94f5dc19cbf211018a53fc17004e7f6fb9ad Mon Sep 17 00:00:00 2001
From: Henrik Solberg
Date: Mon, 27 Apr 2026 17:15:37 +0200
Subject: [PATCH 010/114] [Feature] Change to nginx-unprivileged image for
better kubernetes support (#1922)
Breaking change: Port number for Docker Image has changed from 80 to 8080
---
Dockerfile | 2 +-
README.md | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 856a16ce..ecb56814 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -27,7 +27,7 @@ RUN npm run build
#########################################
# Package static build files into nginx #
#########################################
-FROM nginx:stable-alpine AS cyberchef
+FROM nginxinc/nginx-unprivileged:stable-alpine AS cyberchef
LABEL maintainer="GCHQ "
diff --git a/README.md b/README.md
index cc18a83f..469f8f61 100755
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ docker build --tag cyberchef --ulimit nofile=10000 .
```
2. Run the docker container
```bash
-docker run -it -p 8080:80 cyberchef
+docker run -it -p 8080:8080 cyberchef
```
3. Navigate to `http://localhost:8080` in your browser
@@ -45,7 +45,7 @@ docker run -it -p 8080:80 cyberchef
If you prefer to skip the build process, you can use the pre-built image
```bash
-docker run -it -p 8080:80 ghcr.io/gchq/cyberchef:latest
+docker run -it -p 8080:8080 ghcr.io/gchq/cyberchef:latest
```
Just like before, navigate to `http://localhost:8080` in your browser.
From e0ba5d28128e1dfdbe43fbed4df572fed16149e5 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Mon, 27 Apr 2026 18:17:24 +0100
Subject: [PATCH 011/114] fix(node): enable asynchronous operation support in
Node.js API (#2342)
Authored-by: engin0223
Changes cherry-picked by GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Breaking change: Alters Node API - "bake" and "execute" are now declared async.
---
src/node/NodeRecipe.mjs | 15 ++--
src/node/api.mjs | 6 +-
tests/node/consumers/cjs-consumer.js | 4 +-
tests/node/consumers/esm-consumer.mjs | 4 +-
tests/node/tests/nodeApi.mjs | 113 +++++++++++++-------------
5 files changed, 70 insertions(+), 72 deletions(-)
diff --git a/src/node/NodeRecipe.mjs b/src/node/NodeRecipe.mjs
index bad8fc27..ade9ecb5 100644
--- a/src/node/NodeRecipe.mjs
+++ b/src/node/NodeRecipe.mjs
@@ -88,16 +88,17 @@ class NodeRecipe {
* @param {NodeDish} dish
* @returns {NodeDish}
*/
- execute(dish) {
- return this.opList.reduce((prev, curr) => {
- // CASE where opList item is op and args
+ async execute(dish) {
+ let prev = dish;
+ for (const curr of this.opList) {
if (Object.prototype.hasOwnProperty.call(curr, "op") &&
Object.prototype.hasOwnProperty.call(curr, "args")) {
- return curr.op(prev, curr.args);
+ prev = await curr.op(prev, curr.args);
+ } else {
+ prev = await curr(prev);
}
- // CASE opList item is just op.
- return curr(prev);
- }, dish);
+ }
+ return prev;
}
}
diff --git a/src/node/api.mjs b/src/node/api.mjs
index 88b3f834..a02af9d3 100644
--- a/src/node/api.mjs
+++ b/src/node/api.mjs
@@ -324,10 +324,10 @@ export function help(input) {
* @returns {NodeDish} of the result
* @throws {TypeError} if invalid recipe given.
*/
-export function bake(input, recipeConfig) {
- const recipe = new NodeRecipe(recipeConfig);
+export async function bake(input, recipeConfig) {
+ const recipe = new NodeRecipe(recipeConfig);
const dish = ensureIsDish(input);
- return recipe.execute(dish);
+ return await recipe.execute(dish);
}
diff --git a/tests/node/consumers/cjs-consumer.js b/tests/node/consumers/cjs-consumer.js
index 3a759481..f13d9c35 100644
--- a/tests/node/consumers/cjs-consumer.js
+++ b/tests/node/consumers/cjs-consumer.js
@@ -8,9 +8,9 @@
const assert = require("assert");
-require("cyberchef").then(chef => {
+require("cyberchef").then(async chef => {
- const d = chef.bake("Testing, 1 2 3", [
+ const d = await chef.bake("Testing, 1 2 3", [
chef.toHex,
chef.reverse,
{
diff --git a/tests/node/consumers/esm-consumer.mjs b/tests/node/consumers/esm-consumer.mjs
index 2919e533..3a2648e1 100644
--- a/tests/node/consumers/esm-consumer.mjs
+++ b/tests/node/consumers/esm-consumer.mjs
@@ -9,7 +9,7 @@ import assert from "assert";
import chef from "cyberchef";
import { bake, toHex, reverse, unique, multiply } from "cyberchef";
-const a = bake("Testing, 1 2 3", [
+const a = await bake("Testing, 1 2 3", [
toHex,
reverse,
{
@@ -28,7 +28,7 @@ const a = bake("Testing, 1 2 3", [
assert.equal(a.value, "630957449041920");
-const b = chef.bake("Testing, 1 2 3", [
+const b = await chef.bake("Testing, 1 2 3", [
chef.toHex,
chef.reverse,
{
diff --git a/tests/node/tests/nodeApi.mjs b/tests/node/tests/nodeApi.mjs
index 92d4d991..2510ef17 100644
--- a/tests/node/tests/nodeApi.mjs
+++ b/tests/node/tests/nodeApi.mjs
@@ -170,77 +170,77 @@ TestRegister.addApiTests([
assert(chef.bake);
}),
- it("chef.bake: should return NodeDish", () => {
- const result = chef.bake("input", "to base 64");
+ it("chef.bake: should return NodeDish", async () => {
+ const result = await chef.bake("input", "to base 64");
assert(result instanceof NodeDish);
}),
- it("chef.bake: should take an input and an op name and perform it", () => {
- const result = chef.bake("some input", "to base 32");
+ it("chef.bake: should take an input and an op name and perform it", async () => {
+ const result = await chef.bake("some input", "to base 32");
assert.strictEqual(result.toString(), "ONXW2ZJANFXHA5LU");
}),
- it("chef.bake: should complain if recipe isnt a valid object", () => {
- assert.throws(() => chef.bake("some input", 3264), {
+ it("chef.bake: should complain if recipe isnt a valid object", async () => {
+ await assert.rejects(() => chef.bake("some input", 3264), {
name: "TypeError",
message: "Recipe can only contain function names or functions"
});
}),
- it("chef.bake: Should complain if string op is invalid", () => {
- assert.throws(() => chef.bake("some input", "not a valid operation"), {
+ it("chef.bake: Should complain if string op is invalid", async () => {
+ await assert.rejects(() => chef.bake("some input", "not a valid operation"), {
name: "TypeError",
message: "Couldn't find an operation with name 'not a valid operation'."
});
}),
- it("chef.bake: Should take an input and an operation and perform it", () => {
- const result = chef.bake("https://google.com/search?q=help", chef.parseURI);
+ it("chef.bake: Should take an input and an operation and perform it", async () => {
+ const result = await chef.bake("https://google.com/search?q=help", chef.parseURI);
assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = help\n");
}),
- it("chef.bake: Should complain if an invalid operation is inputted", () => {
- assert.throws(() => chef.bake("https://google.com/search?q=help", () => {}), {
+ it("chef.bake: Should complain if an invalid operation is inputted", async () => {
+ await assert.rejects(() => chef.bake("https://google.com/search?q=help", () => {}), {
name: "TypeError",
message: "Inputted function not a Chef operation."
});
}),
- it("chef.bake: accepts an array of operation names and performs them all in order", () => {
- const result = chef.bake("https://google.com/search?q=that's a complicated question", ["URL encode", "URL decode", "Parse URI"]);
+ it("chef.bake: accepts an array of operation names and performs them all in order", async () => {
+ const result = await chef.bake("https://google.com/search?q=that's a complicated question", ["URL encode", "URL decode", "Parse URI"]);
assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n");
}),
- it("chef.bake: forgiving with operation names", () =>{
- const result = chef.bake("https://google.com/search?q=that's a complicated question", ["urlencode", "url decode", "parseURI"]);
+ it("chef.bake: forgiving with operation names", async () =>{
+ const result = await chef.bake("https://google.com/search?q=that's a complicated question", ["urlencode", "url decode", "parseURI"]);
assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n");
}),
- it("chef.bake: forgiving with operation names", () =>{
- const result = chef.bake("hello", ["to base 64"]);
+ it("chef.bake: forgiving with operation names", async () =>{
+ const result = await chef.bake("hello", ["to base 64"]);
assert.strictEqual(result.toString(), "aGVsbG8=");
}),
- it("chef.bake: if recipe is empty array, return input as dish", () => {
- const result = chef.bake("some input", []);
+ it("chef.bake: if recipe is empty array, return input as dish", async () => {
+ const result = await chef.bake("some input", []);
assert.strictEqual(result.toString(), "some input");
assert(result instanceof NodeDish, "Result is not instance of NodeDish");
}),
- it("chef.bake: accepts an array of operations as recipe", () => {
- const result = chef.bake("https://google.com/search?q=that's a complicated question", [chef.URLEncode, chef.URLDecode, chef.parseURI]);
+ it("chef.bake: accepts an array of operations as recipe", async () => {
+ const result = await chef.bake("https://google.com/search?q=that's a complicated question", [chef.URLEncode, chef.URLDecode, chef.parseURI]);
assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n");
}),
- it("should complain if an invalid operation is inputted as part of array", () => {
- assert.throws(() => chef.bake("something", [() => {}]), {
+ it("should complain if an invalid operation is inputted as part of array", async () => {
+ await assert.rejects(() => chef.bake("something", [() => {}]), {
name: "TypeError",
message: "Inputted function not a Chef operation."
});
}),
- it("chef.bake: should take single JSON object describing op and args OBJ", () => {
- const result = chef.bake("some input", {
+ it("chef.bake: should take single JSON object describing op and args OBJ", async () => {
+ const result = await chef.bake("some input", {
op: chef.toHex,
args: {
Delimiter: "Colon"
@@ -249,23 +249,23 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "73:6f:6d:65:20:69:6e:70:75:74");
}),
- it("chef.bake: should take single JSON object desribing op with optional args", () => {
- const result = chef.bake("some input", {
+ it("chef.bake: should take single JSON object desribing op with optional args", async () => {
+ const result = await chef.bake("some input", {
op: chef.toHex,
});
assert.strictEqual(result.toString(), "73 6f 6d 65 20 69 6e 70 75 74");
}),
- it("chef.bake: should take single JSON object describing op and args ARRAY", () => {
- const result = chef.bake("some input", {
+ it("chef.bake: should take single JSON object describing op and args ARRAY", async () => {
+ const result = await chef.bake("some input", {
op: chef.toHex,
args: ["Colon"]
});
assert.strictEqual(result.toString(), "73:6f:6d:65:20:69:6e:70:75:74");
}),
- it("chef.bake: should error if op in JSON is not chef op", () => {
- assert.throws(() => chef.bake("some input", {
+ it("chef.bake: should error if op in JSON is not chef op", async () => {
+ await assert.rejects(() => chef.bake("some input", {
op: () => {},
args: ["Colon"],
}), {
@@ -274,8 +274,8 @@ TestRegister.addApiTests([
});
}),
- it("chef.bake: should take multiple ops in JSON object form, some ops by string", () => {
- const result = chef.bake("some input", [
+ it("chef.bake: should take multiple ops in JSON object form, some ops by string", async () => {
+ const result = await chef.bake("some input", [
{
op: chef.toHex,
args: ["Colon"]
@@ -290,8 +290,8 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "67;63;72;66;146;72;66;144;72;66;65;72;62;60;72;66;71;72;66;145;72;67;60;72;67;65;72;67;64");
}),
- it("chef.bake: should take multiple ops in JSON object form, some without args", () => {
- const result = chef.bake("some input", [
+ it("chef.bake: should take multiple ops in JSON object form, some without args", async () => {
+ const result = await chef.bake("some input", [
{
op: chef.toHex,
},
@@ -305,8 +305,8 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "67;63;40;66;146;40;66;144;40;66;65;40;62;60;40;66;71;40;66;145;40;67;60;40;67;65;40;67;64");
}),
- it("chef.bake: should handle op with multiple args", () => {
- const result = chef.bake("some input", {
+ it("chef.bake: should handle op with multiple args", async () => {
+ const result = await chef.bake("some input", {
op: "to morse code",
args: {
formatOptions: "Dash/Dot",
@@ -317,13 +317,13 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "DotDotDot\\DashDashDash\\DashDash\\Dot,DotDot\\DashDot\\DotDashDashDot\\DotDotDash\\Dash");
}),
- it("chef.bake: should take compact JSON format from Chef Website as recipe", () => {
- const result = chef.bake("some input", [{"op": "To Morse Code", "args": ["Dash/Dot", "Backslash", "Comma"]}, {"op": "Hex to PEM", "args": ["SOMETHING"]}, {"op": "To Snake case", "args": [false]}]);
+ it("chef.bake: should take compact JSON format from Chef Website as recipe", async () => {
+ const result = await chef.bake("some input", [{"op": "To Morse Code", "args": ["Dash/Dot", "Backslash", "Comma"]}, {"op": "Hex to PEM", "args": ["SOMETHING"]}, {"op": "To Snake case", "args": [false]}]);
assert.strictEqual(result.toString(), "begin_something_anananaaaaak_da_aaak_da_aaaaananaaaaaaan_da_aaaaaaanan_da_aaak_end_something");
}),
- it("chef.bake: should accept Clean JSON format from Chef website as recipe", () => {
- const result = chef.bake("some input", [
+ it("chef.bake: should accept Clean JSON format from Chef website as recipe", async () => {
+ const result = await chef.bake("some input", [
{ "op": "To Morse Code",
"args": ["Dash/Dot", "Backslash", "Comma"] },
{ "op": "Hex to PEM",
@@ -334,8 +334,8 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "begin_something_anananaaaaak_da_aaak_da_aaaaananaaaaaaan_da_aaaaaaanan_da_aaak_end_something");
}),
- it("chef.bake: should accept Clean JSON format from Chef website - args optional", () => {
- const result = chef.bake("some input", [
+ it("chef.bake: should accept Clean JSON format from Chef website - args optional", async () => {
+ const result = await chef.bake("some input", [
{ "op": "To Morse Code" },
{ "op": "Hex to PEM",
"args": ["SOMETHING"] },
@@ -345,31 +345,28 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "begin_something_aaaaaaaaaaaaaa_end_something");
}),
- it("chef.bake: should accept operation names from Chef Website which contain forward slash", () => {
- const result = chef.bake("I'll have the test salmon", [
+ it("chef.bake: should accept operation names from Chef Website which contain forward slash", async () => {
+ const result = await chef.bake("I'll have the test salmon", [
{ "op": "Find / Replace",
"args": [{ "option": "Regex", "string": "test" }, "good", true, false, true, false]}
]);
assert.strictEqual(result.toString(), "I'll have the good salmon");
}),
- it("chef.bake: should accept operation names from Chef Website which contain a hyphen", () => {
- const result = chef.bake("I'll have the test salmon", [
+ it("chef.bake: should accept operation names from Chef Website which contain a hyphen", async () => {
+ const result = await chef.bake("I'll have the test salmon", [
{ "op": "Adler-32 Checksum",
"args": [] }
]);
assert.strictEqual(result.toString(), "6e4208f8");
}),
- it("chef.bake: should accept operation names from Chef Website which contain a period", () => {
- const result = chef.bake("30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f", [
+ it("chef.bake: should accept operation names from Chef Website which contain a period", async () => {
+ const result = await chef.bake("30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f", [
{ "op": "Parse ASN.1 hex string",
"args": [0, 32] }
]);
- assert.strictEqual(result.toString(), `SEQUENCE
- INTEGER 05
- IA5String 'Anybody there?'
-`);
+ assert.strictEqual(result.toString(), `SEQUENCE\n INTEGER 05\n IA5String 'Anybody there?'\n`);
}),
it("Excluded operations: throw a sensible error when you try and call one", () => {
@@ -381,16 +378,16 @@ TestRegister.addApiTests([
}
}),
- it("chef.bake: cannot accept flowControl operations in recipe", () => {
- assert.throws(() => chef.bake("some input", "magic"), {
+ it("chef.bake: cannot accept flowControl operations in recipe", async () => {
+ await assert.rejects(() => chef.bake("some input", "magic"), {
name: "TypeError",
message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API"
});
- assert.throws(() => chef.bake("some input", magic), {
+ await assert.rejects(() => chef.bake("some input", magic), {
name: "TypeError",
message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API"
});
- assert.throws(() => chef.bake("some input", ["to base 64", "magic"]), {
+ await assert.rejects(() => chef.bake("some input", ["to base 64", "magic"]), {
name: "TypeError",
message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API"
});
From 864afa85aaf736514de79dcd751bfac5b8ec05f0 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Tue, 28 Apr 2026 07:18:44 +0100
Subject: [PATCH 012/114] Make compatible with node >=22 (#2273)
Breaking change: uplifts the minimum version of node required to v20
---
.devcontainer/devcontainer.json | 2 +-
.github/workflows/master.yml | 2 +-
.github/workflows/pull_requests.yml | 2 +-
.github/workflows/releases.yml | 2 +-
.nvmrc | 2 +-
Dockerfile | 2 +-
README.md | 2 +-
babel.config.js | 5 ++++-
package.json | 2 +-
src/core/ChefWorker.js | 2 +-
src/core/Recipe.mjs | 2 +-
src/core/lib/Magic.mjs | 2 +-
src/node/api.mjs | 2 +-
src/web/index.js | 4 ++--
src/web/static/sitemap.mjs | 2 +-
tests/node/tests/Categories.mjs | 4 ++--
16 files changed, 21 insertions(+), 18 deletions(-)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 92ebd43c..9b493bb6 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -3,7 +3,7 @@
{
"name": "CyberChef",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
- "image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bookworm",
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:22-trixie",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 62f19745..78ec1f41 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -21,7 +21,7 @@ jobs:
- name: Set node version
uses: actions/setup-node@v6
with:
- node-version: 18
+ node-version: '20.x'
registry-url: "https://registry.npmjs.org"
- name: Install
diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml
index efa6f6c4..7c731176 100644
--- a/.github/workflows/pull_requests.yml
+++ b/.github/workflows/pull_requests.yml
@@ -17,7 +17,7 @@ jobs:
- name: Set node version
uses: actions/setup-node@v6
with:
- node-version: 18
+ node-version: '20.x'
registry-url: "https://registry.npmjs.org"
- name: Install
diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
index 67e01d02..8230dbfd 100644
--- a/.github/workflows/releases.yml
+++ b/.github/workflows/releases.yml
@@ -27,7 +27,7 @@ jobs:
- name: Set node version
uses: actions/setup-node@v6
with:
- node-version: 18
+ node-version: '20.x'
registry-url: "https://registry.npmjs.org"
- name: Install
diff --git a/.nvmrc b/.nvmrc
index 3c032078..2bd5a0a9 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-18
+22
diff --git a/Dockerfile b/Dockerfile
index ecb56814..b0a1024d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@
# Modifier --platform=$BUILDPLATFORM limits the platform to "BUILDPLATFORM" during buildx multi-platform builds
# This is because npm "chromedriver" package is not compatiable with all platforms
# For more info see: https://docs.docker.com/build/building/multi-platform/#cross-compilation
-FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
+FROM --platform=$BUILDPLATFORM node:22-alpine AS builder
WORKDIR /app
diff --git a/README.md b/README.md
index 469f8f61..c1c90f2e 100755
--- a/README.md
+++ b/README.md
@@ -120,7 +120,7 @@ CyberChef is built to support
## Node.js support
-CyberChef is built to fully support Node.js `v16`. For more information, see the ["Node API" wiki page](https://github.com/gchq/CyberChef/wiki/Node-API)
+CyberChef is built to fully support Node.js `v22`. For more information, see the ["Node API" wiki page](https://github.com/gchq/CyberChef/wiki/Node-API)
## Contributing
diff --git a/babel.config.js b/babel.config.js
index 178271fb..b9efd549 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -16,6 +16,9 @@ module.exports = function(api) {
"regenerator": true
}
]
- ]
+ ],
+ "generatorOpts": {
+ "importAttributesKeyword": "with"
+ }
};
};
diff --git a/package.json b/package.json
index c6412cb7..a6dd4979 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
"browserslist": [
"Chrome >= 50",
"Firefox >= 38",
- "node >= 16"
+ "node >= 22"
],
"devDependencies": {
"@babel/eslint-parser": "^7.28.6",
diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js
index a43993f9..43f595f3 100644
--- a/src/core/ChefWorker.js
+++ b/src/core/ChefWorker.js
@@ -7,7 +7,7 @@
*/
import Chef from "./Chef.mjs";
-import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
+import OperationConfig from "./config/OperationConfig.json" with { type: "json" };
import OpModules from "./config/modules/OpModules.mjs";
import loglevelMessagePrefix from "loglevel-message-prefix";
diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs
index b4a10e03..84c91d61 100755
--- a/src/core/Recipe.mjs
+++ b/src/core/Recipe.mjs
@@ -4,7 +4,7 @@
* @license Apache-2.0
*/
-import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
+import OperationConfig from "./config/OperationConfig.json" with { type: "json" };
import OperationError from "./errors/OperationError.mjs";
import Operation from "./Operation.mjs";
import DishError from "./errors/DishError.mjs";
diff --git a/src/core/lib/Magic.mjs b/src/core/lib/Magic.mjs
index 14111ec7..ad407de6 100644
--- a/src/core/lib/Magic.mjs
+++ b/src/core/lib/Magic.mjs
@@ -1,4 +1,4 @@
-import OperationConfig from "../config/OperationConfig.json" assert {type: "json"};
+import OperationConfig from "../config/OperationConfig.json" with { type: "json" };
import Utils, { isWorkerEnvironment } from "../Utils.mjs";
import Recipe from "../Recipe.mjs";
import Dish from "../Dish.mjs";
diff --git a/src/node/api.mjs b/src/node/api.mjs
index a02af9d3..8002a8ac 100644
--- a/src/node/api.mjs
+++ b/src/node/api.mjs
@@ -10,7 +10,7 @@
import NodeDish from "./NodeDish.mjs";
import NodeRecipe from "./NodeRecipe.mjs";
-import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"};
+import OperationConfig from "../core/config/OperationConfig.json" with { type: "json" };
import { sanitise, removeSubheadingsFromArray, sentenceToCamelCase } from "./apiUtils.mjs";
import ExcludedOperationError from "../core/errors/ExcludedOperationError.mjs";
diff --git a/src/web/index.js b/src/web/index.js
index 90142b34..110f0d2b 100755
--- a/src/web/index.js
+++ b/src/web/index.js
@@ -17,8 +17,8 @@ import * as CanvasComponents from "../core/lib/CanvasComponents.mjs";
// CyberChef
import App from "./App.mjs";
-import Categories from "../core/config/Categories.json" assert {type: "json"};
-import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"};
+import Categories from "../core/config/Categories.json" with { type: "json" };
+import OperationConfig from "../core/config/OperationConfig.json" with { type: "json" };
/**
diff --git a/src/web/static/sitemap.mjs b/src/web/static/sitemap.mjs
index 4f8101d4..f373a277 100644
--- a/src/web/static/sitemap.mjs
+++ b/src/web/static/sitemap.mjs
@@ -1,5 +1,5 @@
import sm from "sitemap";
-import OperationConfig from "../../core/config/OperationConfig.json" assert { type: "json" };
+import OperationConfig from "../../core/config/OperationConfig.json" with { type: "json" };
/**
* Generates an XML sitemap for all CyberChef operations and a number of recipes.
diff --git a/tests/node/tests/Categories.mjs b/tests/node/tests/Categories.mjs
index e6f8bd72..070d78d7 100644
--- a/tests/node/tests/Categories.mjs
+++ b/tests/node/tests/Categories.mjs
@@ -1,6 +1,6 @@
import TestRegister from "../../lib/TestRegister.mjs";
-import Categories from "../../../src/core/config/Categories.json" assert {type: "json"};
-import OperationConfig from "../../../src/core/config/OperationConfig.json" assert {type: "json"};
+import Categories from "../../../src/core/config/Categories.json" with { type: "json" };
+import OperationConfig from "../../../src/core/config/OperationConfig.json" with { type: "json" };
import it from "../assertionHandler.mjs";
import assert from "assert";
From 9641ae07f92e9af50f10e978385465b2f4a36c4d Mon Sep 17 00:00:00 2001
From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com>
Date: Tue, 28 Apr 2026 10:02:00 +0100
Subject: [PATCH 013/114] Fix XSS in Show Base64 offsets (#2346)
---
src/core/operations/ShowBase64Offsets.mjs | 56 +++++++++++------------
tests/operations/tests/Base64.mjs | 11 +++++
2 files changed, 39 insertions(+), 28 deletions(-)
diff --git a/src/core/operations/ShowBase64Offsets.mjs b/src/core/operations/ShowBase64Offsets.mjs
index 37d8a6ce..a375d7db 100644
--- a/src/core/operations/ShowBase64Offsets.mjs
+++ b/src/core/operations/ShowBase64Offsets.mjs
@@ -77,84 +77,84 @@ class ShowBase64Offsets extends Operation {
staticSection = offset0.slice(0, -3);
offset0 = "" +
- staticSection + "" +
- "" + offset0.substr(offset0.length - 3, 1) + "" +
- "" + offset0.substr(offset0.length - 2) + "";
+ Utils.escapeHtml(staticSection) + "" +
+ "" + Utils.escapeHtml(offset0.substr(offset0.length - 3, 1)) + "" +
+ "" + Utils.escapeHtml(offset0.substr(offset0.length - 2)) + "";
} else if (len0 % 4 === 3) {
staticSection = offset0.slice(0, -2);
offset0 = "" +
- staticSection + "" +
- "" + offset0.substr(offset0.length - 2, 1) + "" +
- "" + offset0.substr(offset0.length - 1) + "";
+ Utils.escapeHtml(staticSection) + "" +
+ "" + Utils.escapeHtml(offset0.substr(offset0.length - 2, 1)) + "" +
+ "" + Utils.escapeHtml(offset0.substr(offset0.length - 1)) + "";
} else {
staticSection = offset0;
offset0 = "" +
- staticSection + "";
+ Utils.escapeHtml(staticSection) + "";
}
if (!showVariable) {
- offset0 = staticSection;
+ offset0 = Utils.escapeHtml(staticSection);
}
// Highlight offset 1
- padding = "" + offset1.substr(0, 1) + "" +
- "" + offset1.substr(1, 1) + "";
+ padding = "" + Utils.escapeHtml(offset1.substr(0, 1)) + "" +
+ "" + Utils.escapeHtml(offset1.substr(1, 1)) + "";
offset1 = offset1.substr(2);
if (len1 % 4 === 2) {
staticSection = offset1.slice(0, -3);
offset1 = padding + "" +
- staticSection + "" +
- "" + offset1.substr(offset1.length - 3, 1) + "" +
- "" + offset1.substr(offset1.length - 2) + "";
+ Utils.escapeHtml(staticSection) + "" +
+ "" + Utils.escapeHtml(offset1.substr(offset1.length - 3, 1)) + "" +
+ "" + Utils.escapeHtml(offset1.substr(offset1.length - 2)) + "";
} else if (len1 % 4 === 3) {
staticSection = offset1.slice(0, -2);
offset1 = padding + "" +
- staticSection + "" +
- "" + offset1.substr(offset1.length - 2, 1) + "" +
- "" + offset1.substr(offset1.length - 1) + "";
+ Utils.escapeHtml(staticSection) + "" +
+ "" + Utils.escapeHtml(offset1.substr(offset1.length - 2, 1)) + "" +
+ "" + Utils.escapeHtml(offset1.substr(offset1.length - 1)) + "";
} else {
staticSection = offset1;
offset1 = padding + "" +
- staticSection + "";
+ Utils.escapeHtml(staticSection) + "";
}
if (!showVariable) {
- offset1 = staticSection;
+ offset1 = Utils.escapeHtml(staticSection);
}
// Highlight offset 2
- padding = "" + offset2.substr(0, 2) + "" +
- "" + offset2.substr(2, 1) + "";
+ padding = "" + Utils.escapeHtml(offset2.substr(0, 2)) + "" +
+ "" + Utils.escapeHtml(offset2.substr(2, 1)) + "";
offset2 = offset2.substr(3);
if (len2 % 4 === 2) {
staticSection = offset2.slice(0, -3);
offset2 = padding + "" +
- staticSection + "" +
- "" + offset2.substr(offset2.length - 3, 1) + "" +
- "" + offset2.substr(offset2.length - 2) + "";
+ Utils.escapeHtml(staticSection) + "" +
+ "" + Utils.escapeHtml(offset2.substr(offset2.length - 3, 1)) + "" +
+ "" + Utils.escapeHtml(offset2.substr(offset2.length - 2)) + "";
} else if (len2 % 4 === 3) {
staticSection = offset2.slice(0, -2);
offset2 = padding + "" +
- staticSection + "" +
- "" + offset2.substr(offset2.length - 2, 1) + "" +
- "" + offset2.substr(offset2.length - 1) + "";
+ Utils.escapeHtml(staticSection) + "" +
+ "" + Utils.escapeHtml(offset2.substr(offset2.length - 2, 1)) + "" +
+ "" + Utils.escapeHtml(offset2.substr(offset2.length - 1)) + "";
} else {
staticSection = offset2;
offset2 = padding + "" +
- staticSection + "";
+ Utils.escapeHtml(staticSection) + "";
}
if (!showVariable) {
- offset2 = staticSection;
+ offset2 = Utils.escapeHtml(staticSection);
}
return (showVariable ? "Characters highlighted in green could change if the input is surrounded by more data." +
diff --git a/tests/operations/tests/Base64.mjs b/tests/operations/tests/Base64.mjs
index 6e6fa703..5dca403a 100644
--- a/tests/operations/tests/Base64.mjs
+++ b/tests/operations/tests/Base64.mjs
@@ -116,4 +116,15 @@ TestRegister.addTests([
},
],
},
+ {
+ name: "Show Base64 offsets: escapes static output",
+ input: "\x00\x10\x83\x10\x51\x87",
+ expectedOutput: "<script>\n<AQmsBRk66\n<ia1AEIM6",
+ recipeConfig: [
+ {
+ op: "Show Base64 offsets",
+ args: ["
Date: Mon, 18 May 2026 15:51:56 +0100
Subject: [PATCH 042/114] Bump nginxinc/nginx-unprivileged from `808f784` to
`b9f7ba1` (#2389)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dockerfile b/Dockerfile
index c8d40424..873ca32b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -27,7 +27,7 @@ RUN npm run build
#########################################
# Package static build files into nginx #
#########################################
-FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:808f7846d21a9c94cf53833e8807a00a33fd0b65cc47fb05b79efe366c2d201f AS cyberchef
+FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:b9f7ba14f1f7bd3d40d7753584048f92c3aef9ccf5fab14efe4451a8d4c04d63 AS cyberchef
LABEL maintainer="GCHQ "
From 2cf778c25390d1015a9de5abd2f6a33875b1008b Mon Sep 17 00:00:00 2001
From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com>
Date: Wed, 20 May 2026 15:32:46 +0100
Subject: [PATCH 043/114] Include git ref in website download zip name (#2339)
---
Gruntfile.js | 12 ++++++++----
src/web/html/index.html | 6 +++---
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/Gruntfile.js b/Gruntfile.js
index 6f87b2a6..475701b8 100755
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -89,6 +89,8 @@ module.exports = function (grunt) {
const compileYear = grunt.template.today("UTC:yyyy"),
compileTime = grunt.template.today("UTC:dd/mm/yyyy HH:MM:ss") + " UTC",
pkg = grunt.file.readJSON("package.json"),
+ version = process.env.GITHUB_SHA || `v${pkg.version}`,
+ downloadZipFilename = `CyberChef_${version}.zip`,
webpackConfig = require("./webpack.config.js"),
BUILD_CONSTANTS = {
COMPILE_YEAR: JSON.stringify(compileYear),
@@ -129,7 +131,9 @@ module.exports = function (grunt) {
chunks: ["main"],
compileYear: compileYear,
compileTime: compileTime,
- version: pkg.version,
+ version: version,
+ latestReleaseVersion: pkg.version,
+ downloadZipFilename: downloadZipFilename,
minify: {
removeComments: true,
collapseWhitespace: true,
@@ -245,7 +249,7 @@ module.exports = function (grunt) {
"!build/prod/index.html",
"!build/prod/BundleAnalyzerReport.html",
],
- dest: `build/prod/CyberChef_v${pkg.version}.zip`
+ dest: `build/prod/${downloadZipFilename}`
}
},
connect: {
@@ -333,12 +337,12 @@ module.exports = function (grunt) {
switch (process.platform) {
case "darwin":
return chainCommands([
- `shasum -a 256 build/prod/CyberChef_v${pkg.version}.zip | awk '{print $1;}' > build/prod/sha256digest.txt`,
+ `shasum -a 256 build/prod/${downloadZipFilename} | awk '{print $1;}' > build/prod/sha256digest.txt`,
`sed -i '' -e "s/DOWNLOAD_HASH_PLACEHOLDER/$(cat build/prod/sha256digest.txt)/" build/prod/index.html`
]);
default:
return chainCommands([
- `sha256sum build/prod/CyberChef_v${pkg.version}.zip | awk '{print $1;}' > build/prod/sha256digest.txt`,
+ `sha256sum build/prod/${downloadZipFilename} | awk '{print $1;}' > build/prod/sha256digest.txt`,
`sed -i -e "s/DOWNLOAD_HASH_PLACEHOLDER/$(cat build/prod/sha256digest.txt)/" build/prod/index.html`
]);
}
diff --git a/src/web/html/index.html b/src/web/html/index.html
index 335f8c44..6ce8dd9a 100755
--- a/src/web/html/index.html
+++ b/src/web/html/index.html
@@ -868,15 +868,15 @@
Be aware that the standalone version will never update itself, meaning it will not receive bug fixes or new features until you re-download newer versions manually.
- CyberChef v<%= htmlWebpackPlugin.options.version %>
+ CyberChef <%= htmlWebpackPlugin.options.version %>
- Build time: <%= htmlWebpackPlugin.options.compileTime %>
- - The changelog for this version can be viewed here
+ - The changelog for this version can be viewed here
- © Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %>
- Released under the Apache Licence, Version 2.0
- SHA256 hash: DOWNLOAD_HASH_PLACEHOLDER
- Download ZIP file
+ Download ZIP file