cyberchef 9.37.2 → 9.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +7 -2
  2. package/Gruntfile.js +2 -1
  3. package/package.json +4 -2
  4. package/src/core/config/Categories.json +1 -0
  5. package/src/core/config/OperationConfig.json +106 -3
  6. package/src/core/config/modules/Default.mjs +2 -0
  7. package/src/core/config/scripts/newMinorVersion.mjs +144 -0
  8. package/src/core/lib/Binary.mjs +9 -5
  9. package/src/core/lib/Extract.mjs +16 -11
  10. package/src/core/lib/FileSignatures.mjs +12 -12
  11. package/src/core/lib/Protocol.mjs +47 -0
  12. package/src/core/lib/Sort.mjs +105 -0
  13. package/src/core/lib/Stream.mjs +22 -12
  14. package/src/core/operations/ExtractDates.mjs +7 -1
  15. package/src/core/operations/ExtractDomains.mjs +29 -5
  16. package/src/core/operations/ExtractEmailAddresses.mjs +29 -5
  17. package/src/core/operations/ExtractFilePaths.mjs +38 -14
  18. package/src/core/operations/ExtractIPAddresses.mjs +44 -27
  19. package/src/core/operations/ExtractMACAddresses.mjs +28 -6
  20. package/src/core/operations/ExtractURLs.mjs +28 -5
  21. package/src/core/operations/ParseTCP.mjs +245 -0
  22. package/src/core/operations/ParseUDP.mjs +29 -24
  23. package/src/core/operations/Sort.mjs +5 -105
  24. package/src/core/operations/Strings.mjs +36 -14
  25. package/src/core/operations/ToBase45.mjs +4 -0
  26. package/src/core/operations/ToHex.mjs +2 -2
  27. package/src/core/operations/Unique.mjs +25 -5
  28. package/src/core/operations/index.mjs +2 -0
  29. package/src/node/index.mjs +5 -0
  30. package/src/web/App.mjs +10 -5
  31. package/src/web/html/index.html +12 -10
  32. package/src/web/stylesheets/components/_pane.css +11 -4
  33. package/src/web/stylesheets/layout/_controls.css +4 -0
  34. package/src/web/stylesheets/layout/_io.css +1 -2
  35. package/src/web/stylesheets/layout/_structure.css +2 -2
  36. package/src/web/waiters/OperationsWaiter.mjs +5 -1
  37. package/src/web/waiters/OutputWaiter.mjs +2 -0
  38. package/src/web/waiters/WindowWaiter.mjs +2 -2
  39. package/tests/node/tests/operations.mjs +3 -4
  40. package/tests/operations/index.mjs +1 -0
  41. package/tests/operations/tests/ExtractEmailAddresses.mjs +4 -4
  42. package/tests/operations/tests/ParseTCP.mjs +44 -0
  43. package/tests/operations/tests/ParseUDP.mjs +5 -18
  44. package/webpack.config.js +11 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Sorting functions
3
+ *
4
+ * @author n1474335 [n1474335@gmail.com]
5
+ * @copyright Crown Copyright 2022
6
+ * @license Apache-2.0
7
+ *
8
+ */
9
+
10
+ /**
11
+ * Comparison operation for sorting of strings ignoring case.
12
+ *
13
+ * @param {string} a
14
+ * @param {string} b
15
+ * @returns {number}
16
+ */
17
+ export function caseInsensitiveSort(a, b) {
18
+ return a.toLowerCase().localeCompare(b.toLowerCase());
19
+ }
20
+
21
+
22
+ /**
23
+ * Comparison operation for sorting of IPv4 addresses.
24
+ *
25
+ * @param {string} a
26
+ * @param {string} b
27
+ * @returns {number}
28
+ */
29
+ export function ipSort(a, b) {
30
+ let a_ = a.split("."),
31
+ b_ = b.split(".");
32
+
33
+ a_ = a_[0] * 0x1000000 + a_[1] * 0x10000 + a_[2] * 0x100 + a_[3] * 1;
34
+ b_ = b_[0] * 0x1000000 + b_[1] * 0x10000 + b_[2] * 0x100 + b_[3] * 1;
35
+
36
+ if (isNaN(a_) && !isNaN(b_)) return 1;
37
+ if (!isNaN(a_) && isNaN(b_)) return -1;
38
+ if (isNaN(a_) && isNaN(b_)) return a.localeCompare(b);
39
+
40
+ return a_ - b_;
41
+ }
42
+
43
+ /**
44
+ * Comparison operation for sorting of numeric values.
45
+ *
46
+ * @author Chris van Marle
47
+ * @param {string} a
48
+ * @param {string} b
49
+ * @returns {number}
50
+ */
51
+ export function numericSort(a, b) {
52
+ const a_ = a.split(/([^\d]+)/),
53
+ b_ = b.split(/([^\d]+)/);
54
+
55
+ for (let i = 0; i < a_.length && i < b.length; ++i) {
56
+ if (isNaN(a_[i]) && !isNaN(b_[i])) return 1; // Numbers after non-numbers
57
+ if (!isNaN(a_[i]) && isNaN(b_[i])) return -1;
58
+ if (isNaN(a_[i]) && isNaN(b_[i])) {
59
+ const ret = a_[i].localeCompare(b_[i]); // Compare strings
60
+ if (ret !== 0) return ret;
61
+ }
62
+ if (!isNaN(a_[i]) && !isNaN(b_[i])) { // Compare numbers
63
+ if (a_[i] - b_[i] !== 0) return a_[i] - b_[i];
64
+ }
65
+ }
66
+
67
+ return a.localeCompare(b);
68
+ }
69
+
70
+ /**
71
+ * Comparison operation for sorting of hexadecimal values.
72
+ *
73
+ * @author Chris van Marle
74
+ * @param {string} a
75
+ * @param {string} b
76
+ * @returns {number}
77
+ */
78
+ export function hexadecimalSort(a, b) {
79
+ let a_ = a.split(/([^\da-f]+)/i),
80
+ b_ = b.split(/([^\da-f]+)/i);
81
+
82
+ a_ = a_.map(v => {
83
+ const t = parseInt(v, 16);
84
+ return isNaN(t) ? v : t;
85
+ });
86
+
87
+ b_ = b_.map(v => {
88
+ const t = parseInt(v, 16);
89
+ return isNaN(t) ? v : t;
90
+ });
91
+
92
+ for (let i = 0; i < a_.length && i < b.length; ++i) {
93
+ if (isNaN(a_[i]) && !isNaN(b_[i])) return 1; // Numbers after non-numbers
94
+ if (!isNaN(a_[i]) && isNaN(b_[i])) return -1;
95
+ if (isNaN(a_[i]) && isNaN(b_[i])) {
96
+ const ret = a_[i].localeCompare(b_[i]); // Compare strings
97
+ if (ret !== 0) return ret;
98
+ }
99
+ if (!isNaN(a_[i]) && !isNaN(b_[i])) { // Compare numbers
100
+ if (a_[i] - b_[i] !== 0) return a_[i] - b_[i];
101
+ }
102
+ }
103
+
104
+ return a.localeCompare(b);
105
+ }
@@ -27,15 +27,17 @@ export default class Stream {
27
27
  }
28
28
 
29
29
  /**
30
- * Get a number of bytes from the current position.
30
+ * Get a number of bytes from the current position, or all remaining bytes.
31
31
  *
32
- * @param {number} numBytes
32
+ * @param {number} [numBytes=null]
33
33
  * @returns {Uint8Array}
34
34
  */
35
- getBytes(numBytes) {
35
+ getBytes(numBytes=null) {
36
36
  if (this.position > this.length) return undefined;
37
37
 
38
- const newPosition = this.position + numBytes;
38
+ const newPosition = numBytes !== null ?
39
+ this.position + numBytes :
40
+ this.length;
39
41
  const bytes = this.bytes.slice(this.position, newPosition);
40
42
  this.position = newPosition;
41
43
  this.bitPos = 0;
@@ -91,34 +93,40 @@ export default class Stream {
91
93
  }
92
94
 
93
95
  /**
94
- * Reads a number of bits from the buffer.
95
- *
96
- * @TODO Add endianness
96
+ * Reads a number of bits from the buffer in big or little endian.
97
97
  *
98
98
  * @param {number} numBits
99
+ * @param {string} [endianness="be"]
99
100
  * @returns {number}
100
101
  */
101
- readBits(numBits) {
102
+ readBits(numBits, endianness="be") {
102
103
  if (this.position > this.length) return undefined;
103
104
 
104
105
  let bitBuf = 0,
105
106
  bitBufLen = 0;
106
107
 
107
108
  // Add remaining bits from current byte
108
- bitBuf = (this.bytes[this.position++] & bitMask(this.bitPos)) >>> this.bitPos;
109
+ bitBuf = this.bytes[this.position++] & bitMask(this.bitPos);
110
+ if (endianness !== "be") bitBuf >>>= this.bitPos;
109
111
  bitBufLen = 8 - this.bitPos;
110
112
  this.bitPos = 0;
111
113
 
112
114
  // Not enough bits yet
113
115
  while (bitBufLen < numBits) {
114
- bitBuf |= this.bytes[this.position++] << bitBufLen;
116
+ if (endianness === "be")
117
+ bitBuf = (bitBuf << bitBufLen) | this.bytes[this.position++];
118
+ else
119
+ bitBuf |= this.bytes[this.position++] << bitBufLen;
115
120
  bitBufLen += 8;
116
121
  }
117
122
 
118
123
  // Reverse back to numBits
119
124
  if (bitBufLen > numBits) {
120
125
  const excess = bitBufLen - numBits;
121
- bitBuf &= (1 << numBits) - 1;
126
+ if (endianness === "be")
127
+ bitBuf >>>= excess;
128
+ else
129
+ bitBuf &= (1 << numBits) - 1;
122
130
  bitBufLen -= excess;
123
131
  this.position--;
124
132
  this.bitPos = 8 - excess;
@@ -133,7 +141,9 @@ export default class Stream {
133
141
  * @returns {number} The bit mask
134
142
  */
135
143
  function bitMask(bitPos) {
136
- return 256 - (1 << bitPos);
144
+ return endianness === "be" ?
145
+ (1 << (8 - bitPos)) - 1 :
146
+ 256 - (1 << bitPos);
137
147
  }
138
148
  }
139
149
 
@@ -44,7 +44,13 @@ class ExtractDates extends Operation {
44
44
  date3 = "(?:0[1-9]|1[012])[- /.](?:0[1-9]|[12][0-9]|3[01])[- /.](?:19|20)\\d\\d", // mm/dd/yyyy
45
45
  regex = new RegExp(date1 + "|" + date2 + "|" + date3, "ig");
46
46
 
47
- return search(input, regex, null, displayTotal);
47
+ const results = search(input, regex);
48
+
49
+ if (displayTotal) {
50
+ return `Total found: ${results.length}\n\n${results.join("\n")}`;
51
+ } else {
52
+ return results.join("\n");
53
+ }
48
54
  }
49
55
 
50
56
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import Operation from "../Operation.mjs";
8
8
  import { search, DOMAIN_REGEX } from "../lib/Extract.mjs";
9
+ import { caseInsensitiveSort } from "../lib/Sort.mjs";
9
10
 
10
11
  /**
11
12
  * Extract domains operation
@@ -25,9 +26,19 @@ class ExtractDomains extends Operation {
25
26
  this.outputType = "string";
26
27
  this.args = [
27
28
  {
28
- "name": "Display total",
29
- "type": "boolean",
30
- "value": true
29
+ name: "Display total",
30
+ type: "boolean",
31
+ value: false
32
+ },
33
+ {
34
+ name: "Sort",
35
+ type: "boolean",
36
+ value: false
37
+ },
38
+ {
39
+ name: "Unique",
40
+ type: "boolean",
41
+ value: false
31
42
  }
32
43
  ];
33
44
  }
@@ -38,8 +49,21 @@ class ExtractDomains extends Operation {
38
49
  * @returns {string}
39
50
  */
40
51
  run(input, args) {
41
- const displayTotal = args[0];
42
- return search(input, DOMAIN_REGEX, null, displayTotal);
52
+ const [displayTotal, sort, unique] = args;
53
+
54
+ const results = search(
55
+ input,
56
+ DOMAIN_REGEX,
57
+ null,
58
+ sort ? caseInsensitiveSort : null,
59
+ unique
60
+ );
61
+
62
+ if (displayTotal) {
63
+ return `Total found: ${results.length}\n\n${results.join("\n")}`;
64
+ } else {
65
+ return results.join("\n");
66
+ }
43
67
  }
44
68
 
45
69
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import Operation from "../Operation.mjs";
8
8
  import { search } from "../lib/Extract.mjs";
9
+ import { caseInsensitiveSort } from "../lib/Sort.mjs";
9
10
 
10
11
  /**
11
12
  * Extract email addresses operation
@@ -25,9 +26,19 @@ class ExtractEmailAddresses extends Operation {
25
26
  this.outputType = "string";
26
27
  this.args = [
27
28
  {
28
- "name": "Display total",
29
- "type": "boolean",
30
- "value": false
29
+ name: "Display total",
30
+ type: "boolean",
31
+ value: false
32
+ },
33
+ {
34
+ name: "Sort",
35
+ type: "boolean",
36
+ value: false
37
+ },
38
+ {
39
+ name: "Unique",
40
+ type: "boolean",
41
+ value: false
31
42
  }
32
43
  ];
33
44
  }
@@ -38,10 +49,23 @@ class ExtractEmailAddresses extends Operation {
38
49
  * @returns {string}
39
50
  */
40
51
  run(input, args) {
41
- const displayTotal = args[0],
52
+ const [displayTotal, sort, unique] = args,
42
53
  // email regex from: https://www.regextester.com/98066
43
54
  regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}\])/ig;
44
- return search(input, regex, null, displayTotal);
55
+
56
+ const results = search(
57
+ input,
58
+ regex,
59
+ null,
60
+ sort ? caseInsensitiveSort : null,
61
+ unique
62
+ );
63
+
64
+ if (displayTotal) {
65
+ return `Total found: ${results.length}\n\n${results.join("\n")}`;
66
+ } else {
67
+ return results.join("\n");
68
+ }
45
69
  }
46
70
 
47
71
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import Operation from "../Operation.mjs";
8
8
  import { search } from "../lib/Extract.mjs";
9
+ import { caseInsensitiveSort } from "../lib/Sort.mjs";
9
10
 
10
11
  /**
11
12
  * Extract file paths operation
@@ -25,19 +26,29 @@ class ExtractFilePaths extends Operation {
25
26
  this.outputType = "string";
26
27
  this.args = [
27
28
  {
28
- "name": "Windows",
29
- "type": "boolean",
30
- "value": true
29
+ name: "Windows",
30
+ type: "boolean",
31
+ value: true
31
32
  },
32
33
  {
33
- "name": "UNIX",
34
- "type": "boolean",
35
- "value": true
34
+ name: "UNIX",
35
+ type: "boolean",
36
+ value: true
36
37
  },
37
38
  {
38
- "name": "Display total",
39
- "type": "boolean",
40
- "value": false
39
+ name: "Display total",
40
+ type: "boolean",
41
+ value: false
42
+ },
43
+ {
44
+ name: "Sort",
45
+ type: "boolean",
46
+ value: false
47
+ },
48
+ {
49
+ name: "Unique",
50
+ type: "boolean",
51
+ value: false
41
52
  }
42
53
  ];
43
54
  }
@@ -48,7 +59,7 @@ class ExtractFilePaths extends Operation {
48
59
  * @returns {string}
49
60
  */
50
61
  run(input, args) {
51
- const [includeWinPath, includeUnixPath, displayTotal] = args,
62
+ const [includeWinPath, includeUnixPath, displayTotal, sort, unique] = args,
52
63
  winDrive = "[A-Z]:\\\\",
53
64
  winName = "[A-Z\\d][A-Z\\d\\- '_\\(\\)~]{0,61}",
54
65
  winExt = "[A-Z\\d]{1,6}",
@@ -65,12 +76,25 @@ class ExtractFilePaths extends Operation {
65
76
  filePaths = unixPath;
66
77
  }
67
78
 
68
- if (filePaths) {
69
- const regex = new RegExp(filePaths, "ig");
70
- return search(input, regex, null, displayTotal);
71
- } else {
79
+ if (!filePaths) {
72
80
  return "";
73
81
  }
82
+
83
+ const regex = new RegExp(filePaths, "ig");
84
+ const results = search(
85
+ input,
86
+ regex,
87
+ null,
88
+ sort ? caseInsensitiveSort : null,
89
+ unique
90
+ );
91
+
92
+ if (displayTotal) {
93
+ return `Total found: ${results.length}\n\n${results.join("\n")}`;
94
+ } else {
95
+ return results.join("\n");
96
+ }
97
+
74
98
  }
75
99
 
76
100
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import Operation from "../Operation.mjs";
8
8
  import { search } from "../lib/Extract.mjs";
9
+ import { ipSort } from "../lib/Sort.mjs";
9
10
 
10
11
  /**
11
12
  * Extract IP addresses operation
@@ -25,24 +26,34 @@ class ExtractIPAddresses extends Operation {
25
26
  this.outputType = "string";
26
27
  this.args = [
27
28
  {
28
- "name": "IPv4",
29
- "type": "boolean",
30
- "value": true
29
+ name: "IPv4",
30
+ type: "boolean",
31
+ value: true
31
32
  },
32
33
  {
33
- "name": "IPv6",
34
- "type": "boolean",
35
- "value": false
34
+ name: "IPv6",
35
+ type: "boolean",
36
+ value: false
36
37
  },
37
38
  {
38
- "name": "Remove local IPv4 addresses",
39
- "type": "boolean",
40
- "value": false
39
+ name: "Remove local IPv4 addresses",
40
+ type: "boolean",
41
+ value: false
41
42
  },
42
43
  {
43
- "name": "Display total",
44
- "type": "boolean",
45
- "value": false
44
+ name: "Display total",
45
+ type: "boolean",
46
+ value: false
47
+ },
48
+ {
49
+ name: "Sort",
50
+ type: "boolean",
51
+ value: false
52
+ },
53
+ {
54
+ name: "Unique",
55
+ type: "boolean",
56
+ value: false
46
57
  }
47
58
  ];
48
59
  }
@@ -53,7 +64,7 @@ class ExtractIPAddresses extends Operation {
53
64
  * @returns {string}
54
65
  */
55
66
  run(input, args) {
56
- const [includeIpv4, includeIpv6, removeLocal, displayTotal] = args,
67
+ const [includeIpv4, includeIpv6, removeLocal, displayTotal, sort, unique] = args,
57
68
  ipv4 = "(?:(?:\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d|\\d)(?:\\/\\d{1,2})?",
58
69
  ipv6 = "((?=.*::)(?!.*::.+::)(::)?([\\dA-F]{1,4}:(:|\\b)|){5}|([\\dA-F]{1,4}:){6})((([\\dA-F]{1,4}((?!\\3)::|:\\b|(?![\\dA-F])))|(?!\\2\\3)){2}|(((2[0-4]|1\\d|[1-9])?\\d|25[0-5])\\.?\\b){4})";
59
70
  let ips = "";
@@ -66,23 +77,29 @@ class ExtractIPAddresses extends Operation {
66
77
  ips = ipv6;
67
78
  }
68
79
 
69
- if (ips) {
70
- const regex = new RegExp(ips, "ig");
80
+ if (!ips) return "";
71
81
 
72
- if (removeLocal) {
73
- const ten = "10\\..+",
74
- oneninetwo = "192\\.168\\..+",
75
- oneseventwo = "172\\.(?:1[6-9]|2\\d|3[01])\\..+",
76
- onetwoseven = "127\\..+",
77
- removeRegex = new RegExp("^(?:" + ten + "|" + oneninetwo +
78
- "|" + oneseventwo + "|" + onetwoseven + ")");
82
+ const regex = new RegExp(ips, "ig");
79
83
 
80
- return search(input, regex, removeRegex, displayTotal);
81
- } else {
82
- return search(input, regex, null, displayTotal);
83
- }
84
+ const ten = "10\\..+",
85
+ oneninetwo = "192\\.168\\..+",
86
+ oneseventwo = "172\\.(?:1[6-9]|2\\d|3[01])\\..+",
87
+ onetwoseven = "127\\..+",
88
+ removeRegex = new RegExp("^(?:" + ten + "|" + oneninetwo +
89
+ "|" + oneseventwo + "|" + onetwoseven + ")");
90
+
91
+ const results = search(
92
+ input,
93
+ regex,
94
+ removeLocal ? removeRegex : null,
95
+ sort ? ipSort : null,
96
+ unique
97
+ );
98
+
99
+ if (displayTotal) {
100
+ return `Total found: ${results.length}\n\n${results.join("\n")}`;
84
101
  } else {
85
- return "";
102
+ return results.join("\n");
86
103
  }
87
104
  }
88
105
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  import Operation from "../Operation.mjs";
8
8
  import { search } from "../lib/Extract.mjs";
9
+ import { hexadecimalSort } from "../lib/Sort.mjs";
9
10
 
10
11
  /**
11
12
  * Extract MAC addresses operation
@@ -25,9 +26,19 @@ class ExtractMACAddresses extends Operation {
25
26
  this.outputType = "string";
26
27
  this.args = [
27
28
  {
28
- "name": "Display total",
29
- "type": "boolean",
30
- "value": false
29
+ name: "Display total",
30
+ type: "boolean",
31
+ value: false
32
+ },
33
+ {
34
+ name: "Sort",
35
+ type: "boolean",
36
+ value: false
37
+ },
38
+ {
39
+ name: "Unique",
40
+ type: "boolean",
41
+ value: false
31
42
  }
32
43
  ];
33
44
  }
@@ -38,10 +49,21 @@ class ExtractMACAddresses extends Operation {
38
49
  * @returns {string}
39
50
  */
40
51
  run(input, args) {
41
- const displayTotal = args[0],
42
- regex = /[A-F\d]{2}(?:[:-][A-F\d]{2}){5}/ig;
52
+ const [displayTotal, sort, unique] = args,
53
+ regex = /[A-F\d]{2}(?:[:-][A-F\d]{2}){5}/ig,
54
+ results = search(
55
+ input,
56
+ regex,
57
+ null,
58
+ sort ? hexadecimalSort : null,
59
+ unique
60
+ );
43
61
 
44
- return search(input, regex, null, displayTotal);
62
+ if (displayTotal) {
63
+ return `Total found: ${results.length}\n\n${results.join("\n")}`;
64
+ } else {
65
+ return results.join("\n");
66
+ }
45
67
  }
46
68
 
47
69
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import Operation from "../Operation.mjs";
8
8
  import { search, URL_REGEX } from "../lib/Extract.mjs";
9
+ import { caseInsensitiveSort } from "../lib/Sort.mjs";
9
10
 
10
11
  /**
11
12
  * Extract URLs operation
@@ -25,9 +26,19 @@ class ExtractURLs extends Operation {
25
26
  this.outputType = "string";
26
27
  this.args = [
27
28
  {
28
- "name": "Display total",
29
- "type": "boolean",
30
- "value": false
29
+ name: "Display total",
30
+ type: "boolean",
31
+ value: false
32
+ },
33
+ {
34
+ name: "Sort",
35
+ type: "boolean",
36
+ value: false
37
+ },
38
+ {
39
+ name: "Unique",
40
+ type: "boolean",
41
+ value: false
31
42
  }
32
43
  ];
33
44
  }
@@ -38,8 +49,20 @@ class ExtractURLs extends Operation {
38
49
  * @returns {string}
39
50
  */
40
51
  run(input, args) {
41
- const displayTotal = args[0];
42
- return search(input, URL_REGEX, null, displayTotal);
52
+ const [displayTotal, sort, unique] = args;
53
+ const results = search(
54
+ input,
55
+ URL_REGEX,
56
+ null,
57
+ sort ? caseInsensitiveSort : null,
58
+ unique
59
+ );
60
+
61
+ if (displayTotal) {
62
+ return `Total found: ${results.length}\n\n${results.join("\n")}`;
63
+ } else {
64
+ return results.join("\n");
65
+ }
43
66
  }
44
67
 
45
68
  }