2
0
mirror of https://github.com/gchq/CyberChef synced 2025-12-05 23:53:27 +00:00

Compare commits

...

219 Commits

Author SHA1 Message Date
n1474335
7b3efa746e 8.26.2 2019-03-11 11:54:19 +00:00
n1474335
90ddc2bfa7 Merge branch 'MShwed-bugs/xpath-namespace-prefix-fix' 2019-03-11 11:54:11 +00:00
n1474335
8e3425ed6d Merge branch 'bugs/xpath-namespace-prefix-fix' of https://github.com/MShwed/CyberChef into MShwed-bugs/xpath-namespace-prefix-fix 2019-03-11 11:53:06 +00:00
n1474335
c1bb42fe65 8.26.1 2019-03-11 11:44:39 +00:00
n1474335
b99f73919f Merge branch 'artemisbot-bugs/disassembler' 2019-03-11 11:44:32 +00:00
n1474335
978bf75765 Merge branch 'bugs/disassembler' of https://github.com/artemisbot/CyberChef into artemisbot-bugs/disassembler 2019-03-11 11:44:11 +00:00
Matt
432d5b43a1 Fix some misspellings 2019-03-10 17:51:30 +00:00
mshwed
3412372d1e Added support for non-prefixed default namespace selectors 2019-03-10 13:02:13 -04:00
Matt
c2e130f369 Update disassembler 2019-03-10 13:19:04 +00:00
n1474335
e8880f068f 8.26.0 2019-03-09 07:23:57 +00:00
n1474335
45c1c23e09 Merge branch 'j433866-image-operations' 2019-03-09 07:23:31 +00:00
n1474335
e10d4bf45c Tidied up image manipulation ops 2019-03-09 07:23:11 +00:00
n1474335
369b640408 Merge branch 'master' into j433866-image-operations 2019-03-09 06:29:19 +00:00
n1474335
d2d30bf668 8.25.0 2019-03-09 06:28:05 +00:00
n1474335
dcb59edb79 Merge branch 'feature-extract-files' 2019-03-09 06:26:36 +00:00
n1474335
84d31c1d59 Added 'Move to input' button to output file list. Improved zlib extraction efficiency. 2019-03-09 06:25:27 +00:00
n1474335
58d41f4458 8.24.3 2019-03-09 05:38:13 +00:00
n1474335
360effb839 Merge branch 'j433866-fork-fix' 2019-03-09 05:37:57 +00:00
j433866
d923c99975 Fix same bug in subsection 2019-03-07 16:33:38 +00:00
j433866
2b538061e9 Fix fork operation not setting ingredient values correctly. 2019-03-07 16:26:42 +00:00
j433866
0c9db5afe9 Fix typo 2019-03-07 11:36:29 +00:00
j433866
1031429550 Add error handling 2019-03-07 11:19:04 +00:00
j433866
4a7ea469d4 Add status messages for image operations 2019-03-07 10:03:09 +00:00
j433866
833c1cd98f Add Contain Image, Cover Image and Image Hue / Saturation / Lightness ops 2019-03-07 10:02:37 +00:00
j433866
662922be6f Add resizing status message 2019-03-06 10:32:58 +00:00
j433866
370ae323f6 Fix linting 2019-03-05 11:49:25 +00:00
j433866
514eef50de Add image filter operation 2019-03-04 14:48:17 +00:00
j433866
ec1fd7b923 Add image opacity operation 2019-03-04 14:38:25 +00:00
j433866
737ce99398 Add image brightness / contrast operation 2019-03-04 14:24:57 +00:00
j433866
4f1a897e18 Add Crop Image operation 2019-03-04 13:48:48 +00:00
j433866
588a8b2a3a Fix code syntax 2019-03-04 13:48:29 +00:00
j433866
f281a32a4e Add Wikipedia URLs 2019-03-04 13:48:13 +00:00
j433866
d09e6089ca Add min width and height values 2019-03-04 11:52:54 +00:00
j433866
7b6062a4a2 Set min blur amount to 1, add status message for gaussian blur. 2019-03-04 11:47:50 +00:00
j433866
7975fadfe9 Add options for min, max and step values for number inputs. 2019-03-04 11:46:27 +00:00
n1474335
9fa7edffbf Improved file extraction error handling 2019-03-02 16:12:21 +00:00
n1474335
24a47445f6 Merge branch 'master' into feature-extract-files 2019-03-02 15:40:32 +00:00
Matt
68278267e1 Update libyara-wasm 2019-02-23 15:13:58 +00:00
j433866
0d86a7e427 Add resize algorithm option 2019-02-20 15:35:53 +00:00
j433866
9f4aa0a123 Remove trailing space 2019-02-20 13:17:57 +00:00
j433866
da838e266e Add flip image operation 2019-02-20 13:04:15 +00:00
j433866
fd160e87e8 Add image operations to Categories 2019-02-20 11:54:59 +00:00
j433866
0dd4304902 Add new Blur Image operation.
Performs both fast blur and gaussian blur
2019-02-20 11:48:24 +00:00
j433866
a0b94bba4e Change run() functions to be async 2019-02-20 11:26:39 +00:00
j433866
74c2a2b5cb Add Invert Image operation 2019-02-20 11:12:15 +00:00
j433866
b691c30677 Add dither image operation 2019-02-20 09:20:38 +00:00
j433866
01acefe4cf Remove scale image operation.
(Same functionality is implemented in Resize Image)
2019-02-19 16:20:36 +00:00
j433866
1a2c5a95c7 Add resize image operation 2019-02-19 16:19:34 +00:00
j433866
eb8725a0db Fix degrees error 2019-02-19 16:10:53 +00:00
j433866
57e1061063 Add Scale Image operation 2019-02-19 15:37:59 +00:00
j433866
91f4681a3c Add rotate image operation 2019-02-19 15:37:49 +00:00
n1474335
8148c1a8a8 8.24.2 2019-02-11 18:45:27 +00:00
n1474335
ce61bcc078 Merge branch 'd98762625-dynamic-import' 2019-02-11 18:45:00 +00:00
n1474335
49e2b05a11 Dynamic module loading is now cached and Webpack includes the import in the main chunk. 2019-02-11 18:44:41 +00:00
d98762625
481f2a4717 Merge branch 'master' of github.com:gchq/CyberChef into dynamic-import 2019-02-11 16:54:31 +00:00
d98762625
c01c076561 try/catch to allow chef to run from prod file 2019-02-11 16:51:23 +00:00
d98762625
2391e08ac1 tidy up 2019-02-11 15:47:28 +00:00
d98762625
a7a2fe243a prod working with mini css webpack plugin 2019-02-11 15:40:18 +00:00
n1474335
b5c655dd70 8.24.1 2019-02-11 14:48:45 +00:00
n1474335
d84a61d108 Merge branch 'j433866-open-file-button' 2019-02-11 14:48:38 +00:00
n1474335
13abbd2c5d Abstracted out loadFile logic to separate function 2019-02-11 14:48:25 +00:00
d98762625
d22eac9f35 WIP getting prod working with minicss webpack plugin 2019-02-11 14:29:29 +00:00
n1474335
d71aa8d7e2 Merge branch 'open-file-button' of https://github.com/j433866/CyberChef into j433866-open-file-button 2019-02-11 14:28:32 +00:00
n1474335
7a4e0301d2 8.24.0 2019-02-08 18:07:10 +00:00
n1474335
42826e542d Updated changelog 2019-02-08 18:07:03 +00:00
n1474335
42d1c9403c Merge branch 'h345983745-dnsoverhttps' 2019-02-08 18:02:39 +00:00
n1474335
ab43635583 Tidied up 'DNS over HTTPS' operation and fixed manualBake flag. 2019-02-08 18:02:13 +00:00
n1474335
b8ecd83bfd Merge branch 'dnsoverhttps' of https://github.com/h345983745/CyberChef into h345983745-dnsoverhttps 2019-02-08 17:34:33 +00:00
n1474335
0db0ced1ab 8.23.4 2019-02-08 17:28:16 +00:00
n1474335
3d20833d42 Fixed populateOption HTML escape bug. Closes #490 2019-02-08 17:28:10 +00:00
n1474335
7d09ba5669 8.23.3 2019-02-08 17:06:42 +00:00
n1474335
310ff30278 Merge branch 'j433866-xss_fixes' 2019-02-08 17:06:32 +00:00
n1474335
821bc9405c Merge branch 'xss_fixes' of https://github.com/j433866/CyberChef into j433866-xss_fixes 2019-02-08 17:05:51 +00:00
n1474335
d54d66cffc 8.23.2 2019-02-08 16:54:11 +00:00
n1474335
d2b4c40357 Fixed BigNumber/XRegExp incompatibility. CLoses #481 2019-02-08 16:54:04 +00:00
n1474335
f48af97ddc 8.23.1 2019-02-08 15:53:42 +00:00
n1474335
f1264d6310 Update package-lock.json 2019-02-08 15:53:40 +00:00
n1474335
4dc5a1499a Merge branch 'j433866-coords-fix' 2019-02-08 15:52:40 +00:00
d98762625
58a8af20a6 Highligting for Recipe now working. Discovered bug when highlighting on a test case 2019-02-08 14:28:53 +00:00
h345983745
75a58f465c Removed jpath import 2019-02-07 21:05:07 +00:00
h345983745
613cbaa556 Fixing Formating Issues 2019-02-07 08:28:23 +00:00
h345983745
0d0a634255 Added More Request Types 2019-02-06 23:27:27 +00:00
h345983745
105090db60 Spelling Check 2019-02-06 22:50:46 +00:00
h345983745
3e9c75f735 Added to Categories 2019-02-06 22:34:43 +00:00
h345983745
d42075072b Small Updates 2019-02-06 20:54:06 +00:00
h345983745
6a099f0813 Inital Commit 2019-02-06 20:20:20 +00:00
d98762625
9af5e40071 update linting to allow dybnamic import. Recipe highlight still broken 2019-02-01 14:05:48 +00:00
d98762625
4bf2a29070 WIP: bundle all css into main.css with mini-css-extract. Cannot split into vendor and styles without breaking at the moment. 2019-02-01 13:45:47 +00:00
d98762625
069d0e48c1 WIP: use mini-css-extract-plugin instead of extract-text, to work with dynamic import. Currently outputting only one css and recipe highlight doesnt work 2019-02-01 12:00:45 +00:00
d98762625
c8cb2692dd WIP: Recipe refactored to use dynamic import 2019-02-01 10:52:21 +00:00
j433866
74a22bcf9c Swap ordering of truncating and escaping 2019-01-31 15:22:25 +00:00
j433866
8b44927cb6 Fix XSS for To Table operation and Magic button 2019-01-31 15:18:37 +00:00
j433866
3209c94622 Fix conversion breaking when compass directions are used as delimiters 2019-01-21 12:50:30 +00:00
n1474335
6f8a5ea1be Merge branch 'j433866-coordinates' 2019-01-18 17:14:39 +00:00
n1474335
69837837b0 Tidied up co-ordinate operation 2019-01-18 17:14:25 +00:00
n1474335
03d8bf2836 Merge branch 'coordinates' of https://github.com/j433866/CyberChef into j433866-coordinates 2019-01-18 16:05:44 +00:00
n1474335
eca2c142f3 8.23.0 2019-01-18 15:37:42 +00:00
n1474335
715f7bbbc2 Lint 2019-01-18 15:37:25 +00:00
n1474335
291ebd5c12 Merge branch 'artemisbot-features/yara' 2019-01-18 15:35:14 +00:00
n1474335
ba04cac7ac Tidied up YARA operation 2019-01-18 15:34:56 +00:00
j433866
acb8c0b5af Change icon from folder to input 2019-01-18 15:12:03 +00:00
j433866
0c14bacea7 Add button to input to allow opening of files using the file prompt. 2019-01-18 15:07:19 +00:00
n1474335
4cabb849f3 Merge branch 'features/yara' of https://github.com/artemisbot/CyberChef into artemisbot-features/yara 2019-01-18 14:56:26 +00:00
n1474335
445a85798b 8.22.1 2019-01-18 14:50:00 +00:00
n1474335
55775f48e9 Merge branch 'picapi-b64-description-fix' 2019-01-18 14:49:53 +00:00
j433866
4bd923dc06 Improved handling of negative numbers and weirder inputs.
Negative numbers shouldn't make it go weird any more.
Automatic detection of input formats should be more reliable.
2019-01-17 13:53:42 +00:00
j433866
439654ed7f Add tests for new co-ordinate conversion module.
Removed To/From geohash tests
2019-01-17 13:49:36 +00:00
j433866
69797e58cb Add better error handling.
Also now doesn't do anything if there's no input
2019-01-16 16:57:58 +00:00
Matt
d1961ca3fa Marginally reduced size of libyara-wasm 2019-01-16 01:15:51 +00:00
Matt
2e9b1e079c Merge remote-tracking branch 'upstream/master' into features/yara 2019-01-15 23:46:49 +00:00
Matt
3dfaaf4c25 Update libyara for test pass 2019-01-15 23:45:40 +00:00
Matt
fcc39a0397 Added File upload support to textarea 2019-01-15 23:42:05 +00:00
Matt
0602f457ce Added initial tests & counts support 2019-01-15 16:24:29 +00:00
j433866
d00b0f4c0e Basically rewrote the whole thing using the new geodesy module 2019-01-15 15:55:49 +00:00
j433866
5e68959c03 Catch when OS grid references aren't calculated 2019-01-15 10:25:49 +00:00
j433866
ad4451a757 Rewrite MGRS to use new Geodesy module.
Added Ordnance Survey grid reference support
2019-01-15 10:13:11 +00:00
Callum Fraser
4d8127a7d9 Modified description of ToBase64 operation
Addresses #472
2019-01-14 22:25:49 +00:00
n1474335
cd2c8078c8 Added ELF extractor. You can now specific which categories to search for in file type operations. 2019-01-14 18:55:10 +00:00
j433866
ee360521bb Remove MGRS npm module 2019-01-14 16:41:06 +00:00
j433866
04b0b8c723 Tidy up code 2019-01-14 14:58:41 +00:00
j433866
b3ac8d0835 Removed some debug logging 2019-01-14 13:49:49 +00:00
j433866
1a88a0164c Fix delimiter breaking Geohash detection 2019-01-14 13:00:14 +00:00
j433866
8b77ad7748 Stop delimiters breaking MGRS conversion 2019-01-14 12:49:28 +00:00
j433866
8d1f668fc5 Remove old Geohash modules 2019-01-14 11:56:27 +00:00
j433866
68fbbb64db Add new Convert co-ordinate format module.
Also added autodetect of co-ordinate format / delimiter
2019-01-14 11:49:57 +00:00
Matt
8bba4b2973 More speedrun stats (literally 10x faster) 2019-01-12 00:20:25 +00:00
n1474335
2307325af8 Added Zlib extraction 2019-01-11 17:58:25 +00:00
n1474335
4e57b4be88 Completed GZIP extraction 2019-01-11 17:44:13 +00:00
j433866
abdd70c6fa Add ConvertCoordinates to lib folder 2019-01-11 11:59:13 +00:00
n1474335
c077b22410 Stream.readBits() method implemented. Unfinished. 2019-01-10 17:30:52 +00:00
n1474335
9787ab04cd 8.22.0 2019-01-10 15:44:02 +00:00
n1474335
79d3c90026 Merge branch 'j433866-subsection' 2019-01-10 15:43:05 +00:00
n1474335
c2068b343b Tidied up and added global matching to Subsection operation 2019-01-10 15:42:48 +00:00
j433866
9e63e40dab Add new MGRS module and update webpack-dev-server 2019-01-10 15:24:29 +00:00
n1474335
6424839731 Merge branch 'subsection' of https://github.com/j433866/CyberChef into j433866-subsection 2019-01-10 15:11:34 +00:00
n1474335
863a525625 8.21.0 2019-01-10 15:02:26 +00:00
n1474335
f82a727e24 Merge branch 'masq-insense' 2019-01-10 15:01:22 +00:00
n1474335
995fcab071 Tidied up Case Insensitive Regex ops 2019-01-10 15:01:01 +00:00
n1474335
c5270d75a1 Merge branch 'insense' of https://github.com/masq/CyberChef into masq-insense 2019-01-10 14:53:21 +00:00
n1474335
324c409ff1 8.20.0 2019-01-09 16:38:53 +00:00
n1474335
1db8e6dddc Merge branch 'klaxon1-feature/lorem-ipsum-generator' 2019-01-09 16:38:46 +00:00
n1474335
c49a770c59 Tidied up Lorem Ipsum op 2019-01-09 16:36:34 +00:00
Matt
dd9ba4d250 Fixed problems flagged by n's review 2019-01-09 15:28:50 +00:00
n1474335
0e601d5b5f Merge branch 'feature/lorem-ipsum-generator' of https://github.com/klaxon1/CyberChef into klaxon1-feature/lorem-ipsum-generator 2019-01-09 14:50:48 +00:00
Matt
ebb632e888 Added metadata, string identifiers and operation args 2019-01-09 14:29:14 +00:00
Matt
4db2335107 Speedrunning strats (increased speed on big files) 2019-01-09 11:45:11 +00:00
Matt
26a2fb6662 Increased size of rule inp & expanded memory for wasm 2019-01-09 09:56:55 +00:00
Matt
4c1521a98e No data matches & warnings support 2019-01-08 23:26:14 +00:00
Matt
df8abb099c Added code argtype 2019-01-08 22:23:14 +00:00
n1474335
fe1332f18e 8.19.7 2019-01-08 18:29:14 +00:00
n1474335
cb9ab7a2c9 Fixed 'Maximise output' button functionality 2019-01-08 18:29:07 +00:00
n1474335
3a6b2875d5 8.19.6 2019-01-08 17:51:47 +00:00
n1474335
766de7e6fa Fixed bug in 'Regular expression' operation when highlighting lookaheads 2019-01-08 17:51:43 +00:00
Matt
13439e100e Merge remote-tracking branch 'upstream/master' into features/yara 2019-01-08 16:28:14 +00:00
Matt
5ac469b174 Added yara rule support 2019-01-08 16:19:58 +00:00
j433866
8ac5b48493 Update operation description 2019-01-08 11:51:33 +00:00
j433866
1a827ef44f Add Subsection to Flow Control category 2019-01-08 11:17:06 +00:00
j433866
0f0e346a02 Add new Subsection operation 2019-01-08 11:12:02 +00:00
n1474335
2a6db47aeb Began implementing GZIP/DEFLATE extraction. Unfinished. 2019-01-04 18:12:49 +00:00
n1474335
19b7957523 Added RTF extractor 2019-01-04 14:57:31 +00:00
n1474335
0d2cb02f97 Fixed FLV previous tag size error 2019-01-04 11:49:12 +00:00
n1474335
7d8d80ca2c Added extractor for MS Office 2007+ files 2019-01-03 19:01:12 +00:00
n1474335
0449c46b38 Added FLV extractor. 2019-01-03 18:40:22 +00:00
n1474335
cd0c86e0d6 File scan now uses bytesMatch() instead of signatureMatches(), reducing call stack size 2019-01-03 13:03:41 +00:00
n1474335
a56f92cdee Significantly improved performance when scanning for embedded files by implementing a fastcheck algorithm. 2019-01-02 17:50:47 +00:00
n1474335
c82971f8db 8.19.5 2019-01-01 20:49:24 +00:00
n1474335
cb2c376c63 Increasing Node memory limit from 1G to 2G 2019-01-01 20:27:38 +00:00
n1474335
bc00fa0694 8.19.4 2019-01-01 20:15:29 +00:00
n1474335
c86007da71 Removed increase-memory-limit plugin in favour of NODE_OPTIONS environment variable. 2019-01-01 20:15:16 +00:00
n1474335
1bf513ca74 8.19.3 2019-01-01 19:56:20 +00:00
n1474335
29411c903f Added increase-memory-limit plugin to TravisCI build process to reduce 'JavaScript heap out of memory' errors. 2019-01-01 19:56:12 +00:00
n1474335
017dde364c 8.19.2 2019-01-01 19:22:10 +00:00
n1474335
c123d7370a Merge branch 'edwardwall-patch-2' 2019-01-01 19:21:11 +00:00
n1474335
76f1e5e8f3 Merge branch 'patch-2' of https://github.com/edwardwall/CyberChef into edwardwall-patch-2 2019-01-01 19:20:07 +00:00
n1474335
4e466c7886 8.19.1 2019-01-01 19:19:16 +00:00
n1474335
d469fb9c58 Updated dependencies 2019-01-01 19:19:07 +00:00
n1474335
4c285bce57 Refactored scanning for file types to be more than twice as fast. 2019-01-01 15:12:01 +00:00
Edward Wall
050ab03448 Simplify to improve readability 2018-12-30 17:06:48 +00:00
Edward Wall
40acf751a8 Update to understand Generalized / UTC Time
Future proofing for when certificates with dates after 2049 begin being issued.
These certificates' dates will be in Generalized Time not UTC Time as per RFC 5280
2018-12-30 16:46:18 +00:00
Spencer Walden
126ad585c0 Registers tests for 'To/From Case Insensitive Regex' operations 2018-12-30 03:26:44 -08:00
Spencer Walden
1d04b649e0 Adds 'To/From Case Insensitive Regex' operations under 'Utils' 2018-12-30 03:26:44 -08:00
Spencer Walden
b750006cf0 Adds tests for 'To/From Case Insensitive Regex' operations 2018-12-30 03:26:44 -08:00
Spencer Walden
3c16b839b6 Adds 'From Case Insensitive Regex' operation 2018-12-30 03:26:44 -08:00
Spencer Walden
32aea6b86c Adds 'To Case Insensitive Regex' operation 2018-12-30 03:26:44 -08:00
Edward Wall
688c2d0df5 Update ParseX509Certificate.mjs 2018-12-30 03:15:07 +00:00
n1474335
ede75530d0 Added PNG and BMP extractors 2018-12-30 02:21:45 +00:00
n1474335
3ae225ac59 Untar operation now uses lib/Stream library 2018-12-30 01:36:58 +00:00
n1474335
fd07b89028 Merge branch 'master' into feature-extract-files 2018-12-30 01:16:46 +00:00
n1474335
0cea56dc62 8.19.0 2018-12-30 01:07:54 +00:00
n1474335
bb44268c30 Merge branch 'feature-browser-testsuite' 2018-12-30 01:07:42 +00:00
n1474335
19b3dcf1c2 Updated CHANGELOG 2018-12-30 01:07:26 +00:00
n1474335
71e0a4e0ce Increased UI test timeouts 2018-12-30 00:47:10 +00:00
n1474335
7f2e879e24 Added explicit bake after input added in test suite. 2018-12-30 00:37:44 +00:00
n1474335
840e44deac Tidied up UI tests 2018-12-30 00:26:28 +00:00
n1474335
f7707faece Added Chrome to TravisCI config 2018-12-30 00:02:41 +00:00
n1474335
b631e3fef6 Added nightwatch tests to TravisCI build process for prod and inline versions. 2018-12-29 23:46:13 +00:00
n1474335
b0fb9db4b8 Added nightwatch.js test suite for confirming that the app loads correctly and can run operations from each module. Currently only support the latest version of Chrome. 2018-12-29 02:58:05 +00:00
n1474335
c7e9115994 Restructured tests directory 2018-12-28 21:49:40 +00:00
Klaxon
f2d115ee4d add lorem ipsum generator 2018-12-29 00:44:59 +10:00
n1474335
0198f05112 Added and improved file signatures. 2018-12-27 00:03:41 +00:00
n1474335
729307336e Converted all previous file signatures to the new format. 2018-12-26 23:19:46 +00:00
n1474335
f4f9b5c91c Added 'isImage' and 'isType' functions 2018-12-26 18:40:27 +00:00
n1474335
f355fe3447 Merge branch 'feature-extract-files' of github.com:gchq/CyberChef into feature-extract-files 2018-12-26 18:01:55 +00:00
n1474335
321718d43a Merge branch 'master' into feature-extract-files 2018-12-26 16:57:34 +00:00
n1474335
a1b161493c 8.18.1 2018-12-26 16:50:36 +00:00
n1474335
5acee80463 'editableOption's are now full width. 'editableOptionShort' type added to replace the old style. 2018-12-26 16:50:32 +00:00
n1474335
e6932401ad 8.18.0 2018-12-26 16:35:34 +00:00
n1474335
7a4eff0f5c Merge branch 'artemisbot-features/colour-channel' 2018-12-26 16:33:26 +00:00
n1474335
8b533e9893 Tidied up 'Split Colour Channels' operation and added 'Multimedia' category 2018-12-26 16:33:10 +00:00
n1474335
02b92c7977 Merge branch 'features/colour-channel' of https://github.com/artemisbot/CyberChef into artemisbot-features/colour-channel 2018-12-26 16:10:44 +00:00
Matt C
454ef0076b Disabled tests 2018-12-21 17:19:33 +00:00
Matt C
18693d2471 Add tests, however non-functional due to lack of File in Node
Also add jimp to package.json
2018-12-21 17:17:11 +00:00
Matt C
5a9583c970 Add to categories 2018-12-21 17:15:30 +00:00
Matt C
0046f7e3d7 Added colour channel splitting support 2018-12-21 17:08:09 +00:00
n1474335
8d3836cb16 Added support for a number of further file types and file detection methods. 2018-12-21 12:48:08 +00:00
n1474335
9829491c4c Merge branch 'master' into feature-extract-files 2018-12-20 12:28:23 +00:00
n1474335
e6fb0be1d0 Refactored file type detection engine 2018-12-18 17:44:42 +00:00
n1474335
d02124550b Merge branch 'master' into feature-extract-files 2018-12-18 15:45:53 +00:00
n1474335
6aa9d2b492 Added 'Extract Files' operation and 'Forensics' category. 2018-12-14 16:43:03 +00:00
155 changed files with 8699 additions and 1745 deletions

View File

@@ -9,6 +9,6 @@ trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[{package.json,.travis.yml}]
[{package.json,.travis.yml,nightwatch.json}]
indent_style = space
indent_size = 2

View File

@@ -1,10 +1,12 @@
{
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 9,
"ecmaFeatures": {
"impliedStrict": true
},
"sourceType": "module"
"sourceType": "module",
"allowImportExportEverywhere": true
},
"env": {
"browser": true,
@@ -87,10 +89,20 @@
"no-var": "error",
"prefer-const": "error"
},
"overrides": [
{
"files": "tests/**/*",
"rules": {
"no-unused-expressions": "off",
"no-console": "off"
}
}
],
"globals": {
"$": false,
"jQuery": false,
"log": false,
"app": false,
"COMPILE_TIME": false,
"COMPILE_MSG": false,

1
.gitignore vendored
View File

@@ -9,4 +9,5 @@ docs/*
src/core/config/modules/*
src/core/config/OperationConfig.json
src/core/operations/index.mjs
tests/browser/output/*

View File

@@ -1,15 +1,19 @@
language: node_js
node_js:
- node
addons:
chrome: stable
install: npm install
before_script:
- npm install -g grunt
- export NODE_OPTIONS=--max_old_space_size=2048
script:
- grunt lint
- grunt test
- grunt docs
- grunt node
- grunt prod --msg="$COMPILE_MSG"
- xvfb-run --server-args="-screen 0 1200x800x24" grunt testui
before_deploy:
- grunt exec:sitemap
- grunt copy:ghPages

View File

@@ -2,6 +2,30 @@
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).
### [8.24.0] - 2019-02-08
- 'DNS over HTTPS' operation added [@h345983745] | [#489]
### [8.23.1] - 2019-01-18
- 'Convert co-ordinate format' operation added [@j433866] | [#476]
### [8.23.0] - 2019-01-18
- 'YARA Rules' operation added [@artemisbot] | [#468]
### [8.22.0] - 2019-01-10
- 'Subsection' operation added [@j433866] | [#467]
### [8.21.0] - 2019-01-10
- 'To Case Insensitive Regex' and 'From Case Insensitive Regex' operations added [@masq] | [#461]
### [8.20.0] - 2019-01-09
- 'Generate Lorem Ipsum' operation added [@klaxon1] | [#455]
### [8.19.0] - 2018-12-30
- UI test suite added to confirm that the app loads correctly in a reasonable time and that various operations from each module can be run [@n1474335] | [#458]
### [8.18.0] - 2018-12-26
- 'Split Colour Channels' operation added [@artemisbot] | [#449]
### [8.17.0] - 2018-12-25
- 'Generate QR Code' and 'Parse QR Code' operations added [@j433866] | [#448]
@@ -82,6 +106,14 @@ All major and minor version changes will be documented in this file. Details of
[8.24.0]: https://github.com/gchq/CyberChef/releases/tag/v8.24.0
[8.23.1]: https://github.com/gchq/CyberChef/releases/tag/v8.23.1
[8.23.0]: https://github.com/gchq/CyberChef/releases/tag/v8.23.0
[8.22.0]: https://github.com/gchq/CyberChef/releases/tag/v8.22.0
[8.21.0]: https://github.com/gchq/CyberChef/releases/tag/v8.21.0
[8.20.0]: https://github.com/gchq/CyberChef/releases/tag/v8.20.0
[8.19.0]: https://github.com/gchq/CyberChef/releases/tag/v8.19.0
[8.18.0]: https://github.com/gchq/CyberChef/releases/tag/v8.18.0
[8.17.0]: https://github.com/gchq/CyberChef/releases/tag/v8.17.0
[8.16.0]: https://github.com/gchq/CyberChef/releases/tag/v8.16.0
[8.15.0]: https://github.com/gchq/CyberChef/releases/tag/v8.15.0
@@ -109,6 +141,7 @@ All major and minor version changes will be documented in this file. Details of
[@d98762625]: https://github.com/d98762625
[@j433866]: https://github.com/j433866
[@GCHQ77703]: https://github.com/GCHQ77703
[@h345983745]: https://github.com/h345983745
[@artemisbot]: https://github.com/artemisbot
[@picapi]: https://github.com/picapi
[@Dachande663]: https://github.com/Dachande663
@@ -122,6 +155,7 @@ All major and minor version changes will be documented in this file. Details of
[@tcode2k16]: https://github.com/tcode2k16
[@Cynser]: https://github.com/Cynser
[@anthony-arnold]: https://github.com/anthony-arnold
[@masq]: https://github.com/masq
[#95]: https://github.com/gchq/CyberChef/pull/299
[#173]: https://github.com/gchq/CyberChef/pull/173
@@ -150,3 +184,11 @@ All major and minor version changes will be documented in this file. Details of
[#443]: https://github.com/gchq/CyberChef/pull/443
[#446]: https://github.com/gchq/CyberChef/pull/446
[#448]: https://github.com/gchq/CyberChef/pull/448
[#449]: https://github.com/gchq/CyberChef/pull/449
[#455]: https://github.com/gchq/CyberChef/pull/455
[#458]: https://github.com/gchq/CyberChef/pull/458
[#461]: https://github.com/gchq/CyberChef/pull/461
[#467]: https://github.com/gchq/CyberChef/pull/467
[#468]: https://github.com/gchq/CyberChef/pull/468
[#476]: https://github.com/gchq/CyberChef/pull/476
[#489]: https://github.com/gchq/CyberChef/pull/489

View File

@@ -30,8 +30,12 @@ module.exports = function (grunt) {
["clean:node", "clean:config", "exec:generateConfig", "webpack:node", "chmod:build"]);
grunt.registerTask("test",
"A task which runs all the tests in test/tests.",
["exec:generateConfig", "exec:tests"]);
"A task which runs all the operation tests in the tests directory.",
["exec:generateConfig", "exec:opTests"]);
grunt.registerTask("testui",
"A task which runs all the UI tests in the tests directory. The prod task must already have been run.",
["connect:prod", "exec:browserTests"]);
grunt.registerTask("docs",
"Compiles documentation in the /docs directory.",
@@ -67,6 +71,7 @@ module.exports = function (grunt) {
grunt.loadNpmTasks("grunt-exec");
grunt.loadNpmTasks("grunt-accessibility");
grunt.loadNpmTasks("grunt-concurrent");
grunt.loadNpmTasks("grunt-contrib-connect");
// Project configuration
@@ -144,11 +149,11 @@ module.exports = function (grunt) {
options: {
configFile: "./.eslintrc.json"
},
configs: ["*.js"],
configs: ["*.{js,mjs}"],
core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"],
web: ["src/web/**/*.{js,mjs}"],
node: ["src/node/**/*.{js,mjs}"],
tests: ["test/**/*.{js,mjs}"],
tests: ["tests/**/*.{js,mjs}"],
},
jsdoc: {
options: {
@@ -189,7 +194,8 @@ module.exports = function (grunt) {
sitemap: "./src/web/static/sitemap.js"
}, moduleEntryPoints),
output: {
path: __dirname + "/build/prod"
path: __dirname + "/build/prod",
globalObject: "this"
},
resolve: {
alias: {
@@ -246,19 +252,6 @@ module.exports = function (grunt) {
}),
]
},
tests: {
mode: "development",
target: "node",
entry: "./test/index.mjs",
externals: [NodeExternals()],
output: {
filename: "index.js",
path: __dirname + "/build/test"
},
plugins: [
new webpack.DefinePlugin(BUILD_CONSTANTS)
]
},
node: {
mode: "production",
target: "node",
@@ -307,6 +300,9 @@ module.exports = function (grunt) {
"./config/modules/OpModules": "./config/modules/Default"
}
},
output: {
globalObject: "this",
},
plugins: [
new webpack.DefinePlugin(BUILD_CONSTANTS),
new HtmlWebpackPlugin({
@@ -320,6 +316,14 @@ module.exports = function (grunt) {
}
}
},
connect: {
prod: {
options: {
port: 8000,
base: "build/prod/"
}
}
},
copy: {
ghPages: {
options: {
@@ -391,16 +395,17 @@ module.exports = function (grunt) {
generateConfig: {
command: [
"echo '\n--- Regenerating config files. ---'",
"mkdir -p src/core/config/modules",
"echo 'export default {};\n' > src/core/config/modules/OpModules.mjs",
"echo '[]\n' > src/core/config/OperationConfig.json",
"echo [] > src/core/config/OperationConfig.json",
"node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateOpsIndex.mjs",
"node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateConfig.mjs",
"echo '--- Config scripts finished. ---\n'"
].join(";")
},
tests: {
command: "node --experimental-modules --no-warnings --no-deprecation test/index.mjs"
opTests: {
command: "node --experimental-modules --no-warnings --no-deprecation tests/operations/index.mjs"
},
browserTests: {
command: "./node_modules/.bin/nightwatch --env prod,inline"
}
},
});

View File

@@ -15,6 +15,7 @@ module.exports = function(api) {
}]
],
"plugins": [
"babel-plugin-syntax-dynamic-import",
["babel-plugin-transform-builtin-extend", {
"globals": ["Error"]
}]

34
nightwatch.json Normal file
View File

@@ -0,0 +1,34 @@
{
"src_folders": ["tests/browser"],
"output_folder": "tests/browser/output",
"test_settings": {
"default": {
"launch_url": "http://localhost:8080",
"webdriver": {
"start_process": true,
"server_path": "./node_modules/.bin/chromedriver",
"port": 9515,
"log_path": false
},
"desiredCapabilities": {
"browserName": "chrome"
}
},
"dev": {
"launch_url": "http://localhost:8080"
},
"prod": {
"launch_url": "http://localhost:8000/index.html"
},
"inline": {
"launch_url": "http://localhost:8000/cyberchef.htm"
}
}
}

2462
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "cyberchef",
"version": "8.17.2",
"version": "8.26.2",
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
"author": "n1474335 <n1474335@gmail.com>",
"homepage": "https://gchq.github.io/CyberChef",
@@ -30,22 +30,25 @@
"main": "build/node/CyberChef.js",
"bugs": "https://github.com/gchq/CyberChef/issues",
"devDependencies": {
"@babel/core": "^7.1.5",
"@babel/preset-env": "^7.1.5",
"autoprefixer": "^9.3.1",
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"autoprefixer": "^9.4.3",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.4",
"bootstrap": "^4.1.3",
"colors": "^1.3.2",
"css-loader": "^1.0.1",
"eslint": "^5.8.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"bootstrap": "^4.2.1",
"chromedriver": "^2.45.0",
"colors": "^1.3.3",
"css-loader": "^2.1.0",
"eslint": "^5.12.1",
"exports-loader": "^0.7.0",
"extract-text-webpack-plugin": "^4.0.0-alpha0",
"file-loader": "^2.0.0",
"file-loader": "^3.0.1",
"grunt": "^1.0.3",
"grunt-accessibility": "~6.0.0",
"grunt-chmod": "~1.1.1",
"grunt-concurrent": "^2.3.1",
"grunt-contrib-clean": "~2.0.0",
"grunt-contrib-connect": "^2.0.0",
"grunt-contrib-copy": "~1.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-eslint": "^21.0.0",
@@ -56,7 +59,9 @@
"imports-loader": "^0.8.0",
"ink-docstrap": "^1.3.2",
"jsdoc-babel": "^0.5.0",
"node-sass": "^4.10.0",
"mini-css-extract-plugin": "^0.5.0",
"nightwatch": "^1.0.18",
"node-sass": "^4.11.0",
"postcss-css-variables": "^0.11.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
@@ -66,9 +71,9 @@
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"web-resource-inliner": "^4.2.1",
"webpack": "^4.25.1",
"webpack": "^4.28.3",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-dev-server": "^3.1.10",
"webpack-dev-server": "^3.1.14",
"webpack-node-externals": "^1.7.2",
"worker-loader": "^2.0.0"
},
@@ -77,10 +82,10 @@
"babel-plugin-transform-builtin-extend": "1.1.2",
"babel-polyfill": "^6.26.0",
"bcryptjs": "^2.4.3",
"bignumber.js": "^8.0.1",
"bignumber.js": "^8.0.2",
"bootstrap-colorpicker": "^2.5.3",
"bootstrap-material-design": "^4.1.1",
"bson": "^3.0.2",
"bson": "^4.0.1",
"chi-squared": "^1.1.0",
"crypto-api": "^0.8.3",
"crypto-js": "^3.1.9-1",
@@ -91,48 +96,51 @@
"esmangle": "^1.0.1",
"esprima": "^4.0.1",
"exif-parser": "^0.1.12",
"file-saver": "^2.0.0-rc.4",
"file-saver": "^2.0.0",
"geodesy": "^1.1.3",
"highlight.js": "^9.13.1",
"jimp": "^0.6.0",
"jquery": "^3.3.1",
"js-crc": "^0.2.0",
"js-sha3": "^0.8.0",
"jsesc": "^2.5.1",
"jsesc": "^2.5.2",
"jsonpath": "^1.0.0",
"jsonwebtoken": "^8.3.0",
"jsonwebtoken": "^8.4.0",
"jsqr": "^1.1.1",
"jsrsasign": "8.0.12",
"kbpgp": "^2.0.82",
"libyara-wasm": "0.0.12",
"lodash": "^4.17.11",
"loglevel": "^1.6.1",
"loglevel-message-prefix": "^3.0.0",
"moment": "^2.22.2",
"moment": "^2.23.0",
"moment-timezone": "^0.5.23",
"ngeohash": "^0.6.0",
"ngeohash": "^0.6.3",
"node-forge": "^0.7.6",
"node-md6": "^0.1.0",
"notepack.io": "^2.1.3",
"notepack.io": "^2.2.0",
"nwmatcher": "^1.4.4",
"otp": "^0.1.3",
"popper.js": "^1.14.4",
"popper.js": "^1.14.6",
"qr-image": "^3.2.0",
"scryptsy": "^2.0.0",
"snackbarjs": "^1.1.0",
"sortablejs": "^1.7.0",
"split.js": "^1.5.9",
"sortablejs": "^1.8.0-rc1",
"split.js": "^1.5.10",
"ssdeep.js": "0.0.2",
"ua-parser-js": "^0.7.19",
"utf8": "^3.0.0",
"vkbeautify": "^0.99.3",
"xmldom": "^0.1.27",
"xpath": "0.0.27",
"xregexp": "^4.2.0",
"xregexp": "^4.2.4",
"zlibjs": "^0.3.1"
},
"scripts": {
"start": "grunt dev",
"build": "grunt prod",
"test": "grunt test",
"testui": "grunt testui",
"docs": "grunt docs",
"lint": "grunt lint",
"newop": "node --experimental-modules src/core/config/scripts/newOperation.mjs"

View File

@@ -157,9 +157,9 @@ class Chef {
* @param {number} pos.end - The end offset.
* @returns {Object}
*/
calculateHighlights(recipeConfig, direction, pos) {
async calculateHighlights(recipeConfig, direction, pos) {
const recipe = new Recipe(recipeConfig);
const highlights = recipe.generateHighlightList();
const highlights = await recipe.generateHighlightList();
if (!highlights) return false;

View File

@@ -157,8 +157,8 @@ async function getDishAs(data) {
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/
function calculateHighlights(recipeConfig, direction, pos) {
pos = self.chef.calculateHighlights(recipeConfig, direction, pos);
async function calculateHighlights(recipeConfig, direction, pos) {
pos = await self.chef.calculateHighlights(recipeConfig, direction, pos);
self.postMessage({
action: "highlightsCalculated",

View File

@@ -168,7 +168,7 @@ class Dish {
this.value = Array.prototype.slice.call(new Uint8Array(this.value));
break;
case Dish.BIG_NUMBER:
this.value = this.value instanceof BigNumber ? Utils.strToByteArray(this.value.toFixed()) : [];
this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : [];
break;
case Dish.JSON:
this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : [];
@@ -265,7 +265,7 @@ class Dish {
case Dish.ARRAY_BUFFER:
return this.value instanceof ArrayBuffer;
case Dish.BIG_NUMBER:
return this.value instanceof BigNumber;
return BigNumber.isBigNumber(this.value);
case Dish.JSON:
// All values can be serialised in some manner, so we return true in all cases
return true;

View File

@@ -23,9 +23,13 @@ class Ingredient {
this._value = null;
this.disabled = false;
this.hint = "";
this.rows = 0;
this.toggleValues = [];
this.target = null;
this.defaultIndex = 0;
this.min = null;
this.max = null;
this.step = 1;
if (ingredientConfig) {
this._parseConfig(ingredientConfig);
@@ -45,9 +49,13 @@ class Ingredient {
this.defaultValue = ingredientConfig.value;
this.disabled = !!ingredientConfig.disabled;
this.hint = ingredientConfig.hint || false;
this.rows = ingredientConfig.rows || false;
this.toggleValues = ingredientConfig.toggleValues;
this.target = typeof ingredientConfig.target !== "undefined" ? ingredientConfig.target : null;
this.defaultIndex = typeof ingredientConfig.defaultIndex !== "undefined" ? ingredientConfig.defaultIndex : 0;
this.min = ingredientConfig.min;
this.max = ingredientConfig.max;
this.step = ingredientConfig.step;
}
@@ -95,6 +103,7 @@ class Ingredient {
case "binaryString":
case "binaryShortString":
case "editableOption":
case "editableOptionShort":
return Utils.parseEscapedChars(data);
case "byteArray":
if (typeof data == "string") {

View File

@@ -23,6 +23,7 @@ class Operation {
this._breakpoint = false;
this._disabled = false;
this._flowControl = false;
this._manualBake = false;
this._ingList = [];
// Public fields
@@ -179,9 +180,13 @@ class Operation {
if (ing.toggleValues) conf.toggleValues = ing.toggleValues;
if (ing.hint) conf.hint = ing.hint;
if (ing.rows) conf.rows = ing.rows;
if (ing.disabled) conf.disabled = ing.disabled;
if (ing.target) conf.target = ing.target;
if (ing.defaultIndex) conf.defaultIndex = ing.defaultIndex;
if (typeof ing.min === "number") conf.min = ing.min;
if (typeof ing.max === "number") conf.max = ing.max;
if (ing.step) conf.step = ing.step;
return conf;
});
}
@@ -281,6 +286,7 @@ class Operation {
return this._flowControl;
}
/**
* Set whether this Operation is a flowcontrol op.
*
@@ -290,6 +296,26 @@ class Operation {
this._flowControl = !!value;
}
/**
* Returns true if this Operation should not trigger AutoBake.
*
* @returns {boolean}
*/
get manualBake() {
return this._manualBake;
}
/**
* Set whether this Operation should trigger AutoBake.
*
* @param {boolean} value
*/
set manualBake(value) {
this._manualBake = !!value;
}
}
export default Operation;

View File

@@ -4,12 +4,15 @@
* @license Apache-2.0
*/
import OpModules from "./config/modules/OpModules";
import OperationConfig from "./config/OperationConfig.json";
import OperationError from "./errors/OperationError";
import Operation from "./Operation";
import DishError from "./errors/DishError";
import log from "loglevel";
// Cache container for modules
let modules = null;
/**
* The Recipe controls a list of Operations and the Dish they operate on.
*/
@@ -36,16 +39,43 @@ class Recipe {
* @param {Object} recipeConfig
*/
_parseConfig(recipeConfig) {
for (let c = 0; c < recipeConfig.length; c++) {
const operationName = recipeConfig[c].op;
const opConf = OperationConfig[operationName];
const opObj = OpModules[opConf.module][operationName];
const operation = new opObj();
operation.ingValues = recipeConfig[c].args;
operation.breakpoint = recipeConfig[c].breakpoint;
operation.disabled = recipeConfig[c].disabled;
this.addOperation(operation);
recipeConfig.forEach(c => {
this.opList.push({
name: c.op,
module: OperationConfig[c.op].module,
ingValues: c.args,
breakpoint: c.breakpoint,
disabled: c.disabled,
});
});
}
/**
* Populate elements of opList with operation instances.
* Dynamic import here removes top-level cyclic dependency issue.
*
* @private
*/
async _hydrateOpList() {
if (!modules) {
// Using Webpack Magic Comments to force the dynamic import to be included in the main chunk
// https://webpack.js.org/api/module-methods/
modules = await import(/* webpackMode: "eager" */ "./config/modules/OpModules");
modules = modules.default;
}
this.opList = this.opList.map(o => {
if (o instanceof Operation) {
return o;
} else {
const op = new modules[o.module][o.name]();
op.ingValues = o.ingValues;
op.breakpoint = o.breakpoint;
op.disabled = o.disabled;
return op;
}
});
}
@@ -55,7 +85,10 @@ class Recipe {
* @returns {Object[]}
*/
get config() {
return this.opList.map(op => op.config);
return this.opList.map(op => ({
op: op.name,
args: op.ingValues,
}));
}
@@ -75,7 +108,19 @@ class Recipe {
* @param {Operation[]} operations
*/
addOperations(operations) {
this.opList = this.opList.concat(operations);
operations.forEach(o => {
if (o instanceof Operation) {
this.opList.push(o);
} else {
this.opList.push({
name: o.name,
module: o.module,
ingValues: o.args,
breakpoint: o.breakpoint,
disabled: o.disabled,
});
}
});
}
@@ -108,7 +153,7 @@ class Recipe {
/**
* Returns true if there is an Flow Control Operation in this Recipe.
* Returns true if there is a Flow Control Operation in this Recipe.
*
* @returns {boolean}
*/
@@ -137,6 +182,8 @@ class Recipe {
if (startFrom === 0) this.lastRunOp = null;
await this._hydrateOpList();
log.debug(`[*] Executing recipe of ${this.opList.length} operations, starting at ${startFrom}`);
for (let i = startFrom; i < this.opList.length; i++) {
@@ -254,7 +301,8 @@ class Recipe {
* @returns {function} highlights[].b
* @returns {Object[]} highlights[].args
*/
generateHighlightList() {
async generateHighlightList() {
await this._hydrateOpList();
const highlights = [];
for (let i = 0; i < this.opList.length; i++) {

View File

@@ -5,7 +5,7 @@
*/
import utf8 from "utf8";
import {fromBase64} from "./lib/Base64";
import {fromBase64, toBase64} from "./lib/Base64";
import {fromHex} from "./lib/Hex";
import {fromDecimal} from "./lib/Decimal";
import {fromBinary} from "./lib/Binary";
@@ -817,12 +817,24 @@ class Utils {
return html;
};
const formatContent = function (buff, type) {
if (type.startsWith("image")) {
let dataURI = "data:";
dataURI += type + ";";
dataURI += "base64," + toBase64(buff);
return "<img style='max-width: 100%;' src='" + dataURI + "'>";
} else {
return `<pre>${Utils.escapeHtml(Utils.arrayBufferToStr(buff.buffer))}</pre>`;
}
};
const formatFile = async function(file, i) {
const buff = await Utils.readFile(file);
const blob = new Blob(
[buff],
{type: "octet/stream"}
{type: file.type || "octet/stream"}
);
const blobURL = URL.createObjectURL(blob);
const html = `<div class='card' style='white-space: normal;'>
<div class='card-header' id='heading${i}'>
@@ -837,16 +849,25 @@ class Utils {
<span class='float-right' style="margin-top: -3px">
${file.size.toLocaleString()} bytes
<a title="Download ${Utils.escapeHtml(file.name)}"
href='${URL.createObjectURL(blob)}'
download='${Utils.escapeHtml(file.name)}'>
href="${blobURL}"
download="${Utils.escapeHtml(file.name)}"
data-toggle="tooltip">
<i class="material-icons" style="vertical-align: bottom">save</i>
</a>
<a title="Move to input"
href="#"
blob-url="${blobURL}"
file-name="${Utils.escapeHtml(file.name)}"
class="extract-file"
data-toggle="tooltip">
<i class="material-icons" style="vertical-align: bottom">open_in_browser</i>
</a>
</span>
</h6>
</div>
<div id='collapse${i}' class='collapse' aria-labelledby='heading${i}' data-parent="#files">
<div class='card-body'>
<pre>${Utils.escapeHtml(Utils.arrayBufferToStr(buff.buffer))}</pre>
${formatContent(buff, file.type)}
</div>
</div>
</div>`;
@@ -1152,6 +1173,21 @@ String.prototype.count = function(chr) {
};
/**
* Wrapper for self.sendStatusMessage to handle different environments.
*
* @param {string} msg
*/
export function sendStatusMessage(msg) {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage(msg);
else if (ENVIRONMENT_IS_WEB())
app.alert(msg, 10000);
else if (ENVIRONMENT_IS_NODE())
log.debug(msg);
}
/*
* Polyfills
*/

View File

@@ -155,6 +155,7 @@
"name": "Networking",
"ops": [
"HTTP request",
"DNS over HTTPS",
"Strip HTTP headers",
"Dechunk HTTP response",
"Parse User Agent",
@@ -189,6 +190,8 @@
"Remove null bytes",
"To Upper case",
"To Lower case",
"To Case Insensitive Regex",
"From Case Insensitive Regex",
"Add line numbers",
"Remove line numbers",
"To Table",
@@ -213,6 +216,7 @@
"Convert mass",
"Convert speed",
"Convert data units",
"Convert co-ordinate format",
"Parse UNIX file permissions",
"Swap endianness",
"Parse colour code",
@@ -250,7 +254,8 @@
"XPath expression",
"JPath expression",
"CSS selector",
"Extract EXIF"
"Extract EXIF",
"Extract Files"
]
},
{
@@ -304,9 +309,7 @@
"Adler-32 Checksum",
"CRC-16 Checksum",
"CRC-32 Checksum",
"TCP/IP Checksum",
"To Geohash",
"From Geohash"
"TCP/IP Checksum"
]
},
{
@@ -346,10 +349,32 @@
"ops": [
"Detect File Type",
"Scan for Embedded Files",
"Extract Files",
"Remove EXIF",
"Extract EXIF"
]
},
{
"name": "Multimedia",
"ops": [
"Render Image",
"Play Media",
"Remove EXIF",
"Extract EXIF",
"Render Image",
"Play Media"
"Split Colour Channels",
"Rotate Image",
"Resize Image",
"Blur Image",
"Dither Image",
"Invert Image",
"Flip Image",
"Crop Image",
"Image Brightness / Contrast",
"Image Opacity",
"Image Filter",
"Contain Image",
"Cover Image",
"Image Hue/Saturation/Lightness"
]
},
{
@@ -366,6 +391,7 @@
"Generate QR Code",
"Parse QR Code",
"Haversine distance",
"Generate Lorem Ipsum",
"Numberwang",
"XKCD Random Number"
]
@@ -375,6 +401,7 @@
"ops": [
"Magic",
"Fork",
"Subsection",
"Merge",
"Register",
"Label",

View File

@@ -41,6 +41,7 @@ for (const opObj in Ops) {
inputType: op.inputType,
outputType: op.presentType,
flowControl: op.flowControl,
manualBake: op.manualBake,
args: op.args
};

View File

@@ -222,7 +222,7 @@ export default ${moduleName};
console.log(`\nNext steps:
1. Add your operation to ${colors.green("src/core/config/Categories.json")}
2. Write your operation code.
3. Write tests in ${colors.green("test/tests/operations/")}
3. Write tests in ${colors.green("tests/operations/tests/")}
4. Run ${colors.cyan("npm run lint")} and ${colors.cyan("npm run test")}
5. Submit a Pull Request to get your operation added to the official CyberChef repository.`);

0
src/core/lib/BCD.mjs Executable file → Normal file
View File

0
src/core/lib/Base58.mjs Executable file → Normal file
View File

0
src/core/lib/Base64.mjs Executable file → Normal file
View File

0
src/core/lib/CanvasComponents.mjs Executable file → Normal file
View File

View File

@@ -0,0 +1,655 @@
/**
* Co-ordinate conversion resources.
*
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import geohash from "ngeohash";
import geodesy from "geodesy";
import OperationError from "../errors/OperationError";
/**
* Co-ordinate formats
*/
export const FORMATS = [
"Degrees Minutes Seconds",
"Degrees Decimal Minutes",
"Decimal Degrees",
"Geohash",
"Military Grid Reference System",
"Ordnance Survey National Grid",
"Universal Transverse Mercator"
];
/**
* Formats that should be passed to the conversion module as-is
*/
const NO_CHANGE = [
"Geohash",
"Military Grid Reference System",
"Ordnance Survey National Grid",
"Universal Transverse Mercator",
];
/**
* Convert a given latitude and longitude into a different format.
*
* @param {string} input - Input string to be converted
* @param {string} inFormat - Format of the input coordinates
* @param {string} inDelim - The delimiter splitting the lat/long of the input
* @param {string} outFormat - Format to convert to
* @param {string} outDelim - The delimiter to separate the output with
* @param {string} includeDir - Whether or not to include the compass direction in the output
* @param {number} precision - Precision of the result
* @returns {string} A formatted string of the converted co-ordinates
*/
export function convertCoordinates (input, inFormat, inDelim, outFormat, outDelim, includeDir, precision) {
let isPair = false,
split,
latlon,
convLat,
convLon,
conv,
hash,
utm,
mgrs,
osng,
splitLat,
splitLong,
lat,
lon;
// Can't have a precision less than 0!
if (precision < 0) {
precision = 0;
}
if (inDelim === "Auto") {
// Try to detect a delimiter in the input.
inDelim = findDelim(input);
if (inDelim === null) {
throw new OperationError("Unable to detect the input delimiter automatically.");
}
} else if (!inDelim.includes("Direction")) {
// Convert the delimiter argument value to the actual character
inDelim = realDelim(inDelim);
}
if (inFormat === "Auto") {
// Try to detect the format of the input data
inFormat = findFormat(input, inDelim);
if (inFormat === null) {
throw new OperationError("Unable to detect the input format automatically.");
}
}
// Convert the output delimiter argument to the real character
outDelim = realDelim(outDelim);
if (!NO_CHANGE.includes(inFormat)) {
if (inDelim.includes("Direction")) {
// Split on directions
split = input.split(/[NnEeSsWw]/g);
if (split[0] === "") {
// Remove first element if direction preceding
split = split.slice(1);
}
} else {
split = input.split(inDelim);
}
// Replace any co-ordinate symbols with spaces so we can split on them later
for (let i = 0; i < split.length; i++) {
split[i] = split[i].replace(/[°˝´'"]/g, " ");
}
if (split.length > 1) {
isPair = true;
}
} else {
// Remove any delimiters from the input
input = input.replace(inDelim, "");
isPair = true;
}
// Conversions from the input format into a geodesy latlon object
switch (inFormat) {
case "Geohash":
hash = geohash.decode(input.replace(/[^A-Za-z0-9]/g, ""));
latlon = new geodesy.LatLonEllipsoidal(hash.latitude, hash.longitude);
break;
case "Military Grid Reference System":
utm = geodesy.Mgrs.parse(input.replace(/[^A-Za-z0-9]/g, "")).toUtm();
latlon = utm.toLatLonE();
break;
case "Ordnance Survey National Grid":
osng = geodesy.OsGridRef.parse(input.replace(/[^A-Za-z0-9]/g, ""));
latlon = geodesy.OsGridRef.osGridToLatLon(osng);
break;
case "Universal Transverse Mercator":
// Geodesy needs a space between the first 2 digits and the next letter
if (/^[\d]{2}[A-Za-z]/.test(input)) {
input = input.slice(0, 2) + " " + input.slice(2);
}
utm = geodesy.Utm.parse(input);
latlon = utm.toLatLonE();
break;
case "Degrees Minutes Seconds":
if (isPair) {
// Split up the lat/long into degrees / minutes / seconds values
splitLat = splitInput(split[0]);
splitLong = splitInput(split[1]);
if (splitLat.length >= 3 && splitLong.length >= 3) {
lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2], 10);
lon = convDMSToDD(splitLong[0], splitLong[1], splitLong[2], 10);
latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lon.degrees);
} else {
throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
}
} else {
// Not a pair, so only try to convert one set of co-ordinates
splitLat = splitInput(split[0]);
if (splitLat.length >= 3) {
lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2]);
latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees);
} else {
throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
}
}
break;
case "Degrees Decimal Minutes":
if (isPair) {
splitLat = splitInput(split[0]);
splitLong = splitInput(split[1]);
if (splitLat.length !== 2 || splitLong.length !== 2) {
throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
}
// Convert to decimal degrees, and then convert to a geodesy object
lat = convDDMToDD(splitLat[0], splitLat[1], 10);
lon = convDDMToDD(splitLong[0], splitLong[1], 10);
latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lon.degrees);
} else {
// Not a pair, so only try to convert one set of co-ordinates
splitLat = splitInput(input);
if (splitLat.length !== 2) {
throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
}
lat = convDDMToDD(splitLat[0], splitLat[1], 10);
latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees);
}
break;
case "Decimal Degrees":
if (isPair) {
splitLat = splitInput(split[0]);
splitLong = splitInput(split[1]);
if (splitLat.length !== 1 || splitLong.length !== 1) {
throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
}
latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLong[0]);
} else {
// Not a pair, so only try to convert one set of co-ordinates
splitLat = splitInput(split[0]);
if (splitLat.length !== 1) {
throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
}
latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLat[0]);
}
break;
default:
throw new OperationError(`Unknown input format '${inFormat}'`);
}
// Everything is now a geodesy latlon object
// These store the latitude and longitude as decimal
if (inFormat.includes("Degrees")) {
// If the input string contains directions, we need to check if they're S or W.
// If either of the directions are, we should make the decimal value negative
const dirs = input.toUpperCase().match(/[NESW]/g);
if (dirs && dirs.length >= 1) {
// Make positive lat/lon values with S/W directions into negative values
if (dirs[0] === "S" || dirs[0] === "W" && latlon.lat > 0) {
latlon.lat = -latlon.lat;
}
if (dirs.length >= 2) {
if (dirs[1] === "S" || dirs[1] === "W" && latlon.lon > 0) {
latlon.lon = -latlon.lon;
}
}
}
}
// Try to find the compass directions of the lat and long
const [latDir, longDir] = findDirs(latlon.lat + "," + latlon.lon, ",");
// Output conversions for each output format
switch (outFormat) {
case "Decimal Degrees":
// We could use the built in latlon.toString(),
// but this makes adjusting the output harder
lat = convDDToDD(latlon.lat, precision);
lon = convDDToDD(latlon.lon, precision);
convLat = lat.string;
convLon = lon.string;
break;
case "Degrees Decimal Minutes":
lat = convDDToDDM(latlon.lat, precision);
lon = convDDToDDM(latlon.lon, precision);
convLat = lat.string;
convLon = lon.string;
break;
case "Degrees Minutes Seconds":
lat = convDDToDMS(latlon.lat, precision);
lon = convDDToDMS(latlon.lon, precision);
convLat = lat.string;
convLon = lon.string;
break;
case "Geohash":
convLat = geohash.encode(latlon.lat, latlon.lon, precision);
break;
case "Military Grid Reference System":
utm = latlon.toUtm();
mgrs = utm.toMgrs();
// MGRS wants a precision that's an even number between 2 and 10
if (precision % 2 !== 0) {
precision = precision + 1;
}
if (precision > 10) {
precision = 10;
}
convLat = mgrs.toString(precision);
break;
case "Ordnance Survey National Grid":
osng = geodesy.OsGridRef.latLonToOsGrid(latlon);
if (osng.toString() === "") {
throw new OperationError("Could not convert co-ordinates to OS National Grid. Are the co-ordinates in range?");
}
// OSNG wants a precision that's an even number between 2 and 10
if (precision % 2 !== 0) {
precision = precision + 1;
}
if (precision > 10) {
precision = 10;
}
convLat = osng.toString(precision);
break;
case "Universal Transverse Mercator":
utm = latlon.toUtm();
convLat = utm.toString(precision);
break;
}
if (convLat === undefined) {
throw new OperationError("Error converting co-ordinates.");
}
if (outFormat.includes("Degrees")) {
// Format DD/DDM/DMS for output
// If we're outputting a compass direction, remove the negative sign
if (latDir === "S" && includeDir !== "None") {
convLat = convLat.replace("-", "");
}
if (longDir === "W" && includeDir !== "None") {
convLon = convLon.replace("-", "");
}
let outConv = "";
if (includeDir === "Before") {
outConv += latDir + " ";
}
outConv += convLat;
if (includeDir === "After") {
outConv += " " + latDir;
}
outConv += outDelim;
if (isPair) {
if (includeDir === "Before") {
outConv += longDir + " ";
}
outConv += convLon;
if (includeDir === "After") {
outConv += " " + longDir;
}
outConv += outDelim;
}
conv = outConv;
} else {
conv = convLat + outDelim;
}
return conv;
}
/**
* Split up the input using a space or degrees signs, and sanitise the result
*
* @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){
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){
// Turn the item into a float
split.push(parseFloat(item));
}
});
return split;
}
/**
* Convert Degrees Minutes Seconds to Decimal Degrees
*
* @param {number} degrees - The degrees of the input co-ordinates
* @param {number} minutes - The minutes of the input co-ordinates
* @param {number} seconds - The seconds of the input co-ordinates
* @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){
const absDegrees = Math.abs(degrees);
let conv = absDegrees + (minutes / 60) + (seconds / 3600);
let outString = round(conv, precision) + "°";
if (isNegativeZero(degrees) || degrees < 0) {
conv = -conv;
outString = "-" + outString;
}
return {
"degrees": conv,
"string": outString
};
}
/**
* Convert Decimal Degrees Minutes to Decimal Degrees
*
* @param {number} degrees - The input degrees to be converted
* @param {number} minutes - The input minutes to be converted
* @param {number} precision - The precision which 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 convDDMToDD (degrees, minutes, precision) {
const absDegrees = Math.abs(degrees);
let conv = absDegrees + minutes / 60;
let outString = round(conv, precision) + "°";
if (isNegativeZero(degrees) || degrees < 0) {
conv = -conv;
outString = "-" + outString;
}
return {
"degrees": conv,
"string": outString
};
}
/**
* Convert Decimal Degrees to Decimal Degrees
*
* Doesn't affect the input, just puts it into an object
* @param {number} degrees - The input degrees to be converted
* @param {number} precision - The precision which 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 convDDToDD (degrees, precision) {
return {
"degrees": degrees,
"string": round(degrees, precision) + "°"
};
}
/**
* Convert Decimal Degrees to Degrees Minutes Seconds
*
* @param {number} decDegrees - The input data to be converted
* @param {number} precision - The precision which the result should be rounded to
* @returns {{string: string, degrees: number, minutes: number, seconds: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes, .seconds), and a formatted string version (obj.string)
*/
function convDDToDMS (decDegrees, precision) {
const absDegrees = Math.abs(decDegrees);
let degrees = Math.floor(absDegrees);
const minutes = Math.floor(60 * (absDegrees - degrees)),
seconds = round(3600 * (absDegrees - degrees) - 60 * minutes, precision);
let outString = degrees + "° " + minutes + "' " + seconds + "\"";
if (isNegativeZero(decDegrees) || decDegrees < 0) {
degrees = -degrees;
outString = "-" + outString;
}
return {
"degrees": degrees,
"minutes": minutes,
"seconds": seconds,
"string": outString
};
}
/**
* Convert Decimal Degrees to Degrees Decimal Minutes
*
* @param {number} decDegrees - The input degrees to be converted
* @param {number} precision - The precision the input data should be rounded to
* @returns {{string: string, degrees: number, minutes: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes), and a formatted string version (obj.string)
*/
function convDDToDDM (decDegrees, precision) {
const absDegrees = Math.abs(decDegrees);
let degrees = Math.floor(absDegrees);
const minutes = absDegrees - degrees,
decMinutes = round(minutes * 60, precision);
let outString = degrees + "° " + decMinutes + "'";
if (decDegrees < 0 || isNegativeZero(decDegrees)) {
degrees = -degrees;
outString = "-" + outString;
}
return {
"degrees": degrees,
"minutes": decMinutes,
"string": outString,
};
}
/**
* Finds and returns the compass directions in an input string
*
* @param {string} input - The input co-ordinates containing the direction
* @param {string} delim - The delimiter separating latitide and longitude
* @returns {string[]} String array containing the latitude and longitude directions
*/
export function findDirs(input, delim) {
const upperInput = input.toUpperCase();
const dirExp = new RegExp(/[NESW]/g);
const dirs = upperInput.match(dirExp);
if (dirs) {
// If there's actually compass directions
// in the input, use these to work out the direction
if (dirs.length <= 2 && dirs.length >= 1) {
return dirs.length === 2 ? [dirs[0], dirs[1]] : [dirs[0], ""];
}
}
// Nothing was returned, so guess the directions
let lat = upperInput,
long,
latDir = "",
longDir = "";
if (!delim.includes("Direction")) {
if (upperInput.includes(delim)) {
const split = upperInput.split(delim);
if (split.length >= 1) {
if (split[0] !== "") {
lat = split[0];
}
if (split.length >= 2 && split[1] !== "") {
long = split[1];
}
}
}
} else {
const split = upperInput.split(dirExp);
if (split.length > 1) {
lat = split[0] === "" ? split[1] : split[0];
if (split.length > 2 && split[2] !== "") {
long = split[2];
}
}
}
if (lat) {
lat = parseFloat(lat);
latDir = lat < 0 ? "S" : "N";
}
if (long) {
long = parseFloat(long);
longDir = long < 0 ? "W" : "E";
}
return [latDir, longDir];
}
/**
* Detects the co-ordinate format of the input data
*
* @param {string} input - The input data whose format we need to detect
* @param {string} delim - The delimiter separating the data in input
* @returns {string} The input format
*/
export function findFormat (input, delim) {
let testData;
const mgrsPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]{1}\s?[A-HJ-NP-Z][A-HJ-NP-V]\s?[0-9\s]+/),
osngPattern = new RegExp(/^[A-HJ-Z]{2}\s+[0-9\s]+$/),
geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/),
utmPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]\s[0-9.]+\s?[0-9.]+$/),
degPattern = new RegExp(/[°'"]/g);
input = input.trim();
if (delim !== null && delim.includes("Direction")) {
const split = input.split(/[NnEeSsWw]/);
if (split.length > 1) {
testData = split[0] === "" ? split[1] : split[0];
}
} else if (delim !== null && delim !== "") {
if (input.includes(delim)) {
const split = input.split(delim);
if (split.length > 1) {
testData = split[0] === "" ? split[1] : split[0];
}
} else {
testData = input;
}
}
// Test non-degrees formats
if (!degPattern.test(input)) {
const filteredInput = input.toUpperCase().replace(delim, "");
if (utmPattern.test(filteredInput)) {
return "Universal Transverse Mercator";
}
if (mgrsPattern.test(filteredInput)) {
return "Military Grid Reference System";
}
if (osngPattern.test(filteredInput)) {
return "Ordnance Survey National Grid";
}
if (geohashPattern.test(filteredInput)) {
return "Geohash";
}
}
// Test DMS/DDM/DD formats
if (testData !== undefined) {
const split = splitInput(testData);
switch (split.length){
case 3:
return "Degrees Minutes Seconds";
case 2:
return "Degrees Decimal Minutes";
case 1:
return "Decimal Degrees";
}
}
return null;
}
/**
* Automatically find the delimeter type from the given input
*
* @param {string} input
* @returns {string} Delimiter type
*/
export function findDelim (input) {
input = input.trim();
const delims = [",", ";", ":"];
const testDir = input.match(/[NnEeSsWw]/g);
if (testDir !== null && testDir.length > 0 && testDir.length < 3) {
// Possibly contains a direction
const splitInput = input.split(/[NnEeSsWw]/);
if (splitInput.length <= 3 && splitInput.length > 0) {
// If there's 3 splits (one should be empty), then assume we have directions
if (splitInput[0] === "") {
return "Direction Preceding";
} else if (splitInput[splitInput.length - 1] === "") {
return "Direction Following";
}
}
}
// Loop through the standard delimiters, and try to find them in the input
for (let i = 0; i < delims.length; i++) {
const delim = delims[i];
if (input.includes(delim)) {
const splitInput = input.split(delim);
if (splitInput.length <= 3 && splitInput.length > 0) {
// Don't want to try and convert more than 2 co-ordinates
return delim;
}
}
}
return null;
}
/**
* Gets the real string for a delimiter name.
*
* @param {string} delim The delimiter to be matched
* @returns {string}
*/
export function realDelim (delim) {
return {
"Auto": "Auto",
"Space": " ",
"\\n": "\n",
"Comma": ",",
"Semi-colon": ";",
"Colon": ":"
}[delim];
}
/**
* Returns true if a zero is negative
*
* @param {number} zero
* @returns {boolean}
*/
function isNegativeZero(zero) {
return zero === 0 && (1/zero < 0);
}
/**
* Rounds a number to a specified number of decimal places
*
* @param {number} input - The number to be rounded
* @param {precision} precision - The number of decimal places the number should be rounded to
* @returns {number}
*/
function round(input, precision) {
precision = Math.pow(10, precision);
return Math.round(input * precision) / precision;
}

File diff suppressed because it is too large Load Diff

263
src/core/lib/FileType.mjs Normal file
View File

@@ -0,0 +1,263 @@
/**
* File type functions
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*
*/
import {FILE_SIGNATURES} from "./FileSignatures";
import {sendStatusMessage} from "../Utils";
/**
* Checks whether a signature matches a buffer.
*
* @param {Object|Object[]} sig - A dictionary of offsets with values assigned to them.
* These values can be numbers for static checks, arrays of potential valid matches,
* or bespoke functions to check the validity of the buffer value at that offset.
* @param {Uint8Array} buf
* @param {number} [offset=0] Where in the buffer to start searching from
* @returns {boolean}
*/
function signatureMatches(sig, buf, offset=0) {
// Using a length check seems to be more performant than `sig instanceof Array`
if (sig.length) {
// sig is an Array - return true if any of them match
// The following `reduce` method is nice, but performance matters here, so we
// opt for a faster, if less elegant, for loop.
// return sig.reduce((acc, s) => acc || bytesMatch(s, buf, offset), false);
for (let i = 0; i < sig.length; i++) {
if (bytesMatch(sig[i], buf, offset)) return true;
}
return false;
} else {
return bytesMatch(sig, buf, offset);
}
}
/**
* Checks whether a set of bytes match the given buffer.
*
* @param {Object} sig - A dictionary of offsets with values assigned to them.
* These values can be numbers for static checks, arrays of potential valid matches,
* or bespoke functions to check the validity of the buffer value at that offset.
* @param {Uint8Array} buf
* @param {number} [offset=0] Where in the buffer to start searching from
* @returns {boolean}
*/
function bytesMatch(sig, buf, offset=0) {
for (const sigoffset in sig) {
const pos = parseInt(sigoffset, 10) + offset;
switch (typeof sig[sigoffset]) {
case "number": // Static check
if (buf[pos] !== sig[sigoffset])
return false;
break;
case "object": // Array of options
if (sig[sigoffset].indexOf(buf[pos]) < 0)
return false;
break;
case "function": // More complex calculation
if (!sig[sigoffset](buf[pos]))
return false;
break;
default:
throw new Error(`Unrecognised signature type at offset ${sigoffset}`);
}
}
return true;
}
/**
* Given a buffer, detects magic byte sequences at specific positions and returns the
* extension and mime type.
*
* @param {Uint8Array} buf
* @param {string[]} [categories=All] - Which categories of file to look for
* @returns {Object[]} types
* @returns {string} type.name - Name of file type
* @returns {string} type.ext - File extension
* @returns {string} type.mime - Mime type
* @returns {string} [type.desc] - Description
*/
export function detectFileType(buf, categories=Object.keys(FILE_SIGNATURES)) {
if (!(buf && buf.length > 1)) {
return [];
}
const matchingFiles = [];
const signatures = {};
for (const cat in FILE_SIGNATURES) {
if (categories.includes(cat)) {
signatures[cat] = FILE_SIGNATURES[cat];
}
}
for (const cat in signatures) {
const category = signatures[cat];
category.forEach(filetype => {
if (signatureMatches(filetype.signature, buf)) {
matchingFiles.push(filetype);
}
});
}
return matchingFiles;
}
/**
* Given a buffer, searches for magic byte sequences at all possible positions and returns
* the extensions and mime types.
*
* @param {Uint8Array} buf
* @param {string[]} [categories=All] - Which categories of file to look for
* @returns {Object[]} foundFiles
* @returns {number} foundFiles.offset - The position in the buffer at which this file was found
* @returns {Object} foundFiles.fileDetails
* @returns {string} foundFiles.fileDetails.name - Name of file type
* @returns {string} foundFiles.fileDetails.ext - File extension
* @returns {string} foundFiles.fileDetails.mime - Mime type
* @returns {string} [foundFiles.fileDetails.desc] - Description
*/
export function scanForFileTypes(buf, categories=Object.keys(FILE_SIGNATURES)) {
if (!(buf && buf.length > 1)) {
return [];
}
const foundFiles = [];
const signatures = {};
for (const cat in FILE_SIGNATURES) {
if (categories.includes(cat)) {
signatures[cat] = FILE_SIGNATURES[cat];
}
}
for (const cat in signatures) {
const category = signatures[cat];
for (let i = 0; i < category.length; i++) {
const filetype = category[i];
const sigs = filetype.signature.length ? filetype.signature : [filetype.signature];
sigs.forEach(sig => {
let pos = 0;
while ((pos = locatePotentialSig(buf, sig, pos)) >= 0) {
if (bytesMatch(sig, buf, pos)) {
sendStatusMessage(`Found potential signature for ${filetype.name} at pos ${pos}`);
foundFiles.push({
offset: pos,
fileDetails: filetype
});
}
pos++;
}
});
}
}
// Return found files in order of increasing offset
return foundFiles.sort((a, b) => {
return a.offset - b.offset;
});
}
/**
* Fastcheck function to quickly scan the buffer for the first byte in a signature.
*
* @param {Uint8Array} buf - The buffer to search
* @param {Object} sig - A single signature object (Not an array of signatures)
* @param {number} offset - Where to start search from
* @returs {number} The position of the match or -1 if one cannot be found.
*/
function locatePotentialSig(buf, sig, offset) {
// Find values for first key and value in sig
const k = parseInt(Object.keys(sig)[0], 10);
const v = Object.values(sig)[0];
switch (typeof v) {
case "number":
return buf.indexOf(v, offset + k) - k;
case "object":
for (let i = offset + k; i < buf.length; i++) {
if (v.indexOf(buf[i]) >= 0) return i - k;
}
return -1;
case "function":
for (let i = offset + k; i < buf.length; i++) {
if (v(buf[i])) return i - k;
}
return -1;
default:
throw new Error("Unrecognised signature type");
}
}
/**
* Detects whether the given buffer is a file of the type specified.
*
* @param {string|RegExp} type
* @param {Uint8Array} buf
* @returns {string|false} The mime type or false if the type does not match
*/
export function isType(type, buf) {
const types = detectFileType(buf);
if (!(types && types.length)) return false;
if (typeof type === "string") {
return types.reduce((acc, t) => {
const mime = t.mime.startsWith(type) ? t.mime : false;
return acc || mime;
}, false);
} else if (type instanceof RegExp) {
return types.reduce((acc, t) => {
const mime = type.test(t.mime) ? t.mime : false;
return acc || mime;
}, false);
} else {
throw new Error("Invalid type input.");
}
}
/**
* Detects whether the given buffer contains an image file.
*
* @param {Uint8Array} buf
* @returns {string|false} The mime type or false if the type does not match
*/
export function isImage(buf) {
return isType("image", buf);
}
/**
* Attempts to extract a file from a data stream given its offset and extractor function.
*
* @param {Uint8Array} bytes
* @param {Object} fileDetail
* @param {string} fileDetail.mime
* @param {string} fileDetail.extension
* @param {Function} fileDetail.extractor
* @param {number} offset
* @returns {File}
*/
export function extractFile(bytes, fileDetail, offset) {
if (fileDetail.extractor) {
sendStatusMessage(`Attempting to extract ${fileDetail.name} at pos ${offset}...`);
const fileData = fileDetail.extractor(bytes, offset);
const ext = fileDetail.extension.split(",")[0];
return new File([fileData], `extracted_at_0x${offset.toString(16)}.${ext}`, {
type: fileDetail.mime
});
}
throw new Error(`No extraction algorithm available for "${fileDetail.mime}" files`);
}

230
src/core/lib/LoremIpsum.mjs Normal file
View File

@@ -0,0 +1,230 @@
/**
* Lorem Ipsum generator.
*
* @author Klaxon [klaxon@veyr.com]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
/**
* Generate lorem ipsum paragraphs.
*
* @param {number} length
* @returns {string}
*/
export function GenerateParagraphs(length=3) {
const paragraphs = [];
while (paragraphs.length < length) {
const paragraphLength = getRandomLength(PARAGRAPH_LENGTH_MEAN, PARAGRAPH_LENGTH_STD_DEV);
const sentences = [];
while (sentences.length < paragraphLength) {
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
const sentence = getWords(sentenceLength);
sentences.push(formatSentence(sentence));
}
paragraphs.push(formatParagraph(sentences));
}
paragraphs[paragraphs.length-1] = paragraphs[paragraphs.length-1].slice(0, -2);
paragraphs[0] = replaceStart(paragraphs[0]);
return paragraphs.join("");
}
/**
* Generate lorem ipsum sentences.
*
* @param {number} length
* @returns {string}
*/
export function GenerateSentences(length=3) {
const sentences = [];
while (sentences.length < length) {
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
const sentence = getWords(sentenceLength);
sentences.push(formatSentence(sentence));
}
const paragraphs = sentencesToParagraphs(sentences);
return paragraphs.join("");
}
/**
* Generate lorem ipsum words.
*
* @param {number} length
* @returns {string}
*/
export function GenerateWords(length=3) {
const words = getWords(length);
const sentences = wordsToSentences(words);
const paragraphs = sentencesToParagraphs(sentences);
return paragraphs.join("");
}
/**
* Generate lorem ipsum bytes.
*
* @param {number} length
* @returns {string}
*/
export function GenerateBytes(length=3) {
const str = GenerateWords(length/3);
return str.slice(0, length);
}
/**
* Get array of randomly selected words from the lorem ipsum wordList.
*
* @param {number} length
* @returns {string[]}
* @private
*/
function getWords(length=3) {
const words = [];
let word;
let previousWord;
while (words.length < length){
do {
word = wordList[Math.floor(Math.random() * wordList.length)];
} while (previousWord === word);
words.push(word);
previousWord = word;
}
return words;
}
/**
* Convert an array of words into an array of sentences
*
* @param {string[]} words
* @returns {string[]}
* @private
*/
function wordsToSentences(words) {
const sentences = [];
while (words.length > 0) {
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
if (sentenceLength <= words.length) {
sentences.push(formatSentence(words.splice(0, sentenceLength)));
} else {
sentences.push(formatSentence(words.splice(0, words.length)));
}
}
return sentences;
}
/**
* Convert an array of sentences into an array of paragraphs
*
* @param {string[]} sentences
* @returns {string[]}
* @private
*/
function sentencesToParagraphs(sentences) {
const paragraphs = [];
while (sentences.length > 0) {
const paragraphLength = getRandomLength(PARAGRAPH_LENGTH_MEAN, PARAGRAPH_LENGTH_STD_DEV);
paragraphs.push(formatParagraph(sentences.splice(0, paragraphLength)));
}
paragraphs[paragraphs.length-1] = paragraphs[paragraphs.length-1].slice(0, -1);
paragraphs[0] = replaceStart(paragraphs[0]);
return paragraphs;
}
/**
* Format an array of words into a sentence.
*
* @param {string[]} words
* @returns {string}
* @private
*/
function formatSentence(words) {
// 0.35 chance of a comma being added randomly to the sentence.
if (Math.random() < PROBABILITY_OF_A_COMMA) {
const pos = Math.round(Math.random()*(words.length-1));
words[pos] +=",";
}
let sentence = words.join(" ");
sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1);
sentence += ".";
return sentence;
}
/**
* Format an array of sentences into a paragraph.
*
* @param {string[]} sentences
* @returns {string}
* @private
*/
function formatParagraph(sentences) {
let paragraph = sentences.join(" ");
paragraph += "\n\n";
return paragraph;
}
/**
* Get a random number based on a mean and standard deviation.
*
* @param {number} mean
* @param {number} stdDev
* @returns {number}
* @private
*/
function getRandomLength(mean, stdDev) {
let length;
do {
length = Math.round((Math.random()*2-1)+(Math.random()*2-1)+(Math.random()*2-1)*stdDev+mean);
} while (length <= 0);
return length;
}
/**
* Replace first 5 words with "Lorem ipsum dolor sit amet"
*
* @param {string[]} str
* @returns {string[]}
* @private
*/
function replaceStart(str) {
let words = str.split(" ");
if (words.length > 5) {
words.splice(0, 5, "Lorem", "ipsum", "dolor", "sit", "amet");
return words.join(" ");
} else {
const lorem = ["Lorem", "ipsum", "dolor", "sit", "amet"];
words = lorem.slice(0, words.length);
str = words.join(" ");
str += ".";
return str;
}
}
const SENTENCE_LENGTH_MEAN = 15;
const SENTENCE_LENGTH_STD_DEV = 9;
const PARAGRAPH_LENGTH_MEAN = 5;
const PARAGRAPH_LENGTH_STD_DEV = 2;
const PROBABILITY_OF_A_COMMA = 0.35;
const wordList = [
"ad", "adipisicing", "aliqua", "aliquip", "amet", "anim",
"aute", "cillum", "commodo", "consectetur", "consequat", "culpa",
"cupidatat", "deserunt", "do", "dolor", "dolore", "duis",
"ea", "eiusmod", "elit", "enim", "esse", "est",
"et", "eu", "ex", "excepteur", "exercitation", "fugiat",
"id", "in", "incididunt", "ipsum", "irure", "labore",
"laboris", "laborum", "Lorem", "magna", "minim", "mollit",
"nisi", "non", "nostrud", "nulla", "occaecat", "officia",
"pariatur", "proident", "qui", "quis", "reprehenderit", "sint",
"sit", "sunt", "tempor", "ullamco", "ut", "velit",
"veniam", "voluptate",
];

View File

@@ -2,6 +2,7 @@ import OperationConfig from "../config/OperationConfig.json";
import Utils from "../Utils";
import Recipe from "../Recipe";
import Dish from "../Dish";
import {detectFileType} from "./FileType";
import chiSquared from "chi-squared";
/**
@@ -92,7 +93,14 @@ class Magic {
* @returns {string} [type.desc] - Description
*/
detectFileType() {
return Magic.magicFileType(this.inputBuffer);
const fileType = detectFileType(this.inputBuffer);
if (!fileType.length) return null;
return {
ext: fileType[0].extension,
mime: fileType[0].mime,
desc: fileType[0].description
};
}
/**
@@ -785,452 +793,9 @@ class Magic {
}[code];
}
/**
* Given a buffer, detects magic byte sequences at specific positions and returns the
* extension and mime type.
*
* @param {Uint8Array} buf
* @returns {Object} type
* @returns {string} type.ext - File extension
* @returns {string} type.mime - Mime type
* @returns {string} [type.desc] - Description
*/
static magicFileType(buf) {
if (!(buf && buf.length > 1)) {
return null;
}
if (buf[0] === 0xFF && buf[1] === 0xD8 && buf[2] === 0xFF) {
return {
ext: "jpg",
mime: "image/jpeg"
};
}
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) {
return {
ext: "png",
mime: "image/png"
};
}
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) {
return {
ext: "gif",
mime: "image/gif"
};
}
if (buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) {
return {
ext: "webp",
mime: "image/webp"
};
}
// needs to be before `tif` check
if (((buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0x2A && buf[3] === 0x0) || (buf[0] === 0x4D && buf[1] === 0x4D && buf[2] === 0x0 && buf[3] === 0x2A)) && buf[8] === 0x43 && buf[9] === 0x52) {
return {
ext: "cr2",
mime: "image/x-canon-cr2"
};
}
if ((buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0x2A && buf[3] === 0x0) || (buf[0] === 0x4D && buf[1] === 0x4D && buf[2] === 0x0 && buf[3] === 0x2A)) {
return {
ext: "tif",
mime: "image/tiff"
};
}
if (buf[0] === 0x42 && buf[1] === 0x4D) {
return {
ext: "bmp",
mime: "image/bmp"
};
}
if (buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0xBC) {
return {
ext: "jxr",
mime: "image/vnd.ms-photo"
};
}
if (buf[0] === 0x38 && buf[1] === 0x42 && buf[2] === 0x50 && buf[3] === 0x53) {
return {
ext: "psd",
mime: "image/vnd.adobe.photoshop"
};
}
// needs to be before `zip` check
if (buf[0] === 0x50 && buf[1] === 0x4B && buf[2] === 0x3 && buf[3] === 0x4 && buf[30] === 0x6D && buf[31] === 0x69 && buf[32] === 0x6D && buf[33] === 0x65 && buf[34] === 0x74 && buf[35] === 0x79 && buf[36] === 0x70 && buf[37] === 0x65 && buf[38] === 0x61 && buf[39] === 0x70 && buf[40] === 0x70 && buf[41] === 0x6C && buf[42] === 0x69 && buf[43] === 0x63 && buf[44] === 0x61 && buf[45] === 0x74 && buf[46] === 0x69 && buf[47] === 0x6F && buf[48] === 0x6E && buf[49] === 0x2F && buf[50] === 0x65 && buf[51] === 0x70 && buf[52] === 0x75 && buf[53] === 0x62 && buf[54] === 0x2B && buf[55] === 0x7A && buf[56] === 0x69 && buf[57] === 0x70) {
return {
ext: "epub",
mime: "application/epub+zip"
};
}
if (buf[0] === 0x50 && buf[1] === 0x4B && (buf[2] === 0x3 || buf[2] === 0x5 || buf[2] === 0x7) && (buf[3] === 0x4 || buf[3] === 0x6 || buf[3] === 0x8)) {
return {
ext: "zip",
mime: "application/zip"
};
}
if (buf[257] === 0x75 && buf[258] === 0x73 && buf[259] === 0x74 && buf[260] === 0x61 && buf[261] === 0x72) {
return {
ext: "tar",
mime: "application/x-tar"
};
}
if (buf[0] === 0x52 && buf[1] === 0x61 && buf[2] === 0x72 && buf[3] === 0x21 && buf[4] === 0x1A && buf[5] === 0x7 && (buf[6] === 0x0 || buf[6] === 0x1)) {
return {
ext: "rar",
mime: "application/x-rar-compressed"
};
}
if (buf[0] === 0x1F && buf[1] === 0x8B && buf[2] === 0x8) {
return {
ext: "gz",
mime: "application/gzip"
};
}
if (buf[0] === 0x42 && buf[1] === 0x5A && buf[2] === 0x68) {
return {
ext: "bz2",
mime: "application/x-bzip2"
};
}
if (buf[0] === 0x37 && buf[1] === 0x7A && buf[2] === 0xBC && buf[3] === 0xAF && buf[4] === 0x27 && buf[5] === 0x1C) {
return {
ext: "7z",
mime: "application/x-7z-compressed"
};
}
if (buf[0] === 0x78 && buf[1] === 0x01) {
return {
ext: "dmg, zlib",
mime: "application/x-apple-diskimage, application/x-deflate"
};
}
if ((buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && (buf[3] === 0x18 || buf[3] === 0x20) && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) || (buf[0] === 0x33 && buf[1] === 0x67 && buf[2] === 0x70 && buf[3] === 0x35) || (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x1C && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x6D && buf[9] === 0x70 && buf[10] === 0x34 && buf[11] === 0x32 && buf[16] === 0x6D && buf[17] === 0x70 && buf[18] === 0x34 && buf[19] === 0x31 && buf[20] === 0x6D && buf[21] === 0x70 && buf[22] === 0x34 && buf[23] === 0x32 && buf[24] === 0x69 && buf[25] === 0x73 && buf[26] === 0x6F && buf[27] === 0x6D)) {
return {
ext: "mp4",
mime: "video/mp4"
};
}
if ((buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x1C && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x4D && buf[9] === 0x34 && buf[10] === 0x56)) {
return {
ext: "m4v",
mime: "video/x-m4v"
};
}
if (buf[0] === 0x4D && buf[1] === 0x54 && buf[2] === 0x68 && buf[3] === 0x64) {
return {
ext: "mid",
mime: "audio/midi"
};
}
// needs to be before the `webm` check
if (buf[31] === 0x6D && buf[32] === 0x61 && buf[33] === 0x74 && buf[34] === 0x72 && buf[35] === 0x6f && buf[36] === 0x73 && buf[37] === 0x6B && buf[38] === 0x61) {
return {
ext: "mkv",
mime: "video/x-matroska"
};
}
if (buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3) {
return {
ext: "webm",
mime: "video/webm"
};
}
if (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x14 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) {
return {
ext: "mov",
mime: "video/quicktime"
};
}
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x41 && buf[9] === 0x56 && buf[10] === 0x49) {
return {
ext: "avi",
mime: "video/x-msvideo"
};
}
if (buf[0] === 0x30 && buf[1] === 0x26 && buf[2] === 0xB2 && buf[3] === 0x75 && buf[4] === 0x8E && buf[5] === 0x66 && buf[6] === 0xCF && buf[7] === 0x11 && buf[8] === 0xA6 && buf[9] === 0xD9) {
return {
ext: "wmv",
mime: "video/x-ms-wmv"
};
}
if (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x1 && buf[3].toString(16)[0] === "b") {
return {
ext: "mpg",
mime: "video/mpeg"
};
}
if ((buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) || (buf[0] === 0xFF && buf[1] === 0xfb)) {
return {
ext: "mp3",
mime: "audio/mpeg"
};
}
if ((buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x4D && buf[9] === 0x34 && buf[10] === 0x41) || (buf[0] === 0x4D && buf[1] === 0x34 && buf[2] === 0x41 && buf[3] === 0x20)) {
return {
ext: "m4a",
mime: "audio/m4a"
};
}
if (buf[0] === 0x4F && buf[1] === 0x67 && buf[2] === 0x67 && buf[3] === 0x53) {
return {
ext: "ogg",
mime: "audio/ogg"
};
}
if (buf[0] === 0x66 && buf[1] === 0x4C && buf[2] === 0x61 && buf[3] === 0x43) {
return {
ext: "flac",
mime: "audio/x-flac"
};
}
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x57 && buf[9] === 0x41 && buf[10] === 0x56 && buf[11] === 0x45) {
return {
ext: "wav",
mime: "audio/x-wav"
};
}
if (buf[0] === 0x23 && buf[1] === 0x21 && buf[2] === 0x41 && buf[3] === 0x4D && buf[4] === 0x52 && buf[5] === 0x0A) {
return {
ext: "amr",
mime: "audio/amr"
};
}
if (buf[0] === 0x25 && buf[1] === 0x50 && buf[2] === 0x44 && buf[3] === 0x46) {
return {
ext: "pdf",
mime: "application/pdf"
};
}
if (buf[0] === 0x4D && buf[1] === 0x5A) {
return {
ext: "exe",
mime: "application/x-msdownload"
};
}
if ((buf[0] === 0x43 || buf[0] === 0x46) && buf[1] === 0x57 && buf[2] === 0x53) {
return {
ext: "swf",
mime: "application/x-shockwave-flash"
};
}
if (buf[0] === 0x7B && buf[1] === 0x5C && buf[2] === 0x72 && buf[3] === 0x74 && buf[4] === 0x66) {
return {
ext: "rtf",
mime: "application/rtf"
};
}
if (buf[0] === 0x77 && buf[1] === 0x4F && buf[2] === 0x46 && buf[3] === 0x46 && buf[4] === 0x00 && buf[5] === 0x01 && buf[6] === 0x00 && buf[7] === 0x00) {
return {
ext: "woff",
mime: "application/font-woff"
};
}
if (buf[0] === 0x77 && buf[1] === 0x4F && buf[2] === 0x46 && buf[3] === 0x32 && buf[4] === 0x00 && buf[5] === 0x01 && buf[6] === 0x00 && buf[7] === 0x00) {
return {
ext: "woff2",
mime: "application/font-woff"
};
}
if (buf[34] === 0x4C && buf[35] === 0x50 && ((buf[8] === 0x02 && buf[9] === 0x00 && buf[10] === 0x01) || (buf[8] === 0x01 && buf[9] === 0x00 && buf[10] === 0x00) || (buf[8] === 0x02 && buf[9] === 0x00 && buf[10] === 0x02))) {
return {
ext: "eot",
mime: "application/octet-stream"
};
}
if (buf[0] === 0x00 && buf[1] === 0x01 && buf[2] === 0x00 && buf[3] === 0x00 && buf[4] === 0x00) {
return {
ext: "ttf",
mime: "application/font-sfnt"
};
}
if (buf[0] === 0x4F && buf[1] === 0x54 && buf[2] === 0x54 && buf[3] === 0x4F && buf[4] === 0x00) {
return {
ext: "otf",
mime: "application/font-sfnt"
};
}
if (buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x01 && buf[3] === 0x00) {
return {
ext: "ico",
mime: "image/x-icon"
};
}
if (buf[0] === 0x46 && buf[1] === 0x4C && buf[2] === 0x56 && buf[3] === 0x01) {
return {
ext: "flv",
mime: "video/x-flv"
};
}
if (buf[0] === 0x25 && buf[1] === 0x21) {
return {
ext: "ps",
mime: "application/postscript"
};
}
if (buf[0] === 0xFD && buf[1] === 0x37 && buf[2] === 0x7A && buf[3] === 0x58 && buf[4] === 0x5A && buf[5] === 0x00) {
return {
ext: "xz",
mime: "application/x-xz"
};
}
if (buf[0] === 0x53 && buf[1] === 0x51 && buf[2] === 0x4C && buf[3] === 0x69) {
return {
ext: "sqlite",
mime: "application/x-sqlite3"
};
}
/**
*
* Added by n1474335 [n1474335@gmail.com] from here on
*
*/
if ((buf[0] === 0x1F && buf[1] === 0x9D) || (buf[0] === 0x1F && buf[1] === 0xA0)) {
return {
ext: "z, tar.z",
mime: "application/x-gtar"
};
}
if (buf[0] === 0x7F && buf[1] === 0x45 && buf[2] === 0x4C && buf[3] === 0x46) {
return {
ext: "none, axf, bin, elf, o, prx, puff, so",
mime: "application/x-executable",
desc: "Executable and Linkable Format file. No standard file extension."
};
}
if (buf[0] === 0xCA && buf[1] === 0xFE && buf[2] === 0xBA && buf[3] === 0xBE) {
return {
ext: "class",
mime: "application/java-vm"
};
}
if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
return {
ext: "txt",
mime: "text/plain",
desc: "UTF-8 encoded Unicode byte order mark detected, commonly but not exclusively seen in text files."
};
}
// Must be before Little-endian UTF-16 BOM
if (buf[0] === 0xFF && buf[1] === 0xFE && buf[2] === 0x00 && buf[3] === 0x00) {
return {
ext: "UTF32LE",
mime: "charset/utf32le",
desc: "Little-endian UTF-32 encoded Unicode byte order mark detected."
};
}
if (buf[0] === 0xFF && buf[1] === 0xFE) {
return {
ext: "UTF16LE",
mime: "charset/utf16le",
desc: "Little-endian UTF-16 encoded Unicode byte order mark detected."
};
}
if ((buf[0x8001] === 0x43 && buf[0x8002] === 0x44 && buf[0x8003] === 0x30 && buf[0x8004] === 0x30 && buf[0x8005] === 0x31) ||
(buf[0x8801] === 0x43 && buf[0x8802] === 0x44 && buf[0x8803] === 0x30 && buf[0x8804] === 0x30 && buf[0x8805] === 0x31) ||
(buf[0x9001] === 0x43 && buf[0x9002] === 0x44 && buf[0x9003] === 0x30 && buf[0x9004] === 0x30 && buf[0x9005] === 0x31)) {
return {
ext: "iso",
mime: "application/octet-stream",
desc: "ISO 9660 CD/DVD image file"
};
}
if (buf[0] === 0xD0 && buf[1] === 0xCF && buf[2] === 0x11 && buf[3] === 0xE0 && buf[4] === 0xA1 && buf[5] === 0xB1 && buf[6] === 0x1A && buf[7] === 0xE1) {
return {
ext: "doc, xls, ppt",
mime: "application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint",
desc: "Microsoft Office documents"
};
}
if (buf[0] === 0x64 && buf[1] === 0x65 && buf[2] === 0x78 && buf[3] === 0x0A && buf[4] === 0x30 && buf[5] === 0x33 && buf[6] === 0x35 && buf[7] === 0x00) {
return {
ext: "dex",
mime: "application/octet-stream",
desc: "Dalvik Executable (Android)"
};
}
if (buf[0] === 0x4B && buf[1] === 0x44 && buf[2] === 0x4D) {
return {
ext: "vmdk",
mime: "application/vmdk, application/x-virtualbox-vmdk"
};
}
if (buf[0] === 0x43 && buf[1] === 0x72 && buf[2] === 0x32 && buf[3] === 0x34) {
return {
ext: "crx",
mime: "application/crx",
desc: "Google Chrome extension or packaged app"
};
}
if (buf[0] === 0x78 && (buf[1] === 0x01 || buf[1] === 0x9C || buf[1] === 0xDA || buf[1] === 0x5e)) {
return {
ext: "zlib",
mime: "application/x-deflate"
};
}
return null;
}
}
/**
* Byte frequencies of various languages generated from Wikipedia dumps taken in late 2017 and early 2018.
* The Chi-Squared test cannot accept expected values of 0, so 0.0001 has been used to account for bytes

263
src/core/lib/Stream.mjs Normal file
View File

@@ -0,0 +1,263 @@
/**
* Stream class for parsing binary protocols.
*
* @author n1474335 [n1474335@gmail.com]
* @author tlwr [toby@toby.codes]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*
*/
/**
* A Stream can be used to traverse a binary blob, interpreting sections of it
* as various data types.
*/
export default class Stream {
/**
* Stream constructor.
*
* @param {Uint8Array} input
*/
constructor(input) {
this.bytes = input;
this.length = this.bytes.length;
this.position = 0;
this.bitPos = 0;
}
/**
* Get a number of bytes from the current position.
*
* @param {number} numBytes
* @returns {Uint8Array}
*/
getBytes(numBytes) {
if (this.position > this.length) return undefined;
const newPosition = this.position + numBytes;
const bytes = this.bytes.slice(this.position, newPosition);
this.position = newPosition;
this.bitPos = 0;
return bytes;
}
/**
* Interpret the following bytes as a string, stopping at the next null byte or
* the supplied limit.
*
* @param {number} numBytes
* @returns {string}
*/
readString(numBytes) {
if (this.position > this.length) return undefined;
let result = "";
for (let i = this.position; i < this.position + numBytes; i++) {
const currentByte = this.bytes[i];
if (currentByte === 0) break;
result += String.fromCharCode(currentByte);
}
this.position += numBytes;
this.bitPos = 0;
return result;
}
/**
* Interpret the following bytes as an integer in big or little endian.
*
* @param {number} numBytes
* @param {string} [endianness="be"]
* @returns {number}
*/
readInt(numBytes, endianness="be") {
if (this.position > this.length) return undefined;
let val = 0;
if (endianness === "be") {
for (let i = this.position; i < this.position + numBytes; i++) {
val = val << 8;
val |= this.bytes[i];
}
} else {
for (let i = this.position + numBytes - 1; i >= this.position; i--) {
val = val << 8;
val |= this.bytes[i];
}
}
this.position += numBytes;
this.bitPos = 0;
return val;
}
/**
* Reads a number of bits from the buffer.
*
* @TODO Add endianness
*
* @param {number} numBits
* @returns {number}
*/
readBits(numBits) {
if (this.position > this.length) return undefined;
let bitBuf = 0,
bitBufLen = 0;
// Add remaining bits from current byte
bitBuf = (this.bytes[this.position++] & bitMask(this.bitPos)) >>> this.bitPos;
bitBufLen = 8 - this.bitPos;
this.bitPos = 0;
// Not enough bits yet
while (bitBufLen < numBits) {
bitBuf |= this.bytes[this.position++] << bitBufLen;
bitBufLen += 8;
}
// Reverse back to numBits
if (bitBufLen > numBits) {
const excess = bitBufLen - numBits;
bitBuf &= (1 << numBits) - 1;
bitBufLen -= excess;
this.position--;
this.bitPos = 8 - excess;
}
return bitBuf;
/**
* Calculates the bit mask based on the current bit position.
*
* @param {number} bitPos
* @returns {number} The bit mask
*/
function bitMask(bitPos) {
return 256 - (1 << bitPos);
}
}
/**
* Consume the stream until we reach the specified byte or sequence of bytes.
*
* @param {number|List<number>} val
*/
continueUntil(val) {
if (this.position > this.length) return;
this.bitPos = 0;
if (typeof val === "number") {
while (++this.position < this.length && this.bytes[this.position] !== val) {
continue;
}
return;
}
// val is an array
let found = false;
while (!found && this.position < this.length) {
while (++this.position < this.length && this.bytes[this.position] !== val[0]) {
continue;
}
found = true;
for (let i = 1; i < val.length; i++) {
if (this.position + i > this.length || this.bytes[this.position + i] !== val[i])
found = false;
}
}
}
/**
* Consume the next byte if it matches the supplied value.
*
* @param {number} val
*/
consumeIf(val) {
if (this.bytes[this.position] === val) {
this.position++;
this.bitPos = 0;
}
}
/**
* Move forwards through the stream by the specified number of bytes.
*
* @param {number} numBytes
*/
moveForwardsBy(numBytes) {
const pos = this.position + numBytes;
if (pos < 0 || pos > this.length)
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
this.position = pos;
this.bitPos = 0;
}
/**
* Move backwards through the stream by the specified number of bytes.
*
* @param {number} numBytes
*/
moveBackwardsBy(numBytes) {
const pos = this.position - numBytes;
if (pos < 0 || pos > this.length)
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
this.position = pos;
this.bitPos = 0;
}
/**
* Move backwards through the strem by the specified number of bits.
*
* @param {number} numBits
*/
moveBackwardsByBits(numBits) {
if (numBits <= this.bitPos) {
this.bitPos -= numBits;
} else {
if (this.bitPos > 0) {
numBits -= this.bitPos;
this.bitPos = 0;
}
while (numBits > 0) {
this.moveBackwardsBy(1);
this.bitPos = 8;
this.moveBackwardsByBits(numBits);
numBits -= 8;
}
}
}
/**
* Move to a specified position in the stream.
*
* @param {number} pos
*/
moveTo(pos) {
if (pos < 0 || pos > this.length)
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
this.position = pos;
this.bitPos = 0;
}
/**
* Returns true if there are more bytes left in the stream.
*
* @returns {boolean}
*/
hasMore() {
return this.position < this.length;
}
/**
* Returns a slice of the stream up to the current position.
*
* @returns {Uint8Array}
*/
carve() {
if (this.bitPos > 0) this.position++;
return this.bytes.slice(0, this.position);
}
}

View File

@@ -5,7 +5,7 @@
*/
import Operation from "../Operation";
import bsonjs from "bson";
import bson from "bson";
import OperationError from "../errors/OperationError";
/**
@@ -36,8 +36,6 @@ class BSONDeserialise extends Operation {
run(input, args) {
if (!input.byteLength) return "";
const bson = new bsonjs();
try {
const data = bson.deserialize(new Buffer(input));
return JSON.stringify(data, null, 2);

View File

@@ -5,7 +5,7 @@
*/
import Operation from "../Operation";
import bsonjs from "bson";
import bson from "bson";
import OperationError from "../errors/OperationError";
/**
@@ -36,8 +36,6 @@ class BSONSerialise extends Operation {
run(input, args) {
if (!input) return new ArrayBuffer();
const bson = new bsonjs();
try {
const data = JSON.parse(input);
return bson.serialize(data).buffer;

View File

@@ -0,0 +1,102 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64";
import jimp from "jimp";
/**
* Blur Image operation
*/
class BlurImage extends Operation {
/**
* BlurImage constructor
*/
constructor() {
super();
this.name = "Blur Image";
this.module = "Image";
this.description = "Applies a blur effect to the image.<br><br>Gaussian blur is much slower than fast blur, but produces better results.";
this.infoURL = "https://wikipedia.org/wiki/Gaussian_blur";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Amount",
type: "number",
value: 5,
min: 1
},
{
name: "Type",
type: "option",
value: ["Fast", "Gaussian"]
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [blurAmount, blurType] = args;
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
switch (blurType){
case "Fast":
image.blur(blurAmount);
break;
case "Gaussian":
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Gaussian blurring image. This may take a while...");
image.gaussian(blurAmount);
break;
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error blurring image. (${err})`);
}
}
/**
* Displays the blurred image using HTML for web apps
*
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default BlurImage;

View File

@@ -0,0 +1,143 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp";
/**
* Contain Image operation
*/
class ContainImage extends Operation {
/**
* ContainImage constructor
*/
constructor() {
super();
this.name = "Contain Image";
this.module = "Image";
this.description = "Scales an image to the specified width and height, maintaining the aspect ratio. The image may be letterboxed.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Width",
type: "number",
value: 100,
min: 1
},
{
name: "Height",
type: "number",
value: 100,
min: 1
},
{
name: "Horizontal align",
type: "option",
value: [
"Left",
"Center",
"Right"
],
defaultIndex: 1
},
{
name: "Vertical align",
type: "option",
value: [
"Top",
"Middle",
"Bottom"
],
defaultIndex: 1
},
{
name: "Resizing algorithm",
type: "option",
value: [
"Nearest Neighbour",
"Bilinear",
"Bicubic",
"Hermite",
"Bezier"
],
defaultIndex: 1
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [width, height, hAlign, vAlign, alg] = args;
const resizeMap = {
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
"Bilinear": jimp.RESIZE_BILINEAR,
"Bicubic": jimp.RESIZE_BICUBIC,
"Hermite": jimp.RESIZE_HERMITE,
"Bezier": jimp.RESIZE_BEZIER
};
const alignMap = {
"Left": jimp.HORIZONTAL_ALIGN_LEFT,
"Center": jimp.HORIZONTAL_ALIGN_CENTER,
"Right": jimp.HORIZONTAL_ALIGN_RIGHT,
"Top": jimp.VERTICAL_ALIGN_TOP,
"Middle": jimp.VERTICAL_ALIGN_MIDDLE,
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
};
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Containing image...");
image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error containing image. (${err})`);
}
}
/**
* Displays the contained image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default ContainImage;

View File

@@ -0,0 +1,95 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import {FORMATS, convertCoordinates} from "../lib/ConvertCoordinates";
/**
* Convert co-ordinate format operation
*/
class ConvertCoordinateFormat extends Operation {
/**
* ConvertCoordinateFormat constructor
*/
constructor() {
super();
this.name = "Convert co-ordinate format";
this.module = "Hashing";
this.description = "Converts geographical coordinates between different formats.<br><br>Supported formats:<ul><li>Degrees Minutes Seconds (DMS)</li><li>Degrees Decimal Minutes (DDM)</li><li>Decimal Degrees (DD)</li><li>Geohash</li><li>Military Grid Reference System (MGRS)</li><li>Ordnance Survey National Grid (OSNG)</li><li>Universal Transverse Mercator (UTM)</li></ul><br>The operation can try to detect the input co-ordinate format and delimiter automatically, but this may not always work correctly.";
this.infoURL = "https://wikipedia.org/wiki/Geographic_coordinate_conversion";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Input Format",
"type": "option",
"value": ["Auto"].concat(FORMATS)
},
{
"name": "Input Delimiter",
"type": "option",
"value": [
"Auto",
"Direction Preceding",
"Direction Following",
"\\n",
"Comma",
"Semi-colon",
"Colon"
]
},
{
"name": "Output Format",
"type": "option",
"value": FORMATS
},
{
"name": "Output Delimiter",
"type": "option",
"value": [
"Space",
"\\n",
"Comma",
"Semi-colon",
"Colon"
]
},
{
"name": "Include Compass Directions",
"type": "option",
"value": [
"None",
"Before",
"After"
]
},
{
"name": "Precision",
"type": "number",
"value": 3
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
if (input.replace(/[\s+]/g, "") !== "") {
const [inFormat, inDelim, outFormat, outDelim, incDirection, precision] = args;
const result = convertCoordinates(input, inFormat, inDelim, outFormat, outDelim, incDirection, precision);
return result;
} else {
return input;
}
}
}
export default ConvertCoordinateFormat;

View File

@@ -0,0 +1,143 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp";
/**
* Cover Image operation
*/
class CoverImage extends Operation {
/**
* CoverImage constructor
*/
constructor() {
super();
this.name = "Cover Image";
this.module = "Image";
this.description = "Scales the image to the given width and height, keeping the aspect ratio. The image may be clipped.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Width",
type: "number",
value: 100,
min: 1
},
{
name: "Height",
type: "number",
value: 100,
min: 1
},
{
name: "Horizontal align",
type: "option",
value: [
"Left",
"Center",
"Right"
],
defaultIndex: 1
},
{
name: "Vertical align",
type: "option",
value: [
"Top",
"Middle",
"Bottom"
],
defaultIndex: 1
},
{
name: "Resizing algorithm",
type: "option",
value: [
"Nearest Neighbour",
"Bilinear",
"Bicubic",
"Hermite",
"Bezier"
],
defaultIndex: 1
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [width, height, hAlign, vAlign, alg] = args;
const resizeMap = {
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
"Bilinear": jimp.RESIZE_BILINEAR,
"Bicubic": jimp.RESIZE_BICUBIC,
"Hermite": jimp.RESIZE_HERMITE,
"Bezier": jimp.RESIZE_BEZIER
};
const alignMap = {
"Left": jimp.HORIZONTAL_ALIGN_LEFT,
"Center": jimp.HORIZONTAL_ALIGN_CENTER,
"Right": jimp.HORIZONTAL_ALIGN_RIGHT,
"Top": jimp.VERTICAL_ALIGN_TOP,
"Middle": jimp.VERTICAL_ALIGN_MIDDLE,
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
};
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Covering image...");
image.cover(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error covering image. (${err})`);
}
}
/**
* Displays the covered image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default CoverImage;

View File

@@ -0,0 +1,144 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp";
/**
* Crop Image operation
*/
class CropImage extends Operation {
/**
* CropImage constructor
*/
constructor() {
super();
this.name = "Crop Image";
this.module = "Image";
this.description = "Crops an image to the specified region, or automatically crops edges.<br><br><b><u>Autocrop</u></b><br>Automatically crops same-colour borders from the image.<br><br><u>Autocrop tolerance</u><br>A percentage value for the tolerance of colour difference between pixels.<br><br><u>Only autocrop frames</u><br>Only crop real frames (all sides must have the same border)<br><br><u>Symmetric autocrop</u><br>Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)<br><br><u>Autocrop keep border</u><br>The number of pixels of border to leave around the image.";
this.infoURL = "https://wikipedia.org/wiki/Cropping_(image)";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "X Position",
type: "number",
value: 0,
min: 0
},
{
name: "Y Position",
type: "number",
value: 0,
min: 0
},
{
name: "Width",
type: "number",
value: 10,
min: 1
},
{
name: "Height",
type: "number",
value: 10,
min: 1
},
{
name: "Autocrop",
type: "boolean",
value: false
},
{
name: "Autocrop tolerance (%)",
type: "number",
value: 0.02,
min: 0,
max: 100,
step: 0.01
},
{
name: "Only autocrop frames",
type: "boolean",
value: true
},
{
name: "Symmetric autocrop",
type: "boolean",
value: false
},
{
name: "Autocrop keep border (px)",
type: "number",
value: 0,
min: 0
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args;
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Cropping image...");
if (autocrop) {
image.autocrop({
tolerance: (autoTolerance / 100),
cropOnlyFrames: autoFrames,
cropSymmetric: autoSymmetric,
leaveBorder: autoBorder
});
} else {
image.crop(xPos, yPos, width, height);
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error cropping image. (${err})`);
}
}
/**
* Displays the cropped image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default CropImage;

View File

@@ -0,0 +1,125 @@
/**
* @author h345983745
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
/**
* DNS over HTTPS operation
*/
class DNSOverHTTPS extends Operation {
/**
* DNSOverHTTPS constructor
*/
constructor() {
super();
this.name = "DNS over HTTPS";
this.module = "Default";
this.description = [
"Takes a single domain name and performs a DNS lookup using DNS over HTTPS.",
"<br><br>",
"By default, <a href='https://developers.cloudflare.com/1.1.1.1/dns-over-https/'>Cloudflare</a> and <a href='https://developers.google.com/speed/public-dns/docs/dns-over-https'>Google</a> DNS over HTTPS services are supported.",
"<br><br>",
"Can be used with any service that supports the GET parameters <code>name</code> and <code>type</code>."
].join("\n");
this.infoURL = "https://wikipedia.org/wiki/DNS_over_HTTPS";
this.inputType = "string";
this.outputType = "JSON";
this.manualBake = true;
this.args = [
{
name: "Resolver",
type: "editableOption",
value: [
{
name: "Google",
value: "https://dns.google.com/resolve"
},
{
name: "Cloudflare",
value: "https://cloudflare-dns.com/dns-query"
}
]
},
{
name: "Request Type",
type: "option",
value: [
"A",
"AAAA",
"TXT",
"MX",
"DNSKEY",
"NS"
]
},
{
name: "Answer Data Only",
type: "boolean",
value: false
},
{
name: "Validate DNSSEC",
type: "boolean",
value: true
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {JSON}
*/
run(input, args) {
const [resolver, requestType, justAnswer, DNSSEC] = args;
let url = URL;
try {
url = new URL(resolver);
} catch (error) {
throw new OperationError(error.toString() +
"\n\nThis error could be caused by one of the following:\n" +
" - An invalid Resolver URL\n");
}
const params = {name: input, type: requestType, cd: DNSSEC};
url.search = new URLSearchParams(params);
return fetch(url, {headers: {"accept": "application/dns-json"}}).then(response => {
return response.json();
}).then(data => {
if (justAnswer) {
return extractData(data.Answer);
}
return data;
}).catch(e => {
throw new OperationError(`Error making request to ${url}\n${e.toString()}`);
});
}
}
/**
* Construct an array of just data from a DNS Answer section
*
* @private
* @param {JSON} data
* @returns {JSON}
*/
function extractData(data) {
if (typeof(data) == "undefined"){
return [];
} else {
const dataValues = [];
data.forEach(element => {
dataValues.push(element.data);
});
return dataValues;
}
}
export default DNSOverHTTPS;

View File

@@ -5,7 +5,8 @@
*/
import Operation from "../Operation";
import Magic from "../lib/Magic";
import {detectFileType} from "../lib/FileType";
import {FILE_SIGNATURES} from "../lib/FileSignatures";
/**
* Detect File Type operation
@@ -24,7 +25,13 @@ class DetectFileType extends Operation {
this.infoURL = "https://wikipedia.org/wiki/List_of_file_signatures";
this.inputType = "ArrayBuffer";
this.outputType = "string";
this.args = [];
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
return {
name: cat,
type: "boolean",
value: true
};
});
}
/**
@@ -34,17 +41,27 @@ class DetectFileType extends Operation {
*/
run(input, args) {
const data = new Uint8Array(input),
type = Magic.magicFileType(data);
categories = [];
if (!type) {
args.forEach((cat, i) => {
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
});
const types = detectFileType(data, categories);
if (!types.length) {
return "Unknown file type. Have you tried checking the entropy of this data to determine whether it might be encrypted or compressed?";
} else {
let output = "File extension: " + type.ext + "\n" +
"MIME type: " + type.mime;
let output = "";
if (type.desc && type.desc.length) {
output += "\nDescription: " + type.desc;
}
types.forEach(type => {
output += "File extension: " + type.extension + "\n" +
"MIME type: " + type.mime + "\n";
if (type.description && type.description.length) {
output += "\nDescription: " + type.description + "\n";
}
});
return output;
}

View File

@@ -0,0 +1,79 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64";
import jimp from "jimp";
/**
* Image Dither operation
*/
class DitherImage extends Operation {
/**
* DitherImage constructor
*/
constructor() {
super();
this.name = "Dither Image";
this.module = "Image";
this.description = "Apply a dither effect to an image.";
this.infoURL = "https://wikipedia.org/wiki/Dither";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Applying dither to image...");
image.dither565();
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error applying dither to image. (${err})`);
}
}
/**
* Displays the dithered image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default DitherImage;

View File

@@ -43,7 +43,7 @@ class Divide extends Operation {
*/
run(input, args) {
const val = div(createNumArray(input, args[0]));
return val instanceof BigNumber ? val : new BigNumber(NaN);
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
}
}

View File

@@ -0,0 +1,100 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import Utils from "../Utils";
import {scanForFileTypes, extractFile} from "../lib/FileType";
import {FILE_SIGNATURES} from "../lib/FileSignatures";
/**
* Extract Files operation
*/
class ExtractFiles extends Operation {
/**
* ExtractFiles constructor
*/
constructor() {
super();
this.name = "Extract Files";
this.module = "Default";
this.description = "TODO";
this.infoURL = "https://forensicswiki.org/wiki/File_Carving";
this.inputType = "ArrayBuffer";
this.outputType = "List<File>";
this.presentType = "html";
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
return {
name: cat,
type: "boolean",
value: cat === "Miscellaneous" ? false : true
};
}).concat([
{
name: "Ignore failed extractions",
type: "boolean",
value: "true"
}
]);
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {List<File>}
*/
run(input, args) {
const bytes = new Uint8Array(input),
categories = [],
ignoreFailedExtractions = args.pop(1);
args.forEach((cat, i) => {
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
});
// Scan for embedded files
const detectedFiles = scanForFileTypes(bytes, categories);
// Extract each file that we support
const files = [];
const errors = [];
detectedFiles.forEach(detectedFile => {
try {
files.push(extractFile(bytes, detectedFile.fileDetails, detectedFile.offset));
} catch (err) {
if (!ignoreFailedExtractions && err.message.indexOf("No extraction algorithm available") < 0) {
errors.push(
`Error while attempting to extract ${detectedFile.fileDetails.name} ` +
`at offset ${detectedFile.offset}:\n` +
`${err.message}`
);
}
}
});
if (errors.length) {
throw new OperationError(errors.join("\n\n"));
}
return files;
}
/**
* Displays the files in HTML for web apps.
*
* @param {File[]} files
* @returns {html}
*/
async present(files) {
return await Utils.displayFilesAsHTML(files);
}
}
export default ExtractFiles;

View File

@@ -0,0 +1,94 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64";
import jimp from "jimp";
/**
* Flip Image operation
*/
class FlipImage extends Operation {
/**
* FlipImage constructor
*/
constructor() {
super();
this.name = "Flip Image";
this.module = "Image";
this.description = "Flips an image along its X or Y axis.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Axis",
type: "option",
value: ["Horizontal", "Vertical"]
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [flipAxis] = args;
if (!isImage(input)) {
throw new OperationError("Invalid input file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Flipping image...");
switch (flipAxis){
case "Horizontal":
image.flip(true, false);
break;
case "Vertical":
image.flip(false, true);
break;
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error flipping image. (${err})`);
}
}
/**
* Displays the flipped image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default FlipImage;

View File

@@ -89,7 +89,7 @@ class Fork extends Operation {
// Run recipe over each tranche
for (i = 0; i < inputs.length; i++) {
// Baseline ing values for each tranche so that registers are reset
subOpList.forEach((op, i) => {
recipe.opList.forEach((op, i) => {
op.ingValues = JSON.parse(JSON.stringify(ingValues[i]));
});

View File

@@ -0,0 +1,39 @@
/**
* @author masq [github.cyberchef@masq.cc]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import Operation from "../Operation";
/**
* From Case Insensitive Regex operation
*/
class FromCaseInsensitiveRegex extends Operation {
/**
* FromCaseInsensitiveRegex constructor
*/
constructor() {
super();
this.name = "From Case Insensitive Regex";
this.module = "Default";
this.description = "Converts a case-insensitive regex string to a case sensitive regex string (no guarantee on it being the proper original casing) in case the i flag wasn't available at the time but now is, or you need it to be case-sensitive again.<br><br>e.g. <code>[mM][oO][zZ][iI][lL][lL][aA]/[0-9].[0-9] .*</code> becomes <code>Mozilla/[0-9].[0-9] .*</code>";
this.infoURL = "https://wikipedia.org/wiki/Regular_expression";
this.inputType = "string";
this.outputType = "string";
this.args = [];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
return input.replace(/\[[a-z]{2}\]/ig, m => m[1].toUpperCase() === m[2].toUpperCase() ? m[1] : m);
}
}
export default FromCaseInsensitiveRegex;

View File

@@ -1,44 +0,0 @@
/**
* @author gchq77703 []
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import Operation from "../Operation";
import geohash from "ngeohash";
/**
* From Geohash operation
*/
class FromGeohash extends Operation {
/**
* FromGeohash constructor
*/
constructor() {
super();
this.name = "From Geohash";
this.module = "Crypto";
this.description = "Converts Geohash strings into Lat/Long coordinates. For example, <code>ww8p1r4t8</code> becomes <code>37.8324,112.5584</code>.";
this.infoURL = "https://wikipedia.org/wiki/Geohash";
this.inputType = "string";
this.outputType = "string";
this.args = [];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
return input.split("\n").map(line => {
const coords = geohash.decode(line);
return [coords.latitude, coords.longitude].join(",");
}).join("\n");
}
}
export default FromGeohash;

View File

@@ -0,0 +1,70 @@
/**
* @author klaxon [klaxon@veyr.com]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { GenerateParagraphs, GenerateSentences, GenerateWords, GenerateBytes } from "../lib/LoremIpsum";
/**
* Generate Lorem Ipsum operation
*/
class GenerateLoremIpsum extends Operation {
/**
* GenerateLoremIpsum constructor
*/
constructor() {
super();
this.name = "Generate Lorem Ipsum";
this.module = "Default";
this.description = "Generate varying length lorem ipsum placeholder text.";
this.infoURL = "https://wikipedia.org/wiki/Lorem_ipsum";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Length",
"type": "number",
"value": "3"
},
{
"name": "Length in",
"type": "option",
"value": ["Paragraphs", "Sentences", "Words", "Bytes"]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [length, lengthType] = args;
if (length < 1){
throw new OperationError("Length must be greater than 0");
}
switch (lengthType) {
case "Paragraphs":
return GenerateParagraphs(length);
case "Sentences":
return GenerateSentences(length);
case "Words":
return GenerateWords(length);
case "Bytes":
return GenerateBytes(length);
default:
throw new OperationError("Invalid length type");
}
}
}
export default GenerateLoremIpsum;

View File

@@ -8,7 +8,7 @@ import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import qr from "qr-image";
import { toBase64 } from "../lib/Base64";
import Magic from "../lib/Magic";
import { isImage } from "../lib/FileType";
import Utils from "../Utils";
/**
@@ -100,9 +100,9 @@ class GenerateQRCode extends Operation {
if (format === "PNG") {
let dataURI = "data:";
const type = Magic.magicFileType(data);
if (type && type.mime.indexOf("image") === 0){
dataURI += type.mime + ";";
const mime = isImage(data);
if (mime){
dataURI += mime + ";";
} else {
throw new OperationError("Invalid PNG file generated by QR image");
}

View File

@@ -0,0 +1,103 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp";
/**
* Image Brightness / Contrast operation
*/
class ImageBrightnessContrast extends Operation {
/**
* ImageBrightnessContrast constructor
*/
constructor() {
super();
this.name = "Image Brightness / Contrast";
this.module = "Image";
this.description = "Adjust the brightness or contrast of an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Brightness",
type: "number",
value: 0,
min: -100,
max: 100
},
{
name: "Contrast",
type: "number",
value: 0,
min: -100,
max: 100
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [brightness, contrast] = args;
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (brightness !== 0) {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Changing image brightness...");
image.brightness(brightness / 100);
}
if (contrast !== 0) {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Changing image contrast...");
image.contrast(contrast / 100);
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error adjusting image brightness or contrast. (${err})`);
}
}
/**
* Displays the image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default ImageBrightnessContrast;

View File

@@ -0,0 +1,94 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp";
/**
* Image Filter operation
*/
class ImageFilter extends Operation {
/**
* ImageFilter constructor
*/
constructor() {
super();
this.name = "Image Filter";
this.module = "Image";
this.description = "Applies a greyscale or sepia filter to an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Filter type",
type: "option",
value: [
"Greyscale",
"Sepia"
]
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [filterType] = args;
if (!isImage(input)){
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Applying " + filterType.toLowerCase() + " filter to image...");
if (filterType === "Greyscale") {
image.greyscale();
} else {
image.sepia();
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error applying filter to image. (${err})`);
}
}
/**
* Displays the blurred image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default ImageFilter;

View File

@@ -0,0 +1,129 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp";
/**
* Image Hue/Saturation/Lightness operation
*/
class ImageHueSaturationLightness extends Operation {
/**
* ImageHueSaturationLightness constructor
*/
constructor() {
super();
this.name = "Image Hue/Saturation/Lightness";
this.module = "Image";
this.description = "Adjusts the hue / saturation / lightness (HSL) values of an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Hue",
type: "number",
value: 0,
min: -360,
max: 360
},
{
name: "Saturation",
type: "number",
value: 0,
min: -100,
max: 100
},
{
name: "Lightness",
type: "number",
value: 0,
min: -100,
max: 100
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [hue, saturation, lightness] = args;
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (hue !== 0) {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Changing image hue...");
image.colour([
{
apply: "hue",
params: [hue]
}
]);
}
if (saturation !== 0) {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Changing image saturation...");
image.colour([
{
apply: "saturate",
params: [saturation]
}
]);
}
if (lightness !== 0) {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Changing image lightness...");
image.colour([
{
apply: "lighten",
params: [lightness]
}
]);
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error adjusting image hue / saturation / lightness. (${err})`);
}
}
/**
* Displays the image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default ImageHueSaturationLightness;

View File

@@ -0,0 +1,89 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp";
/**
* Image Opacity operation
*/
class ImageOpacity extends Operation {
/**
* ImageOpacity constructor
*/
constructor() {
super();
this.name = "Image Opacity";
this.module = "Image";
this.description = "Adjust the opacity of an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Opacity (%)",
type: "number",
value: 100,
min: 0,
max: 100
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [opacity] = args;
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Changing image opacity...");
image.opacity(opacity / 100);
const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error changing image opacity. (${err})`);
}
}
/**
* Displays the image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default ImageOpacity;

View File

@@ -0,0 +1,79 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64";
import jimp from "jimp";
/**
* Invert Image operation
*/
class InvertImage extends Operation {
/**
* InvertImage constructor
*/
constructor() {
super();
this.name = "Invert Image";
this.module = "Image";
this.description = "Invert the colours of an image.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
if (!isImage(input)) {
throw new OperationError("Invalid input file format.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Inverting image...");
image.invert();
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error inverting image. (${err})`);
}
}
/**
* Displays the inverted image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default InvertImage;

View File

@@ -43,7 +43,7 @@ class Mean extends Operation {
*/
run(input, args) {
const val = mean(createNumArray(input, args[0]));
return val instanceof BigNumber ? val : new BigNumber(NaN);
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
}
}

View File

@@ -43,7 +43,7 @@ class Median extends Operation {
*/
run(input, args) {
const val = median(createNumArray(input, args[0]));
return val instanceof BigNumber ? val : new BigNumber(NaN);
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
}
}

View File

@@ -44,7 +44,7 @@ class Multiply extends Operation {
*/
run(input, args) {
const val = multi(createNumArray(input, args[0]));
return val instanceof BigNumber ? val : new BigNumber(NaN);
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
}
}

View File

@@ -0,0 +1,70 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64";
import jimp from "jimp";
/**
* Normalise Image operation
*/
class NormaliseImage extends Operation {
/**
* NormaliseImage constructor
*/
constructor() {
super();
this.name = "Normalise Image";
this.module = "Image";
this.description = "Normalise the image colours.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType= "html";
this.args = [];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
const image = await jimp.read(Buffer.from(input));
image.normalize();
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
}
/**
* Displays the normalised image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default NormaliseImage;

View File

@@ -6,7 +6,7 @@
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import Magic from "../lib/Magic";
import { isImage } from "../lib/FileType";
import jsqr from "jsqr";
import jimp from "jimp";
@@ -42,64 +42,61 @@ class ParseQRCode extends Operation {
* @returns {string}
*/
async run(input, args) {
const type = Magic.magicFileType(input);
const [normalise] = args;
// Make sure that the input is an image
if (type && type.mime.indexOf("image") === 0) {
let image = input;
if (!isImage(input)) throw new OperationError("Invalid file type.");
if (normalise) {
// Process the image to be easier to read by jsqr
// Disables the alpha channel
// Sets the image default background to white
// Normalises the image colours
// Makes the image greyscale
// Converts image to a JPEG
image = await new Promise((resolve, reject) => {
jimp.read(Buffer.from(input))
.then(image => {
image
.rgba(false)
.background(0xFFFFFFFF)
.normalize()
.greyscale()
.getBuffer(jimp.MIME_JPEG, (error, result) => {
resolve(result);
});
})
.catch(err => {
reject(new OperationError("Error reading the image file."));
});
});
}
let image = input;
if (image instanceof OperationError) {
throw image;
}
return new Promise((resolve, reject) => {
jimp.read(Buffer.from(image))
if (normalise) {
// Process the image to be easier to read by jsqr
// Disables the alpha channel
// Sets the image default background to white
// Normalises the image colours
// Makes the image greyscale
// Converts image to a JPEG
image = await new Promise((resolve, reject) => {
jimp.read(Buffer.from(input))
.then(image => {
if (image.bitmap != null) {
const qrData = jsqr(image.bitmap.data, image.getWidth(), image.getHeight());
if (qrData != null) {
resolve(qrData.data);
} else {
reject(new OperationError("Couldn't read a QR code from the image."));
}
} else {
reject(new OperationError("Error reading the image file."));
}
image
.rgba(false)
.background(0xFFFFFFFF)
.normalize()
.greyscale()
.getBuffer(jimp.MIME_JPEG, (error, result) => {
resolve(result);
});
})
.catch(err => {
reject(new OperationError("Error reading the image file."));
});
});
} else {
throw new OperationError("Invalid file type.");
}
if (image instanceof OperationError) {
throw image;
}
return new Promise((resolve, reject) => {
jimp.read(Buffer.from(image))
.then(image => {
if (image.bitmap != null) {
const qrData = jsqr(image.bitmap.data, image.getWidth(), image.getHeight());
if (qrData != null) {
resolve(qrData.data);
} else {
reject(new OperationError("Couldn't read a QR code from the image."));
}
} else {
reject(new OperationError("Error reading the image file."));
}
})
.catch(err => {
reject(new OperationError("Error reading the image file."));
});
});
}
}

View File

@@ -181,8 +181,8 @@ class ParseX509Certificate extends Operation {
Serial number: ${new r.BigInteger(sn, 16).toString()} (0x${sn})
Algorithm ID: ${cert.getSignatureAlgorithmField()}
Validity
Not Before: ${nbDate} (dd-mm-yy hh:mm:ss) (${cert.getNotBefore()})
Not After: ${naDate} (dd-mm-yy hh:mm:ss) (${cert.getNotAfter()})
Not Before: ${nbDate} (dd-mm-yyyy hh:mm:ss) (${cert.getNotBefore()})
Not After: ${naDate} (dd-mm-yyyy hh:mm:ss) (${cert.getNotAfter()})
Issuer
${issuerStr}
Subject
@@ -206,12 +206,15 @@ ${extensions}`;
* @returns {string}
*/
function formatDate (dateStr) {
return dateStr[4] + dateStr[5] + "/" +
dateStr[2] + dateStr[3] + "/" +
dateStr[0] + dateStr[1] + " " +
dateStr[6] + dateStr[7] + ":" +
if (dateStr.length === 13) { // UTC Time
dateStr = (dateStr[0] < "5" ? "20" : "19") + dateStr;
}
return dateStr[6] + dateStr[7] + "/" +
dateStr[4] + dateStr[5] + "/" +
dateStr[0] + dateStr[1] + dateStr[2] + dateStr[3] + " " +
dateStr[8] + dateStr[9] + ":" +
dateStr[10] + dateStr[11];
dateStr[10] + dateStr[11] + ":" +
dateStr[12] + dateStr[13];
}
export default ParseX509Certificate;

View File

@@ -9,7 +9,7 @@ import { fromHex } from "../lib/Hex";
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import Utils from "../Utils";
import Magic from "../lib/Magic";
import { isType, detectFileType } from "../lib/FileType";
/**
* PlayMedia operation
@@ -66,8 +66,7 @@ class PlayMedia extends Operation {
// Determine file type
const type = Magic.magicFileType(input);
if (!(type && /^audio|video/.test(type.mime))) {
if (!isType(/^(audio|video)/, input)) {
throw new OperationError("Invalid or unrecognised file type");
}
@@ -84,15 +83,15 @@ class PlayMedia extends Operation {
async present(data) {
if (!data.length) return "";
const type = Magic.magicFileType(data);
const matches = /^audio|video/.exec(type.mime);
const types = detectFileType(data);
const matches = /^audio|video/.exec(types[0].mime);
if (!matches) {
throw new OperationError("Invalid file type");
}
const dataURI = `data:${type.mime};base64,${toBase64(data)}`;
const dataURI = `data:${types[0].mime};base64,${toBase64(data)}`;
const element = matches[0];
let html = `<${element} src='${dataURI}' type='${type.mime}' controls>`;
let html = `<${element} src='${dataURI}' type='${types[0].mime}' controls>`;
html += "<p>Unsupported media type.</p>";
html += `</${element}>`;
return html;

View File

@@ -228,40 +228,29 @@ function regexList (input, regex, displayTotal, matches, captureGroups) {
function regexHighlight (input, regex, displayTotal) {
let output = "",
title = "",
m,
hl = 1,
i = 0,
total = 0;
while ((m = regex.exec(input))) {
// Moves pointer when an empty string is matched (prevents infinite loop)
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
output = input.replace(regex, (match, ...args) => {
args.pop(); // Throw away full string
const offset = args.pop(),
groups = args;
// Add up to match
output += Utils.escapeHtml(input.slice(i, m.index));
title = `Offset: ${m.index}\n`;
if (m.length > 1) {
title = `Offset: ${offset}\n`;
if (groups.length) {
title += "Groups:\n";
for (let n = 1; n < m.length; ++n) {
title += `\t${n}: ${m[n]}\n`;
for (let i = 0; i < groups.length; i++) {
title += `\t${i+1}: ${Utils.escapeHtml(groups[i] || "")}\n`;
}
}
// Add match with highlighting
output += "<span class='hl"+hl+"' title='"+title+"'>" + Utils.escapeHtml(m[0]) + "</span>";
// Switch highlight
hl = hl === 1 ? 2 : 1;
i = regex.lastIndex;
total++;
}
// Add all after final match
output += Utils.escapeHtml(input.slice(i, input.length));
return `<span class='hl${hl}' title='${title}'>${Utils.escapeHtml(match)}</span>`;
});
if (displayTotal)
output = "Total found: " + total + "\n\n" + output;

View File

@@ -9,7 +9,7 @@ import { fromHex } from "../lib/Hex";
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import Utils from "../Utils";
import Magic from "../lib/Magic";
import {isImage} from "../lib/FileType";
/**
* Render Image operation
@@ -72,8 +72,7 @@ class RenderImage extends Operation {
}
// Determine file type
const type = Magic.magicFileType(input);
if (!(type && type.mime.indexOf("image") === 0)) {
if (!isImage(input)) {
throw new OperationError("Invalid file type");
}
@@ -92,9 +91,9 @@ class RenderImage extends Operation {
let dataURI = "data:";
// Determine file type
const type = Magic.magicFileType(data);
if (type && type.mime.indexOf("image") === 0) {
dataURI += type.mime + ";";
const mime = isImage(data);
if (mime) {
dataURI += mime + ";";
} else {
throw new OperationError("Invalid file type");
}

View File

@@ -0,0 +1,138 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64.mjs";
import jimp from "jimp";
/**
* Resize Image operation
*/
class ResizeImage extends Operation {
/**
* ResizeImage constructor
*/
constructor() {
super();
this.name = "Resize Image";
this.module = "Image";
this.description = "Resizes an image to the specified width and height values.";
this.infoURL = "https://wikipedia.org/wiki/Image_scaling";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Width",
type: "number",
value: 100,
min: 1
},
{
name: "Height",
type: "number",
value: 100,
min: 1
},
{
name: "Unit type",
type: "option",
value: ["Pixels", "Percent"]
},
{
name: "Maintain aspect ratio",
type: "boolean",
value: false
},
{
name: "Resizing algorithm",
type: "option",
value: [
"Nearest Neighbour",
"Bilinear",
"Bicubic",
"Hermite",
"Bezier"
],
defaultIndex: 1
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
let width = args[0],
height = args[1];
const unit = args[2],
aspect = args[3],
resizeAlg = args[4];
const resizeMap = {
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
"Bilinear": jimp.RESIZE_BILINEAR,
"Bicubic": jimp.RESIZE_BICUBIC,
"Hermite": jimp.RESIZE_HERMITE,
"Bezier": jimp.RESIZE_BEZIER
};
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (unit === "Percent") {
width = image.getWidth() * (width / 100);
height = image.getHeight() * (height / 100);
}
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Resizing image...");
if (aspect) {
image.scaleToFit(width, height, resizeMap[resizeAlg]);
} else {
image.resize(width, height, resizeMap[resizeAlg]);
}
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error resizing image. (${err})`);
}
}
/**
* Displays the resized image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default ResizeImage;

View File

@@ -0,0 +1,87 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import { isImage } from "../lib/FileType";
import { toBase64 } from "../lib/Base64";
import jimp from "jimp";
/**
* Rotate Image operation
*/
class RotateImage extends Operation {
/**
* RotateImage constructor
*/
constructor() {
super();
this.name = "Rotate Image";
this.module = "Image";
this.description = "Rotates an image by the specified number of degrees.";
this.infoURL = "";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
name: "Rotation amount (degrees)",
type: "number",
value: 90
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
async run(input, args) {
const [degrees] = args;
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
let image;
try {
image = await jimp.read(Buffer.from(input));
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Rotating image...");
image.rotate(degrees);
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
return [...imageBuffer];
} catch (err) {
throw new OperationError(`Error rotating image. (${err})`);
}
}
/**
* Displays the rotated image using HTML for web apps
* @param {byteArray} data
* @returns {html}
*/
present(data) {
if (!data.length) return "";
const type = isImage(data);
if (!type) {
throw new OperationError("Invalid file type.");
}
return `<img src="data:${type};base64,${toBase64(data)}">`;
}
}
export default RotateImage;

View File

@@ -6,7 +6,8 @@
import Operation from "../Operation";
import Utils from "../Utils";
import Magic from "../lib/Magic";
import {scanForFileTypes} from "../lib/FileType";
import {FILE_SIGNATURES} from "../lib/FileSignatures";
/**
* Scan for Embedded Files operation
@@ -25,13 +26,13 @@ class ScanForEmbeddedFiles extends Operation {
this.infoURL = "https://wikipedia.org/wiki/List_of_file_signatures";
this.inputType = "ArrayBuffer";
this.outputType = "string";
this.args = [
{
"name": "Ignore common byte sequences",
"type": "boolean",
"value": true
}
];
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
return {
name: cat,
type: "boolean",
value: cat === "Miscellaneous" ? false : true
};
});
}
/**
@@ -41,43 +42,33 @@ class ScanForEmbeddedFiles extends Operation {
*/
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",
type,
numFound = 0,
numCommonFound = 0;
const ignoreCommon = args[0],
commonExts = ["ico", "ttf", ""],
numFound = 0;
const categories = [],
data = new Uint8Array(input);
for (let i = 0; i < data.length; i++) {
type = Magic.magicFileType(data.slice(i));
if (type) {
if (ignoreCommon && commonExts.indexOf(type.ext) > -1) {
numCommonFound++;
continue;
}
numFound++;
output += "\nOffset " + i + " (0x" + Utils.hex(i) + "):\n" +
" File extension: " + type.ext + "\n" +
" MIME type: " + type.mime + "\n";
args.forEach((cat, i) => {
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
});
if (type.desc && type.desc.length) {
output += " Description: " + type.desc + "\n";
const types = scanForFileTypes(data, categories);
if (types.length) {
types.forEach(type => {
numFound++;
output += "\nOffset " + type.offset + " (0x" + Utils.hex(type.offset) + "):\n" +
" File extension: " + type.fileDetails.extension + "\n" +
" MIME type: " + type.fileDetails.mime + "\n";
if (type.fileDetails.description && type.fileDetails.description.length) {
output += " Description: " + type.fileDetails.description + "\n";
}
}
});
}
if (numFound === 0) {
output += "\nNo embedded files were found.";
}
if (numCommonFound > 0) {
output += "\n\n" + numCommonFound;
output += numCommonFound === 1 ?
" file type was detected that has a common byte sequence. This is likely to be a false positive." :
" file types were detected that have common byte sequences. These are likely to be false positives.";
output += " Run this operation with the 'Ignore common byte sequences' option unchecked to see details.";
}
return output;
}

View File

@@ -26,12 +26,12 @@ class Split extends Operation {
this.args = [
{
"name": "Split delimiter",
"type": "editableOption",
"type": "editableOptionShort",
"value": SPLIT_DELIM_OPTIONS
},
{
"name": "Join delimiter",
"type": "editableOption",
"type": "editableOptionShort",
"value": JOIN_DELIM_OPTIONS
}
];

View File

@@ -0,0 +1,102 @@
/**
* @author Matt C [matt@artemisbot.uk]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import Utils from "../Utils";
import {isImage} from "../lib/FileType";
import jimp from "jimp";
/**
* Split Colour Channels operation
*/
class SplitColourChannels extends Operation {
/**
* SplitColourChannels constructor
*/
constructor() {
super();
this.name = "Split Colour Channels";
this.module = "Image";
this.description = "Splits the given image into its red, green and blue colour channels.";
this.infoURL = "https://wikipedia.org/wiki/Channel_(digital_image)";
this.inputType = "byteArray";
this.outputType = "List<File>";
this.presentType = "html";
this.args = [];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {List<File>}
*/
async run(input, args) {
// Make sure that the input is an image
if (!isImage(input)) throw new OperationError("Invalid file type.");
const parsedImage = await jimp.read(Buffer.from(input));
const red = new Promise(async (resolve, reject) => {
try {
const split = parsedImage
.clone()
.color([
{apply: "blue", params: [-255]},
{apply: "green", params: [-255]}
])
.getBufferAsync(jimp.MIME_PNG);
resolve(new File([new Uint8Array((await split).values())], "red.png", {type: "image/png"}));
} catch (err) {
reject(new OperationError(`Could not split red channel: ${err}`));
}
});
const green = new Promise(async (resolve, reject) => {
try {
const split = parsedImage.clone()
.color([
{apply: "red", params: [-255]},
{apply: "blue", params: [-255]},
]).getBufferAsync(jimp.MIME_PNG);
resolve(new File([new Uint8Array((await split).values())], "green.png", {type: "image/png"}));
} catch (err) {
reject(new OperationError(`Could not split green channel: ${err}`));
}
});
const blue = new Promise(async (resolve, reject) => {
try {
const split = parsedImage
.color([
{apply: "red", params: [-255]},
{apply: "green", params: [-255]},
]).getBufferAsync(jimp.MIME_PNG);
resolve(new File([new Uint8Array((await split).values())], "blue.png", {type: "image/png"}));
} catch (err) {
reject(new OperationError(`Could not split blue channel: ${err}`));
}
});
return await Promise.all([red, green, blue]);
}
/**
* Displays the files in HTML for web apps.
*
* @param {File[]} files
* @returns {html}
*/
async present(files) {
return await Utils.displayFilesAsHTML(files);
}
}
export default SplitColourChannels;

View File

@@ -44,7 +44,7 @@ class StandardDeviation extends Operation {
*/
run(input, args) {
const val = stdDev(createNumArray(input, args[0]));
return val instanceof BigNumber ? val : new BigNumber(NaN);
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
}

View File

@@ -0,0 +1,153 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import XRegExp from "xregexp";
import Operation from "../Operation";
import Recipe from "../Recipe";
import Dish from "../Dish";
/**
* Subsection operation
*/
class Subsection extends Operation {
/**
* Subsection constructor
*/
constructor() {
super();
this.name = "Subsection";
this.flowControl = true;
this.module = "Regex";
this.description = "Select a part of the input data using a regular expression (regex), and run all subsequent operations on each match separately.<br><br>You can use up to one capture group, where the recipe will only be run on the data in the capture group. If there's more than one capture group, only the first one will be operated on.";
this.infoURL = "";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Section (regex)",
"type": "string",
"value": ""
},
{
"name": "Case sensitive matching",
"type": "boolean",
"value": true
},
{
"name": "Global matching",
"type": "boolean",
"value": true
},
{
"name": "Ignore errors",
"type": "boolean",
"value": false
}
];
}
/**
* @param {Object} state - The current state of the recipe.
* @param {number} state.progress - The current position in the recipe.
* @param {Dish} state.dish - The Dish being operated on
* @param {Operation[]} state.opList - The list of operations in the recipe
* @returns {Object} - The updated state of the recipe
*/
async run(state) {
const opList = state.opList,
inputType = opList[state.progress].inputType,
outputType = opList[state.progress].outputType,
input = await state.dish.get(inputType),
ings = opList[state.progress].ingValues,
[section, caseSensitive, global, ignoreErrors] = ings,
subOpList = [];
if (input && section !== "") {
// Create subOpList for each tranche to operate on
// all remaining operations unless we encounter a Merge
for (let i = state.progress + 1; i < opList.length; i++) {
if (opList[i].name === "Merge" && !opList[i].disabled) {
break;
} else {
subOpList.push(opList[i]);
}
}
let flags = "",
inOffset = 0,
output = "",
m,
progress = 0;
if (!caseSensitive) flags += "i";
if (global) flags += "g";
const regex = new XRegExp(section, flags),
recipe = new Recipe();
recipe.addOperations(subOpList);
state.forkOffset += state.progress + 1;
// Take a deep(ish) copy of the ingredient values
const ingValues = subOpList.map(op => JSON.parse(JSON.stringify(op.ingValues)));
let matched = false;
// Run recipe over each match
while ((m = regex.exec(input))) {
matched = true;
// Add up to match
let matchStr = m[0];
if (m.length === 1) { // No capture groups
output += input.slice(inOffset, m.index);
inOffset = m.index + m[0].length;
} else if (m.length >= 2) {
matchStr = m[1];
// Need to add some of the matched string that isn't in the capture group
output += input.slice(inOffset, m.index + m[0].indexOf(m[1]));
// Set i to be after the end of the first capture group
inOffset = m.index + m[0].indexOf(m[1]) + m[1].length;
}
// Baseline ing values for each tranche so that registers are reset
recipe.opList.forEach((op, i) => {
op.ingValues = JSON.parse(JSON.stringify(ingValues[i]));
});
const dish = new Dish();
dish.set(matchStr, inputType);
try {
progress = await recipe.execute(dish, 0, state);
} catch (err) {
if (!ignoreErrors) {
throw err;
}
progress = err.progress + 1;
}
output += await dish.get(outputType);
if (!regex.global) break;
}
// If no matches were found, advance progress to after a Merge op
// Otherwise, the operations below Subsection will be run on all the input data
if (!matched) {
state.progress += subOpList.length + 1;
}
output += input.slice(inOffset);
state.progress += progress;
state.dish.set(output, outputType);
}
return state;
}
}
export default Subsection;

View File

@@ -44,7 +44,7 @@ class Subtract extends Operation {
*/
run(input, args) {
const val = sub(createNumArray(input, args[0]));
return val instanceof BigNumber ? val : new BigNumber(NaN);
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
}
}

View File

@@ -44,7 +44,7 @@ class Sum extends Operation {
*/
run(input, args) {
const val = sum(createNumArray(input, args[0]));
return val instanceof BigNumber ? val : new BigNumber(NaN);
return BigNumber.isBigNumber(val) ? val : new BigNumber(NaN);
}
}

View File

@@ -20,7 +20,7 @@ class ToBase64 extends Operation {
this.name = "To Base64";
this.module = "Default";
this.description = "Base64 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers.<br><br>This operation decodes data from an ASCII Base64 string back into its raw format.<br><br>e.g. <code>aGVsbG8=</code> becomes <code>hello</code>";
this.description = "Base64 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers.<br><br>This operation encodes raw data into an ASCII Base64 string.<br><br>e.g. <code>hello</code> becomes <code>aGVsbG8=</code>";
this.infoURL = "https://wikipedia.org/wiki/Base64";
this.inputType = "ArrayBuffer";
this.outputType = "string";

View File

@@ -0,0 +1,39 @@
/**
* @author masq [github.cyberchef@masq.cc]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import Operation from "../Operation";
/**
* To Case Insensitive Regex operation
*/
class ToCaseInsensitiveRegex extends Operation {
/**
* ToCaseInsensitiveRegex constructor
*/
constructor() {
super();
this.name = "To Case Insensitive Regex";
this.module = "Default";
this.description = "Converts a case-sensitive regex string into a case-insensitive regex string in case the i flag is unavailable to you.<br><br>e.g. <code>Mozilla/[0-9].[0-9] .*</code> becomes <code>[mM][oO][zZ][iI][lL][lL][aA]/[0-9].[0-9] .*</code>";
this.infoURL = "https://wikipedia.org/wiki/Regular_expression";
this.inputType = "string";
this.outputType = "string";
this.args = [];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
return input.replace(/[a-z]/ig, m => `[${m.toLowerCase()}${m.toUpperCase()}]`);
}
}
export default ToCaseInsensitiveRegex;

View File

@@ -1,53 +0,0 @@
/**
* @author gchq77703 []
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import Operation from "../Operation";
import geohash from "ngeohash";
/**
* To Geohash operation
*/
class ToGeohash extends Operation {
/**
* ToGeohash constructor
*/
constructor() {
super();
this.name = "To Geohash";
this.module = "Crypto";
this.description = "Converts Lat/Long coordinates into a Geohash string. For example, <code>37.8324,112.5584</code> becomes <code>ww8p1r4t8</code>.";
this.infoURL = "https://wikipedia.org/wiki/Geohash";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Precision",
type: "number",
value: 9
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [precision] = args;
return input.split("\n").map(line => {
line = line.replace(/ /g, "");
if (line === "") return "";
return geohash.encode(...line.split(",").map(num => parseFloat(num)), precision);
}).join("\n");
}
}
export default ToGeohash;

View File

@@ -38,7 +38,9 @@ class ToMessagePack extends Operation {
if (ENVIRONMENT_IS_WORKER()) {
return notepack.encode(input);
} else {
return notepack.encode(input).buffer;
const res = notepack.encode(input);
// Safely convert from Node Buffer to ArrayBuffer using the correct view of the data
return (new Uint8Array(res)).buffer;
}
} catch (err) {
throw new OperationError(`Could not encode JSON to MessagePack: ${err}`);

View File

@@ -57,7 +57,7 @@ class ToTable extends Operation {
const [cellDelims, rowDelims, firstRowHeader, format] = args;
// Process the input into a nested array of elements.
const tableData = Utils.parseCSV(input, cellDelims.split(""), rowDelims.split(""));
const tableData = Utils.parseCSV(Utils.escapeHtml(input), cellDelims.split(""), rowDelims.split(""));
if (!tableData.length) return "";

View File

@@ -6,6 +6,7 @@
import Operation from "../Operation";
import Utils from "../Utils";
import Stream from "../lib/Stream";
/**
* Untar operation
@@ -41,38 +42,6 @@ class Untar extends Operation {
* @returns {List<File>}
*/
run(input, args) {
const Stream = function(input) {
this.bytes = input;
this.position = 0;
};
Stream.prototype.getBytes = function(bytesToGet) {
const newPosition = this.position + bytesToGet;
const bytes = this.bytes.slice(this.position, newPosition);
this.position = newPosition;
return bytes;
};
Stream.prototype.readString = function(numBytes) {
let result = "";
for (let i = this.position; i < this.position + numBytes; i++) {
const currentByte = this.bytes[i];
if (currentByte === 0) break;
result += String.fromCharCode(currentByte);
}
this.position += numBytes;
return result;
};
Stream.prototype.readInt = function(numBytes, base) {
const string = this.readString(numBytes);
return parseInt(string, base);
};
Stream.prototype.hasMore = function() {
return this.position < this.bytes.length;
};
const stream = new Stream(input),
files = [];
@@ -85,7 +54,7 @@ class Untar extends Operation {
ownerUID: stream.readString(8),
ownerGID: stream.readString(8),
size: parseInt(stream.readString(12), 8), // Octal
lastModTime: new Date(1000 * stream.readInt(12, 8)), // Octal
lastModTime: new Date(1000 * parseInt(stream.readString(12), 8)), // Octal
checksum: stream.readString(8),
type: stream.readString(1),
linkedFileName: stream.readString(100),

View File

@@ -57,7 +57,7 @@ class XPathExpression extends Operation {
let nodes;
try {
nodes = xpath.select(query, doc);
nodes = xpath.parse(query).select({ node: doc, allowAnyNamespaceForNoPrefix: true });
} catch (err) {
throw new OperationError(`Invalid XPath. Details:\n${err.message}.`);
}

View File

@@ -0,0 +1,122 @@
/**
* @author Matt C [matt@artemisbot.uk]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import Yara from "libyara-wasm";
/**
* YARA Rules operation
*/
class YARARules extends Operation {
/**
* YARARules constructor
*/
constructor() {
super();
this.name = "YARA Rules";
this.module = "Yara";
this.description = "YARA is a tool developed at VirusTotal, primarily aimed at helping malware researchers to identify and classify malware samples. It matches based on rules specified by the user containing textual or binary patterns and a boolean expression. For help on writing rules, see the <a href='https://yara.readthedocs.io/en/latest/writingrules.html'>YARA documentation.</a>";
this.infoURL = "https://wikipedia.org/wiki/YARA";
this.inputType = "ArrayBuffer";
this.outputType = "string";
this.args = [
{
name: "Rules",
type: "text",
value: "",
rows: 5
},
{
name: "Show strings",
type: "boolean",
value: false
},
{
name: "Show string lengths",
type: "boolean",
value: false
},
{
name: "Show metadata",
type: "boolean",
value: false
},
{
name: "Show counts",
type: "boolean",
value: true
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Instantiating YARA...");
const [rules, showStrings, showLengths, showMeta, showCounts] = args;
return new Promise((resolve, reject) => {
Yara().then(yara => {
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Converting data for YARA.");
let matchString = "";
const inpArr = new Uint8Array(input); // Turns out embind knows that JS uint8array <==> C++ std::string
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Running YARA matching.");
const resp = yara.run(inpArr, rules);
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Processing data.");
if (resp.compileErrors.size() > 0) {
for (let i = 0; i < resp.compileErrors.size(); i++) {
const compileError = resp.compileErrors.get(i);
if (!compileError.warning) {
reject(new OperationError(`Error on line ${compileError.lineNumber}: ${compileError.message}`));
} else {
matchString += `Warning on line ${compileError.lineNumber}: ${compileError.message}`;
}
}
}
const matchedRules = resp.matchedRules;
for (let i = 0; i < matchedRules.size(); i++) {
const rule = matchedRules.get(i);
const matches = rule.resolvedMatches;
let meta = "";
if (showMeta && rule.metadata.size() > 0) {
meta += " [";
for (let j = 0; j < rule.metadata.size(); j++) {
meta += `${rule.metadata.get(j).identifier}: ${rule.metadata.get(j).data}, `;
}
meta = meta.slice(0, -2) + "]";
}
const countString = showCounts ? `${matches.size()} time${matches.size() > 1 ? "s" : ""}` : "";
if (matches.size() === 0 || !(showStrings || showLengths)) {
matchString += `Input matches rule "${rule.ruleName}"${meta}${countString.length > 0 ? ` ${countString}`: ""}.\n`;
} else {
matchString += `Rule "${rule.ruleName}"${meta} matches (${countString}):\n`;
for (let j = 0; j < matches.size(); j++) {
const match = matches.get(j);
if (showStrings || showLengths) {
matchString += `Pos ${match.location}, ${showLengths ? `length ${match.matchLength}, ` : ""}identifier ${match.stringIdentifier}${showStrings ? `, data: "${match.data}"` : ""}\n`;
}
}
}
}
resolve(matchString);
});
});
}
}
export default YARARules;

View File

@@ -1,3 +1,31 @@
/*-------------------------------------------------------------------------------------------------------------------------
Created by Damian Recoskie (https://github.com/Recoskie/X86-64-Disassembler-JS)
& exported for CyberChef by Matt [me@mitt.dev]
---------------------------------------------------------------------------------------------------------------------------
MIT License
Copyright (c) 2019 Damian Recoskie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------
Binary byte code array.
---------------------------------------------------------------------------------------------------------------------------
@@ -3525,7 +3553,7 @@ export function LoadBinCode( HexStr )
var len = HexStr.length;
for( var i = 0, el = 0, Sing = 0, int32 = 0; i < len; i += 8 )
for( var i = 0, el = 0, Sign = 0, int32 = 0; i < len; i += 8 )
{
//It is faster to read 8 hex digits at a time if possible.
@@ -3541,22 +3569,22 @@ export function LoadBinCode( HexStr )
//The variable sing corrects the unusable sing bits during the 4 byte rotation algorithm.
Sing = int32;
Sign = int32;
//Remove the Sing bit value if active for when the number is changed to int32 during rotation.
//Remove the Sign bit value if active for when the number is changed to int32 during rotation.
int32 ^= int32 & 0x80000000;
//Rotate the 32 bit int so that each number is put in order in the BinCode array. Add the Sing Bit positions back though each rotation.
//Rotate the 32 bit int so that each number is put in order in the BinCode array. Add the Sign Bit positions back though each rotation.
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
BinCode[el++] = ( ( ( Sing >> 24 ) & 0x80 ) | int32 ) & 0xFF;
BinCode[el++] = ( ( ( Sign >> 24 ) & 0x80 ) | int32 ) & 0xFF;
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
BinCode[el++] = ( ( ( Sing >> 16 ) & 0x80 ) | int32 ) & 0xFF;
BinCode[el++] = ( ( ( Sign >> 16 ) & 0x80 ) | int32 ) & 0xFF;
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
BinCode[el++] = ( ( ( Sing >> 8 ) & 0x80 ) | int32 ) & 0xFF;
BinCode[el++] = ( ( ( Sign >> 8 ) & 0x80 ) | int32 ) & 0xFF;
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
BinCode[el++] = ( ( Sing & 0x80 ) | int32 ) & 0xFF;
BinCode[el++] = ( ( Sign & 0x80 ) | int32 ) & 0xFF;
}
//Remove elements past the Number of bytes in HexStr because int 32 is always 4 bytes it is possible to end in an uneven number.
@@ -3585,7 +3613,6 @@ function NextByte()
if ( CodePos < BinCode.length ) //If not out of bounds.
{
//Convert current byte to String, and pad.
var t;
( ( t = BinCode[CodePos++].toString(16) ).length === 1) && ( t = "0" + t );
@@ -3947,11 +3974,11 @@ function DecodeImmediate( type, BySize, SizeSetting )
var Pad32 = 0, Pad64 = 0;
//*Initialize the Sing value that is only set for Negative, or Positive Relative displacements.
//*Initialize the Sign value that is only set for Negative, or Positive Relative displacements.
var Sing = 0;
var Sign = 0;
//*Initialize the Sing Extend variable size as 0 Some Immediate numbers Sing extend.
//*Initialize the Sign Extend variable size as 0 Some Immediate numbers Sign extend.
var Extend = 0;
@@ -4017,21 +4044,33 @@ function DecodeImmediate( type, BySize, SizeSetting )
Pad32 = ( Math.min( BitMode, 1 ) << 2 ) + 4; Pad64 = Math.max( Math.min( BitMode, 2 ), 1 ) << 3;
//Add the 32 bit section to V32.
//Carry bit to 64 bit section.
var C64 = 0;
//Relative size.
var n = Math.min( 0x100000000, Math.pow( 2, 4 << ( S + 1 ) ) );
//Sign bit adjust.
if( V32 >= ( n >> 1 ) ) { V32 -= n; }
//Add position.
V32 += Pos32;
//Remove carry bit and add it to C64.
var C64 = 0; V32 += Pos32;
( C64 = ( ( V32 ) >= 0x100000000 ) ) && ( V32 -= 0x100000000 );
//Do not carry to 64 if address is 32, and below.
if ( S <= 2 ) { C64 = false; }
//If bit mode is 16 bits only the first 16 bits are used, or if Size Attribute is 16 bit.
//Add the 64 bit position plus carry.
( BitMode <= 0 || SizeAttrSelect <= 0 ) && ( V32 &= 0xFFFF );
//Adjust the 32 bit relative address section if it was not cropped to 16 bit's.
( C64 = ( ( V32 ) > 0xFFFFFFFF ) ) && ( V32 -= 0x100000000 );
//Add the 64 bit address section if in 64 bit mode, or higher.
( BitMode >= 2 ) && ( ( V64 += Pos64 + C64 ) > 0xFFFFFFFF ) && ( V64 -= 0x100000000 );
( ( V64 += Pos64 + C64 ) > 0xFFFFFFFF ) && ( V64 -= 0x100000000 );
}
/*---------------------------------------------------------------------------------------------------------------------------
@@ -4052,9 +4091,9 @@ function DecodeImmediate( type, BySize, SizeSetting )
var Center = 2 * ( 1 << ( n << 3 ) - 2 );
//By default the Sing is Positive.
//By default the Sign is Positive.
Sing = 1;
Sign = 1;
/*-------------------------------------------------------------------------------------------------------------------------
Calculate the VSIB displacement size if it is a VSIB Disp8.
@@ -4074,9 +4113,9 @@ function DecodeImmediate( type, BySize, SizeSetting )
V32 = Center * 2 - V32;
//The Sing is negative.
//The Sign is negative.
Sing = 2;
Sign = 2;
}
}
@@ -4110,7 +4149,7 @@ function DecodeImmediate( type, BySize, SizeSetting )
//*Return the Imm.
return ( ( Sing > 0 ? ( Sing > 1 ? "-" : "+" ) : "" ) + Imm.toUpperCase() );
return ( ( Sign > 0 ? ( Sign > 1 ? "-" : "+" ) : "" ) + Imm.toUpperCase() );
}

View File

@@ -238,12 +238,18 @@ class App {
/**
* Sets up the adjustable splitter to allow the user to resize areas of the page.
*
* @param {boolean} [minimise=false] - Set this flag if attempting to minimuse frames to 0 width
*/
initialiseSplitter() {
initialiseSplitter(minimise=false) {
if (this.columnSplitter) this.columnSplitter.destroy();
if (this.ioSplitter) this.ioSplitter.destroy();
this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
sizes: [20, 30, 50],
minSize: [240, 370, 450],
minSize: minimise ? [0, 0, 0] : [240, 370, 450],
gutterSize: 4,
expandToMin: false,
onDrag: function() {
this.manager.recipe.adjustWidth();
}.bind(this)
@@ -251,7 +257,8 @@ class App {
this.ioSplitter = Split(["#input", "#output"], {
direction: "vertical",
gutterSize: 4
gutterSize: 4,
minSize: minimise ? [0, 0] : [100, 100]
});
this.resetLayout();

View File

@@ -4,6 +4,8 @@
* @license Apache-2.0
*/
import Utils from "../core/Utils";
/**
* Object to handle the creation of operation ingredients.
*/
@@ -25,10 +27,14 @@ class HTMLIngredient {
this.value = config.value;
this.disabled = config.disabled || false;
this.hint = config.hint || false;
this.rows = config.rows || false;
this.target = config.target;
this.defaultIndex = config.defaultIndex || 0;
this.toggleValues = config.toggleValues;
this.id = "ing-" + this.app.nextIngId();
this.min = (typeof config.min === "number") ? config.min : "";
this.max = (typeof config.max === "number") ? config.max : "";
this.step = config.step || 1;
}
@@ -100,6 +106,9 @@ class HTMLIngredient {
id="${this.id}"
arg-name="${this.name}"
value="${this.value}"
min="${this.min}"
max="${this.max}"
step="${this.step}"
${this.disabled ? "disabled" : ""}>
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
</div>`;
@@ -155,7 +164,7 @@ class HTMLIngredient {
} else if ((m = this.value[i].name.match(/\[\/([a-z0-9 -()^]+)\]/i))) {
html += "</optgroup>";
} else {
html += `<option populate-value="${this.value[i].value}">${this.value[i].name}</option>`;
html += `<option populate-value="${Utils.escapeHtml(this.value[i].value)}">${this.value[i].name}</option>`;
}
}
html += `</select>
@@ -165,6 +174,35 @@ class HTMLIngredient {
this.manager.addDynamicListener("#" + this.id, "change", this.populateOptionChange, this);
break;
case "editableOption":
html += `<div class="form-group input-group">
<label for="${this.id}" class="bmd-label-floating">${this.name}</label>
<input type="text"
class="form-control arg"
id="${this.id}"
arg-name="${this.name}"
value="${this.value[this.defaultIndex].value}"
${this.disabled ? "disabled" : ""}>
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
<div class="input-group-append">
<button type="button"
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown"
data-boundary="scrollParent"
aria-haspopup="true"
aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu editable-option-menu">`;
for (i = 0; i < this.value.length; i++) {
html += `<a class="dropdown-item" href="#" value="${this.value[i].value}">${this.value[i].name}</a>`;
}
html += `</div>
</div>
</div>`;
this.manager.addDynamicListener(".editable-option-menu a", "click", this.editableOptionClick, this);
break;
case "editableOptionShort":
html += `<div class="form-group input-group inline">
<label for="${this.id}" class="bmd-label-floating inline">${this.name}</label>
<input type="text"
@@ -200,6 +238,7 @@ class HTMLIngredient {
class="form-control arg"
id="${this.id}"
arg-name="${this.name}"
rows="${this.rows ? this.rows : 3}"
${this.disabled ? "disabled" : ""}>${this.value}</textarea>
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
</div>`;

View File

@@ -243,14 +243,22 @@ class InputWaiter {
}
if (file) {
this.closeFile();
this.loaderWorker = new LoaderWorker();
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
this.loaderWorker.postMessage({"file": file});
this.set(file);
this.loadFile(file);
}
}
/**
* Handler for open input button events
* Loads the opened data into the input textarea
*
* @param {event} e
*/
inputOpen(e) {
e.preventDefault();
const file = e.srcElement.files[0];
this.loadFile(file);
}
/**
* Handler for messages sent back by the LoaderWorker.
@@ -306,6 +314,22 @@ class InputWaiter {
}
/**
* Loads a file into the input.
*
* @param {File} file
*/
loadFile(file) {
if (file) {
this.closeFile();
this.loaderWorker = new LoaderWorker();
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
this.loaderWorker.postMessage({"file": file});
this.set(file);
}
}
/**
* Handler for clear IO events.
* Resets the input, output and info areas.

View File

@@ -137,12 +137,16 @@ class Manager {
this.addDynamicListener("#rec-list li.operation > div", "dblclick", this.recipe.operationChildDblclick, this.recipe);
this.addDynamicListener("#rec-list .dropdown-menu.toggle-dropdown a", "click", this.recipe.dropdownToggleClick, this.recipe);
this.addDynamicListener("#rec-list", "operationremove", this.recipe.opRemove.bind(this.recipe));
this.addDynamicListener("textarea.arg", "dragover", this.recipe.textArgDragover, this.recipe);
this.addDynamicListener("textarea.arg", "dragleave", this.recipe.textArgDragLeave, this.recipe);
this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe);
// Input
this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input);
this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
document.getElementById("clr-io").addEventListener("click", this.input.clearIoClick.bind(this.input));
this.addListeners("#open-file", "change", this.input.inputOpen, this.input);
this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input);
this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input);
this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input);
@@ -169,6 +173,7 @@ class Manager {
this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
// Options
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));

View File

@@ -319,6 +319,7 @@ class OutputWaiter {
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) {
this.app.initialiseSplitter(true);
this.app.columnSplitter.collapse(0);
this.app.columnSplitter.collapse(1);
this.app.ioSplitter.collapse(0);
@@ -328,6 +329,7 @@ class OutputWaiter {
} else {
$(el).attr("data-original-title", "Maximise output pane");
el.querySelector("i").innerHTML = "fullscreen";
this.app.initialiseSplitter(false);
this.app.resetLayout();
}
}
@@ -476,7 +478,7 @@ class OutputWaiter {
*/
showMagicButton(opSequence, result, recipeConfig) {
const magicButton = document.getElementById("magic");
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.truncate(result, 30)}"</span>`);
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.escapeHtml(Utils.truncate(result), 30)}"</span>`);
magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
magicButton.classList.remove("hidden");
}
@@ -492,6 +494,24 @@ class OutputWaiter {
magicButton.setAttribute("data-original-title", "Magic!");
}
/**
* Handler for extract file events.
*
* @param {Event} e
*/
async extractFileClick(e) {
e.preventDefault();
e.stopPropagation();
const el = e.target.nodeName === "I" ? e.target.parentNode : e.target;
const blobURL = el.getAttribute("blob-url");
const fileName = el.getAttribute("file-name");
const blob = await fetch(blobURL).then(r => r.blob());
this.manager.input.loadFile(new File([blob], fileName, {type: blob.type}));
}
}
export default OutputWaiter;

View File

@@ -376,6 +376,7 @@ class RecipeWaiter {
}
}
/**
* Adds the specified operation to the recipe.
*
@@ -454,6 +455,75 @@ class RecipeWaiter {
}
/**
* Handler for text argument dragover events.
* Gives the user a visual cue to show that items can be dropped here.
*
* @param {event} e
*/
textArgDragover (e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;
e.stopPropagation();
e.preventDefault();
e.target.closest("textarea.arg").classList.add("dropping-file");
}
/**
* Handler for text argument dragleave events.
* Removes the visual cue.
*
* @param {event} e
*/
textArgDragLeave (e) {
e.stopPropagation();
e.preventDefault();
e.target.classList.remove("dropping-file");
}
/**
* Handler for text argument drop events.
* Loads the dragged data into the argument textarea.
*
* @param {event} e
*/
textArgDrop(e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;
e.stopPropagation();
e.preventDefault();
const targ = e.target;
const file = e.dataTransfer.files[0];
const text = e.dataTransfer.getData("Text");
targ.classList.remove("dropping-file");
if (text) {
targ.value = text;
return;
}
if (file) {
const reader = new FileReader();
const self = this;
reader.onload = function (e) {
targ.value = e.target.result;
// Trigger floating label move
const changeEvent = new Event("change");
targ.dispatchEvent(changeEvent);
window.dispatchEvent(self.manager.statechange);
};
reader.readAsText(file);
}
}
/**
* Sets register values.
*
@@ -479,6 +549,7 @@ class RecipeWaiter {
op.insertAdjacentHTML("beforeend", registerListEl);
}
/**
* Adjusts the number of ingredient columns as the width of the recipe changes.
*/
@@ -490,20 +561,25 @@ class RecipeWaiter {
this.ingredientChildRuleID = null;
// Find relevant rules in the stylesheet
for (const i in document.styleSheets[0].cssRules) {
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients") {
this.ingredientRuleID = i;
}
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients > div") {
this.ingredientChildRuleID = i;
// try/catch for chrome 64+ CORS error on cssRules.
try {
for (const i in document.styleSheets[0].cssRules) {
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients") {
this.ingredientRuleID = i;
}
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients > div") {
this.ingredientChildRuleID = i;
}
}
} catch (e) {
// Do nothing.
}
}
if (!this.ingredientRuleID || !this.ingredientChildRuleID) return;
const ingredientRule = document.styleSheets[0].cssRules[this.ingredientRuleID],
ingredientChildRule = document.styleSheets[0].cssRules[this.ingredientChildRuleID];
const ingredientRule = document.styleSheets[0].cssRules[this.ingredientRuleID];
const ingredientChildRule = document.styleSheets[0].cssRules[this.ingredientChildRuleID];
if (recList.clientWidth < 450) {
ingredientRule.style.gridTemplateColumns = "auto auto";

View File

@@ -225,6 +225,10 @@
<div class="title no-select">
<label for="input-text">Input</label>
<span class="float-right">
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input" onclick="document.getElementById('open-file').click();">
<i class="material-icons">input</i>
<input type="file" id="open-file" style="display: none">
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="clr-io" data-toggle="tooltip" title="Clear input and output">
<i class="material-icons">delete</i>
</button>
@@ -267,7 +271,7 @@
<i class="material-icons">content_copy</i>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Move output to input">
<i class="material-icons">loop</i>
<i class="material-icons">open_in_browser</i>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="undo-switch" data-toggle="tooltip" title="Undo" disabled="disabled">
<i class="material-icons">undo</i>

View File

@@ -91,3 +91,7 @@
padding-right: 6px;
padding-left: 6px;
}
#files .card-header .float-right a:hover {
text-decoration: none;
}

View File

@@ -15,7 +15,7 @@
width: 100%;
height: var(--controls-height);
bottom: 0;
padding: 10px;
padding: 0;
padding-top: 12px;
border-top: 1px solid var(--primary-border-colour);
background-color: var(--secondary-background-colour);

View File

@@ -123,6 +123,7 @@
.dropping-file {
border: 5px dashed var(--drop-file-border-colour) !important;
margin: -5px;
}
#stale-indicator {

View File

@@ -1,55 +0,0 @@
/**
* To Geohash tests
*
* @author gchq77703
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import TestRegister from "../../TestRegister";
TestRegister.addTests([
{
name: "From Geohash",
input: "ww8p1r4t8",
expectedOutput: "37.83238649368286,112.55838632583618",
recipeConfig: [
{
op: "From Geohash",
args: [],
},
],
},
{
name: "From Geohash",
input: "ww8p1r",
expectedOutput: "37.83416748046875,112.5604248046875",
recipeConfig: [
{
op: "From Geohash",
args: [],
},
],
},
{
name: "From Geohash",
input: "ww8",
expectedOutput: "37.265625,113.203125",
recipeConfig: [
{
op: "From Geohash",
args: [],
},
],
},
{
name: "From Geohash",
input: "w",
expectedOutput: "22.5,112.5",
recipeConfig: [
{
op: "From Geohash",
args: [],
},
],
},
]);

View File

@@ -1,55 +0,0 @@
/**
* To Geohash tests
*
* @author gchq77703
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import TestRegister from "../../TestRegister";
TestRegister.addTests([
{
name: "To Geohash",
input: "37.8324,112.5584",
expectedOutput: "ww8p1r4t8",
recipeConfig: [
{
op: "To Geohash",
args: [9],
},
],
},
{
name: "To Geohash",
input: "37.9324,-112.2584",
expectedOutput: "9w8pv3ruj",
recipeConfig: [
{
op: "To Geohash",
args: [9],
},
],
},
{
name: "To Geohash",
input: "37.8324,112.5584",
expectedOutput: "ww8",
recipeConfig: [
{
op: "To Geohash",
args: [3],
},
],
},
{
name: "To Geohash",
input: "37.9324,-112.2584",
expectedOutput: "9w8pv3rujxy5b99",
recipeConfig: [
{
op: "To Geohash",
args: [15],
},
],
},
]);

182
tests/browser/nightwatch.js Normal file
View File

@@ -0,0 +1,182 @@
/**
* Tests to ensure that the app loads correctly in a reasonable time and that operations can be run.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
module.exports = {
before: browser => {
browser
.resizeWindow(1280, 800)
.url(browser.launchUrl);
},
"Loading screen": browser => {
// Check that the loading screen appears and then disappears within a reasonable time
browser
.waitForElementVisible("#preloader", 300)
.waitForElementNotPresent("#preloader", 10000);
},
"App loaded": browser => {
browser.useCss();
// Check that various important elements are loaded
browser.expect.element("#operations").to.be.visible;
browser.expect.element("#recipe").to.be.visible;
browser.expect.element("#input").to.be.present;
browser.expect.element("#output").to.be.present;
browser.expect.element(".op-list").to.be.present;
browser.expect.element("#rec-list").to.be.visible;
browser.expect.element("#controls").to.be.visible;
browser.expect.element("#input-text").to.be.visible;
browser.expect.element("#output-text").to.be.visible;
},
"Operations loaded": browser => {
browser.useXpath();
// Check that an operation in every category has been populated
browser.expect.element("//li[contains(@class, 'operation') and text()='To Base64']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='To Binary']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='AES Decrypt']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='PEM to Hex']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Power Set']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Parse IP range']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Remove Diacritics']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Sort']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='To UNIX Timestamp']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Extract dates']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Gzip']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Keccak']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='JSON Beautify']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Detect File Type']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Play Media']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Disassemble x86']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Register']").to.be.present;
},
"Recipe can be run": browser => {
const toHex = "//li[contains(@class, 'operation') and text()='To Hex']";
const op = "#rec-list .operation .op-title";
// Check that operation is visible
browser
.useXpath()
.expect.element(toHex).to.be.visible;
// Add it to the recipe by double clicking
browser
.useXpath()
.moveToElement(toHex, 10, 10)
.useCss()
.waitForElementVisible(".popover-body", 1000)
.doubleClick();
// Confirm that it has been added to the recipe
browser
.useCss()
.waitForElementVisible(op)
.expect.element(op).text.to.contain("To Hex");
// Enter input
browser
.useCss()
.setValue("#input-text", "Don't Panic.")
.click("#bake");
// Check output
browser
.useCss()
.waitForElementNotVisible("#stale-indicator", 1000)
.expect.element("#output-text").to.have.value.that.equals("44 6f 6e 27 74 20 50 61 6e 69 63 2e");
// Clear recipe
browser
.useCss()
.moveToElement(op, 10, 10)
.waitForElementNotPresent(".popover-body", 1000)
.click("#clr-recipe")
.waitForElementNotPresent(op);
},
"Test every module": browser => {
browser.useCss();
// BSON
loadOp("BSON deserialise", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Ciphers
loadOp("AES Encrypt", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Code
loadOp("XPath expression", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Compression
loadOp("Gzip", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Crypto
loadOp("MD5", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Default
loadOp("Fork", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Diff
loadOp("Diff", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Encodings
loadOp("Encode text", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Image
loadOp("Extract EXIF", browser)
.waitForElementNotVisible("#output-loader", 5000);
// PGP
loadOp("PGP Encrypt", browser)
.waitForElementNotVisible("#output-loader", 5000);
// PublicKey
loadOp("Hex to PEM", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Regex
loadOp("Strings", browser)
.waitForElementNotVisible("#output-loader", 5000);
// Shellcode
loadOp("Disassemble x86", browser)
.waitForElementNotVisible("#output-loader", 5000);
// URL
loadOp("URL Encode", browser)
.waitForElementNotVisible("#output-loader", 5000);
// UserAgent
loadOp("Parse User Agent", browser)
.waitForElementNotVisible("#output-loader", 5000);
},
after: browser => {
browser.end();
}
};
/**
* Clears the current recipe and loads a new operation.
*
* @param {string} opName
* @param {Browser} browser
*/
function loadOp(opName, browser) {
return browser
.useCss()
.click("#clr-recipe")
.urlHash("op=" + opName);
}

View File

@@ -8,7 +8,7 @@
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import Chef from "../src/core/Chef";
import Chef from "../../src/core/Chef";
(function() {
/**

View File

@@ -24,64 +24,68 @@ global.ENVIRONMENT_IS_WEB = function() {
};
import TestRegister from "./TestRegister";
import "./tests/operations/BCD";
import "./tests/operations/BSON";
import "./tests/operations/Base58";
import "./tests/operations/Base64";
import "./tests/operations/Base62";
import "./tests/operations/BitwiseOp";
import "./tests/operations/ByteRepr";
import "./tests/operations/CartesianProduct";
import "./tests/operations/CharEnc";
import "./tests/operations/Checksum";
import "./tests/operations/Ciphers";
import "./tests/operations/Code";
import "./tests/operations/Comment";
import "./tests/operations/Compress";
import "./tests/operations/ConditionalJump";
import "./tests/operations/Crypt";
import "./tests/operations/CSV";
import "./tests/operations/DateTime";
import "./tests/operations/ExtractEmailAddresses";
import "./tests/operations/Fork";
import "./tests/operations/FromDecimal";
import "./tests/operations/FromGeohash";
import "./tests/operations/Hash";
import "./tests/operations/HaversineDistance";
import "./tests/operations/Hexdump";
import "./tests/operations/Image";
import "./tests/operations/Jump";
import "./tests/operations/JSONBeautify";
import "./tests/operations/JSONMinify";
import "./tests/operations/JWTDecode";
import "./tests/operations/JWTSign";
import "./tests/operations/JWTVerify";
import "./tests/operations/MS";
import "./tests/operations/Magic";
import "./tests/operations/MorseCode";
import "./tests/operations/NetBIOS";
import "./tests/operations/OTP";
import "./tests/operations/PGP";
import "./tests/operations/PHP";
import "./tests/operations/ParseIPRange";
import "./tests/operations/ParseQRCode";
import "./tests/operations/PowerSet";
import "./tests/operations/Regex";
import "./tests/operations/Register";
import "./tests/operations/RemoveDiacritics";
import "./tests/operations/Rotate";
import "./tests/operations/SeqUtils";
import "./tests/operations/SetDifference";
import "./tests/operations/SetIntersection";
import "./tests/operations/SetUnion";
import "./tests/operations/StrUtils";
import "./tests/operations/SymmetricDifference";
import "./tests/operations/TextEncodingBruteForce";
import "./tests/operations/ToGeohash";
import "./tests/operations/TranslateDateTimeFormat";
import "./tests/operations/Magic";
import "./tests/operations/ParseTLV";
import "./tests/operations/Media";
import "./tests/BCD";
import "./tests/BSON";
import "./tests/Base58";
import "./tests/Base64";
import "./tests/Base62";
import "./tests/BitwiseOp";
import "./tests/ByteRepr";
import "./tests/CartesianProduct";
import "./tests/CharEnc";
import "./tests/Checksum";
import "./tests/Ciphers";
import "./tests/Code";
import "./tests/Comment";
import "./tests/Compress";
import "./tests/ConditionalJump";
import "./tests/Crypt";
import "./tests/CSV";
import "./tests/DateTime";
import "./tests/ExtractEmailAddresses";
import "./tests/Fork";
import "./tests/FromDecimal";
import "./tests/Hash";
import "./tests/HaversineDistance";
import "./tests/Hexdump";
import "./tests/Image";
import "./tests/Jump";
import "./tests/JSONBeautify";
import "./tests/JSONMinify";
import "./tests/JWTDecode";
import "./tests/JWTSign";
import "./tests/JWTVerify";
import "./tests/MS";
import "./tests/Magic";
import "./tests/MorseCode";
import "./tests/NetBIOS";
import "./tests/OTP";
import "./tests/PGP";
import "./tests/PHP";
import "./tests/ParseIPRange";
import "./tests/ParseQRCode";
import "./tests/PowerSet";
import "./tests/Regex";
import "./tests/Register";
import "./tests/RemoveDiacritics";
import "./tests/Rotate";
import "./tests/SeqUtils";
import "./tests/SetDifference";
import "./tests/SetIntersection";
import "./tests/SetUnion";
import "./tests/StrUtils";
import "./tests/SymmetricDifference";
import "./tests/TextEncodingBruteForce";
import "./tests/TranslateDateTimeFormat";
import "./tests/Magic";
import "./tests/ParseTLV";
import "./tests/Media";
import "./tests/ToFromInsensitiveRegex";
import "./tests/YARA.mjs";
import "./tests/ConvertCoordinateFormat";
// Cannot test operations that use the File type yet
//import "./tests/SplitColourChannels";
let allTestsPassing = true;
const testStatusCounts = {

View File

@@ -5,7 +5,7 @@
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import TestRegister from "../../TestRegister";
import TestRegister from "../TestRegister";
TestRegister.addTests([
{

View File

@@ -6,7 +6,7 @@
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import TestRegister from "../../TestRegister";
import TestRegister from "../TestRegister";
TestRegister.addTests([
{

View File

@@ -6,7 +6,7 @@
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import TestRegister from "../../TestRegister";
import TestRegister from "../TestRegister";
TestRegister.addTests([
{

View File

@@ -7,7 +7,7 @@
* @license Apache-2.0
*/
import TestRegister from "../../TestRegister";
import TestRegister from "../TestRegister";
TestRegister.addTests([
{

View File

@@ -6,7 +6,7 @@
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import TestRegister from "../../TestRegister";
import TestRegister from "../TestRegister";
const ALL_BYTES = [
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",

Some files were not shown because too many files have changed in this diff Show More