-
+
);
}
}
-export default Footer;
+export default withRouter(Footer);
diff --git a/frontend/src/views/Footer.scss b/frontend/src/views/Footer.scss
index 6ec4a62..14360d4 100644
--- a/frontend/src/views/Footer.scss
+++ b/frontend/src/views/Footer.scss
@@ -1,13 +1,22 @@
@import "../colors.scss";
.footer {
- padding: 15px 30px;
+ padding: 15px;
- > .row {
+ .time-line {
+ position: relative;
background-color: $color-primary-0;
+
+ .metric-selection {
+ font-size: 0.8em;
+ position: absolute;
+ top: 5px;
+ right: 10px;
+ }
}
- .footer-timeline {
- height: 100px;
+ svg text {
+ font-family: "Fira Code", monospace !important;
+ fill: $color-primary-4 !important;
}
}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 84d6058..fa150ab 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -909,6 +909,14 @@
"@babel/helper-create-regexp-features-plugin" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
+"@babel/polyfill@^7.7.0":
+ version "7.11.5"
+ resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.11.5.tgz#df550b2ec53abbc2ed599367ec59e64c7a707bb5"
+ integrity sha512-FunXnE0Sgpd61pKSj2OSOs1D44rKTD3pGOfGilZ6LGrrIH0QEtJlTjqOqdF8Bs98JmjfGhni2BBkTfv9KcKJ9g==
+ dependencies:
+ core-js "^2.6.5"
+ regenerator-runtime "^0.13.4"
+
"@babel/preset-env@7.9.0":
version "7.9.0"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8"
@@ -1725,9 +1733,9 @@
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.11", "@types/react@^16.9.35":
- version "16.9.49"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872"
- integrity sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==
+ version "16.9.50"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.50.tgz#cb5f2c22d42de33ca1f5efc6a0959feb784a3a2d"
+ integrity sha512-kPx5YsNnKDJejTk1P+lqThwxN2PczrocwsvqXnjvVvKpFescoY62ZiM3TV7dH1T8lFhlHZF+PE5xUyimUwqEGA==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
@@ -2268,6 +2276,11 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+array.prototype.fill@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.fill/-/array.prototype.fill-1.0.2.tgz#ab33207f21d57d1ab2f7f0d1cf122d3419c38ef5"
+ integrity sha1-qzMgfyHVfRqy9/DRzxItNBnDjvU=
+
array.prototype.flat@^1.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
@@ -2535,7 +2548,7 @@ babel-preset-react-app@^9.1.2:
babel-plugin-macros "2.8.0"
babel-plugin-transform-react-remove-prop-types "0.4.24"
-babel-runtime@^6.26.0:
+babel-runtime@^6.23.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
@@ -2981,9 +2994,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135:
- version "1.0.30001140"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001140.tgz#30dae27599f6ede2603a0962c82e468bca894232"
- integrity sha512-xFtvBtfGrpjTOxTpjP5F2LmN04/ZGfYV8EQzUIC/RmKpdrmzJrjqlJ4ho7sGuAMPko2/Jl08h7x9uObCfBFaAA==
+ version "1.0.30001142"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001142.tgz#a8518fdb5fee03ad95ac9f32a9a1e5999469c250"
+ integrity sha512-pDPpn9ankEpBFZXyCv2I4lh1v/ju+bqb78QfKf+w9XgDAFWBwSYPswXqprRdrgQWK0wQnpIbfwRjNHO1HWqvoQ==
capture-exit@^2.0.0:
version "2.0.0"
@@ -3251,6 +3264,11 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
+colorbrewer@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/colorbrewer/-/colorbrewer-1.3.0.tgz#1d7e92a6277e42dc56377911bbd867bdbcb2ff7d"
+ integrity sha512-AzVPpWa+fuO/qY8LxPQjej6F49Lb2Cl+7U9YhPn6y4/SOY6u/EZiXUc7qHzRb6i6fWPStCUdEaU2731QyQKWjg==
+
colorette@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
@@ -3429,7 +3447,7 @@ core-js@^1.0.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
-core-js@^2.4.0:
+core-js@^2.4.0, core-js@^2.6.5:
version "2.6.11"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
@@ -3641,9 +3659,9 @@ css-what@2.1:
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
css-what@^3.2.1:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39"
- integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e"
+ integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==
css.escape@^1.5.1:
version "1.5.1"
@@ -3779,6 +3797,123 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+d3-array@^1.2.0:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
+ integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
+
+d3-axis@^1.0.8:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
+ integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
+
+d3-collection@1:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
+ integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
+
+d3-color@1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
+ integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
+
+d3-dispatch@1:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
+ integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
+
+d3-ease@1, d3-ease@^1.0.3:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2"
+ integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==
+
+d3-format@1, d3-format@^1.2.0:
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
+ integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
+
+d3-interpolate@1, d3-interpolate@^1.1.5:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
+ integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
+ dependencies:
+ d3-color "1"
+
+d3-path@1:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
+ integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+
+d3-scale-chromatic@^1.1.1:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98"
+ integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==
+ dependencies:
+ d3-color "1"
+ d3-interpolate "1"
+
+d3-scale@^1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
+ integrity sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-color "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-selection-multi@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/d3-selection-multi/-/d3-selection-multi-1.0.1.tgz#cd6c25413d04a2cb97470e786f2cd877f3e34f58"
+ integrity sha1-zWwlQT0EosuXRw54byzYd/PjT1g=
+ dependencies:
+ d3-selection "1"
+ d3-transition "1"
+
+d3-selection@1, d3-selection@^1.1.0:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c"
+ integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==
+
+d3-shape@^1.2.0:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
+ integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2, d3-time-format@^2.0.5:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
+ integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==
+ dependencies:
+ d3-time "1"
+
+d3-time@1, d3-time@^1.0.7:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
+ integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
+
+d3-timer@1:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
+ integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
+
+d3-transition@1, d3-transition@^1.1.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398"
+ integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==
+ dependencies:
+ d3-color "1"
+ d3-dispatch "1"
+ d3-ease "1"
+ d3-interpolate "1"
+ d3-selection "^1.1.0"
+ d3-timer "1"
+
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@@ -4041,6 +4176,11 @@ dom-helpers@^5.0.1, dom-helpers@^5.1.0, dom-helpers@^5.1.2:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
+dom-resize@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/dom-resize/-/dom-resize-1.0.3.tgz#df9d71e808171fdb66ee88517b0d1c02cdb98876"
+ integrity sha512-lohasnGy9LABj1Sq7ZPUGIWSYf+4LFUwL0Aev+dAzxSzUiovc+lKnFyrc6M2TycMIoH7674kwKaucRIPZPgJXw==
+
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@@ -4049,6 +4189,11 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
+dom-walk@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
+ integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
+
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -4243,23 +4388,23 @@ error-ex@^1.2.0, error-ex@^1.3.1:
is-arrayish "^0.2.1"
es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
- version "1.17.6"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
- integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
+ version "1.17.7"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
+ integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
- is-callable "^1.2.0"
- is-regex "^1.1.0"
- object-inspect "^1.7.0"
+ is-callable "^1.2.2"
+ is-regex "^1.1.1"
+ object-inspect "^1.8.0"
object-keys "^1.1.1"
- object.assign "^4.1.0"
+ object.assign "^4.1.1"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
-es-abstract@^1.18.0-next.0:
+es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
version "1.18.0-next.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
@@ -4758,7 +4903,7 @@ fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
-fast-levenshtein@~2.0.6:
+fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
@@ -5238,6 +5383,14 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
+global@^4.3.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
+ integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
+ dependencies:
+ min-document "^2.19.0"
+ process "^0.11.10"
+
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -5437,6 +5590,11 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hoist-non-react-statics@^2.5.0:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+ integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
+
hoist-non-react-statics@^3.1.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -5656,6 +5814,16 @@ immer@1.10.0:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
+immutable-devtools@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/immutable-devtools/-/immutable-devtools-0.0.4.tgz#1e7e87f2c7a4f0533955bc4c2922d124bf9129dd"
+ integrity sha1-Hn6H8sek8FM5VbxMKSLRJL+RKd0=
+
+immutable@^3.6.4:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
+ integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -5809,7 +5977,7 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
-invariant@^2.2.2, invariant@^2.2.4:
+invariant@^2.1.1, invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -5894,7 +6062,7 @@ is-buffer@^1.0.2, is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-is-callable@^1.1.4, is-callable@^1.2.0, is-callable@^1.2.2:
+is-callable@^1.1.4, is-callable@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
@@ -6081,7 +6249,7 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
-is-regex@^1.0.4, is-regex@^1.1.0, is-regex@^1.1.1:
+is-regex@^1.0.4, is-regex@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
@@ -7232,6 +7400,11 @@ merge2@^1.2.3:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+merge@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
+ integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
+
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -7301,6 +7474,13 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
+ dependencies:
+ dom-walk "^0.1.0"
+
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
@@ -7413,6 +7593,16 @@ mixin-object@^2.0.1:
dependencies:
minimist "^1.2.5"
+moment-duration-format@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/moment-duration-format/-/moment-duration-format-1.3.0.tgz#541771b5f87a049cc65540475d3ad966737d6908"
+ integrity sha1-VBdxtfh6BJzGVUBHXTrZZnN9aQg=
+
+moment@^2.18.1, moment@^2.24.0:
+ version "2.29.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425"
+ integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -7734,18 +7924,18 @@ object-hash@^2.0.1:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
-object-inspect@^1.7.0, object-inspect@^1.8.0:
+object-inspect@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
object-is@^1.0.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
- integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
+ integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
dependencies:
define-properties "^1.1.3"
- es-abstract "^1.17.5"
+ es-abstract "^1.18.0-next.1"
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
@@ -8301,6 +8491,17 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
+pondjs@^0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/pondjs/-/pondjs-0.9.0.tgz#5c931233f8ef56852914eb669506eaac62b0f13b"
+ integrity sha512-fXgEYrWhgC/N3CVuXarG+/q54jar35Mqf7QPdl7z+mX5RkMyyjLfj9fMgY3oHv2hzMo9zkNx2kK62DrZuG3LXg==
+ dependencies:
+ "@babel/polyfill" "^7.7.0"
+ immutable "^3.6.4"
+ immutable-devtools "0.0.4"
+ moment "^2.24.0"
+ underscore "^1.9.1"
+
portfinder@^1.0.25:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
@@ -9080,7 +9281,7 @@ prop-types-extra@^1.1.0:
react-is "^16.3.2"
warning "^4.0.0"
-prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -9335,6 +9536,18 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
+react-hot-loader@4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.1.2.tgz#5e8025f5bc5605506586b46eb2c6cc4006fd54d7"
+ integrity sha512-7EFwgpJOx4AG4pwVifgr/ZNBPAxl2z424nGJPc/APB3F8YtCA3WdYuGlcerRK2C9vYAoqiiLw745IB3wjnzrRQ==
+ dependencies:
+ fast-levenshtein "^2.0.6"
+ global "^4.3.0"
+ hoist-non-react-statics "^2.5.0"
+ prop-types "^15.6.1"
+ react-lifecycles-compat "^3.0.2"
+ shallowequal "^1.0.2"
+
react-input-mask@^3.0.0-alpha.2:
version "3.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/react-input-mask/-/react-input-mask-3.0.0-alpha.2.tgz#113102942a557edc7a192e66020b8ce1ba699a5c"
@@ -9359,7 +9572,7 @@ react-json-view@^1.19.1:
react-lifecycles-compat "^3.0.4"
react-textarea-autosize "^6.1.0"
-react-lifecycles-compat@^3.0.4:
+react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@@ -9479,6 +9692,35 @@ react-textarea-autosize@^6.1.0:
dependencies:
prop-types "^15.6.0"
+react-timeseries-charts@^0.16.1:
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/react-timeseries-charts/-/react-timeseries-charts-0.16.1.tgz#46675a41a7806155a0b5a1342e3b811172845a5c"
+ integrity sha512-WK2Pege5/FGrE5ifGX1XTVQOZkobRUV5YMwRQi4+TpYAALL5cBLp+XCuAX8x8agq4AmqChpClPIrANc8pRL5RA==
+ dependencies:
+ array.prototype.fill "^1.0.1"
+ babel-runtime "^6.23.0"
+ colorbrewer "^1.0.0"
+ d3-axis "^1.0.8"
+ d3-ease "^1.0.3"
+ d3-format "^1.2.0"
+ d3-interpolate "^1.1.5"
+ d3-scale "^1.0.6"
+ d3-scale-chromatic "^1.1.1"
+ d3-selection "^1.1.0"
+ d3-selection-multi "^1.0.1"
+ d3-shape "^1.2.0"
+ d3-time "^1.0.7"
+ d3-time-format "^2.0.5"
+ d3-transition "^1.1.0"
+ dom-resize "^1.0.3"
+ invariant "^2.1.1"
+ merge "^1.2.0"
+ moment "^2.18.1"
+ moment-duration-format "^1.3.0"
+ prop-types "^15.5.10"
+ react-hot-loader "4.1.2"
+ underscore "^1.8.3"
+
react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@@ -10205,6 +10447,11 @@ shallow-clone@^3.0.0:
dependencies:
kind-of "^6.0.2"
+shallowequal@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+ integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -11108,6 +11355,11 @@ uncontrollable@^7.0.0:
invariant "^2.2.4"
react-lifecycles-compat "^3.0.4"
+underscore@^1.8.3, underscore@^1.9.1:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e"
+ integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==
+
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
diff --git a/statistics_controller.go b/statistics_controller.go
new file mode 100644
index 0000000..65c7d58
--- /dev/null
+++ b/statistics_controller.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ log "github.com/sirupsen/logrus"
+ "time"
+)
+
+type StatisticRecord struct {
+ RangeStart time.Time `json:"range_start" bson:"_id"`
+ ConnectionsPerService map[uint16]int `json:"connections_per_service" bson:"connections_per_service"`
+ ClientBytesPerService map[uint16]int `json:"client_bytes_per_service" bson:"client_bytes_per_service"`
+ ServerBytesPerService map[uint16]int `json:"server_bytes_per_service" bson:"server_bytes_per_service"`
+ DurationPerService map[uint16]int64 `json:"duration_per_service" bson:"duration_per_service"`
+}
+
+type StatisticsFilter struct {
+ RangeFrom time.Time `form:"range_from"`
+ RangeTo time.Time `form:"range_to"`
+ Ports []uint16 `form:"ports"`
+ Metric string `form:"metric"`
+}
+
+type StatisticsController struct {
+ storage Storage
+ metrics []string
+}
+
+func NewStatisticsController(storage Storage) StatisticsController {
+ return StatisticsController{
+ storage: storage,
+ metrics: []string{"connections_per_service", "client_bytes_per_service",
+ "server_bytes_per_service", "duration_per_service"},
+ }
+}
+
+func (sc *StatisticsController) GetStatistics(context context.Context, filter StatisticsFilter) []StatisticRecord {
+ var statisticRecords []StatisticRecord
+ query := sc.storage.Find(Statistics).Context(context)
+ if !filter.RangeFrom.IsZero() {
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$lt": filter.RangeFrom}}})
+ }
+ if !filter.RangeTo.IsZero() {
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$gt": filter.RangeTo}}})
+ }
+ for _, port := range filter.Ports {
+ for _, metric := range sc.metrics {
+ if filter.Metric == "" || filter.Metric == metric {
+ query = query.Projection(OrderedDocument{{fmt.Sprintf("%s.%d", metric, port), 1}})
+ }
+ }
+ }
+ if filter.Metric != "" && len(filter.Ports) == 0 {
+ for _, metric := range sc.metrics {
+ if filter.Metric == metric {
+ query = query.Projection(OrderedDocument{{metric, 1}})
+ }
+ }
+ }
+
+ if err := query.All(&statisticRecords); err != nil {
+ log.WithError(err).WithField("filter", filter).Error("failed to retrieve statistics")
+ return []StatisticRecord{}
+ }
+ if statisticRecords == nil {
+ return []StatisticRecord{}
+ }
+
+ return statisticRecords
+}
diff --git a/storage.go b/storage.go
index aced06b..0888ce0 100644
--- a/storage.go
+++ b/storage.go
@@ -17,6 +17,7 @@ const ImportingSessions = "importing_sessions"
const Rules = "rules"
const Settings = "settings"
const Services = "services"
+const Statistics = "statistics"
var ZeroRowID [12]byte
@@ -57,6 +58,7 @@ func NewMongoStorage(uri string, port int, database string) (*MongoStorage, erro
Rules: db.Collection(Rules),
Settings: db.Collection(Settings),
Services: db.Collection(Services),
+ Statistics: db.Collection(Statistics),
}
if _, err := collections[Services].Indexes().CreateOne(ctx, mongo.IndexModel{
@@ -150,6 +152,7 @@ type UpdateOperation interface {
Filter(filter OrderedDocument) UpdateOperation
Upsert(upsertResults *interface{}) UpdateOperation
One(update interface{}) (bool, error)
+ OneComplex(update interface{}) (bool, error)
Many(update interface{}) (int64, error)
}
@@ -200,6 +203,22 @@ func (fo MongoUpdateOperation) One(update interface{}) (bool, error) {
return result.ModifiedCount == 1, nil
}
+func (fo MongoUpdateOperation) OneComplex(update interface{}) (bool, error) {
+ if fo.err != nil {
+ return false, fo.err
+ }
+
+ result, err := fo.collection.UpdateOne(fo.ctx, fo.filter, update, fo.opt)
+ if err != nil {
+ return false, err
+ }
+
+ if fo.upsertResult != nil {
+ *(fo.upsertResult) = result.UpsertedID
+ }
+ return result.ModifiedCount == 1, nil
+}
+
func (fo MongoUpdateOperation) Many(update interface{}) (int64, error) {
if fo.err != nil {
return 0, fo.err
@@ -238,6 +257,7 @@ func (storage *MongoStorage) Update(collectionName string) UpdateOperation {
type FindOperation interface {
Context(ctx context.Context) FindOperation
Filter(filter OrderedDocument) FindOperation
+ Projection(filter OrderedDocument) FindOperation
Sort(field string, ascending bool) FindOperation
Limit(n int64) FindOperation
First(result interface{}) error
@@ -247,6 +267,7 @@ type FindOperation interface {
type MongoFindOperation struct {
collection *mongo.Collection
filter OrderedDocument
+ projection OrderedDocument
ctx context.Context
optFind *options.FindOptions
optFindOne *options.FindOneOptions
@@ -266,6 +287,15 @@ func (fo MongoFindOperation) Filter(filter OrderedDocument) FindOperation {
return fo
}
+func (fo MongoFindOperation) Projection(projection OrderedDocument) FindOperation {
+ for _, elem := range projection {
+ fo.projection = append(fo.projection, primitive.E{Key: elem.Key, Value: elem.Value})
+ }
+ fo.optFindOne.SetProjection(fo.projection)
+ fo.optFind.SetProjection(fo.projection)
+ return fo
+}
+
func (fo MongoFindOperation) Limit(n int64) FindOperation {
fo.optFind.SetLimit(n)
return fo
@@ -321,6 +351,7 @@ func (storage *MongoStorage) Find(collectionName string) FindOperation {
op := MongoFindOperation{
collection: collection,
filter: OrderedDocument{},
+ projection: OrderedDocument{},
optFind: options.Find(),
optFindOne: options.FindOne(),
sorts: OrderedDocument{},
--
cgit v1.2.3-70-g09d2
From d5f94b76986615b255b77b2a7b7ed336e5ad4838 Mon Sep 17 00:00:00 2001
From: Emiliano Ciavatta
Date: Wed, 7 Oct 2020 14:58:48 +0200
Subject: Implement notifications
---
VERSION | 1 +
application_context.go | 10 +-
application_context_test.go | 4 +-
application_router.go | 27 +++-
application_router_test.go | 6 +-
caronte.go | 12 +-
frontend/package.json | 5 +-
frontend/src/backend.js | 3 +-
frontend/src/components/Connection.js | 11 +-
frontend/src/components/ConnectionContent.scss | 9 +-
frontend/src/components/Notifications.js | 60 +++++++++
frontend/src/components/Notifications.scss | 48 +++++++
frontend/src/components/panels/PcapPane.js | 33 ++---
frontend/src/components/panels/RulePane.js | 116 ++++++++---------
frontend/src/components/panels/ServicePane.js | 35 +++---
frontend/src/components/panels/common.scss | 4 +-
frontend/src/dispatcher.js | 35 ++++++
frontend/src/globals.js | 5 -
frontend/src/index.js | 6 +-
frontend/src/notifications.js | 40 ++++++
frontend/src/setupProxy.js | 7 ++
frontend/src/views/App.js | 43 ++++---
frontend/src/views/Connections.js | 57 ++++++---
frontend/src/views/Connections.scss | 3 +-
frontend/src/views/Footer.js | 20 ++-
frontend/yarn.lock | 32 ++++-
go.mod | 1 +
go.sum | 2 +
notification_controller.go | 165 +++++++++++++++++++++++++
pcap_importer.go | 7 +-
30 files changed, 624 insertions(+), 183 deletions(-)
create mode 100644 VERSION
create mode 100644 frontend/src/components/Notifications.js
create mode 100644 frontend/src/components/Notifications.scss
create mode 100644 frontend/src/dispatcher.js
delete mode 100644 frontend/src/globals.js
create mode 100644 frontend/src/notifications.js
create mode 100644 frontend/src/setupProxy.js
create mode 100644 notification_controller.go
(limited to 'frontend/package.json')
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..ce609ca
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.8
\ No newline at end of file
diff --git a/application_context.go b/application_context.go
index 6ea449d..9a9c97a 100644
--- a/application_context.go
+++ b/application_context.go
@@ -22,9 +22,10 @@ type ApplicationContext struct {
ConnectionStreamsController ConnectionStreamsController
StatisticsController StatisticsController
IsConfigured bool
+ Version string
}
-func CreateApplicationContext(storage Storage) (*ApplicationContext, error) {
+func CreateApplicationContext(storage Storage, version string) (*ApplicationContext, error) {
var configWrapper struct {
Config Config
}
@@ -45,9 +46,10 @@ func CreateApplicationContext(storage Storage) (*ApplicationContext, error) {
}
applicationContext := &ApplicationContext{
- Storage: storage,
- Config: configWrapper.Config,
- Accounts: accountsWrapper.Accounts,
+ Storage: storage,
+ Config: configWrapper.Config,
+ Accounts: accountsWrapper.Accounts,
+ Version: version,
}
applicationContext.configure()
diff --git a/application_context_test.go b/application_context_test.go
index eed0fd6..28c81a5 100644
--- a/application_context_test.go
+++ b/application_context_test.go
@@ -10,7 +10,7 @@ func TestCreateApplicationContext(t *testing.T) {
wrapper := NewTestStorageWrapper(t)
wrapper.AddCollection(Settings)
- appContext, err := CreateApplicationContext(wrapper.Storage)
+ appContext, err := CreateApplicationContext(wrapper.Storage, "test")
assert.NoError(t, err)
assert.False(t, appContext.IsConfigured)
assert.Zero(t, appContext.Config)
@@ -39,7 +39,7 @@ func TestCreateApplicationContext(t *testing.T) {
appContext.SetConfig(config)
appContext.SetAccounts(accounts)
- checkAppContext, err := CreateApplicationContext(wrapper.Storage)
+ checkAppContext, err := CreateApplicationContext(wrapper.Storage, "test")
assert.NoError(t, err)
assert.True(t, checkAppContext.IsConfigured)
assert.Equal(t, checkAppContext.Config, config)
diff --git a/application_router.go b/application_router.go
index 8b5e32f..6431e22 100644
--- a/application_router.go
+++ b/application_router.go
@@ -13,7 +13,8 @@ import (
"time"
)
-func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine {
+func CreateApplicationRouter(applicationContext *ApplicationContext,
+ notificationController *NotificationController) *gin.Engine {
router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())
@@ -47,6 +48,13 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
applicationContext.SetAccounts(settings.Accounts)
c.JSON(http.StatusAccepted, gin.H{})
+ notificationController.Notify("setup", InsertNotification, gin.H{})
+ })
+
+ router.GET("/ws", func(c *gin.Context) {
+ if err := notificationController.NotificationHandler(c.Writer, c.Request); err != nil {
+ serverError(c, err)
+ }
})
api := router.Group("/api")
@@ -68,7 +76,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
if id, err := applicationContext.RulesManager.AddRule(c, rule); err != nil {
unprocessableEntity(c, err)
} else {
- success(c, UnorderedDocument{"id": id})
+ response := UnorderedDocument{"id": id}
+ success(c, response)
+ notificationController.Notify("rules.new", InsertNotification, response)
}
})
@@ -107,6 +117,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
notFound(c, UnorderedDocument{"id": id})
} else {
success(c, rule)
+ notificationController.Notify("rules.edit", UpdateNotification, rule)
}
})
@@ -126,7 +137,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
if sessionID, err := applicationContext.PcapImporter.ImportPcap(fileName, flushAll); err != nil {
unprocessableEntity(c, err)
} else {
- c.JSON(http.StatusAccepted, gin.H{"session": sessionID})
+ response := gin.H{"session": sessionID}
+ c.JSON(http.StatusAccepted, response)
+ notificationController.Notify("pcap.upload", InsertNotification, response)
}
})
@@ -158,7 +171,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
unprocessableEntity(c, err)
} else {
- c.JSON(http.StatusAccepted, gin.H{"session": sessionID})
+ response := gin.H{"session": sessionID}
+ c.JSON(http.StatusAccepted, response)
+ notificationController.Notify("pcap.file", InsertNotification, response)
}
})
@@ -195,6 +210,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
session := gin.H{"session": sessionID}
if cancelled := applicationContext.PcapImporter.CancelSession(sessionID); cancelled {
c.JSON(http.StatusAccepted, session)
+ notificationController.Notify("sessions.delete", DeleteNotification, session)
} else {
notFound(c, session)
}
@@ -254,6 +270,8 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
if result {
c.Status(http.StatusAccepted)
+ notificationController.Notify("connections.action", UpdateNotification,
+ gin.H{"connection_id": c.Param("id"), "action": c.Param("action")})
} else {
notFound(c, gin.H{"connection": id})
}
@@ -285,6 +303,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
if err := applicationContext.ServicesController.SetService(c, service); err == nil {
success(c, service)
+ notificationController.Notify("services.edit", UpdateNotification, service)
} else {
unprocessableEntity(c, err)
}
diff --git a/application_router_test.go b/application_router_test.go
index 4225ab9..f4804e3 100644
--- a/application_router_test.go
+++ b/application_router_test.go
@@ -148,10 +148,12 @@ func NewRouterTestToolkit(t *testing.T, withSetup bool) *RouterTestToolkit {
wrapper := NewTestStorageWrapper(t)
wrapper.AddCollection(Settings)
- appContext, err := CreateApplicationContext(wrapper.Storage)
+ appContext, err := CreateApplicationContext(wrapper.Storage, "test")
require.NoError(t, err)
gin.SetMode(gin.ReleaseMode)
- router := CreateApplicationRouter(appContext)
+ notificationController := NewNotificationController(appContext)
+ go notificationController.Run()
+ router := CreateApplicationRouter(appContext, notificationController)
toolkit := RouterTestToolkit{
appContext: appContext,
diff --git a/caronte.go b/caronte.go
index 098642c..288563c 100644
--- a/caronte.go
+++ b/caronte.go
@@ -4,6 +4,7 @@ import (
"flag"
"fmt"
log "github.com/sirupsen/logrus"
+ "io/ioutil"
)
func main() {
@@ -22,12 +23,19 @@ func main() {
log.WithError(err).WithFields(logFields).Fatal("failed to connect to MongoDB")
}
- applicationContext, err := CreateApplicationContext(storage)
+ versionBytes, err := ioutil.ReadFile("VERSION")
+ if err != nil {
+ log.WithError(err).Fatal("failed to load version file")
+ }
+
+ applicationContext, err := CreateApplicationContext(storage, string(versionBytes))
if err != nil {
log.WithError(err).WithFields(logFields).Fatal("failed to create application context")
}
- applicationRouter := CreateApplicationRouter(applicationContext)
+ notificationController := NewNotificationController(applicationContext)
+ go notificationController.Run()
+ applicationRouter := CreateApplicationRouter(applicationContext, notificationController)
if applicationRouter.Run(fmt.Sprintf("%s:%v", *bindAddress, *bindPort)) != nil {
log.WithError(err).WithFields(logFields).Fatal("failed to create the server")
}
diff --git a/frontend/package.json b/frontend/package.json
index 5bc13f1..b3ad03a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,7 +14,7 @@
"classnames": "^2.2.6",
"dompurify": "^2.1.1",
"eslint-config-react-app": "^5.2.1",
- "flux": "^3.1.3",
+ "http-proxy-middleware": "^1.0.5",
"lodash": "^4.17.20",
"node-sass": "^4.14.0",
"pondjs": "^0.9.0",
@@ -50,6 +50,5 @@
"last 1 firefox version",
"last 1 safari version"
]
- },
- "proxy": "http://localhost:3333"
+ }
}
diff --git a/frontend/src/backend.js b/frontend/src/backend.js
index 72ee9dd..c7abd80 100644
--- a/frontend/src/backend.js
+++ b/frontend/src/backend.js
@@ -1,4 +1,3 @@
-
async function json(method, url, data, json, headers) {
const options = {
method: method,
@@ -28,7 +27,7 @@ async function json(method, url, data, json, headers) {
const backend = {
get: (url = "", headers = null) =>
- json("GET", url, null,null, headers),
+ json("GET", url, null, null, headers),
post: (url = "", data = null, headers = null) =>
json("POST", url, null, data, headers),
put: (url = "", data = null, headers = null) =>
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js
index 44f9f18..46a0cab 100644
--- a/frontend/src/components/Connection.js
+++ b/frontend/src/components/Connection.js
@@ -48,10 +48,11 @@ class Connection extends Component {
render() {
let conn = this.props.data;
let serviceName = "/dev/null";
- let serviceColor = "#0F192E";
- if (conn.service.port !== 0) {
- serviceName = conn.service.name;
- serviceColor = conn.service.color;
+ let serviceColor = "#0f192e";
+ if (this.props.services[conn["port_dst"]]) {
+ const service = this.props.services[conn["port_dst"]];
+ serviceName = service.name;
+ serviceColor = service.color;
}
let startedAt = new Date(conn.started_at);
let closedAt = new Date(conn.closed_at);
@@ -87,7 +88,7 @@ class Connection extends Component {
this.props.addServicePortFilter(conn.port_dst)} />
+ onClick={() => this.props.addServicePortFilter(conn.port_dst)}/>
|
{conn.ip_src} |
diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/ConnectionContent.scss
index de4d699..f4edec9 100644
--- a/frontend/src/components/ConnectionContent.scss
+++ b/frontend/src/components/ConnectionContent.scss
@@ -2,7 +2,6 @@
.connection-content {
height: 100%;
- padding: 10px 10px 0;
background-color: $color-primary-0;
pre {
@@ -91,12 +90,12 @@
.connection-content-header {
height: 33px;
padding: 0;
- background-color: $color-primary-2;
+ background-color: $color-primary-3;
.header-info {
font-size: 12px;
padding-top: 7px;
- padding-left: 20px;
+ padding-left: 25px;
}
.header-actions {
@@ -104,6 +103,10 @@
.choice-field {
margin-top: -5px;
+
+ .field-value {
+ background-color: $color-primary-3;
+ }
}
}
}
diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js
new file mode 100644
index 0000000..4d6dcd4
--- /dev/null
+++ b/frontend/src/components/Notifications.js
@@ -0,0 +1,60 @@
+import React, {Component} from 'react';
+import './Notifications.scss';
+import dispatcher from "../dispatcher";
+
+const _ = require('lodash');
+const classNames = require('classnames');
+
+class Notifications extends Component {
+
+ state = {
+ notifications: [],
+ closedNotifications: [],
+ };
+
+ componentDidMount() {
+ dispatcher.register("notifications", notification => {
+ const notifications = this.state.notifications;
+ notifications.push(notification);
+ this.setState({notifications});
+
+ setTimeout(() => {
+ const notifications = this.state.notifications;
+ notification.open = true;
+ this.setState({notifications});
+ }, 100);
+
+ setTimeout(() => {
+ const notifications = _.without(this.state.notifications, notification);
+ const closedNotifications = this.state.closedNotifications.concat([notification]);
+ notification.closed = true;
+ this.setState({notifications, closedNotifications});
+ }, 5000);
+
+ setTimeout(() => {
+ const closedNotifications = _.without(this.state.closedNotifications, notification);
+ this.setState({closedNotifications});
+ }, 6000);
+ });
+ }
+
+ render() {
+ return (
+
+
+ {
+ this.state.closedNotifications.concat(this.state.notifications).map(n =>
+
+
{n.event}
+ {JSON.stringify(n.message)}
+
+ )
+ }
+
+
+ );
+ }
+}
+
+export default Notifications;
diff --git a/frontend/src/components/Notifications.scss b/frontend/src/components/Notifications.scss
new file mode 100644
index 0000000..b0c334b
--- /dev/null
+++ b/frontend/src/components/Notifications.scss
@@ -0,0 +1,48 @@
+@import "../colors.scss";
+
+.notifications {
+ position: absolute;
+
+ left: 30px;
+ bottom: 50px;
+ z-index: 50;
+
+ .notifications-list {
+
+ }
+
+ .notification {
+ background-color: $color-green;
+ border-left: 5px solid $color-green-dark;
+ padding: 10px;
+ margin: 10px 0;
+ width: 250px;
+ color: $color-green-light;
+ transform: translateX(-300px);
+ transition: all 1s ease;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .notification-title {
+ font-size: 0.9em;
+ margin: 0;
+ }
+
+ .notification-description {
+ font-size: 0.8em;
+ }
+
+ &.notification-open {
+ transform: translateX(0px);
+ }
+
+ &.notification-closed {
+ transform: translateY(-50px);
+ opacity: 0;
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js
index 31d8815..13f7cb3 100644
--- a/frontend/src/components/panels/PcapPane.js
+++ b/frontend/src/components/panels/PcapPane.js
@@ -9,28 +9,31 @@ import CheckField from "../fields/CheckField";
import TextField from "../fields/TextField";
import ButtonField from "../fields/ButtonField";
import LinkPopover from "../objects/LinkPopover";
+import dispatcher from "../../dispatcher";
class PcapPane extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- sessions: [],
- isUploadFileValid: true,
- isUploadFileFocused: false,
- uploadFlushAll: false,
- isFileValid: true,
- isFileFocused: false,
- fileValue: "",
- processFlushAll: false,
- deleteOriginalFile: false
- };
- }
+ state = {
+ sessions: [],
+ isUploadFileValid: true,
+ isUploadFileFocused: false,
+ uploadFlushAll: false,
+ isFileValid: true,
+ isFileFocused: false,
+ fileValue: "",
+ processFlushAll: false,
+ deleteOriginalFile: false
+ };
componentDidMount() {
this.loadSessions();
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "pcap.upload" || payload.event === "pcap.file") {
+ this.loadSessions();
+ }
+ });
+
document.title = "caronte:~/pcaps$";
}
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js
index 4641378..76f3ac0 100644
--- a/frontend/src/components/panels/RulePane.js
+++ b/frontend/src/components/panels/RulePane.js
@@ -14,35 +14,13 @@ import ButtonField from "../fields/ButtonField";
import validation from "../../validation";
import LinkPopover from "../objects/LinkPopover";
import {randomClassName} from "../../utils";
+import dispatcher from "../../dispatcher";
const classNames = require('classnames');
const _ = require('lodash');
class RulePane extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- rules: [],
- newRule: this.emptyRule,
- newPattern: this.emptyPattern
- };
-
- this.directions = {
- 0: "both",
- 1: "c->s",
- 2: "s->c"
- };
- }
-
- componentDidMount() {
- this.reset();
- this.loadRules();
-
- document.title = "caronte:~/rules$";
- }
-
emptyRule = {
"name": "",
"color": "",
@@ -60,7 +38,6 @@ class RulePane extends Component {
},
"version": 0
};
-
emptyPattern = {
"regex": "",
"flags": {
@@ -74,6 +51,34 @@ class RulePane extends Component {
"max_occurrences": 0,
"direction": 0
};
+ state = {
+ rules: [],
+ newRule: this.emptyRule,
+ newPattern: this.emptyPattern
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.directions = {
+ 0: "both",
+ 1: "c->s",
+ 2: "s->c"
+ };
+ }
+
+ componentDidMount() {
+ this.reset();
+ this.loadRules();
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "rules.new" || payload.event === "rules.edit") {
+ this.loadRules();
+ }
+ });
+
+ document.title = "caronte:~/rules$";
+ }
loadRules = () => {
backend.get("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status}))
@@ -226,17 +231,17 @@ class RulePane extends Component {
{
this.reset();
this.setState({selectedRule: _.cloneDeep(r)});
- }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id })}>
+ }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}>
{r["id"].substring(0, 8)} |
{r["name"]} |
- |
+ |
{r["notes"]} |
);
let patterns = (this.state.selectedPattern == null && !isUpdate ?
- rule.patterns.concat(this.state.newPattern) :
- rule.patterns
+ rule.patterns.concat(this.state.newPattern) :
+ rule.patterns
).map(p => p === pattern ?
@@ -244,7 +249,7 @@ class RulePane extends Component {
onChange={(v) => {
this.updateParam(() => pattern.regex = v);
this.setState({patternRegexFocused: pattern.regex === ""});
- }} />
+ }}/>
|
this.updateParam(() => pattern.flags.caseless = v)}/> |
@@ -259,34 +264,35 @@ class RulePane extends Component {
this.updateParam(() => pattern.min_occurrences = v)} />
+ onChange={(v) => this.updateParam(() => pattern.min_occurrences = v)}/>
|
this.updateParam(() => pattern.max_occurrences = v)} />
+ onChange={(v) => this.updateParam(() => pattern.max_occurrences = v)}/>
|
s", "s->c"]}
value={this.directions[pattern.direction]}
- onChange={(v) => this.updateParam(() => pattern.direction = v)} /> |
+ onChange={(v) => this.updateParam(() => pattern.direction = v)}/>
{this.state.selectedPattern == null ?
this.addPattern(p)}/> :
- this.updatePattern(p)}/>}
+ this.updatePattern(p)}/>}
|
:
{p.regex} |
- {p.flags.caseless ? "yes": "no"} |
- {p.flags.dot_all ? "yes": "no"} |
- {p.flags.multi_line ? "yes": "no"} |
- {p.flags.utf_8_mode ? "yes": "no"} |
- {p.flags.unicode_property ? "yes": "no"} |
+ {p.flags.caseless ? "yes" : "no"} |
+ {p.flags.dot_all ? "yes" : "no"} |
+ {p.flags.multi_line ? "yes" : "no"} |
+ {p.flags.utf_8_mode ? "yes" : "no"} |
+ {p.flags.unicode_property ? "yes" : "no"} |
{p.min_occurrences} |
{p.max_occurrences} |
{this.directions[p.direction]} |
{!isUpdate && this.editPattern(p) }/> | }
+ onClick={() => this.editPattern(p)}/>}
);
@@ -296,9 +302,9 @@ class RulePane extends Component {
GET /api/rules
{this.state.rulesStatusCode &&
- }
+ }
@@ -327,7 +333,7 @@ class RulePane extends Component {
+ placement="left"/>
@@ -336,11 +342,11 @@ class RulePane extends Component {
this.updateParam((r) => r.name = v)}
- error={this.state.ruleNameError} />
+ error={this.state.ruleNameError}/>
this.updateParam((r) => r.color = v)} />
+ onChange={(v) => this.updateParam((r) => r.color = v)}/>
this.updateParam((r) => r.notes = v)} />
+ onChange={(v) => this.updateParam((r) => r.notes = v)}/>
@@ -348,29 +354,29 @@ class RulePane extends Component {
this.updateParam((r) => r.filter.service_port = v)}
min={0} max={65565} error={this.state.ruleServicePortError}
- readonly={isUpdate} />
+ readonly={isUpdate}/>
this.updateParam((r) => r.filter.client_port = v)}
min={0} max={65565} error={this.state.ruleClientPortError}
- readonly={isUpdate} />
+ readonly={isUpdate}/>
this.updateParam((r) => r.filter.client_address = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.client_address = v)}/>
this.updateParam((r) => r.filter.min_duration = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.min_duration = v)}/>
this.updateParam((r) => r.filter.max_duration = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.max_duration = v)}/>
this.updateParam((r) => r.filter.min_bytes = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.min_bytes = v)}/>
this.updateParam((r) => r.filter.max_bytes = v)} />
+ onChange={(v) => this.updateParam((r) => r.filter.max_bytes = v)}/>
@@ -388,7 +394,7 @@ class RulePane extends Component {
min |
max |
direction |
- {!isUpdate && actions | }
+ {!isUpdate && actions | }
@@ -403,7 +409,7 @@ class RulePane extends Component {
{}
+ bordered onClick={isUpdate ? this.updateRule : this.addRule}/>
diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js
index 0e99652..22c6655 100644
--- a/frontend/src/components/panels/ServicePane.js
+++ b/frontend/src/components/panels/ServicePane.js
@@ -12,28 +12,13 @@ import ButtonField from "../fields/ButtonField";
import validation from "../../validation";
import LinkPopover from "../objects/LinkPopover";
import {createCurlCommand} from "../../utils";
+import dispatcher from "../../dispatcher";
const classNames = require('classnames');
const _ = require('lodash');
class ServicePane extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- services: [],
- currentService: this.emptyService,
- };
-
- document.title = "caronte:~/services$";
- }
-
- componentDidMount() {
- this.reset();
- this.loadServices();
- }
-
emptyService = {
"port": 0,
"name": "",
@@ -41,6 +26,24 @@ class ServicePane extends Component {
"notes": ""
};
+ state = {
+ services: [],
+ currentService: this.emptyService,
+ };
+
+ componentDidMount() {
+ this.reset();
+ this.loadServices();
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "services.edit") {
+ this.loadServices();
+ }
+ });
+
+ document.title = "caronte:~/services$";
+ }
+
loadServices = () => {
backend.get("/api/services")
.then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status}))
diff --git a/frontend/src/components/panels/common.scss b/frontend/src/components/panels/common.scss
index 121a917..1468f35 100644
--- a/frontend/src/components/panels/common.scss
+++ b/frontend/src/components/panels/common.scss
@@ -2,11 +2,9 @@
.pane-container {
height: 100%;
- padding: 10px 10px 0;
background-color: $color-primary-3;
.pane-section {
- margin-bottom: 10px;
background-color: $color-primary-0;
.section-header {
@@ -14,7 +12,7 @@
font-weight: 500;
display: flex;
padding: 5px 10px;
- background-color: $color-primary-2;
+ background-color: $color-primary-3;
.api-request {
flex: 1;
diff --git a/frontend/src/dispatcher.js b/frontend/src/dispatcher.js
new file mode 100644
index 0000000..4b8b5a4
--- /dev/null
+++ b/frontend/src/dispatcher.js
@@ -0,0 +1,35 @@
+
+class Dispatcher {
+
+ constructor() {
+ this.listeners = [];
+ }
+
+ dispatch = (topic, payload) => {
+ this.listeners.filter(l => l.topic === topic).forEach(l => l.callback(payload));
+ };
+
+ register = (topic, callback) => {
+ if (typeof callback !== "function") {
+ throw new Error("dispatcher callback must be a function");
+ }
+ if (typeof topic === "string") {
+ this.listeners.push({topic, callback});
+ } else if (typeof topic === "object" && Array.isArray(topic)) {
+ topic.forEach(e => {
+ if (typeof e !== "string") {
+ throw new Error("all topics must be strings");
+ }
+ });
+
+ topic.forEach(e => this.listeners.push({e, callback}));
+ } else {
+ throw new Error("topic must be a string or an array of strings");
+ }
+ };
+
+}
+
+const dispatcher = new Dispatcher();
+
+export default dispatcher;
diff --git a/frontend/src/globals.js b/frontend/src/globals.js
deleted file mode 100644
index cd4dc64..0000000
--- a/frontend/src/globals.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import {Dispatcher} from "flux";
-
-const dispatcher = new Dispatcher();
-
-export default dispatcher;
diff --git a/frontend/src/index.js b/frontend/src/index.js
index 2e90371..beb52ae 100644
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -4,6 +4,9 @@ import 'bootstrap/dist/css/bootstrap.css';
import './index.scss';
import App from './views/App';
import * as serviceWorker from './serviceWorker';
+import notifications from "./notifications";
+
+notifications.createWebsocket();
ReactDOM.render(
@@ -12,7 +15,4 @@ ReactDOM.render(
document.getElementById('root')
);
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
diff --git a/frontend/src/notifications.js b/frontend/src/notifications.js
new file mode 100644
index 0000000..2a77ffb
--- /dev/null
+++ b/frontend/src/notifications.js
@@ -0,0 +1,40 @@
+import log from "./log";
+import dispatcher from "./dispatcher";
+
+class Notifications {
+
+ constructor() {
+ const location = document.location;
+ this.wsUrl = `ws://${location.hostname}${location.port ? ":" + location.port : ""}/ws`;
+ }
+
+ createWebsocket = () => {
+ this.ws = new WebSocket(this.wsUrl);
+ this.ws.onopen = this.onWebsocketOpen;
+ this.ws.onerror = this.onWebsocketError;
+ this.ws.onclose = this.onWebsocketClose;
+ this.ws.onmessage = this.onWebsocketMessage;
+ };
+
+ onWebsocketOpen = () => {
+ log.debug("Connected to backend with websocket");
+ };
+
+ onWebsocketError = (err) => {
+ this.ws.close();
+ log.error("Websocket error", err);
+ setTimeout(() => this.createWebsocket(), 3000);
+ };
+
+ onWebsocketClose = () => {
+ log.debug("Closed websocket connection with backend");
+ };
+
+ onWebsocketMessage = (message) => {
+ dispatcher.dispatch("notifications", JSON.parse(message.data));
+ };
+}
+
+const notifications = new Notifications();
+
+export default notifications;
diff --git a/frontend/src/setupProxy.js b/frontend/src/setupProxy.js
new file mode 100644
index 0000000..6f082c8
--- /dev/null
+++ b/frontend/src/setupProxy.js
@@ -0,0 +1,7 @@
+const { createProxyMiddleware } = require('http-proxy-middleware');
+
+module.exports = function(app) {
+ app.use(createProxyMiddleware("/api", { target: "http://localhost:3333" }));
+ app.use(createProxyMiddleware("/setup", { target: "http://localhost:3333" }));
+ app.use(createProxyMiddleware("/ws", { target: "http://localhost:3333", ws: true }));
+};
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js
index 00d9110..c14b7f5 100644
--- a/frontend/src/views/App.js
+++ b/frontend/src/views/App.js
@@ -5,18 +5,22 @@ import MainPane from "../components/panels/MainPane";
import Footer from "./Footer";
import {BrowserRouter as Router} from "react-router-dom";
import Filters from "./Filters";
-import backend from "../backend";
import ConfigurationPane from "../components/panels/ConfigurationPane";
-import log from "../log";
+import Notifications from "../components/Notifications";
+import dispatcher from "../dispatcher";
class App extends Component {
state = {};
componentDidMount() {
- backend.get("/api/services").then(_ => {
- log.debug("Caronte is already configured. Loading main..");
- this.setState({configured: true});
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "connected") {
+ this.setState({
+ connected: true,
+ configured: payload.message["is_configured"]
+ });
+ }
});
setInterval(() => {
@@ -36,19 +40,22 @@ class App extends Component {
return (
-
-
- this.setState({filterWindowOpen: true})}/>
-
-
- {this.state.configured ? :
- this.setState({configured: true})}/>}
- {modal}
-
-
- {this.state.configured && }
-
-
+
+ {this.state.connected &&
+
+
+ this.setState({filterWindowOpen: true})}/>
+
+
+ {this.state.configured ? :
+ this.setState({configured: true})}/>}
+ {modal}
+
+
+ {this.state.configured && }
+
+
+ }
);
}
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
index fe655b3..bd631a2 100644
--- a/frontend/src/views/Connections.js
+++ b/frontend/src/views/Connections.js
@@ -6,9 +6,9 @@ import {Redirect} from 'react-router';
import {withRouter} from "react-router-dom";
import backend from "../backend";
import ConnectionMatchedRules from "../components/ConnectionMatchedRules";
-import dispatcher from "../globals";
import log from "../log";
import ButtonField from "../components/fields/ButtonField";
+import dispatcher from "../dispatcher";
class Connections extends Component {
@@ -17,8 +17,6 @@ class Connections extends Component {
connections: [],
firstConnection: null,
lastConnection: null,
- flagRule: null,
- rules: null,
queryString: null
};
@@ -41,14 +39,24 @@ class Connections extends Component {
// TODO: scroll to initial connection
}
- dispatcher.register((payload) => {
- if (payload.actionType === "timeline-update") {
- this.connectionsListRef.current.scrollTop = 0;
- this.loadConnections({
- started_after: Math.round(payload.from.getTime() / 1000),
- started_before: Math.round(payload.to.getTime() / 1000),
- limit: this.maxConnections
- }).then(() => log.info(`Loading connections between ${payload.from} and ${payload.to}`));
+ dispatcher.register("timeline_updates", payload => {
+ this.connectionsListRef.current.scrollTop = 0;
+ this.loadConnections({
+ started_after: Math.round(payload.from.getTime() / 1000),
+ started_before: Math.round(payload.to.getTime() / 1000),
+ limit: this.maxConnections
+ }).then(() => log.info(`Loading connections between ${payload.from} and ${payload.to}`));
+ });
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "rules.new" || payload.event === "rules.edit") {
+ this.loadRules().then(() => log.debug("Loaded connection rules after notification update"));
+ }
+ });
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "services.edit") {
+ this.loadServices().then(() => log.debug("Services reloaded after notification update"));
}
});
}
@@ -116,6 +124,13 @@ class Connections extends Component {
}
this.setState({loading: true});
+ if (!this.state.rules) {
+ await this.loadRules();
+ }
+ if (!this.state.services) {
+ await this.loadServices();
+ }
+
let res = (await backend.get(`${url}?${urlParams}`)).json;
let connections = this.state.connections;
@@ -154,28 +169,29 @@ class Connections extends Component {
}
}
- let rules = this.state.rules;
- if (rules == null) {
- rules = (await backend.get("/api/rules")).json;
- }
-
this.setState({
loading: false,
connections: connections,
- rules: rules,
firstConnection: firstConnection,
lastConnection: lastConnection
});
if (firstConnection != null && lastConnection != null) {
- dispatcher.dispatch({
- actionType: "connections-update",
+ dispatcher.dispatch("connection_updates", {
from: new Date(lastConnection["started_at"]),
to: new Date(firstConnection["started_at"])
});
}
}
+ loadRules = async () => {
+ return backend.get("/api/rules").then(res => this.setState({rules: res.json}));
+ };
+
+ loadServices = async () => {
+ return backend.get("/api/services").then(res => this.setState({services: res.json}));
+ };
+
render() {
let redirect;
let queryString = this.state.queryString !== null ? this.state.queryString : "";
@@ -222,7 +238,8 @@ class Connections extends Component {
selected={this.state.selected === c.id}
onMarked={marked => c.marked = marked}
onEnabled={enabled => c.hidden = !enabled}
- addServicePortFilter={this.addServicePortFilter}/>,
+ addServicePortFilter={this.addServicePortFilter}
+ services={this.state.services}/>,
c.matched_rules.length > 0 &&
- log.debug("Statistics loaded after mount"));
-
- dispatcher.register((payload) => {
- if (payload.actionType === "connections-update") {
- this.setState({
- selection: new TimeRange(payload.from, payload.to),
- });
- }
+ this.loadStatistics(this.state.metric, filteredPort).then(() => log.debug("Statistics loaded after mount"));
+
+ dispatcher.register("connection_updates", payload => {
+ this.setState({
+ selection: new TimeRange(payload.from, payload.to),
+ });
});
}
@@ -109,8 +106,7 @@ class Footer extends Component {
clearTimeout(this.selectionTimeout);
}
this.selectionTimeout = setTimeout(() => {
- dispatcher.dispatch({
- actionType: "timeline-update",
+ dispatcher.dispatch("timeline_updates", {
from: timeRange.begin(),
to: timeRange.end()
});
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index fa150ab..e3cade9 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1656,6 +1656,13 @@
"@types/minimatch" "*"
"@types/node" "*"
+"@types/http-proxy@^1.17.4":
+ version "1.17.4"
+ resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
+ integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
+ dependencies:
+ "@types/node" "*"
+
"@types/invariant@^2.2.33":
version "2.2.34"
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.34.tgz#05e4f79f465c2007884374d4795452f995720bbe"
@@ -2707,7 +2714,7 @@ braces@^2.3.1, braces@^2.3.2:
split-string "^3.0.2"
to-regex "^3.0.1"
-braces@~3.0.2:
+braces@^3.0.1, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -5738,7 +5745,18 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11"
micromatch "^3.1.10"
-http-proxy@^1.17.0:
+http-proxy-middleware@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.5.tgz#4c6e25d95a411e3d750bc79ccf66290675176dc2"
+ integrity sha512-CKzML7u4RdGob8wuKI//H8Ein6wNTEQR7yjVEzPbhBLGdOfkfvgTnp2HLnniKBDP9QW4eG10/724iTWLBeER3g==
+ dependencies:
+ "@types/http-proxy" "^1.17.4"
+ http-proxy "^1.18.1"
+ is-glob "^4.0.1"
+ lodash "^4.17.19"
+ micromatch "^4.0.2"
+
+http-proxy@^1.17.0, http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
@@ -7434,6 +7452,14 @@ micromatch@^3.1.10, micromatch@^3.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.2"
+micromatch@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+ integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+ dependencies:
+ braces "^3.0.1"
+ picomatch "^2.0.5"
+
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -8405,7 +8431,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
-picomatch@^2.0.4, picomatch@^2.2.1:
+picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
diff --git a/go.mod b/go.mod
index 308b16b..404f64c 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/go-playground/validator/v10 v10.2.0
github.com/golang/protobuf v1.3.5 // indirect
github.com/google/gopacket v1.1.17
+ github.com/gorilla/websocket v1.4.2
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/sirupsen/logrus v1.4.2
diff --git a/go.sum b/go.sum
index fd63c39..d29e0cb 100644
--- a/go.sum
+++ b/go.sum
@@ -58,6 +58,8 @@ github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbB
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
diff --git a/notification_controller.go b/notification_controller.go
new file mode 100644
index 0000000..88c9e8c
--- /dev/null
+++ b/notification_controller.go
@@ -0,0 +1,165 @@
+package main
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+ log "github.com/sirupsen/logrus"
+ "net"
+ "net/http"
+ "time"
+)
+
+const (
+ InsertNotification = "insert"
+ UpdateNotification = "update"
+ DeleteNotification = "delete"
+
+ writeWait = 10 * time.Second
+ pongWait = 60 * time.Second
+ pingPeriod = (pongWait * 9) / 10
+ maxMessageSize = 512
+)
+
+type NotificationController struct {
+ upgrader websocket.Upgrader
+ clients map[net.Addr]*client
+ broadcast chan interface{}
+ register chan *client
+ unregister chan *client
+ applicationContext *ApplicationContext
+}
+
+func NewNotificationController(applicationContext *ApplicationContext) *NotificationController {
+ return &NotificationController{
+ upgrader: websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ },
+ clients: make(map[net.Addr]*client),
+ broadcast: make(chan interface{}),
+ register: make(chan *client),
+ unregister: make(chan *client),
+ applicationContext: applicationContext,
+ }
+}
+
+type client struct {
+ conn *websocket.Conn
+ send chan interface{}
+ notificationController *NotificationController
+}
+
+func (wc *NotificationController) NotificationHandler(w http.ResponseWriter, r *http.Request) error {
+ conn, err := wc.upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.WithError(err).Error("failed to set websocket upgrade")
+ return err
+ }
+
+ client := &client{
+ conn: conn,
+ send: make(chan interface{}),
+ notificationController: wc,
+ }
+ wc.register <- client
+ go client.readPump()
+ go client.writePump()
+
+ return nil
+}
+
+func (wc *NotificationController) Run() {
+ for {
+ select {
+ case client := <-wc.register:
+ wc.clients[client.conn.RemoteAddr()] = client
+ payload := gin.H{"event": "connected", "message": gin.H{
+ "version": wc.applicationContext.Version,
+ "is_configured": wc.applicationContext.IsConfigured,
+ "connected_clients": len(wc.clients),
+ }}
+ client.send <- payload
+ log.WithField("connected_clients", len(wc.clients)).
+ WithField("remote_address", client.conn.RemoteAddr()).
+ Info("[+] a websocket client connected")
+ case client := <-wc.unregister:
+ if _, ok := wc.clients[client.conn.RemoteAddr()]; ok {
+ close(client.send)
+ _ = client.conn.WriteMessage(websocket.CloseMessage, nil)
+ _ = client.conn.Close()
+ delete(wc.clients, client.conn.RemoteAddr())
+ log.WithField("connected_clients", len(wc.clients)).
+ WithField("remote_address", client.conn.RemoteAddr()).
+ Info("[-] a websocket client disconnected")
+ }
+ case payload := <-wc.broadcast:
+ for _, client := range wc.clients {
+ select {
+ case client.send <- payload:
+ default:
+ close(client.send)
+ delete(wc.clients, client.conn.RemoteAddr())
+ }
+ }
+ }
+ }
+}
+
+func (wc *NotificationController) Notify(event string, eventType string, message interface{}) {
+ wc.broadcast <- gin.H{"event": event, "event_type": eventType, "message": message}
+}
+
+func (c *client) readPump() {
+ c.conn.SetReadLimit(maxMessageSize)
+ if err := c.conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
+ c.close()
+ return
+ }
+ c.conn.SetPongHandler(func(string) error { return c.conn.SetReadDeadline(time.Now().Add(pongWait)) })
+ for {
+ if _, _, err := c.conn.ReadMessage(); err != nil {
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
+ log.WithError(err).WithField("remote_address", c.conn.RemoteAddr()).
+ Warn("unexpected websocket disconnection")
+ }
+ break
+ }
+ }
+
+ c.close()
+}
+
+func (c *client) writePump() {
+ ticker := time.NewTicker(pingPeriod)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case payload, ok := <-c.send:
+ if !ok {
+ return
+ }
+ if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
+ c.close()
+ return
+ }
+ if err := c.conn.WriteJSON(payload); err != nil {
+ c.close()
+ return
+ }
+ case <-ticker.C:
+ if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
+ c.close()
+ return
+ }
+ if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
+ c.close()
+ return
+ }
+ }
+ }
+}
+
+func (c *client) close() {
+ c.notificationController.unregister <- c
+}
diff --git a/pcap_importer.go b/pcap_importer.go
index 1739b3f..78a5e6c 100644
--- a/pcap_importer.go
+++ b/pcap_importer.go
@@ -19,7 +19,6 @@ import (
const PcapsBasePath = "pcaps/"
const ProcessingPcapsBasePath = PcapsBasePath + "processing/"
const initialAssemblerPoolSize = 16
-const flushOlderThan = 5 * time.Minute
const importUpdateProgressInterval = 100 * time.Millisecond
type PcapImporter struct {
@@ -201,8 +200,8 @@ func (pi *PcapImporter) parsePcap(session ImportingSession, fileName string, flu
var servicePort uint16
var index int
- isDstServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Dst().Raw())
- isSrcServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Src().Raw())
+ isDstServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Dst().Raw())
+ isSrcServer := pi.serverNet.Contains(packet.NetworkLayer().NetworkFlow().Src().Raw())
if isDstServer && !isSrcServer {
servicePort = uint16(tcp.DstPort)
index = 0
@@ -284,7 +283,7 @@ func deleteProcessingFile(fileName string) {
}
func moveProcessingFile(sessionID string, fileName string) {
- if err := os.Rename(ProcessingPcapsBasePath + fileName, PcapsBasePath + sessionID + path.Ext(fileName)); err != nil {
+ if err := os.Rename(ProcessingPcapsBasePath+fileName, PcapsBasePath+sessionID+path.Ext(fileName)); err != nil {
log.WithError(err).Error("failed to move processed file")
}
}
--
cgit v1.2.3-70-g09d2