Date: Thu, 19 Mar 2026 12:14:28 +0000
Subject: [PATCH 14/60] Add Extract Audio Metadata operation (#2170)
Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> (minor tweak to wikipedia url)
---
src/core/config/Categories.json | 7 +-
src/core/lib/AudioBytes.mjs | 103 +++
src/core/lib/AudioMetaSchema.mjs | 82 +++
src/core/lib/AudioParsers.mjs | 630 ++++++++++++++++++
src/core/operations/ExtractAudioMetadata.mjs | 175 +++++
tests/operations/index.mjs | 1 +
.../operations/tests/ExtractAudioMetadata.mjs | 287 ++++++++
tests/samples/Audio.mjs | 73 ++
8 files changed, 1356 insertions(+), 2 deletions(-)
create mode 100644 src/core/lib/AudioBytes.mjs
create mode 100644 src/core/lib/AudioMetaSchema.mjs
create mode 100644 src/core/lib/AudioParsers.mjs
create mode 100644 src/core/operations/ExtractAudioMetadata.mjs
create mode 100644 tests/operations/tests/ExtractAudioMetadata.mjs
create mode 100644 tests/samples/Audio.mjs
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 8b62046e..88cb6dc1 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -385,6 +385,7 @@
"CSS selector",
"Extract EXIF",
"Extract ID3",
+ "Extract Audio Metadata",
"Extract Files",
"RAKE",
"Template"
@@ -514,7 +515,8 @@
"View Bit Plane",
"Randomize Colour Palette",
"Extract LSB",
- "ELF Info"
+ "ELF Info",
+ "Extract Audio Metadata"
]
},
{
@@ -547,7 +549,8 @@
"Hex Density chart",
"Scatter chart",
"Series chart",
- "Heatmap chart"
+ "Heatmap chart",
+ "Extract Audio Metadata"
]
},
{
diff --git a/src/core/lib/AudioBytes.mjs b/src/core/lib/AudioBytes.mjs
new file mode 100644
index 00000000..9a433fcd
--- /dev/null
+++ b/src/core/lib/AudioBytes.mjs
@@ -0,0 +1,103 @@
+/**
+ * Byte-reading and text-decoding utilities for audio metadata parsing.
+ *
+ * @author d0s1nt [d0s1nt@cyberchefaudio]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+/** @returns {string} 4-byte ASCII at offset, or "" if out of bounds. */
+export function ascii4(b, off) {
+ if (off + 4 > b.length) return "";
+ return String.fromCharCode(b[off], b[off + 1], b[off + 2], b[off + 3]);
+}
+
+/** @returns {number} Byte offset of ASCII needle `s`, or -1. */
+export function indexOfAscii(b, s, start, end) {
+ const limit = Math.max(0, Math.min(end, b.length) - s.length);
+ for (let i = start; i <= limit; i++) {
+ let ok = true;
+ for (let j = 0; j < s.length; j++) {
+ if (b[i + j] !== s.charCodeAt(j)) {
+ ok = false;
+ break;
+ }
+ }
+ if (ok) return i;
+ }
+ return -1;
+}
+
+/** @returns {number} Unsigned 32-bit big-endian read. */
+export function u32be(bytes, off) {
+ return ((bytes[off] << 24) >>> 0) | (bytes[off + 1] << 16) | (bytes[off + 2] << 8) | bytes[off + 3];
+}
+
+/** @returns {number} Unsigned 32-bit little-endian read. */
+export function u32le(bytes, off) {
+ return (bytes[off] | (bytes[off + 1] << 8) | (bytes[off + 2] << 16) | (bytes[off + 3] << 24)) >>> 0;
+}
+
+/** @returns {number} Unsigned 16-bit little-endian read. */
+export function u16le(bytes, off) {
+ return bytes[off] | (bytes[off + 1] << 8);
+}
+
+/** @returns {BigInt} Unsigned 64-bit little-endian read. */
+export function u64le(bytes, off) {
+ return BigInt(u32le(bytes, off)) | (BigInt(u32le(bytes, off + 4)) << 32n);
+}
+
+/** @returns {number} Decoded ID3v2 synchsafe integer from four 7-bit bytes. */
+export function synchsafeToInt(b0, b1, b2, b3) {
+ return ((b0 & 0x7f) << 21) | ((b1 & 0x7f) << 14) | ((b2 & 0x7f) << 7) | (b3 & 0x7f);
+}
+
+/** @returns {string} Decoded UTF-16LE byte range, nulls stripped. */
+export function decodeUtf16LE(b, off, len) {
+ if (len <= 0 || off + len > b.length) return "";
+ try {
+ return new TextDecoder("utf-16le").decode(b.slice(off, off + len)).replace(/\u0000/g, "").trim();
+ } catch {
+ return "";
+ }
+}
+
+/** @returns {{valueBytes: Uint8Array, next: number}} Bytes until null terminator, UTF-16 aware. */
+export function readNullTerminated(bytes, start, encoding) {
+ const isUtf16 = encoding === 1 || encoding === 2;
+ if (!isUtf16) {
+ let i = start;
+ while (i < bytes.length && bytes[i] !== 0x00) i++;
+ return { valueBytes: bytes.slice(start, i), next: i + 1 };
+ }
+ let i = start;
+ while (i + 1 < bytes.length && !(bytes[i] === 0x00 && bytes[i + 1] === 0x00)) i += 2;
+ return { valueBytes: bytes.slice(start, i), next: i + 2 };
+}
+
+const ID3_ENCODINGS = ["iso-8859-1", "utf-16", "utf-16be", "utf-8"];
+
+/** @returns {string} Text decoded using ID3v2 encoding byte (0=latin1, 1=utf16, 2=utf16be, 3=utf8). */
+export function decodeText(bytes, encoding) {
+ if (!bytes || bytes.length === 0) return "";
+ try {
+ return new TextDecoder(ID3_ENCODINGS[encoding] || "utf-16").decode(bytes);
+ } catch {
+ return safeUtf8(bytes);
+ }
+}
+
+/** @returns {string} UTF-8 decode with replacement (never throws). */
+export function safeUtf8(bytes) {
+ try {
+ return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
+ } catch {
+ return "";
+ }
+}
+
+/** @returns {string} ISO-8859-1 decode, nulls stripped, trimmed. */
+export function decodeLatin1Trim(bytes) {
+ return decodeText(bytes, 0).replace(/\u0000/g, "").trim();
+}
diff --git a/src/core/lib/AudioMetaSchema.mjs b/src/core/lib/AudioMetaSchema.mjs
new file mode 100644
index 00000000..c46445be
--- /dev/null
+++ b/src/core/lib/AudioMetaSchema.mjs
@@ -0,0 +1,82 @@
+/**
+ * Report skeleton and container detection for audio metadata extraction.
+ *
+ * @author d0s1nt [d0s1nt@cyberchefaudio]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+/* eslint-disable camelcase */
+
+import { ascii4, indexOfAscii } from "./AudioBytes.mjs";
+
+/** Builds the empty report skeleton ready for a format parser to populate. */
+export function makeEmptyReport(filename, byteLength, container) {
+ return {
+ schema_version: "audio-meta-1.0",
+ artifact: {
+ filename,
+ byte_length: byteLength,
+ container: { type: container.type, brand: container.brand || null, mime: container.mime || null },
+ },
+ detections: { metadata_systems: [], provenance_systems: [] },
+ tags: {
+ common: {
+ title: null, artist: null, album: null, date: null, track: null,
+ genre: null, comment: null, composer: null, copyright: null, language: null,
+ },
+ raw: {},
+ },
+ embedded: [],
+ provenance: {
+ c2pa: {
+ present: false,
+ embedding: [],
+ manifest_store: { active_manifest_urn: null, instance_id: null, claim_generator: null },
+ assertions: [],
+ signature: {
+ algorithm: null, signing_time: null,
+ certificate: { subject_cn: null, issuer_cn: null, serial_number: null },
+ },
+ validation: { validation_state: "Unknown", reasons: [], details_raw: null },
+ },
+ },
+ errors: [],
+ };
+}
+
+/** Detects the audio container format from magic bytes. */
+export function sniffContainer(b) {
+ if (b.length >= 3 && b[0] === 0x49 && b[1] === 0x44 && b[2] === 0x33)
+ return { type: "mp3", mime: "audio/mpeg" };
+ if (b.length >= 2 && b[0] === 0xff && (b[1] & 0xe0) === 0xe0) {
+ if ((b[1] & 0x06) === 0x00) return { type: "aac", mime: "audio/aac" };
+ return { type: "mp3", mime: "audio/mpeg" };
+ }
+ if (b.length >= 8 && b[0] === 0x0b && b[1] === 0x77)
+ return { type: "ac3", mime: "audio/ac3" };
+ if (b.length >= 16 &&
+ b[0] === 0x30 && b[1] === 0x26 && b[2] === 0xb2 && b[3] === 0x75 &&
+ b[4] === 0x8e && b[5] === 0x66 && b[6] === 0xcf && b[7] === 0x11)
+ return { type: "wma", mime: "audio/x-ms-wma" };
+ if (b.length >= 12 && ascii4(b, 0) === "RIFF" && ascii4(b, 8) === "WAVE")
+ return { type: "wav", mime: "audio/wav" };
+ if (b.length >= 12 && ascii4(b, 0) === "BW64" && ascii4(b, 8) === "WAVE")
+ return { type: "bw64", mime: "audio/wav" };
+ if (b.length >= 4 && ascii4(b, 0) === "fLaC")
+ return { type: "flac", mime: "audio/flac" };
+ if (b.length >= 4 && ascii4(b, 0) === "OggS") {
+ const idx = indexOfAscii(b, "OpusHead", 0, Math.min(b.length, 65536));
+ return idx >= 0 ? { type: "opus", mime: "audio/ogg" } : { type: "ogg", mime: "audio/ogg" };
+ }
+ if (b.length >= 12 && ascii4(b, 4) === "ftyp") {
+ const brand = ascii4(b, 8);
+ const isM4A = brand === "M4A " || brand === "M4B " || brand === "M4P ";
+ return { type: isM4A ? "m4a" : "mp4", mime: isM4A ? "audio/mp4" : "video/mp4", brand };
+ }
+ if (b.length >= 12 && ascii4(b, 0) === "FORM") {
+ const formType = ascii4(b, 8);
+ if (formType === "AIFF" || formType === "AIFC") return { type: "aiff", mime: "audio/aiff", brand: formType };
+ }
+ return { type: "unknown", mime: null };
+}
diff --git a/src/core/lib/AudioParsers.mjs b/src/core/lib/AudioParsers.mjs
new file mode 100644
index 00000000..0e564b04
--- /dev/null
+++ b/src/core/lib/AudioParsers.mjs
@@ -0,0 +1,630 @@
+/**
+ * Format-specific audio metadata parsers.
+ *
+ * @author d0s1nt [d0s1nt@cyberchefaudio]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+/* eslint-disable camelcase */
+
+import {
+ ascii4, indexOfAscii,
+ u32be, u32le, u16le, u64le, synchsafeToInt,
+ decodeUtf16LE, readNullTerminated, decodeText,
+ safeUtf8, decodeLatin1Trim,
+} from "./AudioBytes.mjs";
+
+/** Parses MP3 metadata: ID3v2 frames, ID3v1 footer, APEv2 tags. */
+export function parseMp3(b, report) {
+ processId3v2(b, report);
+ processId3v1(b, report);
+
+ const ape = parseApeV2BestEffort(b);
+ if (ape) {
+ report.detections.metadata_systems.push("apev2");
+ report.tags.raw.apev2 = ape;
+ }
+}
+
+/** Iterates ID3v2 frames and populates the report. */
+function processId3v2(b, report) {
+ report.detections.metadata_systems.push("id3v2");
+
+ const id3 = parseId3v2(b);
+ report.tags.raw.id3v2 = id3 ? { header: id3.header, frames: [] } : null;
+
+ if (id3) {
+ for (const f of id3.frames) {
+ const entry = { id: f.id, size: f.size, description: ID3_FRAME_DESCRIPTIONS[f.id] || null };
+
+ if (f.id[0] === "T" && f.id !== "TXXX") {
+ const text = f.data?.length >= 1 ?
+ decodeText(f.data.slice(1), f.data[0]).replace(/\u0000/g, "").trim() :
+ "";
+ entry.decoded = text;
+ if (f.id === "TLEN") {
+ const ms = normalizeTlen(text);
+ if (ms !== null) entry.normalized_ms = ms;
+ }
+ mapCommonId3(report, f.id, text);
+ } else if (f.id === "TXXX") {
+ const txxx = decodeTxxx(f.data);
+ entry.decoded = txxx;
+ if (!report.tags.raw.id3v2.txxx) report.tags.raw.id3v2.txxx = [];
+ report.tags.raw.id3v2.txxx.push(txxx);
+ } else if (f.id === "COMM") {
+ const comm = decodeCommFrame(f.data);
+ entry.decoded = comm;
+ if (comm?.text && !report.tags.common.comment) report.tags.common.comment = comm.text;
+ } else if (f.id === "GEOB") {
+ processGeobFrame(f, entry, report);
+ }
+
+ report.tags.raw.id3v2.frames.push(entry);
+ }
+ } else {
+ report.detections.metadata_systems = report.detections.metadata_systems.filter((x) => x !== "id3v2");
+ }
+}
+
+/** Parses GEOB frame contents, populates entry, embedded objects, and C2PA provenance. */
+function processGeobFrame(f, entry, report) {
+ const d = f.data, enc = d[0];
+ let off = 1;
+ const mime = readNullTerminated(d, off, 0);
+ const mimeType = decodeLatin1Trim(mime.valueBytes);
+ off = mime.next;
+ const file = readNullTerminated(d, off, enc);
+ const filename = decodeText(file.valueBytes, enc).replace(/\u0000/g, "").trim();
+ off = file.next;
+ const desc = readNullTerminated(d, off, enc);
+ const description = decodeText(desc.valueBytes, enc).replace(/\u0000/g, "").trim();
+ off = desc.next;
+ const objLen = d.length - off;
+
+ entry.geob = { mimeType, filename, description, object_bytes: objLen };
+ const geobId = `geob_${report.embedded.filter((x) => x.source === "id3v2:GEOB").length}`;
+ report.embedded.push({
+ id: geobId, source: "id3v2:GEOB",
+ content_type: mimeType || null, byte_length: objLen,
+ description: description || null, filename: filename || null,
+ });
+
+ const mt = (mimeType || "").toLowerCase();
+ if (mt.includes("c2pa") || mt.includes("jumbf") || mt.includes("application/x-c2pa-manifest-store")) {
+ report.provenance.c2pa.present = true;
+ report.provenance.c2pa.embedding.push({
+ carrier: "id3v2:GEOB", content_type: mimeType || null, byte_length: objLen,
+ });
+ }
+}
+
+/** Processes the 128-byte ID3v1 footer tag. */
+function processId3v1(b, report) {
+ const id3v1 = parseId3v1(b);
+ if (!id3v1) return;
+
+ report.detections.metadata_systems.push("id3v1");
+ report.tags.raw.id3v1 = id3v1;
+ mapCommon(report, id3v1, ID3V1_TO_COMMON);
+}
+
+/** Parses WAV/BWF/BW64 RIFF chunks: LIST/INFO, bext, iXML, axml, ds64. */
+export function parseRiffWave(b, report, maxTextBytes) {
+ report.detections.metadata_systems.push("riff_info");
+
+ const chunks = enumerateChunks(b, 12, b.length, 50000);
+ const riff = { chunks: [], info: null, bext: null, ixml: null, axml: null, ds64: null };
+
+ const info = {};
+ for (const c of chunks) {
+ riff.chunks.push({ id: c.id, size: c.size, offset: c.dataOff });
+ processRiffChunk(b, c, riff, info, report, maxTextBytes);
+ }
+
+ riff.info = Object.keys(info).length ? info : null;
+ report.tags.raw.riff = riff;
+ if (riff.info) mapCommon(report, riff.info, RIFF_TO_COMMON);
+}
+
+/** Processes a single RIFF chunk, updating riff state and the report. */
+function processRiffChunk(b, c, riff, info, report, maxTextBytes) {
+ if (c.id === "ds64") {
+ riff.ds64 = { present: true, size: c.size };
+ if (!report.detections.metadata_systems.includes("bw64_ds64")) report.detections.metadata_systems.push("bw64_ds64");
+ if (report.artifact.container.type === "wav") report.artifact.container.type = "bw64";
+ }
+
+ if (c.id === "LIST" && ascii4(b, c.dataOff) === "INFO") {
+ for (const s of enumerateChunks(b, c.dataOff + 4, c.dataOff + c.size, 10000))
+ info[s.id] = decodeLatin1Trim(b.slice(s.dataOff, s.dataOff + s.size));
+ }
+
+ if (c.id === "bext") {
+ if (!report.detections.metadata_systems.includes("bwf_bext")) report.detections.metadata_systems.push("bwf_bext");
+ riff.bext = parseBext(b, c.dataOff, c.size);
+ }
+
+ if (c.id === "iXML" || c.id === "axml") {
+ const key = c.id === "iXML" ? "ixml" : "axml";
+ if (!report.detections.metadata_systems.includes(key)) report.detections.metadata_systems.push(key);
+ const payload = b.slice(c.dataOff, c.dataOff + c.size);
+ riff[key] = { xml: safeUtf8(payload.slice(0, Math.min(payload.length, maxTextBytes))), truncated: payload.length > maxTextBytes };
+ report.embedded.push({
+ id: `${key}_0`, source: `riff:${c.id}`, content_type: "application/xml",
+ byte_length: payload.length, description: `${c.id} chunk`, filename: null,
+ });
+ }
+}
+
+/** Parses FLAC metablocks: STREAMINFO, Vorbis Comment, PICTURE. */
+export function parseFlac(b, report, maxTextBytes) {
+ report.detections.metadata_systems.push("flac_metablocks");
+
+ const blocks = parseFlacMetaBlocks(b);
+ report.tags.raw.flac = { blocks: [] };
+
+ for (const blk of blocks) {
+ report.tags.raw.flac.blocks.push({ type: blk.typeName, length: blk.length });
+
+ if (blk.typeName === "VORBIS_COMMENT") {
+ if (!report.detections.metadata_systems.includes("vorbis_comments")) report.detections.metadata_systems.push("vorbis_comments");
+ const vc = parseVorbisComment(blk.data);
+ report.tags.raw.vorbis_comments = vc;
+ mapVorbisCommon(report, vc);
+ } else if (blk.typeName === "PICTURE") {
+ const pic = parseFlacPicture(blk.data, maxTextBytes);
+ report.embedded.push({
+ id: `cover_art_${report.embedded.filter((x) => x.id.startsWith("cover_art_")).length}`,
+ source: "flac:PICTURE", content_type: pic.mime || null,
+ byte_length: pic.dataLength, description: pic.description || null, filename: null,
+ });
+ }
+ }
+}
+
+/** Parses OGG/Opus Vorbis comments. */
+export function parseOgg(b, report) {
+ if (!report.detections.metadata_systems.includes("ogg_opus_tags")) report.detections.metadata_systems.push("ogg_opus_tags");
+
+ const scanEnd = Math.min(b.length, 1024 * 1024);
+ let tags = null;
+ const opusTagsIdx = indexOfAscii(b, "OpusTags", 0, scanEnd);
+ if (opusTagsIdx >= 0) {
+ report.artifact.container.type = "opus";
+ tags = parseVorbisComment(b.slice(opusTagsIdx + 8, scanEnd));
+ } else {
+ const vorbisIdx = indexOfAscii(b, "\x03vorbis", 0, scanEnd);
+ if (vorbisIdx >= 0) tags = parseVorbisComment(b.slice(vorbisIdx + 7, scanEnd));
+ }
+
+ report.tags.raw.ogg = { has_opustags: opusTagsIdx >= 0, has_vorbis_comment: !!tags };
+
+ if (tags) {
+ if (!report.detections.metadata_systems.includes("vorbis_comments")) report.detections.metadata_systems.push("vorbis_comments");
+ report.tags.raw.vorbis_comments = tags;
+ mapVorbisCommon(report, tags);
+ }
+}
+
+/** Best-effort top-level atom scan for MP4/M4A. */
+export function parseMp4BestEffort(b, report) {
+ report.detections.metadata_systems.push("mp4_atoms");
+ const atoms = [];
+
+ let off = 0;
+ while (off + 8 <= b.length && atoms.length < 2000) {
+ const size = u32be(b, off);
+ const type = ascii4(b, off + 4);
+ if (size < 8) break;
+ atoms.push({ type, size, offset: off });
+ off += size;
+ }
+
+ report.tags.raw.mp4 = {
+ top_level_atoms: atoms.slice(0, 200),
+ hints: {
+ hasMoov: atoms.some((a) => a.type === "moov"),
+ hasUdta: atoms.some((a) => a.type === "udta"),
+ hasMeta: atoms.some((a) => a.type === "meta"),
+ hasIlst: atoms.some((a) => a.type === "ilst"),
+ },
+ };
+}
+
+/** Best-effort AIFF/AIFC chunk scanning for NAME, AUTH, ANNO. */
+export function parseAiffBestEffort(b, report, maxTextBytes) {
+ report.detections.metadata_systems.push("aiff_chunks");
+ let off = 12;
+ const chunks = [];
+ while (off + 8 <= b.length && chunks.length < 2000) {
+ const id = ascii4(b, off);
+ const size = u32be(b, off + 4);
+ const dataOff = off + 8;
+ chunks.push({ id, size, offset: off });
+
+ if (["NAME", "AUTH", "ANNO", "(c) "].includes(id)) {
+ const txt = safeUtf8(b.slice(dataOff, dataOff + Math.min(size, maxTextBytes)));
+ if (!report.tags.raw.aiff) report.tags.raw.aiff = { chunks: [] };
+ report.tags.raw.aiff.chunks.push({ id, value: txt, truncated: size > maxTextBytes });
+ }
+
+ off = dataOff + size + (size % 2);
+ }
+
+ if (!report.tags.raw.aiff) report.tags.raw.aiff = {};
+ report.tags.raw.aiff.chunk_index = chunks.slice(0, 500);
+
+ const nameChunk = report.tags.raw.aiff?.chunks?.find((ch) => ch.id === "NAME")?.value;
+ if (nameChunk) report.tags.common.title = report.tags.common.title || nameChunk;
+}
+
+const AAC_SAMPLE_RATES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
+const AAC_PROFILES = ["Main", "LC", "SSR", "LTP"];
+const AAC_CHANNELS = ["defined in AOT", "mono", "stereo", "3.0", "4.0", "5.0", "5.1", "7.1"];
+
+/** Parses AAC ADTS frame header for audio parameters. */
+export function parseAacAdts(b, report) {
+ report.detections.metadata_systems.push("adts_header");
+ if (b.length < 7) return;
+
+ const id = (b[1] >> 3) & 0x01;
+ const profile = (b[2] >> 6) & 0x03;
+ const freqIdx = (b[2] >> 2) & 0x0f;
+ const chanCfg = ((b[2] & 0x01) << 2) | ((b[3] >> 6) & 0x03);
+
+ report.tags.raw.aac = {
+ mpeg_version: id === 1 ? "MPEG-2" : "MPEG-4",
+ profile: AAC_PROFILES[profile] || `Profile ${profile}`,
+ sample_rate: AAC_SAMPLE_RATES[freqIdx] || null,
+ sample_rate_index: freqIdx,
+ channel_configuration: chanCfg,
+ channel_description: AAC_CHANNELS[chanCfg] || null,
+ };
+}
+
+const AC3_SAMPLE_RATES = [48000, 44100, 32000];
+const AC3_BITRATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640];
+const AC3_ACMODES = [
+ "2.0 (Ch1+Ch2)", "1.0 (C)", "2.0 (L R)", "3.0 (L C R)",
+ "2.1 (L R S)", "3.1 (L C R S)", "2.2 (L R SL SR)", "3.2 (L C R SL SR)",
+];
+
+/** Parses AC3 (Dolby Digital) bitstream info. */
+export function parseAc3(b, report) {
+ report.detections.metadata_systems.push("ac3_bsi");
+ if (b.length < 8) return;
+
+ const fscod = (b[4] >> 6) & 0x03;
+ const frmsizecod = b[4] & 0x3f;
+ const bsid = (b[5] >> 3) & 0x1f;
+ const bsmod = b[5] & 0x07;
+ const acmod = (b[6] >> 5) & 0x07;
+
+ report.tags.raw.ac3 = {
+ sample_rate: AC3_SAMPLE_RATES[fscod] || null,
+ fscod,
+ bitrate_kbps: AC3_BITRATES[frmsizecod >> 1] || null,
+ frmsizecod, bsid, bsmod, acmod,
+ channel_layout: AC3_ACMODES[acmod] || null,
+ };
+}
+
+/** Parses WMA files (ASF container) for content description metadata. */
+export function parseWmaAsf(b, report) {
+ report.detections.metadata_systems.push("asf_header");
+ if (b.length < 30) return;
+
+ const headerSize = Number(u64le(b, 16));
+ const numObjects = u32le(b, 24);
+ const headerEnd = Math.min(b.length, headerSize);
+
+ const objects = [];
+ let off = 30;
+
+ for (let i = 0; i < numObjects && off + 24 <= headerEnd; i++) {
+ const guid4 = [b[off], b[off + 1], b[off + 2], b[off + 3]];
+ const objSize = Number(u64le(b, off + 16));
+ if (objSize < 24 || off + objSize > headerEnd) break;
+
+ const dataOff = off + 24;
+ const dataLen = objSize - 24;
+
+ if (guid4[0] === 0x33 && guid4[1] === 0x26 && guid4[2] === 0xb2 && guid4[3] === 0x75 && dataLen >= 10) {
+ const cd = parseAsfContentDescription(b, dataOff);
+ if (!report.detections.metadata_systems.includes("asf_content_desc"))
+ report.detections.metadata_systems.push("asf_content_desc");
+ if (!report.tags.raw.asf) report.tags.raw.asf = {};
+ report.tags.raw.asf.content_description = cd;
+ mapCommon(report, cd, ASF_CD_TO_COMMON);
+ }
+
+ if (guid4[0] === 0x40 && guid4[1] === 0xa4 && guid4[2] === 0xd0 && guid4[3] === 0xd2 && dataLen >= 2) {
+ const ext = parseAsfExtContentDescription(b, dataOff, dataOff + dataLen);
+ if (!report.detections.metadata_systems.includes("asf_ext_content_desc"))
+ report.detections.metadata_systems.push("asf_ext_content_desc");
+ if (!report.tags.raw.asf) report.tags.raw.asf = {};
+ report.tags.raw.asf.extended_content = ext;
+
+ const c = report.tags.common;
+ for (const d of ext) {
+ const field = WMA_TO_COMMON[(d.name || "").toUpperCase()];
+ if (field && d.value) c[field] = c[field] || d.value;
+ }
+ }
+
+ objects.push({ guid_prefix: guid4.map(x => x.toString(16).padStart(2, "0")).join(""), size: objSize });
+ off += objSize;
+ }
+
+ if (!report.tags.raw.asf) report.tags.raw.asf = {};
+ report.tags.raw.asf.header_objects = objects;
+}
+
+const ID3_FRAME_DESCRIPTIONS = {
+ TIT2: "Title/songname/content description", TPE1: "Lead performer(s)/Soloist(s)",
+ TRCK: "Track number/Position in set", TALB: "Album/Movie/Show title",
+ TDRC: "Recording time", TYER: "Year", TCON: "Content type",
+ TPE2: "Band/orchestra/accompaniment", TLEN: "Length (ms)", TCOM: "Composer",
+ COMM: "Comments", APIC: "Attached picture", GEOB: "General encapsulated object",
+ TXXX: "User defined text information frame", UFID: "Unique file identifier", PRIV: "Private frame",
+};
+
+const ID3_TO_COMMON = {
+ TIT2: "title", TPE1: "artist", TALB: "album", TDRC: "date", TYER: "date",
+ TRCK: "track", TCON: "genre", COMM: "comment", TCOM: "composer", TCOP: "copyright", TLAN: "language",
+};
+const VORBIS_TO_COMMON = {
+ TITLE: "title", ARTIST: "artist", ALBUM: "album", DATE: "date",
+ TRACKNUMBER: "track", GENRE: "genre", COMMENT: "comment", COMPOSER: "composer", LANGUAGE: "language",
+};
+const WMA_TO_COMMON = {
+ "WM/ALBUMTITLE": "album", "WM/GENRE": "genre", "WM/YEAR": "date",
+ "WM/TRACKNUMBER": "track", "WM/COMPOSER": "composer", "WM/LANGUAGE": "language",
+};
+const ID3V1_TO_COMMON = { title: "title", artist: "artist", album: "album", year: "date", comment: "comment", genre: "genre", track: "track" };
+const RIFF_TO_COMMON = { INAM: "title", IART: "artist", ICMT: "comment", IGNR: "genre", ICRD: "date", ICOP: "copyright" };
+const ASF_CD_TO_COMMON = { title: "title", author: "artist", copyright: "copyright", description: "comment" };
+
+/** Maps source object fields to the common tags layer via a mapping table. */
+function mapCommon(report, source, mapping) {
+ const c = report.tags.common;
+ for (const [sk, ck] of Object.entries(mapping))
+ c[ck] = c[ck] || source[sk] || null;
+}
+
+/** Maps an ID3v2 frame value to the common tags layer. */
+function mapCommonId3(report, frameId, text) {
+ const field = ID3_TO_COMMON[frameId];
+ if (field) report.tags.common[field] = report.tags.common[field] || text || null;
+}
+
+/** Decodes an ID3v2 COMM (Comments) frame. */
+function decodeCommFrame(data) {
+ if (!data || data.length < 5) return null;
+ const enc = data[0];
+ const language = String.fromCharCode(data[1], data[2], data[3]);
+ const { valueBytes: descBytes, next } = readNullTerminated(data, 4, enc);
+ const short_description = decodeText(descBytes, enc).replace(/\u0000/g, "").trim() || null;
+ const text = decodeText(data.slice(next), enc).replace(/\u0000/g, "").trim() || null;
+ return { language, short_description, text };
+}
+
+/** Normalizes TLEN to integer milliseconds. */
+function normalizeTlen(s) {
+ if (!s) return null;
+ if (/^\s*\d+\s*$/.test(s)) return parseInt(s.trim(), 10);
+ const f = Number(s);
+ if (Number.isFinite(f) && f > 0 && f < 100000) return Math.round(f * 1000);
+ return null;
+}
+
+/** Parses the ID3v2 tag header and frames. */
+function parseId3v2(mp3) {
+ if (mp3.length < 10 || mp3[0] !== 0x49 || mp3[1] !== 0x44 || mp3[2] !== 0x33) return null;
+
+ const major = mp3[3], minor = mp3[4], flags = mp3[5];
+ const tagSize = synchsafeToInt(mp3[6], mp3[7], mp3[8], mp3[9]);
+ let offset = 10;
+ const end = 10 + tagSize;
+
+ const frames = [];
+ while (offset + 10 <= end) {
+ const id = String.fromCharCode(mp3[offset], mp3[offset + 1], mp3[offset + 2], mp3[offset + 3]);
+ if (!/^[A-Z0-9]{4}$/.test(id)) break;
+ const size = major === 4 ?
+ synchsafeToInt(mp3[offset + 4], mp3[offset + 5], mp3[offset + 6], mp3[offset + 7]) :
+ u32be(mp3, offset + 4);
+ offset += 10;
+ if (size <= 0 || offset + size > mp3.length) break;
+ frames.push({ id, size, data: mp3.slice(offset, offset + size) });
+ offset += size;
+ }
+
+ return { header: { version: `${major}.${minor}`, flags, tag_size: tagSize }, frames };
+}
+
+/** Parses the 128-byte ID3v1 tag at the end of the file. */
+function parseId3v1(b) {
+ if (b.length < 128) return null;
+ const off = b.length - 128;
+ if (b[off] !== 0x54 || b[off + 1] !== 0x41 || b[off + 2] !== 0x47) return null;
+
+ let track = null;
+ if (b[off + 125] === 0x00 && b[off + 126] !== 0x00) track = String(b[off + 126]);
+
+ return {
+ title: decodeLatin1Trim(b.slice(off + 3, off + 33)),
+ artist: decodeLatin1Trim(b.slice(off + 33, off + 63)),
+ album: decodeLatin1Trim(b.slice(off + 63, off + 93)),
+ year: decodeLatin1Trim(b.slice(off + 93, off + 97)),
+ comment: decodeLatin1Trim(b.slice(off + 97, off + 127)),
+ track, genre: String(b[off + 127]),
+ };
+}
+
+/** Decodes an ID3v2 TXXX (user-defined text) frame. */
+function decodeTxxx(data) {
+ if (!data || data.length < 2) return null;
+ const enc = data[0];
+ const { valueBytes: descBytes, next } = readNullTerminated(data, 1, enc);
+ const desc = decodeText(descBytes, enc).replace(/\u0000/g, "").trim();
+ const val = decodeText(data.slice(next), enc).replace(/\u0000/g, "").trim();
+ return { description: desc || null, value: val || null };
+}
+
+/** Best-effort APEv2 tag parser scanning the last 32 KB. */
+function parseApeV2BestEffort(b) {
+ const scanStart = Math.max(0, b.length - 32768);
+ const idx = indexOfAscii(b, "APETAGEX", scanStart, b.length);
+ if (idx < 0) return null;
+ if (idx + 32 > b.length) return { present: true, warning: "APETAGEX found but footer truncated." };
+
+ const ver = u32le(b, idx + 8), size = u32le(b, idx + 12);
+ const count = u32le(b, idx + 16), flags = u32le(b, idx + 20);
+
+ const tagStart = idx + 32 - size;
+ if (tagStart < 0 || tagStart >= b.length)
+ return { present: true, version: ver, size, count, flags, warning: "APEv2 bounds invalid (non-standard placement)." };
+
+ const items = [];
+ let off = tagStart + 32;
+ const end = Math.min(b.length, idx);
+ while (off + 8 < end && items.length < 5000) {
+ const valueSize = u32le(b, off), itemFlags = u32le(b, off + 4);
+ off += 8;
+ let keyEnd = off;
+ while (keyEnd < end && b[keyEnd] !== 0x00) keyEnd++;
+ const key = decodeLatin1Trim(b.slice(off, keyEnd));
+ off = keyEnd + 1;
+ if (!key || off + valueSize > end) break;
+ const value = safeUtf8(b.slice(off, off + valueSize)).replace(/\u0000/g, "").trim();
+ off += valueSize;
+ items.push({ key, value, flags: itemFlags });
+ }
+
+ return { present: true, version: ver, size, count, flags, items };
+}
+
+/** Enumerates RIFF-style chunks (id + LE32 size) within a byte range, padding to even. */
+function enumerateChunks(b, start, end, maxCount) {
+ const chunks = [];
+ let off = start;
+ while (off + 8 <= end && chunks.length < maxCount) {
+ const id = ascii4(b, off);
+ const size = u32le(b, off + 4);
+ const dataOff = off + 8;
+ if (dataOff + size > end) break;
+ chunks.push({ id, size, dataOff });
+ off = dataOff + size + (size % 2);
+ }
+ return chunks;
+}
+
+/** Parses a BWF bext chunk. */
+function parseBext(b, off, size) {
+ const slice = b.slice(off, off + size);
+ const timeRefLow = u32le(slice, 338), timeRefHigh = u32le(slice, 342);
+ return {
+ description: decodeLatin1Trim(slice.slice(0, 256)) || null,
+ originator: decodeLatin1Trim(slice.slice(256, 288)) || null,
+ originator_reference: decodeLatin1Trim(slice.slice(288, 320)) || null,
+ origination_date: decodeLatin1Trim(slice.slice(320, 330)) || null,
+ origination_time: decodeLatin1Trim(slice.slice(330, 338)) || null,
+ time_reference_samples: ((BigInt(timeRefHigh) << 32n) | BigInt(timeRefLow)).toString(),
+ };
+}
+
+const FLAC_TYPE_NAMES = { 0: "STREAMINFO", 1: "PADDING", 2: "APPLICATION", 3: "SEEKTABLE", 4: "VORBIS_COMMENT", 5: "CUESHEET", 6: "PICTURE" };
+
+/** Parses FLAC metadata blocks following the "fLaC" marker. */
+function parseFlacMetaBlocks(b) {
+ const blocks = [];
+ let off = 4;
+ while (off + 4 <= b.length && blocks.length < 10000) {
+ const header = b[off];
+ const isLast = (header & 0x80) !== 0;
+ const type = header & 0x7f;
+ const len = (b[off + 1] << 16) | (b[off + 2] << 8) | b[off + 3];
+ off += 4;
+ if (off + len > b.length) break;
+ blocks.push({ type, typeName: FLAC_TYPE_NAMES[type] || `TYPE_${type}`, length: len, data: b.slice(off, off + len) });
+ off += len;
+ if (isLast) break;
+ }
+ return blocks;
+}
+
+/** Parses a Vorbis Comment block (used by FLAC and OGG). */
+function parseVorbisComment(buf) {
+ let off = 0;
+ const vendorLen = u32le(buf, off); off += 4;
+ if (off + vendorLen > buf.length) return { vendor: null, comments: [], warning: "vendor_len out of bounds" };
+ const vendor = safeUtf8(buf.slice(off, off + vendorLen)); off += vendorLen;
+ const count = u32le(buf, off); off += 4;
+
+ const comments = [];
+ for (let i = 0; i < count && off + 4 <= buf.length && comments.length < 20000; i++) {
+ const l = u32le(buf, off); off += 4;
+ if (off + l > buf.length) break;
+ const s = safeUtf8(buf.slice(off, off + l)); off += l;
+ const eq = s.indexOf("=");
+ if (eq > 0) comments.push({ key: s.slice(0, eq).toUpperCase(), value: s.slice(eq + 1) });
+ }
+ return { vendor, comments };
+}
+
+/** Maps Vorbis Comment fields to the common tags layer. */
+function mapVorbisCommon(report, vc) {
+ const c = report.tags.common;
+ for (const [vk, ck] of Object.entries(VORBIS_TO_COMMON))
+ c[ck] = c[ck] || vc.comments?.find((x) => x.key === vk)?.value || null;
+}
+
+/** Parses a FLAC PICTURE metadata block (extracts mime, description, data length). */
+function parseFlacPicture(data, maxTextBytes) {
+ let off = 4;
+ const mimeLen = u32be(data, off); off += 4;
+ const mime = safeUtf8(data.slice(off, off + Math.min(mimeLen, maxTextBytes))); off += mimeLen;
+ const descLen = u32be(data, off); off += 4;
+ const description = safeUtf8(data.slice(off, off + Math.min(descLen, maxTextBytes))); off += descLen + 16;
+ return { mime, description, dataLength: u32be(data, off) };
+}
+
+/** Parses the ASF Content Description Object fields. */
+function parseAsfContentDescription(b, off) {
+ const titleLen = u16le(b, off), authorLen = u16le(b, off + 2);
+ const copyrightLen = u16le(b, off + 4), descLen = u16le(b, off + 6), ratingLen = u16le(b, off + 8);
+ let pos = off + 10;
+ const title = decodeUtf16LE(b, pos, titleLen); pos += titleLen;
+ const author = decodeUtf16LE(b, pos, authorLen); pos += authorLen;
+ const copyright = decodeUtf16LE(b, pos, copyrightLen); pos += copyrightLen;
+ const description = decodeUtf16LE(b, pos, descLen); pos += descLen;
+ const rating = decodeUtf16LE(b, pos, ratingLen);
+ return { title, author, copyright, description, rating };
+}
+
+/** Parses the ASF Extended Content Description Object descriptors. */
+function parseAsfExtContentDescription(b, off, end) {
+ const count = u16le(b, off);
+ let pos = off + 2;
+ const descriptors = [];
+ for (let i = 0; i < count && pos + 6 <= end && descriptors.length < 5000; i++) {
+ const nameLen = u16le(b, pos); pos += 2;
+ if (pos + nameLen > end) break;
+ const name = decodeUtf16LE(b, pos, nameLen); pos += nameLen;
+ const valueType = u16le(b, pos); pos += 2;
+ const valueLen = u16le(b, pos); pos += 2;
+ if (pos + valueLen > end) break;
+ let value;
+ if (valueType === 0) value = decodeUtf16LE(b, pos, valueLen);
+ else if (valueType === 3) value = u32le(b, pos);
+ else if (valueType === 5) value = u16le(b, pos);
+ else if (valueType === 2) value = u32le(b, pos) !== 0;
+ else value = `(${valueLen} bytes, type ${valueType})`;
+ pos += valueLen;
+ descriptors.push({ name, value_type: valueType, value });
+ }
+ return descriptors;
+}
diff --git a/src/core/operations/ExtractAudioMetadata.mjs b/src/core/operations/ExtractAudioMetadata.mjs
new file mode 100644
index 00000000..7018ffd0
--- /dev/null
+++ b/src/core/operations/ExtractAudioMetadata.mjs
@@ -0,0 +1,175 @@
+/**
+ * @author d0s1nt [d0s1nt@cyberchefaudio]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { makeEmptyReport, sniffContainer } from "../lib/AudioMetaSchema.mjs";
+import {
+ parseMp3, parseRiffWave, parseFlac, parseOgg,
+ parseMp4BestEffort, parseAiffBestEffort,
+ parseAacAdts, parseAc3, parseWmaAsf,
+} from "../lib/AudioParsers.mjs";
+
+/**
+ * Extract Audio Metadata operation.
+ */
+class ExtractAudioMetadata extends Operation {
+ /** Creates the Extract Audio Metadata operation. */
+ constructor() {
+ super();
+
+ this.name = "Extract Audio Metadata";
+ this.module = "Default";
+ this.description =
+ "Extract common audio metadata across MP3 (ID3v2/ID3v1/GEOB), WAV/BWF/BW64 (INFO/bext/iXML/axml), FLAC (Vorbis Comment/Picture), OGG (Vorbis/OpusTags), AAC (ADTS), AC3 (Dolby Digital), WMA (ASF), plus best-effort MP4/M4A and AIFF scanning. Outputs normalized JSON.";
+ this.infoURL = "https://wikipedia.org/wiki/Audio_file_format";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "JSON";
+ this.presentType = "html";
+
+ this.args = [
+ { name: "Filename (optional)", type: "string", value: "" },
+ { name: "Max embedded text bytes (iXML/axml/etc)", type: "number", value: 1024 * 512 },
+ ];
+ }
+
+ /**
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {Object}
+ */
+ run(input, args) {
+ const filename = (args?.[0] || "").trim() || null;
+ const maxTextBytes = Number.isFinite(args?.[1]) ? Math.max(1024, args[1]) : 1024 * 512;
+
+ if (!(input instanceof ArrayBuffer) || input.byteLength === 0)
+ throw new OperationError("No input data. Load an audio file (drag/drop or use the open file button).");
+
+ const bytes = new Uint8Array(input);
+ const container = sniffContainer(bytes);
+ const report = makeEmptyReport(filename, bytes.length, container);
+
+ try {
+ const parsers = {
+ mp3: () => parseMp3(bytes, report),
+ wav: () => parseRiffWave(bytes, report, maxTextBytes),
+ bw64: () => parseRiffWave(bytes, report, maxTextBytes),
+ flac: () => parseFlac(bytes, report, maxTextBytes),
+ ogg: () => parseOgg(bytes, report),
+ opus: () => parseOgg(bytes, report),
+ mp4: () => parseMp4BestEffort(bytes, report),
+ m4a: () => parseMp4BestEffort(bytes, report),
+ aiff: () => parseAiffBestEffort(bytes, report, maxTextBytes),
+ aac: () => parseAacAdts(bytes, report),
+ ac3: () => parseAc3(bytes, report),
+ wma: () => parseWmaAsf(bytes, report),
+ };
+ if (parsers[container.type]) {
+ parsers[container.type]();
+ } else {
+ report.errors.push({ stage: "sniff", message: "Unknown/unsupported container (best-effort scan not implemented)." });
+ }
+ } catch (e) {
+ report.errors.push({ stage: "parse", message: String(e?.message || e) });
+ }
+
+ return report;
+ }
+
+ /** Renders the extracted metadata as an HTML table. */
+ present(data) {
+ if (!data || typeof data !== "object") return JSON.stringify(data, null, 4);
+
+ const esc = Utils.escapeHtml;
+ const row = (k, v) => `${esc(String(k))} ${esc(String(v ?? ""))} \n`;
+ const section = (title) => `${esc(title)} \n`;
+ const objRows = (obj, filter = (v) => v !== null) => {
+ for (const [k, v] of Object.entries(obj)) {
+ if (filter(v)) html += row(k, v);
+ }
+ };
+ const objSection = (obj, title, filter) => {
+ if (!obj) return;
+ html += section(title);
+ objRows(obj, filter);
+ };
+ const listSection = (arr, title, fmt) => {
+ if (!arr?.length) return;
+ html += section(title);
+ for (const item of arr) html += fmt(item);
+ };
+
+ let html = `\n`;
+
+ html += section("Artifact");
+ html += row("Filename", data.artifact?.filename || "(none)");
+ html += row("Size", `${(data.artifact?.byte_length ?? 0).toLocaleString()} bytes`);
+ html += row("Container", data.artifact?.container?.type);
+ html += row("MIME", data.artifact?.container?.mime);
+ if (data.artifact?.container?.brand) html += row("Brand", data.artifact.container.brand);
+
+ html += section("Detections");
+ html += row("Metadata systems", (data.detections?.metadata_systems || []).join(", ") || "None");
+ html += row("Provenance systems", (data.detections?.provenance_systems || []).join(", ") || "None");
+
+ const common = data.tags?.common || {};
+ html += section("Common Tags");
+ if (Object.values(common).some((v) => v !== null)) {
+ for (const [key, val] of Object.entries(common)) {
+ if (val !== null) html += row(key.charAt(0).toUpperCase() + key.slice(1), val);
+ }
+ } else {
+ html += row("(none)", "No common tags found");
+ }
+
+ listSection(data.tags?.raw?.id3v2?.frames, "ID3v2 Frames", (f) => {
+ const val = typeof f.decoded === "object" ? JSON.stringify(f.decoded) : (f.decoded ?? `(${f.size} bytes)`);
+ return row(f.id + (f.description ? ` \u2014 ${f.description}` : ""), val);
+ });
+ objSection(data.tags?.raw?.id3v1, "ID3v1", (v) => !!v);
+ listSection(data.tags?.raw?.apev2?.items, "APEv2 Tags", (i) => row(i.key, i.value));
+
+ if (data.tags?.raw?.vorbis_comments?.comments?.length) {
+ html += section("Vorbis Comments");
+ html += row("Vendor", data.tags.raw.vorbis_comments.vendor);
+ for (const c of data.tags.raw.vorbis_comments.comments) html += row(c.key, c.value);
+ }
+
+ objSection(data.tags?.raw?.riff?.info, "RIFF INFO", () => true);
+ objSection(data.tags?.raw?.riff?.bext, "BWF bext");
+ listSection(data.tags?.raw?.riff?.chunks, "RIFF Chunks", (c) => row(c.id, `${c.size} bytes @ offset ${c.offset}`));
+ listSection(data.tags?.raw?.flac?.blocks, "FLAC Metadata Blocks", (b) => row(b.type, `${b.length} bytes`));
+
+ if (data.tags?.raw?.mp4?.top_level_atoms?.length) {
+ html += section("MP4 Top-Level Atoms");
+ const atoms = data.tags.raw.mp4.top_level_atoms;
+ for (const a of atoms.slice(0, 50)) html += row(a.type, `${a.size} bytes @ offset ${a.offset}`);
+ if (atoms.length > 50) html += row("...", `${atoms.length - 50} more atoms`);
+ }
+
+ listSection(data.tags?.raw?.aiff?.chunks, "AIFF Chunks", (c) => row(c.id, c.value));
+ objSection(data.tags?.raw?.aac, "AAC ADTS");
+ objSection(data.tags?.raw?.ac3, "AC3 (Dolby Digital)");
+ objSection(data.tags?.raw?.asf?.content_description, "ASF Content Description", (v) => !!v);
+ listSection(data.tags?.raw?.asf?.extended_content, "ASF Extended Content", (d) => row(d.name, d.value));
+ listSection(data.embedded, "Embedded Objects", (e) => row(e.id, `${e.content_type || "unknown"} \u2014 ${(e.byte_length ?? 0).toLocaleString()} bytes`));
+
+ if (data.provenance?.c2pa?.present) {
+ html += section("C2PA Provenance");
+ html += row("Present", "Yes");
+ for (const emb of (data.provenance.c2pa.embedding || []))
+ html += row("Carrier", `${emb.carrier} \u2014 ${(emb.byte_length ?? 0).toLocaleString()} bytes`);
+ }
+
+ listSection(data.errors, "Errors", (e) => row(e.stage, e.message));
+
+ html += "
";
+ return html;
+ }
+}
+
+export default ExtractAudioMetadata;
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index 3585e270..2134cdd9 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -67,6 +67,7 @@ import "./tests/DropNthBytes.mjs";
import "./tests/ECDSA.mjs";
import "./tests/ELFInfo.mjs";
import "./tests/Enigma.mjs";
+import "./tests/ExtractAudioMetadata.mjs";
import "./tests/ExtractEmailAddresses.mjs";
import "./tests/ExtractHashes.mjs";
import "./tests/ExtractIPAddresses.mjs";
diff --git a/tests/operations/tests/ExtractAudioMetadata.mjs b/tests/operations/tests/ExtractAudioMetadata.mjs
new file mode 100644
index 00000000..24fa3671
--- /dev/null
+++ b/tests/operations/tests/ExtractAudioMetadata.mjs
@@ -0,0 +1,287 @@
+/**
+ * Extract Audio Metadata operation tests.
+ *
+ * @author d0s1nt
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+import {
+ MP3_HEX, WAV_HEX, FLAC_HEX, AAC_HEX,
+ AC3_HEX, OGG_HEX, OPUS_HEX, WMA_HEX,
+ M4A_HEX, AIFF_HEX
+} from "../../samples/Audio.mjs";
+
+TestRegister.addTests([
+ // ---- MP3 ----
+ {
+ name: "Extract Audio Metadata: MP3 container and MIME",
+ input: MP3_HEX,
+ expectedMatch: /Container<\/td>mp3<\/td>.*MIME<\/td> audio\/mpeg<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.mp3", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: MP3 common tags (title, artist)",
+ input: MP3_HEX,
+ expectedMatch: /Title<\/td> Galway<\/td>.*Artist<\/td> Kevin MacLeod<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.mp3", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: MP3 ID3v2 frames (TIT2, TPE1, TSSE)",
+ input: MP3_HEX,
+ expectedMatch: /ID3v2 Frames.*TIT2.*Galway.*TPE1.*Kevin MacLeod.*TSSE.*Lavf56\.40\.101/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.mp3", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: MP3 detections (id3v2)",
+ input: MP3_HEX,
+ expectedMatch: /Metadata systems<\/td> id3v2<\/td>/,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.mp3", 524288] }
+ ]
+ },
+
+ // ---- WAV ----
+ {
+ name: "Extract Audio Metadata: WAV container and MIME",
+ input: WAV_HEX,
+ expectedMatch: /Container<\/td> wav<\/td>.*MIME<\/td> audio\/wav<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.wav", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: WAV RIFF chunks (fmt)",
+ input: WAV_HEX,
+ expectedMatch: /RIFF Chunks.*fmt .*16 bytes @ offset 20/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.wav", 524288] }
+ ]
+ },
+
+ // ---- FLAC ----
+ {
+ name: "Extract Audio Metadata: FLAC container and common tags",
+ input: FLAC_HEX,
+ expectedMatch: /Container<\/td> flac<\/td>.*Title<\/td> Galway<\/td>.*Artist<\/td> Kevin MacLeod<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.flac", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: FLAC metadata blocks (STREAMINFO, VORBIS_COMMENT)",
+ input: FLAC_HEX,
+ expectedMatch: /FLAC Metadata Blocks.*STREAMINFO<\/td> 34 bytes<\/td>.*VORBIS_COMMENT<\/td> 86 bytes<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.flac", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: FLAC Vorbis comments (vendor, tags)",
+ input: FLAC_HEX,
+ expectedMatch: /Vorbis Comments.*Vendor<\/td> Lavf56\.40\.101<\/td>.*TITLE<\/td> Galway<\/td>.*ARTIST<\/td> Kevin MacLeod<\/td>.*ENCODER<\/td> Lavf56\.40\.101<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.flac", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: FLAC detections",
+ input: FLAC_HEX,
+ expectedMatch: /Metadata systems<\/td> flac_metablocks, vorbis_comments<\/td>/,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.flac", 524288] }
+ ]
+ },
+
+ // ---- AAC ----
+ {
+ name: "Extract Audio Metadata: AAC container and MIME",
+ input: AAC_HEX,
+ expectedMatch: /Container<\/td> aac<\/td>.*MIME<\/td> audio\/aac<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.aac", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: AAC ADTS technical fields",
+ input: AAC_HEX,
+ expectedMatch: /AAC ADTS.*mpeg_version<\/td> MPEG-4<\/td>.*profile<\/td> LC<\/td>.*sample_rate<\/td> 44100<\/td>.*channel_description<\/td> stereo<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.aac", 524288] }
+ ]
+ },
+
+ // ---- AC3 ----
+ {
+ name: "Extract Audio Metadata: AC3 container and MIME",
+ input: AC3_HEX,
+ expectedMatch: /Container<\/td> ac3<\/td>.*MIME<\/td> audio\/ac3<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.ac3", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: AC3 technical fields (sample rate, bitrate, channels)",
+ input: AC3_HEX,
+ expectedMatch: /AC3 \(Dolby Digital\).*sample_rate<\/td> 44100<\/td>.*bitrate_kbps<\/td> 192<\/td>.*channel_layout<\/td> 2\.0 \(L R\)<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.ac3", 524288] }
+ ]
+ },
+
+ // ---- OGG Vorbis ----
+ {
+ name: "Extract Audio Metadata: OGG container and common tags",
+ input: OGG_HEX,
+ expectedMatch: /Container<\/td> ogg<\/td>.*Title<\/td> Galway<\/td>.*Artist<\/td> Kevin MacLeod<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.ogg", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: OGG Vorbis comments (vendor, encoder)",
+ input: OGG_HEX,
+ expectedMatch: /Vorbis Comments.*Vendor<\/td> Lavf56\.40\.101<\/td>.*ENCODER<\/td> Lavc56\.60\.100 libvorbis<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.ogg", 524288] }
+ ]
+ },
+
+ // ---- Opus ----
+ {
+ name: "Extract Audio Metadata: Opus container and common tags",
+ input: OPUS_HEX,
+ expectedMatch: /Container<\/td> opus<\/td>.*Title<\/td> Galway<\/td>.*Artist<\/td> Kevin MacLeod<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.opus", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: Opus Vorbis comments (vendor, encoder)",
+ input: OPUS_HEX,
+ expectedMatch: /Vorbis Comments.*Vendor<\/td> Lavf58\.19\.102<\/td>.*ENCODER<\/td> Lavc58\.34\.100 libopus<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.opus", 524288] }
+ ]
+ },
+
+ // ---- WMA/ASF ----
+ {
+ name: "Extract Audio Metadata: WMA container and MIME",
+ input: WMA_HEX,
+ expectedMatch: /Container<\/td> wma<\/td>.*MIME<\/td> audio\/x-ms-wma<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.wma", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: WMA common tags (title, artist)",
+ input: WMA_HEX,
+ expectedMatch: /Title<\/td> Galway<\/td>.*Artist<\/td> Kevin MacLeod<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.wma", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: WMA ASF Content Description",
+ input: WMA_HEX,
+ expectedMatch: /ASF Content Description.*title<\/td> Galway<\/td>.*author<\/td> Kevin MacLeod<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.wma", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: WMA ASF Extended Content (encoding settings)",
+ input: WMA_HEX,
+ expectedMatch: /ASF Extended Content.*WM\/EncodingSettings<\/td> Lavf56\.40\.101<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.wma", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: WMA detections",
+ input: WMA_HEX,
+ expectedMatch: /Metadata systems<\/td> asf_header, asf_content_desc, asf_ext_content_desc<\/td>/,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.wma", 524288] }
+ ]
+ },
+
+ // ---- M4A ----
+ {
+ name: "Extract Audio Metadata: M4A container, MIME and brand",
+ input: M4A_HEX,
+ expectedMatch: /Container<\/td> m4a<\/td>.*MIME<\/td> audio\/mp4<\/td>.*Brand<\/td> M4A <\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.m4a", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: M4A top-level atoms (ftyp, mdat)",
+ input: M4A_HEX,
+ expectedMatch: /MP4 Top-Level Atoms.*ftyp<\/td>.*mdat<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.m4a", 524288] }
+ ]
+ },
+
+ // ---- AIFF ----
+ {
+ name: "Extract Audio Metadata: AIFF container, MIME and brand",
+ input: AIFF_HEX,
+ expectedMatch: /Container<\/td> aiff<\/td>.*MIME<\/td> audio\/aiff<\/td>.*Brand<\/td> AIFF<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.aiff", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: AIFF common tag (title from NAME chunk)",
+ input: AIFF_HEX,
+ expectedMatch: /Title<\/td> Galway<\/td>/,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.aiff", 524288] }
+ ]
+ },
+ {
+ name: "Extract Audio Metadata: AIFF chunks (NAME)",
+ input: AIFF_HEX,
+ expectedMatch: /AIFF Chunks.*NAME<\/td> Galway<\/td>/s,
+ recipeConfig: [
+ { op: "From Hex", args: ["None"] },
+ { op: "Extract Audio Metadata", args: ["test.aiff", 524288] }
+ ]
+ },
+]);
diff --git a/tests/samples/Audio.mjs b/tests/samples/Audio.mjs
new file mode 100644
index 00000000..e792677e
--- /dev/null
+++ b/tests/samples/Audio.mjs
@@ -0,0 +1,73 @@
+/**
+ * Audio file headers in various formats for use in tests.
+ *
+ * Each constant contains the minimal bytes needed for container
+ * detection and metadata extraction (trimmed from real audio files).
+ *
+ * @author d0s1nt [d0s1nt@cyberchefaudio]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+/**
+ * MP3 with ID3v2.4 header — title: Galway, artist: Kevin MacLeod
+ * 78 bytes: ID3v2 header + TIT2 + TPE1 + TSSE frames
+ */
+export const MP3_HEX = "4944330400000000004e544954320000000800000347616c77617900545045310000000f0000034b6576696e204d61634c656f6400545353450000000f0000034c61766635362e34302e3130310000000000000000000000";
+
+/**
+ * WAV (RIFF/WAVE) header — 2 channels, 16-bit, 44100 Hz
+ * 48 bytes: RIFF header + fmt chunk + data chunk start
+ */
+export const WAV_HEX = "52494646e69d2a0057415645666d7420100000000100020044ac000010b102000400100064617461489d2a0000000100";
+
+/**
+ * FLAC with streaminfo + Vorbis comment block — title: Galway, artist: Kevin MacLeod
+ * 174 bytes: fLaC magic + STREAMINFO (34 bytes) + VORBIS_COMMENT block (86 bytes)
+ */
+export const FLAC_HEX = "664c6143000000221200120000052e00319f0ac442f0000aa752c925754a50e5f02e117eeb46467e7053040000560d0000004c61766635362e34302e313031030000000c0000007469746c653d47616c776179140000006172746973743d4b6576696e204d61634c656f6415000000656e636f6465723d4c61766635362e34302e313031";
+
+/**
+ * AAC ADTS frame header — MPEG-4, LC profile, 44100 Hz, stereo
+ * 32 bytes: ADTS sync + frame header fields
+ */
+export const AAC_HEX = "fff150800bbffcde02004c61766335382e33342e31303000423590002000001e";
+
+/**
+ * AC3 (Dolby Digital) sync frame header — 44100 Hz, 192 kbps, 2.0 stereo
+ * 32 bytes: AC3 sync word + BSI fields
+ */
+export const AC3_HEX = "0b773968544043e106f575f0d4da1c1ac159850953e549a125736e8d37359d3f";
+
+/**
+ * OGG Vorbis — two OGG pages with identification + comment headers
+ * title: Galway, artist: Kevin MacLeod, vendor: Lavf56.40.101
+ * 281 bytes
+ */
+export const OGG_HEX = "4f6767530002000000000000000027a7032a000000002acbc833011e01766f72626973000000000244ac00000000000080b5010000000000b8014f6767530000000000000000000027a7032a010000007e1abea41168ffffffffffffffffffffffffffffff0703766f726269730d0000004c61766635362e34302e313031030000001f000000656e636f6465723d4c61766335362e36302e313030206c6962766f726269730c0000007469746c653d47616c776179140000006172746973743d4b6576696e204d61634c656f64";
+
+/**
+ * Opus — two OGG pages with OpusHead + OpusTags headers
+ * title: Galway, artist: Kevin MacLeod, vendor: Lavf58.19.102
+ * 233 bytes
+ */
+export const OPUS_HEX = "4f67675300020000000000000000919a59f200000000f6117eb601134f707573486561640102380180bb00000000004f67675300000000000000000000919a59f201000000b047e56601664f707573546167730d0000004c61766635382e31392e313032030000001d000000656e636f6465723d4c61766335382e33342e313030206c69626f7075730c0000007469746c653d47616c776179140000006172746973743d4b6576696e204d61634c656f64";
+
+/**
+ * WMA/ASF — ASF header with Content Description + Extended Content
+ * title: Galway, author: Kevin MacLeod, encoder: Lavf56.40.101
+ * 700 bytes: ASF Header Object + all sub-objects
+ */
+export const WMA_HEX = "3026b2758e66cf11a6d900aa0062ce6c8a02000000000000060000000102a1dcab8c47a9cf118ee400c00c205365680000000000000000000000000000000000000000000000bc3504000000000000803ed5deb19d0156000000000000007040490b00000000b03a7009000000001c0c00000000000002000000800c0000800c000000f40100b503bf5f2ea9cf118ee300c00c2053652e0000000000000011d2d3abbaa9cf118ee600c00c2053650600000000003326b2758e66cf11a6d900aa0062ce6c4c000000000000000e001c00000000000000470061006c0077006100790000004b006500760069006e0020004d00610063004c0065006f006400000040a4d0d207e3d21197f000a0c95ea850b40000000000000003000c007400690074006c006500000000000e00470061006c0077006100790000000e0041007500740068006f007200000000001c004b006500760069006e0020004d00610063004c0065006f0064000000280057004d002f0045006e0063006f00640069006e006700530065007400740069006e0067007300000000001c004c00610076006600350036002e00340030002e0031003000310000009107dcb7b7a9cf118ee600c00c2053657200000000000000409e69f84d5bcf11a8fd00805f5c442b50cdc3bf8f61cf118bb200aa00b4e22000000000000000001c000000080000000100000000006101020044ac0000803e0000e70210000a000000000001000000000001e702e7020100004052d1861d31d011a3a400a0c90348f664000000000000004152d1861d31d011a3a400a0c90348f60100000002001700570069006e0064006f007700730020004d006500640069006100200041007500640069006f0020005600380000000000020061013626b2758e66cf11a6d900aa0062ce6c32330400000000000000000000000000000000000000000056000000000000000101";
+
+/**
+ * M4A (MPEG-4 Audio) — ftyp atom with brand "M4A ", plus mdat
+ * 512 bytes: ftyp + free + mdat start (moov not included in slice)
+ */
+export const M4A_HEX = "0000001c667479704d344120000002004d34412069736f6d69736f3200000008667265650003e2236d6461742111450014500146fff10a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5de98214b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4bc211a93a09c3e310803595989841e21e02814c4d2f28f925da49e5fe61d4521f0088400d65662610788780a0563ab2a671af1d0cd2fd9997d18be037ff8852d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d";
+
+/**
+ * AIFF (FORM/AIFF) header with NAME chunk = "Galway", COMM and SSND chunks
+ * 36 bytes: FORM header + NAME chunk + COMM chunk start
+ */
+export const AIFF_HEX = "464f524d002a9d84414946464e414d450000000647616c776179434f4d4d00000012000200";
From 607acbd24e6465cb6d6b725714c40524f3d4dc98 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 19 Mar 2026 12:20:03 +0000
Subject: [PATCH 15/60] chore (deps): bump the patch-updates group with 6
updates (#2260)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 48 +++++++++++++++++++++++------------------------
package.json | 10 +++++-----
2 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b4b293e2..4035b7ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -73,7 +73,7 @@
"lz4js": "^0.2.0",
"markdown-it": "^14.1.1",
"moment": "^2.30.1",
- "moment-timezone": "^0.6.0",
+ "moment-timezone": "^0.6.1",
"ngeohash": "^0.6.3",
"node-forge": "^1.3.3",
"node-md6": "^0.1.0",
@@ -111,15 +111,15 @@
"@babel/eslint-parser": "^7.28.6",
"@babel/plugin-syntax-import-assertions": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
- "@babel/preset-env": "^7.29.0",
+ "@babel/preset-env": "^7.29.2",
"@babel/runtime": "^7.28.6",
- "@codemirror/commands": "^6.10.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",
"autoprefixer": "^10.4.27",
- "babel-loader": "^10.0.0",
+ "babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^130.0.4",
"cli-progress": "^3.12.0",
@@ -154,7 +154,7 @@
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
- "terser": "^5.46.0",
+ "terser": "^5.46.1",
"webpack": "^5.105.4",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
@@ -1640,9 +1640,9 @@
}
},
"node_modules/@babel/preset-env": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz",
- "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz",
+ "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1840,14 +1840,14 @@
}
},
"node_modules/@codemirror/commands": {
- "version": "6.10.2",
- "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz",
- "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==",
+ "version": "6.10.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
+ "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
- "@codemirror/state": "^6.4.0",
+ "@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
@@ -1880,9 +1880,9 @@
}
},
"node_modules/@codemirror/state": {
- "version": "6.5.4",
- "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
- "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
+ "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5181,9 +5181,9 @@
}
},
"node_modules/babel-loader": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.0.tgz",
- "integrity": "sha512-5HTUZa013O4SWEYlJDHexrqSIYkWatfA9w/ZZQa7V2nMc0dRWkfu/0pmioC7XMYm8M7Z/3+q42NWj6e+fAT0MQ==",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz",
+ "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13300,9 +13300,9 @@
}
},
"node_modules/moment-timezone": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.0.tgz",
- "integrity": "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==",
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.1.tgz",
+ "integrity": "sha512-1B9lmAhB9D9/sHaPC1N7wLFEVUoFldxOpOO96lOD1PvJ43vCd0ozDPbu0FEL3++VvawOlDkq8YD373tJmP5JHw==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
@@ -17033,9 +17033,9 @@
"license": "MIT"
},
"node_modules/terser": {
- "version": "5.46.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
- "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
+ "version": "5.46.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
+ "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
diff --git a/package.json b/package.json
index 2948556e..868389b7 100644
--- a/package.json
+++ b/package.json
@@ -42,15 +42,15 @@
"@babel/eslint-parser": "^7.28.6",
"@babel/plugin-syntax-import-assertions": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
- "@babel/preset-env": "^7.29.0",
+ "@babel/preset-env": "^7.29.2",
"@babel/runtime": "^7.28.6",
- "@codemirror/commands": "^6.10.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",
"autoprefixer": "^10.4.27",
- "babel-loader": "^10.0.0",
+ "babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^130.0.4",
"cli-progress": "^3.12.0",
@@ -85,7 +85,7 @@
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
- "terser": "^5.46.0",
+ "terser": "^5.46.1",
"webpack": "^5.105.4",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
@@ -156,7 +156,7 @@
"lz4js": "^0.2.0",
"markdown-it": "^14.1.1",
"moment": "^2.30.1",
- "moment-timezone": "^0.6.0",
+ "moment-timezone": "^0.6.1",
"ngeohash": "^0.6.3",
"node-forge": "^1.3.3",
"node-md6": "^0.1.0",
From 7f4f90e4f3c46180cfdf6d124040bb9f1d228337 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 20 Mar 2026 08:44:28 +0000
Subject: [PATCH 16/60] chore (deps): bump core-js from 3.48.0 to 3.49.0
(#2261)
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 4035b7ff..67f2ef00 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -126,7 +126,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",
@@ -6813,9 +6813,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",
diff --git a/package.json b/package.json
index 868389b7..0c7b2160 100644
--- a/package.json
+++ b/package.json
@@ -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",
From 290f824e18f16db2efab1b533c0914ee730b77bd Mon Sep 17 00:00:00 2001
From: Roman Karwacik <108284286+rtpt-romankarwacik@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:26:39 +0100
Subject: [PATCH 17/60] feat: add Raw option for Jq operation (#2237)
---
src/core/operations/Jq.mjs | 16 ++++++++++++----
tests/operations/tests/Jq.mjs | 32 ++++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+), 4 deletions(-)
create mode 100644 tests/operations/tests/Jq.mjs
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/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],
+ },
+ ],
+ },
+]);
From 32ff3cd55ebbc9fd0ed2bce9887debd59c71f7fe Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Fri, 20 Mar 2026 11:00:53 +0000
Subject: [PATCH 18/60] Bump flatted from 3.3.2 to 3.4.2 (#2266)
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 67f2ef00..b03ab1f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9627,9 +9627,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"
},
From a1f9208221a6f7df9eff08dfc612a3ff37997dfc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 20 Mar 2026 11:32:50 +0000
Subject: [PATCH 19/60] chore (deps): bump @codemirror/view from 6.39.17 to
6.40.0 (#2262)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 10 +++++-----
package.json | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b03ab1f1..62705eb7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -117,7 +117,7 @@
"@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",
@@ -1890,13 +1890,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"
diff --git a/package.json b/package.json
index 0c7b2160..46464298 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@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",
From 8c4a55b2b18cc13205e03420e1f0bd4c0545c745 Mon Sep 17 00:00:00 2001
From: Cherry <35111165+Lamby777@users.noreply.github.com>
Date: Fri, 20 Mar 2026 13:03:45 -0400
Subject: [PATCH 20/60] Add more helpful error for when numerical ingredient is
left empty (#1540)
Co-authored-by: GCHQ Developer C85297 <95289555+C85297@users.noreply.github.com>
---
src/core/Chef.mjs | 9 ++++++++-
src/core/Ingredient.mjs | 7 +++++--
src/core/Operation.mjs | 7 ++++++-
src/core/Recipe.mjs | 14 +++++++++-----
4 files changed, 28 insertions(+), 9 deletions(-)
mode change 100755 => 100644 src/core/Ingredient.mjs
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}`);
+ }
}
});
}
From 38a0adaf33194644575af7b719c135d15f84a819 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 21 Mar 2026 15:23:42 +0000
Subject: [PATCH 21/60] chore (deps): bump @babel/runtime from 7.28.6 to 7.29.2
(#2263)
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 62705eb7..607aeca3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -112,7 +112,7 @@
"@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",
@@ -1754,9 +1754,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": {
diff --git a/package.json b/package.json
index 46464298..4c188560 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"@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",
From 2b370b9616f14eacee5db15096dd69445ed0ef84 Mon Sep 17 00:00:00 2001
From: j264415 <128609898+j264415@users.noreply.github.com>
Date: Sat, 21 Mar 2026 17:49:13 +0000
Subject: [PATCH 22/60] Selection and Deselection of autobake checkbox using
keyboard (#1727)
---
src/web/Manager.mjs | 1 +
src/web/waiters/ControlsWaiter.mjs | 12 ++++++++++++
2 files changed, 13 insertions(+)
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/waiters/ControlsWaiter.mjs b/src/web/waiters/ControlsWaiter.mjs
index b57940a3..d281fb89 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.
From 78d40eab60a46066e1d6bf5e50e7afa9432c162d Mon Sep 17 00:00:00 2001
From: Ted Kruijff
Date: Sat, 21 Mar 2026 19:15:29 +0100
Subject: [PATCH 23/60] Add Parse Ethernet frame Operation, allow Parse IPv4
Header to cascade (#1722)
---
src/core/config/Categories.json | 1 +
src/core/operations/ParseEthernetFrame.mjs | 115 ++++++++++++++++++
src/core/operations/ParseIPv4Header.mjs | 29 ++++-
tests/browser/02_ops.js | 2 +-
tests/operations/index.mjs | 1 +
tests/operations/tests/ParseEthernetFrame.mjs | 45 +++++++
6 files changed, 186 insertions(+), 7 deletions(-)
create mode 100644 src/core/operations/ParseEthernetFrame.mjs
create mode 100644 tests/operations/tests/ParseEthernetFrame.mjs
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 88cb6dc1..a2bd2d08 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/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/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 2134cdd9..f030349d 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/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"]
+ }
+ ]
+ }
+]);
From 9cf82cc1a10f30146a85cf8aae8fb648831cd651 Mon Sep 17 00:00:00 2001
From: j264415 <128609898+j264415@users.noreply.github.com>
Date: Sat, 21 Mar 2026 20:28:59 +0000
Subject: [PATCH 24/60] Added tab focus to top banner and navigation to
About/Support Modal (#1733)
---
src/web/html/index.html | 12 ++---
src/web/stylesheets/layout/_banner.css | 30 +++++++++++
src/web/stylesheets/layout/_modals.css | 5 ++
src/web/waiters/ControlsWaiter.mjs | 70 ++++++++++++++++++++++++++
4 files changed, 111 insertions(+), 6 deletions(-)
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 @@
star
-
+
@@ -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 d281fb89..7f2bb683 100755
--- a/src/web/waiters/ControlsWaiter.mjs
+++ b/src/web/waiters/ControlsWaiter.mjs
@@ -399,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/");
@@ -415,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.
From ff63ec97b75706b7e12442c7c814e96c89f2db55 Mon Sep 17 00:00:00 2001
From: Sascha Buehrle <47737812+saschabuehrle@users.noreply.github.com>
Date: Mon, 23 Mar 2026 13:26:09 +0100
Subject: [PATCH 25/60] fix: return empty output for zero-length To Modhex
input (#2249)
---
src/core/lib/Modhex.mjs | 2 ++
tests/operations/tests/Modhex.mjs | 20 ++++++++++++++++++++
2 files changed, 22 insertions(+)
diff --git a/src/core/lib/Modhex.mjs b/src/core/lib/Modhex.mjs
index 4f28e9a1..ab4a7c8b 100644
--- a/src/core/lib/Modhex.mjs
+++ b/src/core/lib/Modhex.mjs
@@ -50,6 +50,7 @@ const HEX_ALPHABET_MAP = HEX_ALPHABET.split("");
export function toModhex(data, delim=" ", padding=2, extraDelim="", lineSize=0) {
if (!data) return "";
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
+ if (data.length === 0) return "";
const regularHexString = toHex(data, "", padding, "", 0);
@@ -100,6 +101,7 @@ export function toModhex(data, delim=" ", padding=2, extraDelim="", lineSize=0)
export function toModhexFast(data) {
if (!data) return "";
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
+ if (data.length === 0) return "";
const output = [];
diff --git a/tests/operations/tests/Modhex.mjs b/tests/operations/tests/Modhex.mjs
index 1e0f2791..07f38910 100644
--- a/tests/operations/tests/Modhex.mjs
+++ b/tests/operations/tests/Modhex.mjs
@@ -147,4 +147,24 @@ dc;ii;hv;ig;hr;hf;dc;he;hj;hv;hv;ie;hg;du",
}
]
},
+ {
+ name: "Empty input through From Hex and To Modhex returns empty output",
+ input: "",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Hex",
+ "args": [
+ "Auto"
+ ]
+ },
+ {
+ "op": "To Modhex",
+ "args": [
+ "Space",
+ 0
+ ]
+ }
+ ]
+ },
]);
From b0fa1f8d1bebc855d6b38552e86e19115ee1e33f Mon Sep 17 00:00:00 2001
From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com>
Date: Mon, 23 Mar 2026 14:01:42 +0000
Subject: [PATCH 26/60] Add pull request template with AI usage disclosure
(#2279)
---
.github/ISSUE_TEMPLATE.md | 1 -
.github/ISSUE_TEMPLATE/operation-request.md | 12 +++++++++---
.github/pull_request_template.md | 15 +++++++++++++++
.github/CONTRIBUTING.md => CONTRIBUTING.md | 0
4 files changed, 24 insertions(+), 4 deletions(-)
delete mode 100644 .github/ISSUE_TEMPLATE.md
create mode 100644 .github/pull_request_template.md
rename .github/CONTRIBUTING.md => CONTRIBUTING.md (100%)
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index e90ab51f..00000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/.github/ISSUE_TEMPLATE/operation-request.md b/.github/ISSUE_TEMPLATE/operation-request.md
index d88e6703..05231a86 100644
--- a/.github/ISSUE_TEMPLATE/operation-request.md
+++ b/.github/ISSUE_TEMPLATE/operation-request.md
@@ -7,8 +7,14 @@ assignees: ''
---
-## Summary
+**Is your operation request related to a problem? Please describe.**
+A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
-### Example Input
+**Describe the solution you'd like**
+A clear and concise description of the new operation you would like.
-### Example Output
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Example input and output**
+Provide an example input to the operation, along with the output that you would expect.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..8d6cc970
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,15 @@
+**Description**
+Provide a description of the pull request and the changes that it makes.
+
+**Existing Issue**
+If this pull request relates to an existing issue in the repository, please link it here.
+
+**Screenshots**
+If the pull request changes any visual aspects of CyberChef, please include screenshots.
+
+**AI disclosure**
+If you have used any AI tools while creating this code, **you must declare your usage along with the name of the tools that you used**.
+Regardless of AI tool usage, you are responsible for any code that you submit, and we expect you to have checked the code and have enough of an understanding of it to answer any questions we might have.
+
+**Test Coverage**
+Please ensure you have added test coverage for your changes.
diff --git a/.github/CONTRIBUTING.md b/CONTRIBUTING.md
similarity index 100%
rename from .github/CONTRIBUTING.md
rename to CONTRIBUTING.md
From 6aa98b4a66a038c520c4c37748efb1ecae070a44 Mon Sep 17 00:00:00 2001
From: Ted Kruijff
Date: Thu, 26 Mar 2026 17:33:12 +0100
Subject: [PATCH 27/60] ParseEthernetFrame - Fix vlan calculation (#2295)
---
src/core/operations/ParseEthernetFrame.mjs | 9 +++------
tests/operations/tests/ParseEthernetFrame.mjs | 4 ++--
2 files changed, 5 insertions(+), 8 deletions(-)
diff --git a/src/core/operations/ParseEthernetFrame.mjs b/src/core/operations/ParseEthernetFrame.mjs
index 9dac5d57..0be5e1a4 100644
--- a/src/core/operations/ParseEthernetFrame.mjs
+++ b/src/core/operations/ParseEthernetFrame.mjs
@@ -74,17 +74,14 @@ class ParseEthernetFrame extends Operation {
const ethType = Utils.byteArrayToChars(input.slice(offset, offset+2));
offset += 2;
-
- if (ethType === "\x08\x00") {
- break;
- } else if (ethType === "\x81\x00" || ethType === "\x88\xA8") {
+ 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]);
+ const vlanTag = input.slice(offset, offset+2);
+ vlans.push(((vlanTag[0] & 0b00001111) << 8) | vlanTag[1]);
offset += 2;
} else {
diff --git a/tests/operations/tests/ParseEthernetFrame.mjs b/tests/operations/tests/ParseEthernetFrame.mjs
index c849e207..063255ed 100644
--- a/tests/operations/tests/ParseEthernetFrame.mjs
+++ b/tests/operations/tests/ParseEthernetFrame.mjs
@@ -23,7 +23,7 @@ TestRegister.addTests([
{
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",
+ expectedOutput: "Source MAC: 00:13:c3:df:ae:18\nDestination MAC: 01:00:0c:cd:cd:d0\nVLAN: 118\nData:\naa aa",
recipeConfig: [
{
"op": "Parse Ethernet frame",
@@ -34,7 +34,7 @@ TestRegister.addTests([
{
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",
+ expectedOutput: "Source MAC: 00:21:55:c8:f1:3c\nDestination MAC: 00:19:aa:7d:e6:88\nVLAN: 209, 20\nData:\n45 00",
recipeConfig: [
{
"op": "Parse Ethernet frame",
From 088da9de0169d3361a86d67756413e63a5c256f8 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Thu, 26 Mar 2026 16:38:33 +0000
Subject: [PATCH 28/60] chore (deps) bump chromedriver from 130.0.4 to 146.0.6
(#2292)
---
package-lock.json | 26 ++++++++++++++++++--------
package.json | 2 +-
2 files changed, 19 insertions(+), 9 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 607aeca3..e4fce8f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -121,7 +121,7 @@
"autoprefixer": "^10.4.27",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
- "chromedriver": "^130.0.4",
+ "chromedriver": "^146.0.6",
"cli-progress": "^3.12.0",
"colors": "^1.4.0",
"compression-webpack-plugin": "^11.1.0",
@@ -6246,26 +6246,36 @@
}
},
"node_modules/chromedriver": {
- "version": "130.0.4",
- "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-130.0.4.tgz",
- "integrity": "sha512-lpR+PWXszij1k4Ig3t338Zvll9HtCTiwoLM7n4pCCswALHxzmgwaaIFBh3rt9+5wRk9D07oFblrazrBxwaYYAQ==",
+ "version": "146.0.6",
+ "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-146.0.6.tgz",
+ "integrity": "sha512-FIRi3hy0nRiyirK03etVXEpYTIodevFcvTBAM5ZCq+pX3w31jLm6JE8BVW1ypAVLvSp6HJDvboCcdgUroS3miw==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@testim/chrome-version": "^1.1.4",
- "axios": "^1.7.4",
+ "axios": "^1.13.5",
"compare-versions": "^6.1.0",
"extract-zip": "^2.0.1",
- "proxy-agent": "^6.4.0",
- "proxy-from-env": "^1.1.0",
+ "proxy-agent": "^6.5.0",
+ "proxy-from-env": "^2.0.0",
"tcp-port-used": "^1.0.2"
},
"bin": {
"chromedriver": "bin/chromedriver"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
+ }
+ },
+ "node_modules/chromedriver/node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/ci-info": {
diff --git a/package.json b/package.json
index 4c188560..ad45cf74 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
"autoprefixer": "^10.4.27",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
- "chromedriver": "^130.0.4",
+ "chromedriver": "^146.0.6",
"cli-progress": "^3.12.0",
"colors": "^1.4.0",
"compression-webpack-plugin": "^11.1.0",
From f9184d39385bc3058dbd49a44c1d1d008ec8fd7d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 11:12:29 +0000
Subject: [PATCH 29/60] chore (deps): bump the patch-updates group with 3
updates (#2296)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> (add browser test for Template operation)
---
package-lock.json | 24 ++++++++++++------------
package.json | 6 +++---
tests/browser/02_ops.js | 1 +
3 files changed, 16 insertions(+), 15 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index e4fce8f7..54bb921d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -48,7 +48,7 @@
"file-saver": "^2.0.5",
"flat": "^6.0.1",
"geodesy": "1.1.3",
- "handlebars": "^4.7.8",
+ "handlebars": "^4.7.9",
"hash-wasm": "^4.12.0",
"highlight.js": "^11.11.1",
"ieee754": "^1.2.1",
@@ -114,7 +114,7 @@
"@babel/preset-env": "^7.29.2",
"@babel/runtime": "^7.29.2",
"@codemirror/commands": "^6.10.3",
- "@codemirror/language": "^6.12.2",
+ "@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.40.0",
@@ -145,7 +145,7 @@
"grunt-zip": "^1.0.0",
"html-webpack-plugin": "^5.6.6",
"imports-loader": "^5.0.0",
- "mini-css-extract-plugin": "2.10.1",
+ "mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
"nightwatch": "^3.15.0",
"postcss": "^8.5.8",
@@ -1853,9 +1853,9 @@
}
},
"node_modules/@codemirror/language": {
- "version": "6.12.2",
- "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
- "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
+ "version": "6.12.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
+ "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10638,9 +10638,9 @@
"license": "MIT"
},
"node_modules/handlebars": {
- "version": "4.7.8",
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
- "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+ "version": "4.7.9",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+ "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
@@ -13113,9 +13113,9 @@
}
},
"node_modules/mini-css-extract-plugin": {
- "version": "2.10.1",
- "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.1.tgz",
- "integrity": "sha512-k7G3Y5QOegl380tXmZ68foBRRjE9Ljavx835ObdvmZjQ639izvZD8CS7BkWw1qKPPzHsGL/JDhl0uyU1zc2rJw==",
+ "version": "2.10.2",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz",
+ "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index ad45cf74..d69a8495 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
"@babel/preset-env": "^7.29.2",
"@babel/runtime": "^7.29.2",
"@codemirror/commands": "^6.10.3",
- "@codemirror/language": "^6.12.2",
+ "@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.40.0",
@@ -76,7 +76,7 @@
"grunt-zip": "^1.0.0",
"html-webpack-plugin": "^5.6.6",
"imports-loader": "^5.0.0",
- "mini-css-extract-plugin": "2.10.1",
+ "mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
"nightwatch": "^3.15.0",
"postcss": "^8.5.8",
@@ -131,7 +131,7 @@
"file-saver": "^2.0.5",
"flat": "^6.0.1",
"geodesy": "1.1.3",
- "handlebars": "^4.7.8",
+ "handlebars": "^4.7.9",
"hash-wasm": "^4.12.0",
"highlight.js": "^11.11.1",
"ieee754": "^1.2.1",
diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js
index 5ab55451..896de7b0 100644
--- a/tests/browser/02_ops.js
+++ b/tests/browser/02_ops.js
@@ -354,6 +354,7 @@ module.exports = {
// testOp(browser, "Tail", "test input", "test_output");
// testOp(browser, "Take bytes", "test input", "test_output");
testOp(browser, "Tar", "test input", /^file\.txt\x00{92}/);
+ testOp(browser, "Template", "{\"one\": 1, \"two\": 2}", "1 2", ["{{ one }} {{ two }}"]);
testOpHtml(browser, "Text Encoding Brute Force", "test input", "tr:nth-of-type(4) td:last-child", /t\u2400e\u2400s\u2400t\u2400/);
// testOp(browser, "To BCD", "test input", "test_output");
// testOp(browser, "To Base", "test input", "test_output");
From c00824a89ab3b0b57831793d01886b718783ac5c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 11:29:35 +0000
Subject: [PATCH 30/60] chore (deps): bump node-forge from 1.3.3 to 1.4.0
(#2297)
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 54bb921d..52f80a17 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -75,7 +75,7 @@
"moment": "^2.30.1",
"moment-timezone": "^0.6.1",
"ngeohash": "^0.6.3",
- "node-forge": "^1.3.3",
+ "node-forge": "^1.4.0",
"node-md6": "^0.1.0",
"nodom": "^2.4.0",
"notepack.io": "^3.0.1",
@@ -13778,9 +13778,9 @@
}
},
"node_modules/node-forge": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
- "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
+ "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
"node": ">= 6.13.0"
diff --git a/package.json b/package.json
index d69a8495..3dbf2c8f 100644
--- a/package.json
+++ b/package.json
@@ -158,7 +158,7 @@
"moment": "^2.30.1",
"moment-timezone": "^0.6.1",
"ngeohash": "^0.6.3",
- "node-forge": "^1.3.3",
+ "node-forge": "^1.4.0",
"node-md6": "^0.1.0",
"nodom": "^2.4.0",
"notepack.io": "^3.0.1",
From 80286f1e6ff6716b828fa02f729c8ed175f69553 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 11:59:44 +0000
Subject: [PATCH 31/60] chore (deps): bump picomatch (#2299)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 52f80a17..ed4d1c8e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7137,9 +7137,9 @@
}
},
"node_modules/cspell-glob/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14688,9 +14688,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -17224,9 +17224,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
From 7d9fd4b26f4d9ff86e0d5b799e7fac706695dfc5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 1 Apr 2026 11:21:55 +0100
Subject: [PATCH 32/60] chore (deps): bump @xmldom/xmldom from 0.8.11 to 0.8.12
(#2302)
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 ed4d1c8e..a614b961 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.11",
+ "@xmldom/xmldom": "^0.8.12",
"argon2-browser": "^1.18.0",
"arrive": "^2.5.2",
"assert": "^2.1.0",
@@ -4524,9 +4524,9 @@
}
},
"node_modules/@xmldom/xmldom": {
- "version": "0.8.11",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
- "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "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==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/package.json b/package.json
index 3dbf2c8f..7a5d7f05 100644
--- a/package.json
+++ b/package.json
@@ -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.11",
+ "@xmldom/xmldom": "^0.8.12",
"argon2-browser": "^1.18.0",
"arrive": "^2.5.2",
"assert": "^2.1.0",
From eea8bcd2500278fb91f20103a1821abd59cc5c79 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 4 Apr 2026 11:09:50 +0100
Subject: [PATCH 33/60] chore (deps): bump the patch-updates group with 2
updates (#2303)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 16 ++++++++--------
package.json | 4 ++--
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index a614b961..d87442a0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
"@xmldom/xmldom": "^0.8.12",
"argon2-browser": "^1.18.0",
- "arrive": "^2.5.2",
+ "arrive": "^2.5.3",
"assert": "^2.1.0",
"avsc": "^5.7.9",
"bcryptjs": "^2.4.3",
@@ -93,7 +93,7 @@
"snackbarjs": "^1.1.0",
"sortablejs": "^1.15.7",
"split.js": "^1.6.5",
- "sql-formatter": "^15.6.12",
+ "sql-formatter": "^15.7.3",
"ssdeep.js": "0.0.3",
"stream-browserify": "^3.0.0",
"tesseract.js": "^6.0.1",
@@ -5003,9 +5003,9 @@
}
},
"node_modules/arrive": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/arrive/-/arrive-2.5.2.tgz",
- "integrity": "sha512-A9asXhuUR6pgHtwUciZw4FRZDR09ZwVsz1ckEGE6W9gXWrYM9cAkuKzHplpYN0bd/iobg0xqQ9fpRr/GHPsXUw==",
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/arrive/-/arrive-2.5.3.tgz",
+ "integrity": "sha512-FsZjDDxS2BZ1TuGvqhTf8KbGaH0hOx+DS7HhYIvsreCkAuSUFgJ9NUhIC4f3SM3c5T/1he1KAn566GZjzqJarA==",
"license": "MIT"
},
"node_modules/asn1.js": {
@@ -16633,9 +16633,9 @@
"license": "BSD-3-Clause"
},
"node_modules/sql-formatter": {
- "version": "15.7.2",
- "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.7.2.tgz",
- "integrity": "sha512-b0BGoM81KFRVSpZFwPpIPU5gng4YD8DI/taLD96NXCFRf5af3FzSE4aSwjKmxcyTmf/MfPu91j75883nRrWDBw==",
+ "version": "15.7.3",
+ "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.7.3.tgz",
+ "integrity": "sha512-5+zl9Nqg5aNjss0tb1G+StpC4dJKbjv3+g8CL/+V+00PfZop+2RKGyi53ScFl0dr+Dkx1LjmUO54Q3N7K3EtMw==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
diff --git a/package.json b/package.json
index 7a5d7f05..0774cd91 100644
--- a/package.json
+++ b/package.json
@@ -99,7 +99,7 @@
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
"@xmldom/xmldom": "^0.8.12",
"argon2-browser": "^1.18.0",
- "arrive": "^2.5.2",
+ "arrive": "^2.5.3",
"assert": "^2.1.0",
"avsc": "^5.7.9",
"bcryptjs": "^2.4.3",
@@ -176,7 +176,7 @@
"snackbarjs": "^1.1.0",
"sortablejs": "^1.15.7",
"split.js": "^1.6.5",
- "sql-formatter": "^15.6.12",
+ "sql-formatter": "^15.7.3",
"ssdeep.js": "0.0.3",
"stream-browserify": "^3.0.0",
"tesseract.js": "^6.0.1",
From e1352065d83b0ec8c46b326336888ddfb8894aad Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 4 Apr 2026 11:14:25 +0100
Subject: [PATCH 34/60] chore (deps): bump @codemirror/view from 6.40.0 to
6.41.0 (#2305)
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 d87442a0..fe5ea29d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -117,7 +117,7 @@
"@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
- "@codemirror/view": "^6.40.0",
+ "@codemirror/view": "^6.41.0",
"autoprefixer": "^10.4.27",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
@@ -1890,9 +1890,9 @@
}
},
"node_modules/@codemirror/view": {
- "version": "6.40.0",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz",
- "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==",
+ "version": "6.41.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz",
+ "integrity": "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 0774cd91..a2273fa3 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
- "@codemirror/view": "^6.40.0",
+ "@codemirror/view": "^6.41.0",
"autoprefixer": "^10.4.27",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
From 33314ac6585053cf9c4cfbf6eb0b38184ab40f5d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 4 Apr 2026 11:21:45 +0100
Subject: [PATCH 35/60] chore (deps): bump lodash from 4.17.23 to 4.18.1
(#2304)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 29 +++++++++++++++++++++++++----
package.json | 2 +-
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index fe5ea29d..f4045c83 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -66,7 +66,7 @@
"kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
- "lodash": "^4.17.23",
+ "lodash": "^4.18.1",
"loglevel": "^1.9.2",
"loglevel-message-prefix": "^3.0.0",
"lz-string": "^1.5.0",
@@ -10482,6 +10482,13 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/grunt-legacy-log-utils/node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/grunt-legacy-log-utils/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -10505,6 +10512,13 @@
"node": ">=0.1.90"
}
},
+ "node_modules/grunt-legacy-log/node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/grunt-legacy-util": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz",
@@ -10524,6 +10538,13 @@
"node": ">=10"
}
},
+ "node_modules/grunt-legacy-util/node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/grunt-retro": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/grunt-retro/-/grunt-retro-0.6.4.tgz",
@@ -12676,9 +12697,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
diff --git a/package.json b/package.json
index a2273fa3..7972c053 100644
--- a/package.json
+++ b/package.json
@@ -149,7 +149,7 @@
"kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
- "lodash": "^4.17.23",
+ "lodash": "^4.18.1",
"loglevel": "^1.9.2",
"loglevel-message-prefix": "^3.0.0",
"lz-string": "^1.5.0",
From 167cc398ce56d2d32ce3af09a311fc919644d478 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Sat, 4 Apr 2026 12:13:05 +0100
Subject: [PATCH 36/60] Properly escape HTML entities in sampleDelim to avoid
XSS issue (#2307)
---
src/core/operations/OffsetChecker.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/operations/OffsetChecker.mjs b/src/core/operations/OffsetChecker.mjs
index 0f66e591..2a417e4b 100644
--- a/src/core/operations/OffsetChecker.mjs
+++ b/src/core/operations/OffsetChecker.mjs
@@ -99,7 +99,7 @@ class OffsetChecker extends Operation {
}
}
- return outputs.join(sampleDelim);
+ return outputs.join(Utils.escapeHtml(sampleDelim));
}
}
From 42adf5b1034829a552f230ea01dc57151b2bb892 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Tue, 7 Apr 2026 06:11:44 +0100
Subject: [PATCH 37/60] Bump v10.23.0 (#2310)
---
CHANGELOG.md | 131 ++++++++++++++++++++++++++++++++++++++++++++++
package-lock.json | 4 +-
package.json | 2 +-
3 files changed, 134 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 960e922b..4f9d7969 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,64 @@ All major and minor version changes will be documented in this file. Details of
## Details
+### [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]
+- chore (deps): bump @codemirror/view from 6.40.0 to 6.41.0 | [#2305]
+- chore (deps): bump the patch-updates group with 2 updates | [#2303]
+- chore (deps): bump @xmldom/xmldom from 0.8.11 to 0.8.12 | [#2302]
+- chore (deps): bump picomatch | [#2299]
+- chore (deps): bump node-forge from 1.3.3 to 1.4.0 | [#2297]
+- chore (deps): bump the patch-updates group with 3 updates | [#2296]
+- chore (deps) bump chromedriver from 130.0.4 to 146.0.6 [@GCHQDeveloper581] | [#2292]
+- ParseEthernetFrame - Fix vlan calculation [@Kalkran] | [#2295]
+- Add pull request template with AI usage disclosure [@C85297] | [#2279]
+- fix: return empty output for zero-length To Modhex input [@saschabuehrle] | [#2249]
+- Added tab focus to top banner and navigation to About/Support Modal [@j264415] | [#1733]
+- Add Parse Ethernet frame Operation, allow Parse IPv4 Header to cascade [@Kalkran] | [#1722]
+- Selection and Deselection of autobake checkbox using keyboard [@j264415] | [#1727]
+- chore (deps): bump @babel/runtime from 7.28.6 to 7.29.2 | [#2263]
+- Add more helpful error for when numerical ingredient is left empty [@Lamby777] [@C85297] | [#1540]
+- chore (deps): bump @codemirror/view from 6.39.17 to 6.40.0 | [#2262]
+- Bump flatted from 3.3.2 to 3.4.2 [@GCHQDeveloper581] | [#2266]
+- feat: add Raw option for Jq operation [@rtpt-romankarwacik] | [#2237]
+- chore (deps): bump core-js from 3.48.0 to 3.49.0 | [#2261]
+- chore (deps): bump the patch-updates group with 6 updates | [#2260]
+- Add Extract Audio Metadata operation [@d0s1nt] [@GCHQDeveloper581] | [#2170]
+- Fix Jq issue [@GCHQDeveloper581] | [#2210]
+- Configure dependabot updates [@GCHQDeveloper581] | [#2259]
+- fix(A1Z26): return empty string instead of empty array for empty input [@brick-pixel] | [#2257]
+- Fix broken Docker link in README [@am-periphery] | [#2250]
+- Update some dependencies, including a number causing npm audit warnings [@GCHQDeveloper581] | [#2236]
+- Bump axios from 1.7.9 to 1.13.6 | [#2234]
+- Bump jws from 3.2.2 to 3.2.3 | [#2235]
+- Bump pbkdf2 from 3.1.2 to 3.1.5 | [#2229]
+- Bump form-data from 4.0.1 to 4.0.5 | [#2228]
+- Bump basic-ftp from 5.0.5 to 5.2.0 | [#2231]
+- feat: add ARM disassembler operation [@thomasxm] | [#2156]
+- Add Text/Integer Converter operation [@p-leriche] [@GCHQDeveloper581] | [#2213]
+- Feat/rc6 add RC6 Encrypt/Decrypt operations [@thomasxm] | [#2163]
+- [bugfix] Add Bootstrap form style for CodeMirror editor [@Swonkie] | [#2161]
+- Add Flask Session operations (Decode, Sign, Verify) [@ThePlayer372-FR] | [#2208]
+- fix: `jq-web` -> `jq-wasm`, includes `jq` version `1.8.1` [@W-Floyd] [@GCHQDeveloper581] | [#2223]
+- Bump jsonwebtoken from 8.5.1 to 9.0.0 [@GCHQDeveloper581] | [#2219]
+- Bump basic-ftp from 5.0.5 to 5.2.0 | [#2218]
+- feat: add random integer generation operation [@cktgh] | [#2151]
+- Add BigInt utility functions for number theory operations [@p-leriche] [@GCHQDeveloper581] | [#2205]
+- Improve SQL Beautify: use sql-formatter and support bind variables [@aby-jo] [@GCHQDeveloper581] | [#2071]
+- update tesseract.js to 6.0.1 [@atsiv1] | [#2133]
+- Fix hint tooltip display issues [@bartvanandel] | [#2017]
+- Simplify babel dependencies [@GCHQDeveloper581] | [#2204]
+- Dependency updates [@GCHQDeveloper581] | [#2201]
+- Fix: Move Magic checks from Escape to Unescape Unicode Characters [@fjh1997] | [#2195]
+- Paste spreadsheets as text [@C85297] | [#2200]
+- Fix Roboto Mono font [@C85297] | [#2199]
+- Fix return of buffer for PNG QR image generation [@GCHQDeveloper581] [@C85297] | [#2125]
+- Update JIMP [@C85297] | [#2171]
+- Overwrite NGINX maintainer label [@C85297] | [#2194]
+- Bump v10.22.1 [@GCHQDeveloper581] | [#2193]
+- Fix npm publish - Run "npm ci" and "npm run node" under node 18 then switch to node 24.5 [@GCHQDeveloper581] | [#2192]
+
### [10.22.0] - 2026-02-11
- Separate npm publish out into separate job and run with Node 24.5 [@GCHQDeveloper581] | [#2188]
- Fixed Percent delimiter for hex encoding [@beneri] [@C85297] | [#2137]
@@ -538,6 +596,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.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
[10.20.0]: https://github.com/gchq/CyberChef/releases/tag/v10.20.0
@@ -797,6 +856,22 @@ All major and minor version changes will be documented in this file. Details of
[@t-martine]: https://github.com/t-martine
[@wesinator]: https://github.com/wesinator
[@Raka-loah]: https://github.com/Raka-loah
+[@Kalkran]: https://github.com/Kalkran
+[@saschabuehrle]: https://github.com/saschabuehrle
+[@j264415]: https://github.com/j264415
+[@Lamby777]: https://github.com/Lamby777
+[@rtpt-romankarwacik]: https://github.com/rtpt-romankarwacik
+[@d0s1nt]: https://github.com/d0s1nt
+[@brick-pixel]: https://github.com/brick-pixel
+[@am-periphery]: https://github.com/am-periphery
+[@p-leriche]: https://github.com/p-leriche
+[@Swonkie]: https://github.com/Swonkie
+[@ThePlayer372-FR]: https://github.com/ThePlayer372-FR
+[@W-Floyd]: https://github.com/W-Floyd
+[@cktgh]: https://github.com/cktgh
+[@aby-jo]: https://github.com/aby-jo
+[@atsiv1]: https://github.com/atsiv1
+[@fjh1997]: https://github.com/fjh1997
[8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7
@@ -1010,4 +1085,60 @@ All major and minor version changes will be documented in this file. Details of
[#2183]: https://github.com/gchq/CyberChef/pull/2183
[#2182]: https://github.com/gchq/CyberChef/pull/2182
[#2181]: https://github.com/gchq/CyberChef/pull/2181
+[#2307]: https://github.com/gchq/CyberChef/pull/2307
+[#2304]: https://github.com/gchq/CyberChef/pull/2304
+[#2305]: https://github.com/gchq/CyberChef/pull/2305
+[#2303]: https://github.com/gchq/CyberChef/pull/2303
+[#2302]: https://github.com/gchq/CyberChef/pull/2302
+[#2299]: https://github.com/gchq/CyberChef/pull/2299
+[#2297]: https://github.com/gchq/CyberChef/pull/2297
+[#2296]: https://github.com/gchq/CyberChef/pull/2296
+[#2292]: https://github.com/gchq/CyberChef/pull/2292
+[#2295]: https://github.com/gchq/CyberChef/pull/2295
+[#2279]: https://github.com/gchq/CyberChef/pull/2279
+[#2249]: https://github.com/gchq/CyberChef/pull/2249
+[#1733]: https://github.com/gchq/CyberChef/pull/1733
+[#1722]: https://github.com/gchq/CyberChef/pull/1722
+[#1727]: https://github.com/gchq/CyberChef/pull/1727
+[#2263]: https://github.com/gchq/CyberChef/pull/2263
+[#1540]: https://github.com/gchq/CyberChef/pull/1540
+[#2262]: https://github.com/gchq/CyberChef/pull/2262
+[#2266]: https://github.com/gchq/CyberChef/pull/2266
+[#2237]: https://github.com/gchq/CyberChef/pull/2237
+[#2261]: https://github.com/gchq/CyberChef/pull/2261
+[#2260]: https://github.com/gchq/CyberChef/pull/2260
+[#2170]: https://github.com/gchq/CyberChef/pull/2170
+[#2210]: https://github.com/gchq/CyberChef/pull/2210
+[#2259]: https://github.com/gchq/CyberChef/pull/2259
+[#2257]: https://github.com/gchq/CyberChef/pull/2257
+[#2250]: https://github.com/gchq/CyberChef/pull/2250
+[#2236]: https://github.com/gchq/CyberChef/pull/2236
+[#2234]: https://github.com/gchq/CyberChef/pull/2234
+[#2235]: https://github.com/gchq/CyberChef/pull/2235
+[#2229]: https://github.com/gchq/CyberChef/pull/2229
+[#2228]: https://github.com/gchq/CyberChef/pull/2228
+[#2231]: https://github.com/gchq/CyberChef/pull/2231
+[#2156]: https://github.com/gchq/CyberChef/pull/2156
+[#2213]: https://github.com/gchq/CyberChef/pull/2213
+[#2163]: https://github.com/gchq/CyberChef/pull/2163
+[#2161]: https://github.com/gchq/CyberChef/pull/2161
+[#2208]: https://github.com/gchq/CyberChef/pull/2208
+[#2223]: https://github.com/gchq/CyberChef/pull/2223
+[#2219]: https://github.com/gchq/CyberChef/pull/2219
+[#2218]: https://github.com/gchq/CyberChef/pull/2218
+[#2151]: https://github.com/gchq/CyberChef/pull/2151
+[#2205]: https://github.com/gchq/CyberChef/pull/2205
+[#2071]: https://github.com/gchq/CyberChef/pull/2071
+[#2133]: https://github.com/gchq/CyberChef/pull/2133
+[#2017]: https://github.com/gchq/CyberChef/pull/2017
+[#2204]: https://github.com/gchq/CyberChef/pull/2204
+[#2201]: https://github.com/gchq/CyberChef/pull/2201
+[#2195]: https://github.com/gchq/CyberChef/pull/2195
+[#2200]: https://github.com/gchq/CyberChef/pull/2200
+[#2199]: https://github.com/gchq/CyberChef/pull/2199
+[#2125]: https://github.com/gchq/CyberChef/pull/2125
+[#2171]: https://github.com/gchq/CyberChef/pull/2171
+[#2194]: https://github.com/gchq/CyberChef/pull/2194
+[#2193]: https://github.com/gchq/CyberChef/pull/2193
+[#2192]: https://github.com/gchq/CyberChef/pull/2192
diff --git a/package-lock.json b/package-lock.json
index f4045c83..1104671d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cyberchef",
- "version": "10.22.1",
+ "version": "10.23.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cyberchef",
- "version": "10.22.1",
+ "version": "10.23.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
diff --git a/package.json b/package.json
index 7972c053..160fc4ca 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cyberchef",
- "version": "10.22.1",
+ "version": "10.23.0",
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
"author": "n1474335 ",
"homepage": "https://gchq.github.io/CyberChef",
From 39f97a2af312dd9d6b702c23babd344bb040aeb5 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Tue, 7 Apr 2026 10:43:28 +0100
Subject: [PATCH 38/60] Update vulnerable dependencies (#2311)
Results of running 'npm audit fix'
---
package-lock.json | 58 ++++++++++++++++++++---------------------------
1 file changed, 25 insertions(+), 33 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 1104671d..617b9b51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5738,9 +5738,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9479,9 +9479,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10436,14 +10436,13 @@
}
},
"node_modules/grunt-legacy-log-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz",
- "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.3.tgz",
+ "integrity": "sha512-sgG+QvKmdb44wZyzJP+ejDsy3jYxG2wzohpol+JTMlXqMUBDoZb01JPQ5jKAedtZBFwhmABAc88T9hEBLy3U+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "chalk": "~4.1.0",
- "lodash": "~4.17.19"
+ "chalk": "^4.1.0"
},
"engines": {
"node": ">=10"
@@ -10482,13 +10481,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/grunt-legacy-log-utils/node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/grunt-legacy-log-utils/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -13235,9 +13227,9 @@
}
},
"node_modules/mocha/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14639,9 +14631,9 @@
"license": "ISC"
},
"node_modules/path-to-regexp": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
- "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"dev": true,
"license": "MIT"
},
@@ -15460,9 +15452,9 @@
}
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -18106,9 +18098,9 @@
}
},
"node_modules/webpack-dev-server/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -18736,9 +18728,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"dev": true,
"license": "ISC",
"bin": {
From c68cc5a09602a65b66cb1569ebc39235ee7d60b5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:36:55 +0100
Subject: [PATCH 39/60] chore (deps): bump basic-ftp from 5.2.0 to 5.2.1
(#2313)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 617b9b51..f96b80f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5316,9 +5316,9 @@
"license": "MIT"
},
"node_modules/basic-ftp": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
- "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.1.tgz",
+ "integrity": "sha512-0yaL8JdxTknKDILitVpfYfV2Ob6yb3udX/hK97M7I3jOeznBNxQPtVvTUtnhUkyHlxFWyr5Lvknmgzoc7jf+1Q==",
"dev": true,
"license": "MIT",
"engines": {
From 7b9ff751d21ee83f422f5522147b5191e8cf75bc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Apr 2026 07:26:01 +0100
Subject: [PATCH 40/60] chore (deps): bump webpack from 5.105.4 to 5.106.0
(#2315)
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 f96b80f9..d38debbc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -155,7 +155,7 @@
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
"terser": "^5.46.1",
- "webpack": "^5.105.4",
+ "webpack": "^5.106.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
"webpack-node-externals": "^3.0.0",
@@ -17884,9 +17884,9 @@
}
},
"node_modules/webpack": {
- "version": "5.105.4",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
- "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
+ "version": "5.106.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.0.tgz",
+ "integrity": "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 160fc4ca..df6e7e97 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,7 @@
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
"terser": "^5.46.1",
- "webpack": "^5.105.4",
+ "webpack": "^5.106.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
"webpack-node-externals": "^3.0.0",
From d8830e2b3b682b4448421bd3dbdd545bf6edecfe Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 11 Apr 2026 10:49:29 +0100
Subject: [PATCH 41/60] chore (deps): bump axios from 1.13.6 to 1.15.0 (#2316)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Checked by: GCHQDeveloper581
---
package-lock.json | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d38debbc..71eb8f3e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5169,15 +5169,25 @@
}
},
"node_modules/axios": {
- "version": "1.13.6",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
- "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
+ "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
- "proxy-from-env": "^1.1.0"
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/babel-loader": {
From 2eb8810bf66cb17fac105016e04c1ebc945bdabb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 11 Apr 2026 11:20:20 +0100
Subject: [PATCH 42/60] chore (deps): bump basic-ftp from 5.2.1 to 5.2.2
(#2317)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 71eb8f3e..4cd466af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5326,9 +5326,9 @@
"license": "MIT"
},
"node_modules/basic-ftp": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.1.tgz",
- "integrity": "sha512-0yaL8JdxTknKDILitVpfYfV2Ob6yb3udX/hK97M7I3jOeznBNxQPtVvTUtnhUkyHlxFWyr5Lvknmgzoc7jf+1Q==",
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz",
+ "integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==",
"dev": true,
"license": "MIT",
"engines": {
From f646065c36d51a928b7fe691c5b114d82b140354 Mon Sep 17 00:00:00 2001
From: BigYellowHammer <6392942+BigYellowHammer@users.noreply.github.com>
Date: Sat, 11 Apr 2026 13:03:16 +0200
Subject: [PATCH 43/60] Rewriting fixCryptoApiImports and fixSnackbarMarkup to
js to make it OS agnostic (#2298)
---
Gruntfile.js | 18 +------
.../config/scripts/fixCryptoApiImports.mjs | 54 +++++++++++++++++++
src/core/config/scripts/fixSnackBarMarkup.mjs | 28 ++++++++++
3 files changed, 84 insertions(+), 16 deletions(-)
create mode 100644 src/core/config/scripts/fixCryptoApiImports.mjs
create mode 100644 src/core/config/scripts/fixSnackBarMarkup.mjs
diff --git a/Gruntfile.js b/Gruntfile.js
index a67aa5b8..b04f3048 100755
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -411,25 +411,11 @@ module.exports = function (grunt) {
stdout: false,
},
fixCryptoApiImports: {
- command: function () {
- switch (process.platform) {
- case "darwin":
- return `find ./node_modules/crypto-api/src/ \\( -type d -name .git -prune \\) -o -type f -print0 | xargs -0 sed -i '' -e '/\\.mjs/!s/\\(from "\\.[^"]*\\)";/\\1.mjs";/g'`;
- default:
- return `find ./node_modules/crypto-api/src/ \\( -type d -name .git -prune \\) -o -type f -print0 | xargs -0 sed -i -e '/\\.mjs/!s/\\(from "\\.[^"]*\\)";/\\1.mjs";/g'`;
- }
- },
+ command: `node ${nodeFlags} src/core/config/scripts/fixCryptoApiImports.mjs`,
stdout: false
},
fixSnackbarMarkup: {
- command: function () {
- switch (process.platform) {
- case "darwin":
- return `sed -i '' 's//
/g' ./node_modules/snackbarjs/src/snackbar.js`;
- default:
- return `sed -i 's/
/
/g' ./node_modules/snackbarjs/src/snackbar.js`;
- }
- },
+ command: `node ${nodeFlags} src/core/config/scripts/fixSnackBarMarkup.mjs`,
stdout: false
},
},
diff --git a/src/core/config/scripts/fixCryptoApiImports.mjs b/src/core/config/scripts/fixCryptoApiImports.mjs
new file mode 100644
index 00000000..f4b7c767
--- /dev/null
+++ b/src/core/config/scripts/fixCryptoApiImports.mjs
@@ -0,0 +1,54 @@
+/**
+ * This script updates crypto-api package
+ * It adds .mjs to local imports where its missing
+ *
+ * before:
+ * import foo from "./bar";
+ * after
+ * import foo from "./bar.mjs";
+ *
+ */
+
+/* eslint no-console: ["off"] */
+
+import { readdirSync, readFileSync, writeFileSync } from "fs";
+import { join } from "path";
+
+// Base directory of crypto-api source
+const baseDir = join(process.cwd(), "node_modules/crypto-api/src");
+
+/**
+ * Recursively walk a directory, updating import statements
+ * to include ".mjs" if missing
+ */
+function walk(dir) {
+ const entries = readdirSync(dir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ if (entry.name === ".git") continue;
+
+ const fullPath = join(dir, entry.name);
+
+ if (entry.isDirectory()) {
+ walk(fullPath);
+ } else if (entry.isFile()) {
+ const content = readFileSync(fullPath, "utf8");
+
+ // Add .mjs to imports if not present
+ const updated = content.replace(
+ /from "(\.[^"]*)";/g,
+ (match, p1) => {
+ if (p1.endsWith(".mjs")) return match;
+ return `from "${p1}.mjs";`;
+ }
+ );
+
+ if (updated !== content) {
+ writeFileSync(fullPath, updated, "utf8");
+ }
+ }
+ }
+}
+
+// Run the walker
+walk(baseDir);
diff --git a/src/core/config/scripts/fixSnackBarMarkup.mjs b/src/core/config/scripts/fixSnackBarMarkup.mjs
new file mode 100644
index 00000000..eba011ab
--- /dev/null
+++ b/src/core/config/scripts/fixSnackBarMarkup.mjs
@@ -0,0 +1,28 @@
+/**
+ * This script updates snackbarjs package
+ * Replaces self-closing div with standard opening div
+ *
+ * before:
+ *
+ * after:
+ *
+ *
+ */
+
+/* eslint no-console: ["off"] */
+
+import { readFileSync, writeFileSync } from "fs";
+import { join } from "path";
+
+// Base directory of snackbarjs source
+const filePath = join(process.cwd(), "node_modules/snackbarjs/src/snackbar.js");
+
+const content = readFileSync(filePath, "utf8");
+
+// Replace self-closing div with standard opening div
+const updated = content.replace(
+ /
/g,
+ "
"
+);
+
+writeFileSync(filePath, updated, "utf8");
From 535838146c0491fd337fd0d4ca6f3b0d21ed7cc2 Mon Sep 17 00:00:00 2001
From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com>
Date: Mon, 13 Apr 2026 13:04:33 +0100
Subject: [PATCH 44/60] Regular Expression operation email address regex:
Support IPv4 domains (#2167)
Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
---
src/core/lib/Extract.mjs | 5 ++
src/core/operations/ExtractEmailAddresses.mjs | 5 +-
src/core/operations/RegularExpression.mjs | 3 +-
tests/operations/index.mjs | 1 +
.../tests/ExtractEmailAddresses.mjs | 50 ++++++++++---
tests/operations/tests/RegularExpression.mjs | 75 +++++++++++++++++++
6 files changed, 123 insertions(+), 16 deletions(-)
create mode 100644 tests/operations/tests/RegularExpression.mjs
diff --git a/src/core/lib/Extract.mjs b/src/core/lib/Extract.mjs
index 3de2dd6d..8828671e 100644
--- a/src/core/lib/Extract.mjs
+++ b/src/core/lib/Extract.mjs
@@ -45,6 +45,11 @@ export function search(input, searchRegex, removeRegex=null, sortBy=null, unique
return results;
}
+/**
+ * Email regular expression
+ */
+export const EMAIL_REGEX = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig;
+
/**
* URL regular expression
diff --git a/src/core/operations/ExtractEmailAddresses.mjs b/src/core/operations/ExtractEmailAddresses.mjs
index 34b838ab..fe368a12 100644
--- a/src/core/operations/ExtractEmailAddresses.mjs
+++ b/src/core/operations/ExtractEmailAddresses.mjs
@@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
-import { search } from "../lib/Extract.mjs";
+import { EMAIL_REGEX, search } from "../lib/Extract.mjs";
import { caseInsensitiveSort } from "../lib/Sort.mjs";
/**
@@ -50,8 +50,7 @@ class ExtractEmailAddresses extends Operation {
*/
run(input, args) {
const [displayTotal, sort, unique] = args,
- // email regex from: https://www.regextester.com/98066
- regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig;
+ regex = EMAIL_REGEX;
const results = search(
input,
diff --git a/src/core/operations/RegularExpression.mjs b/src/core/operations/RegularExpression.mjs
index 9ea17e83..c3ab5b1d 100644
--- a/src/core/operations/RegularExpression.mjs
+++ b/src/core/operations/RegularExpression.mjs
@@ -7,6 +7,7 @@
import XRegExp from "xregexp";
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
+import { EMAIL_REGEX } from "../lib/Extract.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
@@ -45,7 +46,7 @@ class RegularExpression extends Operation {
},
{
name: "Email address",
- value: "(?:[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9](?:[\\u00A0-\\uD7FF\\uE000-\\uFFFF-a-z0-9-]*[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9])?\\.)+[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9](?:[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9-]*[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}\\])"
+ value: EMAIL_REGEX.source // We use a different regex library, so just take the source regex string here
},
{
name: "URL",
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index f030349d..d18fbffe 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -143,6 +143,7 @@ import "./tests/Rabbit.mjs";
import "./tests/RAKE.mjs";
import "./tests/Regex.mjs";
import "./tests/Register.mjs";
+import "./tests/RegularExpression.mjs";
import "./tests/RisonEncodeDecode.mjs";
import "./tests/Rotate.mjs";
import "./tests/RSA.mjs";
diff --git a/tests/operations/tests/ExtractEmailAddresses.mjs b/tests/operations/tests/ExtractEmailAddresses.mjs
index 658484cf..97ff7e3e 100644
--- a/tests/operations/tests/ExtractEmailAddresses.mjs
+++ b/tests/operations/tests/ExtractEmailAddresses.mjs
@@ -11,44 +11,70 @@ TestRegister.addTests([
{
name: "Extract email address",
input: "email@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com email@example.name\nemail@example.museum email@example.co.jp firstname-lastname@example.com",
- expectedOutput: "email@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com\nemail@example.name\nemail@example.museum\nemail@example.co.jp\nfirstname-lastname@example.com",
+ expectedOutput:
+ "email@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com\nemail@example.name\nemail@example.museum\nemail@example.co.jp\nfirstname-lastname@example.com",
recipeConfig: [
{
- "op": "Extract email addresses",
- "args": [false]
+ op: "Extract email addresses",
+ args: [false],
},
],
},
{
name: "Extract email address - Display total",
input: "email@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com email@example.name\nemail@example.museum email@example.co.jp firstname-lastname@example.com",
- expectedOutput: "Total found: 11\n\nemail@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com\nemail@example.name\nemail@example.museum\nemail@example.co.jp\nfirstname-lastname@example.com",
+ expectedOutput:
+ "Total found: 11\n\nemail@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com\nemail@example.name\nemail@example.museum\nemail@example.co.jp\nfirstname-lastname@example.com",
recipeConfig: [
{
- "op": "Extract email addresses",
- "args": [true]
+ op: "Extract email addresses",
+ args: [true],
},
],
},
{
name: "Extract email address (Internationalized)",
input: "\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9 \u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c \u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc Jos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com and Jos\u1ec5Silva@google.com\nFoO@BaR.CoM, john@192.168.10.100\ng\xf3mez@junk.br and Abc.123@example.com.\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
- expectedOutput: "\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9\n\u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nJos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com\nJos\u1ec5Silva@google.com\nFoO@BaR.CoM\njohn@192.168.10.100\ng\xf3mez@junk.br\nAbc.123@example.com\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
+ expectedOutput:
+ "\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9\n\u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nJos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com\nJos\u1ec5Silva@google.com\nFoO@BaR.CoM\njohn@192.168.10.100\ng\xf3mez@junk.br\nAbc.123@example.com\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
recipeConfig: [
{
- "op": "Extract email addresses",
- "args": [false]
+ op: "Extract email addresses",
+ args: [false],
},
],
},
{
name: "Extract email address - Display total (Internationalized)",
input: "\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9 \u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c \u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc Jos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com and Jos\u1ec5Silva@google.com\nFoO@BaR.CoM, john@192.168.10.100\ng\xf3mez@junk.br and Abc.123@example.com.\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
- expectedOutput: "Total found: 19\n\n\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9\n\u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nJos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com\nJos\u1ec5Silva@google.com\nFoO@BaR.CoM\njohn@192.168.10.100\ng\xf3mez@junk.br\nAbc.123@example.com\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
+ expectedOutput:
+ "Total found: 19\n\n\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9\n\u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nJos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com\nJos\u1ec5Silva@google.com\nFoO@BaR.CoM\njohn@192.168.10.100\ng\xf3mez@junk.br\nAbc.123@example.com\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
recipeConfig: [
{
- "op": "Extract email addresses",
- "args": [true]
+ op: "Extract email addresses",
+ args: [true],
+ },
+ ],
+ },
+ {
+ name: "Extract email address - IP address",
+ input: "yaunwfkb\nexample@[127.0.0.1]\n091nvka",
+ expectedOutput: "example@[127.0.0.1]",
+ recipeConfig: [
+ {
+ op: "Extract email addresses",
+ args: [false],
+ },
+ ],
+ },
+ {
+ name: "Extract email address - invalid IP address",
+ input: "yaunwfkb\nfalse_positive@[1.2.3.]\n091nvka",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ op: "Extract email addresses",
+ args: [false],
},
],
},
diff --git a/tests/operations/tests/RegularExpression.mjs b/tests/operations/tests/RegularExpression.mjs
new file mode 100644
index 00000000..80af3ce7
--- /dev/null
+++ b/tests/operations/tests/RegularExpression.mjs
@@ -0,0 +1,75 @@
+/**
+ * Regular Expression tests.
+ *
+ * @author C85297 [95289555+C85297@users.noreply.github.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+import { EMAIL_REGEX } from "../../../src/core/lib/Extract.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Regular Expression - built in email regex - IP address",
+ input: "yaunwfkb\nexample@[127.0.0.1]\n091nvka",
+ expectedOutput: "example@[127.0.0.1]",
+ recipeConfig: [
+ {
+ op: "Regular expression",
+ args: [
+ "Email address",
+ EMAIL_REGEX.source,
+ true,
+ true,
+ false,
+ false,
+ false,
+ false,
+ "List matches",
+ ],
+ },
+ ],
+ },
+ {
+ name: "Regular Expression - built in email regex - invalid IP address",
+ input: "yaunwfkb\nfalse_positive@[1.2.3.]\n091nvka",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ op: "Regular expression",
+ args: [
+ "Email address",
+ EMAIL_REGEX.source,
+ true,
+ true,
+ false,
+ false,
+ false,
+ false,
+ "List matches",
+ ],
+ },
+ ],
+ },
+ {
+ name: "Regular Expression - built in email regex - IPv4 from #2318",
+ input: "user@[1.2.3.4]\ntest@[192.168.0.1]\nno-match@[1.2.3.]",
+ expectedOutput: "user@[1.2.3.4]\ntest@[192.168.0.1]",
+ recipeConfig: [
+ {
+ op: "Regular expression",
+ args: [
+ "Email address",
+ EMAIL_REGEX.source,
+ true,
+ true,
+ false,
+ false,
+ false,
+ false,
+ "List matches",
+ ],
+ },
+ ],
+ },
+]);
From 991af0f798d0d63677a32be0d613937f2fed3d8d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 16 Apr 2026 11:03:06 +0100
Subject: [PATCH 45/60] chore (deps): bump follow-redirects from 1.15.11 to
1.16.0 (#2320)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 4cd466af..6e063e20 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9654,9 +9654,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
- "version": "1.15.11",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
- "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"dev": true,
"funding": [
{
From 093346859d232dff2792e00bc89919ff042b53fa Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 16 Apr 2026 13:33:42 +0100
Subject: [PATCH 46/60] chore (deps): bump dompurify from 3.3.3 to 3.4.0
(#2321)
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 6e063e20..968e68cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,7 +38,7 @@
"d3": "7.9.0",
"d3-hexbin": "^0.2.2",
"diff": "^5.2.2",
- "dompurify": "^3.3.3",
+ "dompurify": "^3.4.0",
"es6-promisify": "^7.0.0",
"escodegen": "^2.1.0",
"esprima": "^4.0.1",
@@ -8296,9 +8296,9 @@
}
},
"node_modules/dompurify": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
- "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
+ "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
diff --git a/package.json b/package.json
index df6e7e97..9936c5cb 100644
--- a/package.json
+++ b/package.json
@@ -121,7 +121,7 @@
"d3": "7.9.0",
"d3-hexbin": "^0.2.2",
"diff": "^5.2.2",
- "dompurify": "^3.3.3",
+ "dompurify": "^3.4.0",
"es6-promisify": "^7.0.0",
"escodegen": "^2.1.0",
"esprima": "^4.0.1",
From a2491325121f1179c2366f66cc3ef411515e3bd0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 17 Apr 2026 09:35:58 +0100
Subject: [PATCH 47/60] chore (deps): bump autoprefixer from 10.4.27 to 10.5.0
(#2324)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 46 +++++++++++++++++++++++-----------------------
package.json | 2 +-
2 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 968e68cc..e0b0e03d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -118,7 +118,7 @@
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.41.0",
- "autoprefixer": "^10.4.27",
+ "autoprefixer": "^10.5.0",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^146.0.6",
@@ -5089,9 +5089,9 @@
"license": "MIT"
},
"node_modules/autoprefixer": {
- "version": "10.4.27",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
- "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
"dev": true,
"funding": [
{
@@ -5109,8 +5109,8 @@
],
"license": "MIT",
"dependencies": {
- "browserslist": "^4.28.1",
- "caniuse-lite": "^1.0.30001774",
+ "browserslist": "^4.28.2",
+ "caniuse-lite": "^1.0.30001787",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
@@ -5293,9 +5293,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
- "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+ "version": "2.10.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
+ "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -5865,9 +5865,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
- "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"dev": true,
"funding": [
{
@@ -5885,11 +5885,11 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.9.0",
- "caniuse-lite": "^1.0.30001759",
- "electron-to-chromium": "^1.5.263",
- "node-releases": "^2.0.27",
- "update-browserslist-db": "^1.2.0"
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
},
"bin": {
"browserslist": "cli.js"
@@ -6097,9 +6097,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001777",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
- "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
+ "version": "1.0.30001788",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
+ "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
"dev": true,
"funding": [
{
@@ -8417,9 +8417,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.307",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
- "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==",
+ "version": "1.5.339",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz",
+ "integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==",
"dev": true,
"license": "ISC"
},
diff --git a/package.json b/package.json
index 9936c5cb..a8b3bca8 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.41.0",
- "autoprefixer": "^10.4.27",
+ "autoprefixer": "^10.5.0",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^146.0.6",
From 4675a7dabe22c3e3929ac73e55b0de8311ed928d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 17 Apr 2026 12:37:50 +0100
Subject: [PATCH 48/60] chore (deps): bump the patch-updates group with 6
updates (#2323)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> (removed jimp from updated dependencies)
---
package-lock.json | 81 ++++++++++++++++++++++++++---------------------
package.json | 12 +++----
2 files changed, 51 insertions(+), 42 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index e0b0e03d..592420ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -52,7 +52,7 @@
"hash-wasm": "^4.12.0",
"highlight.js": "^11.11.1",
"ieee754": "^1.2.1",
- "jimp": "^1.6.0",
+ "jimp": "1.6.0",
"jq-web": "^0.5.1",
"jquery": "3.7.1",
"js-sha3": "^0.9.3",
@@ -62,7 +62,7 @@
"jsonpath-plus": "^10.4.0",
"jsonwebtoken": "9.0.3",
"jsqr": "^1.4.0",
- "jsrsasign": "^11.1.1",
+ "jsrsasign": "^11.1.2",
"kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
@@ -85,7 +85,7 @@
"path": "^0.12.7",
"popper.js": "^1.16.1",
"process": "^0.11.10",
- "protobufjs": "^7.5.4",
+ "protobufjs": "^7.5.5",
"qr-image": "^3.2.0",
"reflect-metadata": "^0.2.2",
"rison": "^0.1.1",
@@ -132,7 +132,7 @@
"eslint": "^9.39.4",
"eslint-plugin-jsdoc": "^50.8.0",
"globals": "^15.15.0",
- "grunt": "^1.6.1",
+ "grunt": "^1.6.2",
"grunt-chmod": "~1.1.1",
"grunt-concurrent": "^3.0.0",
"grunt-contrib-clean": "~2.0.1",
@@ -148,14 +148,14 @@
"mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
"nightwatch": "^3.15.0",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.10",
"postcss-css-variables": "^0.19.0",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
"terser": "^5.46.1",
- "webpack": "^5.106.0",
+ "webpack": "^5.106.2",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
"webpack-node-externals": "^3.0.0",
@@ -10170,9 +10170,9 @@
"license": "ISC"
},
"node_modules/grunt": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz",
- "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==",
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.2.tgz",
+ "integrity": "sha512-bUzh5nA/P5L66ihXTDP6J5BGnMB/8lXJXejYWSbH4Y4TvWM9t2S39sggQDYYQlx06cYcCsmu63HMYHGCIzUVfg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10181,14 +10181,14 @@
"exit": "~0.1.2",
"findup-sync": "~5.0.0",
"glob": "~7.1.6",
- "grunt-cli": "~1.4.3",
+ "grunt-cli": "^1.4.3",
"grunt-known-options": "~2.0.0",
"grunt-legacy-log": "~3.0.0",
"grunt-legacy-util": "~2.0.1",
"iconv-lite": "~0.6.3",
"js-yaml": "~3.14.0",
- "minimatch": "~3.0.4",
- "nopt": "~3.0.6"
+ "minimatch": "^3.1.5",
+ "nopt": "^5.0.0"
},
"bin": {
"grunt": "bin/grunt"
@@ -10618,9 +10618,9 @@
}
},
"node_modules/grunt/node_modules/minimatch": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
- "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -12416,13 +12416,10 @@
"license": "Apache-2.0"
},
"node_modules/jsrsasign": {
- "version": "11.1.1",
- "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.1.tgz",
- "integrity": "sha512-6w95OOXH8DNeGxakqLndBEqqwQ6A70zGaky1oxfg8WVLWOnghTfJsc5Tknx+Z88MHSb1bGLcqQHImOF8Lk22XA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/kjur/jsrsasign#donations"
- }
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.2.tgz",
+ "integrity": "sha512-GJuqiU/Grs6BaBBXMAZM9kxhsBrksZE0pF3qIfpkopMd7OMJ9zZmE/+CpV//97srfEyyyq1Ec0ELQtSlW/gPTA==",
+ "license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
@@ -13838,9 +13835,9 @@
}
},
"node_modules/nopt": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
- "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -13848,6 +13845,9 @@
},
"bin": {
"nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
}
},
"node_modules/normalize-path": {
@@ -14819,9 +14819,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
- "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"dev": true,
"funding": [
{
@@ -15078,9 +15078,9 @@
"license": "MIT"
},
"node_modules/protobufjs": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
- "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
+ "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -17894,9 +17894,9 @@
}
},
"node_modules/webpack": {
- "version": "5.106.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.0.tgz",
- "integrity": "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==",
+ "version": "5.106.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz",
+ "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -17916,9 +17916,8 @@
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.11",
- "json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.3.1",
- "mime-types": "^2.1.27",
+ "mime-db": "^1.54.0",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
@@ -18239,6 +18238,16 @@
"node": ">=10.13.0"
}
},
+ "node_modules/webpack/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/websocket-driver": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
diff --git a/package.json b/package.json
index a8b3bca8..783a0980 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"eslint": "^9.39.4",
"eslint-plugin-jsdoc": "^50.8.0",
"globals": "^15.15.0",
- "grunt": "^1.6.1",
+ "grunt": "^1.6.2",
"grunt-chmod": "~1.1.1",
"grunt-concurrent": "^3.0.0",
"grunt-contrib-clean": "~2.0.1",
@@ -79,14 +79,14 @@
"mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
"nightwatch": "^3.15.0",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.10",
"postcss-css-variables": "^0.19.0",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
"terser": "^5.46.1",
- "webpack": "^5.106.0",
+ "webpack": "^5.106.2",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
"webpack-node-externals": "^3.0.0",
@@ -135,7 +135,7 @@
"hash-wasm": "^4.12.0",
"highlight.js": "^11.11.1",
"ieee754": "^1.2.1",
- "jimp": "^1.6.0",
+ "jimp": "1.6.0",
"jq-web": "^0.5.1",
"jquery": "3.7.1",
"js-sha3": "^0.9.3",
@@ -145,7 +145,7 @@
"jsonpath-plus": "^10.4.0",
"jsonwebtoken": "9.0.3",
"jsqr": "^1.4.0",
- "jsrsasign": "^11.1.1",
+ "jsrsasign": "^11.1.2",
"kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
@@ -168,7 +168,7 @@
"path": "^0.12.7",
"popper.js": "^1.16.1",
"process": "^0.11.10",
- "protobufjs": "^7.5.4",
+ "protobufjs": "^7.5.5",
"qr-image": "^3.2.0",
"reflect-metadata": "^0.2.2",
"rison": "^0.1.1",
From 32b748afd3f76ff0c9ec31ddc58a91a9fc4d640a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 17 Apr 2026 13:03:50 +0100
Subject: [PATCH 49/60] chore (deps): bump lodash, grunt-legacy-log and
grunt-legacy-util (#2327)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 44 ++++++++++++++++++++------------------------
1 file changed, 20 insertions(+), 24 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 592420ef..a21038ee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9148,6 +9148,16 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/exit-x": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz",
+ "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/expand-tilde": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
@@ -10430,16 +10440,16 @@
}
},
"node_modules/grunt-legacy-log": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz",
- "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.1.tgz",
+ "integrity": "sha512-vytI3IUC8qUK9TcvvpHpGJzDojua/sfJV4TdLB4FtCFzospqduzBuL3+dEfpvO+tGECv7/273+33hjjMXSa92g==",
"dev": true,
"license": "MIT",
"dependencies": {
"colors": "~1.1.2",
- "grunt-legacy-log-utils": "~2.1.0",
+ "grunt-legacy-log-utils": "^2.1.3",
"hooker": "~0.2.3",
- "lodash": "~4.17.19"
+ "lodash": "^4.18.0"
},
"engines": {
"node": ">= 0.10.0"
@@ -10514,25 +10524,18 @@
"node": ">=0.1.90"
}
},
- "node_modules/grunt-legacy-log/node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/grunt-legacy-util": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz",
- "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.2.tgz",
+ "integrity": "sha512-0xoDILyR4BVJel5uJwnhjdWN9evOQ8A0uXbQUIJ0hgVthIA6kloXHSoqATQPj6BRrHrHkcQtCeGVb0ixFoHyEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"async": "~3.2.0",
- "exit": "~0.1.2",
+ "exit-x": "~0.2.2",
"getobject": "~1.0.0",
"hooker": "~0.2.3",
- "lodash": "~4.17.21",
+ "lodash": "^4.18.0",
"underscore.string": "~3.3.5",
"which": "~2.0.2"
},
@@ -10540,13 +10543,6 @@
"node": ">=10"
}
},
- "node_modules/grunt-legacy-util/node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/grunt-retro": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/grunt-retro/-/grunt-retro-0.6.4.tgz",
From d591d2be1b850d67ce47fcd12c31300b63f052d2 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Fri, 17 Apr 2026 14:16:51 +0100
Subject: [PATCH 50/60] Update dependabot.yml (#2326)
Ignore jimp-1.6.1 (broken) when searching for updates.
---
.github/dependabot.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index cc66c319..c1b09d96 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -43,6 +43,8 @@ updates:
versions: [ '>=0.4.0' ]
- dependency-name: 'geodesy'
versions: [ '>=2.0.0' ]
+ - dependency-name: 'jimp'
+ versions: [ '1.6.1' ]
- dependency-name: 'otpauth'
versions: [ '>=9.4.0' ]
- dependency-name: 'webpack-dev-server'
From dd9e8c018d77e233676505d41752c59d715d19e5 Mon Sep 17 00:00:00 2001
From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com>
Date: Mon, 20 Apr 2026 10:49:48 +0100
Subject: [PATCH 51/60] (Feature) Improve CI (#2328)
* Use "npm ci" rather than "npm install"
* Move UI tests before production image build
* Save zip file artefact from build
---
.github/workflows/master.yml | 3 +--
.github/workflows/pull_requests.yml | 23 ++++++++++++++++-------
.github/workflows/releases.yml | 1 -
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 13d280f6..62f19745 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -26,8 +26,7 @@ jobs:
- name: Install
run: |
- export DETECT_CHROMEDRIVER_VERSION=true
- npm install
+ npm ci
npm run setheapsize
- name: Lint
diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml
index 8f04df72..efa6f6c4 100644
--- a/.github/workflows/pull_requests.yml
+++ b/.github/workflows/pull_requests.yml
@@ -22,8 +22,7 @@ jobs:
- name: Install
run: |
- export DETECT_CHROMEDRIVER_VERSION=true
- npm install
+ npm ci
npm run setheapsize
- name: Lint
@@ -38,7 +37,22 @@ jobs:
if: success()
run: npx grunt prod
+ - name: Upload Build Artefact
+ if: success()
+ uses: actions/upload-artifact@v7
+ with:
+ name: zipped-build
+ path: build/prod/*.zip
+ retention-days: 5
+
+ - name: UI Tests
+ if: success()
+ run: |
+ sudo apt-get install xvfb
+ xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
+
- name: Set up Docker Buildx
+ if: success()
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
@@ -50,8 +64,3 @@ jobs:
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
- - name: UI Tests
- if: success()
- run: |
- sudo apt-get install xvfb
- xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
index ef397a16..67e01d02 100644
--- a/.github/workflows/releases.yml
+++ b/.github/workflows/releases.yml
@@ -32,7 +32,6 @@ jobs:
- name: Install
run: |
- export DETECT_CHROMEDRIVER_VERSION=true
npm ci
npm run setheapsize
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 52/60] 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 53/60] 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 54/60] 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 55/60] 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 56/60] 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 57/60] [#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 58/60] 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 59/60] 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 60/60] 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",