Modernize CyberChef build system, dependencies, UI, and testing

Build System:
- Replace Grunt with npm scripts and Node.js helper scripts
- Remove worker-loader, use native Webpack 5 worker syntax
- Add Rspack config as faster alternative bundler (5-23x faster)
- Replace platform-specific sed postinstall hacks with cross-platform Node.js script
- Update browser targets from Chrome 50/Firefox 38 to Chrome 80/Firefox 78/Safari 14
- Fix import assertions (assert -> with) for Node 24 compatibility

Dependencies:
- Replace deprecated crypto-js with native RC4 implementation and @noble/hashes EVP KDF
- Replace blakejs with @noble/hashes/blake2b and blake2s
- Add @noble/hashes for MD5, SHA1/2/3, RIPEMD160, HMAC, HKDF (crypto-api fallback for legacy algos)
- Replace lodash with native CaseConvert.mjs utility
- Replace moment.js in web layer with date-fns
- Add date-fns and date-fns-tz dependencies
- Remove jquery, snackbarjs, arrive, bootstrap-colorpicker, bootstrap-material-design, popper.js v1, lodash, blakejs, crypto-js

UI Modernization:
- Upgrade Bootstrap 4.6.2 to Bootstrap 5.3 (87 data attribute renames)
- Remove bootstrap-material-design (unmaintained)
- Remove jQuery dependency entirely (38 calls replaced with native DOM + BS5 JS API)
- Add custom Snackbar.mjs notification utility (replaces snackbarjs)
- Replace bootstrap-colorpicker with native HTML color input

Testing:
- Add Vitest config and adapter for running existing TestRegister tests
- Add Playwright config and E2E test replacing Nightwatch
- Update CI workflows to use npm scripts and Playwright
This commit is contained in:
consigcody94 2026-03-23 13:59:24 -04:00 committed by Pentest Agent
parent b0fa1f8d1b
commit 1ac3190dae
65 changed files with 2084 additions and 416 deletions

View File

@ -26,12 +26,11 @@ jobs:
- name: Install
run: |
export DETECT_CHROMEDRIVER_VERSION=true
npm install
npm run setheapsize
- name: Lint
run: npx grunt lint
run: npm run lint
- name: Unit Tests
run: |
@ -40,20 +39,22 @@ jobs:
- name: Production Build
if: success()
run: npx grunt prod --msg=""
run: npm run build
- name: Generate sitemap
run: npx grunt exec:sitemap
run: npm run build:sitemap
- name: Install Playwright Browsers
if: success()
run: npx playwright install --with-deps chromium
- name: UI Tests
if: success()
run: |
sudo apt-get install xvfb
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
run: npm run testui
- name: Prepare for GitHub Pages
if: success()
run: npx grunt copy:ghPages
run: npm run build:ghpages
- name: Deploy to GitHub Pages
if: success() && github.ref == 'refs/heads/master'

View File

@ -22,12 +22,11 @@ jobs:
- name: Install
run: |
export DETECT_CHROMEDRIVER_VERSION=true
npm install
npm run setheapsize
- name: Lint
run: npx grunt lint
run: npm run lint
- name: Unit Tests
run: |
@ -36,7 +35,7 @@ jobs:
- name: Production Build
if: success()
run: npx grunt prod
run: npm run build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@ -50,8 +49,11 @@ jobs:
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
- name: Install Playwright Browsers
if: success()
run: npx playwright install --with-deps chromium
- name: UI Tests
if: success()
run: |
sudo apt-get install xvfb
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
run: npm run testui

View File

@ -12,14 +12,14 @@ COPY package.json .
COPY package-lock.json .
# Install dependencies
# --ignore-scripts prevents postinstall script (which runs grunt) as it depends on files other than package.json
# --ignore-scripts prevents postinstall script as it depends on files other than package.json
RUN npm ci --ignore-scripts
# Copy files needed for postinstall and build
COPY . .
# npm postinstall runs grunt, which depends on files other than package.json
RUN npm run postinstall
# Run postinstall to fix dependency issues
RUN node scripts/postinstall.mjs
# Build the app
RUN npm run build

View File

@ -5,17 +5,12 @@ module.exports = function(api) {
"presets": [
["@babel/preset-env", {
"modules": false,
"useBuiltIns": "entry",
"useBuiltIns": "usage",
"corejs": 3
}]
],
"plugins": [
"@babel/plugin-syntax-import-assertions",
[
"@babel/plugin-transform-runtime", {
"regenerator": true
}
]
"@babel/plugin-syntax-import-assertions"
]
};
};

View File

@ -34,80 +34,59 @@
},
"bugs": "https://github.com/gchq/CyberChef/issues",
"browserslist": [
"Chrome >= 50",
"Firefox >= 38",
"node >= 16"
"Chrome >= 80",
"Firefox >= 78",
"Safari >= 14",
"node >= 18"
],
"devDependencies": {
"@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.2",
"@babel/runtime": "^7.29.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.40.0",
"archiver": "^7.0.0",
"autoprefixer": "^10.4.27",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^130.0.4",
"cli-progress": "^3.12.0",
"colors": "^1.4.0",
"compression-webpack-plugin": "^11.1.0",
"copy-webpack-plugin": "^13.0.1",
"core-js": "^3.49.0",
"concurrently": "^9.0.0",
"cspell": "^8.19.4",
"css-loader": "7.1.4",
"eslint": "^9.39.4",
"eslint-plugin-jsdoc": "^50.8.0",
"globals": "^15.15.0",
"grunt": "^1.6.1",
"grunt-chmod": "~1.1.1",
"grunt-concurrent": "^3.0.0",
"grunt-contrib-clean": "~2.0.1",
"grunt-contrib-connect": "^5.0.1",
"grunt-contrib-copy": "~1.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-eslint": "^25.0.0",
"grunt-exec": "~3.0.0",
"grunt-webpack": "^6.0.0",
"grunt-zip": "^1.0.0",
"html-webpack-plugin": "^5.6.6",
"imports-loader": "^5.0.0",
"mini-css-extract-plugin": "2.10.1",
"modify-source-webpack-plugin": "^4.1.0",
"nightwatch": "^3.15.0",
"@playwright/test": "^1.50.0",
"postcss": "^8.5.8",
"postcss-css-variables": "^0.19.0",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"rimraf": "^6.0.0",
"vitest": "^3.0.0",
"sitemap": "^8.0.3",
"terser": "^5.46.1",
"webpack": "^5.105.4",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
"webpack-node-externals": "^3.0.0",
"worker-loader": "^3.0.8"
"@rspack/cli": "^1.2.0",
"@rspack/core": "^1.2.0"
},
"dependencies": {
"@alexaltea/capstone-js": "^3.0.5",
"@noble/hashes": "^1.7.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"@astronautlabs/amf": "^0.0.6",
"@blu3r4y/lzma": "^2.3.3",
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
"@xmldom/xmldom": "^0.8.11",
"argon2-browser": "^1.18.0",
"arrive": "^2.5.2",
"assert": "^2.1.0",
"avsc": "^5.7.9",
"bcryptjs": "^2.4.3",
"bignumber.js": "^9.3.1",
"blakejs": "^1.2.1",
"bootstrap": "4.6.2",
"bootstrap-colorpicker": "^3.4.0",
"bootstrap-material-design": "^4.1.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.3",
"browserify-zlib": "^0.2.0",
"bson": "^4.7.2",
"buffer": "^6.0.3",
@ -116,7 +95,6 @@
"codepage": "^1.15.0",
"crypto-api": "^0.8.5",
"crypto-browserify": "^3.12.1",
"crypto-js": "^4.2.0",
"ctph.js": "0.0.5",
"d3": "7.9.0",
"d3-hexbin": "^0.2.2",
@ -137,7 +115,6 @@
"ieee754": "^1.2.1",
"jimp": "^1.6.0",
"jq-web": "^0.5.1",
"jquery": "3.7.1",
"js-sha3": "^0.9.3",
"jsesc": "^3.1.0",
"json5": "^2.2.3",
@ -149,7 +126,6 @@
"kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
"lodash": "^4.17.23",
"loglevel": "^1.9.2",
"loglevel-message-prefix": "^3.0.0",
"lz-string": "^1.5.0",
@ -166,14 +142,12 @@
"nwmatcher": "^1.4.4",
"otpauth": "9.3.6",
"path": "^0.12.7",
"popper.js": "^1.16.1",
"process": "^0.11.10",
"protobufjs": "^7.5.4",
"qr-image": "^3.2.0",
"reflect-metadata": "^0.2.2",
"rison": "^0.1.1",
"scryptsy": "^2.1.0",
"snackbarjs": "^1.1.0",
"sortablejs": "^1.15.7",
"split.js": "^1.6.5",
"sql-formatter": "^15.6.12",
@ -191,19 +165,38 @@
"zlibjs": "^0.3.1"
},
"scripts": {
"start": "npx grunt dev",
"build": "npx grunt prod",
"node": "npx grunt node",
"repl": "node --experimental-modules --experimental-json-modules --experimental-specifier-resolution=node --no-experimental-fetch --no-warnings src/node/repl.mjs",
"test": "npx grunt configTests && node --experimental-modules --experimental-json-modules --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch tests/node/index.mjs && node --experimental-modules --experimental-json-modules --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch --trace-uncaught tests/operations/index.mjs",
"testnodeconsumer": "npx grunt testnodeconsumer",
"testui": "npx grunt testui",
"testuidev": "npx nightwatch --env=dev",
"lint": "npx grunt lint",
"start": "npm run dev",
"dev": "npm run clean:config && npm run generate:config && concurrently --names config,webpack \"npm run watch:config\" \"npm run dev:server\"",
"dev:server": "rspack serve --config rspack.dev.config.js",
"watch:config": "node --watch-path=src/core/operations scripts/watchConfig.mjs",
"build": "npm run lint && npm run clean:prod && npm run clean:config && npm run generate:config && rspack build --config rspack.prod.config.js && node scripts/buildStandalone.mjs",
"build:node": "npm run clean:node && npm run clean:config && npm run clean:nodeConfig && npm run generate:config && npm run generate:nodeIndex",
"build:ghpages": "node scripts/prepareGhPages.mjs",
"build:sitemap": "node --no-warnings --no-deprecation src/web/static/sitemap.mjs > build/prod/sitemap.xml",
"clean:dev": "rimraf build/dev/*",
"clean:prod": "rimraf build/prod/*",
"clean:node": "rimraf build/node/*",
"clean:config": "rimraf src/core/config/OperationConfig.json src/core/config/modules/* src/core/operations/index.mjs",
"clean:nodeConfig": "rimraf src/node/index.mjs src/node/config/OperationConfig.json",
"generate:config": "echo [] > src/core/config/OperationConfig.json && node --no-warnings --no-deprecation src/core/config/scripts/generateOpsIndex.mjs && node --no-warnings --no-deprecation src/core/config/scripts/generateConfig.mjs",
"generate:nodeIndex": "node --no-warnings --no-deprecation src/node/config/scripts/generateNodeIndex.mjs",
"test": "npm run pretest && npx vitest run",
"test:legacy": "npm run pretest && node --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch tests/node/index.mjs && node --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch --trace-uncaught tests/operations/index.mjs",
"pretest": "npm run clean:config && npm run clean:nodeConfig && npm run generate:config && npm run generate:nodeIndex",
"testui": "npx playwright test",
"testui:headed": "npx playwright test --headed",
"testnodeconsumer": "node scripts/testNodeConsumers.mjs",
"lint": "eslint *.{js,mjs} src/core/**/*.{js,mjs} src/web/**/*.{js,mjs} src/node/**/*.{js,mjs} tests/**/*.{js,mjs} --ignore-pattern src/core/vendor/** --ignore-pattern src/core/operations/legacy/** --ignore-pattern src/web/static/**",
"lint:grammar": "cspell ./src",
"postinstall": "npx grunt exec:fixCryptoApiImports && npx grunt exec:fixSnackbarMarkup",
"newop": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newOperation.mjs",
"minor": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newMinorVersion.mjs && npm version minor --git-tag-version=false && echo \"Updated to version v$(npm pkg get version | xargs), please create a pull request and once merged use 'npm run tag'\"",
"postinstall": "node scripts/postinstall.mjs",
"newop": "node --no-warnings --no-deprecation src/core/config/scripts/newOperation.mjs",
"minor": "node --no-warnings --no-deprecation src/core/config/scripts/newMinorVersion.mjs && npm version minor --git-tag-version=false && echo \"Updated to version v$(npm pkg get version | xargs), please create a pull request and once merged use 'npm run tag'\"",
"tag": "git tag -s \"v$(npm pkg get version | xargs)\" -m \"$(npm pkg get version | xargs)\" && echo \"Created v$(npm pkg get version | xargs), now check and push the tag\"",
"getheapsize": "node -e 'console.log(`node heap limit = ${require(\"v8\").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'",
"setheapsize": "export NODE_OPTIONS=--max_old_space_size=2048"

45
playwright.config.mjs Normal file
View File

@ -0,0 +1,45 @@
/**
* Playwright configuration for CyberChef E2E tests.
* Replaces Nightwatch browser testing setup.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/browser",
testMatch: "**/*.spec.mjs",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
timeout: 30000,
use: {
baseURL: "http://localhost:8080",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
webServer: {
command: "npm run dev:server",
url: "http://localhost:8080",
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});

207
rspack.config.js Normal file
View File

@ -0,0 +1,207 @@
const rspack = require("@rspack/core");
const path = require("path");
const zlib = require("zlib");
/**
* Rspack configuration for CyberChef.
* Migrated from Webpack 5 for faster build times.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
const d = new Date();
const banner = `/**
* CyberChef - The Cyber Swiss Army Knife
*
* @copyright Crown Copyright 2016-${d.getUTCFullYear()}
* @license Apache-2.0
*
* Copyright 2016-${d.getUTCFullYear()} Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/`;
module.exports = {
output: {
publicPath: "",
globalObject: "this",
assetModuleFilename: "assets/[hash][ext][query]"
},
plugins: [
new rspack.ProvidePlugin({
log: "loglevel",
process: "process",
Buffer: ["buffer", "Buffer"]
}),
new rspack.BannerPlugin({
banner: banner,
raw: true,
entryOnly: true
}),
new rspack.DefinePlugin({
"process.browser": "true"
}),
new rspack.CssExtractRspackPlugin({
filename: "assets/[name].css"
}),
new rspack.CopyRspackPlugin({
patterns: [
{
context: "src/core/vendor/",
from: "tesseract/**/*",
to: "assets/"
}, {
context: "node_modules/tesseract.js/",
from: "dist/worker.min.js",
to: "assets/tesseract"
}, {
context: "node_modules/tesseract.js-core/",
from: "tesseract-core.wasm.js",
to: "assets/tesseract"
}, {
context: "node_modules/node-forge/dist",
from: "prime.worker.min.js",
to: "assets/forge/"
}
]
}),
],
resolve: {
extensions: [".mjs", ".js", ".json"],
alias: {},
fallback: {
"assert": require.resolve("assert/"),
"buffer": require.resolve("buffer/"),
"child_process": false,
"crypto": require.resolve("crypto-browserify"),
"events": require.resolve("events/"),
"fs": false,
"net": false,
"path": require.resolve("path/"),
"process": false,
"stream": require.resolve("stream-browserify"),
"tls": false,
"url": require.resolve("url/"),
"vm": false,
"zlib": require.resolve("browserify-zlib")
}
},
module: {
noParse: /argon2\.wasm$/,
rules: [
{
test: /\.m?js$/,
exclude: /node_modules\/(?!crypto-api|bootstrap)/,
loader: "builtin:swc-loader",
options: {
jsc: {
parser: {
syntax: "ecmascript",
dynamicImport: true,
importAssertions: true,
},
target: "es2020",
},
env: {
targets: "Chrome >= 80, Firefox >= 78, Safari >= 14",
},
},
type: "javascript/auto",
},
{
test: /node-forge/,
loader: "imports-loader",
options: {
additionalCode: "var jQuery = false;"
}
},
{
test: /argon2\.wasm$/,
loader: "base64-loader",
type: "javascript/auto"
},
{
test: /prime.worker.min.js$/,
type: "asset/source"
},
{
test: /blueimp-load-image/,
loader: "imports-loader",
options: {
type: "commonjs",
imports: "single min-document document"
}
},
{
test: /\.css$/,
use: [
{
loader: rspack.CssExtractRspackPlugin.loader,
options: {
publicPath: "../"
}
},
"css-loader",
"postcss-loader",
]
},
{
test: /\.(ico|eot|ttf|woff|woff2)$/,
type: "asset/resource",
},
{
test: /\.svg$/,
type: "asset/inline",
},
{
test: /(\.fnt$|bmfonts\/.+\.png$)/,
type: "asset/resource",
generator: {
filename: "assets/fonts/[name][ext]"
}
},
{
test: /\.(png|jpg|gif)$/,
exclude: /(node_modules|bmfonts)/,
type: "asset/resource",
generator: {
filename: "images/[name][ext]"
}
},
{
test: /\.(png|jpg|gif)$/,
exclude: /web\/static/,
type: "asset/inline",
},
]
},
stats: {
children: false,
chunks: false,
modules: false,
entrypoints: false
},
ignoreWarnings: [
/source-map/,
/source map/,
/dependency is an expression/,
/export 'default'/,
/Can't resolve 'sodium'/
],
performance: {
hints: false
}
};

51
rspack.dev.config.js Normal file
View File

@ -0,0 +1,51 @@
"use strict";
const rspack = require("@rspack/core");
const baseConfig = require("./rspack.config.js");
const { listEntryModules } = require("./scripts/listEntryModulesSync.cjs");
const pkg = require("./package.json");
const d = new Date();
const compileYear = d.getUTCFullYear().toString();
const compileTime = `${String(d.getUTCDate()).padStart(2, "0")}/${String(d.getUTCMonth() + 1).padStart(2, "0")}/${d.getUTCFullYear()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")} UTC`;
const BUILD_CONSTANTS = {
COMPILE_YEAR: JSON.stringify(compileYear),
COMPILE_TIME: JSON.stringify(compileTime),
COMPILE_MSG: JSON.stringify(process.env.COMPILE_MSG || ""),
PKG_VERSION: JSON.stringify(pkg.version),
};
const moduleEntryPoints = listEntryModules();
module.exports = {
...baseConfig,
mode: "development",
target: "web",
entry: Object.assign({
main: "./src/web/index.js"
}, moduleEntryPoints),
resolve: {
...baseConfig.resolve,
alias: {
...baseConfig.resolve.alias,
"./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
}
},
devServer: {
port: parseInt(process.env.PORT || "8080", 10),
client: {
logging: "error",
overlay: true
},
hot: "only"
},
plugins: [
...baseConfig.plugins,
new rspack.DefinePlugin(BUILD_CONSTANTS),
new rspack.HtmlRspackPlugin({
filename: "index.html",
template: "./src/web/html/index.html",
}),
]
};

51
rspack.prod.config.js Normal file
View File

@ -0,0 +1,51 @@
"use strict";
const rspack = require("@rspack/core");
const baseConfig = require("./rspack.config.js");
const { listEntryModules } = require("./scripts/listEntryModulesSync.cjs");
const pkg = require("./package.json");
const d = new Date();
const compileYear = d.getUTCFullYear().toString();
const compileTime = `${String(d.getUTCDate()).padStart(2, "0")}/${String(d.getUTCMonth() + 1).padStart(2, "0")}/${d.getUTCFullYear()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")} UTC`;
const BUILD_CONSTANTS = {
COMPILE_YEAR: JSON.stringify(compileYear),
COMPILE_TIME: JSON.stringify(compileTime),
COMPILE_MSG: JSON.stringify(process.env.COMPILE_MSG || ""),
PKG_VERSION: JSON.stringify(pkg.version),
};
const moduleEntryPoints = listEntryModules();
module.exports = {
...baseConfig,
mode: "production",
target: "web",
entry: Object.assign({
main: "./src/web/index.js"
}, moduleEntryPoints),
output: {
...baseConfig.output,
path: __dirname + "/build/prod",
filename: chunkData => {
return chunkData.chunk.name === "main" ? "assets/[name].js" : "[name].js";
},
},
resolve: {
...baseConfig.resolve,
alias: {
...baseConfig.resolve.alias,
"./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
}
},
plugins: [
...baseConfig.plugins,
new rspack.DefinePlugin(BUILD_CONSTANTS),
new rspack.HtmlRspackPlugin({
filename: "index.html",
template: "./src/web/html/index.html",
minify: true,
}),
]
};

View File

@ -0,0 +1,86 @@
/**
* Builds standalone CyberChef HTML file, creates zip archive, and calculates SHA256 hash.
* Replaces the Grunt tasks: copy:standalone, zip:standalone, clean:standalone, exec:calcDownloadHash
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import { readFileSync, writeFileSync, unlinkSync, createReadStream, createWriteStream, readdirSync, statSync } from "fs";
import { createHash } from "crypto";
import { join, relative } from "path";
import { createGzip } from "zlib";
import { pipeline } from "stream/promises";
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
const version = pkg.version;
const buildDir = "build/prod";
// Step 1: Create standalone HTML (copy:standalone equivalent)
console.log("--- Creating standalone HTML ---");
let indexHtml = readFileSync(join(buildDir, "index.html"), "utf8");
// Replace download link with version number
indexHtml = indexHtml.replace(/<a [^>]+>Download CyberChef.+?<\/a>/,
`<span>Version ${version}</span>`);
const standaloneFilename = `CyberChef_v${version}.html`;
writeFileSync(join(buildDir, standaloneFilename), indexHtml);
console.log(`Created ${standaloneFilename}`);
// Step 2: Create zip archive (zip:standalone equivalent)
console.log("--- Creating zip archive ---");
const archiver = (await import("archiver")).default;
const zipFilename = `CyberChef_v${version}.zip`;
const zipPath = join(buildDir, zipFilename);
const output = createWriteStream(zipPath);
const archive = archiver("zip", { zlib: { level: 9 } });
await new Promise((resolve, reject) => {
output.on("close", resolve);
archive.on("error", reject);
archive.pipe(output);
// Add all files from build/prod except index.html and BundleAnalyzerReport.html
const addDir = (dir) => {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relPath = relative(buildDir, fullPath);
if (entry.isDirectory()) {
addDir(fullPath);
} else if (entry.isFile()) {
if (relPath === "index.html" || relPath === "BundleAnalyzerReport.html") continue;
if (relPath.startsWith("CyberChef_v")) continue; // skip standalone files
archive.file(fullPath, { name: relPath });
}
}
};
addDir(buildDir);
archive.finalize();
});
console.log(`Created ${zipFilename}`);
// Step 3: Clean standalone HTML (clean:standalone equivalent)
unlinkSync(join(buildDir, standaloneFilename));
console.log(`Cleaned ${standaloneFilename}`);
// Step 4: Calculate SHA256 hash (exec:calcDownloadHash equivalent)
console.log("--- Calculating SHA256 hash ---");
const zipBuffer = readFileSync(zipPath);
const hash = createHash("sha256").update(zipBuffer).digest("hex");
writeFileSync(join(buildDir, "sha256digest.txt"), hash);
// Replace placeholder in index.html
let prodIndexHtml = readFileSync(join(buildDir, "index.html"), "utf8");
prodIndexHtml = prodIndexHtml.replace(/DOWNLOAD_HASH_PLACEHOLDER/g, hash);
writeFileSync(join(buildDir, "index.html"), prodIndexHtml);
console.log(`SHA256: ${hash}`);
console.log("--- Standalone build complete ---");

View File

@ -0,0 +1,44 @@
/**
* Lists all generated module entry points for Webpack/Rspack.
* Replaces the Grunt findModules task.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import { readdirSync } from "fs";
import { basename, resolve } from "path";
/**
* Generates an entry list for all the modules.
* @returns {Object} Entry point mapping
*/
export function listEntryModules() {
const entryModules = {};
const modulesDir = "./src/core/config/modules";
try {
const files = readdirSync(modulesDir).filter(f => f.endsWith(".mjs"));
for (const file of files) {
if (file !== "Default.mjs" && file !== "OpModules.mjs") {
const name = basename(file, ".mjs");
entryModules["modules/" + name] = resolve(modulesDir, file);
}
}
} catch (e) {
// Modules directory may not exist yet during initial config generation
console.warn("Warning: Could not read modules directory:", e.message);
}
return entryModules;
}
// When run directly, print the module list
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("listEntryModules.mjs")) {
const modules = listEntryModules();
console.log(`Found ${Object.keys(modules).length} modules:`);
for (const [name, path] of Object.entries(modules)) {
console.log(` ${name}: ${path}`);
}
}

View File

@ -0,0 +1,32 @@
/**
* Lists all generated module entry points for Webpack/Rspack (CommonJS version).
* Used by webpack config files which must be CommonJS.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
const { readdirSync } = require("fs");
const { basename, resolve } = require("path");
function listEntryModules() {
const entryModules = {};
const modulesDir = "./src/core/config/modules";
try {
const files = readdirSync(modulesDir).filter(f => f.endsWith(".mjs"));
for (const file of files) {
if (file !== "Default.mjs" && file !== "OpModules.mjs") {
const name = basename(file, ".mjs");
entryModules["modules/" + name] = resolve(modulesDir, file);
}
}
} catch (e) {
console.warn("Warning: Could not read modules directory:", e.message);
}
return entryModules;
}
module.exports = { listEntryModules };

81
scripts/postinstall.mjs Normal file
View File

@ -0,0 +1,81 @@
/**
* Cross-platform postinstall script.
* Fixes known issues in dependencies without requiring platform-specific `sed`.
* Replaces Grunt exec:fixCryptoApiImports and exec:fixSnackbarMarkup.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
import { join, extname } from "path";
/**
* Recursively find all files in a directory.
*/
function findFiles(dir, files = []) {
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory() && entry.name !== ".git") {
findFiles(fullPath, files);
} else if (entry.isFile()) {
files.push(fullPath);
}
}
} catch {
// Directory may not exist
}
return files;
}
// Fix 1: crypto-api imports - add .mjs extensions to relative imports
console.log("Fixing crypto-api imports...");
const cryptoApiDir = join("node_modules", "crypto-api", "src");
let cryptoApiFixed = 0;
try {
const files = findFiles(cryptoApiDir);
for (const file of files) {
let content = readFileSync(file, "utf8");
const original = content;
// Add .mjs extension to relative imports that don't already have it
content = content.replace(/from\s+"(\.[^"]*?)(?<!\.mjs)";/g, (match, importPath) => {
// Don't add .mjs if it already has a file extension
if (extname(importPath) !== "") return match;
return `from "${importPath}.mjs";`;
});
if (content !== original) {
writeFileSync(file, content);
cryptoApiFixed++;
}
}
console.log(` Fixed ${cryptoApiFixed} files in crypto-api.`);
} catch (e) {
console.log(` Skipping crypto-api fix (not installed or not found): ${e.message}`);
}
// Fix 2: snackbarjs - fix self-closing div
console.log("Fixing snackbarjs markup...");
const snackbarFile = join("node_modules", "snackbarjs", "src", "snackbar.js");
try {
let content = readFileSync(snackbarFile, "utf8");
const original = content;
content = content.replace(/<div id=snackbar-container\/>/g, "<div id=snackbar-container>");
if (content !== original) {
writeFileSync(snackbarFile, content);
console.log(" Fixed snackbarjs self-closing div.");
} else {
console.log(" snackbarjs already fixed or pattern not found.");
}
} catch (e) {
console.log(` Skipping snackbarjs fix (not installed or not found): ${e.message}`);
}
console.log("Postinstall complete.");

View File

@ -0,0 +1,32 @@
/**
* Prepares build/prod/index.html for GitHub Pages deployment.
* Adds Google Analytics and Structured Data.
* Replaces the Grunt copy:ghPages task.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import { readFileSync, writeFileSync } from "fs";
import { join } from "path";
const buildDir = "build/prod";
console.log("--- Preparing for GitHub Pages ---");
let indexHtml = readFileSync(join(buildDir, "index.html"), "utf8");
// Add Google Analytics code
const gaHtml = readFileSync("src/web/static/ga.html", "utf8");
indexHtml = indexHtml.replace("</body></html>", gaHtml + "</body></html>");
// Add Structured Data for SEO
const structuredData = JSON.parse(readFileSync("src/web/static/structuredData.json", "utf8"));
indexHtml = indexHtml.replace("</head>",
"<script type='application/ld+json'>" +
JSON.stringify(structuredData) +
"</script></head>");
writeFileSync(join(buildDir, "index.html"), indexHtml);
console.log("--- GitHub Pages preparation complete ---");

View File

@ -0,0 +1,51 @@
/**
* Tests that CyberChef can be consumed as both CJS and ESM modules.
* Replaces Grunt exec:setupNodeConsumers, testCJSNodeConsumer, testESMNodeConsumer, teardownNodeConsumers.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import { execSync } from "child_process";
import { mkdirSync, cpSync, rmSync, existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";
const testPath = join(homedir(), "tmp-cyberchef");
const nodeFlags = "--no-warnings --no-deprecation";
try {
console.log("\n--- Testing node consumers ---");
// Setup
execSync("npm link", { stdio: "inherit" });
mkdirSync(testPath, { recursive: true });
// Copy consumer test files
const consumersDir = "tests/node/consumers";
cpSync(consumersDir, testPath, { recursive: true });
// Link cyberchef in the test directory
execSync("npm link cyberchef", { cwd: testPath, stdio: "inherit" });
// Test CJS consumer
console.log("Testing CJS consumer...");
execSync(`node ${nodeFlags} cjs-consumer.js`, { cwd: testPath, stdio: "pipe" });
console.log("CJS consumer test passed.");
// Test ESM consumer
console.log("Testing ESM consumer...");
execSync(`node ${nodeFlags} esm-consumer.mjs`, { cwd: testPath, stdio: "pipe" });
console.log("ESM consumer test passed.");
console.log("\n--- Node consumer tests complete ---");
} catch (e) {
console.error("Node consumer test failed:", e.message);
process.exitCode = 1;
} finally {
// Teardown
if (existsSync(testPath)) {
rmSync(testPath, { recursive: true, force: true });
}
}

43
scripts/watchConfig.mjs Normal file
View File

@ -0,0 +1,43 @@
/**
* Watches for changes in operations and regenerates config files.
* Replaces Grunt watch:config task.
* Uses Node.js --watch-path flag for file watching.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import { execSync } from "child_process";
import { watch } from "fs";
import { join } from "path";
const opsDir = "src/core/operations";
let debounceTimer = null;
function regenerateConfig() {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
console.log("\n--- Regenerating config files ---");
try {
execSync("node --no-warnings --no-deprecation src/node/config/scripts/generateNodeIndex.mjs", { stdio: "inherit" });
execSync("node --no-warnings --no-deprecation src/core/config/scripts/generateOpsIndex.mjs", { stdio: "inherit" });
execSync("node --no-warnings --no-deprecation src/core/config/scripts/generateConfig.mjs", { stdio: "inherit" });
console.log("--- Config regenerated ---\n");
} catch (e) {
console.error("Config generation failed:", e.message);
}
}, 500);
}
console.log(`Watching ${opsDir} for changes...`);
watch(opsDir, { recursive: true }, (eventType, filename) => {
if (filename && filename !== "index.mjs") {
console.log(`Change detected: ${filename}`);
regenerateConfig();
}
});
// Keep process alive
process.on("SIGINT", () => process.exit(0));

View File

@ -7,7 +7,7 @@
*/
import Chef from "./Chef.mjs";
import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
import OperationConfig from "./config/OperationConfig.json" with {type: "json"};
import OpModules from "./config/modules/OpModules.mjs";
import loglevelMessagePrefix from "loglevel-message-prefix";

View File

@ -4,7 +4,7 @@
* @license Apache-2.0
*/
import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
import OperationConfig from "./config/OperationConfig.json" with {type: "json"};
import OperationError from "./errors/OperationError.mjs";
import Operation from "./Operation.mjs";
import DishError from "./errors/DishError.mjs";

View File

@ -0,0 +1,56 @@
/**
* Case conversion utilities.
* Replaces lodash/camelCase, lodash/kebabCase, lodash/snakeCase.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
/**
* Splits a string into words, handling camelCase, PascalCase, snake_case,
* kebab-case, spaces, and mixed separators.
*
* @param {string} str
* @returns {string[]}
*/
function splitWords(str) {
if (!str) return [];
// Insert boundary before uppercase letters following lowercase or digits
return str
.replace(/([a-z\d])([A-Z])/g, "$1\0$2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1\0$2")
.split(/[\0\s_\-./\\]+/)
.filter(Boolean)
.map(w => w.toLowerCase());
}
/**
* Converts a string to camelCase.
* @param {string} str
* @returns {string}
*/
export function camelCase(str) {
const words = splitWords(str);
return words
.map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1))
.join("");
}
/**
* Converts a string to kebab-case.
* @param {string} str
* @returns {string}
*/
export function kebabCase(str) {
return splitWords(str).join("-");
}
/**
* Converts a string to snake_case.
* @param {string} str
* @returns {string}
*/
export function snakeCase(str) {
return splitWords(str).join("_");
}

View File

@ -12,7 +12,6 @@
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import CryptoJS from "crypto-js";
/**
* Affine Cipher Encode operation.
@ -71,18 +70,3 @@ export function genPolybiusSquare (keyword) {
return polybius;
}
/**
* A mapping of string formats to their classes in the CryptoJS library.
*
* @private
* @constant
*/
export const format = {
"Hex": CryptoJS.enc.Hex,
"Base64": CryptoJS.enc.Base64,
"UTF8": CryptoJS.enc.Utf8,
"UTF16": CryptoJS.enc.Utf16,
"UTF16LE": CryptoJS.enc.Utf16LE,
"UTF16BE": CryptoJS.enc.Utf16BE,
"Latin1": CryptoJS.enc.Latin1,
};

View File

@ -8,11 +8,53 @@
*/
import Utils from "../Utils.mjs";
import { md5, sha1, ripemd160 } from "@noble/hashes/legacy";
import { sha224, sha256, sha384, sha512, sha512_224, sha512_256 } from "@noble/hashes/sha2";
import { sha3_224, sha3_256, sha3_384, sha3_512 } from "@noble/hashes/sha3";
import { hmac } from "@noble/hashes/hmac";
import { hkdf } from "@noble/hashes/hkdf";
import { bytesToHex } from "@noble/hashes/utils";
import CryptoApi from "crypto-api/src/crypto-api.mjs";
/**
* Map of hash algorithm names (lowercased) to noble hash functions.
*/
const NOBLE_HASH_FUNCTIONS = {
"md5": md5,
"sha1": sha1,
"sha224": sha224,
"sha256": sha256,
"sha384": sha384,
"sha512": sha512,
"sha512/224": sha512_224,
"sha512/256": sha512_256,
"sha3-224": sha3_224,
"sha3-256": sha3_256,
"sha3-384": sha3_384,
"sha3-512": sha3_512,
"ripemd160": ripemd160,
// Legacy aliases
"sha-1": sha1,
"sha-224": sha224,
"sha-256": sha256,
"sha-384": sha384,
"sha-512": sha512,
};
/**
* Gets a noble hash function by name. Returns null if not supported by noble.
*
* @param {string} name - Hash algorithm name (case-insensitive)
* @returns {Function|null} Noble hash function or null
*/
export function getHashFunction(name) {
const normalizedName = name.toLowerCase().replace(/\s+/g, "");
return NOBLE_HASH_FUNCTIONS[normalizedName] || null;
}
/**
* Generic hash function.
* Uses @noble/hashes for common algorithms, falls back to crypto-api for legacy ones.
*
* @param {string} name
* @param {ArrayBuffer} input
@ -20,9 +62,19 @@ import CryptoApi from "crypto-api/src/crypto-api.mjs";
* @returns {string}
*/
export function runHash(name, input, options={}) {
const hashFn = getHashFunction(name);
if (hashFn) {
// Use noble for supported algorithms
const data = new Uint8Array(input);
return bytesToHex(hashFn(data));
}
// Fall back to crypto-api for legacy algorithms (Snefru, Whirlpool, MD2, MD4, SHA0, HAS160, etc.)
const msg = Utils.arrayBufferToStr(input, false),
hasher = CryptoApi.getHasher(name, options);
hasher.update(msg);
return CryptoApi.encoder.toHex(hasher.finalize());
}
export { hmac, hkdf, bytesToHex };

View File

@ -1,4 +1,4 @@
import OperationConfig from "../config/OperationConfig.json" assert {type: "json"};
import OperationConfig from "../config/OperationConfig.json" with {type: "json"};
import Utils, { isWorkerEnvironment } from "../Utils.mjs";
import Recipe from "../Recipe.mjs";
import Dish from "../Dish.mjs";

74
src/core/lib/RC4.mjs Normal file
View File

@ -0,0 +1,74 @@
/**
* RC4 stream cipher implementation.
* Replaces crypto-js RC4 for CyberChef operations.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
/**
* RC4 Key Scheduling Algorithm (KSA).
*
* @param {Uint8Array} key
* @returns {Uint8Array} The initialized S-box
*/
function ksa(key) {
const S = new Uint8Array(256);
for (let i = 0; i < 256; i++) S[i] = i;
let j = 0;
for (let i = 0; i < 256; i++) {
j = (j + S[i] + key[i % key.length]) & 0xFF;
[S[i], S[j]] = [S[j], S[i]];
}
return S;
}
/**
* RC4 Pseudo-Random Generation Algorithm (PRGA).
*
* @param {Uint8Array} S - The S-box from KSA
* @param {number} length - Number of keystream bytes to generate
* @param {number} [drop=0] - Number of initial bytes to drop
* @returns {Uint8Array} Keystream bytes
*/
function prga(S, length, drop = 0) {
const output = new Uint8Array(length);
let i = 0, j = 0;
// Drop initial bytes
for (let d = 0; d < drop; d++) {
i = (i + 1) & 0xFF;
j = (j + S[i]) & 0xFF;
[S[i], S[j]] = [S[j], S[i]];
}
// Generate keystream
for (let k = 0; k < length; k++) {
i = (i + 1) & 0xFF;
j = (j + S[i]) & 0xFF;
[S[i], S[j]] = [S[j], S[i]];
output[k] = S[(S[i] + S[j]) & 0xFF];
}
return output;
}
/**
* Encrypt/decrypt data using RC4.
* RC4 is symmetric encryption and decryption are the same operation.
*
* @param {Uint8Array} data - Input data
* @param {Uint8Array} key - Key bytes
* @param {number} [drop=0] - Number of initial keystream bytes to drop (for RC4-drop)
* @returns {Uint8Array} Output data
*/
export function rc4(data, key, drop = 0) {
const S = ksa(key);
const keystream = prga(S, data.length, drop);
const output = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
output[i] = data[i] ^ keystream[i];
}
return output;
}

View File

@ -5,7 +5,8 @@
*/
import Operation from "../Operation.mjs";
import blakejs from "blakejs";
import { blake2b } from "@noble/hashes/blake2b";
import { bytesToHex } from "@noble/hashes/utils";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { toBase64 } from "../lib/Base64.mjs";
@ -23,7 +24,7 @@ class BLAKE2b extends Operation {
this.name = "BLAKE2b";
this.module = "Hashing";
this.description = `Performs BLAKE2b hashing on the input.
this.description = `Performs BLAKE2b hashing on the input.
<br><br> BLAKE2b is a flavour of the BLAKE cryptographic hash function that is optimized for 64-bit platforms and produces digests of any size between 1 and 64 bytes.
<br><br> Supports the use of an optional key.`;
this.infoURL = "https://wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2b_algorithm";
@ -56,19 +57,27 @@ class BLAKE2b extends Operation {
const [outSize, outFormat] = args;
let key = Utils.convertToByteArray(args[2].string || "", args[2].option);
if (key.length === 0) {
key = null;
key = undefined;
} else if (key.length > 64) {
throw new OperationError(["Key cannot be greater than 64 bytes", "It is currently " + key.length + " bytes."].join("\n"));
} else {
key = new Uint8Array(key);
}
input = new Uint8Array(input);
const data = new Uint8Array(input);
const dkLen = outSize / 8;
const opts = { dkLen };
if (key) opts.key = key;
const hash = blake2b(data, opts);
switch (outFormat) {
case "Hex":
return blakejs.blake2bHex(input, key, outSize / 8);
return bytesToHex(hash);
case "Base64":
return toBase64(blakejs.blake2b(input, key, outSize / 8));
return toBase64(hash);
case "Raw":
return Utils.arrayBufferToStr(blakejs.blake2b(input, key, outSize / 8).buffer);
return Utils.arrayBufferToStr(hash.buffer);
default:
return new OperationError("Unsupported Output Type");
}

View File

@ -5,7 +5,8 @@
*/
import Operation from "../Operation.mjs";
import blakejs from "blakejs";
import { blake2s } from "@noble/hashes/blake2s";
import { bytesToHex } from "@noble/hashes/utils";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { toBase64 } from "../lib/Base64.mjs";
@ -23,7 +24,7 @@ class BLAKE2s extends Operation {
this.name = "BLAKE2s";
this.module = "Hashing";
this.description = `Performs BLAKE2s hashing on the input.
this.description = `Performs BLAKE2s hashing on the input.
<br><br>BLAKE2s is a flavour of the BLAKE cryptographic hash function that is optimized for 8- to 32-bit platforms and produces digests of any size between 1 and 32 bytes.
<br><br>Supports the use of an optional key.`;
this.infoURL = "https://wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2";
@ -57,19 +58,27 @@ class BLAKE2s extends Operation {
const [outSize, outFormat] = args;
let key = Utils.convertToByteArray(args[2].string || "", args[2].option);
if (key.length === 0) {
key = null;
key = undefined;
} else if (key.length > 32) {
throw new OperationError(["Key cannot be greater than 32 bytes", "It is currently " + key.length + " bytes."].join("\n"));
} else {
key = new Uint8Array(key);
}
input = new Uint8Array(input);
const data = new Uint8Array(input);
const dkLen = outSize / 8;
const opts = { dkLen };
if (key) opts.key = key;
const hash = blake2s(data, opts);
switch (outFormat) {
case "Hex":
return blakejs.blake2sHex(input, key, outSize / 8);
return bytesToHex(hash);
case "Base64":
return toBase64(blakejs.blake2s(input, key, outSize / 8));
return toBase64(hash);
case "Raw":
return Utils.arrayBufferToStr(blakejs.blake2s(input, key, outSize / 8).buffer);
return Utils.arrayBufferToStr(hash.buffer);
default:
return new OperationError("Unsupported Output Type");
}

View File

@ -6,7 +6,58 @@
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import CryptoJS from "crypto-js";
import { md5, sha1 } from "@noble/hashes/legacy";
import { sha256, sha384, sha512 } from "@noble/hashes/sha2";
import { bytesToHex } from "@noble/hashes/utils";
/**
* Map of hash function names to noble implementations.
*/
const HASH_MAP = {
"MD5": md5,
"SHA1": sha1,
"SHA256": sha256,
"SHA384": sha384,
"SHA512": sha512,
};
/**
* EVP_BytesToKey key derivation function (OpenSSL).
* Derives key material from password + salt using iterated hashing.
*
* @param {Uint8Array} password
* @param {Uint8Array} salt
* @param {number} keySize - Key size in bytes
* @param {number} iterations
* @param {Function} hashFn - Noble hash function
* @returns {Uint8Array}
*/
function evpKDF(password, salt, keySize, iterations, hashFn) {
let derivedKey = new Uint8Array(0);
let block = new Uint8Array(0);
while (derivedKey.length < keySize) {
// Concatenate previous block + password + salt
const input = new Uint8Array(block.length + password.length + salt.length);
input.set(block, 0);
input.set(password, block.length);
input.set(salt, block.length + password.length);
// Hash with iterations
block = hashFn(input);
for (let i = 1; i < iterations; i++) {
block = hashFn(block);
}
// Append to derived key
const combined = new Uint8Array(derivedKey.length + block.length);
combined.set(derivedKey, 0);
combined.set(block, derivedKey.length);
derivedKey = combined;
}
return derivedKey.slice(0, keySize);
}
/**
* Derive EVP key operation
@ -62,86 +113,28 @@ class DeriveEVPKey extends Operation {
* @returns {string}
*/
run(input, args) {
const passphrase = CryptoJS.enc.Latin1.parse(
Utils.convertToByteString(args[0].string, args[0].option)),
keySize = args[1] / 32,
iterations = args[2],
hasher = args[3],
salt = CryptoJS.enc.Latin1.parse(
Utils.convertToByteString(args[4].string, args[4].option)),
key = CryptoJS.EvpKDF(passphrase, salt, { // lgtm [js/insufficient-password-hash]
keySize: keySize,
hasher: CryptoJS.algo[hasher],
iterations: iterations,
});
const passStr = Utils.convertToByteString(args[0].string, args[0].option);
const keySize = args[1] / 8; // Convert bits to bytes
const iterations = args[2];
const hasherName = args[3];
const saltStr = Utils.convertToByteString(args[4].string, args[4].option);
return key.toString(CryptoJS.enc.Hex);
const hashFn = HASH_MAP[hasherName];
if (!hashFn) {
throw new Error(`Unsupported hash function: ${hasherName}`);
}
// Convert strings to Uint8Array
const password = new Uint8Array(passStr.length);
for (let i = 0; i < passStr.length; i++) password[i] = passStr.charCodeAt(i) & 0xFF;
const salt = new Uint8Array(saltStr.length);
for (let i = 0; i < saltStr.length; i++) salt[i] = saltStr.charCodeAt(i) & 0xFF;
const key = evpKDF(password, salt, keySize, iterations, hashFn);
return bytesToHex(key);
}
}
export default DeriveEVPKey;
/**
* Overwriting the CryptoJS OpenSSL key derivation function so that it is possible to not pass a
* salt in.
* @param {string} password - The password to derive from.
* @param {number} keySize - The size in words of the key to generate.
* @param {number} ivSize - The size in words of the IV to generate.
* @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be
* generated randomly. If set to false, no salt will be added.
*
* @returns {CipherParams} A cipher params object with the key, IV, and salt.
*
* @static
*
* @example
* // Randomly generates a salt
* var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32);
* // Uses the salt 'saltsalt'
* var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt');
* // Does not use a salt
* var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, false);
*/
CryptoJS.kdf.OpenSSL.execute = function (password, keySize, ivSize, salt) {
// Generate random salt if no salt specified and not set to false
// This line changed from `if (!salt) {` to the following
if (salt === undefined || salt === null) {
salt = CryptoJS.lib.WordArray.random(64/8);
}
// Derive key and IV
const key = CryptoJS.algo.EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);
// Separate key and IV
const iv = CryptoJS.lib.WordArray.create(key.words.slice(keySize), ivSize * 4);
key.sigBytes = keySize * 4;
// Return params
return CryptoJS.lib.CipherParams.create({ key: key, iv: iv, salt: salt });
};
/**
* Override for the CryptoJS Hex encoding parser to remove whitespace before attempting to parse
* the hex string.
*
* @param {string} hexStr
* @returns {CryptoJS.lib.WordArray}
*/
CryptoJS.enc.Hex.parse = function (hexStr) {
// Remove whitespace
hexStr = hexStr.replace(/\s/g, "");
// Shortcut
const hexStrLength = hexStr.length;
// Convert
const words = [];
for (let i = 0; i < hexStrLength; i += 2) {
words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
}
return new CryptoJS.lib.WordArray.init(words, hexStrLength / 2);
};

View File

@ -4,10 +4,11 @@
*/
import Operation from "../Operation.mjs";
import CryptoApi from "crypto-api/src/crypto-api.mjs";
import Utils from "../Utils.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import OperationError from "../errors/OperationError.mjs";
import { getHashFunction } from "../lib/Hash.mjs";
import { hmac } from "@noble/hashes/hmac";
/**
* Flask Session Sign operation
@ -57,12 +58,15 @@ class FlaskSessionSign extends Operation {
const key = Utils.convertToByteString(args[0].string, args[0].option);
const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option);
const algorithm = args[2] || "sha1";
const hashFn = getHashFunction(algorithm);
const payloadB64 = toBase64(Utils.strToByteArray(JSON.stringify(input)));
const payload = payloadB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm));
derivedKey.update(salt);
// Derive key: HMAC(secret, salt)
const keyBytes = new Uint8Array(Utils.strToArrayBuffer(key));
const saltBytes = new Uint8Array(Utils.strToArrayBuffer(salt));
const derivedKeyBytes = hmac(hashFn, keyBytes, saltBytes);
const currentTimeStamp = Math.ceil(Date.now() / 1000);
const buffer = new ArrayBuffer(4);
@ -75,10 +79,15 @@ class FlaskSessionSign extends Operation {
const time = timeB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
const data = Utils.convertToByteString(payload + "." + time, "utf8");
const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm));
sign.update(data);
const dataBytes = new Uint8Array(Utils.strToArrayBuffer(data));
const signB64 = toBase64(sign.finalize());
// Sign: HMAC(derivedKey, data)
const signBytes = hmac(hashFn, derivedKeyBytes, dataBytes);
// Convert Uint8Array back to byte string for toBase64
let signStr = "";
signBytes.forEach(b => signStr += String.fromCharCode(b));
const signB64 = toBase64(Utils.strToByteArray(signStr));
const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
return payload + "." + time + "." + sign64;

View File

@ -5,9 +5,10 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import CryptoApi from "crypto-api/src/crypto-api.mjs";
import Utils from "../Utils.mjs";
import { toBase64, fromBase64 } from "../lib/Base64.mjs";
import { getHashFunction } from "../lib/Hash.mjs";
import { hmac } from "@noble/hashes/hmac";
/**
* Flask Session Verify operation
@ -64,6 +65,7 @@ class FlaskSessionVerify extends Operation {
const key = Utils.convertToByteString(args[0].string, args[0].option);
const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option);
const algorithm = args[2] || "sha1";
const hashFn = getHashFunction(algorithm);
input = input.trim();
@ -75,12 +77,14 @@ class FlaskSessionVerify extends Operation {
const data = Utils.convertToByteString(parts[0] + "." + parts[1], "utf8");
// Derive key: HMAC(secret, salt)
const keyBytes = new Uint8Array(Utils.strToArrayBuffer(key));
const saltBytes = new Uint8Array(Utils.strToArrayBuffer(salt));
const derivedKeyBytes = hmac(hashFn, keyBytes, saltBytes);
const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm));
derivedKey.update(salt);
const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm));
sign.update(data);
// Sign: HMAC(derivedKey, data)
const dataBytes = new Uint8Array(Utils.strToArrayBuffer(data));
const signBytes = hmac(hashFn, derivedKeyBytes, dataBytes);
const payloadB64 = parts[0];
const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
@ -104,7 +108,10 @@ class FlaskSessionVerify extends Operation {
throw new OperationError("Invalid Base64 payload");
}
const signB64 = toBase64(sign.finalize());
// Convert Uint8Array back to byte string for toBase64
let signStr = "";
signBytes.forEach(b => signStr += String.fromCharCode(b));
const signB64 = toBase64(Utils.strToByteArray(signStr));
const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
if (sign64 !== parts[2]) {

View File

@ -154,7 +154,7 @@ class Magic extends Operation {
</tr>`;
});
output += "</table><script type='application/javascript'>$('[data-toggle=\"tooltip\"]').tooltip()</script>";
output += "</table><script type='application/javascript'>document.querySelectorAll('[data-bs-toggle=\"tooltip\"]').forEach(function(el){new bootstrap.Tooltip(el)})</script>";
if (!options.length) {
output = "Nothing of interest could be detected about the input data.\nHave you tried modifying the operation arguments?";

View File

@ -104,17 +104,23 @@ HSL: ${hsl}
HSLA: ${hsla}
CMYK: ${cmyk}
<script>
$('#colorpicker').colorpicker({
format: 'rgba',
color: '${rgba}',
container: true,
inline: true,
useAlpha: true
}).on('colorpickerChange', function(e) {
var color = e.color.string('rgba');
window.app.manager.input.setInput(color);
window.app.manager.input.inputChange(new Event("keyup"));
});
(function() {
var el = document.getElementById('colorpicker');
if (el) {
var picker = document.createElement('input');
picker.type = 'color';
picker.value = '${hex}';
picker.style.width = '200px';
picker.style.height = '200px';
picker.style.border = 'none';
picker.style.cursor = 'pointer';
el.appendChild(picker);
picker.addEventListener('input', function(e) {
window.app.manager.input.setInput(e.target.value);
window.app.manager.input.inputChange(new Event("keyup"));
});
}
})();
</script>`;
}

View File

@ -5,8 +5,10 @@
*/
import Operation from "../Operation.mjs";
import CryptoJS from "crypto-js";
import { format } from "../lib/Ciphers.mjs";
import Utils from "../Utils.mjs";
import { rc4 } from "../lib/RC4.mjs";
import { toHex, fromHex } from "../lib/Hex.mjs";
import { toBase64, fromBase64 } from "../lib/Base64.mjs";
/**
* RC4 operation
@ -51,11 +53,16 @@ class RC4 extends Operation {
* @returns {string}
*/
run(input, args) {
const message = format[args[1]].parse(input),
passphrase = format[args[0].option].parse(args[0].string),
encrypted = CryptoJS.RC4.encrypt(message, passphrase);
const inputFormat = args[1];
const outputFormat = args[2];
const keyFormat = args[0].option;
const keyStr = args[0].string;
return encrypted.ciphertext.toString(format[args[2]]);
const inputBytes = formatParse(input, inputFormat);
const keyBytes = formatParse(keyStr, keyFormat);
const outputBytes = rc4(inputBytes, keyBytes);
return formatStringify(outputBytes, outputFormat);
}
/**
@ -86,4 +93,90 @@ class RC4 extends Operation {
}
/**
* Parse a string in the given format to Uint8Array.
*
* @param {string} str
* @param {string} format
* @returns {Uint8Array}
*/
function formatParse(str, format) {
switch (format) {
case "Hex":
return new Uint8Array(fromHex(str, "Auto"));
case "Base64":
return Utils.strToByteArray(fromBase64(str));
case "UTF8":
return new TextEncoder().encode(str);
case "Latin1":
default: {
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i) & 0xFF;
}
return bytes;
}
case "UTF16":
case "UTF16BE": {
const bytes = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
bytes[i * 2] = (code >> 8) & 0xFF;
bytes[i * 2 + 1] = code & 0xFF;
}
return bytes;
}
case "UTF16LE": {
const bytes = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
bytes[i * 2] = code & 0xFF;
bytes[i * 2 + 1] = (code >> 8) & 0xFF;
}
return bytes;
}
}
}
/**
* Stringify a Uint8Array in the given format.
*
* @param {Uint8Array} bytes
* @param {string} format
* @returns {string}
*/
function formatStringify(bytes, format) {
switch (format) {
case "Hex":
return toHex(bytes, "");
case "Base64":
return toBase64(bytes);
case "UTF8":
return new TextDecoder("utf-8").decode(bytes);
case "Latin1":
default: {
let str = "";
for (let i = 0; i < bytes.length; i++) {
str += String.fromCharCode(bytes[i]);
}
return str;
}
case "UTF16":
case "UTF16BE": {
let str = "";
for (let i = 0; i < bytes.length - 1; i += 2) {
str += String.fromCharCode((bytes[i] << 8) | bytes[i + 1]);
}
return str;
}
case "UTF16LE": {
let str = "";
for (let i = 0; i < bytes.length - 1; i += 2) {
str += String.fromCharCode(bytes[i] | (bytes[i + 1] << 8));
}
return str;
}
}
}
export default RC4;

View File

@ -5,8 +5,10 @@
*/
import Operation from "../Operation.mjs";
import { format } from "../lib/Ciphers.mjs";
import CryptoJS from "crypto-js";
import Utils from "../Utils.mjs";
import { rc4 } from "../lib/RC4.mjs";
import { toHex, fromHex } from "../lib/Hex.mjs";
import { toBase64, fromBase64 } from "../lib/Base64.mjs";
/**
* RC4 Drop operation
@ -56,12 +58,18 @@ class RC4Drop extends Operation {
* @returns {string}
*/
run(input, args) {
const message = format[args[1]].parse(input),
passphrase = format[args[0].option].parse(args[0].string),
drop = args[3],
encrypted = CryptoJS.RC4Drop.encrypt(message, passphrase, { drop: drop });
const inputFormat = args[1];
const outputFormat = args[2];
const drop = args[3];
const keyFormat = args[0].option;
const keyStr = args[0].string;
return encrypted.ciphertext.toString(format[args[2]]);
const inputBytes = formatParse(input, inputFormat);
const keyBytes = formatParse(keyStr, keyFormat);
// RC4Drop drops dwords (4 bytes each)
const outputBytes = rc4(inputBytes, keyBytes, drop * 4);
return formatStringify(outputBytes, outputFormat);
}
/**
@ -92,4 +100,82 @@ class RC4Drop extends Operation {
}
/**
* Parse a string in the given format to Uint8Array.
*/
function formatParse(str, format) {
switch (format) {
case "Hex":
return new Uint8Array(fromHex(str, "Auto"));
case "Base64":
return Utils.strToByteArray(fromBase64(str));
case "UTF8":
return new TextEncoder().encode(str);
case "Latin1":
default: {
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i) & 0xFF;
}
return bytes;
}
case "UTF16":
case "UTF16BE": {
const bytes = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
bytes[i * 2] = (code >> 8) & 0xFF;
bytes[i * 2 + 1] = code & 0xFF;
}
return bytes;
}
case "UTF16LE": {
const bytes = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
bytes[i * 2] = code & 0xFF;
bytes[i * 2 + 1] = (code >> 8) & 0xFF;
}
return bytes;
}
}
}
/**
* Stringify a Uint8Array in the given format.
*/
function formatStringify(bytes, format) {
switch (format) {
case "Hex":
return toHex(bytes, "");
case "Base64":
return toBase64(bytes);
case "UTF8":
return new TextDecoder("utf-8").decode(bytes);
case "Latin1":
default: {
let str = "";
for (let i = 0; i < bytes.length; i++) {
str += String.fromCharCode(bytes[i]);
}
return str;
}
case "UTF16":
case "UTF16BE": {
let str = "";
for (let i = 0; i < bytes.length - 1; i += 2) {
str += String.fromCharCode((bytes[i] << 8) | bytes[i + 1]);
}
return str;
}
case "UTF16LE": {
let str = "";
for (let i = 0; i < bytes.length - 1; i += 2) {
str += String.fromCharCode(bytes[i] | (bytes[i + 1] << 8));
}
return str;
}
}
}
export default RC4Drop;

View File

@ -66,7 +66,7 @@ class ShowBase64Offsets extends Operation {
const len0 = offset0.indexOf("="),
len1 = offset1.indexOf("="),
len2 = offset2.indexOf("="),
script = "<script type='application/javascript'>$('[data-toggle=\"tooltip\"]').tooltip()</script>";
script = "<script type='application/javascript'>document.querySelectorAll('[data-bs-toggle=\"tooltip\"]').forEach(function(el){new bootstrap.Tooltip(el)})</script>";
if (input.length < 1) {
throw new OperationError("Please enter a string.");

View File

@ -4,7 +4,7 @@
* @license Apache-2.0
*/
import camelCase from "lodash/camelCase.js";
import { camelCase } from "../lib/CaseConvert.mjs";
import Operation from "../Operation.mjs";
import { replaceVariableNames } from "../lib/Code.mjs";

View File

@ -4,7 +4,7 @@
* @license Apache-2.0
*/
import kebabCase from "lodash/kebabCase.js";
import { kebabCase } from "../lib/CaseConvert.mjs";
import Operation from "../Operation.mjs";
import { replaceVariableNames } from "../lib/Code.mjs";

View File

@ -4,7 +4,7 @@
* @license Apache-2.0
*/
import snakeCase from "lodash/snakeCase.js";
import { snakeCase } from "../lib/CaseConvert.mjs";
import Operation from "../Operation.mjs";
import { replaceVariableNames } from "../lib/Code.mjs";

View File

@ -10,7 +10,7 @@
import NodeDish from "./NodeDish.mjs";
import NodeRecipe from "./NodeRecipe.mjs";
import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"};
import OperationConfig from "../core/config/OperationConfig.json" with {type: "json"};
import { sanitise, removeSubheadingsFromArray, sentenceToCamelCase } from "./apiUtils.mjs";
import ExcludedOperationError from "../core/errors/ExcludedOperationError.mjs";

View File

@ -10,8 +10,10 @@ import Manager from "./Manager.mjs";
import HTMLCategory from "./HTMLCategory.mjs";
import HTMLOperation from "./HTMLOperation.mjs";
import Split from "split.js";
import moment from "moment-timezone";
import { formatDistance } from "date-fns";
import cptable from "codepage";
import {showSnackbar} from "./utils/Snackbar.mjs";
import * as bootstrap from "bootstrap";
/**
@ -638,7 +640,7 @@ class App {
// Display time since last build and compile message
const now = new Date(),
msSinceCompile = now.getTime() - window.compileTime,
timeSinceCompile = moment.duration(msSinceCompile, "milliseconds").humanize();
timeSinceCompile = formatDistance(new Date(window.compileTime), now, { addSuffix: false });
// Calculate previous version to compare to
const prev = PKG_VERSION.split(".").map(n => {
@ -703,14 +705,11 @@ class App {
log.info("[" + time.toLocaleString() + "] " + str);
if (silent) return;
this.snackbars.push($.snackbar({
showSnackbar({
content: str,
timeout: timeout,
htmlAllowed: true,
onClose: () => {
this.snackbars.shift().remove();
}
}));
style: "snackbar"
});
}
@ -738,25 +737,40 @@ class App {
document.getElementById("confirm-modal").style.display = "block";
this.confirmClosed = false;
$("#confirm-modal").modal()
.one("show.bs.modal", function(e) {
this.confirmClosed = false;
}.bind(this))
.one("click", "#confirm-yes", function() {
this.confirmClosed = true;
callback.bind(scope)(true);
$("#confirm-modal").modal("hide");
}.bind(this))
.one("click", "#confirm-no", function() {
this.confirmClosed = true;
callback.bind(scope)(false);
}.bind(this))
.one("hide.bs.modal", function(e) {
if (!this.confirmClosed) {
callback.bind(scope)(undefined);
}
this.confirmClosed = true;
}.bind(this));
const confirmModalEl = document.getElementById("confirm-modal");
const confirmModal = bootstrap.Modal.getOrCreateInstance(confirmModalEl);
const onShow = (e) => {
this.confirmClosed = false;
};
const onYes = () => {
this.confirmClosed = true;
callback.bind(scope)(true);
confirmModal.hide();
};
const onNo = () => {
this.confirmClosed = true;
callback.bind(scope)(false);
};
const onHide = (e) => {
if (!this.confirmClosed) {
callback.bind(scope)(undefined);
}
this.confirmClosed = true;
// Clean up one-time listeners
confirmModalEl.removeEventListener("show.bs.modal", onShow);
confirmModalEl.removeEventListener("hide.bs.modal", onHide);
document.getElementById("confirm-yes").removeEventListener("click", onYes);
document.getElementById("confirm-no").removeEventListener("click", onNo);
};
confirmModalEl.addEventListener("show.bs.modal", onShow, { once: true });
confirmModalEl.addEventListener("hide.bs.modal", onHide, { once: true });
document.getElementById("confirm-yes").addEventListener("click", onYes, { once: true });
document.getElementById("confirm-no").addEventListener("click", onNo, { once: true });
confirmModal.show();
}

View File

@ -40,13 +40,13 @@ class HTMLCategory {
toHtml() {
const catName = "cat" + this.name.replace(/[\s/\-:_]/g, "");
let html = `<div class="panel category">
<a class="category-title" data-toggle="collapse" data-target="#${catName}">
<a class="category-title" data-bs-toggle="collapse" data-bs-target="#${catName}">
${this.name}
<span class="op-count hidden">
${this.opList.length}
</span>
</a>
<div id="${catName}" class="panel-collapse collapse ${(this.selected ? " show" : "")}" data-parent="#categories">
<div id="${catName}" class="panel-collapse collapse ${(this.selected ? " show" : "")}" data-bs-parent="#categories">
<ul class="op-list">`;
for (let i = 0; i < this.opList.length; i++) {

View File

@ -49,7 +49,7 @@ class HTMLIngredient {
toHtml() {
let html = "",
i, m, eventFn;
const hintHtml = this.hint ? `data-toggle="tooltip" title="${this.hint}"` : "";
const hintHtml = this.hint ? `data-bs-toggle="tooltip" title="${this.hint}"` : "";
switch (this.type) {
case "string":
@ -95,7 +95,7 @@ class HTMLIngredient {
${this.maxLength ? `maxlength="${this.maxLength}"` : ""}>
</div>
<div class="input-group-append">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${this.toggleValues[0]}</button>
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${this.toggleValues[0]}</button>
<div class="dropdown-menu toggle-dropdown">`;
for (i = 0; i < this.toggleValues.length; i++) {
html += `<a class="dropdown-item" href="#">${this.toggleValues[i]}</a>`;
@ -200,7 +200,7 @@ class HTMLIngredient {
<div class="input-group-append">
<button type="button"
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown"
data-bs-toggle="dropdown"
data-boundary="scrollParent"
aria-haspopup="true"
aria-expanded="false">
@ -229,7 +229,7 @@ class HTMLIngredient {
<div class="input-group-append inline">
<button type="button"
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown"
data-bs-toggle="dropdown"
data-boundary="scrollParent"
aria-haspopup="true"
aria-expanded="false">

View File

@ -51,8 +51,8 @@ class HTMLOperation {
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'
html += ` data-bs-container='body' data-bs-toggle='popover' data-bs-placement='right'
data-bs-content="${this.description}${infoLink}" data-bs-html='true' data-bs-trigger='hover'
data-boundary='viewport'`;
}

View File

@ -142,13 +142,13 @@
<div id="preloader-error" class="loading-error"></div>
</div>
<!-- End preloader overlay -->
<button type="button" aria-label="Edit Favourites" class="btn btn-warning bmd-btn-icon" id="edit-favourites" data-toggle="tooltip" title="Edit favourites">
<button type="button" aria-label="Edit Favourites" class="btn btn-warning bmd-btn-icon" id="edit-favourites" data-bs-toggle="tooltip" title="Edit favourites">
<i class="material-icons" aria-hidden="true">star</i>
</button>
<div tabindex="0" id="content-wrapper">
<div id="banner" class="row">
<div class="col" style="text-align: left; padding-left: 10px;">
<a href="#" data-toggle="modal" data-target="#download-modal" data-help-title="Downloading CyberChef" data-help="<p>CyberChef can be downloaded to run locally or hosted within your own network. It has no server-side component so all that is required is that the ZIP file is uncompressed and the files are accessible.</p><p>As a user, it is worth noting that unofficial versions of CyberChef could have been modified to introduce Input and/or Recipe exfiltration. We recommend always using the official, open source, up-to-date version of CyberChef hosted at <a href='https://gchq.github.io/CyberChef'>https://gchq.github.io/CyberChef</a> if accessible.</p><p>The Network tab in your browser's Developer console (F12) can be used to inspect the network requests made by a website. This can confirm that no data is uploaded when a CyberChef recipe is baked.</p>">Download CyberChef <i class="material-icons">file_download</i></a>
<a href="#" data-bs-toggle="modal" data-bs-target="#download-modal" data-help-title="Downloading CyberChef" data-help="<p>CyberChef can be downloaded to run locally or hosted within your own network. It has no server-side component so all that is required is that the ZIP file is uncompressed and the files are accessible.</p><p>As a user, it is worth noting that unofficial versions of CyberChef could have been modified to introduce Input and/or Recipe exfiltration. We recommend always using the official, open source, up-to-date version of CyberChef hosted at <a href='https://gchq.github.io/CyberChef'>https://gchq.github.io/CyberChef</a> if accessible.</p><p>The Network tab in your browser's Developer console (F12) can be used to inspect the network requests made by a website. This can confirm that no data is uploaded when a CyberChef recipe is baked.</p>">Download CyberChef <i class="material-icons">file_download</i></a>
</div>
<div class="col-md-6" id="notice-wrapper">
<span id="notice">
@ -164,7 +164,7 @@
</div>
<div class="col" style="text-align: right; padding-right: 0;">
<a href="#" id="options" data-help-title="Options and Settings" data-help="Configurable options to change how CyberChef behaves. These settings are stored in your browser's local storage, meaning they will persist between sessions that use the same browser profile.">Options <i class="material-icons">settings</i></a>
<a href="#" id="support" data-toggle="modal" data-target="#support-modal" data-help-title="About / Support" data-help="This pane provides information about the CyberChef web app, how to use some of the features, and how to raise bug reports.">About / Support <i class="material-icons">help</i></a>
<a href="#" id="support" data-bs-toggle="modal" data-bs-target="#support-modal" data-help-title="About / Support" data-help="This pane provides information about the CyberChef web app, how to use some of the features, and how to raise bug reports.">About / Support <i class="material-icons">help</i></a>
</div>
</div>
<div id="workspace-wrapper">
@ -182,16 +182,16 @@
<div class="title no-select">
Recipe
<span class="pane-controls hide-on-maximised-output">
<button type="button" aria-label="Hide arguments" class="btn btn-primary bmd-btn-icon" id="hide-icon" data-toggle="tooltip" title="Hide arguments" hide-args="false" data-help-title="Hiding every Operation's argument view in a Recipe" data-help="Clicking 'Hide arguments' will hide all the argument views for every Operation in the Recipe, to save space when you have too many Operation in your Recipe">
<button type="button" aria-label="Hide arguments" class="btn btn-primary bmd-btn-icon" id="hide-icon" data-bs-toggle="tooltip" title="Hide arguments" hide-args="false" data-help-title="Hiding every Operation's argument view in a Recipe" data-help="Clicking 'Hide arguments' will hide all the argument views for every Operation in the Recipe, to save space when you have too many Operation in your Recipe">
<i class="material-icons">keyboard_arrow_up</i>
</button>
<button type="button" aria-label="Save recipe" class="btn btn-primary bmd-btn-icon" id="save" data-toggle="tooltip" title="Save recipe" data-help-title="Saving a recipe" data-help="<p>Recipes can be represented in a few different formats and saved for use at a later date. You can either copy the Recipe configuration and save it somewhere offline for later use, or use your browser's local storage.</p><ul><li><b>Deep link:</b> The easiest way to share a CyberChef Recipe is to copy the deep link, either from the address bar (which is updated as the Recipe or Input changes), or from the 'Save recipe' pane. When you visit this link, the Recipe and Input will be populated from where you left off.</li><li><b>Chef format:</b> This custom format is designed to be compact and easily readable. It is the format used in CyberChef's URL, so it largely uses characters that do not have to be escaped in URL encoding, making it a little easier to understand what a CyberChef URL contains.</li><li><b>Clean JSON:</b> This JSON format uses whitespace and indentation in a way that makes the Recipe easy to read.</li><li><b>Compact JSON:</b> This is the most compact way that the Recipe can be represented in JSON.</li><li><b>Local storage:</b> Alternatively, you can enter a name into the 'Recipe name' field and save to your browser's local storage. The Recipe will then be available to load from the 'Load Recipe' pane as long as you are using the same browser profile. Be aware that if your browser profile is cleaned, you may lose this data.</li></ul>">
<button type="button" aria-label="Save recipe" class="btn btn-primary bmd-btn-icon" id="save" data-bs-toggle="tooltip" title="Save recipe" data-help-title="Saving a recipe" data-help="<p>Recipes can be represented in a few different formats and saved for use at a later date. You can either copy the Recipe configuration and save it somewhere offline for later use, or use your browser's local storage.</p><ul><li><b>Deep link:</b> The easiest way to share a CyberChef Recipe is to copy the deep link, either from the address bar (which is updated as the Recipe or Input changes), or from the 'Save recipe' pane. When you visit this link, the Recipe and Input will be populated from where you left off.</li><li><b>Chef format:</b> This custom format is designed to be compact and easily readable. It is the format used in CyberChef's URL, so it largely uses characters that do not have to be escaped in URL encoding, making it a little easier to understand what a CyberChef URL contains.</li><li><b>Clean JSON:</b> This JSON format uses whitespace and indentation in a way that makes the Recipe easy to read.</li><li><b>Compact JSON:</b> This is the most compact way that the Recipe can be represented in JSON.</li><li><b>Local storage:</b> Alternatively, you can enter a name into the 'Recipe name' field and save to your browser's local storage. The Recipe will then be available to load from the 'Load Recipe' pane as long as you are using the same browser profile. Be aware that if your browser profile is cleaned, you may lose this data.</li></ul>">
<i class="material-icons" aria-hidden="true">save</i>
</button>
<button type="button" aria-label="Load recipe" class="btn btn-primary bmd-btn-icon" id="load" data-toggle="tooltip" title="Load recipe" data-help-title="Loading a recipe" data-help="<p>Saved recipes can be loaded using one of the following methods:</p><ul><li>If you have a CyberChef deep link, simply visit that link and the Recipe and Input will be populated automatically.</li><li>If you have a Recipe string in any of the accepted formats, paste it into the 'Load recipe' pane textbox and click 'Load'.</li><li>If you have saved a Recipe to your browser's local storage, it should be available in the dropdown menu in the 'Load recipe' pane. If it is not there, you may not be using the same browser profile, or your profile may have been cleared.</li></ul>">
<button type="button" aria-label="Load recipe" class="btn btn-primary bmd-btn-icon" id="load" data-bs-toggle="tooltip" title="Load recipe" data-help-title="Loading a recipe" data-help="<p>Saved recipes can be loaded using one of the following methods:</p><ul><li>If you have a CyberChef deep link, simply visit that link and the Recipe and Input will be populated automatically.</li><li>If you have a Recipe string in any of the accepted formats, paste it into the 'Load recipe' pane textbox and click 'Load'.</li><li>If you have saved a Recipe to your browser's local storage, it should be available in the dropdown menu in the 'Load recipe' pane. If it is not there, you may not be using the same browser profile, or your profile may have been cleared.</li></ul>">
<i class="material-icons" aria-hidden="true">folder</i>
</button>
<button type="button" aria-label="Clear recipe" class="btn btn-primary bmd-btn-icon" id="clr-recipe" data-toggle="tooltip" title="Clear recipe" data-help-title="Clearing a recipe" data-help="Clicking the 'Clear recipe' button will remove all operations from the Recipe. It will not clear the Input, but it will trigger a Bake if Auto-bake is turned on, which will change the value of the Output.">
<button type="button" aria-label="Clear recipe" class="btn btn-primary bmd-btn-icon" id="clr-recipe" data-bs-toggle="tooltip" title="Clear recipe" data-help-title="Clearing a recipe" data-help="Clicking the 'Clear recipe' button will remove all operations from the Recipe. It will not clear the Input, but it will trigger a Bake if Auto-bake is turned on, which will change the value of the Output.">
<i class="material-icons" aria-hidden="true">delete</i>
</button>
</span>
@ -200,7 +200,7 @@
<div id="controls" class="no-select hide-on-maximised-output">
<div id="controls-content">
<button type="button" class="mx-2 btn btn-lg btn-secondary" id="step" data-toggle="tooltip" title="Step through the recipe" data-help-title="Stepping through the Recipe" data-help="<p>The Step button allows you to execute one operation at a time, rather than running the whole Recipe from beginning to end.</p><p>Step allows you to inspect the data at each stage of the Recipe and understand what is being passed to the next operation.</p>">
<button type="button" class="mx-2 btn btn-lg btn-secondary" id="step" data-bs-toggle="tooltip" title="Step through the recipe" data-help-title="Stepping through the Recipe" data-help="<p>The Step button allows you to execute one operation at a time, rather than running the whole Recipe from beginning to end.</p><p>Step allows you to inspect the data at each stage of the Recipe and understand what is being passed to the next operation.</p>">
Step
</button>
@ -227,21 +227,21 @@
<label for="input-text">Input</label>
<span class="pane-controls">
<div class="io-info" id="input-files-info"></div>
<button type="button" aria-label="Add new input tab" class="btn btn-primary bmd-btn-icon" id="btn-new-tab" data-toggle="tooltip" title="Add a new input tab" data-help-title="Tabs" data-help="<p>New tabs can be created to support multiple Inputs. These tabs have their own associated character encodings and EOL separators, as defined in their status bars.</p><p>The deep link in the URL bar only contains information about the currently active tab.</p>">
<button type="button" aria-label="Add new input tab" class="btn btn-primary bmd-btn-icon" id="btn-new-tab" data-bs-toggle="tooltip" title="Add a new input tab" data-help-title="Tabs" data-help="<p>New tabs can be created to support multiple Inputs. These tabs have their own associated character encodings and EOL separators, as defined in their status bars.</p><p>The deep link in the URL bar only contains information about the currently active tab.</p>">
<i class="material-icons" aria-hidden="true">add</i>
</button>
<button type="button" aria-label="Open folder as input" class="btn btn-primary bmd-btn-icon" id="btn-open-folder" data-toggle="tooltip" title="Open folder as input" data-help-title="Opening a folder" data-help="<p>You can open a whole folder into CyberChef, which will result in each file being loaded into a separate Input tab.</p><p>CyberChef can handle lots of Input files, but be aware that performance may suffer, especially if the files are large in size.</p><p>Folders can also be loaded by dragging them over the Input pane and dropping them.</p>">
<button type="button" aria-label="Open folder as input" class="btn btn-primary bmd-btn-icon" id="btn-open-folder" data-bs-toggle="tooltip" title="Open folder as input" data-help-title="Opening a folder" data-help="<p>You can open a whole folder into CyberChef, which will result in each file being loaded into a separate Input tab.</p><p>CyberChef can handle lots of Input files, but be aware that performance may suffer, especially if the files are large in size.</p><p>Folders can also be loaded by dragging them over the Input pane and dropping them.</p>">
<i class="material-icons" aria-hidden="true">folder_open</i>
<input type="file" id="open-folder" style="display: none" multiple directory webkitdirectory>
</button>
<button type="button" aria-label="Open file as input" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input" data-help-title="Opening a file" data-help="<p>Files can be loaded into CyberChef individually or in groups, either using the 'Open file as input' button, or by dragging and dropping them over the Input pane.</p><p>CyberChef can handle reasonably large files (at least 500MB, depending on hardware), but performance may be impacted and some Operations will run very slowly over large Inputs.</p>">
<button type="button" aria-label="Open file as input" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-bs-toggle="tooltip" title="Open file as input" data-help-title="Opening a file" data-help="<p>Files can be loaded into CyberChef individually or in groups, either using the 'Open file as input' button, or by dragging and dropping them over the Input pane.</p><p>CyberChef can handle reasonably large files (at least 500MB, depending on hardware), but performance may be impacted and some Operations will run very slowly over large Inputs.</p>">
<i class="material-icons" aria-hidden="true">input</i>
<input type="file" id="open-file" style="display: none" multiple>
</button>
<button type="button" aria-label="Clear input and output" class="btn btn-primary bmd-btn-icon" id="clr-io" data-toggle="tooltip" title="Clear input and output" data-help-title="Clearing the Input and Output" data-help="Clicking the 'Clear input and output' button will remove all Inputs and Outputs. It will not clear the Recipe.">
<button type="button" aria-label="Clear input and output" class="btn btn-primary bmd-btn-icon" id="clr-io" data-bs-toggle="tooltip" title="Clear input and output" data-help-title="Clearing the Input and Output" data-help="Clicking the 'Clear input and output' button will remove all Inputs and Outputs. It will not clear the Recipe.">
<i class="material-icons" aria-hidden="true">delete</i>
</button>
<button type="button" aria-label="Reset pane layout" class="btn btn-primary bmd-btn-icon" id="reset-layout" data-toggle="tooltip" title="Reset pane layout" data-help-title="Resetting the pane layout" data-help="CyberChef's panes can be resized to suit your area of focus. This button will reset the pane sizes to their default configuration.">
<button type="button" aria-label="Reset pane layout" class="btn btn-primary bmd-btn-icon" id="reset-layout" data-bs-toggle="tooltip" title="Reset pane layout" data-help-title="Resetting the pane layout" data-help="CyberChef's panes can be resized to suit your area of focus. This button will reset the pane sizes to their default configuration.">
<i class="material-icons" aria-hidden="true">view_compact</i>
</button>
</span>
@ -252,7 +252,7 @@
<span id="btn-previous-input-tab" class="input-tab-buttons">
&lt;
</span>
<span id="btn-input-tab-dropdown" class="input-tab-buttons" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span id="btn-input-tab-dropdown" class="input-tab-buttons" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
···
</span>
<div class="dropdown-menu" aria-labelledby="btn-input-tab-dropdown">
@ -281,29 +281,29 @@
<label for="output-text">Output</label>
<span class="pane-controls">
<div class="io-info" id="bake-info"></div>
<button type="button" class="btn btn-primary bmd-btn-icon" id="save-all-to-file" data-toggle="tooltip" title="Save all outputs to a zip file" style="display: none" data-help-title="Saving all outputs to a zip file" data-help="<p>When operating with multiple tabbed Inputs and Outputs, you can use this button to save off all the Outputs at once in a ZIP file.</p><p>Use the 'Bake' button to bake all Inputs at once.</p><p>You will be given the choice to specify the file extension for the Outputs, or you can let CyberChef attempt to detect the filetype of each one. If an Output's type is not clear, CyberChef will use the '.dat' extension.</p>">
<button type="button" class="btn btn-primary bmd-btn-icon" id="save-all-to-file" data-bs-toggle="tooltip" title="Save all outputs to a zip file" style="display: none" data-help-title="Saving all outputs to a zip file" data-help="<p>When operating with multiple tabbed Inputs and Outputs, you can use this button to save off all the Outputs at once in a ZIP file.</p><p>Use the 'Bake' button to bake all Inputs at once.</p><p>You will be given the choice to specify the file extension for the Outputs, or you can let CyberChef attempt to detect the filetype of each one. If an Output's type is not clear, CyberChef will use the '.dat' extension.</p>">
<i class="material-icons">archive</i>
</button>
<button type="button" aria-label="save" class="btn btn-primary bmd-btn-icon" id="save-to-file" data-toggle="tooltip" title="Save output to file" data-help-title="Saving output to a file" data-help="The currently active Output can be saved to a file. You will be asked to specify a filename. CyberChef will attempt to guess the correct file extension based on the data. If a file type cannot be detected, the extension defaults to '.dat' but can be changed manually.">
<button type="button" aria-label="save" class="btn btn-primary bmd-btn-icon" id="save-to-file" data-bs-toggle="tooltip" title="Save output to file" data-help-title="Saving output to a file" data-help="The currently active Output can be saved to a file. You will be asked to specify a filename. CyberChef will attempt to guess the correct file extension based on the data. If a file type cannot be detected, the extension defaults to '.dat' but can be changed manually.">
<i class="material-icons" aria-hidden="true">save</i>
</button>
<button type="button" aria-label="copy content" class="btn btn-primary bmd-btn-icon" id="copy-output" data-toggle="tooltip" title="Copy raw output to the clipboard" data-help-title="Copying raw output to the clipboard" data-help="<p>Data can be copied from the Output in the normal way by selecting text and copying it. This button provides a quick way of copying the entire output to the clipboard without having to select it. It directly copies the raw data rather than selecting text in the Output editor. Each method will have the same result, but the button may be more efficient for large Outputs as it does not require any DOM interaction.</p>">
<button type="button" aria-label="copy content" class="btn btn-primary bmd-btn-icon" id="copy-output" data-bs-toggle="tooltip" title="Copy raw output to the clipboard" data-help-title="Copying raw output to the clipboard" data-help="<p>Data can be copied from the Output in the normal way by selecting text and copying it. This button provides a quick way of copying the entire output to the clipboard without having to select it. It directly copies the raw data rather than selecting text in the Output editor. Each method will have the same result, but the button may be more efficient for large Outputs as it does not require any DOM interaction.</p>">
<i class="material-icons" aria-hidden="true">content_copy</i>
</button>
<button type="button" aria-label="replace input with output" class="btn btn-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Replace input with output" data-help-title="Replacing input with output" data-help="<p>This button moves the currently active Output data into the currently active Input tab, overwriting whatever data was already there.</p><p>The Input character encoding and EOL sequence will be changed to match the current Output values, so that the data is interpreted correctly.</p>">
<button type="button" aria-label="replace input with output" class="btn btn-primary bmd-btn-icon" id="switch" data-bs-toggle="tooltip" title="Replace input with output" data-help-title="Replacing input with output" data-help="<p>This button moves the currently active Output data into the currently active Input tab, overwriting whatever data was already there.</p><p>The Input character encoding and EOL sequence will be changed to match the current Output values, so that the data is interpreted correctly.</p>">
<i class="material-icons" aria-hidden="true">open_in_browser</i>
</button>
<button type="button" aria-label="maximise output pane" class="btn btn-primary bmd-btn-icon" id="maximise-output" data-toggle="tooltip" title="Maximise output pane" data-help-title="Maximising the Output pane" data-help="This button allows you to view the Output pane at maximum size, hiding the Operations, Recipe and Input panes. You can restore the pane to its normal size by clicking the same button again.">
<button type="button" aria-label="maximise output pane" class="btn btn-primary bmd-btn-icon" id="maximise-output" data-bs-toggle="tooltip" title="Maximise output pane" data-help-title="Maximising the Output pane" data-help="This button allows you to view the Output pane at maximum size, hiding the Operations, Recipe and Input panes. You can restore the pane to its normal size by clicking the same button again.">
<i class="material-icons" aria-hidden="true">fullscreen</i>
</button>
</span>
<button type="button" class="btn btn-primary bmd-btn-icon hidden" id="magic" data-toggle="tooltip" title="Magic!" data-html="true" data-help-title="CyberChef Magic!" data-help="<p>One of CyberChef's best features is its ability to automatically detect which Operations might make more sense of your data. The Magic button appears when CyberChef has a suggested Operation for you based on the data in the Output.</p><p>Clicking on the button will add the suggested Operation(s) to your Recipe.</p><p>This background Magic detection will inspect your Output up to three levels deep and attempt to unwrap it using a range of techniques. For more control, use the 'Magic' operation, which allows you to configure greater depth and filter based on various parameters.</p><p>Further information about CyberChef Magic can be found <a href='https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic'>here</a>.</p>">
<button type="button" class="btn btn-primary bmd-btn-icon hidden" id="magic" data-bs-toggle="tooltip" title="Magic!" data-bs-html="true" data-help-title="CyberChef Magic!" data-help="<p>One of CyberChef's best features is its ability to automatically detect which Operations might make more sense of your data. The Magic button appears when CyberChef has a suggested Operation for you based on the data in the Output.</p><p>Clicking on the button will add the suggested Operation(s) to your Recipe.</p><p>This background Magic detection will inspect your Output up to three levels deep and attempt to unwrap it using a range of techniques. For more control, use the 'Magic' operation, which allows you to configure greater depth and filter based on various parameters.</p><p>Further information about CyberChef Magic can be found <a href='https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic'>here</a>.</p>">
<svg width="22" height="22" viewBox="0 0 24 24">
<path d="M7.5,5.6L5,7L6.4,4.5L5,2L7.5,3.4L10,2L8.6,4.5L10,7L7.5,5.6M19.5,15.4L22,14L20.6,16.5L22,19L19.5,17.6L17,19L18.4,16.5L17,14L19.5,15.4M22,2L20.6,4.5L22,7L19.5,5.6L17,7L18.4,4.5L17,2L19.5,3.4L22,2M13.34,12.78L15.78,10.34L13.66,8.22L11.22,10.66L13.34,12.78M14.37,7.29L16.71,9.63C17.1,10 17.1,10.65 16.71,11.04L5.04,22.71C4.65,23.1 4,23.1 3.63,22.71L1.29,20.37C0.9,20 0.9,19.35 1.29,18.96L12.96,7.29C13.35,6.9 14,6.9 14.37,7.29Z" />
</svg>
</button>
<span id="stale-indicator" class="hidden" data-toggle="tooltip" title="The output is stale. The input or recipe has changed since this output was generated. Bake again to get the new value." data-help-title="Staleness indicator" data-help="The staleness indicator is displayed when the Recipe or Input has changed but the Output has not yet been updated to reflect this. It is most commonly displayed when Auto-bake is turned off and indicates that you need to Bake in order to see an accurate Output.">
<span id="stale-indicator" class="hidden" data-bs-toggle="tooltip" title="The output is stale. The input or recipe has changed since this output was generated. Bake again to get the new value." data-help-title="Staleness indicator" data-help="The staleness indicator is displayed when the Recipe or Input has changed but the Output has not yet been updated to reflect this. It is most commonly displayed when Auto-bake is turned off and indicates that you need to Bake in order to see an accurate Output.">
<i class="material-icons">access_time</i>
</span>
</div>
@ -313,7 +313,7 @@
<span id="btn-previous-output-tab" class="output-tab-buttons">
&lt;
</span>
<span id="btn-output-tab-dropdown" class="output-tab-buttons" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span id="btn-output-tab-dropdown" class="output-tab-buttons" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
···
</span>
<div class="dropdown-menu" aria-labelledby="btn-input-tab-dropdown">
@ -353,13 +353,13 @@
<div class="form-group">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" href="#chef-format" role="tab" data-toggle="tab">Chef format</a>
<a class="nav-link active" href="#chef-format" role="tab" data-bs-toggle="tab">Chef format</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#clean-json" role="tab" data-toggle="tab">Clean JSON</a>
<a class="nav-link" href="#clean-json" role="tab" data-bs-toggle="tab">Clean JSON</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#compact-json" role="tab" data-toggle="tab">Compact JSON</a>
<a class="nav-link" href="#compact-json" role="tab" data-bs-toggle="tab">Compact JSON</a>
</li>
</ul>
<div class="tab-content" id="save-texts">
@ -381,8 +381,8 @@
</div>
</div>
<div class="modal-footer" id="save-footer">
<button type="button" class="btn btn-primary" id="save-button" data-dismiss="modal">Save</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Done</button>
<button type="button" class="btn btn-primary" id="save-button" data-bs-dismiss="modal">Save</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Done</button>
</div>
<div class="modal-body">
<div class="form-group" id="save-link-group">
@ -422,9 +422,9 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="load-button" data-dismiss="modal">Load</button>
<button type="button" class="btn btn-primary" id="load-button" data-bs-dismiss="modal">Load</button>
<button type="button" class="btn btn-danger" id="load-delete-button">Delete</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
@ -532,7 +532,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
@ -540,7 +540,7 @@
<div class="modal fade" id="favourites-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" data-help-proxy="a[data-target='#catFavourites']">
<div class="modal-content" data-help-proxy="a[data-bs-target='#catFavourites']">
<div class="modal-header">
<h5 class="modal-title">Edit Favourites</h5>
</div>
@ -556,9 +556,9 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="reset-favourites">Reset favourites to default</button>
<button type="button" class="btn btn-success" data-dismiss="modal" id="save-favourites">Save</button>
<button type="button" class="btn btn-danger" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="reset-favourites">Reset favourites to default</button>
<button type="button" class="btn btn-success" data-bs-dismiss="modal" id="save-favourites">Save</button>
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
@ -584,22 +584,22 @@
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<a id="tab-1" class="nav-link active" href="#faqs" aria-controls="profile" role="tab" data-toggle="tab">
<a id="tab-1" class="nav-link active" href="#faqs" aria-controls="profile" role="tab" data-bs-toggle="tab">
FAQs
</a>
</li>
<li class="nav-item" role="presentation">
<a id="tab-2" class="nav-link" href="#report-bug" aria-controls="messages" role="tab" data-toggle="tab">
<a id="tab-2" class="nav-link" href="#report-bug" aria-controls="messages" role="tab" data-bs-toggle="tab">
Report a bug
</a>
</li>
<li class="nav-item" role="presentation">
<a id="tab-3" class="nav-link" href="#about" aria-controls="messages" role="tab" data-toggle="tab">
<a id="tab-3" class="nav-link" href="#about" aria-controls="messages" role="tab" data-bs-toggle="tab">
About
</a>
</li>
<li class="nav-item" role="presentation">
<a id="tab-4" class="nav-link" href="#keybindings" aria-controls="messages" role="tab" data-toggle="tab">
<a id="tab-4" class="nav-link" href="#keybindings" aria-controls="messages" role="tab" data-bs-toggle="tab">
Keybindings
</a>
</li>
@ -607,7 +607,7 @@
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="faqs" data-help-title="FAQ pane" data-help="The Frequently Asked Questions pane provides answers to some of the most common queries people have about CyberChef.">
<br>
<a class="btn btn-primary" data-toggle="collapse" data-target="#faq-contextual-help">
<a class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#faq-contextual-help">
How does X feature work?
</a>
<div class="collapse" id="faq-contextual-help">
@ -615,7 +615,7 @@
</div>
<br>
<a class="btn btn-primary" data-toggle="collapse" data-target="#faq-examples">
<a class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#faq-examples">
What sort of things can I do with CyberChef?
</a>
<div class="collapse" id="faq-examples">
@ -633,7 +633,7 @@
</div>
<br>
<a class="btn btn-primary" data-toggle="collapse" data-target="#faq-load-files">
<a class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#faq-load-files">
Can I load input directly from files?
</a>
<div class="collapse" id="faq-load-files">
@ -643,7 +643,7 @@
</div>
<br>
<a class="btn btn-primary" data-toggle="collapse" data-target="#faq-fork">
<a class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#faq-fork">
How do I run operation X over multiple inputs at once?
</a>
<div class="collapse" id="faq-fork">
@ -653,7 +653,7 @@
</div>
<br>
<a class="btn btn-primary" data-toggle="collapse" data-target="#faq-magic">
<a class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#faq-magic">
How does the 'Magic' operation work?
</a>
<div class="collapse" id="faq-magic">
@ -699,7 +699,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
<a href="https://github.com/gchq/CyberChef">
<img aria-hidden="true" style="position: absolute; top: 0; right: 0; border: 0;" src="<%- require('../static/images/fork_me.png') %>" alt="Fork me on GitHub">
@ -719,7 +719,7 @@
<button type="button" class="btn btn-success" id="confirm-yes">
Yes
</button>
<button type="button" class="btn btn-danger" id="confirm-no" data-dismiss="modal">
<button type="button" class="btn btn-danger" id="confirm-no" data-bs-dismiss="modal">
No
</button>
</div>
@ -763,7 +763,7 @@
<input type="text" class="form-control toggle-string" id="input-filter">
</div>
<div class="input-group-append">
<button class="btn btn-secondary dropdown-toggle" id="input-filter-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">CONTENT</button>
<button class="btn btn-secondary dropdown-toggle" id="input-filter-button" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">CONTENT</button>
<div class="dropdown-menu toggle-dropdown">
<a class="dropdown-item" id="input-filter-content">Content</a>
<a class="dropdown-item" id="input-filter-filename">Filename</a>
@ -780,7 +780,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="input-filter-refresh">Refresh</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
@ -842,7 +842,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="output-filter-refresh">Refresh</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
@ -850,7 +850,7 @@
<div class="modal fade" id="download-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" data-help-proxy="a[data-target='#download-modal']">
<div class="modal-content" data-help-proxy="a[data-bs-target='#download-modal']">
<div class="modal-header">
<h5 class="modal-title">Download CyberChef</h5>
</div>
@ -879,7 +879,7 @@
<a href="CyberChef_v<%= htmlWebpackPlugin.options.version %>.zip" download class="btn btn-outline-primary">Download ZIP file</a>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Ok</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Ok</button>
</div>
</div>
</div>
@ -899,7 +899,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="help-ok" data-dismiss="modal">Ok</button>
<button type="button" class="btn btn-primary" id="help-ok" data-bs-dismiss="modal">Ok</button>
</div>
</div>
</div>

View File

@ -8,17 +8,14 @@
import "./stylesheets/index.js";
// Libs
import "arrive";
import "snackbarjs";
import "bootstrap-material-design/js/index";
import "bootstrap-colorpicker";
import moment from "moment-timezone";
import "bootstrap";
import { parse } from "date-fns";
import * as CanvasComponents from "../core/lib/CanvasComponents.mjs";
// CyberChef
import App from "./App.mjs";
import Categories from "../core/config/Categories.json" assert {type: "json"};
import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"};
import Categories from "../core/config/Categories.json" with {type: "json"};
import OperationConfig from "../core/config/OperationConfig.json" with {type: "json"};
/**
@ -60,7 +57,8 @@ function main() {
window.app.setup();
}
window.compileTime = moment.tz(COMPILE_TIME, "DD/MM/YYYY HH:mm:ss z", "UTC").valueOf();
// Parse compile time string (format: "DD/MM/YYYY HH:mm:ss UTC")
window.compileTime = parse(COMPILE_TIME.replace(/ UTC$/, ""), "dd/MM/yyyy HH:mm:ss", new Date()).getTime();
window.compileMessage = COMPILE_MSG;
// Make libs available to operation outputs

View File

@ -10,8 +10,7 @@
import "highlight.js/styles/vs.css";
/* Frameworks */
import "bootstrap-material-design/dist/css/bootstrap-material-design.css";
import "bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css";
import "bootstrap/dist/css/bootstrap.css";
/* CyberChef styles */
import "./index.css";

View File

@ -0,0 +1,64 @@
/**
* Lightweight snackbar/toast notification utility.
* Replaces snackbarjs dependency (which required jQuery).
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
let container = null;
/**
* Get or create the snackbar container.
* @returns {HTMLElement}
*/
function getContainer() {
if (!container) {
container = document.getElementById("snackbar-container");
if (!container) {
container = document.createElement("div");
container.id = "snackbar-container";
container.style.cssText = "position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:9999;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;";
document.body.appendChild(container);
}
}
return container;
}
/**
* Show a snackbar notification.
*
* @param {Object} options
* @param {string} options.content - The message to display
* @param {number} [options.timeout=2000] - Duration in ms before auto-dismiss
* @param {string} [options.style="snackbar"] - CSS class style
*/
export function showSnackbar({ content, timeout = 2000, style = "snackbar" }) {
const el = document.createElement("div");
el.className = `cc-snackbar ${style}`;
el.textContent = content;
el.style.cssText = "background:#323232;color:#fff;padding:10px 24px;border-radius:4px;font-size:14px;opacity:0;transition:opacity 0.3s;pointer-events:auto;max-width:500px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,0.3);";
const cont = getContainer();
cont.appendChild(el);
// Trigger fade-in
requestAnimationFrame(() => {
el.style.opacity = "1";
});
// Auto-dismiss
if (timeout > 0) {
setTimeout(() => {
el.style.opacity = "0";
setTimeout(() => el.remove(), 300);
}, timeout);
}
}
/**
* jQuery-compatible $.snackbar() replacement.
* Called as: showSnackbar({ content: "message" })
*/
export default showSnackbar;

View File

@ -7,6 +7,7 @@
import {showSidePanel} from "./sidePanel.mjs";
import Utils from "../../core/Utils.mjs";
import {isImage, detectFileType} from "../../core/lib/FileType.mjs";
import * as bootstrap from "bootstrap";
/**
* A File Details extension for CodeMirror
@ -40,7 +41,7 @@ class FileDetailsPanel {
const fileThumb = require("../static/images/file-128x128.png");
dom.innerHTML = `
<div class="${this.hidden ? "file-details-toggle-hidden" : "file-details-toggle-shown"}"
data-toggle="tooltip"
data-bs-toggle="tooltip"
title="${this.hidden ? "Show" : "Hide"} file details">
${this.hidden ? "&#10096;" : "&#10097;"}
</div>
@ -130,7 +131,9 @@ function makePanel(opts) {
update(update) {
},
mount() {
$("[data-toggle='tooltip']").tooltip();
document.querySelectorAll("[data-bs-toggle='tooltip']").forEach(el => {
new bootstrap.Tooltip(el);
});
}
};
};

View File

@ -344,21 +344,21 @@ class StatusBarPanel {
*/
constructLHS() {
return `
<span data-toggle="tooltip" title="${this.label} length" data-help-title="${this.label} length" data-help="This number represents the number of characters in the ${this.label}.<br><br>The CRLF end of line separator is counted as two characters which impacts this value.">
<span data-bs-toggle="tooltip" title="${this.label} length" data-help-title="${this.label} length" data-help="This number represents the number of characters in the ${this.label}.<br><br>The CRLF end of line separator is counted as two characters which impacts this value.">
<i class="material-icons">abc</i>
<span class="stats-length-value"></span>
</span>
<span data-toggle="tooltip" title="Number of lines" data-help-title="Number of lines" data-help="This number represents the number of lines in the ${this.label}. Lines are separated by the End of Line Sequence which can be changed using the EOL selector at the far right of this status bar.">
<span data-bs-toggle="tooltip" title="Number of lines" data-help-title="Number of lines" data-help="This number represents the number of lines in the ${this.label}. Lines are separated by the End of Line Sequence which can be changed using the EOL selector at the far right of this status bar.">
<i class="material-icons">sort</i>
<span class="stats-lines-value"></span>
</span>
<span class="sel-info" data-toggle="tooltip" title="Main selection" data-help-title="Main selection" data-help="These numbers show which offsets have been selected and how many characters are in the current selection. If multiple selections are made, these numbers refer to the latest one. ">
<span class="sel-info" data-bs-toggle="tooltip" title="Main selection" data-help-title="Main selection" data-help="These numbers show which offsets have been selected and how many characters are in the current selection. If multiple selections are made, these numbers refer to the latest one. ">
<i class="material-icons">highlight_alt</i>
<span class="sel-start-value"></span>\u279E<span class="sel-end-value"></span>
(<span class="sel-length-value"></span> selected)
</span>
<span class="cur-offset-info" data-toggle="tooltip" title="Cursor offset" data-help-title="Cursor offset" data-help="This number indicates what the current offset of the cursor is from the beginning of the ${this.label}.<br><br>The CRLF end of line separator is counted as two characters which impacts this value.">
<span class="cur-offset-info" data-bs-toggle="tooltip" title="Cursor offset" data-help-title="Cursor offset" data-help="This number indicates what the current offset of the cursor is from the beginning of the ${this.label}.<br><br>The CRLF end of line separator is counted as two characters which impacts this value.">
<i class="material-icons">location_on</i>
<span class="cur-offset-value"></span>
</span>`;
@ -386,13 +386,13 @@ class StatusBarPanel {
}
return `
<span class="baking-time-info" style="display: none" data-toggle="tooltip" data-html="true" title="Baking time" data-help-title="Baking time" data-help="The baking time is the total time between data being read from the input, processed, and then displayed in the output.<br><br>The 'Threading overhead' value accounts for the transfer of data between different processing threads, as well as some garbage collection. It is not included in the overall bake time displayed in the status bar as it is largely influenced by background operating system and browser activity which can fluctuate significantly.">
<span class="baking-time-info" style="display: none" data-bs-toggle="tooltip" data-bs-html="true" title="Baking time" data-help-title="Baking time" data-help="The baking time is the total time between data being read from the input, processed, and then displayed in the output.<br><br>The 'Threading overhead' value accounts for the transfer of data between different processing threads, as well as some garbage collection. It is not included in the overall bake time displayed in the status bar as it is largely influenced by background operating system and browser activity which can fluctuate significantly.">
<i class="material-icons">schedule</i>
<span class="baking-time-value"></span>ms
</span>
<div class="cm-status-bar-select chr-enc-select" data-help-title="${this.label} character encoding" data-help="${chrEncHelpText}">
<span class="cm-status-bar-select-btn" data-toggle="tooltip" data-html="true" data-placement="left" title="${this.label} character encoding">
<span class="cm-status-bar-select-btn" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="left" title="${this.label} character encoding">
<i class="material-icons">text_fields</i> <span class="chr-enc-value">Raw Bytes</span>
</span>
<div class="cm-status-bar-select-content">
@ -412,7 +412,7 @@ class StatusBarPanel {
</div>
<div class="cm-status-bar-select eol-select" data-help-title="${this.label} EOL sequence" data-help="${eolHelpText}">
<span class="cm-status-bar-select-btn" data-toggle="tooltip" data-html="true" data-placement="left" title="End of line sequence">
<span class="cm-status-bar-select-btn" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="left" title="End of line sequence">
<i class="material-icons">keyboard_return</i> <span class="eol-value"></span>
</span>
<div class="cm-status-bar-select-content no-select">

View File

@ -4,7 +4,8 @@
* @license Apache-2.0
*/
import ChefWorker from "worker-loader?inline=no-fallback!../../core/ChefWorker.js";
// Native Webpack 5 worker support (replaces deprecated worker-loader)
const createChefWorker = () => new Worker(new URL("../../core/ChefWorker.js", import.meta.url));
/**
* Waiter to handle conversations with a ChefWorker in the background.
@ -33,7 +34,7 @@ class BackgroundWorkerWaiter {
*/
registerChefWorker() {
log.debug("Registering new background ChefWorker");
this.chefWorker = new ChefWorker();
this.chefWorker = createChefWorker();
this.chefWorker.addEventListener("message", this.handleChefMessage.bind(this));
this.chefWorker.postMessage({
action: "setLogPrefix",

View File

@ -4,6 +4,8 @@
* @license Apache-2.0
*/
import * as bootstrap from "bootstrap";
/**
* Waiter to handle keybindings to CyberChef functions (i.e. Bake, Step, Save, Load etc.)
*/
@ -300,7 +302,7 @@ class BindingsWaiter {
document.querySelector("#help-modal .modal-body").innerHTML = helpText;
document.querySelector("#help-modal #help-title").innerHTML = helpTitle;
$("#help-modal").modal();
bootstrap.Modal.getOrCreateInstance(document.getElementById("help-modal")).show();
}
}

View File

@ -6,6 +6,7 @@
import Utils from "../../core/Utils.mjs";
import { eolSeqToCode } from "../utils/editorUtils.mjs";
import * as bootstrap from "bootstrap";
/**
@ -29,12 +30,13 @@ class ControlsWaiter {
* Initialise Bootstrap components
*/
initComponents() {
$("body").bootstrapMaterialDesign();
$("[data-toggle=tooltip]").tooltip({
animation: false,
container: "body",
boundary: "viewport",
trigger: "hover"
document.querySelectorAll("[data-bs-toggle=tooltip]").forEach(el => {
new bootstrap.Tooltip(el, {
animation: false,
container: "body",
boundary: "viewport",
trigger: "hover"
});
});
// Set number of operations in various places in the DOM
@ -211,7 +213,7 @@ class ControlsWaiter {
document.getElementById("save-text-compact").value = recipeStr;
this.initialiseSaveLink(recipeConfig);
$("#save-modal").modal();
bootstrap.Modal.getOrCreateInstance(document.getElementById("save-modal")).show();
}
@ -236,7 +238,7 @@ class ControlsWaiter {
*/
loadClick() {
this.populateLoadRecipesList();
$("#load-modal").modal();
bootstrap.Modal.getOrCreateInstance(document.getElementById("load-modal")).show();
}
@ -355,7 +357,9 @@ class ControlsWaiter {
this.app.setRecipeConfig(recipeConfig);
this.app.autoBake();
$("#rec-list [data-toggle=popover]").popover();
document.querySelectorAll("#rec-list [data-bs-toggle=popover]").forEach(el => {
new bootstrap.Popover(el);
});
} catch (e) {
this.app.alert("Invalid recipe", 2000);
}

View File

@ -5,11 +5,13 @@
* @license Apache-2.0
*/
import LoaderWorker from "worker-loader?inline=no-fallback!../workers/LoaderWorker.js";
import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker.mjs";
// Native Webpack 5 worker support (replaces deprecated worker-loader)
const createLoaderWorker = () => new Worker(new URL("../workers/LoaderWorker.js", import.meta.url));
const createInputWorker = () => new Worker(new URL("../workers/InputWorker.mjs", import.meta.url));
import Utils, {debounce} from "../../core/Utils.mjs";
import {toBase64} from "../../core/lib/Base64.mjs";
import cptable from "codepage";
import * as bootstrap from "bootstrap";
import {
EditorView,
@ -359,7 +361,7 @@ class InputWaiter {
}
log.debug("Adding new InputWorker");
this.inputWorker = new InputWorker();
this.inputWorker = createInputWorker();
this.inputWorker.postMessage({
action: "setLogLevel",
data: log.getLevel()
@ -405,7 +407,7 @@ class InputWaiter {
return -1;
}
log.debug(`Adding new LoaderWorker (${this.loaderWorkers.length + 1}/${this.maxWorkers}).`);
const newWorker = new LoaderWorker();
const newWorker = createLoaderWorker();
const workerId = this.workerId++;
newWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
newWorker.postMessage({
@ -719,7 +721,10 @@ class InputWaiter {
* @param {event} e
*/
toggleFileDetails(e) {
$("[data-toggle='tooltip']").tooltip("hide");
document.querySelectorAll("[data-bs-toggle='tooltip']").forEach(el => {
const tip = bootstrap.Tooltip.getInstance(el);
if (tip) tip.hide();
});
this.fileDetails.hidden = !this.fileDetails.hidden;
this.inputEditorView.dispatch({
effects: this.inputEditorConf.fileDetailsPanel.reconfigure(
@ -1600,7 +1605,7 @@ class InputWaiter {
*/
findTab() {
this.filterTabSearch();
$("#input-tab-modal").modal();
bootstrap.Modal.getOrCreateInstance(document.getElementById("input-tab-modal")).show();
}
/**
@ -1671,7 +1676,7 @@ class InputWaiter {
const inputNum = parseInt(e.target.getAttribute("inputNum"), 10);
if (inputNum <= 0) return;
$("#input-tab-modal").modal("hide");
bootstrap.Modal.getOrCreateInstance(document.getElementById("input-tab-modal")).hide();
this.changeTab(inputNum, this.app.options.syncTabs);
}

View File

@ -7,6 +7,7 @@
import HTMLOperation from "../HTMLOperation.mjs";
import Sortable from "sortablejs";
import {fuzzyMatch, calcMatchRanges} from "../../core/lib/FuzzyMatch.mjs";
import * as bootstrap from "bootstrap";
/**
@ -78,12 +79,15 @@ class OperationsWaiter {
while (searchResultsEl.firstChild) {
try {
$(searchResultsEl.firstChild).popover("dispose");
const existingPopover = bootstrap.Popover.getInstance(searchResultsEl.firstChild);
if (existingPopover) existingPopover.dispose();
} catch (err) {}
searchResultsEl.removeChild(searchResultsEl.firstChild);
}
$("#categories .show").collapse("hide");
document.querySelectorAll("#categories .show").forEach(el => {
bootstrap.Collapse.getOrCreateInstance(el).hide();
});
if (str) {
const matchedOps = this.filterOperations(str, true);
const matchedOpsHtml = matchedOps
@ -183,25 +187,41 @@ class OperationsWaiter {
* @param {Element} el - The element to start selecting from
*/
enableOpsListPopovers(el) {
$(el).find("[data-toggle=popover]").addBack("[data-toggle=popover]")
.popover({trigger: "manual"})
.on("mouseenter", function(e) {
// Collect elements: those inside el plus el itself if it matches
const popoverEls = [];
if (el.matches && el.matches("[data-bs-toggle=popover]")) {
popoverEls.push(el);
}
el.querySelectorAll("[data-bs-toggle=popover]").forEach(e => popoverEls.push(e));
popoverEls.forEach(popEl => {
// Dispose any existing popover to avoid duplicates
const existing = bootstrap.Popover.getInstance(popEl);
if (existing) existing.dispose();
const pop = new bootstrap.Popover(popEl, {trigger: "manual"});
popEl.addEventListener("mouseenter", function(e) {
if (e.buttons > 0) return; // Mouse button held down - likely dragging an operation
const _this = this;
$(this).popover("show");
$(".popover").on("mouseleave", function () {
$(_this).popover("hide");
});
}).on("mouseleave", function () {
const _this = this;
pop.show();
// When the popover tip is shown, attach a mouseleave handler to it
const tip = pop.tip;
if (tip) {
tip.addEventListener("mouseleave", function() {
pop.hide();
});
}
});
popEl.addEventListener("mouseleave", function() {
setTimeout(function() {
// Determine if the popover associated with this element is being hovered over
if ($(_this).data("bs.popover") &&
($(_this).data("bs.popover").tip && !$($(_this).data("bs.popover").tip).is(":hover"))) {
$(_this).popover("hide");
const tip = pop.tip;
if (tip && !tip.matches(":hover")) {
pop.hide();
}
}, 50);
});
});
}
@ -249,13 +269,15 @@ class OperationsWaiter {
onFilter: function (evt) {
const el = editableList.closest(evt.item);
if (el && el.parentNode) {
$(el).popover("dispose");
const pop = bootstrap.Popover.getInstance(el);
if (pop) pop.dispose();
el.parentNode.removeChild(el);
}
},
onEnd: function(evt) {
if (this.removeIntent) {
$(evt.item).popover("dispose");
const pop = bootstrap.Popover.getInstance(evt.item);
if (pop) pop.dispose();
evt.item.remove();
}
}.bind(this),
@ -269,8 +291,10 @@ class OperationsWaiter {
this.removeIntent = false;
}.bind(this));
$("#edit-favourites-list [data-toggle=popover]").popover();
$("#favourites-modal").modal();
document.querySelectorAll("#edit-favourites-list [data-bs-toggle=popover]").forEach(el => {
new bootstrap.Popover(el);
});
bootstrap.Modal.getOrCreateInstance(document.getElementById("favourites-modal")).show();
}

View File

@ -4,6 +4,8 @@
* @license Apache-2.0
*/
import * as bootstrap from "bootstrap";
/**
* Waiter to handle events related to the CyberChef options.
*/
@ -62,7 +64,7 @@ class OptionsWaiter {
*/
optionsClick(e) {
e.preventDefault();
$("#options-modal").modal();
bootstrap.Modal.getOrCreateInstance(document.getElementById("options-modal")).show();
}

View File

@ -10,7 +10,9 @@ import Dish from "../../core/Dish.mjs";
import {isUTF8, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs";
import {detectFileType} from "../../core/lib/FileType.mjs";
import FileSaver from "file-saver";
import ZipWorker from "worker-loader?inline=no-fallback!../workers/ZipWorker.mjs";
import * as bootstrap from "bootstrap";
// Native Webpack 5 worker support (replaces deprecated worker-loader)
const createZipWorker = () => new Worker(new URL("../workers/ZipWorker.mjs", import.meta.url));
import {
EditorView,
@ -987,7 +989,7 @@ class OutputWaiter {
downloadButton.firstElementChild.innerHTML = "autorenew";
log.debug("Creating ZipWorker");
this.zipWorker = new ZipWorker();
this.zipWorker = createZipWorker();
this.zipWorker.postMessage({
action: "setLogLevel",
data: log.getLevel()
@ -1508,7 +1510,8 @@ class OutputWaiter {
switchButton.classList.add("spin");
switchButton.disabled = true;
switchButton.firstElementChild.innerHTML = "autorenew";
$(switchButton).tooltip("hide");
const switchTooltip = bootstrap.Tooltip.getInstance(switchButton);
if (switchTooltip) switchTooltip.hide();
const activeData = await this.getDishBuffer(this.getOutputDish(activeTab));
@ -1540,13 +1543,13 @@ class OutputWaiter {
this.app.columnSplitter.collapse(1);
this.app.ioSplitter.collapse(0);
$(el).attr("data-original-title", "Restore output pane");
$(el).attr("aria-label", "Restore output pane");
el.setAttribute("data-original-title", "Restore output pane");
el.setAttribute("aria-label", "Restore output pane");
el.querySelector("i").innerHTML = "fullscreen_exit";
} else {
document.body.classList.remove("output-maximised");
$(el).attr("data-original-title", "Maximise output pane");
$(el).attr("aria-label", "Maximise output pane");
el.setAttribute("data-original-title", "Maximise output pane");
el.setAttribute("aria-label", "Maximise output pane");
el.querySelector("i").innerHTML = "fullscreen";
this.app.initialiseSplitter(false);
this.app.resetLayout();
@ -1558,7 +1561,7 @@ class OutputWaiter {
*/
findTab() {
this.filterTabSearch();
$("#output-tab-modal").modal();
bootstrap.Modal.getOrCreateInstance(document.getElementById("output-tab-modal")).show();
}
/**
@ -1669,7 +1672,7 @@ class OutputWaiter {
const inputNum = parseInt(e.target.getAttribute("inputNum"), 10);
if (inputNum <= 0) return;
$("#output-tab-modal").modal("hide");
bootstrap.Modal.getOrCreateInstance(document.getElementById("output-tab-modal")).hide();
this.changeTab(inputNum, this.app.options.syncTabs);
}

View File

@ -9,6 +9,7 @@ import Sortable from "sortablejs";
import Utils from "../../core/Utils.mjs";
import {escapeControlChars} from "../utils/editorUtils.mjs";
import DOMPurify from "dompurify";
import * as bootstrap from "bootstrap";
/**
@ -102,15 +103,12 @@ class RecipeWaiter {
// Removes popover element and event bindings from the dragged operation but not the
// event bindings from the one left in the operations list. Without manually removing
// these bindings, we cannot re-initialise the popover on the stub operation.
$(evt.item)
.popover("dispose")
.removeData("bs.popover")
.off("mouseenter")
.off("mouseleave")
.attr("data-toggle", "popover-disabled");
$(evt.clone)
.off(".popover")
.removeData("bs.popover");
const itemPopover = bootstrap.Popover.getInstance(evt.item);
if (itemPopover) itemPopover.dispose();
evt.item.setAttribute("data-toggle", "popover-disabled");
const clonePopover = bootstrap.Popover.getInstance(evt.clone);
if (clonePopover) clonePopover.dispose();
},
onEnd: this.opSortEnd.bind(this)
});
@ -138,7 +136,7 @@ class RecipeWaiter {
enableOpsElement = evt.clone;
} else {
enableOpsElement = evt.item;
$(evt.item).attr("data-toggle", "popover");
evt.item.setAttribute("data-toggle", "popover");
}
this.manager.ops.enableOpsListPopovers(enableOpsElement);
@ -417,7 +415,9 @@ class RecipeWaiter {
el.classList.add("flow-control-op");
}
$(el).find("[data-toggle='tooltip']").tooltip();
el.querySelectorAll("[data-bs-toggle='tooltip']").forEach(tooltipEl => {
new bootstrap.Tooltip(tooltipEl);
});
// Disable auto-bake if this is a manual op
if (op.manualBake && this.app.autoBake_) {

View File

@ -48,7 +48,7 @@ class SeasonalWaiter {
break;
}
if (i === konami.length - 1) {
$("body").children().toggleClass("konami");
document.body.querySelectorAll(":scope > *").forEach(el => el.classList.toggle("konami"));
this.kkeys = [];
}
}

View File

@ -5,8 +5,9 @@
* @license Apache-2.0
*/
import ChefWorker from "worker-loader?inline=no-fallback!../../core/ChefWorker.js";
import DishWorker from "worker-loader?inline=no-fallback!../workers/DishWorker.mjs";
// Native Webpack 5 worker support (replaces deprecated worker-loader)
const createChefWorker = () => new Worker(new URL("../../core/ChefWorker.js", import.meta.url));
const createDishWorker = () => new Worker(new URL("../workers/DishWorker.mjs", import.meta.url));
import { debounce } from "../../core/Utils.mjs";
/**
@ -70,7 +71,7 @@ class WorkerWaiter {
}
log.debug("Adding new DishWorker");
this.dishWorker.worker = new DishWorker();
this.dishWorker.worker = createDishWorker();
this.dishWorker.worker.addEventListener("message", this.handleDishMessage.bind(this));
this.dishWorker.worker.postMessage({
action: "setLogLevel",
@ -96,7 +97,7 @@ class WorkerWaiter {
log.debug(`Adding new ChefWorker (${this.chefWorkers.length + 1}/${this.maxWorkers})`);
// Create a new ChefWorker and send it the docURL
const newWorker = new ChefWorker();
const newWorker = createChefWorker();
newWorker.addEventListener("message", this.handleChefMessage.bind(this));
newWorker.postMessage({
action: "setLogPrefix",

View File

@ -0,0 +1,75 @@
/**
* Playwright E2E tests for CyberChef.
* Replaces Nightwatch browser tests.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import { test, expect } from "@playwright/test";
test.describe("CyberChef App", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
// Wait for the app to fully load
await page.waitForSelector("#preloader", { state: "hidden", timeout: 30000 });
});
test("should load the app", async ({ page }) => {
await expect(page).toHaveTitle(/CyberChef/);
});
test("should have operations panel", async ({ page }) => {
const opsPanel = page.locator("#operations");
await expect(opsPanel).toBeVisible();
});
test("should have input and output panels", async ({ page }) => {
await expect(page.locator("#input")).toBeVisible();
await expect(page.locator("#output")).toBeVisible();
});
test("should search for operations", async ({ page }) => {
const searchBox = page.locator("#search");
await searchBox.fill("Base64");
// Wait for search results
await page.waitForTimeout(500);
const results = page.locator("#search-results .op-title");
await expect(results.first()).toBeVisible();
});
test("should encode to Base64", async ({ page }) => {
// Type input
const inputEditor = page.locator("#input-text .cm-content");
await inputEditor.click();
await page.keyboard.type("Hello, World!");
// Search and add the operation
const searchBox = page.locator("#search");
await searchBox.fill("To Base64");
await page.waitForTimeout(500);
// Click on the operation to add it
const toBase64Op = page.locator("#search-results .op-title").filter({ hasText: "To Base64" });
await toBase64Op.first().dblclick();
// Wait for bake
await page.waitForTimeout(2000);
// Check output
const outputEditor = page.locator("#output-text .cm-content");
await expect(outputEditor).toContainText("SGVsbG8sIFdvcmxkIQ==");
});
test("should handle recipe URL loading", async ({ page }) => {
// Navigate to a recipe URL
await page.goto("/#recipe=To_Base64('A-Za-z0-9%2B/%3D')&input=SGVsbG8");
await page.waitForSelector("#preloader", { state: "hidden", timeout: 30000 });
await page.waitForTimeout(2000);
// Verify the recipe loaded
const recipeList = page.locator("#rec-list .op-title");
await expect(recipeList.first()).toContainText("To Base64");
});
});

View File

@ -0,0 +1,76 @@
/**
* Vitest adapter for CyberChef's TestRegister format.
* Allows existing test files using TestRegister.addTests() to work with Vitest
* without modifying every test file.
*
* Usage in test files:
* import TestRegister from "../../lib/TestRegister.mjs";
* TestRegister.addTests([{ name, input, expectedOutput, recipeConfig }]);
*
* Then in a .test.mjs file:
* import { runRegisteredTests } from "../../lib/VitestAdapter.mjs";
* import "./tests/Base64.mjs"; // registers tests
* runRegisteredTests();
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import { describe, it, expect } from "vitest";
import Chef from "../../src/core/Chef.mjs";
import TestRegister from "./TestRegister.mjs";
/**
* Run all tests registered via TestRegister.addTests() as Vitest tests.
*
* @param {string} [suiteName="CyberChef Operations"] - Name for the test suite
*/
export function runRegisteredTests(suiteName = "CyberChef Operations") {
describe(suiteName, () => {
const tests = TestRegister.tests;
for (const test of tests) {
it(test.name, async () => {
const chef = new Chef();
const result = await chef.bake(
test.input,
test.recipeConfig,
{ returnType: "string" }
);
if (test.expectedError) {
expect(result.error).toBeTruthy();
if (test.expectedOutput) {
expect(result.error.displayStr).toBe(test.expectedOutput);
}
} else if (result.error) {
throw new Error(`Unexpected error: ${result.error.displayStr}`);
} else if ("expectedMatch" in test) {
expect(result.result).toMatch(test.expectedMatch);
} else if ("unexpectedMatch" in test) {
expect(result.result).not.toMatch(test.unexpectedMatch);
} else {
expect(result.result).toBe(test.expectedOutput);
}
});
}
});
}
/**
* Run all API tests registered via TestRegister.addApiTests() as Vitest tests.
*
* @param {string} [suiteName="CyberChef Node API"] - Name for the test suite
*/
export function runRegisteredApiTests(suiteName = "CyberChef Node API") {
describe(suiteName, () => {
const tests = TestRegister.apiTests;
for (const test of tests) {
it(test.name, async () => {
await test.run();
});
}
});
}

View File

@ -1,6 +1,6 @@
import TestRegister from "../../lib/TestRegister.mjs";
import Categories from "../../../src/core/config/Categories.json" assert {type: "json"};
import OperationConfig from "../../../src/core/config/OperationConfig.json" assert {type: "json"};
import Categories from "../../../src/core/config/Categories.json" with {type: "json"};
import OperationConfig from "../../../src/core/config/OperationConfig.json" with {type: "json"};
import it from "../assertionHandler.mjs";
import assert from "assert";

View File

@ -0,0 +1,181 @@
/**
* Vitest test runner for CyberChef operations.
* Imports all existing test files (which register tests via TestRegister.addTests())
* and runs them through the Vitest adapter.
*
* @author CyberChef Modernization
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import { runRegisteredTests } from "../lib/VitestAdapter.mjs";
// Import all operation test files (these call TestRegister.addTests())
import "./tests/A1Z26CipherDecode.mjs";
import "./tests/AESKeyWrap.mjs";
import "./tests/AlternatingCaps.mjs";
import "./tests/AvroToJSON.mjs";
import "./tests/BaconCipher.mjs";
import "./tests/Base32.mjs";
import "./tests/Base45.mjs";
import "./tests/Base58.mjs";
import "./tests/Base62.mjs";
import "./tests/Base64.mjs";
import "./tests/Base85.mjs";
import "./tests/Base92.mjs";
import "./tests/BCD.mjs";
import "./tests/Bech32.mjs";
import "./tests/BitwiseOp.mjs";
import "./tests/BLAKE2b.mjs";
import "./tests/BLAKE2s.mjs";
import "./tests/BLAKE3.mjs";
import "./tests/Bombe.mjs";
import "./tests/BSON.mjs";
import "./tests/ByteRepr.mjs";
import "./tests/CaesarBoxCipher.mjs";
import "./tests/CaretMdecode.mjs";
import "./tests/CartesianProduct.mjs";
import "./tests/CBORDecode.mjs";
import "./tests/CBOREncode.mjs";
import "./tests/CetaceanCipherDecode.mjs";
import "./tests/CetaceanCipherEncode.mjs";
import "./tests/ChaCha.mjs";
import "./tests/ChangeIPFormat.mjs";
import "./tests/CharEnc.mjs";
import "./tests/Charts.mjs";
import "./tests/Ciphers.mjs";
import "./tests/CipherSaber2.mjs";
import "./tests/CMAC.mjs";
import "./tests/Code.mjs";
import "./tests/Colossus.mjs";
import "./tests/Comment.mjs";
import "./tests/Compress.mjs";
import "./tests/ConditionalJump.mjs";
import "./tests/ConvertCoordinateFormat.mjs";
import "./tests/ConvertLeetSpeak.mjs";
import "./tests/ConvertToNATOAlphabet.mjs";
import "./tests/CRCChecksum.mjs";
import "./tests/Crypt.mjs";
import "./tests/CSV.mjs";
import "./tests/DateTime.mjs";
import "./tests/DefangIP.mjs";
import "./tests/DisassembleARM.mjs";
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";
import "./tests/Float.mjs";
import "./tests/FileTree.mjs";
import "./tests/FletcherChecksum.mjs";
import "./tests/Fork.mjs";
import "./tests/FromDecimal.mjs";
import "./tests/GenerateAllChecksums.mjs";
import "./tests/GenerateAllHashes.mjs";
import "./tests/GenerateDeBruijnSequence.mjs";
import "./tests/GenerateQRCode.mjs";
import "./tests/GetAllCasings.mjs";
import "./tests/GOST.mjs";
import "./tests/Gunzip.mjs";
import "./tests/Gzip.mjs";
import "./tests/Hash.mjs";
import "./tests/HASSH.mjs";
import "./tests/HaversineDistance.mjs";
import "./tests/Hex.mjs";
import "./tests/Hexdump.mjs";
import "./tests/HKDF.mjs";
import "./tests/Image.mjs";
import "./tests/IndexOfCoincidence.mjs";
import "./tests/JA3Fingerprint.mjs";
import "./tests/JA4.mjs";
import "./tests/JA3SFingerprint.mjs";
import "./tests/Jsonata.mjs";
import "./tests/JSONBeautify.mjs";
import "./tests/JSONMinify.mjs";
import "./tests/JSONtoCSV.mjs";
import "./tests/JSONtoYAML.mjs";
import "./tests/Jump.mjs";
import "./tests/JWK.mjs";
import "./tests/JWTDecode.mjs";
import "./tests/JWTSign.mjs";
import "./tests/JWTVerify.mjs";
import "./tests/LevenshteinDistance.mjs";
import "./tests/Lorenz.mjs";
import "./tests/LS47.mjs";
import "./tests/LuhnChecksum.mjs";
import "./tests/LZNT1Decompress.mjs";
import "./tests/LZString.mjs";
import "./tests/Magic.mjs";
import "./tests/Media.mjs";
import "./tests/MIMEDecoding.mjs";
import "./tests/Modhex.mjs";
import "./tests/MorseCode.mjs";
import "./tests/MS.mjs";
import "./tests/MultipleBombe.mjs";
import "./tests/MurmurHash3.mjs";
import "./tests/NetBIOS.mjs";
import "./tests/NormaliseUnicode.mjs";
import "./tests/NTLM.mjs";
import "./tests/OTP.mjs";
import "./tests/ParseCSR.mjs";
import "./tests/ParseEthernetFrame.mjs";
import "./tests/ParseIPRange.mjs";
import "./tests/ParseObjectIDTimestamp.mjs";
import "./tests/ParseQRCode.mjs";
import "./tests/ParseSSHHostKey.mjs";
import "./tests/ParseTCP.mjs";
import "./tests/ParseTLSRecord.mjs";
import "./tests/ParseTLV.mjs";
import "./tests/ParseUDP.mjs";
import "./tests/PEMtoHex.mjs";
import "./tests/PGP.mjs";
import "./tests/PHP.mjs";
import "./tests/PHPSerialize.mjs";
import "./tests/PowerSet.mjs";
import "./tests/Protobuf.mjs";
import "./tests/PubKeyFromCert.mjs";
import "./tests/PubKeyFromPrivKey.mjs";
import "./tests/Rabbit.mjs";
import "./tests/RAKE.mjs";
import "./tests/RC6.mjs";
import "./tests/Regex.mjs";
import "./tests/Register.mjs";
import "./tests/RisonEncodeDecode.mjs";
import "./tests/Rotate.mjs";
import "./tests/RSA.mjs";
import "./tests/Salsa20.mjs";
import "./tests/SeqUtils.mjs";
import "./tests/SetDifference.mjs";
import "./tests/SetIntersection.mjs";
import "./tests/SetUnion.mjs";
import "./tests/Shuffle.mjs";
import "./tests/SIGABA.mjs";
import "./tests/SM2.mjs";
import "./tests/SM4.mjs";
import "./tests/SQLBeautify.mjs";
import "./tests/StrUtils.mjs";
import "./tests/StripIPv4Header.mjs";
import "./tests/StripTCPHeader.mjs";
import "./tests/StripUDPHeader.mjs";
import "./tests/Subsection.mjs";
import "./tests/SwapCase.mjs";
import "./tests/SymmetricDifference.mjs";
import "./tests/TakeNthBytes.mjs";
import "./tests/Template.mjs";
import "./tests/TextEncodingBruteForce.mjs";
import "./tests/TextIntegerConverter.mjs";
import "./tests/ToFromInsensitiveRegex.mjs";
import "./tests/TranslateDateTimeFormat.mjs";
import "./tests/Typex.mjs";
import "./tests/UnescapeString.mjs";
import "./tests/Unicode.mjs";
import "./tests/URLEncodeDecode.mjs";
import "./tests/XSalsa20.mjs";
import "./tests/XXTEA.mjs";
import "./tests/YARA.mjs";
// Run all registered tests through Vitest
runRegisteredTests();

12
vitest.config.mjs Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: [
"tests/**/*.test.mjs",
"tests/**/*.spec.mjs",
],
testTimeout: 30000,
globals: true,
},
});

View File

@ -45,8 +45,6 @@ module.exports = {
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
log: "loglevel",
// process and Buffer are no longer polyfilled in webpack 5 but
// many of our dependencies expect them, so it is easiest to just
@ -116,9 +114,7 @@ module.exports = {
],
resolve: {
extensions: [".mjs", ".js", ".json"], // Allows importing files without extensions
alias: {
jquery: "jquery/src/jquery",
},
alias: {},
fallback: {
"assert": require.resolve("assert/"),
"buffer": require.resolve("buffer/"),
@ -168,13 +164,6 @@ module.exports = {
test: /prime.worker.min.js$/,
type: "asset/source"
},
{
test: /bootstrap-material-design/,
loader: "imports-loader",
options: {
imports: "default popper.js/dist/umd/popper.js Popper"
}
},
{
test: /blueimp-load-image/,
loader: "imports-loader",

56
webpack.dev.config.js Normal file
View File

@ -0,0 +1,56 @@
"use strict";
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const baseConfig = require("./webpack.config.js");
const { listEntryModules } = require("./scripts/listEntryModulesSync.cjs");
const pkg = require("./package.json");
const d = new Date();
const compileYear = d.getUTCFullYear().toString();
const compileTime = `${String(d.getUTCDate()).padStart(2, "0")}/${String(d.getUTCMonth() + 1).padStart(2, "0")}/${d.getUTCFullYear()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")} UTC`;
const BUILD_CONSTANTS = {
COMPILE_YEAR: JSON.stringify(compileYear),
COMPILE_TIME: JSON.stringify(compileTime),
COMPILE_MSG: JSON.stringify(process.env.COMPILE_MSG || ""),
PKG_VERSION: JSON.stringify(pkg.version),
};
const moduleEntryPoints = listEntryModules();
module.exports = {
...baseConfig,
mode: "development",
target: "web",
entry: Object.assign({
main: "./src/web/index.js"
}, moduleEntryPoints),
resolve: {
...baseConfig.resolve,
alias: {
...baseConfig.resolve.alias,
"./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
}
},
devServer: {
port: parseInt(process.env.PORT || "8080", 10),
client: {
logging: "error",
overlay: true
},
hot: "only"
},
plugins: [
...baseConfig.plugins,
new webpack.DefinePlugin(BUILD_CONSTANTS),
new HtmlWebpackPlugin({
filename: "index.html",
template: "./src/web/html/index.html",
chunks: ["main"],
compileYear: compileYear,
compileTime: compileTime,
version: pkg.version,
})
]
};

67
webpack.prod.config.js Normal file
View File

@ -0,0 +1,67 @@
"use strict";
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const baseConfig = require("./webpack.config.js");
const { listEntryModules } = require("./scripts/listEntryModulesSync.cjs");
const pkg = require("./package.json");
const d = new Date();
const compileYear = d.getUTCFullYear().toString();
const compileTime = `${String(d.getUTCDate()).padStart(2, "0")}/${String(d.getUTCMonth() + 1).padStart(2, "0")}/${d.getUTCFullYear()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")} UTC`;
const BUILD_CONSTANTS = {
COMPILE_YEAR: JSON.stringify(compileYear),
COMPILE_TIME: JSON.stringify(compileTime),
COMPILE_MSG: JSON.stringify(process.env.COMPILE_MSG || ""),
PKG_VERSION: JSON.stringify(pkg.version),
};
const moduleEntryPoints = listEntryModules();
module.exports = {
...baseConfig,
mode: "production",
target: "web",
entry: Object.assign({
main: "./src/web/index.js"
}, moduleEntryPoints),
output: {
...baseConfig.output,
path: __dirname + "/build/prod",
filename: chunkData => {
return chunkData.chunk.name === "main" ? "assets/[name].js" : "[name].js";
},
},
resolve: {
...baseConfig.resolve,
alias: {
...baseConfig.resolve.alias,
"./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
}
},
plugins: [
...baseConfig.plugins,
new webpack.DefinePlugin(BUILD_CONSTANTS),
new HtmlWebpackPlugin({
filename: "index.html",
template: "./src/web/html/index.html",
chunks: ["main"],
compileYear: compileYear,
compileTime: compileTime,
version: pkg.version,
minify: {
removeComments: true,
collapseWhitespace: true,
minifyJS: true,
minifyCSS: true
}
}),
new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "BundleAnalyzerReport.html",
openAnalyzer: false
}),
]
};