diff --git a/README.md b/README.md
index e0fe3c2c..519ca4af 100755
--- a/README.md
+++ b/README.md
@@ -6,7 +6,30 @@

[](https://github.com/gchq/CyberChef/blob/master/LICENSE)
[](https://gitter.im/gchq/CyberChef?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+This is a fork of original CyberChef by GCHQ. This project has implemented my own operations for the daily SoC tasks. Please feel free to clone/test/share/contribute it.
+## Features
+
+- opeartion **nTcpdump**: tcpdump hexdump convert
+- operation **From 0x[Hex]**: e.g. 0x217e21
to !~!
+- operation **From char(hex)**:
+ - e.g. chr(33)
to !
+ - This operation supports char() and chr()
+ - Combining the usage of **From 0x[Hex]** and **From char(hex)** can decode chr(0x3333)
to !
+- operation **HTTP gzip decrypt**: Decrypt gzip payload in HTTP
+
+## Todo
+
+- Operation SQL comment strip function
+- Adding more support to char(hex, hex)
+- Adding support of themes
+
+
+---
+
+## Original project info
+
+####*The Cyber Swiss Army Knife*
#### *The Cyber Swiss Army Knife*
diff --git a/prod_image_list.txt b/prod_image_list.txt
new file mode 100644
index 00000000..e3ab8d31
--- /dev/null
+++ b/prod_image_list.txt
@@ -0,0 +1,26 @@
+./src/web/static/images/bug-16x16.png
+./src/web/static/images/clean-16x16.png
+./src/web/static/images/cook_male-32x32.png
+./src/web/static/images/cyberchef-128x128.png
+./src/web/static/images/download-24x24.png
+./src/web/static/images/erase-16x16.png
+./src/web/static/images/favourite-16x16.png
+./src/web/static/images/favourite-24x24.png
+./src/web/static/images/fork_me.png
+./src/web/static/images/gitter-badge.svg
+./src/web/static/images/help-16x16.png
+./src/web/static/images/help-22x22.png
+./src/web/static/images/layout-16x16.png
+./src/web/static/images/maximise-16x16.png
+./src/web/static/images/open_yellow-16x16.png
+./src/web/static/images/open_yellow-24x24.png
+./src/web/static/images/recycle-16x16.png
+./src/web/static/images/save-22x22.png
+./src/web/static/images/save_as-16x16.png
+./src/web/static/images/settings-22x22.png
+./src/web/static/images/speech-16x16.png
+./src/web/static/images/step-16x16.png
+./src/web/static/images/switch-16x16.png
+./src/web/static/images/thumb_down-16x16.png
+./src/web/static/images/thumb_up-16x16.png
+./src/web/static/images/undo-16x16.png
\ No newline at end of file
diff --git a/sample_recipe.md b/sample_recipe.md
new file mode 100644
index 00000000..f0583bdd
--- /dev/null
+++ b/sample_recipe.md
@@ -0,0 +1,16 @@
+# Ready-To-Go Sample Recipe
+
+## tcpdump (no ASCII) Daily Use
+
+1. tcpdump (no ASCII) convert to plaintext (e.g. 0x0000: 0020 0A0D
)
+2. URL decode loop (e.g. %3d
to =
)
+3. From HTML Entity (e.g. &
to &
)
+4. From 0x[Hex] (e.g. 0x33
to !
)
+5. From Char(Hex) (e.g. char(33)
to !
)
+
+ [{"op":"From nTcpdump","args":[]},
+ {"op":"URL Decode","args":[]},
+ {"op":"Conditional Jump","args":["\\%([0-9a-fA-F]{2,})","-1","45"]},
+ {"op":"From HTML Entity","args":[]},
+ {"op":"From 0x[Hex]","args":[]},
+ {"op":"From Char(Hex)","args":[]}]
\ No newline at end of file
diff --git a/src/core/Utils.js b/src/core/Utils.js
index e7243201..1d5c1654 100755
--- a/src/core/Utils.js
+++ b/src/core/Utils.js
@@ -340,6 +340,68 @@ const Utils = {
},
+ /**
+ * Translates a hex string into an array of bytes.
+ *
+ * @param {string} hexStr
+ * @returns {byteArray}
+ *
+ * @example
+ * // returns [0xfe, 0x09, 0xa7]
+ * Utils.hexToByteArray("fe09a7");
+ */
+ hexToByteArray: function(hexStr) {
+ // TODO: Handle errors i.e. input string is not hex
+ if (!hexStr) return [];
+ hexStr = hexStr.replace(/\s+/g, "");
+ const byteArray = [];
+ for (let i = 0; i < hexStr.length; i += 2) {
+ byteArray.push(parseInt(hexStr.substr(i, 2), 16));
+ }
+ return byteArray;
+ },
+
+
+ /**
+ * Translates an array of bytes to a hex string.
+ *
+ * @param {byteArray} byteArray
+ * @returns {string}
+ *
+ * @example
+ * // returns "fe09a7"
+ * Utils.byteArrayToHex([0xfe, 0x09, 0xa7]);
+ */
+ byteArrayToHex: function(byteArray) {
+ if (!byteArray) return "";
+ let hexStr = "";
+ for (let i = 0; i < byteArray.length; i++) {
+ hexStr += Utils.hex(byteArray[i]) + " ";
+ }
+ return hexStr.slice(0, hexStr.length-1);
+ },
+
+
+ /**
+ * Translates an array of bytes to a hex string.
+ *
+ * @param {byteArray} byteArray
+ * @returns {string}
+ *
+ * @example
+ * // returns "fe09a7"
+ * Utils.byteArrayToHex([0xfe, 0x09, 0xa7]);
+ */
+ byteArrayToHexNoSpace: function(byteArray) {
+ if (!byteArray) return "";
+ let hexStr = "";
+ for (let i = 0; i < byteArray.length; i++) {
+ hexStr += Utils.hex(byteArray[i]);
+ }
+ return hexStr.slice(0, hexStr.length-1);
+ },
+
+
/**
* Converts a string to a byte array.
* Treats the string as UTF-8 if any values are over 255.
diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js
index e2ee57cf..f05885b3 100755
--- a/src/core/config/Categories.js
+++ b/src/core/config/Categories.js
@@ -27,6 +27,8 @@ const Categories = [
ops: [
"To Hexdump",
"From Hexdump",
+ "From 0x[Hex]",
+ "From Char(Hex)",
"To Hex",
"From Hex",
"To Charcode",
@@ -304,6 +306,14 @@ const Categories = [
"To Kebab case",
]
},
+ {
+ name: "Packets",
+ ops: [
+ "From Tcpdump",
+ "HTTP gzip Decrypt",
+ "Strip TCP Headers",
+ ]
+ },
{
name: "Other",
ops: [
diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js
index 2f1d6781..ae1a7c28 100755
--- a/src/core/config/OperationConfig.js
+++ b/src/core/config/OperationConfig.js
@@ -37,7 +37,7 @@ import StrUtils from "../operations/StrUtils.js";
import Tidy from "../operations/Tidy.js";
import Unicode from "../operations/Unicode.js";
import URL_ from "../operations/URL.js";
-
+import Packets from "../operations/Packets.js";
/**
* Type definition for an OpConf.
@@ -520,6 +520,24 @@ const OperationConfig = {
}
]
},
+ "From 0x[Hex]": {
+ module: "Default",
+ description: "Converts a hexadecimal byte string back into a its raw value.
e.g. 0x217e21
becomes the UTF-8 encoded string !~!
",
+ highlight: ByteRepr.highlightFrom,
+ highlightReverse: ByteRepr.highlightTo,
+ inputType: "string",
+ outputType: "string",
+ args: []
+ },
+ "From Char(Hex)": {
+ module: "Default",
+ description: "Converts a hexadecimal byte string back into a its raw value.
e.g. chr(33)
becomes the UTF-8 encoded string !
",
+ highlight: ByteRepr.highlightFrom,
+ highlightReverse: ByteRepr.highlightTo,
+ inputType: "string",
+ outputType: "string",
+ args: []
+ },
"Sum": {
module: "Default",
description: "Adds together a list of numbers. If an item in the string is not a number it is excluded from the list.
e.g. 0x0a 8 .5
becomes 18.5
",
@@ -1851,6 +1869,24 @@ const OperationConfig = {
outputType: "string",
args: []
},
+ "HTTP gzip Decrypt": {
+ module: "Compression",
+ description: "Decrypts Gzip payload from a request or response and returning plaintext of the header and decrypted payload.
Arguments:
Library: The library used for decoding GZIP data.
Threshold: The threshold value for searching non-GZIP data. It has to be at least 8.",
+ inputType: "byteArray",
+ outputType: "byteArray",
+ args: [
+ {
+ name: "Library",
+ type: "option",
+ value: Compress.HTTP_GZIP_OPTION
+ },
+ {
+ name: "Threshold",
+ type: "number",
+ value: Compress.HTTP_GZIP_THRESHOLD
+ },
+ ]
+ },
"Parse User Agent": {
module: "HTTP",
description: "Attempts to identify and categorise information contained in a user-agent string.",
@@ -3964,6 +4000,36 @@ const OperationConfig = {
}
]
},
+ "From Tcpdump": {
+ module: "Packets",
+ description: "[DEPRECATED] Converts Tcpdump hex to string",
+ inputType: "string",
+ outputType: "byteArray",
+ args: []
+ },
+ "Strip TCP Headers": {
+ module: "Packets",
+ description: "Remove selected TCP headers from hexstream using Regular Expressions.
Ethernet Header: /^(([0-9a-f]{4} ){6,8}0800 )/igm
IP Header: /^((45[0-9a-f]{2} ([0-9a-f]{4} ){9}))/igm
TCP Header: /^([0-9a-f]{4} ){6}((80[0-9a-f]{2} ([0-9a-f]{4} ?){9})|(50[0-9a-f]{2} ([0-9a-f]{4} ?){3}))/igm
",
+ inputType: "string",
+ outputType: "string",
+ args: [
+ {
+ name: "Ethernet Header",
+ type: "boolean",
+ value: Packets.STRIP_ETHERNET_HEADER,
+ },
+ {
+ name: "IP Header",
+ type: "boolean",
+ value: Packets.STRIP_IP_HEADER,
+ },
+ {
+ name: "Ethernet Header",
+ type: "boolean",
+ value: Packets.STRIP_TCP_HEADER,
+ },
+ ]
+ },
"PHP Deserialize": {
module: "Default",
description: "Deserializes PHP serialized data, outputting keyed arrays as JSON.
This function does not support object
tags.
Example:
a:2:{s:1:"a";i:10;i:0;a:1:{s:2:"ab";b:1;}}
becomes
{"a": 10,0: {"ab": true}}
Output valid JSON: JSON doesn't support integers as keys, whereas PHP serialization does. Enabling this will cast these integers to strings. This will also escape backslashes.",
diff --git a/src/core/config/modules/Compression.js b/src/core/config/modules/Compression.js
index aa8d074f..9d55b400 100644
--- a/src/core/config/modules/Compression.js
+++ b/src/core/config/modules/Compression.js
@@ -15,17 +15,18 @@ import Compress from "../../operations/Compress.js";
let OpModules = typeof self === "undefined" ? {} : self.OpModules || {};
OpModules.Compression = {
- "Raw Deflate": Compress.runRawDeflate,
- "Raw Inflate": Compress.runRawInflate,
- "Zlib Deflate": Compress.runZlibDeflate,
- "Zlib Inflate": Compress.runZlibInflate,
- "Gzip": Compress.runGzip,
- "Gunzip": Compress.runGunzip,
- "Zip": Compress.runPkzip,
- "Unzip": Compress.runPkunzip,
- "Bzip2 Decompress": Compress.runBzip2Decompress,
- "Tar": Compress.runTar,
- "Untar": Compress.runUntar,
+ "Raw Deflate": Compress.runRawDeflate,
+ "Raw Inflate": Compress.runRawInflate,
+ "Zlib Deflate": Compress.runZlibDeflate,
+ "Zlib Inflate": Compress.runZlibInflate,
+ "Gzip": Compress.runGzip,
+ "Gunzip": Compress.runGunzip,
+ "Zip": Compress.runPkzip,
+ "Unzip": Compress.runPkunzip,
+ "Bzip2 Decompress": Compress.runBzip2Decompress,
+ "Tar": Compress.runTar,
+ "Untar": Compress.runUntar,
+ "HTTP gzip Decrypt": Compress.runHttpGzip,
};
diff --git a/src/core/config/modules/Default.js b/src/core/config/modules/Default.js
index 4fb76887..44c4d9c9 100644
--- a/src/core/config/modules/Default.js
+++ b/src/core/config/modules/Default.js
@@ -53,6 +53,8 @@ OpModules.Default = {
"From Hexdump": Hexdump.runFrom,
"To Hex": ByteRepr.runToHex,
"From Hex": ByteRepr.runFromHex,
+ "From 0x[Hex]": ByteRepr.runFrom0xHex,
+ "From Char(Hex)": ByteRepr.runFromCharHex,
"To Octal": ByteRepr.runToOct,
"From Octal": ByteRepr.runFromOct,
"To Charcode": ByteRepr.runToCharcode,
diff --git a/src/core/config/modules/OpModules.js b/src/core/config/modules/OpModules.js
index 3f3963c3..b7a9ca2b 100644
--- a/src/core/config/modules/OpModules.js
+++ b/src/core/config/modules/OpModules.js
@@ -20,6 +20,7 @@ import JSBNModule from "./JSBN.js";
import PublicKeyModule from "./PublicKey.js";
import ShellcodeModule from "./Shellcode.js";
import URLModule from "./URL.js";
+import PacketsModule from "./Packets.js";
Object.assign(
OpModules,
@@ -35,7 +36,8 @@ Object.assign(
JSBNModule,
PublicKeyModule,
ShellcodeModule,
- URLModule
+ URLModule,
+ PacketsModule
);
export default OpModules;
diff --git a/src/core/config/modules/Packets.js b/src/core/config/modules/Packets.js
new file mode 100644
index 00000000..1d441ca4
--- /dev/null
+++ b/src/core/config/modules/Packets.js
@@ -0,0 +1,20 @@
+import Packets from "../../operations/Packets.js";
+
+/**
+ * Packets module.
+ *
+ * Libraries:
+ * - Utils.js
+ *
+ * @author drkna [whytho@email]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+let OpModules = typeof self === "undefined" ? {} : self.OpModules || {};
+
+OpModules.Packets = {
+ "From Tcpdump": Packets.runFromTcpdump,
+ "Strip TCP Headers": Packets.stripPacketHeader
+};
+
+export default OpModules;
diff --git a/src/core/operations/ByteRepr.js b/src/core/operations/ByteRepr.js
index 87a543ff..3183a7c9 100755
--- a/src/core/operations/ByteRepr.js
+++ b/src/core/operations/ByteRepr.js
@@ -54,6 +54,48 @@ const ByteRepr = {
},
+ /**
+ * From 0xHex operation.
+ *
+ * @param {string} input (Starting with 0x only in the raw input)
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ runFrom0xHex: function(input, args) {
+ let data = input.replace(/0x([0-9a-f]{2,})/ig,
+ function(match, p1) {
+ if (p1) {
+ return Utils.byteArrayToChars(Utils.fromHex(p1));
+ }
+ });
+ return data;
+ },
+
+
+ /**
+ * From char(hex) operation.
+ *
+ * @param {string} input (Starting with chr or char only in the raw input)
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ runFromCharHex: function(input, args) {
+ let re = /cha?r\((((\d{1,3})(,\s?)?)+)\)/ig;
+ let innerRe = /(\d{1,3}),?/g;
+ let data = input.replace(re,
+ function(match, p1) {
+ if (p1) {
+ let result = "", innerMatch;
+ while ((innerMatch = innerRe.exec(p1)) != null) {
+ result += Utils.byteArrayToChars([parseInt(innerMatch[1], 10)]);
+ }
+ return result;
+ }
+ });
+ return data;
+ },
+
+
/**
* To Octal operation.
*
diff --git a/src/core/operations/Compress.js b/src/core/operations/Compress.js
index 639b89c6..7748f9d9 100755
--- a/src/core/operations/Compress.js
+++ b/src/core/operations/Compress.js
@@ -5,6 +5,7 @@ import zlibAndGzip from "zlibjs/bin/zlib_and_gzip.min";
import zip from "zlibjs/bin/zip.min";
import unzip from "zlibjs/bin/unzip.min";
import bzip2 from "exports-loader?bzip2!../lib/bzip2.js";
+import pako from "pako/index.js";
const Zlib = {
RawDeflate: rawdeflate.Zlib.RawDeflate,
@@ -254,6 +255,69 @@ const Compress = {
},
+ /**
+ * @constant
+ * @default
+ */
+ HTTP_GZIP_OPTION: ["pako.js", "zlib.js"],
+ /**
+ * @constant
+ * @default
+ */
+ HTTP_GZIP_THRESHOLD: 8,
+
+ /**
+ * HTTP Gzip operation.
+ *
+ * @param {byteArray} input
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ runHttpGzip: function(input, args) {
+ const library = Compress.HTTP_GZIP_OPTION.indexOf(args[0]);
+ let threshold = Compress.HTTP_GZIP_THRESHOLD;
+ if (args[1] > 8) {
+ threshold = args[1];
+ }
+ input = Utils.byteArrayToHexNoSpace(input);
+ let output = input;
+
+ let regexStr = /1f8b080[0-8][0-9a-f]{12}/;
+ let gzipPos = input.search(regexStr);
+ if (gzipPos === -1) {
+ return Utils.hexToByteArray(input);
+ }
+
+ while (gzipPos !== -1) {
+ output = input;
+
+ let plainData = output.substr(0, gzipPos);
+ let gzipData = output.substr(gzipPos);
+ let httpDataAfter = "";
+
+ let httpDataPosRegex = new RegExp("/((3[0-9])|(6[0-9a-f])|(7[0-9a])|(4[0-9a-f])|(5[0-9a])|(2[e-f])|(2b)|(20)){" + threshold + "}/");
+ let httpDataPos = gzipData.search(httpDataPosRegex);
+ if (httpDataPos !== -1) {
+ httpDataAfter = gzipData.substr(httpDataPos);
+ gzipData = gzipData.substr(0, httpDataPos);
+ }
+
+ console.log(httpDataPos);
+ gzipData = Utils.hexToByteArray(gzipData);
+ if (library === 0) {
+ output = Utils.hexToByteArray(plainData).concat(Array.prototype.slice.call(pako.inflate(gzipData))).concat(Utils.hexToByteArray(httpDataAfter));
+ } else if (library === 1) {
+ let gzipDataRaw = new Zlib.Gunzip(gzipData);
+ output = Utils.hexToByteArray(plainData).concat(Array.prototype.slice.call(gzipDataRaw.decompress())).concat(Utils.hexToByteArray("0d0a 0d0a")).concat(Utils.hexToByteArray(httpDataAfter));
+ }
+
+ input = Utils.byteArrayToHexNoSpace(output);
+ gzipPos = input.search(regexStr);
+ }
+ return output;
+ },
+
+
/**
* @constant
* @default
diff --git a/src/core/operations/Packets.js b/src/core/operations/Packets.js
new file mode 100644
index 00000000..b5825003
--- /dev/null
+++ b/src/core/operations/Packets.js
@@ -0,0 +1,81 @@
+import Utils from "../Utils.js";
+
+
+/**
+ * Packets operations.
+ *
+ * @author drkna [whytho@email]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ *
+ * @namespace
+ */
+const Packets = {
+ /**
+ * From Tcpdump Hexstring operation.
+ *
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ runFromTcpdump: function(input, args) {
+ let output = [];
+ let regex = /^\s*(?:0x[\dA-F]{4}:?)?\s*((?:[\dA-F]{4}\s?){1,8})/igm;
+ let block = regex.exec(input);
+ while (block) {
+ let line = Utils.fromHex(block[1].replace(/-/g, " "));
+ for (let i = 0; i < line.length; i++) {
+ output.push(line[i]);
+ }
+ block = regex.exec(input);
+ }
+ return output;
+ },
+
+
+ /**
+ * @constant
+ * @default
+ */
+ STRIP_ETHERNET_HEADER: true,
+
+ /**
+ * @constant
+ * @default
+ */
+ STRIP_IP_HEADER: true,
+
+ /**
+ * @constant
+ * @default
+ */
+ STRIP_TCP_HEADER: true,
+
+ /**
+ * Strip TCP Headersoperation.
+ *
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ stripPacketHeader: function(input, args) {
+ let output = input,
+ stripEthernet = args[0],
+ stripIP = args[1],
+ stripTCP = args[2];
+
+ if (stripEthernet) {
+ output = output.replace(/^(([0-9a-f]{4} ?){6,8}0800 ?)/igm, "");
+ }
+ if (stripIP) {
+ output = output.replace(/^((45[0-9a-f]{2} ?([0-9a-f]{4} ?){9}))/igm, "");
+ }
+ if (stripTCP) {
+ output = output.replace(/^([0-9a-f]{4} ?){6}((80[0-9a-f]{2} ?([0-9a-f]{4} ?){9})|(50[0-9a-f]{2} ?([0-9a-f]{4} ?){3}))/igm, "");
+ }
+
+ return output;
+ },
+};
+
+export default Packets;
diff --git a/src/css/themes/orange.css b/src/css/themes/orange.css
new file mode 100644
index 00000000..88f9bfff
--- /dev/null
+++ b/src/css/themes/orange.css
@@ -0,0 +1,278 @@
+#banner {
+ border-bottom: 1px solid #ddd;
+}
+
+.title {
+ border-bottom: 1px solid #ddd;
+ font-weight: bold;
+ color: #424242;
+ background-color: #fafafa;
+}
+
+.gutter {
+ background-color: #eee;
+ background-repeat: no-repeat;
+ background-position: 50%;
+}
+
+.gutter.gutter-horizontal {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAeCAYAAAAGos/EAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAlSURBVChTYzxz5sx/BiBgAhEgwPju3TtUEZZ79+6BGcNcDQMDACWJMFs4hNOSAAAAAElFTkSuQmCC');
+ cursor: ew-resize;
+}
+
+.gutter.gutter-vertical {
+ background-image: url('data:image/png;base64,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');
+ cursor: ns-resize;
+}
+
+.operation {
+ border: 1px solid #999;
+ border-top-width: 0;
+}
+
+.op-list .operation { /*blue*/
+ color: #3a87ad;
+ background-color: #d9edf7;
+ border-color: #bce8f1;
+}
+
+#rec-list .no-select { /*Overriding*/
+ color: orange;
+ background-color: black;
+ border-color: orange;
+}
+
+#rec-list .operation { /*green*/
+ color: orange;
+ background-color: whitesmoke;
+ border-color: orange;
+}
+
+#controls {
+ border-top: 1px solid #ddd;
+ background-color: #fafafa;
+}
+
+.textarea-wrapper textarea,
+.textarea-wrapper div {
+ font-family: Consolas, monospace;
+ font-size: inherit;
+}
+
+.io-info {
+ font-family: Consolas, monospace;
+ font-weight: normal;
+ font-size: 8pt;
+}
+
+.arg-title {
+ font-weight: bold;
+}
+
+.arg-input {
+ height: 34px;
+ font-size: 15px;
+ line-height: 1.428571429;
+ color: #424242;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ font-family: Consolas, monospace;
+}
+
+select {
+ padding: 6px 8px;
+ height: 34px;
+ border: 1px solid #ddd;
+ background-color: #fff;
+ color: #424242;
+}
+
+.arg[disabled] {
+ background-color: #eee;
+}
+
+textarea.arg {
+ color: #424242;
+ font-family: Consolas, monospace;
+}
+
+.break {
+ color: #b94a48 !important;
+ background-color: #f2dede !important;
+ border-color: #eed3d7 !important;
+}
+
+.category-title {
+ background-color: #fafafa;
+ border-bottom: 1px solid #eee;
+ font-weight: bold;
+}
+
+.category-title[href='#catFavourites'] {
+ border-bottom-color: #ddd;
+}
+
+.category-title[aria-expanded=true] {
+ border-bottom-color: #ddd;
+}
+
+.category-title.collapsed {
+ border-bottom-color: #eee;
+}
+
+.category-title:hover {
+ color: #3a87ad;
+}
+
+#search {
+ border-bottom: 1px solid #e3e3e3;
+}
+
+.dropping-file {
+ border: 5px dashed #3a87ad !important;
+}
+
+.selected-op {
+ color: #c09853 !important;
+ background-color: #fcf8e3 !important;
+ border-color: #fbeed5 !important;
+}
+
+.option-item input[type=number] {
+ font-size: 14px;
+ line-height: 1.428571429;
+ color: #555;
+ background-color: #fff;
+ border: 1px solid #ccc;
+}
+
+.favourites-hover {
+ color: #468847;
+ background-color: #dff0d8;
+ border: 2px dashed #468847 !important;
+ padding: 8px 8px 9px 8px;
+}
+
+#edit-favourites-list {
+ border: 1px solid #bce8f1;
+}
+
+#edit-favourites-list .operation {
+ border-left: none;
+ border-right: none;
+}
+
+#edit-favourites-list .operation:last-child {
+ border-bottom: none;
+}
+
+.subtext {
+ font-style: italic;
+ font-size: 13px;
+ color: #999;
+}
+
+#save-footer {
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.flow-control-op {
+ color: #396f3a !important;
+ background-color: #c7e4ba !important;
+ border-color: #b3dba2 !important;
+}
+
+.flow-control-op.break {
+ color: #94312f !important;
+ background-color: #eabfbf !important;
+ border-color: #e2aeb5 !important;
+}
+
+#support-modal textarea {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+#save-text,
+#load-text {
+ font-family: Consolas, monospace;
+}
+
+button.dropdown-toggle {
+ background-color: #f4f4f4;
+}
+
+
+
+
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+::-webkit-scrollbar-track {
+ background-color: #fafafa;
+}
+::-webkit-scrollbar-thumb {
+ background-color: #ccc;
+}
+::-webkit-scrollbar-thumb:hover {
+ background-color: #bbb;
+}
+::-webkit-scrollbar-corner {
+ background-color: #fafafa;
+}
+
+.disabled {
+ color: #999 !important;
+ background-color: #dfdfdf !important;
+ border-color: #cdcdcd !important;
+}
+
+.grey {
+ color: #333;
+ background-color: #f5f5f5;
+ border-color: #ddd;
+}
+
+.dark-blue {
+ color: #fff;
+ background-color: #428bca;
+ border-color: #428bca;
+}
+
+.red {
+ color: #b94a48;
+ background-color: #f2dede;
+ border-color: #eed3d7;
+}
+
+.amber {
+ color: #c09853;
+ background-color: #fcf8e3;
+ border-color: #fbeed5;
+}
+
+.green {
+ color: #468847;
+ background-color: #dff0d8;
+ border-color: #d6e9c6;
+}
+
+.blue {
+ color: #3a87ad;
+ background-color: #d9edf7;
+ border-color: #bce8f1;
+}
+
+#input-text,
+#output-text,
+#output-html {
+ position: relative;
+ border-width: 0px;
+ margin: 0;
+ resize: none;
+ background-color: transparent;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ color: orange;
+ background-color: black;
+}
diff --git a/src/web/static/ga.html b/src/web/static/ga.html
index e5f8db82..2edf01f5 100644
--- a/src/web/static/ga.html
+++ b/src/web/static/ga.html
@@ -1,11 +1,6 @@
-