diff --git a/.eslintrc.json b/.eslintrc.json index 495c000f..e307112b 100755 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -43,6 +43,7 @@ // stylistic conventions "brace-style": ["error", "1tbs"], + "space-before-blocks": ["error", "always"], "block-spacing": "error", "array-bracket-spacing": "error", "comma-spacing": "error", diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e90196e5..abb37d42 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,7 +6,7 @@ There are lots of opportunities to contribute to CyberChef. If you want ideas, t Before your contributions can be accepted, you must: - - Sign the [GCHQ Contributor Licence Agreement](https://github.com/gchq/Gaffer/wiki/GCHQ-OSS-Contributor-License-Agreement-V1.0) + - Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef) - Push your changes to your fork. - Submit a pull request. @@ -25,9 +25,9 @@ Before your contributions can be accepted, you must: ## Design Principles 1. If at all possible, all operations and features should be client-side and not rely on connections to an external server. This increases the utility of CyberChef on closed networks and in virtual machines that are not connected to the Internet. Calls to external APIs may be accepted if there is no other option, but not for critical components. -2. Latency should be kept to a minimum to enhance the user experience. This means that all operation code should sit on the client, rather than being loaded dynamically from a server. -3. Use Vanilla JS if at all possible to reduce the number of libraries required and relied upon. Frameworks like jQuery, although included, should not be used unless absolutely necessary. -4. Minimise the use of large libraries, especially for niche operations that won't be used very often - these will be downloaded by everyone using the app, whether they use that operation or not (due to principal 2). +2. Latency should be kept to a minimum to enhance the user experience. This means that operation code should sit on the client and be executed there. However, as a trade-off between latency and bandwidth, operation code with large dependencies can be loaded in discrete modules in order to reduce the size of the initial download. The downloading of additional modules must remain entirely transparent so that the user is not inconvenienced. +3. Large libraries should be kept in separate modules so that they are not downloaded by everyone who uses the app, just those who specifically require the relevant operations. +4. Use Vanilla JS if at all possible to reduce the number of libraries required and relied upon. Frameworks like jQuery, although included, should not be used unless absolutely necessary. With these principles in mind, any changes or additions to CyberChef should keep it: diff --git a/.gitignore b/.gitignore index 6c9f300a..e546c949 100755 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,6 @@ node_modules npm-debug.log travis.log build -docs/* -!docs/*.conf.json -!docs/*.ico .vscode .*.swp .DS_Store diff --git a/.npmignore b/.npmignore index 6f32ec06..05ab5f52 100755 --- a/.npmignore +++ b/.npmignore @@ -3,6 +3,5 @@ npm-debug.log travis.log build/* !build/node -docs .vscode .github diff --git a/.travis.yml b/.travis.yml index e99e3903..6996a5e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,9 @@ before_script: script: - grunt lint - grunt test - - grunt docs + - grunt testnodeconsumer - grunt prod --msg="$COMPILE_MSG" - xvfb-run --server-args="-screen 0 1200x800x24" grunt testui - - grunt testnodeconsumer before_deploy: - grunt exec:sitemap - grunt copy:ghPages diff --git a/CHANGELOG.md b/CHANGELOG.md index a19eec80..9ec9af1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master). -### [9.2.0] - 2019-08-13 -- 'Defang IP Addresses' operation added [@h345983745] | [#556] +### [9.2.0] - 2019-08-23 +- 'Parse UDP' operation added [@h345983745] | [#614] -### [9.1.0] - 2019-08-13 +### [9.1.0] - 2019-08-22 - 'Parse SSH Host Key' operation added [@j433866] | [#595] +- 'Defang IP Addresses' operation added [@h345983745] | [#556] ## [9.0.0] - 2019-07-09 - [Multiple inputs](https://github.com/gchq/CyberChef/wiki/Multiple-Inputs) are now supported in the main web UI, allowing you to upload and process multiple files at once [@j433866] | [#566] @@ -14,6 +15,9 @@ All major and minor version changes will be documented in this file. Details of - A [read-eval-print loop (REPL)](https://github.com/gchq/CyberChef/wiki/Node-API#repl) is also included to enable prototyping and experimentation with the API [@d98762625] | [#291] - Light and dark Solarized themes added [@j433866] | [#566] +
+ Click to expand v8 minor versions + ### [8.38.0] - 2019-07-03 - 'Streebog' and 'GOST hash' operations added [@MShwed] [@n1474335] | [#530] @@ -135,6 +139,8 @@ All major and minor version changes will be documented in this file. Details of ### [8.1.0] - 2018-08-19 - 'Dechunk HTTP response' operation added [@sevzero] | [#311] +
+ ## [8.0.0] - 2018-08-05 - Codebase rewritten using [ES modules](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/) and [classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) [@n1474335] [@d98762625] [@artemisbot] [@picapi] | [#284] - Operation architecture restructured to make adding new operations a lot simpler [@n1474335] | [#284] @@ -289,3 +295,4 @@ All major and minor version changes will be documented in this file. Details of [#585]: https://github.com/gchq/CyberChef/pull/585 [#591]: https://github.com/gchq/CyberChef/pull/591 [#595]: https://github.com/gchq/CyberChef/pull/595 +[#614]: https://github.com/gchq/CyberChef/pull/614 diff --git a/Gruntfile.js b/Gruntfile.js index e228d785..cd4c998e 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -52,17 +52,10 @@ module.exports = function (grunt) { "A task which checks whether consuming CJS and ESM apps work with the CyberChef build", ["exec:setupNodeConsumers", "exec:testCJSNodeConsumer", "exec:testESMNodeConsumer", "exec:testESMDeepImportNodeConsumer", "exec:teardownNodeConsumers"]); - grunt.registerTask("docs", - "Compiles documentation in the /docs directory.", - ["clean:docs", "jsdoc", "chmod:docs"]); - - grunt.registerTask("default", "Lints the code base", ["eslint", "exec:repoSize"]); - - grunt.registerTask("doc", "docs"); grunt.registerTask("tests", "test"); grunt.registerTask("lint", "eslint"); @@ -70,7 +63,6 @@ module.exports = function (grunt) { // Load tasks provided by each plugin grunt.loadNpmTasks("grunt-eslint"); grunt.loadNpmTasks("grunt-webpack"); - grunt.loadNpmTasks("grunt-jsdoc"); grunt.loadNpmTasks("grunt-contrib-clean"); grunt.loadNpmTasks("grunt-contrib-copy"); grunt.loadNpmTasks("grunt-contrib-watch"); @@ -117,7 +109,6 @@ module.exports = function (grunt) { node: ["build/node/*"], config: ["src/core/config/OperationConfig.json", "src/core/config/modules/*", "src/code/operations/index.mjs"], nodeConfig: ["src/node/index.mjs", "src/node/config/OperationConfig.json"], - docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico", "!docs/*.png"], standalone: ["build/prod/CyberChef*.html"] }, eslint: { @@ -130,22 +121,6 @@ module.exports = function (grunt) { node: ["src/node/**/*.{js,mjs}"], tests: ["tests/**/*.{js,mjs}"], }, - jsdoc: { - options: { - destination: "docs", - template: "node_modules/ink-docstrap/template", - recurse: true, - readme: "./README.md", - configure: "docs/jsdoc.conf.json" - }, - all: { - src: [ - "src/**/*.js", - "src/**/*.mjs", - "!src/core/vendor/**/*" - ], - } - }, accessibility: { options: { accessibilityLevel: "WCAG2A", @@ -295,12 +270,7 @@ module.exports = function (grunt) { { src: "build/prod/index.html", dest: "build/prod/index.html" - }, - { - expand: true, - src: "docs/**", - dest: "build/prod/" - }, + } ] }, standalone: { @@ -332,12 +302,6 @@ module.exports = function (grunt) { mode: "755", }, src: ["build/**/*", "build/"] - }, - docs: { - options: { - mode: "755", - }, - src: ["docs/**/*", "docs/"] } }, watch: { diff --git a/README.md b/README.md index 299b5d87..d79c99a9 100755 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Build Status](https://travis-ci.org/gchq/CyberChef.svg?branch=master)](https://travis-ci.org/gchq/CyberChef) [![dependencies Status](https://david-dm.org/gchq/CyberChef/status.svg)](https://david-dm.org/gchq/CyberChef) [![npm](https://img.shields.io/npm/v/cyberchef.svg)](https://www.npmjs.com/package/cyberchef) -![](https://reposs.herokuapp.com/?path=gchq/CyberChef&color=blue) [![](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/gchq/CyberChef/blob/master/LICENSE) [![Gitter](https://badges.gitter.im/gchq/CyberChef.svg)](https://gitter.im/gchq/CyberChef?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) @@ -77,9 +76,9 @@ You can use as many operations as you like in simple or complex ways. Some examp CyberChef is built to support - - Google Chrome 40+ - - Mozilla Firefox 35+ - - Microsoft Edge 14+ + - Google Chrome 50+ + - Mozilla Firefox 38+ + ## Node.js support diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..c934c934 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security Policy + +## Supported Versions + +CyberChef is supported on a best endeavours basis. Patches will be applied to +the latest version rather than retroactively to older versions. To ensure you +are using the most secure version of CyberChef, please make sure you have the +[latest release](https://github.com/gchq/CyberChef/releases/latest). The +official [live demo](https://gchq.github.io/CyberChef/) is always up to date. + +## Reporting a Vulnerability + +In most scenarios, the most appropriate way to report a vulnerability is to +[raise a new issue](https://github.com/gchq/CyberChef/issues/new/choose) +describing the problem in as much detail as possible, ideally with examples. +This will obviously be public. If you feel that the vulnerability is +significant enough to warrant a private disclosure, please email +[oss@gchq.gov.uk](mailto:oss@gchq.gov.uk) and +[n1474335@gmail.com](mailto:n1474335@gmail.com). + +Disclosures of vulnerabilities in CyberChef are always welcomed. Whilst we aim +to write clean and secure code free from bugs, we recognise that this is an open +source project written by analysts in their spare time, relying on dozens of +open source libraries that are modified and updated on a regular basis. We hope +that the community will continue to support us as we endeavour to maintain and +develop this tool together. diff --git a/docs/favicon.ico b/docs/favicon.ico deleted file mode 100755 index fa2deb03..00000000 Binary files a/docs/favicon.ico and /dev/null differ diff --git a/docs/jsdoc.conf.json b/docs/jsdoc.conf.json deleted file mode 100755 index 36e9611b..00000000 --- a/docs/jsdoc.conf.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "tags": { - "allowUnknownTags": true - }, - "plugins": [ - "plugins/markdown", - "node_modules/jsdoc-babel" - ], - "templates": { - "systemName": "CyberChef", - "footer": "", - "copyright": "© Crown Copyright 2017", - "navType": "inline", - "theme": "cerulean", - "linenums": true, - "collapseSymbols": false, - "inverseNav": true, - "outputSourceFiles": true, - "outputSourcePath": true, - "dateFormat": "ddd MMM Do YYYY", - "sort": false, - "logoFile": "cyberchef-32x32.png", - "cleverLinks": false, - "monospaceLinks": false, - "protocol": "html://", - "methodHeadingReturns": false - }, - "markdown": { - "parser": "gfm", - "hardwrap": true - } -} diff --git a/package-lock.json b/package-lock.json index cd899ffc..7812c3d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.0.8", + "version": "9.2.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4623,15 +4623,6 @@ "webidl-conversions": "^4.0.2" } }, - "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, "domutils": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", @@ -7216,16 +7207,6 @@ "integrity": "sha512-cgAlreXf3muSYS5LzW0Cc4xHK03BjFOYk0MqCQ/MZ3k1Xz2GU7D+IAJg4UKicxpO+XdONJdx/NJ6kpy2wI+uHg==", "dev": true }, - "grunt-jsdoc": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/grunt-jsdoc/-/grunt-jsdoc-2.4.0.tgz", - "integrity": "sha512-JpZd1W7HbK0sHbpiL9+VyDFwZlkYoDQMaP+v6z1R23W/NYLoqJM76L9eBOr7O6NycqtddRHN5DzlSkW45MJ82w==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.5", - "jsdoc": "~3.6.0" - } - }, "grunt-known-options": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", @@ -8000,16 +7981,6 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, - "ink-docstrap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ink-docstrap/-/ink-docstrap-1.3.2.tgz", - "integrity": "sha512-STx5orGQU1gfrkoI/fMU7lX6CSP7LBGO10gXNgOZhwKhUqbtNjCkYSewJtNnLmWP1tAGN6oyEpG1HFPw5vpa5Q==", - "dev": true, - "requires": { - "moment": "^2.14.1", - "sanitize-html": "^1.13.0" - } - }, "inquirer": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.4.1.tgz", @@ -8510,106 +8481,6 @@ "esprima": "^4.0.0" } }, - "js2xmlparser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.0.tgz", - "integrity": "sha512-WuNgdZOXVmBk5kUPMcTcVUpbGRzLfNkv7+7APq7WiDihpXVKrgxo6wwRpRl9OQeEBgKCVk9mR7RbzrnNWC8oBw==", - "dev": true, - "requires": { - "xmlcreate": "^2.0.0" - } - }, - "jsdoc": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.3.tgz", - "integrity": "sha512-Yf1ZKA3r9nvtMWHO1kEuMZTlHOF8uoQ0vyo5eH7SQy5YeIiHM+B0DgKnn+X6y6KDYZcF7G2SPkKF+JORCXWE/A==", - "dev": true, - "requires": { - "@babel/parser": "^7.4.4", - "bluebird": "^3.5.4", - "catharsis": "^0.8.11", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.0", - "klaw": "^3.0.0", - "markdown-it": "^8.4.2", - "markdown-it-anchor": "^5.0.2", - "marked": "^0.7.0", - "mkdirp": "^0.5.1", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.0.1", - "taffydb": "2.6.2", - "underscore": "~1.9.1" - }, - "dependencies": { - "catharsis": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz", - "integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - }, - "klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.9" - } - }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", - "dev": true - }, - "requizzle": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", - "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true - }, - "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", - "dev": true - } - } - }, - "jsdoc-babel": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsdoc-babel/-/jsdoc-babel-0.5.0.tgz", - "integrity": "sha512-PYfTbc3LNTeR8TpZs2M94NLDWqARq0r9gx3SvuziJfmJS7/AeMKvtj0xjzOX0R/4MOVA7/FqQQK7d6U0iEoztQ==", - "dev": true, - "requires": { - "jsdoc-regex": "^1.0.1", - "lodash": "^4.17.10" - } - }, - "jsdoc-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/jsdoc-regex/-/jsdoc-regex-1.0.1.tgz", - "integrity": "sha1-hCRCjVtWOtjFx/vsB5uaiwnI3Po=", - "dev": true - }, "jsdom": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", @@ -8915,15 +8786,6 @@ "resolved": "https://registry.npmjs.org/libyara-wasm/-/libyara-wasm-0.0.12.tgz", "integrity": "sha512-AjTe4FiBuH4F7HwGT/3UxoRenczXtrbM6oWGrifxb44LrkDh5VxRNg9zwfPpDA5Fcc1iYcXS0WVA/b3DGtD8cQ==" }, - "linkify-it": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", - "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", - "dev": true, - "requires": { - "uc.micro": "^1.0.1" - } - }, "livereload-js": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", @@ -9096,24 +8958,12 @@ "lodash._isiterateecall": "^3.0.0" } }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, "lodash.defaultsdeep": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", "dev": true }, - "lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=", - "dev": true - }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -9179,12 +9029,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "dev": true - }, "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -9297,33 +9141,6 @@ "object-visit": "^1.0.0" } }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "dependencies": { - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - } - } - }, - "markdown-it-anchor": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.4.tgz", - "integrity": "sha512-n8zCGjxA3T+Mx1pG8HEgbJbkB8JFUuRkeTZQuIM8iPY6oQ8sWOPRZJDFC9a/pNg2QkHEjjGkhBEl/RSyzaDZ3A==", - "dev": true - }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -9335,12 +9152,6 @@ "safe-buffer": "^5.1.2" } }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", - "dev": true - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -12127,151 +11938,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sanitize-html": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.20.1.tgz", - "integrity": "sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "htmlparser2": "^3.10.0", - "lodash.clonedeep": "^4.5.0", - "lodash.escaperegexp": "^4.1.2", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.mergewith": "^4.6.1", - "postcss": "^7.0.5", - "srcset": "^1.0.0", - "xtend": "^4.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", - "dev": true - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "htmlparser2": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz", - "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==", - "dev": true, - "requires": { - "domelementtype": "^1.3.0", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.0.6" - } - }, - "postcss": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", - "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "readable-stream": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.0.6.tgz", - "integrity": "sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "sass-graph": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", @@ -13014,16 +12680,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "srcset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-1.0.0.tgz", - "integrity": "sha1-pWad4StC87HV6D7QPHEEb8SPQe8=", - "dev": true, - "requires": { - "array-uniq": "^1.0.2", - "number-is-nan": "^1.0.0" - } - }, "ssdeep.js": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/ssdeep.js/-/ssdeep.js-0.0.2.tgz", @@ -13485,12 +13141,6 @@ } } }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", - "dev": true - }, "tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", @@ -13946,12 +13596,6 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true - }, "uglify-js": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", @@ -14973,12 +14617,6 @@ } } }, - "xmlcreate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.1.tgz", - "integrity": "sha512-MjGsXhKG8YjTKrDCXseFo3ClbMGvUD4en29H2Cev1dv4P/chlpw6KdYmlCWDkhosBVKRDjM836+3e3pm1cBNJA==", - "dev": true - }, "xmldom": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", diff --git a/package.json b/package.json index 571f9015..b8db8cfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.0.8", + "version": "9.2.3", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", @@ -31,10 +31,9 @@ "module": "src/node/index.mjs", "bugs": "https://github.com/gchq/CyberChef/issues", "browserslist": [ - "Chrome >= 40", - "Firefox >= 35", - "Edge >= 14", - "node >= 6.5" + "Chrome >= 50", + "Firefox >= 38", + "node >= 10" ], "devDependencies": { "@babel/core": "^7.5.0", @@ -60,13 +59,10 @@ "grunt-contrib-watch": "^1.1.0", "grunt-eslint": "^22.0.0", "grunt-exec": "~3.0.0", - "grunt-jsdoc": "^2.4.0", "grunt-webpack": "^3.1.3", "grunt-zip": "^0.18.2", "html-webpack-plugin": "^3.2.0", "imports-loader": "^0.8.0", - "ink-docstrap": "^1.3.2", - "jsdoc-babel": "^0.5.0", "mini-css-extract-plugin": "^0.7.0", "nightwatch": "^1.1.13", "node-sass": "^4.12.0", @@ -162,7 +158,6 @@ "test": "grunt test", "test-node-consumer": "grunt testnodeconsumer", "testui": "grunt testui", - "docs": "grunt docs", "lint": "grunt lint", "newop": "node --experimental-modules src/core/config/scripts/newOperation.mjs" } diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index f1a7b815..89b93b87 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -167,6 +167,7 @@ "Parse IP range", "Parse IPv6 address", "Parse IPv4 header", + "Parse UDP", "Parse SSH Host Key", "Parse URI", "URL Encode", diff --git a/src/core/lib/Arithmetic.mjs b/src/core/lib/Arithmetic.mjs index 2c56e483..7c10855f 100644 --- a/src/core/lib/Arithmetic.mjs +++ b/src/core/lib/Arithmetic.mjs @@ -109,7 +109,7 @@ export function mean(data) { */ export function median(data) { if ((data.length % 2) === 0 && data.length > 0) { - data.sort(function(a, b){ + data.sort(function(a, b) { return a.minus(b); }); const first = data[Math.floor(data.length / 2)]; diff --git a/src/core/lib/ConvertCoordinates.mjs b/src/core/lib/ConvertCoordinates.mjs index 1acb5ef4..d688d29e 100644 --- a/src/core/lib/ConvertCoordinates.mjs +++ b/src/core/lib/ConvertCoordinates.mjs @@ -327,13 +327,13 @@ export function convertCoordinates (input, inFormat, inDelim, outFormat, outDeli * @param {string} input - The input data to be split * @returns {number[]} An array of the different items in the string, stored as floats */ -function splitInput (input){ +function splitInput (input) { const split = []; input.split(/\s+/).forEach(item => { // Remove any character that isn't a digit, decimal point or negative sign item = item.replace(/[^0-9.-]/g, ""); - if (item.length > 0){ + if (item.length > 0) { // Turn the item into a float split.push(parseFloat(item)); } @@ -350,7 +350,7 @@ function splitInput (input){ * @param {number} precision - The precision the result should be rounded to * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string) */ -function convDMSToDD (degrees, minutes, seconds, precision){ +function convDMSToDD (degrees, minutes, seconds, precision) { const absDegrees = Math.abs(degrees); let conv = absDegrees + (minutes / 60) + (seconds / 3600); let outString = round(conv, precision) + "°"; @@ -566,7 +566,7 @@ export function findFormat (input, delim) { // Test DMS/DDM/DD formats if (testData !== undefined) { const split = splitInput(testData); - switch (split.length){ + switch (split.length) { case 3: return "Degrees Minutes Seconds"; case 2: diff --git a/src/core/lib/IP.mjs b/src/core/lib/IP.mjs index f378763e..f9c54ad0 100644 --- a/src/core/lib/IP.mjs +++ b/src/core/lib/IP.mjs @@ -241,7 +241,7 @@ export function ipv6ListedRange(match, includeNetworkInfo) { ipv6List = ipv6List.filter(function(str) { return str.trim(); }); - for (let i =0; i < ipv6List.length; i++){ + for (let i =0; i < ipv6List.length; i++) { ipv6List[i] = ipv6List[i].trim(); } const ipv6CidrList = ipv6List.filter(function(a) { @@ -502,8 +502,8 @@ export function ipv6Compare(a, b) { const a_ = strToIpv6(a), b_ = strToIpv6(b); - for (let i = 0; i < a_.length; i++){ - if (a_[i] !== b_[i]){ + for (let i = 0; i < a_.length; i++) { + if (a_[i] !== b_[i]) { return a_[i] - b_[i]; } } diff --git a/src/core/lib/LoremIpsum.mjs b/src/core/lib/LoremIpsum.mjs index d7fff69b..3d60fa89 100644 --- a/src/core/lib/LoremIpsum.mjs +++ b/src/core/lib/LoremIpsum.mjs @@ -85,7 +85,7 @@ function getWords(length=3) { const words = []; let word; let previousWord; - while (words.length < length){ + while (words.length < length) { do { word = wordList[Math.floor(Math.random() * wordList.length)]; } while (previousWord === word); diff --git a/src/core/operations/AESDecrypt.mjs b/src/core/operations/AESDecrypt.mjs index be823b9b..8fe0b93c 100644 --- a/src/core/operations/AESDecrypt.mjs +++ b/src/core/operations/AESDecrypt.mjs @@ -71,8 +71,8 @@ class AESDecrypt extends Operation { * @throws {OperationError} if cannot decrypt input or invalid key length */ run(input, args) { - const key = Utils.convertToByteArray(args[0].string, args[0].option), - iv = Utils.convertToByteArray(args[1].string, args[1].option), + const key = Utils.convertToByteString(args[0].string, args[0].option), + iv = Utils.convertToByteString(args[1].string, args[1].option), mode = args[2], inputType = args[3], outputType = args[4], diff --git a/src/core/operations/BlurImage.mjs b/src/core/operations/BlurImage.mjs index 8561f49d..c6f54421 100644 --- a/src/core/operations/BlurImage.mjs +++ b/src/core/operations/BlurImage.mjs @@ -64,7 +64,7 @@ class BlurImage extends Operation { throw new OperationError(`Error loading image. (${err})`); } try { - switch (blurType){ + switch (blurType) { case "Fast": if (isWorkerEnvironment()) self.sendStatusMessage("Fast blurring image..."); diff --git a/src/core/operations/ChangeIPFormat.mjs b/src/core/operations/ChangeIPFormat.mjs index fea5c561..c9adc5d8 100644 --- a/src/core/operations/ChangeIPFormat.mjs +++ b/src/core/operations/ChangeIPFormat.mjs @@ -29,12 +29,12 @@ class ChangeIPFormat extends Operation { { "name": "Input format", "type": "option", - "value": ["Dotted Decimal", "Decimal", "Hex"] + "value": ["Dotted Decimal", "Decimal", "Octal", "Hex"] }, { "name": "Output format", "type": "option", - "value": ["Dotted Decimal", "Decimal", "Hex"] + "value": ["Dotted Decimal", "Decimal", "Octal", "Hex"] } ]; } @@ -54,7 +54,6 @@ class ChangeIPFormat extends Operation { if (lines[i] === "") continue; let baIp = []; let octets; - let decimal; if (inFormat === outFormat) { output += lines[i] + "\n"; @@ -70,11 +69,10 @@ class ChangeIPFormat extends Operation { } break; case "Decimal": - decimal = lines[i].toString(); - baIp.push(decimal >> 24 & 255); - baIp.push(decimal >> 16 & 255); - baIp.push(decimal >> 8 & 255); - baIp.push(decimal & 255); + baIp = this.fromNumber(lines[i].toString(), 10); + break; + case "Octal": + baIp = this.fromNumber(lines[i].toString(), 8); break; case "Hex": baIp = fromHex(lines[i]); @@ -100,6 +98,10 @@ class ChangeIPFormat extends Operation { decIp = ((baIp[0] << 24) | (baIp[1] << 16) | (baIp[2] << 8) | baIp[3]) >>> 0; output += decIp.toString() + "\n"; break; + case "Octal": + decIp = ((baIp[0] << 24) | (baIp[1] << 16) | (baIp[2] << 8) | baIp[3]) >>> 0; + output += "0" + decIp.toString(8) + "\n"; + break; case "Hex": hexIp = ""; for (j = 0; j < baIp.length; j++) { @@ -115,6 +117,22 @@ class ChangeIPFormat extends Operation { return output.slice(0, output.length-1); } + /** + * Constructs an array of IP address octets from a numerical value. + * @param {string} value The value of the IP address + * @param {number} radix The numeral system to be used + * @returns {number[]} + */ + fromNumber(value, radix) { + const decimal = parseInt(value, radix); + const baIp = []; + baIp.push(decimal >> 24 & 255); + baIp.push(decimal >> 16 & 255); + baIp.push(decimal >> 8 & 255); + baIp.push(decimal & 255); + return baIp; + } + } export default ChangeIPFormat; diff --git a/src/core/operations/DNSOverHTTPS.mjs b/src/core/operations/DNSOverHTTPS.mjs index b6e98250..ca779815 100644 --- a/src/core/operations/DNSOverHTTPS.mjs +++ b/src/core/operations/DNSOverHTTPS.mjs @@ -111,7 +111,7 @@ class DNSOverHTTPS extends Operation { * @returns {JSON} */ function extractData(data) { - if (typeof(data) == "undefined"){ + if (typeof(data) == "undefined") { return []; } else { const dataValues = []; diff --git a/src/core/operations/ExtractDomains.mjs b/src/core/operations/ExtractDomains.mjs index ed1bbdb6..cc65ff4b 100644 --- a/src/core/operations/ExtractDomains.mjs +++ b/src/core/operations/ExtractDomains.mjs @@ -20,7 +20,7 @@ class ExtractDomains extends Operation { this.name = "Extract domains"; this.module = "Regex"; - this.description = "Extracts domain names.
Note that this will not include paths. Use Extract URLs to find entire URLs."; + this.description = "Extracts fully qualified domain names.
Note that this will not include paths. Use Extract URLs to find entire URLs."; this.inputType = "string"; this.outputType = "string"; this.args = [ diff --git a/src/core/operations/FlipImage.mjs b/src/core/operations/FlipImage.mjs index b04469ca..a6a262fc 100644 --- a/src/core/operations/FlipImage.mjs +++ b/src/core/operations/FlipImage.mjs @@ -58,7 +58,7 @@ class FlipImage extends Operation { try { if (isWorkerEnvironment()) self.sendStatusMessage("Flipping image..."); - switch (flipAxis){ + switch (flipAxis) { case "Horizontal": image.flip(true, false); break; diff --git a/src/core/operations/GenerateLoremIpsum.mjs b/src/core/operations/GenerateLoremIpsum.mjs index 50056b46..7bc636ac 100644 --- a/src/core/operations/GenerateLoremIpsum.mjs +++ b/src/core/operations/GenerateLoremIpsum.mjs @@ -47,7 +47,7 @@ class GenerateLoremIpsum extends Operation { */ run(input, args) { const [length, lengthType] = args; - if (length < 1){ + if (length < 1) { throw new OperationError("Length must be greater than 0"); } switch (lengthType) { diff --git a/src/core/operations/ImageFilter.mjs b/src/core/operations/ImageFilter.mjs index 8501ab34..95c89197 100644 --- a/src/core/operations/ImageFilter.mjs +++ b/src/core/operations/ImageFilter.mjs @@ -48,7 +48,7 @@ class ImageFilter extends Operation { */ async run(input, args) { const [filterType] = args; - if (!isImage(new Uint8Array(input))){ + if (!isImage(new Uint8Array(input))) { throw new OperationError("Invalid file type."); } diff --git a/src/core/operations/MicrosoftScriptDecoder.mjs b/src/core/operations/MicrosoftScriptDecoder.mjs index 460573b2..952a8788 100644 --- a/src/core/operations/MicrosoftScriptDecoder.mjs +++ b/src/core/operations/MicrosoftScriptDecoder.mjs @@ -34,7 +34,7 @@ class MicrosoftScriptDecoder extends Operation { run(input, args) { const matcher = /#@~\^.{6}==(.+).{6}==\^#~@/; const encodedData = matcher.exec(input); - if (encodedData){ + if (encodedData) { return MicrosoftScriptDecoder._decode(encodedData[1]); } else { return ""; diff --git a/src/core/operations/ParseColourCode.mjs b/src/core/operations/ParseColourCode.mjs index f24b1043..9cf40ba7 100644 --- a/src/core/operations/ParseColourCode.mjs +++ b/src/core/operations/ParseColourCode.mjs @@ -134,7 +134,7 @@ CMYK: ${cmyk} static _hslToRgb(h, s, l) { let r, g, b; - if (s === 0){ + if (s === 0) { r = g = b = l; // achromatic } else { const hue2rgb = function hue2rgb(p, q, t) { diff --git a/src/core/operations/ParseUDP.mjs b/src/core/operations/ParseUDP.mjs new file mode 100644 index 00000000..0a88fd5d --- /dev/null +++ b/src/core/operations/ParseUDP.mjs @@ -0,0 +1,84 @@ +/** + * @author h345983745 [] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Stream from "../lib/Stream.mjs"; +import {toHex} from "../lib/Hex.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Parse UDP operation + */ +class ParseUDP extends Operation { + + /** + * ParseUDP constructor + */ + constructor() { + super(); + + this.name = "Parse UDP"; + this.module = "Default"; + this.description = "Parses a UDP header and payload (if present)."; + this.infoURL = "https://wikipedia.org/wiki/User_Datagram_Protocol"; + this.inputType = "ArrayBuffer"; + this.outputType = "json"; + this.presentType = "html"; + this.args = []; + } + + /** + * @param {ArrayBuffer} input + * @returns {Object} + */ + run(input, args) { + if (input.byteLength < 8) { + throw new OperationError("Need 8 bytes for a UDP Header"); + } + + const s = new Stream(new Uint8Array(input)); + // Parse Header + const UDPPacket = { + "Source port": s.readInt(2), + "Destination port": s.readInt(2), + "Length": s.readInt(2), + "Checksum": toHex(s.getBytes(2), "") + }; + // Parse data if present + if (s.hasMore()) { + UDPPacket.Data = toHex(s.getBytes(UDPPacket.Length - 8), ""); + } + + return UDPPacket; + } + + /** + * Displays the UDP Packet in a table style + * @param {Object} data + * @returns {html} + */ + present(data) { + const html = []; + html.push(""); + html.push(""); + html.push(""); + html.push(""); + html.push(""); + + for (const key in data) { + html.push(""); + html.push(""); + html.push(""); + html.push(""); + } + html.push("
FieldValue
" + key + "" + data[key] + "
"); + return html.join(""); + } + +} + + +export default ParseUDP; diff --git a/src/core/operations/ScanForEmbeddedFiles.mjs b/src/core/operations/ScanForEmbeddedFiles.mjs index b1497407..11102f21 100644 --- a/src/core/operations/ScanForEmbeddedFiles.mjs +++ b/src/core/operations/ScanForEmbeddedFiles.mjs @@ -41,7 +41,7 @@ class ScanForEmbeddedFiles extends Operation { * @returns {string} */ run(input, args) { - let output = "Scanning data for 'magic bytes' which may indicate embedded files. The following results may be false positives and should not be treat as reliable. Any suffiently long file is likely to contain these magic bytes coincidentally.\n", + let output = "Scanning data for 'magic bytes' which may indicate embedded files. The following results may be false positives and should not be treat as reliable. Any sufficiently long file is likely to contain these magic bytes coincidentally.\n", numFound = 0; const categories = [], data = new Uint8Array(input); diff --git a/src/core/operations/SharpenImage.mjs b/src/core/operations/SharpenImage.mjs index 2ea8a122..338679c0 100644 --- a/src/core/operations/SharpenImage.mjs +++ b/src/core/operations/SharpenImage.mjs @@ -62,7 +62,7 @@ class SharpenImage extends Operation { async run(input, args) { const [radius, amount, threshold] = args; - if (!isImage(new Uint8Array(input))){ + if (!isImage(new Uint8Array(input))) { throw new OperationError("Invalid file type."); } diff --git a/src/core/operations/SwapEndianness.mjs b/src/core/operations/SwapEndianness.mjs index d0a7492b..872d3529 100644 --- a/src/core/operations/SwapEndianness.mjs +++ b/src/core/operations/SwapEndianness.mjs @@ -79,7 +79,7 @@ class SwapEndianness extends Operation { const word = data.slice(i, i + wordLength); // Pad word if too short - if (padIncompleteWords && word.length < wordLength){ + if (padIncompleteWords && word.length < wordLength) { for (j = word.length; j < wordLength; j++) { word.push(0); } diff --git a/src/core/operations/UNIXTimestampToWindowsFiletime.mjs b/src/core/operations/UNIXTimestampToWindowsFiletime.mjs index b93c881f..5a042885 100644 --- a/src/core/operations/UNIXTimestampToWindowsFiletime.mjs +++ b/src/core/operations/UNIXTimestampToWindowsFiletime.mjs @@ -51,7 +51,7 @@ class UNIXTimestampToWindowsFiletime extends Operation { input = new BigNumber(input); - if (units === "Seconds (s)"){ + if (units === "Seconds (s)") { input = input.multipliedBy(new BigNumber("10000000")); } else if (units === "Milliseconds (ms)") { input = input.multipliedBy(new BigNumber("10000")); @@ -65,7 +65,7 @@ class UNIXTimestampToWindowsFiletime extends Operation { input = input.plus(new BigNumber("116444736000000000")); - if (format === "Hex"){ + if (format === "Hex") { return input.toString(16); } else { return input.toFixed(); diff --git a/src/core/operations/WindowsFiletimeToUNIXTimestamp.mjs b/src/core/operations/WindowsFiletimeToUNIXTimestamp.mjs index 18542bee..57d1e477 100644 --- a/src/core/operations/WindowsFiletimeToUNIXTimestamp.mjs +++ b/src/core/operations/WindowsFiletimeToUNIXTimestamp.mjs @@ -57,7 +57,7 @@ class WindowsFiletimeToUNIXTimestamp extends Operation { input = input.minus(new BigNumber("116444736000000000")); - if (units === "Seconds (s)"){ + if (units === "Seconds (s)") { input = input.dividedBy(new BigNumber("10000000")); } else if (units === "Milliseconds (ms)") { input = input.dividedBy(new BigNumber("10000")); diff --git a/src/node/api.mjs b/src/node/api.mjs index e136143c..f03d45b6 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -295,7 +295,7 @@ export function help(input) { * bake [Wrapped] - Perform an array of operations on some input. * @returns {Function} */ -export function bake(){ +export function bake() { /** * bake diff --git a/src/web/App.mjs b/src/web/App.mjs index 56746a41..606b2aff 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -633,7 +633,7 @@ class App { * Pops up a message to the user and writes it to the console log. * * @param {string} str - The message to display (HTML supported) - * @param {number} timeout - The number of milliseconds before the alert closes automatically + * @param {number} [timeout=0] - The number of milliseconds before the alert closes automatically * 0 for never (until the user closes it) * @param {boolean} [silent=false] - Don't show the message in the popup, only print it to the * console @@ -646,14 +646,12 @@ class App { * // Pops up a box with the message "Happy Christmas!" that will disappear after 5 seconds. * this.alert("Happy Christmas!", 5000); */ - alert(str, timeout, silent) { + alert(str, timeout=0, silent=false) { const time = new Date(); log.info("[" + time.toLocaleString() + "] " + str); if (silent) return; - timeout = timeout || 0; - this.currentSnackbar = $.snackbar({ content: str, timeout: timeout, @@ -670,18 +668,22 @@ class App { * * @param {string} title - The title of the box * @param {string} body - The question (HTML supported) + * @param {string} accept - The text of the accept button + * @param {string} reject - The text of the reject button * @param {function} callback - A function accepting one boolean argument which handles the * response e.g. function(answer) {...} * @param {Object} [scope=this] - The object to bind to the callback function * * @example * // Pops up a box asking if the user would like a cookie. Prints the answer to the console. - * this.confirm("Question", "Would you like a cookie?", function(answer) {console.log(answer);}); + * this.confirm("Question", "Would you like a cookie?", "Yes", "No", function(answer) {console.log(answer);}); */ - confirm(title, body, callback, scope) { + confirm(title, body, accept, reject, callback, scope) { scope = scope || this; document.getElementById("confirm-title").innerHTML = title; document.getElementById("confirm-body").innerHTML = body; + document.getElementById("confirm-yes").innerText = accept; + document.getElementById("confirm-no").innerText = reject; document.getElementById("confirm-modal").style.display = "block"; this.confirmClosed = false; @@ -694,9 +696,14 @@ class App { 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)(false); + if (!this.confirmClosed) { + callback.bind(scope)(undefined); + } this.confirmClosed = true; }.bind(this)); } diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 5ab44b21..cb579721 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -224,7 +224,7 @@ class Manager { document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options)); document.getElementById("reset-options").addEventListener("click", this.options.resetOptionsClick.bind(this.options)); this.addDynamicListener(".option-item input[type=checkbox]", "change", this.options.switchChange, this.options); - this.addDynamicListener(".option-item input[type=checkbox]", "change", this.options.setWordWrap, this.options); + this.addDynamicListener(".option-item input[type=checkbox]#wordWrap", "change", this.options.setWordWrap, this.options); this.addDynamicListener(".option-item input[type=checkbox]#useMetaKey", "change", this.bindings.updateKeybList, this.bindings); this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options); this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options); diff --git a/src/web/html/index.html b/src/web/html/index.html index 011742d3..4063e138 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -123,9 +123,9 @@ document.getElementById("preloader-error").innerHTML = "CyberChef encountered an error while loading.

" + "The following browser versions are supported:" + - "" + + "" + "Your user agent is:
" + escapeHtml(navigator.userAgent) + "

" + - "If your browser is supported, please " + + "If your browser is supported, please " + "raise an issue including the following details:

" + "
" + escapeHtml(msg) + "
"; }; @@ -563,16 +563,23 @@
- -
+ + + +
+ +
What
diff --git a/src/web/index.js b/src/web/index.js index 1d2bc242..736b512f 100755 --- a/src/web/index.js +++ b/src/web/index.js @@ -53,7 +53,9 @@ function main() { logLevel: "info", autoMagic: true, imagePreview: true, - syncTabs: true + syncTabs: true, + preserveCR: true, + userSetCR: false }; document.removeEventListener("DOMContentLoaded", main, false); diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index d7d628cb..2c8be70e 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -337,6 +337,29 @@ fill: var(--primary-font-colour); } +.pulse { + box-shadow: 0 0 0 0 rgba(90, 153, 212, .3); + animation: pulse 1.5s 1; +} + +.pulse:hover { + animation-play-state: paused; +} + +@keyframes pulse { + 0% { + transform: scale(1); + } + 70% { + transform: scale(1.1); + box-shadow: 0 0 0 20px rgba(90, 153, 212, 0); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(90, 153, 212, 0); + } +} + #input-find-options, #output-find-options { display: flex; diff --git a/src/web/stylesheets/layout/_structure.css b/src/web/stylesheets/layout/_structure.css index 77902d09..62ea1f9d 100755 --- a/src/web/stylesheets/layout/_structure.css +++ b/src/web/stylesheets/layout/_structure.css @@ -39,10 +39,16 @@ div#output { .split { box-sizing: border-box; - overflow: auto; + /* overflow: auto; + Removed to enable Background Magic button pulse to overflow. + Replace this rule if it seems to be causing problems. */ position: relative; } +#operations { + overflow: auto; +} + .split.split-horizontal, .gutter.gutter-horizontal { height: 100%; float: left; diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index 40ec412f..e519b963 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -222,8 +222,6 @@ class InputWaiter { if (Object.prototype.hasOwnProperty.call(r, "progress") && Object.prototype.hasOwnProperty.call(r, "inputNum")) { this.manager.tabs.updateInputTabProgress(r.inputNum, r.progress, 100); - } else if (Object.prototype.hasOwnProperty.call(r, "fileBuffer")) { - this.manager.tabs.updateInputTabProgress(r.inputNum, 100, 100); } const transferable = Object.prototype.hasOwnProperty.call(r, "fileBuffer") ? [r.fileBuffer] : undefined; @@ -305,6 +303,9 @@ class InputWaiter { case "removeChefWorker": this.removeChefWorker(); break; + case "fileLoaded": + this.fileLoaded(r.data.inputNum); + break; default: log.error(`Unknown action ${r.action}.`); } @@ -331,7 +332,7 @@ class InputWaiter { * @param {number} inputData.size - The size in bytes of the input file * @param {string} inputData.type - The MIME type of the input file * @param {number} inputData.progress - The load progress of the input file - * @param {boolean} [silent=false] - If true, fires the manager statechange event + * @param {boolean} [silent=false] - If false, fires the manager statechange event */ async set(inputData, silent=false) { return new Promise(function(resolve, reject) { @@ -373,7 +374,7 @@ class InputWaiter { if (!silent) window.dispatchEvent(this.manager.statechange); } else { - this.setFile(inputData); + this.setFile(inputData, silent); } }.bind(this)); @@ -389,8 +390,9 @@ class InputWaiter { * @param {number} inputData.size - The size in bytes of the input file * @param {string} inputData.type - The MIME type of the input file * @param {number} inputData.progress - The load progress of the input file + * @param {boolean} [silent=true] - If false, fires the manager statechange event */ - setFile(inputData) { + setFile(inputData, silent=true) { const activeTab = this.manager.tabs.getActiveInputTab(); if (inputData.inputNum !== activeTab) return; @@ -414,6 +416,30 @@ class InputWaiter { this.setInputInfo(inputData.size, null); this.displayFilePreview(inputData); + + if (!silent) window.dispatchEvent(this.manager.statechange); + } + + /** + * Update file details when a file completes loading + * + * @param {number} inputNum - The inputNum of the input which has finished loading + */ + fileLoaded(inputNum) { + this.manager.tabs.updateInputTabProgress(inputNum, 100, 100); + + const activeTab = this.manager.tabs.getActiveInputTab(); + if (activeTab !== inputNum) return; + + this.inputWorker.postMessage({ + action: "setInput", + data: { + inputNum: inputNum, + silent: false + } + }); + + this.updateFileProgress(inputNum, 100); } /** @@ -495,19 +521,6 @@ class InputWaiter { fileLoaded.textContent = progress + "%"; fileLoaded.style.color = ""; } - - if (progress === 100 && progress !== oldProgress) { - // Don't set the input if the progress hasn't changed - this.inputWorker.postMessage({ - action: "setInput", - data: { - inputNum: inputNum, - silent: false - } - }); - window.dispatchEvent(this.manager.statechange); - - } } /** @@ -711,33 +724,50 @@ class InputWaiter { * * @param {event} e */ - inputPaste(e) { - const pastedData = e.clipboardData.getData("Text"); - if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) { - // Pasting normally fires the inputChange() event before - // changing the value, so instead change it here ourselves - // and manually fire inputChange() - e.preventDefault(); - const inputText = document.getElementById("input-text"); - const selStart = inputText.selectionStart; - const selEnd = inputText.selectionEnd; - const startVal = inputText.value.slice(0, selStart); - const endVal = inputText.value.slice(selEnd); - - inputText.value = startVal + pastedData + endVal; - inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length); - this.debounceInputChange(e); - } else { - e.preventDefault(); - e.stopPropagation(); + async inputPaste(e) { + e.preventDefault(); + e.stopPropagation(); + const self = this; + /** + * Triggers the input file/binary data overlay + * + * @param {string} pastedData + */ + function triggerOverlay(pastedData) { const file = new File([pastedData], "PastedData", { type: "text/plain", lastModified: Date.now() }); - this.loadUIFiles([file]); + self.loadUIFiles([file]); + } + + const pastedData = e.clipboardData.getData("Text"); + const inputText = document.getElementById("input-text"); + const selStart = inputText.selectionStart; + const selEnd = inputText.selectionEnd; + const startVal = inputText.value.slice(0, selStart); + const endVal = inputText.value.slice(selEnd); + const val = startVal + pastedData + endVal; + + if (val.length >= (this.app.options.ioDisplayThreshold * 1024)) { + // Data too large to display, use overlay + triggerOverlay(val); return false; + } else if (await this.preserveCarriageReturns(val)) { + // Data contains a carriage return and the user doesn't wish to edit it, use overlay + // We check this in a separate condition to make sure it is not run unless absolutely + // necessary. + triggerOverlay(val); + return false; + } else { + // Pasting normally fires the inputChange() event before + // changing the value, so instead change it here ourselves + // and manually fire inputChange() + inputText.value = val; + inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length); + this.debounceInputChange(e); } } @@ -815,6 +845,46 @@ class InputWaiter { } } + /** + * Checks if an input contains carriage returns. + * If a CR is detected, checks if the preserve CR option has been set, + * and if not, asks the user for their preference. + * + * @param {string} input - The input to be checked + * @returns {boolean} - If true, the input contains a CR which should be + * preserved, so display an overlay so it can't be edited + */ + async preserveCarriageReturns(input) { + if (input.indexOf("\r") < 0) return false; + + const optionsStr = "This behaviour can be changed in the Options pane"; + if (!this.app.options.userSetCR) { + // User has not set a CR preference yet + let preserve = await new Promise(function(resolve, reject) { + this.app.confirm( + "Carriage Return Detected", + "A carriage return (\\r, 0x0d) was detected in your input. As HTML textareas can't display carriage returns, editing must be turned off to preserve them.
Alternatively, you can enable editing but your carriage returns will not be preserved.

This preference will be saved but can be toggled in the options pane.", + "Preserve Carriage Returns", + "Enable Editing", resolve, this); + }.bind(this)); + if (preserve === undefined) { + // The confirm pane was closed without picking a specific choice + this.app.alert(`Not preserving carriage returns.\n${optionsStr}`, 5000); + preserve = false; + } + this.manager.options.updateOption("preserveCR", preserve); + this.manager.options.updateOption("userSetCR", true); + } else { + if (this.app.options.preserveCR) { + this.app.alert(`A carriage return (\\r, 0x0d) was detected in your input, so editing has been disabled to preserve it.
${optionsStr}`, 10000); + } else { + this.app.alert(`A carriage return (\\r, 0x0d) was detected in your input. Editing is remaining enabled, but carriage returns will not be preserved.
${optionsStr}`, 10000); + } + } + + return this.app.options.preserveCR; + } + /** * Load files from the UI into the inputWorker * diff --git a/src/web/waiters/OptionsWaiter.mjs b/src/web/waiters/OptionsWaiter.mjs index eb6bac18..3dde372d 100755 --- a/src/web/waiters/OptionsWaiter.mjs +++ b/src/web/waiters/OptionsWaiter.mjs @@ -1,174 +1,180 @@ /** - * Waiter to handle events related to the CyberChef options. - * * @author n1474335 [n1474335@gmail.com] * @copyright Crown Copyright 2016 * @license Apache-2.0 - * - * @constructor - * @param {App} app - The main view object for CyberChef. */ -const OptionsWaiter = function(app, manager) { - this.app = app; - this.manager = manager; -}; - /** - * Loads options and sets values of switches and inputs to match them. - * - * @param {Object} options + * Waiter to handle events related to the CyberChef options. */ -OptionsWaiter.prototype.load = function(options) { - for (const option in options) { - this.app.options[option] = options[option]; +class OptionsWaiter { + + /** + * OptionsWaiter constructor. + * + * @param {App} app - The main view object for CyberChef. + * @param {Manager} manager - The CyberChef event manager. + */ + constructor(app, manager) { + this.app = app; + this.manager = manager; } - // Set options to match object - const cboxes = document.querySelectorAll("#options-body input[type=checkbox]"); - let i; - for (i = 0; i < cboxes.length; i++) { - cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")]; - } + /** + * Loads options and sets values of switches and inputs to match them. + * + * @param {Object} options + */ + load(options) { + for (const option in options) { + this.app.options[option] = options[option]; + } - const nboxes = document.querySelectorAll("#options-body input[type=number]"); - for (i = 0; i < nboxes.length; i++) { - nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")]; - nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true})); - } + // Set options to match object + const cboxes = document.querySelectorAll("#options-body input[type=checkbox]"); + let i; + for (i = 0; i < cboxes.length; i++) { + cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")]; + } - const selects = document.querySelectorAll("#options-body select"); - for (i = 0; i < selects.length; i++) { - const val = this.app.options[selects[i].getAttribute("option")]; - if (val) { - selects[i].value = val; - selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true})); - } else { - selects[i].selectedIndex = 0; + const nboxes = document.querySelectorAll("#options-body input[type=number]"); + for (i = 0; i < nboxes.length; i++) { + nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")]; + nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true})); + } + + const selects = document.querySelectorAll("#options-body select"); + for (i = 0; i < selects.length; i++) { + const val = this.app.options[selects[i].getAttribute("option")]; + if (val) { + selects[i].value = val; + selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true})); + } else { + selects[i].selectedIndex = 0; + } } } -}; -/** - * Handler for options click events. - * Dispays the options pane. - * - * @param {event} e - */ -OptionsWaiter.prototype.optionsClick = function(e) { - e.preventDefault(); - $("#options-modal").modal(); -}; - - -/** - * Handler for reset options click events. - * Resets options back to their default values. - */ -OptionsWaiter.prototype.resetOptionsClick = function() { - this.load(this.app.doptions); -}; - - -/** - * Handler for switch change events. - * Modifies the option state and saves it to local storage. - * - * @param {event} e - */ -OptionsWaiter.prototype.switchChange = function(e) { - const el = e.target; - const option = el.getAttribute("option"); - const state = el.checked; - - log.debug(`Setting ${option} to ${state}`); - this.app.options[option] = state; - - if (this.app.isLocalStorageAvailable()) - localStorage.setItem("options", JSON.stringify(this.app.options)); -}; - - -/** - * Handler for number change events. - * Modifies the option value and saves it to local storage. - * - * @param {event} e - */ -OptionsWaiter.prototype.numberChange = function(e) { - const el = e.target; - const option = el.getAttribute("option"); - const val = parseInt(el.value, 10); - - log.debug(`Setting ${option} to ${val}`); - this.app.options[option] = val; - - if (this.app.isLocalStorageAvailable()) - localStorage.setItem("options", JSON.stringify(this.app.options)); -}; - - -/** - * Handler for select change events. - * Modifies the option value and saves it to local storage. - * - * @param {event} e - */ -OptionsWaiter.prototype.selectChange = function(e) { - const el = e.target; - const option = el.getAttribute("option"); - - log.debug(`Setting ${option} to ${el.value}`); - this.app.options[option] = el.value; - - if (this.app.isLocalStorageAvailable()) - localStorage.setItem("options", JSON.stringify(this.app.options)); -}; - - -/** - * Sets or unsets word wrap on the input and output depending on the wordWrap option value. - */ -OptionsWaiter.prototype.setWordWrap = function() { - document.getElementById("input-text").classList.remove("word-wrap"); - document.getElementById("output-text").classList.remove("word-wrap"); - document.getElementById("output-html").classList.remove("word-wrap"); - document.getElementById("input-highlighter").classList.remove("word-wrap"); - document.getElementById("output-highlighter").classList.remove("word-wrap"); - - if (!this.app.options.wordWrap) { - document.getElementById("input-text").classList.add("word-wrap"); - document.getElementById("output-text").classList.add("word-wrap"); - document.getElementById("output-html").classList.add("word-wrap"); - document.getElementById("input-highlighter").classList.add("word-wrap"); - document.getElementById("output-highlighter").classList.add("word-wrap"); + /** + * Handler for options click events. + * Dispays the options pane. + * + * @param {event} e + */ + optionsClick(e) { + e.preventDefault(); + $("#options-modal").modal(); } -}; -/** - * Changes the theme by setting the class of the element. - * - * @param {Event} e - */ -OptionsWaiter.prototype.themeChange = function (e) { - const themeClass = e.target.value; - - document.querySelector(":root").className = themeClass; -}; + /** + * Handler for reset options click events. + * Resets options back to their default values. + */ + resetOptionsClick() { + this.load(this.app.doptions); + } -/** - * Changes the console logging level. - * - * @param {Event} e - */ -OptionsWaiter.prototype.logLevelChange = function (e) { - const level = e.target.value; - log.setLevel(level, false); - this.manager.worker.setLogLevel(); - this.manager.input.setLogLevel(); -}; + /** + * Handler for switch change events. + * + * @param {event} e + */ + switchChange(e) { + const el = e.target; + const option = el.getAttribute("option"); + const state = el.checked; + + this.updateOption(option, state); + } + + + /** + * Handler for number change events. + * + * @param {event} e + */ + numberChange(e) { + const el = e.target; + const option = el.getAttribute("option"); + const val = parseInt(el.value, 10); + + this.updateOption(option, val); + } + + + /** + * Handler for select change events. + * + * @param {event} e + */ + selectChange(e) { + const el = e.target; + const option = el.getAttribute("option"); + + this.updateOption(option, el.value); + } + + /** + * Modifies an option value and saves it to local storage. + * + * @param {string} option - The option to be updated + * @param {string|number|boolean} value - The new value of the option + */ + updateOption(option, value) { + log.debug(`Setting ${option} to ${value}`); + this.app.options[option] = value; + + if (this.app.isLocalStorageAvailable()) + localStorage.setItem("options", JSON.stringify(this.app.options)); + } + + + /** + * Sets or unsets word wrap on the input and output depending on the wordWrap option value. + */ + setWordWrap() { + document.getElementById("input-text").classList.remove("word-wrap"); + document.getElementById("output-text").classList.remove("word-wrap"); + document.getElementById("output-html").classList.remove("word-wrap"); + document.getElementById("input-highlighter").classList.remove("word-wrap"); + document.getElementById("output-highlighter").classList.remove("word-wrap"); + + if (!this.app.options.wordWrap) { + document.getElementById("input-text").classList.add("word-wrap"); + document.getElementById("output-text").classList.add("word-wrap"); + document.getElementById("output-html").classList.add("word-wrap"); + document.getElementById("input-highlighter").classList.add("word-wrap"); + document.getElementById("output-highlighter").classList.add("word-wrap"); + } + } + + + /** + * Changes the theme by setting the class of the element. + * + * @param {Event} e + */ + themeChange(e) { + const themeClass = e.target.value; + + document.querySelector(":root").className = themeClass; + } + + + /** + * Changes the console logging level. + * + * @param {Event} e + */ + logLevelChange(e) { + const level = e.target.value; + log.setLevel(level, false); + this.manager.worker.setLogLevel(); + this.manager.input.setLogLevel(); + } +} export default OptionsWaiter; diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index 005d9533..4a08fe8d 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -217,6 +217,9 @@ class OutputWaiter { */ removeAllOutputs() { this.outputs = {}; + + this.resetSwitch(); + const tabsList = document.getElementById("output-tabs"); const tabsListChildren = tabsList.children; @@ -516,9 +519,10 @@ class OutputWaiter { this.app.alert("Could not find any output data to download. Has this output been baked?", 3000); return; } - let fileName = window.prompt("Please enter a filename: ", "download.dat"); + const fileName = window.prompt("Please enter a filename: ", "download.dat"); - if (fileName === null) fileName = "download.dat"; + // Assume if the user clicks cancel they don't want to download + if (fileName === null) return; const data = await dish.get(Dish.ARRAY_BUFFER), file = new File([data], fileName); @@ -529,12 +533,22 @@ class OutputWaiter { * Handler for save all click event * Saves all outputs to a single archvie file */ - saveAllClick() { + async saveAllClick() { const downloadButton = document.getElementById("save-all-to-file"); if (downloadButton.firstElementChild.innerHTML === "archive") { this.downloadAllFiles(); - } else if (window.confirm("Cancel zipping of outputs?")) { - this.terminateZipWorker(); + } else { + const cancel = await new Promise(function(resolve, reject) { + this.app.confirm( + "Cancel zipping?", + "The outputs are currently being zipped for download.
Cancel zipping?", + "Continue zipping", + "Cancel zipping", + resolve, this); + }.bind(this)); + if (!cancel) { + this.terminateZipWorker(); + } } } @@ -544,57 +558,61 @@ class OutputWaiter { * be zipped for download */ async downloadAllFiles() { - return new Promise(resolve => { - const inputNums = Object.keys(this.outputs); - for (let i = 0; i < inputNums.length; i++) { - const iNum = inputNums[i]; - if (this.outputs[iNum].status !== "baked" || - this.outputs[iNum].bakeId !== this.manager.worker.bakeId) { - if (window.confirm("Not all outputs have been baked yet. Continue downloading outputs?")) { - break; - } else { - return; - } + const inputNums = Object.keys(this.outputs); + for (let i = 0; i < inputNums.length; i++) { + const iNum = inputNums[i]; + if (this.outputs[iNum].status !== "baked" || + this.outputs[iNum].bakeId !== this.manager.worker.bakeId) { + const continueDownloading = await new Promise(function(resolve, reject) { + this.app.confirm( + "Incomplete outputs", + "Not all outputs have been baked yet. Continue downloading outputs?", + "Download", "Cancel", resolve, this); + }.bind(this)); + if (continueDownloading) { + break; + } else { + return; } } + } - let fileName = window.prompt("Please enter a filename: ", "download.zip"); + let fileName = window.prompt("Please enter a filename: ", "download.zip"); - if (fileName === null || fileName === "") { - // Don't zip the files if there isn't a filename - this.app.alert("No filename was specified.", 3000); - return; - } + if (fileName === null || fileName === "") { + // Don't zip the files if there isn't a filename + this.app.alert("No filename was specified.", 3000); + return; + } - if (!fileName.match(/.zip$/)) { - fileName += ".zip"; - } + if (!fileName.match(/.zip$/)) { + fileName += ".zip"; + } - let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", ""); + let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", ""); - if (fileExt === null) fileExt = ""; + if (fileExt === null) fileExt = ""; - if (this.zipWorker !== null) { - this.terminateZipWorker(); - } + if (this.zipWorker !== null) { + this.terminateZipWorker(); + } - const downloadButton = document.getElementById("save-all-to-file"); + const downloadButton = document.getElementById("save-all-to-file"); - downloadButton.classList.add("spin"); - downloadButton.title = `Zipping ${inputNums.length} files...`; - downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`); + downloadButton.classList.add("spin"); + downloadButton.title = `Zipping ${inputNums.length} files...`; + downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`); - downloadButton.firstElementChild.innerHTML = "autorenew"; + downloadButton.firstElementChild.innerHTML = "autorenew"; - log.debug("Creating ZipWorker"); - this.zipWorker = new ZipWorker(); - this.zipWorker.postMessage({ - outputs: this.outputs, - filename: fileName, - fileExtension: fileExt - }); - this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this)); + log.debug("Creating ZipWorker"); + this.zipWorker = new ZipWorker(); + this.zipWorker.postMessage({ + outputs: this.outputs, + filename: fileName, + fileExtension: fileExt }); + this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this)); } /** @@ -1065,6 +1083,7 @@ class OutputWaiter { magicButton.setAttribute("data-original-title", `${opSequence} will produce "${Utils.escapeHtml(Utils.truncate(result), 30)}"`); magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, ""); magicButton.classList.remove("hidden"); + magicButton.classList.add("pulse"); } @@ -1074,6 +1093,7 @@ class OutputWaiter { hideMagicButton() { const magicButton = document.getElementById("magic"); magicButton.classList.add("hidden"); + magicButton.classList.remove("pulse"); magicButton.setAttribute("data-recipe", ""); magicButton.setAttribute("data-original-title", "Magic!"); } @@ -1213,14 +1233,39 @@ class OutputWaiter { * Moves the current output into the input textarea. */ async switchClick() { - const active = await this.getDishBuffer(this.getOutputDish(this.manager.tabs.getActiveOutputTab())); + const activeTab = this.manager.tabs.getActiveOutputTab(); + const transferable = []; + + const switchButton = document.getElementById("switch"); + switchButton.classList.add("spin"); + switchButton.disabled = true; + switchButton.firstElementChild.innerHTML = "autorenew"; + $(switchButton).tooltip("hide"); + + let active = await this.getDishBuffer(this.getOutputDish(activeTab)); + + if (!this.outputExists(activeTab)) { + this.resetSwitchButton(); + return; + } + + if (this.outputs[activeTab].data.type === "string" && + active.byteLength <= this.app.options.ioDisplayThreshold * 1024) { + const dishString = await this.getDishStr(this.getOutputDish(activeTab)); + if (!await this.manager.input.preserveCarriageReturns(dishString)) { + active = dishString; + } + } else { + transferable.push(active); + } + this.manager.input.inputWorker.postMessage({ action: "inputSwitch", data: { - inputNum: this.manager.tabs.getActiveInputTab(), + inputNum: activeTab, outputData: active } - }, [active]); + }, transferable); } /** @@ -1238,6 +1283,9 @@ class OutputWaiter { inputSwitch(switchData) { this.switchOrigData = switchData; document.getElementById("undo-switch").disabled = false; + + this.resetSwitchButton(); + } /** @@ -1246,17 +1294,35 @@ class OutputWaiter { */ undoSwitchClick() { this.manager.input.updateInputObj(this.switchOrigData.inputNum, this.switchOrigData.data); + + this.manager.input.fileLoaded(this.switchOrigData.inputNum); + + this.resetSwitch(); + } + + /** + * Removes the switch data and resets the switch buttons + */ + resetSwitch() { + if (this.switchOrigData !== undefined) { + delete this.switchOrigData; + } + const undoSwitch = document.getElementById("undo-switch"); undoSwitch.disabled = true; $(undoSwitch).tooltip("hide"); - this.manager.input.inputWorker.postMessage({ - action: "setInput", - data: { - inputNum: this.switchOrigData.inputNum, - silent: false - } - }); + this.resetSwitchButton(); + } + + /** + * Resets the switch button to its usual state + */ + resetSwitchButton() { + const switchButton = document.getElementById("switch"); + switchButton.classList.remove("spin"); + switchButton.disabled = false; + switchButton.firstElementChild.innerHTML = "open_in_browser"; } /** diff --git a/src/web/workers/InputWorker.mjs b/src/web/workers/InputWorker.mjs index cec68627..521539c5 100644 --- a/src/web/workers/InputWorker.mjs +++ b/src/web/workers/InputWorker.mjs @@ -202,6 +202,7 @@ self.bakeInput = function(inputNum, bakeId) { if (inputObj === null || inputObj === undefined || inputObj.status !== "loaded") { + self.postMessage({ action: "queueInputError", data: { @@ -441,7 +442,7 @@ self.updateTabHeader = function(inputNum) { * * @param {object} inputData * @param {number} inputData.inputNum - The input to get the data for - * @param {boolean} inputData.silent - If false, the manager statechange event won't be fired + * @param {boolean} inputData.silent - If false, the manager statechange event will be fired */ self.setInput = function(inputData) { const inputNum = inputData.inputNum; @@ -590,7 +591,7 @@ self.updateInputObj = function(inputData) { const inputNum = inputData.inputNum; const data = inputData.data; - if (self.getInputObj(inputNum) === -1) return; + if (self.getInputObj(inputNum) === undefined) return; self.inputs[inputNum].data = data; }; @@ -663,11 +664,19 @@ self.handleLoaderMessage = function(r) { if ("fileBuffer" in r) { log.debug(`Input file ${inputNum} loaded.`); self.loadingInputs--; + self.updateInputValue({ inputNum: inputNum, value: r.fileBuffer }); + self.postMessage({ + action: "fileLoaded", + data: { + inputNum: inputNum + } + }); + const idx = self.getLoaderWorkerIdx(r.id); self.loadNextFile(idx); } else if ("progress" in r) { @@ -782,7 +791,7 @@ self.loadFiles = function(filesData) { } self.getLoadProgress(); - self.setInput({inputNum: activeTab, silent: false}); + self.setInput({inputNum: activeTab, silent: true}); }; /** @@ -1025,7 +1034,7 @@ self.inputSwitch = function(switchData) { const currentData = currentInput.data; if (currentInput === undefined || currentInput === null) return; - if (typeof switchData.outputData === "object") { + if (typeof switchData.outputData !== "string") { const output = new Uint8Array(switchData.outputData), types = detectFileType(output); let type = "unknown", @@ -1036,15 +1045,22 @@ self.inputSwitch = function(switchData) { } // ArrayBuffer - currentInput.data = { - fileBuffer: switchData.outputData, - name: `output.${ext}`, - size: switchData.outputData.byteLength.toLocaleString(), - type: type - }; + self.updateInputObj({ + inputNum: switchData.inputNum, + data: { + fileBuffer: switchData.outputData, + name: `output.${ext}`, + size: switchData.outputData.byteLength.toLocaleString(), + type: type + } + }); } else { // String - currentInput.data = switchData.outputData; + self.updateInputValue({ + inputNum: switchData.inputNum, + value: switchData.outputData, + force: true + }); } self.postMessage({ @@ -1055,6 +1071,11 @@ self.inputSwitch = function(switchData) { } }); - self.setInput({inputNum: switchData.inputNum, silent: false}); + self.postMessage({ + action: "fileLoaded", + data: { + inputNum: switchData.inputNum + } + }); }; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index eac6ab9a..696deba0 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -26,6 +26,7 @@ import "./tests/BitwiseOp"; import "./tests/ByteRepr"; import "./tests/CartesianProduct"; import "./tests/CharEnc"; +import "./tests/ChangeIPFormat"; import "./tests/Charts"; import "./tests/Checksum"; import "./tests/Ciphers"; @@ -88,6 +89,7 @@ import "./tests/BLAKE2s"; import "./tests/Protobuf"; import "./tests/ParseSSHHostKey"; import "./tests/DefangIP"; +import "./tests/ParseUDP"; // Cannot test operations that use the File type yet //import "./tests/SplitColourChannels"; diff --git a/tests/operations/tests/ChangeIPFormat.mjs b/tests/operations/tests/ChangeIPFormat.mjs new file mode 100644 index 00000000..d92ffb79 --- /dev/null +++ b/tests/operations/tests/ChangeIPFormat.mjs @@ -0,0 +1,52 @@ +/** + * Change IP format tests. + * + * @author Chris Smith + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Change IP format: Dotted Decimal to Hex", + input: "192.168.1.1", + expectedOutput: "c0a80101", + recipeConfig: [ + { + op: "Change IP format", + args: ["Dotted Decimal", "Hex"], + }, + ], + }, { + name: "Change IP format: Decimal to Dotted Decimal", + input: "3232235777", + expectedOutput: "192.168.1.1", + recipeConfig: [ + { + op: "Change IP format", + args: ["Decimal", "Dotted Decimal"], + }, + ], + }, { + name: "Change IP format: Hex to Octal", + input: "c0a80101", + expectedOutput: "030052000401", + recipeConfig: [ + { + op: "Change IP format", + args: ["Hex", "Octal"], + }, + ], + }, { + name: "Change IP format: Octal to Decimal", + input: "030052000401", + expectedOutput: "3232235777", + recipeConfig: [ + { + op: "Change IP format", + args: ["Octal", "Decimal"], + }, + ], + }, +]); diff --git a/tests/operations/tests/Crypt.mjs b/tests/operations/tests/Crypt.mjs index 1f92bcb0..1db8093f 100644 --- a/tests/operations/tests/Crypt.mjs +++ b/tests/operations/tests/Crypt.mjs @@ -18,6 +18,42 @@ TestRegister.addTests([ * * All random data blocks (binary input, keys and IVs) were generated from /dev/urandom using dd: * > dd if=/dev/urandom of=key.txt bs=16 count=1 + * + * + * The following is a Python script used to generate the AES-GCM tests. + * It uses PyCryptodome (https://www.pycryptodome.org) to handle the AES encryption and decryption. + * + * from Crypto.Cipher import AES + * import binascii + + * input_data = "0123456789ABCDEF" + * key = binascii.unhexlify("00112233445566778899aabbccddeeff") + * iv = binascii.unhexlify("ffeeddccbbaa99887766554433221100") + * + * cipher = AES.new(key, AES.MODE_GCM, nonce=iv) + * cipher_text, tag = cipher.encrypt_and_digest(binascii.unhexlify(input_data)) + * + * cipher = AES.new(key, AES.MODE_GCM, nonce=iv) + * decrypted = cipher.decrypt_and_verify(cipher_text, tag) + * + * key = binascii.hexlify(key).decode("UTF-8") + * iv = binascii.hexlify(iv).decode("UTF-8") + * cipher_text = binascii.hexlify(cipher_text).decode("UTF-8") + * tag = binascii.hexlify(tag).decode("UTF-8") + * decrypted = binascii.hexlify(decrypted).decode("UTF-8") + * + * print("Key: {}\nIV : {}\nInput data: {}\n\nEncrypted ciphertext: {}\nGCM tag: {}\n\nDecrypted plaintext : {}".format(key, iv, input_data, cipher_text, tag, decrypted)) + * + * + * Outputs: + * Key: 00112233445566778899aabbccddeeff + * IV : ffeeddccbbaa99887766554433221100 + * Input data: 0123456789ABCDEF + * + * Encrypted ciphertext: 8feeafedfdb2f6f9 + * GCM tag: 654ef4957c6e2b0cc6501d8f9bcde032 + * + * Decrypted plaintext : 0123456789abcdef */ { name: "AES Encrypt: no key", @@ -838,7 +874,7 @@ The following algorithms will be used based on the size of the key: }, { name: "AES Decrypt: AES-128-GCM, Binary", - input: "fa17fcbf5e8763322c1b0c8562e1512ed9d702ef70c1643572b9de3e34ae6b535e6c1b992432aa6d06fb6f80c861262aef66e7c26035afe77bd3861261e4e092b523f058f8ebef2143db21bc16d02f7a011efb07419300cb41c3b884d1d8d6a766b8963c", + input: "5a29debb5c5f38cdf8aee421bd94dbbf3399947faddf205f88b3ad8ecb0c51214ec0e28bf78942dfa212d7eb15259bbdcac677b4c05f473eeb9331d74f31d441d97d56eb5c73b586342d72128ca528813543dc0fc7eddb7477172cc9194c18b2e1383e4e", expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", recipeConfig: [ { @@ -847,7 +883,7 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "Hex", "string": "fa6bbb34c8cde65a3d7b93fb094fc84f"} + {"option": "Hex", "string": "70fad2ca19412c20f40fd06918736e56"} ] } ], @@ -934,7 +970,7 @@ The following algorithms will be used based on the size of the key: }, { name: "AES Decrypt: AES-192-GCM, Binary", - input: "ed22946f96964d300b45f5ce2d9601ba87682da1a603c90e6d4f7738729b0602f613ee392c9bfc7792594474f1213fb99185851f02ece4df0e93995e49f97aa4d0a337d7a80d83e4219dae5a3d36658f8659cdd5ed7c32707f98656fab7fb43f7a61e37c", + input: "318b479d919d506f0cd904f2676fab263a7921b6d7e0514f36e03ae2333b77fa66ef5600babcb2ee9718aeb71fc357412343c1f2cb351d8715bb0aedae4a6468124f9c4aaf6a721b306beddbe63a978bec8baeeba4b663be33ee5bc982746bd4aed1c38b", expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", recipeConfig: [ { @@ -943,7 +979,7 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "Hex", "string": "be17cb31edb77f648b9d1032b235b33d"} + {"option": "Hex", "string": "86db597d5302595223cadbd990f1309b"} ] } ], @@ -1030,7 +1066,7 @@ The following algorithms will be used based on the size of the key: }, { name: "AES Decrypt: AES-256-GCM, Binary", - input: "e3f1b236eaf3b9df69df8133a1b417fa42b242d8ad49e4d2f3469aca7e2a41737e4f2c8a0d212143287088fad51743577dc6dfa8ed328ca90113cbeb9b137926b2168cc037bdc371777e6ee02b9d9c017b6054fd83d43b4885fbe9c044a8574f1491a893", + input: "1287f188ad4d7ab0d9ff69b3c29cb11f861389532d8cb9337181da2e8cfc74a84927e8c0dd7a28a32fd485afe694259a63c199b199b95edd87c7aa95329feac340f2b78b72956a85f367044d821766b1b7135815571df44900695f1518cf3ae38ecb650f", expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", recipeConfig: [ { @@ -1039,7 +1075,7 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "Hex", "string": "23ddbd3ee4de33f98a9ea9a170bdf268"} + {"option": "Hex", "string": "821b1e5f32dad052e502775a523d957a"} ] } ], diff --git a/tests/operations/tests/ParseUDP.mjs b/tests/operations/tests/ParseUDP.mjs new file mode 100644 index 00000000..2c519232 --- /dev/null +++ b/tests/operations/tests/ParseUDP.mjs @@ -0,0 +1,68 @@ +/** + * Parse UDP tests. + * + * @author h345983745 + * + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Parse UDP: No Data - JSON", + input: "04 89 00 35 00 2c 01 01", + expectedOutput: "{\"Source port\":1161,\"Destination port\":53,\"Length\":44,\"Checksum\":\"0101\"}", + recipeConfig: [ + { + op: "From Hex", + args: ["Auto"], + }, + { + op: "Parse UDP", + args: [], + }, + { + op: "JSON Minify", + args: [], + }, + ], + }, { + name: "Parse UDP: With Data - JSON", + input: "04 89 00 35 00 2c 01 01 02 02", + expectedOutput: "{\"Source port\":1161,\"Destination port\":53,\"Length\":44,\"Checksum\":\"0101\",\"Data\":\"0202\"}", + recipeConfig: [ + { + op: "From Hex", + args: ["Auto"], + }, + { + op: "Parse UDP", + args: [], + }, + { + op: "JSON Minify", + args: [], + }, + ], + }, + { + name: "Parse UDP: Not Enough Bytes", + input: "04 89 00", + expectedOutput: "Need 8 bytes for a UDP Header", + recipeConfig: [ + { + op: "From Hex", + args: ["Auto"], + }, + { + op: "Parse UDP", + args: [], + }, + { + op: "JSON Minify", + args: [], + }, + ], + } +]);