Merge branch 'master' into master

This commit is contained in:
GCHQDeveloper581 2026-04-25 08:32:57 +01:00 committed by GitHub
commit df155634a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 252 additions and 60 deletions

56
package-lock.json generated
View File

@ -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",
@ -115,9 +115,9 @@
"@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",
"@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",
@ -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": {
@ -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": {

View File

@ -46,9 +46,9 @@
"@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",
"@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",

View File

@ -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");
}

View File

@ -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 = "<li class='operation'";
if (elementId) {
html += ` id='${elementId}'`;
}
if (this.description) {
const infoLink = this.infoURL ? `<hr>${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;

View File

@ -173,7 +173,7 @@
Operations
<span class="op-count"></span>
</div>
<input id="search" type="search" class="form-control" placeholder="Search..." autocomplete="off" tabindex="0" data-help-title="Searching for operations" data-help="<p>Use the search box to find useful operations.</p><p>Both operation names and descriptions are queried using a fuzzy matching algorithm.</p>">
<input id="search" type="search" class="form-control" placeholder="Search..." autocomplete="off" tabindex="0" data-help-title="Searching for operations" data-help="<p>Use the search box to find useful operations.</p><p>Both operation names and descriptions are queried using a fuzzy matching algorithm.</p>" aria-owns="search-results">
<ul id="search-results" class="op-list"></ul>
<div id="categories" class="panel-group no-select"></div>
</div>

View File

@ -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 = [];

View File

@ -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", () => {

View File

@ -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";

View File

@ -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]
}
]
}
]);