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:
parent
b0fa1f8d1b
commit
1ac3190dae
17
.github/workflows/master.yml
vendored
17
.github/workflows/master.yml
vendored
@ -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'
|
||||
|
||||
14
.github/workflows/pull_requests.yml
vendored
14
.github/workflows/pull_requests.yml
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
101
package.json
101
package.json
@ -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
45
playwright.config.mjs
Normal 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
207
rspack.config.js
Normal 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
51
rspack.dev.config.js
Normal 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
51
rspack.prod.config.js
Normal 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,
|
||||
}),
|
||||
]
|
||||
};
|
||||
86
scripts/buildStandalone.mjs
Normal file
86
scripts/buildStandalone.mjs
Normal 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 ---");
|
||||
44
scripts/listEntryModules.mjs
Normal file
44
scripts/listEntryModules.mjs
Normal 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}`);
|
||||
}
|
||||
}
|
||||
32
scripts/listEntryModulesSync.cjs
Normal file
32
scripts/listEntryModulesSync.cjs
Normal 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
81
scripts/postinstall.mjs
Normal 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.");
|
||||
32
scripts/prepareGhPages.mjs
Normal file
32
scripts/prepareGhPages.mjs
Normal 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 ---");
|
||||
51
scripts/testNodeConsumers.mjs
Normal file
51
scripts/testNodeConsumers.mjs
Normal 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
43
scripts/watchConfig.mjs
Normal 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));
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
56
src/core/lib/CaseConvert.mjs
Normal file
56
src/core/lib/CaseConvert.mjs
Normal 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("_");
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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
74
src/core/lib/RC4.mjs
Normal 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;
|
||||
}
|
||||
@ -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";
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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";
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]) {
|
||||
|
||||
@ -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?";
|
||||
|
||||
@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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++) {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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'`;
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
<
|
||||
</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">
|
||||
<
|
||||
</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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
64
src/web/utils/Snackbar.mjs
Normal file
64
src/web/utils/Snackbar.mjs
Normal 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;
|
||||
@ -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 ? "❰" : "❱"}
|
||||
</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);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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_) {
|
||||
|
||||
@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
75
tests/browser/app.spec.mjs
Normal file
75
tests/browser/app.spec.mjs
Normal 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");
|
||||
});
|
||||
});
|
||||
76
tests/lib/VitestAdapter.mjs
Normal file
76
tests/lib/VitestAdapter.mjs
Normal 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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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";
|
||||
|
||||
|
||||
181
tests/operations/operations.test.mjs
Normal file
181
tests/operations/operations.test.mjs
Normal 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
12
vitest.config.mjs
Normal 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,
|
||||
},
|
||||
});
|
||||
@ -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
56
webpack.dev.config.js
Normal 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
67
webpack.prod.config.js
Normal 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
|
||||
}),
|
||||
]
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user