chg: Migrate to new framework

dev
Raphaël Vinot 2021-12-06 14:30:08 +01:00
parent aa7741710e
commit 80b5c045ac
99 changed files with 4552 additions and 2851 deletions

40
.gitignore vendored
View File

@ -1,3 +1,8 @@
# Local exclude
scraped/
*.swp
lookyloo/ete3_webserver/webapi.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -100,29 +105,20 @@ ENV/
# mypy
.mypy_cache/
# Redis
# web
secret_key
cache.pid
*.rdb
# Storage
# Local config files
config/*.json
config/*.json.bkp
rawdata
# ardb
storage/ardb.pid
storage/data
storage/repl
# Config file shadow server with password
bgpranking/config/shadowserver.json
# Ths shadow server config files are dynamically generated
bgpranking/config/modules/shadowserver_*.json
# Do not store the d3 lib in the repo
website/web/static/d3*.js
# Same got bootstrap-select
website/web/static/bootstrap-select*
# Session key
website/secret_key
*.swp
storage/db/
storage/kvrocks*
website/web/static/d3.v5.js
website/web/static/bootstrap-select.min.*

32
Pipfile
View File

@ -1,32 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
pybgpranking = {editable = true,path = "./client"}
bgpranking = {editable = true,path = "."}
redis = ">=3"
hiredis = "*"
python-dateutil = "*"
aiohttp = "*"
requests = "*"
simplejson = "*"
beautifulsoup4 = "*"
flask = "*"
flask-bootstrap = "*"
gunicorn = {extras = ["gevent"],version = "<20"}
pyipasnhistory = {editable = true,git = "https://github.com/D4-project/IPASN-History.git/",subdirectory = "client"}
pycountry = "*"
pid = {editable = true,git = "https://github.com/trbs/pid.git/"}
pytaxonomies = {editable = true,git = "https://github.com/MISP/PyTaxonomies.git"}
pymispgalaxies = {editable = true,git = "https://github.com/MISP/PyMISPGalaxies.git"}
Jinja2 = ">=2.10.1" # CVE-2019-10906
idna-ssl = {markers = "python_version < '3.7'"}
typing-extensions = {markers = "python_version < '3.7'"}
werkzeug = ">=0.15.3" # CVE-2019-14806
[requires]
python_version = "3"

627
Pipfile.lock generated
View File

@ -1,627 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "8408df42fa1da47b55611862c09307fd4e2cb77b9dd45aef412c6b99b99f6b10"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiohttp": {
"hashes": [
"sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0",
"sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6",
"sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf",
"sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9",
"sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e",
"sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0",
"sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329",
"sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2",
"sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40",
"sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a",
"sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4",
"sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de",
"sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9",
"sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9",
"sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb",
"sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076",
"sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de",
"sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907",
"sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d",
"sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536",
"sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d",
"sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54",
"sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc",
"sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212",
"sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9",
"sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d",
"sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b",
"sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7",
"sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81",
"sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c",
"sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895",
"sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297",
"sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb",
"sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe",
"sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242",
"sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0",
"sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2"
],
"index": "pypi",
"version": "==3.7.4"
},
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"version": "==3.0.1"
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"version": "==20.3.0"
},
"beautifulsoup4": {
"hashes": [
"sha256:5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97169",
"sha256:6135db2ba678168c07950f9a16c4031822c6f4aec75a65e0a97bc5ca09789931",
"sha256:dcdef580e18a76d54002088602eba453eec38ebbcafafeaabd8cab12b6155d57"
],
"index": "pypi",
"version": "==4.8.1"
},
"bgpranking": {
"editable": true,
"path": "."
},
"certifi": {
"hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.12.5"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"version": "==7.1.2"
},
"dominate": {
"hashes": [
"sha256:76ec2cde23700a6fc4fee098168b9dee43b99c2f1dd0ca6a711f683e8eb7e1e4",
"sha256:84b5f71ed30021193cb0faa45d7776e1083f392cfe67a49f44e98cb2ed76c036"
],
"version": "==2.6.0"
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"index": "pypi",
"version": "==1.1.1"
},
"flask-bootstrap": {
"hashes": [
"sha256:cb08ed940183f6343a64e465e83b3a3f13c53e1baabb8d72b5da4545ef123ac8"
],
"index": "pypi",
"version": "==3.3.7.1"
},
"gevent": {
"hashes": [
"sha256:16574e4aa902ebc7bad564e25aa9740a82620fdeb61e0bbf5cbc32e84c13cb6a",
"sha256:188c3c6da67e17ffa28f960fc80f8b7e4ba0f4efdc7519822c9d3a1784ca78ea",
"sha256:1e5af63e452cc1758924528a2ba6d3e472f5338e1534b7233cd01d3429fc1082",
"sha256:242e32cc011ad7127525ca9181aef3379ce4ad9c733aefe311ecf90248ad9a6f",
"sha256:2a9ae0a0fd956cbbc9c326b8f290dcad2b58acfb2e2732855fe1155fb110a04d",
"sha256:33741e3cd51b90483b14f73b6a3b32b779acf965aeb91d22770c0c8e0c937b73",
"sha256:3694f393ab08372bd337b9bc8eebef3ccab3c1623ef94536762a1eee68821449",
"sha256:464ec84001ba5108a9022aded4c5e69ea4d13ef11a2386d3ec37c1d08f3074c9",
"sha256:520cc2a029a9eef436e4e56b007af7859315cafa21937d43c1d5269f12f2c981",
"sha256:77b65a68c83e1c680f52dc39d5e5406763dd10a18ce08420665504b6f047962e",
"sha256:7bdfee07be5eee4f687bf90c54c2a65c909bcf2b6c4878faee51218ffa5d5d3e",
"sha256:969743debf89d6409423aaeae978437cc042247f91f5801e946a07a0a3b59148",
"sha256:96f704561a9dd9a817c67f2e279e23bfad6166cf95d63d35c501317e17f68bcf",
"sha256:9f99c3ec61daed54dc074fbcf1a86bcf795b9dfac2f6d4cdae6dfdb8a9125692",
"sha256:a130a1885603eabd8cea11b3e1c3c7333d4341b537eca7f0c4794cb5c7120db1",
"sha256:a54b9c7516c211045d7897a73a4ccdc116b3720c9ad3c591ef9592b735202a3b",
"sha256:ac98570649d9c276e39501a1d1cbf6c652b78f57a0eb1445c5ff25ff80336b63",
"sha256:afaeda9a7e8e93d0d86bf1d65affe912366294913fe43f0d107145dc32cd9545",
"sha256:b6ffc1131e017aafa70d7ec19cc24010b19daa2f11d5dc2dc191a79c3c9ea147",
"sha256:ba0c6ad94614e9af4240affbe1b4839c54da5a0a7e60806c6f7f69c1a7f5426e",
"sha256:bdb3677e77ab4ebf20c4752ac49f3b1e47445678dd69f82f9905362c68196456",
"sha256:c2c4326bb507754ef354635c05f560a217c171d80f26ca65bea81aa59b1ac179",
"sha256:cfb2878c2ecf27baea436bb9c4d8ab8c2fa7763c3916386d5602992b6a056ff3",
"sha256:e370e0a861db6f63c75e74b6ee56a40f5cdac90212ec404621445afa12bfc94b",
"sha256:e8a5d9fcf5d031f2e4c499f5f4b53262face416e22e8769078354f641255a663",
"sha256:ecff28416c99e0f73137f35849c3027cc3edde9dc13b7707825ebbf728623928",
"sha256:f0498df97a303da77e180a9368c9228b0fc94d10dd2ce79fc5ebb63fec0d2fc9",
"sha256:f91fd07b9cf642f24e58ed381e19ec33e28b8eee8726c19b026ea24fcc9ff897"
],
"version": "==21.1.2"
},
"greenlet": {
"hashes": [
"sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196",
"sha256:0ddd77586553e3daf439aa88b6642c5f252f7ef79a39271c25b1d4bf1b7cbb85",
"sha256:111cfd92d78f2af0bc7317452bd93a477128af6327332ebf3c2be7df99566683",
"sha256:122c63ba795fdba4fc19c744df6277d9cfd913ed53d1a286f12189a0265316dd",
"sha256:181300f826625b7fd1182205b830642926f52bd8cdb08b34574c9d5b2b1813f7",
"sha256:1a1ada42a1fd2607d232ae11a7b3195735edaa49ea787a6d9e6a53afaf6f3476",
"sha256:1bb80c71de788b36cefb0c3bb6bfab306ba75073dbde2829c858dc3ad70f867c",
"sha256:1d1d4473ecb1c1d31ce8fd8d91e4da1b1f64d425c1dc965edc4ed2a63cfa67b2",
"sha256:292e801fcb3a0b3a12d8c603c7cf340659ea27fd73c98683e75800d9fd8f704c",
"sha256:2c65320774a8cd5fdb6e117c13afa91c4707548282464a18cf80243cf976b3e6",
"sha256:4365eccd68e72564c776418c53ce3c5af402bc526fe0653722bc89efd85bf12d",
"sha256:5352c15c1d91d22902582e891f27728d8dac3bd5e0ee565b6a9f575355e6d92f",
"sha256:58ca0f078d1c135ecf1879d50711f925ee238fe773dfe44e206d7d126f5bc664",
"sha256:5d4030b04061fdf4cbc446008e238e44936d77a04b2b32f804688ad64197953c",
"sha256:5d69bbd9547d3bc49f8a545db7a0bd69f407badd2ff0f6e1a163680b5841d2b0",
"sha256:5f297cb343114b33a13755032ecf7109b07b9a0020e841d1c3cedff6602cc139",
"sha256:62afad6e5fd70f34d773ffcbb7c22657e1d46d7fd7c95a43361de979f0a45aef",
"sha256:647ba1df86d025f5a34043451d7c4a9f05f240bee06277a524daad11f997d1e7",
"sha256:719e169c79255816cdcf6dccd9ed2d089a72a9f6c42273aae12d55e8d35bdcf8",
"sha256:7cd5a237f241f2764324396e06298b5dee0df580cf06ef4ada0ff9bff851286c",
"sha256:875d4c60a6299f55df1c3bb870ebe6dcb7db28c165ab9ea6cdc5d5af36bb33ce",
"sha256:90b6a25841488cf2cb1c8623a53e6879573010a669455046df5f029d93db51b7",
"sha256:94620ed996a7632723a424bccb84b07e7b861ab7bb06a5aeb041c111dd723d36",
"sha256:b5f1b333015d53d4b381745f5de842f19fe59728b65f0fbb662dafbe2018c3a5",
"sha256:c5b22b31c947ad8b6964d4ed66776bcae986f73669ba50620162ba7c832a6b6a",
"sha256:c93d1a71c3fe222308939b2e516c07f35a849c5047f0197442a4d6fbcb4128ee",
"sha256:cdb90267650c1edb54459cdb51dab865f6c6594c3a47ebd441bc493360c7af70",
"sha256:cfd06e0f0cc8db2a854137bd79154b61ecd940dce96fad0cba23fe31de0b793c",
"sha256:d3789c1c394944084b5e57c192889985a9f23bd985f6d15728c745d380318128",
"sha256:da7d09ad0f24270b20f77d56934e196e982af0d0a2446120cb772be4e060e1a2",
"sha256:df3e83323268594fa9755480a442cabfe8d82b21aba815a71acf1bb6c1776218",
"sha256:df8053867c831b2643b2c489fe1d62049a98566b1646b194cc815f13e27b90df",
"sha256:e1128e022d8dce375362e063754e129750323b67454cac5600008aad9f54139e",
"sha256:e6e9fdaf6c90d02b95e6b0709aeb1aba5affbbb9ccaea5502f8638e4323206be",
"sha256:eac8803c9ad1817ce3d8d15d1bb82c2da3feda6bee1153eec5c58fa6e5d3f770",
"sha256:eb333b90036358a0e2c57373f72e7648d7207b76ef0bd00a4f7daad1f79f5203",
"sha256:ed1d1351f05e795a527abc04a0d82e9aecd3bdf9f46662c36ff47b0b00ecaf06",
"sha256:f3dc68272990849132d6698f7dc6df2ab62a88b0d36e54702a8fd16c0490e44f",
"sha256:f59eded163d9752fd49978e0bab7a1ff21b1b8d25c05f0995d140cc08ac83379",
"sha256:f5e2d36c86c7b03c94b8459c3bd2c9fe2c7dab4b258b8885617d44a22e453fb7",
"sha256:f6f65bf54215e4ebf6b01e4bb94c49180a589573df643735107056f7a910275b",
"sha256:f8450d5ef759dbe59f84f2c9f77491bb3d3c44bc1a573746daf086e70b14c243",
"sha256:f97d83049715fd9dec7911860ecf0e17b48d8725de01e45de07d8ac0bd5bc378"
],
"markers": "platform_python_implementation == 'CPython'",
"version": "==1.0.0"
},
"gunicorn": {
"extras": [
"gevent"
],
"hashes": [
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
],
"index": "pypi",
"version": "==19.9.0"
},
"hiredis": {
"hashes": [
"sha256:01b577f84c20ecc9c07fc4c184231b08e3c3942de096fa99978e053de231c423",
"sha256:01ff0900134166961c9e339df77c33b72f7edc5cb41739f0babcd9faa345926e",
"sha256:03ed34a13316d0c34213c4fd46e0fa3a5299073f4d4f08e93fed8c2108b399b3",
"sha256:040436e91df5143aff9e0debb49530d0b17a6bd52200ce568621c31ef581b10d",
"sha256:091eb38fbf968d1c5b703e412bbbd25f43a7967d8400842cee33a5a07b33c27b",
"sha256:102f9b9dc6ed57feb3a7c9bdf7e71cb7c278fe8df1edfcfe896bc3e0c2be9447",
"sha256:2b4b392c7e3082860c8371fab3ae762139090f9115819e12d9f56060f9ede05d",
"sha256:2c9cc0b986397b833073f466e6b9e9c70d1d4dc2c2c1b3e9cae3a23102ff296c",
"sha256:2fa65a9df683bca72073cd77709ddeb289ea2b114d3775d225fbbcc5faf808c5",
"sha256:38437a681f17c975fd22349e72c29bc643f8e7eb2d6dc5df419eac59afa4d7ce",
"sha256:3b3428fa3cf1ee178807b52c9bee8950ab94cd4eaa9bfae8c1bbae3c49501d34",
"sha256:3dd8c2fae7f5494978facb0e93297dd627b1a3f536f3b070cf0a7d9157a07dcb",
"sha256:4414a96c212e732723b5c3d7c04d386ebbb2ec359e1de646322cbc3f875cbd0d",
"sha256:48c627581ad4ef60adbac980981407939acf13a0e18f093502c7b542223c4f19",
"sha256:4a60e71625a2d78d8ab84dfb2fa2cfd9458c964b6e6c04fea76d9ade153fb371",
"sha256:585ace09f434e43d8a8dbeb366865b1a044d7c06319b3c7372a0a00e63b860f4",
"sha256:74b364b3f06c9cf0a53f7df611045bc9437ed972a283fa1f0b12537236d23ddc",
"sha256:75c65c3850e89e9daa68d1b9bedd5806f177d60aa5a7b0953b4829481cfc1f72",
"sha256:7f052de8bf744730a9120dbdc67bfeb7605a01f69fb8e7ba5c475af33c24e145",
"sha256:8113a7d5e87ecf57cd4ae263cc9e429adb9a3e59f5a7768da5d3312a8d0a051a",
"sha256:84857ce239eb8ed191ac78e77ff65d52902f00f30f4ee83bf80eb71da73b70e6",
"sha256:8644a48ddc4a40b3e3a6b9443f396c2ee353afb2d45656c4fc68d04a82e8e3f7",
"sha256:936aa565e673536e8a211e43ec43197406f24cd1f290138bd143765079c8ba00",
"sha256:9afeb88c67bbc663b9f27385c496da056d06ad87f55df6e393e1516cfecb0461",
"sha256:9d62cc7880110e4f83b0a51d218f465d3095e2751fbddd34e553dbd106a929ff",
"sha256:a1fadd062fc8d647ff39220c57ea2b48c99bb73f18223828ec97f88fc27e7898",
"sha256:a7754a783b1e5d6f627c19d099b178059c62f782ab62b4d8ba165b9fbc2ee34c",
"sha256:aa59dd63bb3f736de4fc2d080114429d5d369dfb3265f771778e8349d67a97a4",
"sha256:ae2ee0992f8de249715435942137843a93db204dd7db1e7cc9bdc5a8436443e8",
"sha256:b36842d7cf32929d568f37ec5b3173b72b2ec6572dec4d6be6ce774762215aee",
"sha256:bcbf9379c553b5facc6c04c1e5569b44b38ff16bcbf354676287698d61ee0c92",
"sha256:cbccbda6f1c62ab460449d9c85fdf24d0d32a6bf45176581151e53cc26a5d910",
"sha256:d0caf98dfb8af395d6732bd16561c0a2458851bea522e39f12f04802dbf6f502",
"sha256:d6456afeddba036def1a36d8a2758eca53202308d83db20ab5d0b66590919627",
"sha256:dbaef9a21a4f10bc281684ee4124f169e62bb533c2a92b55f8c06f64f9af7b8f",
"sha256:dce84916c09aaece006272b37234ae84a8ed13abb3a4d341a23933b8701abfb5",
"sha256:eb8c9c8b9869539d58d60ff4a28373a22514d40495911451343971cb4835b7a9",
"sha256:efc98b14ee3a8595e40b1425e8d42f5fd26f11a7b215a81ef9259068931754f4",
"sha256:fa2dc05b87d97acc1c6ae63f3e0f39eae5246565232484b08db6bf2dc1580678",
"sha256:fe7d6ce9f6a5fbe24f09d95ea93e9c7271abc4e1565da511e1449b107b4d7848"
],
"index": "pypi",
"version": "==1.0.1"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"idna-ssl": {
"hashes": [
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
],
"index": "pypi",
"markers": "python_version < '3.7'",
"version": "==1.1.0"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
],
"index": "pypi",
"version": "==2.11.3"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
],
"version": "==1.1.1"
},
"multidict": {
"hashes": [
"sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a",
"sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93",
"sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632",
"sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656",
"sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79",
"sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7",
"sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d",
"sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5",
"sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224",
"sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26",
"sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea",
"sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348",
"sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6",
"sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76",
"sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1",
"sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f",
"sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952",
"sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a",
"sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37",
"sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9",
"sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359",
"sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8",
"sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da",
"sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3",
"sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d",
"sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf",
"sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841",
"sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d",
"sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93",
"sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f",
"sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647",
"sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635",
"sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456",
"sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda",
"sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5",
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
],
"version": "==5.1.0"
},
"pid": {
"editable": true,
"git": "https://github.com/trbs/pid.git/",
"ref": "6a5d43a57149f6d70c61ad10f5622f88a3ae663c"
},
"pybgpranking": {
"editable": true,
"path": "./client"
},
"pycountry": {
"hashes": [
"sha256:3c57aa40adcf293d59bebaffbe60d8c39976fba78d846a018dc0c2ec9c6cb3cb"
],
"index": "pypi",
"version": "==19.8.18"
},
"pyipasnhistory": {
"editable": true,
"git": "https://github.com/D4-project/IPASN-History.git/",
"ref": "98ed95eeb33dff69b350bbab638da5634614d685",
"subdirectory": "client"
},
"pymispgalaxies": {
"editable": true,
"git": "https://github.com/MISP/PyMISPGalaxies.git",
"ref": "a43655ba85f00ba3a212a7f38e89380a2480adac"
},
"pytaxonomies": {
"editable": true,
"git": "https://github.com/MISP/PyTaxonomies.git",
"ref": "01d18a50fd786d359df0a448200f10d64c06d175"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"index": "pypi",
"version": "==2.8.1"
},
"redis": {
"hashes": [
"sha256:3613daad9ce5951e426f460deddd5caf469e08a3af633e9578fc77d362becf62",
"sha256:8d0fc278d3f5e1249967cba2eb4a5632d19e45ce5c09442b8422d15ee2c22cc2"
],
"index": "pypi",
"version": "==3.3.11"
},
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"index": "pypi",
"version": "==2.22.0"
},
"simplejson": {
"hashes": [
"sha256:067a7177ddfa32e1483ba5169ebea1bc2ea27f224853211ca669325648ca5642",
"sha256:2fc546e6af49fb45b93bbe878dea4c48edc34083729c0abd09981fe55bdf7f91",
"sha256:354fa32b02885e6dae925f1b5bbf842c333c1e11ea5453ddd67309dc31fdb40a",
"sha256:37e685986cf6f8144607f90340cff72d36acf654f3653a6c47b84c5c38d00df7",
"sha256:3af610ee72efbe644e19d5eaad575c73fb83026192114e5f6719f4901097fce2",
"sha256:3b919fc9cf508f13b929a9b274c40786036b31ad28657819b3b9ba44ba651f50",
"sha256:3dd289368bbd064974d9a5961101f080e939cbe051e6689a193c99fb6e9ac89b",
"sha256:6c3258ffff58712818a233b9737fe4be943d306c40cf63d14ddc82ba563f483a",
"sha256:75e3f0b12c28945c08f54350d91e624f8dd580ab74fd4f1bbea54bc6b0165610",
"sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5",
"sha256:ee9625fc8ee164902dfbb0ff932b26df112da9f871c32f0f9c1bcf20c350fe2a",
"sha256:fb2530b53c28f0d4d84990e945c2ebb470edb469d63e389bf02ff409012fe7c5"
],
"index": "pypi",
"version": "==3.16.0"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
},
"soupsieve": {
"hashes": [
"sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc",
"sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"
],
"version": "==2.2.1"
},
"typing-extensions": {
"hashes": [
"sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2",
"sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d",
"sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"
],
"index": "pypi",
"markers": "python_version < '3.7'",
"version": "==3.7.4.1"
},
"urllib3": {
"hashes": [
"sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
"sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
],
"version": "==1.25.11"
},
"visitor": {
"hashes": [
"sha256:2c737903b2b6864ebc6167eef7cf3b997126f1aa94bdf590f90f1436d23e480a"
],
"version": "==0.1.3"
},
"werkzeug": {
"hashes": [
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
],
"index": "pypi",
"version": "==0.16.0"
},
"yarl": {
"hashes": [
"sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e",
"sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434",
"sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366",
"sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3",
"sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec",
"sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959",
"sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e",
"sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c",
"sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6",
"sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a",
"sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6",
"sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424",
"sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e",
"sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f",
"sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50",
"sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2",
"sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc",
"sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4",
"sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970",
"sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10",
"sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0",
"sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406",
"sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896",
"sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643",
"sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721",
"sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478",
"sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724",
"sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e",
"sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8",
"sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96",
"sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25",
"sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76",
"sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2",
"sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2",
"sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c",
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
],
"version": "==1.6.3"
},
"zope.event": {
"hashes": [
"sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
"sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"
],
"version": "==4.5.0"
},
"zope.interface": {
"hashes": [
"sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1",
"sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d",
"sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123",
"sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232",
"sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549",
"sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102",
"sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5",
"sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45",
"sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00",
"sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc",
"sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7",
"sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104",
"sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034",
"sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3",
"sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3",
"sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4",
"sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86",
"sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96",
"sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546",
"sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb",
"sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3",
"sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b",
"sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b",
"sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec",
"sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae",
"sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e",
"sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386",
"sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2",
"sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a",
"sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d",
"sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a",
"sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24",
"sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d",
"sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b",
"sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50",
"sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523",
"sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a",
"sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095",
"sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a",
"sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520",
"sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65",
"sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11",
"sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c",
"sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7",
"sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332",
"sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e",
"sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c",
"sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7",
"sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20",
"sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc",
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
"sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
],
"version": "==5.2.0"
}
},
"develop": {}
}

View File

@ -1,33 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from abc import ABC, abstractmethod
import logging
from .libs.helpers import long_sleep, shutdown_requested
class AbstractManager(ABC):
def __init__(self, loglevel: int=logging.DEBUG):
self.loglevel = loglevel
self.logger = logging.getLogger(f'{self.__class__.__name__}')
self.logger.setLevel(loglevel)
self.logger.info(f'Initializing {self.__class__.__name__}')
@abstractmethod
def _to_run_forever(self):
pass
def run(self, sleep_in_sec: int):
self.logger.info(f'Launching {self.__class__.__name__}')
while True:
if shutdown_requested():
break
try:
self._to_run_forever()
except Exception:
self.logger.exception(f'Something went wrong in {self.__class__.__name__}.')
if not long_sleep(sleep_in_sec):
break
self.logger.info(f'Shutting down {self.__class__.__name__}')

View File

@ -1,61 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from dateutil import parser
from datetime import date
from pathlib import Path
from dateutil.relativedelta import relativedelta
from collections import defaultdict
import zipfile
import logging
try:
import simplejson as json
except ImportError:
import json
from .libs.helpers import safe_create_dir, set_running, unset_running
class DeepArchive():
def __init__(self, config_file: Path, storage_directory: Path,
loglevel: int=logging.DEBUG):
'''Archive everyfile older than 2 month.'''
with open(config_file, 'r') as f:
module_parameters = json.load(f)
self.vendor = module_parameters['vendor']
self.listname = module_parameters['name']
self.directory = storage_directory / self.vendor / self.listname / 'archive'
safe_create_dir(self.directory)
self.deep_archive = self.directory / 'deep'
safe_create_dir(self.deep_archive)
self.__init_logger(loglevel)
def __init_logger(self, loglevel):
self.logger = logging.getLogger(f'{self.__class__.__name__}-{self.vendor}-{self.listname}')
self.logger.setLevel(loglevel)
def archive(self):
set_running(self.__class__.__name__)
to_archive = defaultdict(list)
today = date.today()
last_day_to_keep = date(today.year, today.month, 1) - relativedelta(months=2)
for p in self.directory.iterdir():
if not p.is_file():
continue
filedate = parser.parse(p.name.split('.')[0]).date()
if filedate >= last_day_to_keep:
continue
to_archive['{}.zip'.format(filedate.strftime('%Y%m'))].append(p)
if to_archive:
self.logger.info('Found old files. Archiving: {}'.format(', '.join(to_archive.keys())))
else:
self.logger.debug('No old files.')
for archivename, path_list in to_archive.items():
with zipfile.ZipFile(self.deep_archive / archivename, 'x', zipfile.ZIP_DEFLATED) as z:
for f in path_list:
z.write(f, f.name)
# Delete all the files if the archiving worked out properly
[f.unlink() for f in path_list]
unset_running(self.__class__.__name__)

View File

@ -1,69 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from pathlib import Path
import requests
from redis import StrictRedis
from dateutil.parser import parse
import re
from .libs.helpers import set_running, unset_running, get_socket_path, safe_create_dir
class ASNDescriptions():
def __init__(self, storage_directory: Path, loglevel: int=logging.DEBUG):
self.__init_logger(loglevel)
self.asn_meta = StrictRedis(unix_socket_path=get_socket_path('storage'), db=2, decode_responses=True)
self.logger.debug('Starting ASN History')
self.directory = storage_directory / 'asn_descriptions'
safe_create_dir(self.directory)
self.archives = self.directory / 'archive'
safe_create_dir(self.archives)
self.url = 'https://www.cidr-report.org/as2.0/autnums.html'
def __init_logger(self, loglevel):
self.logger = logging.getLogger(f'{self.__class__.__name__}')
self.logger.setLevel(loglevel)
def __update_available(self):
r = requests.head(self.url)
current_last_modified = parse(r.headers['Last-Modified'])
if not self.asn_meta.exists('ans_description_last_update'):
return True
last_update = parse(self.asn_meta.get('ans_description_last_update'))
if last_update < current_last_modified:
return True
return False
def load_descriptions(self):
if not self.__update_available():
self.logger.debug('No new file to import.')
return
set_running(self.__class__.__name__)
self.logger.info('Importing new ASN descriptions.')
r = requests.get(self.url)
last_modified = parse(r.headers['Last-Modified']).isoformat()
p = self.asn_meta.pipeline()
new_asn = 0
new_description = 0
for asn, descr in re.findall('as=AS(.*)&.*</a> (.*)\n', r.text):
existing_descriptions = self.asn_meta.hgetall(f'{asn}|descriptions')
if not existing_descriptions:
self.logger.debug(f'New ASN: {asn} - {descr}')
p.hset(f'{asn}|descriptions', last_modified, descr)
new_asn += 1
else:
last_descr = sorted(existing_descriptions.keys(), reverse=True)[0]
if descr != existing_descriptions[last_descr]:
self.logger.debug(f'New description for {asn}: {existing_descriptions[last_descr]} -> {descr}')
p.hset(f'{asn}|descriptions', last_modified, descr)
new_description += 1
p.set('ans_description_last_update', last_modified)
p.execute()
self.logger.info(f'Done with import. New ASNs: {new_asn}, new descriptions: {new_description}')
if new_asn or new_description:
with open(self.archives / f'{last_modified}.html', 'w') as f:
f.write(r.text)
unset_running(self.__class__.__name__)

View File

@ -1,37 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import TypeVar, Union
import logging
import re
from redis import ConnectionPool, Redis
from redis.connection import UnixDomainSocketConnection
from .default import get_config, get_socket_path
from typing import TypeVar, Union, Optional, Dict, Any, List, Tuple
import datetime
from datetime import timedelta
from dateutil.parser import parse
from collections import defaultdict
import logging
import json
from redis import StrictRedis
from .libs.helpers import get_socket_path, get_config_path
from .libs.exceptions import InvalidDateFormat
from .libs.statsripe import StatsRIPE
from .default import InvalidDateFormat
from .helpers import get_modules
from .statsripe import StatsRIPE
Dates = TypeVar('Dates', datetime.datetime, datetime.date, str)
class Querying():
class BGPRanking():
def __init__(self, loglevel: int=logging.DEBUG):
self.__init_logger(loglevel)
self.storage = StrictRedis(unix_socket_path=get_socket_path('storage'), decode_responses=True)
self.ranking = StrictRedis(unix_socket_path=get_socket_path('storage'), db=1)
self.asn_meta = StrictRedis(unix_socket_path=get_socket_path('storage'), db=2, decode_responses=True)
self.cache = StrictRedis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
def __init_logger(self, loglevel: int):
def __init__(self) -> None:
self.logger = logging.getLogger(f'{self.__class__.__name__}')
self.logger.setLevel(loglevel)
self.logger.setLevel(get_config('generic', 'loglevel'))
def __normalize_date(self, date: Dates):
self.cache_pool: ConnectionPool = ConnectionPool(connection_class=UnixDomainSocketConnection,
path=get_socket_path('cache'), decode_responses=True)
self.storage = Redis(get_config('generic', 'storage_db_hostname'), get_config('generic', 'storage_db_port'), decode_responses=True)
self.asn_meta = Redis(get_config('generic', 'storage_db_hostname'), get_config('generic', 'storage_db_port'), decode_responses=True)
self.ranking = Redis(get_config('generic', 'ranking_db_hostname'), get_config('generic', 'ranking_db_port'))
@property
def cache(self):
return Redis(connection_pool=self.cache_pool, db=1)
def check_redis_up(self) -> bool:
return self.cache.ping()
def __normalize_date(self, date: Optional[Dates]) -> str:
if not date:
return datetime.date.today().isoformat()
if isinstance(date, datetime.datetime):
return date.date().isoformat()
elif isinstance(date, datetime.date):
@ -45,16 +60,19 @@ class Querying():
def _ranking_cache_wrapper(self, key):
if not self.cache.exists(key):
if self.ranking.exists(key):
key_dump = self.ranking.dump(key)
content: List[Tuple[bytes, float]] = self.ranking.zrangebyscore(key, '-Inf', '+Inf', withscores=True)
# Cache for 10 hours
self.cache.restore(key, 36000, key_dump, True)
self.cache.zadd(key, {value: rank for value, rank in content})
self.cache.expire(key, 36000)
def asns_global_ranking(self, date: Dates=datetime.date.today(), source: Union[list, str]='',
def asns_global_ranking(self, date: Optional[Dates]=None, source: Union[list, str]='',
ipversion: str='v4', limit: int=100):
'''Aggregated ranking of all the ASNs known in the system, weighted by source.'''
to_return = {'meta': {'ipversion': ipversion, 'limit': limit}, 'source': source,
'response': set()}
to_return: Dict[str, Any] = {
'meta': {'ipversion': ipversion, 'limit': limit},
'source': source,
'response': set()
}
d = self.__normalize_date(date)
to_return['meta']['date'] = d
if source:
@ -76,11 +94,13 @@ class Querying():
to_return['response'] = self.cache.zrevrange(key, start=0, end=limit, withscores=True)
return to_return
def asn_details(self, asn: int, date: Dates= datetime.date.today(), source: Union[list, str]='',
def asn_details(self, asn: int, date: Optional[Dates]=None, source: Union[list, str]='',
ipversion: str='v4'):
'''Aggregated ranking of all the prefixes anounced by the given ASN, weighted by source.'''
to_return = {'meta': {'asn': asn, 'ipversion': ipversion, 'source': source},
'response': set()}
to_return: Dict[str, Any] = {
'meta': {'asn': asn, 'ipversion': ipversion, 'source': source},
'response': set()
}
d = self.__normalize_date(date)
to_return['meta']['date'] = d
@ -103,12 +123,14 @@ class Querying():
to_return['response'] = self.cache.zrevrange(key, start=0, end=-1, withscores=True)
return to_return
def asn_rank(self, asn: int, date: Dates=datetime.date.today(), source: Union[list, str]='',
def asn_rank(self, asn: int, date: Optional[Dates]=None, source: Union[list, str]='',
ipversion: str='v4', with_position: bool=False):
'''Get the rank of a single ASN, weighted by source.'''
to_return = {'meta': {'asn': asn, 'ipversion': ipversion,
'source': source, 'with_position': with_position},
'response': 0.0}
to_return: Dict[str, Any] = {
'meta': {'asn': asn, 'ipversion': ipversion,
'source': source, 'with_position': with_position},
'response': 0.0
}
d = self.__normalize_date(date)
to_return['meta']['date'] = d
@ -141,9 +163,9 @@ class Querying():
to_return['response'] = float(r)
return to_return
def get_sources(self, date: Dates=datetime.date.today()):
def get_sources(self, date: Optional[Dates]=None):
'''Get the sources availables for a specific day (default: today).'''
to_return = {'meta': {}, 'response': set()}
to_return: Dict[str, Any] = {'meta': {}, 'response': set()}
d = self.__normalize_date(date)
to_return['meta']['date'] = d
@ -151,9 +173,11 @@ class Querying():
to_return['response'] = self.storage.smembers(key)
return to_return
def get_asn_descriptions(self, asn: int, all_descriptions=False):
to_return = {'meta': {'asn': asn, 'all_descriptions': all_descriptions},
'response': []}
def get_asn_descriptions(self, asn: int, all_descriptions=False) -> Dict[str, Any]:
to_return: Dict[str, Union[Dict, List, str]] = {
'meta': {'asn': asn, 'all_descriptions': all_descriptions},
'response': []
}
descriptions = self.asn_meta.hgetall(f'{asn}|descriptions')
if all_descriptions or not descriptions:
to_return['response'] = descriptions
@ -161,11 +185,13 @@ class Querying():
to_return['response'] = descriptions[sorted(descriptions.keys(), reverse=True)[0]]
return to_return
def get_prefix_ips(self, asn: int, prefix: str, date: Dates=datetime.date.today(),
def get_prefix_ips(self, asn: int, prefix: str, date: Optional[Dates]=None,
source: Union[list, str]='', ipversion: str='v4'):
to_return = {'meta': {'asn': asn, 'prefix': prefix, 'ipversion': ipversion,
'source': source},
'response': defaultdict(list)}
to_return: Dict[str, Any] = {
'meta': {'asn': asn, 'prefix': prefix, 'ipversion': ipversion,
'source': source},
'response': defaultdict(list)
}
d = self.__normalize_date(date)
to_return['meta']['date'] = d
@ -186,33 +212,37 @@ class Querying():
return to_return
def get_asn_history(self, asn: int, period: int=100, source: Union[list, str]='',
ipversion: str='v4', date: Dates=datetime.date.today()):
to_return = {'meta': {'asn': asn, 'period': period, 'ipversion': ipversion,
'source': source},
'response': []}
ipversion: str='v4', date: Optional[Dates]=None):
to_return: Dict[str, Any] = {
'meta': {'asn': asn, 'period': period, 'ipversion': ipversion, 'source': source},
'response': []
}
if isinstance(date, str):
date = parse(date).date()
if date + timedelta(days=period / 3) > datetime.date.today():
# the period to display will be around the date passed at least 2/3 before the date, at most 1/3 after
# FIXME: That is not doing what it is supposed to...
date = datetime.date.today()
if date is None:
python_date: datetime.date = datetime.date.today()
elif isinstance(date, str):
python_date = parse(date).date()
elif isinstance(date, datetime.datetime):
python_date = date.date()
else:
python_date = date
to_return['meta']['date'] = date.isoformat()
to_return['meta']['date'] = python_date.isoformat()
for i in range(period):
d = date - timedelta(days=i)
d = python_date - timedelta(days=i)
rank = self.asn_rank(asn, d, source, ipversion)
if 'response' not in rank:
rank = 0
to_return['response'].insert(0, (d.isoformat(), rank['response']))
return to_return
def country_rank(self, country: str, date: Dates=datetime.date.today(), source: Union[list, str]='',
def country_rank(self, country: str, date: Optional[Dates]=None, source: Union[list, str]='',
ipversion: str='v4'):
to_return = {'meta': {'country': country, 'ipversion': ipversion,
'source': source},
'response': []}
to_return: Dict[str, Any] = {
'meta': {'country': country, 'ipversion': ipversion, 'source': source},
'response': []
}
d = self.__normalize_date(date)
to_return['meta']['date'] = d
@ -224,29 +254,31 @@ class Querying():
logging.warning(f'Invalid response: {response}')
# FIXME: return something
return 0, [(0, 0)]
routed_asns = response['data']['countries'][0]['routed']
routed_asns = re.findall(r"AsnSingle\(([\d]*)\)", response['data']['countries'][0]['routed'])
ranks = [self.asn_rank(asn, d, source, ipversion)['response'] for asn in routed_asns]
to_return['response'] = [sum(ranks), zip(routed_asns, ranks)]
return to_return
def country_history(self, country: Union[list, str], period: int=30, source: Union[list, str]='',
ipversion: str='v4', date: Dates=datetime.date.today()):
to_return = {}
to_return = {'meta': {'country': country, 'ipversion': ipversion,
'source': source},
'response': defaultdict(list)}
if isinstance(date, str):
date = parse(date).date()
if date + timedelta(days=period / 3) > datetime.date.today():
# the period to display will be around the date passed at least 2/3 before the date, at most 1/3 after
date = datetime.date.today()
ipversion: str='v4', date: Optional[Dates]=None):
to_return: Dict[str, Any] = {
'meta': {'country': country, 'ipversion': ipversion, 'source': source},
'response': defaultdict(list)
}
if date is None:
python_date: datetime.date = datetime.date.today()
elif isinstance(date, str):
python_date = parse(date).date()
elif isinstance(date, datetime.datetime):
python_date = date.date()
else:
python_date = date
if isinstance(country, str):
country = [country]
for c in country:
for i in range(period):
d = date - timedelta(days=i)
d = python_date - timedelta(days=i)
rank, details = self.country_rank(c, d, source, ipversion)['response']
if rank is None:
rank = 0
@ -257,9 +289,8 @@ class Querying():
pass
def get_sources_configs(self):
config_dir = get_config_path() / 'modules'
loaded = []
for modulepath in config_dir.glob('*.json'):
for modulepath in get_modules():
with open(modulepath) as f:
loaded.append(json.load(f))
return {'{}-{}'.format(config['vendor'], config['name']): config for config in loaded}

View File

@ -1,3 +0,0 @@
{
"ipasnhistory_url": "https://ipasnhistory.circl.lu/"
}

View File

@ -1,7 +0,0 @@
{
"url": "http://www.nothink.org/blacklist/blacklist_snmp_day.txt",
"vendor": "nothink",
"name": "snmp",
"impact": 5,
"parser": ".parsers.nothink"
}

View File

@ -1,7 +0,0 @@
{
"url": "http://www.nothink.org/blacklist/blacklist_ssh_day.txt",
"vendor": "nothink",
"name": "ssh",
"impact": 5,
"parser": ".parsers.nothink"
}

View File

@ -1,7 +0,0 @@
{
"url": "http://www.nothink.org/blacklist/blacklist_telnet_day.txt",
"vendor": "nothink",
"name": "telnet",
"impact": 5,
"parser": ".parsers.nothink"
}

View File

@ -1,6 +0,0 @@
{
"url": "https://palevotracker.abuse.ch/blocklists.php?download=ipblocklist",
"vendor": "palevotracker",
"name": "ipblocklist",
"impact": 5
}

View File

@ -1,6 +0,0 @@
{
"url": "https://www.openbl.org/lists/base.txt",
"vendor": "sshbl",
"name": "base",
"impact": 5
}

View File

@ -1,6 +0,0 @@
{
"url": "https://zeustracker.abuse.ch/blocklist.php?download=ipblocklist",
"vendor": "zeustracker",
"name": "ipblocklist",
"impact": 5
}

View File

@ -1,113 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import time
from redis import StrictRedis
from .libs.helpers import shutdown_requested, set_running, unset_running, get_socket_path, get_ipasn, sanity_check_ipasn
class DatabaseInsert():
def __init__(self, loglevel: int=logging.DEBUG):
self.__init_logger(loglevel)
self.ardb_storage = StrictRedis(unix_socket_path=get_socket_path('storage'), decode_responses=True)
self.redis_sanitized = StrictRedis(unix_socket_path=get_socket_path('prepare'), db=0, decode_responses=True)
self.ipasn = get_ipasn()
self.logger.debug('Starting import')
def __init_logger(self, loglevel):
self.logger = logging.getLogger(f'{self.__class__.__name__}')
self.logger.setLevel(loglevel)
def insert(self):
ready, message = sanity_check_ipasn(self.ipasn)
if not ready:
# Try again later.
self.logger.warning(message)
return
self.logger.debug(message)
set_running(self.__class__.__name__)
while True:
if shutdown_requested():
break
try:
if not self.ipasn.is_up:
break
except Exception:
self.logger.warning('Unable to query ipasnhistory')
time.sleep(10)
continue
uuids = self.redis_sanitized.spop('to_insert', 100)
if not uuids:
break
p = self.redis_sanitized.pipeline(transaction=False)
[p.hgetall(uuid) for uuid in uuids]
sanitized_data = p.execute()
for_query = []
for i, uuid in enumerate(uuids):
data = sanitized_data[i]
if not data:
self.logger.warning(f'No data for UUID {uuid}. This should not happen, but lets move on.')
continue
for_query.append({'ip': data['ip'], 'address_family': data['address_family'], 'source': 'caida',
'date': data['datetime'], 'precision_delta': {'days': 3}})
try:
responses = self.ipasn.mass_query(for_query)
except Exception:
self.logger.exception('Mass query in IPASN History failed, trying again later.')
# Rollback the spop
self.redis_sanitized.sadd('to_insert', *uuids)
time.sleep(10)
continue
retry = []
done = []
ardb_pipeline = self.ardb_storage.pipeline(transaction=False)
for i, uuid in enumerate(uuids):
data = sanitized_data[i]
if not data:
self.logger.warning(f'No data for UUID {uuid}. This should not happen, but lets move on.')
continue
routing_info = responses['responses'][i]['response'] # our queries are on one single date, not a range
# Data gathered from IPASN History:
# * IP Block of the IP
# * AS number
if 'error' in routing_info:
self.logger.warning(f"Unable to find routing information for {data['ip']} - {data['datetime']}: {routing_info['error']}")
continue
# Single date query, getting from the object
datetime_routing = list(routing_info.keys())[0]
entry = routing_info[datetime_routing]
if not entry:
# routing info is missing, need to try again later.
retry.append(uuid)
continue
if 'asn' in entry and entry['asn'] is None:
self.logger.warning(f"Unable to find the AS number associated to {data['ip']} - {data['datetime']} (got None). This should not happen...")
continue
if 'prefix' in entry and entry['prefix'] is None:
self.logger.warning(f"Unable to find the prefix associated to {data['ip']} - {data['datetime']} (got None). This should not happen...")
continue
# Format: <YYYY-MM-DD>|sources -> set([<source>, ...])
ardb_pipeline.sadd(f"{data['date']}|sources", data['source'])
# Format: <YYYY-MM-DD>|<source> -> set([<asn>, ...])
ardb_pipeline.sadd(f"{data['date']}|{data['source']}", entry['asn'])
# Format: <YYYY-MM-DD>|<source>|<asn> -> set([<prefix>, ...])
ardb_pipeline.sadd(f"{data['date']}|{data['source']}|{entry['asn']}", entry['prefix'])
# Format: <YYYY-MM-DD>|<source>|<asn>|<prefix> -> set([<ip>|<datetime>, ...])
ardb_pipeline.sadd(f"{data['date']}|{data['source']}|{entry['asn']}|{entry['prefix']}",
f"{data['ip']}|{data['datetime']}")
done.append(uuid)
ardb_pipeline.execute()
p = self.redis_sanitized.pipeline(transaction=False)
if done:
p.delete(*done)
if retry:
p.sadd('to_insert', *retry)
p.execute()
unset_running(self.__class__.__name__)

View File

@ -0,0 +1,16 @@
env_global_name: str = 'BGPRANKING_HOME'
from .exceptions import (BGPRankingException, FetcherException, ArchiveException, # noqa
CreateDirectoryException, MissingEnv, InvalidDateFormat, # noqa
MissingConfigFile, MissingConfigEntry, ThirdPartyUnreachable) # noqa
# NOTE: the imports below are there to avoid too long paths when importing the
# classes/methods in the rest of the project while keeping all that in a subdirectory
# and allow to update them easily.
# You should not have to change anything in this file below this line.
from .abstractmanager import AbstractManager # noqa
from .exceptions import MissingEnv, CreateDirectoryException, ConfigError # noqa
from .helpers import get_homedir, load_configs, get_config, safe_create_dir, get_socket_path, try_make_file # noqa

View File

@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import asyncio
import logging
import signal
import time
from abc import ABC
from datetime import datetime, timedelta
from subprocess import Popen
from typing import List, Optional, Tuple
from redis import Redis
from redis.exceptions import ConnectionError
from .helpers import get_socket_path
class AbstractManager(ABC):
script_name: str
def __init__(self, loglevel: int=logging.DEBUG):
self.loglevel = loglevel
self.logger = logging.getLogger(f'{self.__class__.__name__}')
self.logger.setLevel(loglevel)
self.logger.info(f'Initializing {self.__class__.__name__}')
self.process: Optional[Popen] = None
self.__redis = Redis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
@staticmethod
def is_running() -> List[Tuple[str, float]]:
try:
r = Redis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
return r.zrangebyscore('running', '-inf', '+inf', withscores=True)
except ConnectionError:
print('Unable to connect to redis, the system is down.')
return []
@staticmethod
def force_shutdown():
try:
r = Redis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
r.set('shutdown', 1)
except ConnectionError:
print('Unable to connect to redis, the system is down.')
def set_running(self) -> None:
self.__redis.zincrby('running', 1, self.script_name)
def unset_running(self) -> None:
current_running = self.__redis.zincrby('running', -1, self.script_name)
if int(current_running) <= 0:
self.__redis.zrem('running', self.script_name)
def long_sleep(self, sleep_in_sec: int, shutdown_check: int=10) -> bool:
if shutdown_check > sleep_in_sec:
shutdown_check = sleep_in_sec
sleep_until = datetime.now() + timedelta(seconds=sleep_in_sec)
while sleep_until > datetime.now():
time.sleep(shutdown_check)
if self.shutdown_requested():
return False
return True
async def long_sleep_async(self, sleep_in_sec: int, shutdown_check: int=10) -> bool:
if shutdown_check > sleep_in_sec:
shutdown_check = sleep_in_sec
sleep_until = datetime.now() + timedelta(seconds=sleep_in_sec)
while sleep_until > datetime.now():
await asyncio.sleep(shutdown_check)
if self.shutdown_requested():
return False
return True
def shutdown_requested(self) -> bool:
try:
return True if self.__redis.exists('shutdown') else False
except ConnectionRefusedError:
return True
except ConnectionError:
return True
def _to_run_forever(self) -> None:
pass
def run(self, sleep_in_sec: int) -> None:
self.logger.info(f'Launching {self.__class__.__name__}')
try:
while True:
if self.shutdown_requested():
break
try:
if self.process:
if self.process.poll() is not None:
self.logger.critical(f'Unable to start {self.script_name}.')
break
else:
self.set_running()
self._to_run_forever()
except Exception:
self.logger.exception(f'Something went terribly wrong in {self.__class__.__name__}.')
finally:
if not self.process:
# self.process means we run an external script, all the time,
# do not unset between sleep.
self.unset_running()
if not self.long_sleep(sleep_in_sec):
break
except KeyboardInterrupt:
self.logger.warning(f'{self.script_name} killed by user.')
finally:
if self.process:
try:
# Killing everything if possible.
self.process.send_signal(signal.SIGWINCH)
self.process.send_signal(signal.SIGTERM)
except Exception:
pass
try:
self.unset_running()
except Exception:
# the services can already be down at that point.
pass
self.logger.info(f'Shutting down {self.__class__.__name__}')
async def _to_run_forever_async(self) -> None:
pass
async def run_async(self, sleep_in_sec: int) -> None:
self.logger.info(f'Launching {self.__class__.__name__}')
try:
while True:
if self.shutdown_requested():
break
try:
if self.process:
if self.process.poll() is not None:
self.logger.critical(f'Unable to start {self.script_name}.')
break
else:
self.set_running()
await self._to_run_forever_async()
except Exception:
self.logger.exception(f'Something went terribly wrong in {self.__class__.__name__}.')
finally:
if not self.process:
# self.process means we run an external script, all the time,
# do not unset between sleep.
self.unset_running()
if not await self.long_sleep_async(sleep_in_sec):
break
except KeyboardInterrupt:
self.logger.warning(f'{self.script_name} killed by user.')
finally:
if self.process:
try:
# Killing everything if possible.
self.process.send_signal(signal.SIGWINCH)
self.process.send_signal(signal.SIGTERM)
except Exception:
pass
try:
self.unset_running()
except Exception:
# the services can already be down at that point.
pass
self.logger.info(f'Shutting down {self.__class__.__name__}')

View File

@ -36,3 +36,7 @@ class MissingConfigEntry(BGPRankingException):
class ThirdPartyUnreachable(BGPRankingException):
pass
class ConfigError(BGPRankingException):
pass

View File

@ -0,0 +1,102 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import logging
import os
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, Optional, Union
from . import env_global_name
from .exceptions import ConfigError, CreateDirectoryException, MissingEnv
configs: Dict[str, Dict[str, Any]] = {}
logger = logging.getLogger('Helpers')
@lru_cache(64)
def get_homedir() -> Path:
if not os.environ.get(env_global_name):
# Try to open a .env file in the home directory if it exists.
if (Path(__file__).resolve().parent.parent.parent / '.env').exists():
with (Path(__file__).resolve().parent.parent.parent / '.env').open() as f:
for line in f:
key, value = line.strip().split('=', 1)
if value[0] in ['"', "'"]:
value = value[1:-1]
os.environ[key] = value
if not os.environ.get(env_global_name):
guessed_home = Path(__file__).resolve().parent.parent.parent
raise MissingEnv(f"{env_global_name} is missing. \
Run the following command (assuming you run the code from the clonned repository):\
export {env_global_name}='{guessed_home}'")
return Path(os.environ[env_global_name])
@lru_cache(64)
def load_configs(path_to_config_files: Optional[Union[str, Path]]=None):
global configs
if configs:
return
if path_to_config_files:
if isinstance(path_to_config_files, str):
config_path = Path(path_to_config_files)
else:
config_path = path_to_config_files
else:
config_path = get_homedir() / 'config'
if not config_path.exists():
raise ConfigError(f'Configuration directory {config_path} does not exists.')
elif not config_path.is_dir():
raise ConfigError(f'Configuration directory {config_path} is not a directory.')
configs = {}
for path in config_path.glob('*.json'):
with path.open() as _c:
configs[path.stem] = json.load(_c)
@lru_cache(64)
def get_config(config_type: str, entry: str, quiet: bool=False) -> Any:
"""Get an entry from the given config_type file. Automatic fallback to the sample file"""
global configs
if not configs:
load_configs()
if config_type in configs:
if entry in configs[config_type]:
return configs[config_type][entry]
else:
if not quiet:
logger.warning(f'Unable to find {entry} in config file.')
else:
if not quiet:
logger.warning(f'No {config_type} config file available.')
if not quiet:
logger.warning(f'Falling back on sample config, please initialize the {config_type} config file.')
with (get_homedir() / 'config' / f'{config_type}.json.sample').open() as _c:
sample_config = json.load(_c)
return sample_config[entry]
def safe_create_dir(to_create: Path) -> None:
if to_create.exists() and not to_create.is_dir():
raise CreateDirectoryException(f'The path {to_create} already exists and is not a directory')
to_create.mkdir(parents=True, exist_ok=True)
def get_socket_path(name: str) -> str:
mapping = {
'cache': Path('cache', 'cache.sock'),
'intake': Path('temp', 'intake.sock'),
'prepare': Path('temp', 'prepare.sock')
}
return str(get_homedir() / mapping[name])
def try_make_file(filename: Path):
try:
filename.touch(exist_ok=False)
return True
except FileExistsError:
return False

67
bgpranking/helpers.py Normal file
View File

@ -0,0 +1,67 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
from functools import lru_cache
from pathlib import Path
from typing import Dict, List
import requests
from pyipasnhistory import IPASNHistory
from .default import get_homedir, get_config, ThirdPartyUnreachable, safe_create_dir
@lru_cache(64)
def get_data_dir() -> Path:
capture_dir = get_homedir() / 'rawdata'
safe_create_dir(capture_dir)
return capture_dir
@lru_cache(64)
def get_modules_dir() -> Path:
modules_dir = get_homedir() / 'config' / 'modules'
safe_create_dir(modules_dir)
return modules_dir
@lru_cache(64)
def get_modules() -> List[Path]:
return [modulepath for modulepath in get_modules_dir().glob('*.json')]
@lru_cache(64)
def load_all_modules_configs() -> Dict[str, Dict]:
configs = {}
for p in get_modules():
with p.open() as f:
j = json.load(f)
configs[f"{j['vendor']}-{j['name']}"] = j
return configs
def get_ipasn():
ipasnhistory_url = get_config('generic', 'ipasnhistory_url')
ipasn = IPASNHistory(ipasnhistory_url)
if not ipasn.is_up:
raise ThirdPartyUnreachable(f"Unable to reach IPASNHistory on {ipasnhistory_url}")
return ipasn
def sanity_check_ipasn(ipasn):
try:
meta = ipasn.meta()
except requests.exceptions.ConnectionError:
return False, "IP ASN History is not reachable, try again later."
if 'error' in meta:
raise ThirdPartyUnreachable(f'IP ASN History has a problem: {meta["error"]}')
v4_percent = meta['cached_dates']['caida']['v4']['percent']
v6_percent = meta['cached_dates']['caida']['v6']['percent']
if v4_percent < 90 or v6_percent < 90: # (this way it works if we only load 10 days)
# Try again later.
return False, f"IP ASN History is not ready: v4 {v4_percent}% / v6 {v6_percent}% loaded"
return True, f"IP ASN History is ready: v4 {v4_percent}% / v6 {v6_percent}% loaded"

View File

@ -1,142 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
from pathlib import Path
from .exceptions import CreateDirectoryException, MissingEnv, MissingConfigFile, MissingConfigEntry, ThirdPartyUnreachable
from redis import StrictRedis
from redis.exceptions import ConnectionError
from datetime import datetime, timedelta
import time
try:
import simplejson as json
except ImportError:
import json
from pyipasnhistory import IPASNHistory
def load_config_files(config_dir: Path=None) -> dict:
if not config_dir:
config_dir = get_config_path()
modules_config = config_dir / 'modules'
modules_paths = [modulepath for modulepath in modules_config.glob('*.json')]
configs = {}
for p in modules_paths:
with open(p, 'r') as f:
j = json.load(f)
configs[f"{j['vendor']}-{j['name']}"] = j
return configs
def get_config_path():
if Path('bgpranking', 'config').exists():
# Running from the repository
return Path('bgpranking', 'config')
return Path(sys.modules['bgpranking'].__file__).parent / 'config'
def get_list_storage_path():
if not os.environ.get('VIRTUAL_ENV'):
raise MissingEnv("VIRTUAL_ENV is missing. This project really wants to run from a virtual envoronment.")
return Path(os.environ['VIRTUAL_ENV'])
def get_homedir():
if not os.environ.get('BGPRANKING_HOME'):
guessed_home = Path(__file__).resolve().parent.parent.parent
raise MissingEnv(f"BGPRANKING_HOME is missing. \
Run the following command (assuming you run the code from the clonned repository):\
export BGPRANKING_HOME='{guessed_home}'")
return Path(os.environ['BGPRANKING_HOME'])
def safe_create_dir(to_create: Path):
if to_create.exists() and not to_create.is_dir():
raise CreateDirectoryException(f'The path {to_create} already exists and is not a directory')
os.makedirs(to_create, exist_ok=True)
def set_running(name: str):
r = StrictRedis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
r.hset('running', name, 1)
def unset_running(name: str):
r = StrictRedis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
r.hdel('running', name)
def is_running():
r = StrictRedis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
return r.hgetall('running')
def get_socket_path(name: str):
mapping = {
'cache': Path('cache', 'cache.sock'),
'storage': Path('storage', 'storage.sock'),
'intake': Path('temp', 'intake.sock'),
'prepare': Path('temp', 'prepare.sock'),
}
return str(get_homedir() / mapping[name])
def load_general_config():
general_config_file = get_config_path() / 'bgpranking.json'
if not general_config_file.exists():
raise MissingConfigFile(f'The general configuration file ({general_config_file}) does not exists.')
with open(general_config_file) as f:
config = json.load(f)
return config, general_config_file
def get_ipasn():
config, general_config_file = load_general_config()
if 'ipasnhistory_url' not in config:
raise MissingConfigEntry(f'"ipasnhistory_url" is missing in {general_config_file}.')
ipasn = IPASNHistory(config['ipasnhistory_url'])
if not ipasn.is_up:
raise ThirdPartyUnreachable(f"Unable to reach IPASNHistory on {config['ipasnhistory_url']}")
return ipasn
def sanity_check_ipasn(ipasn):
meta = ipasn.meta()
if 'error' in meta:
raise ThirdPartyUnreachable(f'IP ASN History has a problem: meta["error"]')
v4_percent = meta['cached_dates']['caida']['v4']['percent']
v6_percent = meta['cached_dates']['caida']['v6']['percent']
if v4_percent < 90 or v6_percent < 90: # (this way it works if we only load 10 days)
# Try again later.
return False, f"IP ASN History is not ready: v4 {v4_percent}% / v6 {v6_percent}% loaded"
return True, f"IP ASN History is ready: v4 {v4_percent}% / v6 {v6_percent}% loaded"
def check_running(name: str):
socket_path = get_socket_path(name)
try:
r = StrictRedis(unix_socket_path=socket_path)
return r.ping()
except ConnectionError:
return False
def shutdown_requested():
try:
r = StrictRedis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
return r.exists('shutdown')
except ConnectionRefusedError:
return True
except ConnectionError:
return True
def long_sleep(sleep_in_sec: int, shutdown_check: int=10):
sleep_until = datetime.now() + timedelta(seconds=sleep_in_sec)
while sleep_until > datetime.now():
time.sleep(shutdown_check)
if shutdown_requested():
return False
return True

View File

@ -1,150 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import aiohttp
from dateutil import parser
from datetime import datetime, date
from hashlib import sha512 # Faster than sha256 on 64b machines.
from pathlib import Path
import logging
from pid import PidFile, PidFileError
try:
import simplejson as json
except ImportError:
import json
from .libs.helpers import safe_create_dir, set_running, unset_running
class Fetcher():
def __init__(self, config_file: Path, storage_directory: Path,
loglevel: int=logging.DEBUG):
'''Load `config_file`, and store the fetched data into `storage_directory`
Note: if the `config_file` does not provide a URL (the file is
gathered by some oter mean), the fetcher is automatically stoped.'''
with open(config_file, 'r') as f:
module_parameters = json.load(f)
self.vendor = module_parameters['vendor']
self.listname = module_parameters['name']
self.__init_logger(loglevel)
self.fetcher = True
if 'url' not in module_parameters:
self.logger.info('No URL to fetch, breaking.')
self.fetcher = False
return
self.url = module_parameters['url']
self.logger.debug(f'Starting fetcher on {self.url}')
self.directory = storage_directory / self.vendor / self.listname
safe_create_dir(self.directory)
self.meta = self.directory / 'meta'
safe_create_dir(self.meta)
self.archive_dir = self.directory / 'archive'
safe_create_dir(self.archive_dir)
self.first_fetch = True
def __init_logger(self, loglevel):
self.logger = logging.getLogger(f'{self.__class__.__name__}-{self.vendor}-{self.listname}')
self.logger.setLevel(loglevel)
async def __get_last_modified(self):
async with aiohttp.ClientSession() as session:
async with session.head(self.url) as r:
headers = r.headers
if 'Last-Modified' in headers:
return parser.parse(headers['Last-Modified'])
return None
async def __newer(self):
'''Check if the file available for download is newed than the one
already downloaded by checking the `Last-Modified` header.
Note: return False if the file containing the last header content
is not existing, or the header doesn't have this key.
'''
last_modified_path = self.meta / 'lastmodified'
if not last_modified_path.exists():
# The file doesn't exists
if not self.first_fetch:
# The URL has no Last-Modified header, we cannot use it.
self.logger.debug('No Last-Modified header available')
return True
self.first_fetch = False
last_modified = await self.__get_last_modified()
if last_modified:
self.logger.debug('Last-Modified header available')
with last_modified_path.open('w') as f:
f.write(last_modified.isoformat())
else:
self.logger.debug('No Last-Modified header available')
return True
with last_modified_path.open() as f:
file_content = f.read()
last_modified_file = parser.parse(file_content)
last_modified = await self.__get_last_modified()
if not last_modified:
# No more Last-Modified header Oo
self.logger.warning(f'{self.listname}: Last-Modified header was present, isn\'t anymore!')
last_modified_path.unlink()
return True
if last_modified > last_modified_file:
self.logger.info('Got a new file.')
with last_modified_path.open('w') as f:
f.write(last_modified.isoformat())
return True
return False
def __same_as_last(self, downloaded):
'''Figure out the last downloaded file, check if it is the same as the
newly downloaded one. Returns true if both files have been downloaded the
same day.
Note: we check the new and the archive directory because we may have backlog
and the newest file is always the first one we process
'''
to_check = []
to_check_new = sorted([f for f in self.directory.iterdir() if f.is_file()])
if to_check_new:
# we have files waiting to be processed
self.logger.debug('{} file(s) are waiting to be processed'.format(len(to_check_new)))
to_check.append(to_check_new[-1])
to_check_archive = sorted([f for f in self.archive_dir.iterdir() if f.is_file()])
if to_check_archive:
# we have files already processed, in the archive
self.logger.debug('{} file(s) have been processed'.format(len(to_check_archive)))
to_check.append(to_check_archive[-1])
if not to_check:
self.logger.debug('New list, no hisorical files')
# nothing has been downloaded ever, moving on
return False
dl_hash = sha512(downloaded)
for last_file in to_check:
with last_file.open('rb') as f:
last_hash = sha512(f.read())
if (dl_hash.digest() == last_hash.digest() and
parser.parse(last_file.name.split('.')[0]).date() == date.today()):
self.logger.debug('Same file already downloaded today.')
return True
return False
async def fetch_list(self):
'''Fetch & store the list'''
if not self.fetcher:
return
set_running(f'{self.__class__.__name__}-{self.vendor}-{self.listname}')
try:
with PidFile(f'{self.listname}.pid', piddir=self.meta):
if not await self.__newer():
unset_running(f'{self.__class__.__name__}-{self.vendor}-{self.listname}')
return
async with aiohttp.ClientSession() as session:
async with session.get(self.url) as r:
content = await r.content.read()
if self.__same_as_last(content):
return
self.logger.info('Got a new file \o/')
with (self.directory / '{}.txt'.format(datetime.now().isoformat())).open('wb') as f:
f.write(content)
unset_running(f'{self.__class__.__name__}-{self.vendor}-{self.listname}')
except PidFileError:
self.logger.info('Fetcher already running')
finally:
unset_running(f'{self.__class__.__name__}-{self.vendor}-{self.listname}')

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from datetime import datetime
from pathlib import Path
import logging
try:
import simplejson as json
except ImportError:
import json
import re
from redis import StrictRedis
from uuid import uuid4
from io import BytesIO
import importlib
from typing import List
import types
from .libs.helpers import safe_create_dir, set_running, unset_running, get_socket_path
class RawFilesParser():
def __init__(self, config_file: Path, storage_directory: Path,
loglevel: int=logging.DEBUG) -> None:
with open(config_file, 'r') as f:
module_parameters = json.load(f)
self.vendor = module_parameters['vendor']
self.listname = module_parameters['name']
if 'parser' in module_parameters:
self.parse_raw_file = types.MethodType(importlib.import_module(module_parameters['parser'], 'bgpranking').parse_raw_file, self)
self.source = f'{self.vendor}-{self.listname}'
self.directory = storage_directory / self.vendor / self.listname
safe_create_dir(self.directory)
self.unparsable_dir = self.directory / 'unparsable'
safe_create_dir(self.unparsable_dir)
self.__init_logger(loglevel)
self.redis_intake = StrictRedis(unix_socket_path=get_socket_path('intake'), db=0)
self.logger.debug(f'Starting intake on {self.source}')
def __init_logger(self, loglevel) -> None:
self.logger = logging.getLogger(f'{self.__class__.__name__}-{self.vendor}-{self.listname}')
self.logger.setLevel(loglevel)
@property
def files_to_parse(self) -> List[Path]:
return sorted([f for f in self.directory.iterdir() if f.is_file()], reverse=True)
def extract_ipv4(self, bytestream: bytes) -> List[bytes]:
return re.findall(rb'[0-9]+(?:\.[0-9]+){3}', bytestream)
def strip_leading_zeros(self, ips: List[bytes]) -> List[bytes]:
'''Helper to get rid of leading 0s in an IP list.
Only run it when needed, it is nasty and slow'''
return ['.'.join(str(int(part)) for part in ip.split(b'.')).encode() for ip in ips]
def parse_raw_file(self, f: BytesIO) -> List[bytes]:
# If the list doesn't provide a time, fallback to current day, midnight
self.datetime = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
return self.extract_ipv4(f.getvalue())
def parse_raw_files(self) -> None:
set_running(f'{self.__class__.__name__}-{self.source}')
nb_unparsable_files = len([f for f in self.unparsable_dir.iterdir() if f.is_file()])
if nb_unparsable_files:
self.logger.warning(f'Was unable to parse {nb_unparsable_files} files.')
try:
for filepath in self.files_to_parse:
self.logger.debug('Parsing {}, {} to go.'.format(filepath, len(self.files_to_parse) - 1))
with open(filepath, 'rb') as f:
to_parse = BytesIO(f.read())
p = self.redis_intake.pipeline()
for ip in self.parse_raw_file(to_parse):
if isinstance(ip, tuple):
ip, datetime = ip
else:
datetime = self.datetime
uuid = uuid4()
p.hmset(str(uuid), {'ip': ip, 'source': self.source,
'datetime': datetime.isoformat()})
p.sadd('intake', str(uuid))
p.execute()
self._archive(filepath)
except Exception as e:
self.logger.exception("That didn't go well")
self._unparsable(filepath)
finally:
unset_running(f'{self.__class__.__name__}-{self.source}')
def _archive(self, filepath: Path) -> None:
'''After processing, move file to the archive directory'''
filepath.rename(self.directory / 'archive' / filepath.name)
def _unparsable(self, filepath: Path) -> None:
'''After processing, move file to the archive directory'''
filepath.rename(self.unparsable_dir / filepath.name)

View File

@ -1,16 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from dateutil.parser import parse
import re
from io import BytesIO
from typing import List
def parse_raw_file(self, f: BytesIO) -> List[bytes]:
if re.findall(b'This feed is not generated for this family', f.getvalue()):
return []
self.datetime = parse(re.findall(b'## Feed generated at: (.*)\n', f.getvalue())[0])
return self.extract_ipv4(f.getvalue())

View File

@ -1,107 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from redis import StrictRedis
from .libs.helpers import set_running, unset_running, get_socket_path, load_config_files, get_ipasn, sanity_check_ipasn
from datetime import datetime, date, timedelta
from ipaddress import ip_network
from pathlib import Path
class Ranking():
def __init__(self, config_dir: Path=None, loglevel: int=logging.DEBUG):
self.__init_logger(loglevel)
self.storage = StrictRedis(unix_socket_path=get_socket_path('storage'), decode_responses=True)
self.ranking = StrictRedis(unix_socket_path=get_socket_path('storage'), db=1, decode_responses=True)
self.ipasn = get_ipasn()
self.config_dir = config_dir
def __init_logger(self, loglevel):
self.logger = logging.getLogger(f'{self.__class__.__name__}')
self.logger.setLevel(loglevel)
def rank_a_day(self, day: str, config_files: dict):
asns_aggregation_key_v4 = f'{day}|asns|v4'
asns_aggregation_key_v6 = f'{day}|asns|v6'
to_delete = set([asns_aggregation_key_v4, asns_aggregation_key_v6])
r_pipeline = self.ranking.pipeline()
cached_meta = {}
for source in self.storage.smembers(f'{day}|sources'):
self.logger.info(f'{day} - Ranking source: {source}')
source_aggregation_key_v4 = f'{day}|{source}|asns|v4'
source_aggregation_key_v6 = f'{day}|{source}|asns|v6'
to_delete.update([source_aggregation_key_v4, source_aggregation_key_v6])
for asn in self.storage.smembers(f'{day}|{source}'):
prefixes_aggregation_key_v4 = f'{day}|{asn}|v4'
prefixes_aggregation_key_v6 = f'{day}|{asn}|v6'
to_delete.update([prefixes_aggregation_key_v4, prefixes_aggregation_key_v6])
if asn == '0':
# Default ASN when no matches. Probably spoofed.
continue
self.logger.debug(f'{day} - Ranking source: {source} / ASN: {asn}')
asn_rank_v4 = 0.0
asn_rank_v6 = 0.0
for prefix in self.storage.smembers(f'{day}|{source}|{asn}'):
if prefix == 'None':
# This should not happen and requires a DB cleanup.
self.logger.critical(f'Fucked up prefix in "{day}|{source}|{asn}"')
continue
ips = set([ip_ts.split('|')[0]
for ip_ts in self.storage.smembers(f'{day}|{source}|{asn}|{prefix}')])
py_prefix = ip_network(prefix)
prefix_rank = float(len(ips)) / py_prefix.num_addresses
r_pipeline.zadd(f'{day}|{source}|{asn}|v{py_prefix.version}|prefixes', {prefix: prefix_rank})
if py_prefix.version == 4:
asn_rank_v4 += len(ips) * config_files[source]['impact']
r_pipeline.zincrby(prefixes_aggregation_key_v4, prefix_rank * config_files[source]['impact'], prefix)
else:
asn_rank_v6 += len(ips) * config_files[source]['impact']
r_pipeline.zincrby(prefixes_aggregation_key_v6, prefix_rank * config_files[source]['impact'], prefix)
if asn in cached_meta:
v4info = cached_meta[asn]['v4']
v6info = cached_meta[asn]['v6']
else:
v4info = self.ipasn.asn_meta(asn=asn, source='caida', address_family='v4', date=day)
v6info = self.ipasn.asn_meta(asn=asn, source='caida', address_family='v6', date=day)
cached_meta[asn] = {'v4': v4info, 'v6': v6info}
ipasnhistory_date_v4 = list(v4info['response'].keys())[0]
v4count = v4info['response'][ipasnhistory_date_v4][asn]['ipcount']
ipasnhistory_date_v6 = list(v6info['response'].keys())[0]
v6count = v6info['response'][ipasnhistory_date_v6][asn]['ipcount']
if v4count:
asn_rank_v4 /= float(v4count)
if asn_rank_v4:
r_pipeline.set(f'{day}|{source}|{asn}|v4', asn_rank_v4)
r_pipeline.zincrby(asns_aggregation_key_v4, asn_rank_v4, asn)
r_pipeline.zadd(source_aggregation_key_v4, {asn: asn_rank_v4})
if v6count:
asn_rank_v6 /= float(v6count)
if asn_rank_v6:
r_pipeline.set(f'{day}|{source}|{asn}|v6', asn_rank_v6)
r_pipeline.zincrby(asns_aggregation_key_v6, asn_rank_v6, asn)
r_pipeline.zadd(source_aggregation_key_v6, {asn: asn_rank_v6})
self.ranking.delete(*to_delete)
r_pipeline.execute()
def compute(self):
config_files = load_config_files(self.config_dir)
ready, message = sanity_check_ipasn(self.ipasn)
if not ready:
# Try again later.
self.logger.warning(message)
return
self.logger.debug(message)
self.logger.info('Start ranking')
set_running(self.__class__.__name__)
today = date.today()
now = datetime.now()
today12am = now.replace(hour=12, minute=0, second=0, microsecond=0)
if now < today12am:
# Compute yesterday and today's ranking (useful when we have lists generated only once a day)
self.rank_a_day((today - timedelta(days=1)).isoformat(), config_files)
self.rank_a_day(today.isoformat(), config_files)
unset_running(self.__class__.__name__)
self.logger.info('Ranking done.')

View File

@ -1,85 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from datetime import timezone
from dateutil import parser
import logging
from redis import StrictRedis
import ipaddress
from .libs.helpers import shutdown_requested, set_running, unset_running, get_socket_path, get_ipasn, sanity_check_ipasn
class Sanitizer():
def __init__(self, loglevel: int=logging.DEBUG):
self.__init_logger(loglevel)
self.redis_intake = StrictRedis(unix_socket_path=get_socket_path('intake'), db=0, decode_responses=True)
self.redis_sanitized = StrictRedis(unix_socket_path=get_socket_path('prepare'), db=0, decode_responses=True)
self.ipasn = get_ipasn()
self.logger.debug('Starting import')
def __init_logger(self, loglevel):
self.logger = logging.getLogger(f'{self.__class__.__name__}')
self.logger.setLevel(loglevel)
def sanitize(self):
ready, message = sanity_check_ipasn(self.ipasn)
if not ready:
# Try again later.
self.logger.warning(message)
return
self.logger.debug(message)
set_running(self.__class__.__name__)
while True:
if shutdown_requested() or not self.ipasn.is_up:
break
uuids = self.redis_intake.spop('intake', 100)
if not uuids:
break
for_cache = []
pipeline = self.redis_sanitized.pipeline(transaction=False)
for uuid in uuids:
data = self.redis_intake.hgetall(uuid)
try:
ip = ipaddress.ip_address(data['ip'])
if isinstance(ip, ipaddress.IPv6Address):
address_family = 'v6'
else:
address_family = 'v4'
except ValueError:
self.logger.info(f"Invalid IP address: {data['ip']}")
continue
except KeyError:
self.logger.info(f"Invalid entry {data}")
continue
if not ip.is_global:
self.logger.info(f"The IP address {data['ip']} is not global")
continue
datetime = parser.parse(data['datetime'])
if datetime.tzinfo:
# Make sure the datetime isn't TZ aware, and UTC.
datetime = datetime.astimezone(timezone.utc).replace(tzinfo=None)
for_cache.append({'ip': str(ip), 'address_family': address_family, 'source': 'caida',
'date': datetime.isoformat(), 'precision_delta': {'days': 3}})
# Add to temporay DB for further processing
pipeline.hmset(uuid, {'ip': str(ip), 'source': data['source'], 'address_family': address_family,
'date': datetime.date().isoformat(), 'datetime': datetime.isoformat()})
pipeline.sadd('to_insert', uuid)
pipeline.execute()
self.redis_intake.delete(*uuids)
try:
# Just cache everything so the lookup scripts can do their thing.
self.ipasn.mass_cache(for_cache)
except Exception:
self.logger.exception('Mass cache in IPASN History failed, trying again later.')
# Rollback the spop
self.redis_intake.sadd('intake', *uuids)
break
unset_running(self.__class__.__name__)

View File

@ -1,171 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import aiohttp
import logging
from bs4 import BeautifulSoup
from dateutil.parser import parse
from typing import Tuple
from datetime import datetime, date
from pathlib import Path
from .libs.helpers import safe_create_dir, set_running, unset_running
try:
import simplejson as json
except ImportError:
import json
class ShadowServerFetcher():
def __init__(self, user, password, config_path_modules: Path, storage_directory: Path,
loglevel: int=logging.DEBUG) -> None:
self.__init_logger(loglevel)
self.storage_directory = storage_directory
self.config_path_modules = config_path_modules
self.user = user
self.password = password
self.index_page = 'https://dl.shadowserver.org/reports/index.php'
self.vendor = 'shadowserver'
self.known_list_types = ('blacklist', 'botnet', 'cc', 'cisco', 'cwsandbox', 'drone',
'microsoft', 'scan', 'sinkhole6', 'sinkhole', 'outdated',
'compromised', 'hp', 'darknet', 'ddos')
self.first_available_day = None
self.last_available_day = None
self.available_entries = {}
def __init_logger(self, loglevel):
self.logger = logging.getLogger(f'{self.__class__.__name__}')
self.logger.setLevel(loglevel)
async def __get_index(self):
auth_details = {'user': self.user, 'password': self.password, 'login': 'Login'}
async with aiohttp.ClientSession() as s:
self.logger.debug('Fetching the index.')
async with s.post(self.index_page, data=auth_details) as r:
return await r.text()
async def __build_daily_dict(self):
html_index = await self.__get_index()
soup = BeautifulSoup(html_index, 'html.parser')
treeview = soup.find(id='treemenu1')
for y in treeview.select(':scope > li'):
year = y.contents[0]
for m in y.contents[1].select(':scope > li'):
month = m.contents[0]
for d in m.contents[1].select(':scope > li'):
day = d.contents[0]
date = parse(f'{year} {month} {day}').date()
self.available_entries[date.isoformat()] = []
for a in d.contents[1].find_all('a', href=True):
if not self.first_available_day:
self.first_available_day = date
self.last_available_day = date
self.available_entries[date.isoformat()].append((a['href'], a.string))
self.logger.debug('Dictionary created.')
def __normalize_day(self, day: Tuple[str, date, datetime]=None) -> str:
if not day:
if not self.last_available_day:
raise Exception('Unable to figure out the last available day. You need to run build_daily_dict first')
day = self.last_available_day
else:
if isinstance(day, str):
day = parse(day).date()
elif isinstance(day, datetime):
day = day.date()
return day.isoformat()
def __split_name(self, name):
type_content, country, list_type = name.split('-')
if '_' in type_content:
type_content, details_type = type_content.split('_', maxsplit=1)
if '_' in details_type:
details_type, sub = details_type.split('_', maxsplit=1)
return list_type, country, (type_content, details_type, sub)
return list_type, country, (type_content, details_type)
return list_type, country, (type_content)
def __check_config(self, filename: str) -> Path:
self.logger.debug(f'Working on config for {filename}.')
config = {'vendor': 'shadowserver', 'parser': '.parsers.shadowserver'}
type_content, _, type_details = self.__split_name(filename)
prefix = type_content.split('.')[0]
if isinstance(type_details, str):
main_type = type_details
config['name'] = '{}-{}'.format(prefix, type_details)
else:
main_type = type_details[0]
config['name'] = '{}-{}'.format(prefix, '_'.join(type_details))
if main_type not in self.known_list_types:
self.logger.warning(f'Unknown type: {main_type}. Please update the config creator script.')
return None
if main_type == 'blacklist':
config['impact'] = 5
elif main_type == 'botnet':
config['impact'] = 2
elif main_type == 'cc':
config['impact'] = 5
elif main_type == 'cisco':
config['impact'] = 3
elif main_type == 'cwsandbox':
config['impact'] = 5
elif main_type == 'drone':
config['impact'] = 2
elif main_type == 'microsoft':
config['impact'] = 3
elif main_type == 'scan':
config['impact'] = 1
elif main_type == 'sinkhole6':
config['impact'] = 2
elif main_type == 'sinkhole':
config['impact'] = 2
else:
config['impact'] = 1
if not (self.config_path_modules / f"{config['vendor']}_{config['name']}.json").exists():
self.logger.debug(f'Creating config file for {filename}.')
with open(self.config_path_modules / f"{config['vendor']}_{config['name']}.json", 'w') as f:
json.dump(config, f, indent=2)
else:
with open(self.config_path_modules / f"{config['vendor']}_{config['name']}.json", 'r') as f:
# Validate new config file with old
config_current = json.load(f)
if config_current != config:
self.logger.warning('The config file created by this script is different from the one on disk: \n{}\n{}'.format(json.dumps(config), json.dumps(config_current)))
# Init list directory
directory = self.storage_directory / config['vendor'] / config['name']
safe_create_dir(directory)
meta = directory / 'meta'
safe_create_dir(meta)
archive_dir = directory / 'archive'
safe_create_dir(archive_dir)
self.logger.debug(f'Done with config for {filename}.')
return directory
async def download_daily_entries(self, day: Tuple[str, date, datetime]=None):
set_running(f'{self.__class__.__name__}')
await self.__build_daily_dict()
for url, filename in self.available_entries[self.__normalize_day(day)]:
storage_dir = self.__check_config(filename)
if not storage_dir:
continue
# Check if the file we're trying to download has already been downloaded. Skip if True.
uuid = url.split('/')[-1]
if (storage_dir / 'meta' / 'last_download').exists():
with open(storage_dir / 'meta' / 'last_download') as f:
last_download_uuid = f.read()
if last_download_uuid == uuid:
self.logger.debug(f'Already downloaded: {url}.')
continue
async with aiohttp.ClientSession() as s:
async with s.get(url) as r:
self.logger.info(f'Downloading {url}.')
content = await r.content.read()
with (storage_dir / '{}.txt'.format(datetime.now().isoformat())).open('wb') as f:
f.write(content)
with open(storage_dir / 'meta' / 'last_download', 'w') as f:
f.write(uuid)
unset_running(f'{self.__class__.__name__}')

View File

@ -1,18 +1,19 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
from enum import Enum
from datetime import datetime, timedelta
from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network
from typing import TypeVar
from .helpers import get_homedir, safe_create_dir
try:
import simplejson as json
except ImportError:
import json
from dateutil.parser import parse
import copy
import json
from datetime import datetime, timedelta
from enum import Enum
from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network
from typing import TypeVar, Optional, Dict, Any
import requests
from dateutil.parser import parse
from .helpers import get_homedir, safe_create_dir
IPTypes = TypeVar('IPTypes', IPv4Address, IPv6Address, 'str')
PrefixTypes = TypeVar('PrefixTypes', IPv4Network, IPv6Network, 'str')
@ -84,7 +85,7 @@ class StatsRIPE():
with open(c_path, 'w') as f:
json.dump(response, f, indent=2)
def _get(self, method: str, parameters: dict) -> dict:
def _get(self, method: str, parameters: Dict) -> Dict:
parameters['sourceapp'] = self.sourceapp
cached = self._get_cache(method, parameters)
if cached:
@ -100,8 +101,8 @@ class StatsRIPE():
return self._get('network-info', parameters)
def prefix_overview(self, prefix: PrefixTypes, min_peers_seeing: int= 0,
max_related: int=0, query_time: TimeTypes=None) -> dict:
parameters = {'resource': prefix}
max_related: int=0, query_time: Optional[TimeTypes]=None) -> dict:
parameters: Dict[str, Any] = {'resource': prefix}
if min_peers_seeing:
parameters['min_peers_seeing'] = min_peers_seeing
if max_related:
@ -110,8 +111,8 @@ class StatsRIPE():
parameters['query_time'] = self.__time_to_text(query_time)
return self._get('prefix-overview', parameters)
def ris_asns(self, query_time: TimeTypes=None, list_asns: bool=False, asn_types: ASNsTypes=ASNsTypes.undefined):
parameters = {}
def ris_asns(self, query_time: Optional[TimeTypes]=None, list_asns: bool=False, asn_types: ASNsTypes=ASNsTypes.undefined):
parameters: Dict[str, Any] = {}
if list_asns:
parameters['list_asns'] = list_asns
if asn_types:
@ -120,10 +121,10 @@ class StatsRIPE():
parameters['query_time'] = self.__time_to_text(query_time)
return self._get('ris-asns', parameters)
def ris_prefixes(self, asn: int, query_time: TimeTypes=None,
def ris_prefixes(self, asn: int, query_time: Optional[TimeTypes]=None,
list_prefixes: bool=False, types: ASNsTypes=ASNsTypes.undefined,
af: AddressFamilies=AddressFamilies.undefined, noise: Noise=Noise.keep):
parameters = {'resource': str(asn)}
parameters: Dict[str, Any] = {'resource': str(asn)}
if query_time:
parameters['query_time'] = self.__time_to_text(query_time)
if list_prefixes:
@ -136,8 +137,8 @@ class StatsRIPE():
parameters['noise'] = noise.value
return self._get('ris-prefixes', parameters)
def country_asns(self, country: str, details: int=0, query_time: TimeTypes=None):
parameters = {'resource': country}
def country_asns(self, country: str, details: int=0, query_time: Optional[TimeTypes]=None):
parameters: Dict[str, Any] = {'resource': country}
if details:
parameters['lod'] = details
if query_time:

View File

@ -1,42 +1,79 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from bgpranking.archive import DeepArchive
import json
import logging
import zipfile
from collections import defaultdict
from datetime import date
from logging import Logger
from pathlib import Path
from bgpranking.libs.helpers import get_config_path, get_homedir
from pid import PidFile, PidFileError
from dateutil import parser
from dateutil.relativedelta import relativedelta
from bgpranking.default import safe_create_dir, AbstractManager
from bgpranking.helpers import get_modules, get_data_dir
logger = logging.getLogger('Archiver')
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO, datefmt='%I:%M:%S')
# NOTE:
# * Supposed to run once every ~2 months
class DeepArchive():
class ModulesArchiver():
def __init__(self, config_dir: Path=None, storage_directory: Path=None, loglevel: int=logging.INFO):
if not config_dir:
config_dir = get_config_path()
if not storage_directory:
self.storage_directory = get_homedir() / 'rawdata'
modules_config = config_dir / 'modules'
modules_paths = [modulepath for modulepath in modules_config.glob('*.json')]
self.modules = [DeepArchive(path, self.storage_directory, loglevel) for path in modules_paths]
def __init__(self, config_file: Path, logger: Logger):
'''Archive everyfile older than 2 month.'''
with config_file.open() as f:
module_parameters = json.load(f)
self.logger = logger
self.vendor = module_parameters['vendor']
self.listname = module_parameters['name']
self.directory = get_data_dir() / self.vendor / self.listname / 'archive'
safe_create_dir(self.directory)
self.deep_archive = self.directory / 'deep'
safe_create_dir(self.deep_archive)
def archive(self):
to_archive = defaultdict(list)
today = date.today()
last_day_to_keep = date(today.year, today.month, 1) - relativedelta(months=2)
for p in self.directory.iterdir():
if not p.is_file():
continue
filedate = parser.parse(p.name.split('.')[0]).date()
if filedate >= last_day_to_keep:
continue
to_archive['{}.zip'.format(filedate.strftime('%Y%m'))].append(p)
if to_archive:
self.logger.info('Found old files. Archiving: {}'.format(', '.join(to_archive.keys())))
else:
self.logger.debug('No old files.')
for archivename, path_list in to_archive.items():
with zipfile.ZipFile(self.deep_archive / archivename, 'x', zipfile.ZIP_DEFLATED) as z:
for f in path_list:
z.write(f, f.name)
# Delete all the files if the archiving worked out properly
[f.unlink() for f in path_list]
class ModulesArchiver(AbstractManager):
def __init__(self, loglevel: int=logging.INFO):
super().__init__(loglevel)
self.script_name = 'archiver'
self.modules = [DeepArchive(path, self.logger) for path in get_modules()]
def _to_run_forever(self):
[module.archive() for module in self.modules]
if __name__ == '__main__':
def main():
archiver = ModulesArchiver()
try:
with PidFile(piddir=archiver.storage_directory):
logger.info('Archiving...')
archiver.archive()
logger.info('... done.')
except PidFileError:
logger.warning('Archiver already running, skip.')
archiver.run(sleep_in_sec=360000)
if __name__ == '__main__':
main()

View File

@ -2,28 +2,80 @@
# -*- coding: utf-8 -*-
import logging
from pathlib import Path
import re
import requests
from bgpranking.abstractmanager import AbstractManager
from bgpranking.asn_descriptions import ASNDescriptions
from bgpranking.libs.helpers import get_homedir
from dateutil.parser import parse
from redis import Redis
from bgpranking.default import get_socket_path, safe_create_dir, AbstractManager, get_config
from bgpranking.helpers import get_data_dir
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO, datefmt='%I:%M:%S')
class ASNDescriptionsManager(AbstractManager):
class ASNDescriptions(AbstractManager):
def __init__(self, storage_directory: Path=None, loglevel: int=logging.DEBUG):
def __init__(self, loglevel: int=logging.INFO):
super().__init__(loglevel)
if not storage_directory:
storage_directory = get_homedir() / 'rawdata'
self.asn_descr = ASNDescriptions(storage_directory, loglevel)
self.script_name = 'asn_descr'
self.asn_meta = Redis(get_config('generic', 'storage_db_hostname'), get_config('generic', 'storage_db_port'), db=2, decode_responses=True)
self.logger.debug('Starting ASN History')
self.directory = get_data_dir() / 'asn_descriptions'
safe_create_dir(self.directory)
self.archives = self.directory / 'archive'
safe_create_dir(self.archives)
self.url = 'https://www.cidr-report.org/as2.0/autnums.html'
def __update_available(self):
r = requests.head(self.url)
print(r.headers)
current_last_modified = parse(r.headers['Last-Modified'])
if not self.asn_meta.exists('ans_description_last_update'):
return True
last_update = parse(self.asn_meta.get('ans_description_last_update')) # type: ignore
if last_update < current_last_modified:
return True
return False
def load_descriptions(self):
if not self.__update_available():
self.logger.debug('No new file to import.')
return
self.logger.info('Importing new ASN descriptions.')
r = requests.get(self.url)
last_modified = parse(r.headers['Last-Modified']).isoformat()
p = self.asn_meta.pipeline()
new_asn = 0
new_description = 0
for asn, descr in re.findall('as=AS(.*)&.*</a> (.*)\n', r.text):
existing_descriptions = self.asn_meta.hgetall(f'{asn}|descriptions')
if not existing_descriptions:
self.logger.debug(f'New ASN: {asn} - {descr}')
p.hset(f'{asn}|descriptions', last_modified, descr)
new_asn += 1
else:
last_descr = sorted(existing_descriptions.keys(), reverse=True)[0]
if descr != existing_descriptions[last_descr]:
self.logger.debug(f'New description for {asn}: {existing_descriptions[last_descr]} -> {descr}')
p.hset(f'{asn}|descriptions', last_modified, descr)
new_description += 1
p.set('ans_description_last_update', last_modified)
p.execute()
self.logger.info(f'Done with import. New ASNs: {new_asn}, new descriptions: {new_description}')
if new_asn or new_description:
with open(self.archives / f'{last_modified}.html', 'w') as f:
f.write(r.text)
def _to_run_forever(self):
self.asn_descr.load_descriptions()
self.load_descriptions()
def main():
asnd_manager = ASNDescriptions()
asnd_manager.run(sleep_in_sec=3600)
if __name__ == '__main__':
asnd_manager = ASNDescriptionsManager()
asnd_manager.run(sleep_in_sec=3600)
main()

View File

@ -2,8 +2,15 @@
# -*- coding: utf-8 -*-
import logging
from bgpranking.abstractmanager import AbstractManager
from bgpranking.dbinsert import DatabaseInsert
import time
from typing import List
from redis import Redis
from bgpranking.default import get_socket_path, AbstractManager, get_config
from bgpranking.helpers import get_ipasn, sanity_check_ipasn
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO, datefmt='%I:%M:%S')
@ -13,12 +20,107 @@ class DBInsertManager(AbstractManager):
def __init__(self, loglevel: int=logging.INFO):
super().__init__(loglevel)
self.dbinsert = DatabaseInsert(loglevel)
self.script_name = 'db_insert'
self.kvrocks_storage = Redis(get_config('generic', 'storage_db_hostname'), get_config('generic', 'storage_db_port'), decode_responses=True)
self.redis_sanitized = Redis(unix_socket_path=get_socket_path('prepare'), db=0, decode_responses=True)
self.ipasn = get_ipasn()
self.logger.debug('Starting import')
def _to_run_forever(self):
self.dbinsert.insert()
ready, message = sanity_check_ipasn(self.ipasn)
if not ready:
# Try again later.
self.logger.warning(message)
return
self.logger.debug(message)
while True:
if self.shutdown_requested():
break
try:
if not self.ipasn.is_up:
break
except Exception:
self.logger.warning('Unable to query ipasnhistory')
time.sleep(10)
continue
uuids: List[str] = self.redis_sanitized.spop('to_insert', 100) # type: ignore
if not uuids:
break
p = self.redis_sanitized.pipeline(transaction=False)
[p.hgetall(uuid) for uuid in uuids]
sanitized_data = p.execute()
for_query = []
for i, uuid in enumerate(uuids):
data = sanitized_data[i]
if not data:
self.logger.warning(f'No data for UUID {uuid}. This should not happen, but lets move on.')
continue
for_query.append({'ip': data['ip'], 'address_family': data['address_family'], 'source': 'caida',
'date': data['datetime'], 'precision_delta': {'days': 3}})
try:
responses = self.ipasn.mass_query(for_query)
except Exception:
self.logger.exception('Mass query in IPASN History failed, trying again later.')
# Rollback the spop
self.redis_sanitized.sadd('to_insert', *uuids)
time.sleep(10)
continue
retry = []
done = []
ardb_pipeline = self.kvrocks_storage.pipeline(transaction=False)
for i, uuid in enumerate(uuids):
data = sanitized_data[i]
if not data:
self.logger.warning(f'No data for UUID {uuid}. This should not happen, but lets move on.')
continue
routing_info = responses['responses'][i]['response'] # our queries are on one single date, not a range
# Data gathered from IPASN History:
# * IP Block of the IP
# * AS number
if 'error' in routing_info:
self.logger.warning(f"Unable to find routing information for {data['ip']} - {data['datetime']}: {routing_info['error']}")
continue
# Single date query, getting from the object
datetime_routing = list(routing_info.keys())[0]
entry = routing_info[datetime_routing]
if not entry:
# routing info is missing, need to try again later.
retry.append(uuid)
continue
if 'asn' in entry and entry['asn'] is None:
self.logger.warning(f"Unable to find the AS number associated to {data['ip']} - {data['datetime']} (got None). This should not happen...")
continue
if 'prefix' in entry and entry['prefix'] is None:
self.logger.warning(f"Unable to find the prefix associated to {data['ip']} - {data['datetime']} (got None). This should not happen...")
continue
# Format: <YYYY-MM-DD>|sources -> set([<source>, ...])
ardb_pipeline.sadd(f"{data['date']}|sources", data['source'])
# Format: <YYYY-MM-DD>|<source> -> set([<asn>, ...])
ardb_pipeline.sadd(f"{data['date']}|{data['source']}", entry['asn'])
# Format: <YYYY-MM-DD>|<source>|<asn> -> set([<prefix>, ...])
ardb_pipeline.sadd(f"{data['date']}|{data['source']}|{entry['asn']}", entry['prefix'])
# Format: <YYYY-MM-DD>|<source>|<asn>|<prefix> -> set([<ip>|<datetime>, ...])
ardb_pipeline.sadd(f"{data['date']}|{data['source']}|{entry['asn']}|{entry['prefix']}",
f"{data['ip']}|{data['datetime']}")
done.append(uuid)
ardb_pipeline.execute()
p = self.redis_sanitized.pipeline(transaction=False)
if done:
p.delete(*done)
if retry:
p.sadd('to_insert', *retry)
p.execute()
def main():
dbinsert = DBInsertManager()
dbinsert.run(sleep_in_sec=120)
if __name__ == '__main__':
dbinsert = DBInsertManager()
dbinsert.run(sleep_in_sec=120)
main()

View File

@ -1,50 +1,177 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import json
import asyncio
from pathlib import Path
import aiohttp
import logging
from datetime import datetime, date
from hashlib import sha512 # Faster than sha256 on 64b machines.
from logging import Logger
from pathlib import Path
import aiohttp
from dateutil import parser
from pid import PidFile, PidFileError # type: ignore
from bgpranking.default import AbstractManager, safe_create_dir
from bgpranking.helpers import get_modules, get_data_dir, get_modules_dir
from bgpranking.abstractmanager import AbstractManager
from bgpranking.modulesfetcher import Fetcher
from bgpranking.libs.helpers import get_config_path, get_homedir
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO, datefmt='%I:%M:%S')
class Fetcher():
def __init__(self, config_file: Path, logger: Logger):
'''Load `config_file`, and store the fetched data into `storage_directory`
Note: if the `config_file` does not provide a URL (the file is
gathered by some oter mean), the fetcher is automatically stoped.'''
with open(config_file, 'r') as f:
module_parameters = json.load(f)
self.vendor = module_parameters['vendor']
self.listname = module_parameters['name']
self.logger = logger
self.fetcher = True
if 'url' not in module_parameters:
self.logger.info(f'{self.vendor}-{self.listname}: No URL to fetch, breaking.')
self.fetcher = False
return
self.url = module_parameters['url']
self.logger.debug(f'{self.vendor}-{self.listname}: Starting fetcher on {self.url}')
self.directory = get_data_dir() / self.vendor / self.listname
safe_create_dir(self.directory)
self.meta = self.directory / 'meta'
safe_create_dir(self.meta)
self.archive_dir = self.directory / 'archive'
safe_create_dir(self.archive_dir)
self.first_fetch = True
async def __get_last_modified(self):
async with aiohttp.ClientSession() as session:
async with session.head(self.url) as r:
headers = r.headers
if 'Last-Modified' in headers:
return parser.parse(headers['Last-Modified'])
return None
async def __newer(self):
'''Check if the file available for download is newed than the one
already downloaded by checking the `Last-Modified` header.
Note: return False if the file containing the last header content
is not existing, or the header doesn't have this key.
'''
last_modified_path = self.meta / 'lastmodified'
if not last_modified_path.exists():
# The file doesn't exists
if not self.first_fetch:
# The URL has no Last-Modified header, we cannot use it.
self.logger.debug(f'{self.vendor}-{self.listname}: No Last-Modified header available')
return True
self.first_fetch = False
last_modified = await self.__get_last_modified()
if last_modified:
self.logger.debug(f'{self.vendor}-{self.listname}: Last-Modified header available')
with last_modified_path.open('w') as f:
f.write(last_modified.isoformat())
else:
self.logger.debug(f'{self.vendor}-{self.listname}: No Last-Modified header available')
return True
with last_modified_path.open() as f:
file_content = f.read()
last_modified_file = parser.parse(file_content)
last_modified = await self.__get_last_modified()
if not last_modified:
# No more Last-Modified header Oo
self.logger.warning(f'{self.vendor}-{self.listname}: Last-Modified header was present, isn\'t anymore!')
last_modified_path.unlink()
return True
if last_modified > last_modified_file:
self.logger.info(f'{self.vendor}-{self.listname}: Got a new file.')
with last_modified_path.open('w') as f:
f.write(last_modified.isoformat())
return True
return False
def __same_as_last(self, downloaded):
'''Figure out the last downloaded file, check if it is the same as the
newly downloaded one. Returns true if both files have been downloaded the
same day.
Note: we check the new and the archive directory because we may have backlog
and the newest file is always the first one we process
'''
to_check = []
to_check_new = sorted([f for f in self.directory.iterdir() if f.is_file()])
if to_check_new:
# we have files waiting to be processed
self.logger.debug(f'{self.vendor}-{self.listname}: {len(to_check_new)} file(s) are waiting to be processed')
to_check.append(to_check_new[-1])
to_check_archive = sorted([f for f in self.archive_dir.iterdir() if f.is_file()])
if to_check_archive:
# we have files already processed, in the archive
self.logger.debug(f'{self.vendor}-{self.listname}: {len(to_check_archive)} file(s) have been processed')
to_check.append(to_check_archive[-1])
if not to_check:
self.logger.debug(f'{self.vendor}-{self.listname}: New list, no hisorical files')
# nothing has been downloaded ever, moving on
return False
dl_hash = sha512(downloaded)
for last_file in to_check:
with last_file.open('rb') as f:
last_hash = sha512(f.read())
if (dl_hash.digest() == last_hash.digest()
and parser.parse(last_file.name.split('.')[0]).date() == date.today()):
self.logger.debug(f'{self.vendor}-{self.listname}: Same file already downloaded today.')
return True
return False
async def fetch_list(self):
'''Fetch & store the list'''
if not self.fetcher:
return
try:
with PidFile(f'{self.listname}.pid', piddir=self.meta):
if not await self.__newer():
return
async with aiohttp.ClientSession() as session:
async with session.get(self.url) as r:
content = await r.content.read()
if self.__same_as_last(content):
return
self.logger.info(f'{self.vendor}-{self.listname}: Got a new file!')
with (self.directory / '{}.txt'.format(datetime.now().isoformat())).open('wb') as f:
f.write(content)
except PidFileError:
self.logger.info(f'{self.vendor}-{self.listname}: Fetcher already running')
class ModulesManager(AbstractManager):
def __init__(self, config_dir: Path=None, storage_directory: Path=None, loglevel: int=logging.DEBUG):
def __init__(self, loglevel: int=logging.DEBUG):
super().__init__(loglevel)
if not config_dir:
config_dir = get_config_path()
if not storage_directory:
self.storage_directory = get_homedir() / 'rawdata'
self.modules_config = config_dir / 'modules'
self.modules_paths = [modulepath for modulepath in self.modules_config.glob('*.json')]
self.modules = [Fetcher(path, self.storage_directory, loglevel) for path in self.modules_paths]
self.script_name = 'modules_manager'
self.modules_paths = get_modules()
self.modules = [Fetcher(path, self.logger) for path in self.modules_paths]
def _to_run_forever(self):
async def _to_run_forever_async(self):
# Check if there are new config files
new_modules_paths = [modulepath for modulepath in self.modules_config.glob('*.json') if modulepath not in self.modules_paths]
self.modules += [Fetcher(path, self.storage_directory, self.loglevel) for path in new_modules_paths]
new_modules_paths = [modulepath for modulepath in get_modules_dir().glob('*.json') if modulepath not in self.modules_paths]
self.modules += [Fetcher(path, self.logger) for path in new_modules_paths]
self.modules_paths += new_modules_paths
if self.modules:
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(asyncio.gather(
*[module.fetch_list() for module in self.modules if module.fetcher],
return_exceptions=True)
)
except aiohttp.client_exceptions.ClientConnectorError as e:
self.logger.critical(f'Exception while fetching lists: {e}')
for module in self.modules:
if module.fetcher:
await module.fetch_list()
else:
self.logger.info('No config files were found so there are no fetchers running yet. Will try again later.')
def main():
m = ModulesManager()
asyncio.run(m.run_async(sleep_in_sec=3600))
if __name__ == '__main__':
modules_manager = ModulesManager()
modules_manager.run(sleep_in_sec=3600)
main()

View File

@ -6,8 +6,8 @@ import logging
from dateutil.parser import parse
from datetime import timedelta
from bgpranking.libs.helpers import load_config_files
from bgpranking.ranking import Ranking
from bgpranking.helpers import load_all_modules_configs
from .ranking import Ranking
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.DEBUG, datefmt='%I:%M:%S')
@ -21,13 +21,13 @@ if __name__ == '__main__':
args = parser.parse_args()
ranking = Ranking(loglevel=logging.DEBUG)
config_files = load_config_files()
config_files = load_all_modules_configs()
if args.day:
day = parse(args.day).date().isoformat()
ranking.rank_a_day(day, config_files)
ranking.rank_a_day(day)
else:
current = parse(args.interval[1]).date()
stop_date = parse(args.interval[0]).date()
while current >= stop_date:
ranking.rank_a_day(current.isoformat(), config_files)
ranking.rank_a_day(current.isoformat())
current -= timedelta(days=1)

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from bgpranking.monitor import Monitor
import logging
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO, datefmt='%I:%M:%S')
class MonitorManager():
def __init__(self, loglevel: int=logging.INFO):
self.monitor = Monitor()
def get_values(self):
return self.monitor.get_values()
if __name__ == '__main__':
m = MonitorManager()
print(m.get_values())

View File

@ -1,41 +1,124 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import importlib
import json
import logging
from pathlib import Path
import re
import types
from datetime import datetime
from io import BytesIO
from logging import Logger
from pathlib import Path
from typing import List, Union, Tuple
from uuid import uuid4
from redis import Redis
from bgpranking.default import AbstractManager, safe_create_dir, get_socket_path
from bgpranking.helpers import get_modules, get_data_dir, get_modules_dir
from bgpranking.abstractmanager import AbstractManager
from bgpranking.parser import RawFilesParser
from bgpranking.libs.helpers import get_config_path, get_homedir
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO, datefmt='%I:%M:%S')
class RawFilesParser():
def __init__(self, config_file: Path, logger: Logger) -> None:
self.logger = logger
with open(config_file, 'r') as f:
module_parameters = json.load(f)
self.vendor = module_parameters['vendor']
self.listname = module_parameters['name']
if 'parser' in module_parameters:
self.parse_raw_file = types.MethodType(importlib.import_module(module_parameters['parser'], 'bgpranking').parse_raw_file, self) # type: ignore
self.source = f'{self.vendor}-{self.listname}'
self.directory = get_data_dir() / self.vendor / self.listname
safe_create_dir(self.directory)
self.unparsable_dir = self.directory / 'unparsable'
safe_create_dir(self.unparsable_dir)
self.redis_intake = Redis(unix_socket_path=get_socket_path('intake'), db=0)
self.logger.debug(f'{self.source}: Starting intake.')
@property
def files_to_parse(self) -> List[Path]:
return sorted([f for f in self.directory.iterdir() if f.is_file()], reverse=True)
def extract_ipv4(self, bytestream: bytes) -> List[Union[bytes, Tuple[bytes, datetime]]]:
return re.findall(rb'[0-9]+(?:\.[0-9]+){3}', bytestream)
def strip_leading_zeros(self, ips: List[bytes]) -> List[bytes]:
'''Helper to get rid of leading 0s in an IP list.
Only run it when needed, it is nasty and slow'''
return ['.'.join(str(int(part)) for part in ip.split(b'.')).encode() for ip in ips]
def parse_raw_file(self, f: BytesIO) -> List[Union[bytes, Tuple[bytes, datetime]]]:
# If the list doesn't provide a time, fallback to current day, midnight
self.datetime = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
return self.extract_ipv4(f.getvalue())
def parse_raw_files(self) -> None:
nb_unparsable_files = len([f for f in self.unparsable_dir.iterdir() if f.is_file()])
if nb_unparsable_files:
self.logger.warning(f'{self.source}: Was unable to parse {nb_unparsable_files} files.')
try:
for filepath in self.files_to_parse:
self.logger.debug(f'{self.source}: Parsing {filepath}, {len(self.files_to_parse) - 1} to go.')
with open(filepath, 'rb') as f:
to_parse = BytesIO(f.read())
p = self.redis_intake.pipeline()
for line in self.parse_raw_file(to_parse):
if isinstance(line, tuple):
ip, datetime = line
else:
ip = line
datetime = self.datetime
uuid = uuid4()
p.hmset(str(uuid), {'ip': ip, 'source': self.source,
'datetime': datetime.isoformat()})
p.sadd('intake', str(uuid))
p.execute()
self._archive(filepath)
except Exception as e:
self.logger.warning(f"{self.source}: That didn't go well: {e}")
self._unparsable(filepath)
def _archive(self, filepath: Path) -> None:
'''After processing, move file to the archive directory'''
filepath.rename(self.directory / 'archive' / filepath.name)
def _unparsable(self, filepath: Path) -> None:
'''After processing, move file to the archive directory'''
filepath.rename(self.unparsable_dir / filepath.name)
class ParserManager(AbstractManager):
def __init__(self, config_dir: Path=None, storage_directory: Path=None, loglevel: int=logging.DEBUG):
def __init__(self, loglevel: int=logging.DEBUG):
super().__init__(loglevel)
if not config_dir:
config_dir = get_config_path()
if not storage_directory:
self.storage_directory = get_homedir() / 'rawdata'
self.modules_config = config_dir / 'modules'
self.modules_paths = [modulepath for modulepath in self.modules_config.glob('*.json')]
self.modules = [RawFilesParser(path, self.storage_directory, loglevel) for path in self.modules_paths]
self.script_name = 'parser'
self.modules_paths = get_modules()
self.modules = [RawFilesParser(path, self.logger) for path in self.modules_paths]
def _to_run_forever(self):
# Check if there are new config files
new_modules_paths = [modulepath for modulepath in self.modules_config.glob('*.json') if modulepath not in self.modules_paths]
self.modules += [RawFilesParser(path, self.storage_directory, self.loglevel) for path in new_modules_paths]
new_modules_paths = [modulepath for modulepath in get_modules_dir().glob('*.json') if modulepath not in self.modules_paths]
self.modules += [RawFilesParser(path, self.logger) for path in new_modules_paths]
self.modules_paths += new_modules_paths
if self.modules:
[module.parse_raw_files() for module in self.modules]
for module in self.modules:
module.parse_raw_files()
else:
self.logger.warning('No config files were found so there are no parsers running yet. Will try again later.')
if __name__ == '__main__':
def main():
parser_manager = ParserManager()
parser_manager.run(sleep_in_sec=120)
if __name__ == '__main__':
main()

View File

@ -2,24 +2,131 @@
# -*- coding: utf-8 -*-
import logging
from bgpranking.abstractmanager import AbstractManager
from bgpranking.ranking import Ranking
from pathlib import Path
from datetime import datetime, date, timedelta
from ipaddress import ip_network
from typing import Dict, Any
from redis import Redis
import requests
from bgpranking.default import AbstractManager, get_config
from bgpranking.helpers import get_ipasn, sanity_check_ipasn, load_all_modules_configs
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO, datefmt='%I:%M:%S')
class RankingManager(AbstractManager):
class Ranking(AbstractManager):
def __init__(self, config_dir: Path=None, loglevel: int=logging.INFO):
def __init__(self, loglevel: int=logging.INFO):
super().__init__(loglevel)
self.ranking = Ranking(config_dir, loglevel)
self.script_name = 'ranking'
self.storage = Redis(get_config('generic', 'storage_db_hostname'), get_config('generic', 'storage_db_port'), decode_responses=True)
self.ranking = Redis(get_config('generic', 'ranking_db_hostname'), get_config('generic', 'ranking_db_port'), decode_responses=True)
self.ipasn = get_ipasn()
def rank_a_day(self, day: str):
asns_aggregation_key_v4 = f'{day}|asns|v4'
asns_aggregation_key_v6 = f'{day}|asns|v6'
to_delete = set([asns_aggregation_key_v4, asns_aggregation_key_v6])
r_pipeline = self.ranking.pipeline()
cached_meta: Dict[str, Dict[str, Any]] = {}
config_files = load_all_modules_configs()
for source in self.storage.smembers(f'{day}|sources'):
self.logger.info(f'{day} - Ranking source: {source}')
source_aggregation_key_v4 = f'{day}|{source}|asns|v4'
source_aggregation_key_v6 = f'{day}|{source}|asns|v6'
to_delete.update([source_aggregation_key_v4, source_aggregation_key_v6])
for asn in self.storage.smembers(f'{day}|{source}'):
prefixes_aggregation_key_v4 = f'{day}|{asn}|v4'
prefixes_aggregation_key_v6 = f'{day}|{asn}|v6'
to_delete.update([prefixes_aggregation_key_v4, prefixes_aggregation_key_v6])
if asn == '0':
# Default ASN when no matches. Probably spoofed.
continue
self.logger.debug(f'{day} - Ranking source: {source} / ASN: {asn}')
asn_rank_v4 = 0.0
asn_rank_v6 = 0.0
for prefix in self.storage.smembers(f'{day}|{source}|{asn}'):
if prefix == 'None':
# This should not happen and requires a DB cleanup.
self.logger.critical(f'Fucked up prefix in "{day}|{source}|{asn}"')
continue
ips = set([ip_ts.split('|')[0]
for ip_ts in self.storage.smembers(f'{day}|{source}|{asn}|{prefix}')])
py_prefix = ip_network(prefix)
prefix_rank = float(len(ips)) / py_prefix.num_addresses
r_pipeline.zadd(f'{day}|{source}|{asn}|v{py_prefix.version}|prefixes', {prefix: prefix_rank})
if py_prefix.version == 4:
asn_rank_v4 += len(ips) * config_files[source]['impact']
r_pipeline.zincrby(prefixes_aggregation_key_v4, prefix_rank * config_files[source]['impact'], prefix)
else:
asn_rank_v6 += len(ips) * config_files[source]['impact']
r_pipeline.zincrby(prefixes_aggregation_key_v6, prefix_rank * config_files[source]['impact'], prefix)
if asn in cached_meta:
v4info = cached_meta[asn]['v4']
v6info = cached_meta[asn]['v6']
else:
retry = 3
while retry:
try:
v4info = self.ipasn.asn_meta(asn=asn, source='caida', address_family='v4', date=day)
v6info = self.ipasn.asn_meta(asn=asn, source='caida', address_family='v6', date=day)
break
except requests.exceptions.ConnectionError:
# Sometimes, ipasnhistory is unreachable try again a few times
retry -= 1
else:
# if it keeps failing, the ASN will be ranked on next run.
continue
cached_meta[asn] = {'v4': v4info, 'v6': v6info}
ipasnhistory_date_v4 = list(v4info['response'].keys())[0]
v4count = v4info['response'][ipasnhistory_date_v4][asn]['ipcount']
ipasnhistory_date_v6 = list(v6info['response'].keys())[0]
v6count = v6info['response'][ipasnhistory_date_v6][asn]['ipcount']
if v4count:
asn_rank_v4 /= float(v4count)
if asn_rank_v4:
r_pipeline.set(f'{day}|{source}|{asn}|v4', asn_rank_v4)
r_pipeline.zincrby(asns_aggregation_key_v4, asn_rank_v4, asn)
r_pipeline.zadd(source_aggregation_key_v4, {asn: asn_rank_v4})
if v6count:
asn_rank_v6 /= float(v6count)
if asn_rank_v6:
r_pipeline.set(f'{day}|{source}|{asn}|v6', asn_rank_v6)
r_pipeline.zincrby(asns_aggregation_key_v6, asn_rank_v6, asn)
r_pipeline.zadd(source_aggregation_key_v6, {asn: asn_rank_v6})
self.ranking.delete(*to_delete)
r_pipeline.execute()
def compute(self):
ready, message = sanity_check_ipasn(self.ipasn)
if not ready:
# Try again later.
self.logger.warning(message)
return
self.logger.debug(message)
self.logger.info('Start ranking')
today = date.today()
now = datetime.now()
today12am = now.replace(hour=12, minute=0, second=0, microsecond=0)
if now < today12am:
# Compute yesterday and today's ranking (useful when we have lists generated only once a day)
self.rank_a_day((today - timedelta(days=1)).isoformat())
self.rank_a_day(today.isoformat())
self.logger.info('Ranking done.')
def _to_run_forever(self):
self.ranking.compute()
self.compute()
def main():
ranking = Ranking()
ranking.run(sleep_in_sec=3600)
if __name__ == '__main__':
ranking = RankingManager()
ranking.run(sleep_in_sec=3600)
main()

View File

@ -1,79 +1,120 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from bgpranking.libs.helpers import get_homedir, check_running
from subprocess import Popen
import argparse
import os
import time
from pathlib import Path
from subprocess import Popen
from typing import Optional, Dict
import argparse
from redis import Redis
from redis.exceptions import ConnectionError
from bgpranking.default import get_homedir, get_socket_path, get_config
def launch_cache(storage_directory: Path=None):
def check_running(name: str) -> bool:
if name == "storage":
r = Redis(get_config('generic', 'storage_db_hostname'), get_config('generic', 'storage_db_port'))
elif name == "ranking":
r = Redis(get_config('generic', 'ranking_db_hostname'), get_config('generic', 'ranking_db_port'))
else:
socket_path = get_socket_path(name)
if not os.path.exists(socket_path):
return False
r = Redis(unix_socket_path=socket_path)
try:
return True if r.ping() else False
except ConnectionError:
return False
def launch_cache(storage_directory: Optional[Path]=None):
if not storage_directory:
storage_directory = get_homedir()
if not check_running('cache'):
Popen(["./run_redis.sh"], cwd=(storage_directory / 'cache'))
def shutdown_cache(storage_directory: Path=None):
def shutdown_cache(storage_directory: Optional[Path]=None):
if not storage_directory:
storage_directory = get_homedir()
Popen(["./shutdown_redis.sh"], cwd=(storage_directory / 'cache'))
r = Redis(unix_socket_path=get_socket_path('cache'))
r.shutdown(save=True)
print('Redis cache database shutdown.')
def launch_temp(storage_directory: Path=None):
def launch_temp(storage_directory: Optional[Path]=None):
if not storage_directory:
storage_directory = get_homedir()
if not check_running('intake') and not check_running('prepare'):
Popen(["./run_redis.sh"], cwd=(storage_directory / 'temp'))
def shutdown_temp(storage_directory: Path=None):
def shutdown_temp(storage_directory: Optional[Path]=None):
if not storage_directory:
storage_directory = get_homedir()
Popen(["./shutdown_redis.sh"], cwd=(storage_directory / 'temp'))
r = Redis(unix_socket_path=get_socket_path('intake'))
r.shutdown(save=True)
print('Redis intake database shutdown.')
r = Redis(unix_socket_path=get_socket_path('prepare'))
r.shutdown(save=True)
print('Redis prepare database shutdown.')
def launch_storage(storage_directory: Path=None):
def launch_storage(storage_directory: Optional[Path]=None):
if not storage_directory:
storage_directory = get_homedir()
if not check_running('storage'):
Popen(["./run_ardb.sh"], cwd=(storage_directory / 'storage'))
Popen(["./run_kvrocks.sh"], cwd=(storage_directory / 'storage'))
def shutdown_storage(storage_directory: Path=None):
def shutdown_storage(storage_directory: Optional[Path]=None):
redis = Redis(get_config('generic', 'storage_db_hostname'), get_config('generic', 'storage_db_port'))
redis.shutdown()
def launch_ranking(storage_directory: Optional[Path]=None):
if not storage_directory:
storage_directory = get_homedir()
Popen(["./shutdown_ardb.sh"], cwd=(storage_directory / 'storage'))
if not check_running('ranking'):
Popen(["./run_kvrocks.sh"], cwd=(storage_directory / 'ranking'))
def shutdown_ranking(storage_directory: Optional[Path]=None):
redis = Redis(get_config('generic', 'ranking_db_hostname'), get_config('generic', 'ranking_db_port'))
redis.shutdown()
def launch_all():
launch_cache()
launch_temp()
launch_storage()
launch_ranking()
def check_all(stop=False):
backends = [['cache', False], ['storage', False],
['intake', False], ['prepare', False]]
def check_all(stop: bool=False):
backends: Dict[str, bool] = {'cache': False, 'storage': False, 'ranking': False,
'intake': False, 'prepare': False}
while True:
for b in backends:
for db_name in backends.keys():
print(backends[db_name])
try:
b[1] = check_running(b[0])
backends[db_name] = check_running(db_name)
except Exception:
b[1] = False
backends[db_name] = False
if stop:
if not any(b[1] for b in backends):
if not any(running for running in backends.values()):
break
else:
if all(b[1] for b in backends):
if all(running for running in backends.values()):
break
for b in backends:
if not stop and not b[1]:
print(f"Waiting on {b[0]}")
if stop and b[1]:
print(f"Waiting on {b[0]}")
for db_name, running in backends.items():
if not stop and not running:
print(f"Waiting on {db_name} to start")
if stop and running:
print(f"Waiting on {db_name} to stop")
time.sleep(1)
@ -81,9 +122,10 @@ def stop_all():
shutdown_cache()
shutdown_temp()
shutdown_storage()
shutdown_ranking()
if __name__ == '__main__':
def main():
parser = argparse.ArgumentParser(description='Manage backend DBs.')
parser.add_argument("--start", action='store_true', default=False, help="Start all")
parser.add_argument("--stop", action='store_true', default=False, help="Stop all")
@ -96,3 +138,7 @@ if __name__ == '__main__':
stop_all()
if not args.stop and args.status:
check_all()
if __name__ == '__main__':
main()

View File

@ -1,25 +1,109 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import ipaddress
import logging
import time
from bgpranking.abstractmanager import AbstractManager
from bgpranking.sanitizer import Sanitizer
from datetime import timezone
from typing import Optional, List
from dateutil import parser
from redis import Redis
import requests
from bgpranking.default import AbstractManager, get_socket_path
from bgpranking.helpers import get_ipasn, sanity_check_ipasn
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.WARNING, datefmt='%I:%M:%S')
class SanitizerManager(AbstractManager):
class Sanitizer(AbstractManager):
def __init__(self, loglevel: int=logging.WARNING):
def __init__(self, loglevel: int=logging.INFO):
super().__init__(loglevel)
self.sanitizer = Sanitizer(loglevel)
self.script_name = 'sanitizer'
self.redis_intake = Redis(unix_socket_path=get_socket_path('intake'), db=0, decode_responses=True)
self.redis_sanitized = Redis(unix_socket_path=get_socket_path('prepare'), db=0, decode_responses=True)
self.ipasn = get_ipasn()
self.logger.debug('Starting import')
def sanitize(self):
ready, message = sanity_check_ipasn(self.ipasn)
if not ready:
# Try again later.
self.logger.warning(message)
return
self.logger.debug(message)
while True:
try:
if self.shutdown_requested() or not self.ipasn.is_up:
break
except requests.exceptions.ConnectionError:
# Temporary issue with ipasnhistory
self.logger.info('Temporary issue with ipasnhistory, trying again later.')
time.sleep(10)
continue
uuids: Optional[List[str]] = self.redis_intake.spop('intake', 100) # type: ignore
if not uuids:
break
for_cache = []
pipeline = self.redis_sanitized.pipeline(transaction=False)
for uuid in uuids:
data = self.redis_intake.hgetall(uuid)
if not data:
continue
try:
ip = ipaddress.ip_address(data['ip'])
if isinstance(ip, ipaddress.IPv6Address):
address_family = 'v6'
else:
address_family = 'v4'
except ValueError:
self.logger.info(f"Invalid IP address: {data['ip']}")
continue
except KeyError:
self.logger.info(f"Invalid entry {data}")
continue
if not ip.is_global:
self.logger.info(f"The IP address {data['ip']} is not global")
continue
datetime = parser.parse(data['datetime'])
if datetime.tzinfo:
# Make sure the datetime isn't TZ aware, and UTC.
datetime = datetime.astimezone(timezone.utc).replace(tzinfo=None)
for_cache.append({'ip': str(ip), 'address_family': address_family, 'source': 'caida',
'date': datetime.isoformat(), 'precision_delta': {'days': 3}})
# Add to temporay DB for further processing
pipeline.hmset(uuid, {'ip': str(ip), 'source': data['source'], 'address_family': address_family,
'date': datetime.date().isoformat(), 'datetime': datetime.isoformat()})
pipeline.sadd('to_insert', uuid)
pipeline.execute()
self.redis_intake.delete(*uuids)
try:
# Just cache everything so the lookup scripts can do their thing.
self.ipasn.mass_cache(for_cache)
except Exception:
self.logger.info('Mass cache in IPASN History failed, trying again later.')
# Rollback the spop
self.redis_intake.sadd('intake', *uuids)
break
def _to_run_forever(self):
self.sanitizer.sanitize()
self.sanitize()
def main():
sanitizer = Sanitizer()
sanitizer.run(sleep_in_sec=120)
if __name__ == '__main__':
sanitizer = SanitizerManager()
sanitizer.run(sleep_in_sec=120)
main()

View File

@ -1,16 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from bgpranking.libs.helpers import is_running, get_socket_path
import time
from redis import StrictRedis
if __name__ == '__main__':
r = StrictRedis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
r.set('shutdown', 1)
from bgpranking.default import AbstractManager
def main():
AbstractManager.force_shutdown()
time.sleep(5)
while True:
running = is_running()
print(running)
try:
running = AbstractManager.is_running()
except FileNotFoundError:
print('Redis is already down.')
break
if not running:
break
time.sleep(10)
print(running)
time.sleep(5)
if __name__ == '__main__':
main()

View File

@ -2,49 +2,202 @@
# -*- coding: utf-8 -*-
import logging
try:
import simplejson as json
except ImportError:
import json
from logging import Logger
import json
import asyncio
from pathlib import Path
import aiohttp
from bgpranking.abstractmanager import AbstractManager
from bgpranking.shadowserverfetcher import ShadowServerFetcher
from bgpranking.libs.helpers import get_config_path, get_homedir
from typing import Tuple, Dict, List, Optional, TypeVar, Any
from datetime import datetime, date
from pathlib import Path
import aiohttp
from bs4 import BeautifulSoup # type: ignore
from dateutil.parser import parse
from bgpranking.default import AbstractManager, get_homedir, safe_create_dir
from bgpranking.helpers import get_data_dir, get_modules_dir
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO, datefmt='%I:%M:%S')
Dates = TypeVar('Dates', datetime, date, str)
class ShadowServerFetcher():
def __init__(self, user, password, logger: Logger) -> None:
self.logger = logger
self.storage_directory = get_data_dir()
self.config_path_modules = get_modules_dir()
self.user = user
self.password = password
self.index_page = 'https://dl.shadowserver.org/reports/index.php'
self.vendor = 'shadowserver'
self.known_list_types = ('blacklist', 'botnet', 'cc', 'cisco', 'cwsandbox', 'drone',
'microsoft', 'scan', 'sinkhole6', 'sinkhole', 'outdated',
'compromised', 'hp', 'darknet', 'ddos')
self.first_available_day: date
self.last_available_day: date
self.available_entries: Dict[str, List[Tuple[str, str]]] = {}
async def __get_index(self):
auth_details = {'user': self.user, 'password': self.password, 'login': 'Login'}
async with aiohttp.ClientSession() as s:
self.logger.debug('Fetching the index.')
async with s.post(self.index_page, data=auth_details) as r:
return await r.text()
async def __build_daily_dict(self):
html_index = await self.__get_index()
soup = BeautifulSoup(html_index, 'html.parser')
treeview = soup.find(id='treemenu1')
for y in treeview.select(':scope > li'):
year = y.contents[0]
for m in y.contents[1].select(':scope > li'):
month = m.contents[0]
for d in m.contents[1].select(':scope > li'):
day = d.contents[0]
date = parse(f'{year} {month} {day}').date()
self.available_entries[date.isoformat()] = []
for a in d.contents[1].find_all('a', href=True):
if not self.first_available_day:
self.first_available_day = date
self.last_available_day = date
self.available_entries[date.isoformat()].append((a['href'], a.string))
self.logger.debug('Dictionary created.')
def __normalize_day(self, day: Optional[Dates]=None) -> str:
if not day:
if not self.last_available_day:
raise Exception('Unable to figure out the last available day. You need to run build_daily_dict first')
to_return = self.last_available_day
else:
if isinstance(day, str):
to_return = parse(day).date()
elif isinstance(day, datetime):
to_return = day.date()
return to_return.isoformat()
def __split_name(self, name):
type_content, country, list_type = name.split('-')
if '_' in type_content:
type_content, details_type = type_content.split('_', maxsplit=1)
if '_' in details_type:
details_type, sub = details_type.split('_', maxsplit=1)
return list_type, country, (type_content, details_type, sub)
return list_type, country, (type_content, details_type)
return list_type, country, (type_content)
def __check_config(self, filename: str) -> Optional[Path]:
self.logger.debug(f'Working on config for {filename}.')
config: Dict[str, Any] = {'vendor': 'shadowserver', 'parser': '.parsers.shadowserver'}
type_content, _, type_details = self.__split_name(filename)
prefix = type_content.split('.')[0]
if isinstance(type_details, str):
main_type = type_details
config['name'] = '{}-{}'.format(prefix, type_details)
else:
main_type = type_details[0]
config['name'] = '{}-{}'.format(prefix, '_'.join(type_details))
if main_type not in self.known_list_types:
self.logger.warning(f'Unknown type: {main_type}. Please update the config creator script.')
return None
if main_type == 'blacklist':
config['impact'] = 5
elif main_type == 'botnet':
config['impact'] = 2
elif main_type == 'cc':
config['impact'] = 5
elif main_type == 'cisco':
config['impact'] = 3
elif main_type == 'cwsandbox':
config['impact'] = 5
elif main_type == 'drone':
config['impact'] = 2
elif main_type == 'microsoft':
config['impact'] = 3
elif main_type == 'scan':
config['impact'] = 1
elif main_type == 'sinkhole6':
config['impact'] = 2
elif main_type == 'sinkhole':
config['impact'] = 2
else:
config['impact'] = 1
if not (self.config_path_modules / f"{config['vendor']}_{config['name']}.json").exists():
self.logger.debug(f'Creating config file for {filename}.')
with open(self.config_path_modules / f"{config['vendor']}_{config['name']}.json", 'w') as f:
json.dump(config, f, indent=2)
else:
with open(self.config_path_modules / f"{config['vendor']}_{config['name']}.json", 'r') as f:
# Validate new config file with old
config_current = json.load(f)
if config_current != config:
self.logger.warning('The config file created by this script is different from the one on disk: \n{}\n{}'.format(json.dumps(config), json.dumps(config_current)))
# Init list directory
directory = self.storage_directory / config['vendor'] / config['name']
safe_create_dir(directory)
meta = directory / 'meta'
safe_create_dir(meta)
archive_dir = directory / 'archive'
safe_create_dir(archive_dir)
self.logger.debug(f'Done with config for {filename}.')
return directory
async def download_daily_entries(self, day: Optional[Dates]=None):
await self.__build_daily_dict()
for url, filename in self.available_entries[self.__normalize_day(day)]:
storage_dir = self.__check_config(filename)
if not storage_dir:
continue
# Check if the file we're trying to download has already been downloaded. Skip if True.
uuid = url.split('/')[-1]
if (storage_dir / 'meta' / 'last_download').exists():
with open(storage_dir / 'meta' / 'last_download') as _fr:
last_download_uuid = _fr.read()
if last_download_uuid == uuid:
self.logger.debug(f'Already downloaded: {url}.')
continue
async with aiohttp.ClientSession() as s:
async with s.get(url) as r:
self.logger.info(f'Downloading {url}.')
content = await r.content.read()
with (storage_dir / f'{datetime.now().isoformat()}.txt').open('wb') as _fw:
_fw.write(content)
with (storage_dir / 'meta' / 'last_download').open('w') as _fwt:
_fwt.write(uuid)
class ShadowServerManager(AbstractManager):
def __init__(self, config_dir: Path=None, storage_directory: Path=None, loglevel: int=logging.INFO):
def __init__(self, loglevel: int=logging.INFO):
super().__init__(loglevel)
self.script_name = 'shadowserver_fetcher'
shadow_server_config_file = get_homedir() / 'config' / 'shadowserver.json'
self.config = True
if not config_dir:
config_dir = get_config_path()
if not (config_dir / 'shadowserver.json').exists():
if not shadow_server_config_file.exists():
self.config = False
self.logger.warning(f'No config file available, the shadow server module will not be launched.')
self.logger.warning(f'No config file available {shadow_server_config_file}, the shadow server module will not be launched.')
return
with open(config_dir / 'shadowserver.json') as f:
with shadow_server_config_file.open() as f:
ss_config = json.load(f)
if not storage_directory:
storage_directory = get_homedir() / 'rawdata'
modules_config = config_dir / 'modules'
self.fetcher = ShadowServerFetcher(ss_config['user'], ss_config['password'], modules_config, storage_directory, loglevel)
self.fetcher = ShadowServerFetcher(ss_config['user'], ss_config['password'], self.logger)
def _to_run_forever(self):
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(self.fetcher.download_daily_entries())
except aiohttp.client_exceptions.ClientConnectorError as e:
self.logger.critical(f'Exception while fetching Shadow Server lists: {e}')
async def _to_run_forever_async(self):
await self.fetcher.download_daily_entries()
def main():
modules_manager = ShadowServerManager()
if modules_manager.config:
asyncio.run(modules_manager.run_async(sleep_in_sec=3600))
if __name__ == '__main__':
modules_manager = ShadowServerManager()
if modules_manager.config:
modules_manager.run(sleep_in_sec=3600)
main()

View File

@ -1,25 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from subprocess import Popen
from bgpranking.libs.helpers import get_homedir
from subprocess import Popen, run
import redis
import sys
from bgpranking.default import get_homedir
if redis.VERSION < (3, ):
print('redis-py >= 3 is required.')
sys.exit()
if __name__ == '__main__':
def main():
# Just fail if the env isn't set.
get_homedir()
p = Popen(['run_backend.py', '--start'])
p.wait()
Popen(['fetcher.py'])
Popen(['ssfetcher.py'])
Popen(['parser.py'])
Popen(['sanitizer.py'])
Popen(['dbinsert.py'])
Popen(['ranking.py'])
Popen(['asn_descriptions.py'])
print('Start backend (redis)...')
p = run(['run_backend', '--start'])
p.check_returncode()
print('done.')
Popen(['fetcher'])
# Popen(['ssfetcher'])
Popen(['parser'])
Popen(['sanitizer'])
Popen(['dbinsert'])
Popen(['ranking'])
Popen(['asn_descriptions'])
print('Start website...')
# Popen(['start_website'])
print('done.')
if __name__ == '__main__':
main()

View File

@ -1,14 +1,40 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from subprocess import Popen
from bgpranking.libs.helpers import get_homedir
from bgpranking.default import AbstractManager
from bgpranking.default import get_config, get_homedir
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO)
class Website(AbstractManager):
def __init__(self, loglevel: int=logging.INFO):
super().__init__(loglevel)
self.script_name = 'website'
self.process = self._launch_website()
self.set_running()
def _launch_website(self):
website_dir = get_homedir() / 'website'
ip = get_config('generic', 'website_listen_ip')
port = get_config('generic', 'website_listen_port')
return Popen(['gunicorn', '-w', '10',
'--graceful-timeout', '2', '--timeout', '300',
'-b', f'{ip}:{port}',
'--log-level', 'info',
'web:app'],
cwd=website_dir)
def main():
w = Website()
w.run(sleep_in_sec=10)
if __name__ == '__main__':
website_dir = get_homedir() / 'website'
Popen([f'{website_dir}/3drparty.sh'], cwd=website_dir)
try:
Popen(['gunicorn', '--worker-class', 'gevent', '-w', '10', '-b', '0.0.0.0:5005', 'web:app'],
cwd=website_dir).communicate()
except KeyboardInterrupt:
print('Stopping gunicorn.')
main()

View File

@ -1,11 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from subprocess import Popen
from bgpranking.libs.helpers import get_homedir
from subprocess import Popen, run
from redis import Redis
from redis.exceptions import ConnectionError
from bgpranking.default import get_homedir, get_socket_path
def main():
get_homedir()
p = Popen(['shutdown'])
p.wait()
try:
r = Redis(unix_socket_path=get_socket_path('cache'), db=1)
r.delete('shutdown')
print('Shutting down databases...')
p_backend = run(['run_backend', '--stop'])
p_backend.check_returncode()
print('done.')
except ConnectionError:
# Already down, skip the stacktrace
pass
if __name__ == '__main__':
get_homedir()
p = Popen(['shutdown.py'])
p.wait()
Popen(['run_backend.py', '--stop'])
main()

113
bin/update.py Executable file
View File

@ -0,0 +1,113 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import hashlib
import logging
import platform
import shlex
import subprocess
import sys
from pathlib import Path
from bgpranking.default import get_homedir, get_config
logging.basicConfig(format='%(asctime)s %(name)s %(levelname)s:%(message)s',
level=logging.INFO)
def compute_hash_self():
m = hashlib.sha256()
with (get_homedir() / 'bin' / 'update.py').open('rb') as f:
m.update(f.read())
return m.digest()
def keep_going(ignore=False):
if ignore:
return
keep_going = input('Continue? (y/N) ')
if keep_going.lower() != 'y':
print('Okay, quitting.')
sys.exit()
def run_command(command, expect_fail: bool=False, capture_output: bool=True):
args = shlex.split(command)
homedir = get_homedir()
process = subprocess.run(args, cwd=homedir, capture_output=capture_output)
if capture_output:
print(process.stdout.decode())
if process.returncode and not expect_fail:
print(process.stderr.decode())
sys.exit()
def check_poetry_version():
args = shlex.split("poetry self -V")
homedir = get_homedir()
process = subprocess.run(args, cwd=homedir, capture_output=True)
poetry_version_str = process.stdout.decode()
version = poetry_version_str.split()[2]
version_details = tuple(int(i) for i in version.split('.'))
if version_details < (1, 1, 0):
print('The project requires poetry >= 1.1.0, please update.')
print('If you installed with "pip install --user poetry", run "pip install --user -U poetry"')
print('If you installed via the recommended method, use "poetry self update"')
print('More details: https://github.com/python-poetry/poetry#updating-poetry')
sys.exit()
def main():
parser = argparse.ArgumentParser(description='Pull latest release, update dependencies, update and validate the config files, update 3rd deps for the website.')
parser.add_argument('--yes', default=False, action='store_true', help='Run all commands without asking.')
args = parser.parse_args()
old_hash = compute_hash_self()
print('* Update repository.')
keep_going(args.yes)
run_command('git pull')
new_hash = compute_hash_self()
if old_hash != new_hash:
print('Update script changed, please do "poetry run update"')
sys.exit()
check_poetry_version()
print('* Install/update dependencies.')
keep_going(args.yes)
run_command('poetry install')
print('* Validate configuration files.')
keep_going(args.yes)
run_command(f'poetry run {(Path("tools") / "validate_config_files.py").as_posix()} --check')
print('* Update configuration files.')
keep_going(args.yes)
run_command(f'poetry run {(Path("tools") / "validate_config_files.py").as_posix()} --update')
print('* Restarting')
keep_going(args.yes)
if platform.system() == 'Windows':
print('Restarting with poetry...')
run_command('poetry run stop', expect_fail=True)
run_command('poetry run start', capture_output=False)
print('Started.')
else:
service = get_config('generic', 'systemd_service_name')
p = subprocess.run(["systemctl", "is-active", "--quiet", service])
try:
p.check_returncode()
print('Restarting with systemd...')
run_command(f'sudo service {service} restart')
print('done.')
except subprocess.CalledProcessError:
print('Restarting with poetry...')
run_command('poetry run stop', expect_fail=True)
run_command('poetry run start', capture_output=False)
print('Started.')
if __name__ == '__main__':
main()

View File

@ -1,55 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
try:
import simplejson as json
except ImportError:
import json
from urllib.parse import urljoin
from pybgpranking import BGPRanking
from pyipasnhistory import IPASNHistory
from datetime import date, timedelta
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Run a query against BGP Ranking')
parser.add_argument('--url', type=str, help='URL of the instance.')
parser.add_argument('--date', default=date.today().isoformat(), help='Date of the dataset required')
sub_parsers = parser.add_subparsers(title='Available commands')
index_query = sub_parsers.add_parser('index')
index_query.add_argument('--limit', default=100, help='Max number of ASN to get')
index_query.add_argument('--family', default='v4', help='v4 or v6')
index_query.set_defaults(which='index')
simple_query = sub_parsers.add_parser('simple')
group = simple_query.add_mutually_exclusive_group(required=True)
group.add_argument('--asn', help='ASN to lookup')
group.add_argument('--ip', help='IP to lookup')
simple_query.set_defaults(which='simple')
args = parser.parse_args()
if args.url:
bgpranking = BGPRanking(args.url)
ipasn = IPASNHistory(urljoin(args.url, 'ipasn_history'))
else:
bgpranking = BGPRanking()
ipasn = IPASNHistory()
if args.which == 'simple':
if args.ip:
response = ipasn.query(args.ip)
print(json.dumps(response, indent=2))
if 'response' in response and response['response']:
asn = response['response'][list(response['response'].keys())[0]]['asn']
else:
asn = args.asn
response = bgpranking.query(asn, date=(date.today() - timedelta(1)).isoformat())
elif args.which == 'index':
response = bgpranking.asns_global_ranking(address_family=args.family, limit=args.limit, date=args.date)
print(json.dumps(response, indent=2))

View File

@ -1 +0,0 @@
from .api import BGPRanking # noqa

View File

@ -1,49 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
try:
import simplejson as json
except ImportError:
import json
from typing import Union
import requests
from urllib.parse import urljoin
from datetime import date
class BGPRanking():
def __init__(self, root_url: str='https://bgpranking-ng.circl.lu/'):
self.root_url = root_url
if not self.root_url.endswith('/'):
self.root_url += '/'
self.session = requests.session()
@property
def is_up(self):
r = self.session.head(self.root_url)
return r.status_code == 200
def query(self, asn: str, address_family: str='v4', date: str=None,
source: Union[list, str]=''):
'''Launch a query.
:param asn: ASN to lookup
:param address_family: v4 or v6
:param date: Exact date to lookup. Fallback to most recent available.
:param source: Source to query. Can be a list of sources.
'''
to_query = {'asn': asn, 'address_family': address_family}
if date:
to_query['date'] = date
if source:
to_query['source'] = source
r = self.session.post(urljoin(self.root_url, '/json/asn'), data=json.dumps(to_query))
return r.json()
def asns_global_ranking(self, date: str=date.today().isoformat(), address_family: str='v4', limit: int=100):
'''Get the top `limit` ASNs, from worse to best'''
to_query = {'date': date, 'ipversion': address_family, 'limit': limit}
r = self.session.post(urljoin(self.root_url, '/json/asns_global_ranking'), data=json.dumps(to_query))
return r.json()

View File

@ -1,29 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup
setup(
name='pybgpranking',
version='0.1',
author='Raphaël Vinot',
author_email='raphael.vinot@circl.lu',
maintainer='Raphaël Vinot',
url='https://github.com/D4-project/BGP-Ranking/client',
description='Python client for BGP Ranking',
packages=['pybgpranking'],
scripts=['bin/bgpranking'],
install_requires=['requests'],
classifiers=[
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Operating System :: POSIX :: Linux',
'Intended Audience :: Science/Research',
'Intended Audience :: Telecommunications Industry',
'Intended Audience :: Information Technology',
'Programming Language :: Python :: 3',
'Topic :: Security',
'Topic :: Internet',
]
)

View File

@ -0,0 +1,22 @@
{
"loglevel": "INFO",
"website_listen_ip": "0.0.0.0",
"website_listen_port": 5005,
"systemd_service_name": "bgpranking",
"storage_db_hostname": "127.0.0.1",
"storage_db_port": 5188,
"ranking_db_hostname": "127.0.0.1",
"ranking_db_port": 5189,
"ipasnhistory_url": "https://ipasnhistory.circl.lu/",
"_notes": {
"loglevel": "(lookyloo) Can be one of the value listed here: https://docs.python.org/3/library/logging.html#levels",
"website_listen_ip": "IP Flask will listen on. Defaults to 0.0.0.0, meaning all interfaces.",
"website_listen_port": "Port Flask will listen on.",
"systemd_service_name": "(Optional) Name of the systemd service if your project has one.",
"storage_db_hostname": "Hostname of the storage database (kvrocks)",
"storage_db_port": "Port of the storage database (kvrocks)",
"ranking_db_hostname": "Hostname of the ranking database (kvrocks)",
"ranking_db_port": "Port of the ranking database (kvrocks)",
"ipasnhistory_url": "URL of the IP ASN History service, defaults to the public one."
}
}

1418
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

64
pyproject.toml Normal file
View File

@ -0,0 +1,64 @@
[tool.poetry]
name = "bgpranking"
version = "2.0"
description = "BGP Ranking is a software to rank AS numbers based on their malicious activities."
authors = ["Raphaël Vinot <raphael.vinot@circl.lu>"]
license = "AGPLv3"
[tool.poetry.scripts]
start = "bin.start:main"
stop = "bin.stop:main"
update = "bin.update:main"
shutdown = "bin.shutdown:main"
run_backend = "bin.run_backend:main"
start_website = "bin.start_website:main"
archiver = "bin.archiver:main"
asn_descriptions = "bin.asn_descriptions:main"
dbinsert = "bin.dbinsert:main"
fetcher = "bin.fetcher:main"
parser = "bin.parser:main"
ranking = "bin.ranking:main"
sanitizer = "bin.sanitizer:main"
ssfetcher = "bin.ssfetcher:main"
[tool.poetry.dependencies]
python = "^3.8"
redis = {version = "^4.0.2", extras = ["hiredis"]}
flask-restx = "^0.5.1"
gunicorn = "^20.1.0"
python-dateutil = "^2.8.2"
pyipasnhistory = "^2.0"
pycountry = "^20.7.3"
beautifulsoup4 = "^4.10.0"
aiohttp = "^3.8.1"
Bootstrap-Flask = "^1.8.0"
pid = "^3.0.4"
[tool.poetry.dev-dependencies]
ipython = "^7.23.0"
mypy = "^0.920"
types-setuptools = "^57.4.4"
types-redis = "^4.0.3"
types-requests = "^2.26.1"
types-python-dateutil = "^2.8.3"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]
python_version = 3.8
check_untyped_defs = true
ignore_errors = false
ignore_missing_imports = false
strict_optional = true
no_implicit_optional = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unreachable = true
show_error_context = true
pretty = true

497
ranking/kvrocks.conf Normal file
View File

@ -0,0 +1,497 @@
################################ GENERAL #####################################
# By default kvrocks listens for connections from all the network interfaces
# available on the server. It is possible to listen to just one or multiple
# interfaces using the "bind" configuration directive, followed by one or
# more IP addresses.
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1
bind 0.0.0.0
# Accept connections on the specified port, default is 6666.
port 5189
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
# The number of worker's threads, increase or decrease it would effect the performance.
workers 8
# The number of replication worker's threads, increase or decrease it would effect the replication performance.
# Default: 1
repl-workers 1
# By default kvrocks does not run as a daemon. Use 'yes' if you need it.
# Note that kvrocks will write a pid file in /var/run/kvrocks.pid when daemonized.
daemonize no
# Kvrocks implements cluster solution that is similar with redis cluster sulution.
# You can get cluster information by CLUSTER NODES|SLOTS|INFO command, it also is
# adapted to redis-cli, redis-benchmark, redis cluster SDK and redis cluster proxy.
# But kvrocks doesn't support to communicate with each others, so you must set
# cluster topology by CLUSTER SETNODES|SETNODEID commands, more details: #219.
#
# PLEASE NOTE:
# If you enable cluster, kvrocks will encode key with its slot id calculated by
# CRC16 and modulo 16384, endoding key with its slot id makes it efficient to
# migrate keys based on slot. So if you enabled at first time, cluster mode must
# not be disabled after restarting, and vice versa. That is to say, data is not
# compatible between standalone mode with cluster mode, you must migrate data
# if you want to change mode, otherwise, kvrocks will make data corrupt.
#
# Default: no
cluster-enabled no
# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
#
# Once the limit is reached the server will close all the new connections sending
# an error 'max number of clients reached'.
#
maxclients 10000
# Require clients to issue AUTH <PASSWORD> before processing any other
# commands. This might be useful in environments in which you do not trust
# others with access to the host running kvrocks.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#
# Warning: since kvrocks is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
#
# requirepass foobared
# If the master is password protected (using the "masterauth" configuration
# directive below) it is possible to tell the slave to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the slave request.
#
# masterauth foobared
# Master-Salve replication would check db name is matched. if not, the slave should
# refuse to sync the db from master. Don't use default value, set the db-name to identify
# the cluster.
db-name storage.db
# The working directory
#
# The DB will be written inside this directory
# Note that you must specify a directory here, not a file name.
dir ./
# The logs of server will be stored in this directory. If you don't specify
# one directory, by default, we store logs in the working directory that set
# by 'dir' above.
# log-dir /tmp/kvrocks
# When running daemonized, kvrocks writes a pid file in ${CONFIG_DIR}/kvrocks.pid by
# default. You can specify a custom pid file location here.
# pidfile /var/run/kvrocks.pid
pidfile storage.pid
# You can configure a slave instance to accept writes or not. Writing against
# a slave instance may be useful to store some ephemeral data (because data
# written on a slave will be easily deleted after resync with the master) but
# may also cause problems if clients are writing to it because of a
# misconfiguration.
slave-read-only yes
# The slave priority is an integer number published by Kvrocks in the INFO output.
# It is used by Redis Sentinel in order to select a slave to promote into a
# master if the master is no longer working correctly.
#
# A slave with a low priority number is considered better for promotion, so
# for instance if there are three slave with priority 10, 100, 25 Sentinel will
# pick the one with priority 10, that is the lowest.
#
# However a special priority of 0 marks the replica as not able to perform the
# role of master, so a slave with priority of 0 will never be selected by
# Redis Sentinel for promotion.
#
# By default the priority is 100.
slave-priority 100
# TCP listen() backlog.
#
# In high requests-per-second environments you need an high backlog in order
# to avoid slow clients connections issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
# in order to Get the desired effect.
tcp-backlog 511
# If the master is an old version, it may have specified replication threads
# that use 'port + 1' as listening port, but in new versions, we don't use
# extra port to implement replication. In order to allow the new replicas to
# copy old masters, you should indicate that the master uses replication port
# or not.
# If yes, that indicates master uses replication port and replicas will connect
# to 'master's listening port + 1' when synchronization.
# If no, that indicates master doesn't use replication port and replicas will
# connect 'master's listening port' when synchronization.
master-use-repl-port no
# Master-Slave replication. Use slaveof to make a kvrocks instance a copy of
# another kvrocks server. A few things to understand ASAP about kvrocks replication.
#
# 1) Kvrocks replication is asynchronous, but you can configure a master to
# stop accepting writes if it appears to be not connected with at least
# a given number of slaves.
# 2) Kvrocks slaves are able to perform a partial resynchronization with the
# master if the replication link is lost for a relatively small amount of
# time. You may want to configure the replication backlog size (see the next
# sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
# network partition slaves automatically try to reconnect to masters
# and resynchronize with them.
#
# slaveof <masterip> <masterport>
# slaveof 127.0.0.1 6379
# When a slave loses its connection with the master, or when the replication
# is still in progress, the slave can act in two different ways:
#
# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will
# still reply to client requests, possibly with out of date data, or the
# data set may just be empty if this is the first synchronization.
#
# 2) if slave-serve-stale-data is set to 'no' the slave will reply with
# an error "SYNC with master in progress" to all the kind of commands
# but to INFO and SLAVEOF.
#
slave-serve-stale-data yes
# To guarantee slave's data safe and serve when it is in full synchronization
# state, slave still keep itself data. But this way needs to occupy much disk
# space, so we provide a way to reduce disk occupation, slave will delete itself
# entire database before fetching files from master during full synchronization.
# If you want to enable this way, you can set 'slave-delete-db-before-fullsync'
# to yes, but you must know that database will be lost if master is down during
# full synchronization, unless you have a backup of database.
#
# This option is similar redis replicas RDB diskless load option:
# repl-diskless-load on-empty-db
#
# Default: no
slave-empty-db-before-fullsync no
# If replicas need full synchronization with master, master need to create
# checkpoint for feeding replicas, and replicas also stage a checkpoint of
# the master. If we also keep the backup, it maybe occupy extra disk space.
# You can enable 'purge-backup-on-fullsync' if disk is not sufficient, but
# that may cause remote backup copy failing.
#
# Default: no
purge-backup-on-fullsync no
# The maximum allowed rate (in MB/s) that should be used by Replication.
# If the rate exceeds max-replication-mb, replication will slow down.
# Default: 0 (i.e. no limit)
max-replication-mb 0
# The maximum allowed aggregated write rate of flush and compaction (in MB/s).
# If the rate exceeds max-io-mb, io will slow down.
# 0 is no limit
# Default: 500
max-io-mb 500
# The maximum allowed space (in GB) that should be used by RocksDB.
# If the total size of the SST files exceeds max_allowed_space, writes to RocksDB will fail.
# Please see: https://github.com/facebook/rocksdb/wiki/Managing-Disk-Space-Utilization
# Default: 0 (i.e. no limit)
max-db-size 0
# The maximum backup to keep, server cron would run every minutes to check the num of current
# backup, and purge the old backup if exceed the max backup num to keep. If max-backup-to-keep
# is 0, no backup would be keep. But now, we only support 0 or 1.
max-backup-to-keep 1
# The maximum hours to keep the backup. If max-backup-keep-hours is 0, wouldn't purge any backup.
# default: 1 day
max-backup-keep-hours 24
# Ratio of the samples would be recorded when the profiling was enabled.
# we simply use the rand to determine whether to record the sample or not.
#
# Default: 0
profiling-sample-ratio 0
# There is no limit to this length. Just be aware that it will consume memory.
# You can reclaim memory used by the perf log with PERFLOG RESET.
#
# Default: 256
profiling-sample-record-max-len 256
# profiling-sample-record-threshold-ms use to tell the kvrocks when to record.
#
# Default: 100 millisecond
profiling-sample-record-threshold-ms 100
################################## SLOW LOG ###################################
# The Kvrocks Slow Log is a system to log queries that exceeded a specified
# execution time. The execution time does not include the I/O operations
# like talking with the client, sending the reply and so forth,
# but just the time needed to actually execute the command (this is the only
# stage of command execution where the thread is blocked and can not serve
# other requests in the meantime).
#
# You can configure the slow log with two parameters: one tells Kvrocks
# what is the execution time, in microseconds, to exceed in order for the
# command to get logged, and the other parameter is the length of the
# slow log. When a new command is logged the oldest one is removed from the
# queue of logged commands.
# The following time is expressed in microseconds, so 1000000 is equivalent
# to one second. Note that -1 value disables the slow log, while
# a value of zero forces the logging of every command.
slowlog-log-slower-than 100000
# There is no limit to this length. Just be aware that it will consume memory.
# You can reclaim memory used by the slow log with SLOWLOG RESET.
slowlog-max-len 128
# If you run kvrocks from upstart or systemd, kvrocks can interact with your
# supervision tree. Options:
# supervised no - no supervision interaction
# supervised upstart - signal upstart by putting kvrocks into SIGSTOP mode
# supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
# supervised auto - detect upstart or systemd method based on
# UPSTART_JOB or NOTIFY_SOCKET environment variables
# Note: these supervision methods only signal "process is ready."
# They do not enable continuous liveness pings back to your supervisor.
supervised no
################################## CRON ###################################
# Compact Scheduler, auto compact at schedule time
# time expression format is the same as crontab(currently only support * and int)
# e.g. compact-cron 0 3 * * * 0 4 * * *
# would compact the db at 3am and 4am everyday
# compact-cron 0 3 * * *
# The hour range that compaction checker would be active
# e.g. compaction-checker-range 0-7 means compaction checker would be worker between
# 0-7am every day.
compaction-checker-range 0-7
# Bgsave scheduler, auto bgsave at schedule time
# time expression format is the same as crontab(currently only support * and int)
# e.g. bgsave-cron 0 3 * * * 0 4 * * *
# would bgsave the db at 3am and 4am everyday
# Command renaming.
#
# It is possible to change the name of dangerous commands in a shared
# environment. For instance the KEYS command may be renamed into something
# hard to guess so that it will still be available for internal-use tools
# but not available for general clients.
#
# Example:
#
# rename-command KEYS b840fc02d524045429941cc15f59e41cb7be6c52
#
# It is also possible to completely kill a command by renaming it into
# an empty string:
#
# rename-command KEYS ""
# The key-value size may so be quite different in many scenes, and use 256MiB as SST file size
# may cause data loading(large index/filter block) ineffective when the key-value was too small.
# kvrocks supports user-defined SST file in config(rocksdb.target_file_size_base),
# but it still too trivial and inconvenient to adjust the different sizes for different instances.
# so we want to periodic auto-adjust the SST size in-flight with user avg key-value size.
#
# If enabled, kvrocks will auto resize rocksdb.target_file_size_base
# and rocksdb.write_buffer_size in-flight with user avg key-value size.
# Please see #118.
#
# Default: yes
auto-resize-block-and-sst yes
################################ ROCKSDB #####################################
# Specify the capacity of metadata column family block cache. Larger block cache
# may make request faster while more keys would be cached. Max Size is 200*1024.
# Default: 2048MB
rocksdb.metadata_block_cache_size 2048
# Specify the capacity of subkey column family block cache. Larger block cache
# may make request faster while more keys would be cached. Max Size is 200*1024.
# Default: 2048MB
rocksdb.subkey_block_cache_size 2048
# Metadata column family and subkey column family will share a single block cache
# if set 'yes'. The capacity of shared block cache is
# metadata_block_cache_size + subkey_block_cache_size
#
# Default: yes
rocksdb.share_metadata_and_subkey_block_cache yes
# Number of open files that can be used by the DB. You may need to
# increase this if your database has a large working set. Value -1 means
# files opened are always kept open. You can estimate number of files based
# on target_file_size_base and target_file_size_multiplier for level-based
# compaction. For universal-style compaction, you can usually set it to -1.
# Default: 4096
rocksdb.max_open_files 8096
# Amount of data to build up in memory (backed by an unsorted log
# on disk) before converting to a sorted on-disk file.
#
# Larger values increase performance, especially during bulk loads.
# Up to max_write_buffer_number write buffers may be held in memory
# at the same time,
# so you may wish to adjust this parameter to control memory usage.
# Also, a larger write buffer will result in a longer recovery time
# the next time the database is opened.
#
# Note that write_buffer_size is enforced per column family.
# See db_write_buffer_size for sharing memory across column families.
# default is 64MB
rocksdb.write_buffer_size 16
# Target file size for compaction, target file size for Leve N can be caculated
# by target_file_size_base * (target_file_size_multiplier ^ (L-1))
#
# Default: 128MB
rocksdb.target_file_size_base 16
# The maximum number of write buffers that are built up in memory.
# The default and the minimum number is 2, so that when 1 write buffer
# is being flushed to storage, new writes can continue to the other
# write buffer.
# If max_write_buffer_number > 3, writing will be slowed down to
# options.delayed_write_rate if we are writing to the last write buffer
# allowed.
rocksdb.max_write_buffer_number 4
# Maximum number of concurrent background compaction jobs, submitted to
# the default LOW priority thread pool.
rocksdb.max_background_compactions 4
# Maximum number of concurrent background memtable flush jobs, submitted by
# default to the HIGH priority thread pool. If the HIGH priority thread pool
# is configured to have zero threads, flush jobs will share the LOW priority
# thread pool with compaction jobs.
rocksdb.max_background_flushes 4
# This value represents the maximum number of threads that will
# concurrently perform a compaction job by breaking it into multiple,
# smaller ones that are run simultaneously.
# Default: 2 (i.e. no subcompactions)
rocksdb.max_sub_compactions 2
# In order to limit the size of WALs, RocksDB uses DBOptions::max_total_wal_size
# as the trigger of column family flush. Once WALs exceed this size, RocksDB
# will start forcing the flush of column families to allow deletion of some
# oldest WALs. This config can be useful when column families are updated at
# non-uniform frequencies. If there's no size limit, users may need to keep
# really old WALs when the infrequently-updated column families hasn't flushed
# for a while.
#
# In kvrocks, we use multiple column families to store metadata, subkeys, etc.
# If users always use string type, but use list, hash and other complex data types
# infrequently, there will be a lot of old WALs if we don't set size limit
# (0 by default in rocksdb), because rocksdb will dynamically choose the WAL size
# limit to be [sum of all write_buffer_size * max_write_buffer_number] * 4 if set to 0.
#
# Moreover, you should increase this value if you already set rocksdb.write_buffer_size
# to a big value, to avoid influencing the effect of rocksdb.write_buffer_size and
# rocksdb.max_write_buffer_number.
#
# default is 512MB
rocksdb.max_total_wal_size 512
# We impl the repliction with rocksdb WAL, it would trigger full sync when the seq was out of range.
# wal_ttl_seconds and wal_size_limit_mb would affect how archived logswill be deleted.
# If WAL_ttl_seconds is not 0, then WAL files will be checked every WAL_ttl_seconds / 2 and those that
# are older than WAL_ttl_seconds will be deleted#
#
# Default: 3 Hours
rocksdb.wal_ttl_seconds 10800
# If WAL_ttl_seconds is 0 and WAL_size_limit_MB is not 0,
# WAL files will be checked every 10 min and if total size is greater
# then WAL_size_limit_MB, they will be deleted starting with the
# earliest until size_limit is met. All empty files will be deleted
# Default: 16GB
rocksdb.wal_size_limit_mb 16384
# Approximate size of user data packed per block. Note that the
# block size specified here corresponds to uncompressed data. The
# actual size of the unit read from disk may be smaller if
# compression is enabled.
#
# Default: 4KB
rocksdb.block_size 2048
# Indicating if we'd put index/filter blocks to the block cache
#
# Default: no
rocksdb.cache_index_and_filter_blocks yes
# Specify the compression to use.
# Accept value: "no", "snappy"
# default snappy
rocksdb.compression snappy
# If non-zero, we perform bigger reads when doing compaction. If you're
# running RocksDB on spinning disks, you should set this to at least 2MB.
# That way RocksDB's compaction is doing sequential instead of random reads.
# When non-zero, we also force new_table_reader_for_compaction_inputs to
# true.
#
# Default: 2 MB
rocksdb.compaction_readahead_size 2097152
# he limited write rate to DB if soft_pending_compaction_bytes_limit or
# level0_slowdown_writes_trigger is triggered.
# If the value is 0, we will infer a value from `rater_limiter` value
# if it is not empty, or 16MB if `rater_limiter` is empty. Note that
# if users change the rate in `rate_limiter` after DB is opened,
# `delayed_write_rate` won't be adjusted.
#
rocksdb.delayed_write_rate 0
# If enable_pipelined_write is true, separate write thread queue is
# maintained for WAL write and memtable write.
#
# Default: no
rocksdb.enable_pipelined_write no
# Soft limit on number of level-0 files. We start slowing down writes at this
# point. A value <0 means that no writing slow down will be triggered by
# number of files in level-0.
#
# Default: 20
rocksdb.level0_slowdown_writes_trigger 20
# Maximum number of level-0 files. We stop writes at this point.
#
# Default: 40
rocksdb.level0_stop_writes_trigger 40
# if not zero, dump rocksdb.stats to LOG every stats_dump_period_sec
#
# Default: 0
rocksdb.stats_dump_period_sec 0
# if yes, the auto compaction would be disabled, but the manual compaction remain works
#
# Default: no
rocksdb.disable_auto_compactions no
################################ NAMESPACE #####################################
# namespace.test change.me
backup-dir .//backup
log-dir ./

6
ranking/run_kvrocks.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
set -x
../../kvrocks/src/kvrocks -c kvrocks.conf

View File

@ -1,39 +0,0 @@
-i https://pypi.org/simple
-e .
-e ./client
-e git+https://github.com/D4-project/IPASN-History.git/@283539cfbbde4bb54497726634407025f7d685c2#egg=pyipasnhistory&subdirectory=client
-e git+https://github.com/MISP/PyMISPGalaxies.git@a59a5c18024aedda0c1306c917e09bdb8596ef48#egg=pymispgalaxies
-e git+https://github.com/MISP/PyTaxonomies.git@f28fc11bd682aba35778efbcd8a65e68d1225a3f#egg=pytaxonomies
-e git+https://github.com/trbs/pid.git/@240d6e848fcb0ebbf80c88b9d724dcb85978a019#egg=pid
aiohttp==3.5.4
async-timeout==3.0.1
attrs==19.1.0
beautifulsoup4==4.8.0
certifi==2019.6.16
chardet==3.0.4
click==7.0
dominate==2.4.0
flask-bootstrap==3.3.7.1
flask==1.1.1
gevent==1.4.0
greenlet==0.4.15 ; platform_python_implementation == 'CPython'
gunicorn[gevent]==19.9.0
hiredis==1.0.0
idna-ssl==1.1.0 ; python_version < '3.7'
idna==2.8
itsdangerous==1.1.0
jinja2==2.10.1
markupsafe==1.1.1
multidict==4.5.2
pycountry==19.8.18
python-dateutil==2.8.0
redis==3.3.8
requests==2.22.0
simplejson==3.16.0
six==1.12.0
soupsieve==1.9.3
typing-extensions==3.7.4 ; python_version < '3.7'
urllib3==1.25.3
visitor==0.1.3
werkzeug==0.15.5
yarl==1.3.0

View File

@ -1,468 +0,0 @@
# Ardb configuration file example, modified from redis's conf file.
# Home dir for ardb instance, it can be referenced by ${ARDB_HOME} in this config file
home .
# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
# By default Ardb does not run as a daemon. Use 'yes' if you need it.
daemonize yes
# When running daemonized, Ardb writes a pid file in ${ARDB_HOME}/ardb.pid by
# default. You can specify a custom pid file location here.
pidfile ${ARDB_HOME}/ardb.pid
# The thread pool size for the corresponding all listen servers, -1 means current machine's cpu number
thread-pool-size -1
#Accept connections on the specified host&port/unix socket, default is 0.0.0.0:16379.
#server[0].listen 0.0.0.0:16579
# If current qps exceed the limit, Ardb would return an error.
#server[0].qps-limit 1000
#listen on unix socket
server[0].listen storage.sock
server[0].unixsocketperm 755
#server[1].qps-limit 1000
# 'qps-limit-per-host' used to limit the request per second from same host
# 'qps-limit-per-connection' used to limit the request per second from same connection
qps-limit-per-host 0
qps-limit-per-connection 0
# Specify the optimized RocksDB compaction strategies.
# If anything other than none is set then the rocksdb.options will not be used.
# The property can one of:
# OptimizeLevelStyleCompaction
# OptimizeUniversalStyleCompaction
# none
#
rocksdb.compaction OptimizeLevelStyleCompaction
# Enable this to indicate that hsca/sscan/zscan command use total order mode for rocksdb engine
rocksdb.scan-total-order false
# Disable RocksDB WAL may improve the write performance but
# data in the un-flushed memtables might be lost in case of a RocksDB shutdown.
# Disabling WAL provides similar guarantees as Redis.
rocksdb.disableWAL false
#rocksdb's options
rocksdb.options write_buffer_size=1024M;max_write_buffer_number=5;min_write_buffer_number_to_merge=3;compression=kSnappyCompression;\
bloom_locality=1;memtable_prefix_bloom_size_ratio=0.1;\
block_based_table_factory={block_cache=512M;filter_policy=bloomfilter:10:true};\
create_if_missing=true;max_open_files=10000;rate_limiter_bytes_per_sec=50M;\
use_direct_io_for_flush_and_compaction=true;use_adaptive_mutex=true
#leveldb's options
leveldb.options block_cache_size=512M,write_buffer_size=128M,max_open_files=5000,block_size=4k,block_restart_interval=16,\
bloom_bits=10,compression=snappy,logenable=yes,max_file_size=2M
#lmdb's options
lmdb.options database_maxsize=10G,database_maxdbs=4096,readahead=no,batch_commit_watermark=1024
#perconaft's options
perconaft.options cache_size=128M,compression=snappy
#wiredtiger's options
wiredtiger.options cache_size=512M,session_max=8k,chunk_size=100M,block_size=4k,bloom_bits=10,\
mmap=false,compressor=snappy
#forestdb's options
forestdb.options chunksize=8,blocksize=4K
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
# TCP keepalive.
#
# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
# of communication. This is useful for two reasons:
#
# 1) Detect dead peers.
# 2) Take the connection alive from the point of view of network
# equipment in the middle.
#
# On Linux, the specified value (in seconds) is the period used to send ACKs.
# Note that to close the connection the double of the time is needed.
# On other kernels the period depends on the kernel configuration.
#
# A reasonable value for this option is 60 seconds.
tcp-keepalive 0
# Specify the server verbosity level.
# This can be one of:
# error
# warn
# info
# debug
# trace
loglevel info
# Specify the log file name. Also 'stdout' can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
#logfile ${ARDB_HOME}/log/ardb-server.log
logfile stdout
# The working data directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
data-dir ${ARDB_HOME}/data
################################# REPLICATION #################################
# Master-Slave replication. Use slaveof to make a Ardb instance a copy of
# another Ardb server. Note that the configuration is local to the slave
# so for example it is possible to configure the slave to save the DB with a
# different interval, or to listen to another port, and so on.
#
# slaveof <masterip>:<masterport>
#slaveof 127.0.0.1:6379
# By default, ardb use 2 threads to execute commands synced from master.
# -1 means use current CPU number threads instead.
slave-workers 2
# Max synced command queue size in memory.
max-slave-worker-queue 1024
# The directory for replication.
repl-dir ${ARDB_HOME}/repl
# When a slave loses its connection with the master, or when the replication
# is still in progress, the slave can act in two different ways:
#
# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will
# still reply to client requests, possibly with out of date data, or the
# data set may just be empty if this is the first synchronization.
#
# 2) if slave-serve-stale-data is set to 'no' the slave will reply with
# an error "SYNC with master in progress" to all the kind of commands
# but to INFO and SLAVEOF.
#
slave-serve-stale-data yes
# The slave priority is an integer number published by Ardb/Redis in the INFO output.
# It is used by Redis Sentinel in order to select a slave to promote into a
# master if the master is no longer working correctly.
#
# A slave with a low priority number is considered better for promotion, so
# for instance if there are three slaves with priority 10, 100, 25 Sentinel will
# pick the one with priority 10, that is the lowest.
#
# However a special priority of 0 marks the slave as not able to perform the
# role of master, so a slave with priority of 0 will never be selected by
# Redis Sentinel for promotion.
#
# By default the priority is 100.
slave-priority 100
# You can configure a slave instance to accept writes or not. Writing against
# a slave instance may be useful to store some ephemeral data (because data
# written on a slave will be easily deleted after resync with the master) but
# may also cause problems if clients are writing to it because of a
# misconfiguration.
#
# Note: read only slaves are not designed to be exposed to untrusted clients
# on the internet. It's just a protection layer against misuse of the instance.
# Still a read only slave exports by default all the administrative commands
# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve
# security of read only slaves using 'rename-command' to shadow all the
# administrative / dangerous commands.
#
# Note: any requests processed by non read only slaves would no write to replication
# log and sync to connected slaves.
slave-read-only yes
# The directory for backup.
backup-dir ${ARDB_HOME}/backup
#
# You can configure the backup file format as 'redis' or 'ardb'. The 'ardb' format
# can only used by ardb instance, while 'redis' format file can be used by redis
# and ardb instance.
backup-file-format ardb
# Slaves send PINGs to server in a predefined interval. It's possible to change
# this interval with the repl_ping_slave_period option. The default value is 10
# seconds.
#
# repl-ping-slave-period 10
# The following option sets a timeout for both Bulk transfer I/O timeout and
# master data or ping response timeout. The default value is 60 seconds.
#
# It is important to make sure that this value is greater than the value
# specified for repl-ping-slave-period otherwise a timeout will be detected
# every time there is low traffic between the master and the slave.
#
# repl-timeout 60
# Disable TCP_NODELAY on the slave socket after SYNC?
#
# If you select "yes" Ardb will use a smaller number of TCP packets and
# less bandwidth to send data to slaves. But this can add a delay for
# the data to appear on the slave side, up to 40 milliseconds with
# Linux kernels using a default configuration.
#
# If you select "no" the delay for data to appear on the slave side will
# be reduced but more bandwidth will be used for replication.
#
# By default we optimize for low latency, but in very high traffic conditions
# or when the master and slaves are many hops away, turning this to "yes" may
# be a good idea.
repl-disable-tcp-nodelay no
# Set the replication backlog size. The backlog is a buffer that accumulates
# slave data when slaves are disconnected for some time, so that when a slave
# wants to reconnect again, often a full resync is not needed, but a partial
# resync is enough, just passing the portion of data the slave missed while
# disconnected.
#
# The biggest the replication backlog, the longer the time the slave can be
# disconnected and later be able to perform a partial resynchronization.
#
# If the size is configured by 0, then Ardb instance can NOT serve as a master.
#
# repl-backlog-size 500m
repl-backlog-size 1G
repl-backlog-cache-size 100M
snapshot-max-lag-offset 500M
# Set the max number of snapshots. By default this limit is set to 10 snapshot.
# Once the limit is reached Ardb would try to remove the oldest snapshots
maxsnapshots 10
# It is possible for a master to stop accepting writes if there are less than
# N slaves connected, having a lag less or equal than M seconds.
#
# The N slaves need to be in "online" state.
#
# The lag in seconds, that must be <= the specified value, is calculated from
# the last ping received from the slave, that is usually sent every second.
#
# This option does not GUARANTEE that N replicas will accept the write, but
# will limit the window of exposure for lost writes in case not enough slaves
# are available, to the specified number of seconds.
#
# For example to require at least 3 slaves with a lag <= 10 seconds use:
#
# min-slaves-to-write 3
# min-slaves-max-lag 10
# When a slave loses its connection with the master, or when the replication
# is still in progress, the slave can act in two different ways:
#
# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will
# still reply to client requests, possibly with out of date data, or the
# data set may just be empty if this is the first synchronization.
#
# 2) if slave-serve-stale-data is set to 'no' the slave will reply with
# an error "SYNC with master in progress" to all the kind of commands
# but to INFO and SLAVEOF.
#
slave-serve-stale-data yes
# After a master has no longer connected slaves for some time, the backlog
# will be freed. The following option configures the amount of seconds that
# need to elapse, starting from the time the last slave disconnected, for
# the backlog buffer to be freed.
#
# A value of 0 means to never release the backlog.
#
# repl-backlog-ttl 3600
# Slave clear current data store before full resync to master.
# It make sure that slave keep consistent with master's data. But slave may cost a
# long time to delete data, it depends on
# If set by no, then slave may have different data with master.
slave-cleardb-before-fullresync yes
# Master/Slave instance would persist sync state every 'repl-backlog-sync-period' secs.
repl-backlog-sync-period 5
# Slave would ignore any 'expire' setting from replication command if set by 'yes'.
# It could be used if master is redis instance serve hot data with expire setting, slave is
# ardb instance which persist all data.
# Since master redis instance would generate a 'del' for each expired key, slave should ignore
# all 'del' command too by setting 'slave-ignore-del' to 'yes' for this scenario.
slave-ignore-expire no
slave-ignore-del no
# After a master has no longer connected slaves for some time, the backlog
# will be freed. The following option configures the amount of seconds that
# need to elapse, starting from the time the last slave disconnected, for
# the backlog buffer to be freed.
#
# A value of 0 means to never release the backlog.
#
# repl-backlog-ttl 3600
################################## SECURITY ###################################
# Require clients to issue AUTH <PASSWORD> before processing any other
# commands. This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#
# Warning: since Redis is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
#
# requirepass foobared
# Command renaming.
#
# It is possible to change the name of dangerous commands in a shared
# environment. For instance the CONFIG command may be renamed into something
# hard to guess so that it will still be available for internal-use tools
# but not available for general clients.
#
# Example:
#
# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
#
# It is also possible to completely kill a command by renaming it into
# an empty string:
#
# rename-command CONFIG ""
#
# Please note that changing the name of commands that are logged into the
# AOF file or transmitted to slaves may cause problems.
################################ CLUSTER ###############################
# Max execution time of a Lua script in milliseconds.
#zookeeper-servers 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
#zk-recv-timeout 10000
#zk-clientid-file ${ARDB_HOME}/ardb.zkclientid
cluster-name ardb-cluster
################################### LIMITS ####################################
# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# maxclients 10000
# The client output buffer limits can be used to force disconnection of clients
# that are not reading data from the server fast enough for some reason (a
# common reason is that a Pub/Sub/Slave client can't consume messages as fast as the
# publisher can produce them).
slave-client-output-buffer-limit 256mb
pubsub-client-output-buffer-limit 32mb
################################## SLOW LOG ###################################
# The Redis Slow Log is a system to log queries that exceeded a specified
# execution time. The execution time does not include the I/O operations
# like talking with the client, sending the reply and so forth,
# but just the time needed to actually execute the command (this is the only
# stage of command execution where the thread is blocked and can not serve
# other requests in the meantime).
#
# You can configure the slow log with two parameters: one tells Redis
# what is the execution time, in microseconds, to exceed in order for the
# command to get logged, and the other parameter is the length of the
# slow log. When a new command is logged the oldest one is removed from the
# queue of logged commands.
# The following time is expressed in microseconds, so 1000000 is equivalent
# to one second. Note that a negative number disables the slow log, while
# a value of zero forces the logging of every command.
slowlog-log-slower-than 10000
# There is no limit to this length. Just be aware that it will consume memory.
# You can reclaim memory used by the slow log with SLOWLOG RESET.
slowlog-max-len 128
################################ LUA SCRIPTING ###############################
# Max execution time of a Lua script in milliseconds.
#
# If the maximum execution time is reached Redis will log that a script is
# still in execution after the maximum allowed time and will start to
# reply to queries with an error.
#
# When a long running script exceed the maximum execution time only the
# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be
# used to stop a script that did not yet called write commands. The second
# is the only way to shut down the server in the case a write commands was
# already issue by the script but the user don't want to wait for the natural
# termination of the script.
#
# Set it to 0 or a negative value for unlimited execution without warnings.
lua-time-limit 5000
############################### ADVANCED CONFIG ###############################
## Since some redis clients would check info command's output, this configuration
## would be set in 'misc' section of 'info's output
#additional-misc-info redis_version:2.8.9\nredis_trick:yes
# HyperLogLog sparse representation bytes limit. The limit includes the
# 16 bytes header. When an HyperLogLog using the sparse representation crosses
# this limit, it is convereted into the dense representation.
#
# A value greater than 16000 is totally useless, since at that point the
# dense representation is more memory efficient.
#
# The suggested value is ~ 3000 in order to have the benefits of
# the space efficient encoding without slowing down too much PFADD,
# which is O(N) with the sparse encoding. Thev value can be raised to
# ~ 10000 when CPU is not a concern, but space is, and the data set is
# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.
hll-sparse-max-bytes 3000
#trusted-ip 10.10.10.10
#trusted-ip 10.10.10.*
# By default Ardb would not compact whole db after loading a snapshot, which may happens
# when slave syncing from master, processing 'import' command from client.
# This configuration only works with rocksdb engine.
# If ardb dord not compact data after loading snapshot file, there would be poor read performance before rocksdb
# completes the next compaction task internally. While the compaction task would cost very long time for a huge data set.
compact-after-snapshot-load false
# Ardb would store cursor in memory
scan-redis-compatible yes
scan-cursor-expire-after 60
redis-compatible-mode yes
redis-compatible-version 2.8.0
statistics-log-period 600
# Range deletion min size trigger
range-delete-min-size 100

497
storage/kvrocks.conf Normal file
View File

@ -0,0 +1,497 @@
################################ GENERAL #####################################
# By default kvrocks listens for connections from all the network interfaces
# available on the server. It is possible to listen to just one or multiple
# interfaces using the "bind" configuration directive, followed by one or
# more IP addresses.
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1
bind 0.0.0.0
# Accept connections on the specified port, default is 6666.
port 5188
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
# The number of worker's threads, increase or decrease it would effect the performance.
workers 8
# The number of replication worker's threads, increase or decrease it would effect the replication performance.
# Default: 1
repl-workers 1
# By default kvrocks does not run as a daemon. Use 'yes' if you need it.
# Note that kvrocks will write a pid file in /var/run/kvrocks.pid when daemonized.
daemonize no
# Kvrocks implements cluster solution that is similar with redis cluster sulution.
# You can get cluster information by CLUSTER NODES|SLOTS|INFO command, it also is
# adapted to redis-cli, redis-benchmark, redis cluster SDK and redis cluster proxy.
# But kvrocks doesn't support to communicate with each others, so you must set
# cluster topology by CLUSTER SETNODES|SETNODEID commands, more details: #219.
#
# PLEASE NOTE:
# If you enable cluster, kvrocks will encode key with its slot id calculated by
# CRC16 and modulo 16384, endoding key with its slot id makes it efficient to
# migrate keys based on slot. So if you enabled at first time, cluster mode must
# not be disabled after restarting, and vice versa. That is to say, data is not
# compatible between standalone mode with cluster mode, you must migrate data
# if you want to change mode, otherwise, kvrocks will make data corrupt.
#
# Default: no
cluster-enabled no
# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
#
# Once the limit is reached the server will close all the new connections sending
# an error 'max number of clients reached'.
#
maxclients 10000
# Require clients to issue AUTH <PASSWORD> before processing any other
# commands. This might be useful in environments in which you do not trust
# others with access to the host running kvrocks.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#
# Warning: since kvrocks is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
#
# requirepass foobared
# If the master is password protected (using the "masterauth" configuration
# directive below) it is possible to tell the slave to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the slave request.
#
# masterauth foobared
# Master-Salve replication would check db name is matched. if not, the slave should
# refuse to sync the db from master. Don't use default value, set the db-name to identify
# the cluster.
db-name storage.db
# The working directory
#
# The DB will be written inside this directory
# Note that you must specify a directory here, not a file name.
dir ./
# The logs of server will be stored in this directory. If you don't specify
# one directory, by default, we store logs in the working directory that set
# by 'dir' above.
# log-dir /tmp/kvrocks
# When running daemonized, kvrocks writes a pid file in ${CONFIG_DIR}/kvrocks.pid by
# default. You can specify a custom pid file location here.
# pidfile /var/run/kvrocks.pid
pidfile storage.pid
# You can configure a slave instance to accept writes or not. Writing against
# a slave instance may be useful to store some ephemeral data (because data
# written on a slave will be easily deleted after resync with the master) but
# may also cause problems if clients are writing to it because of a
# misconfiguration.
slave-read-only yes
# The slave priority is an integer number published by Kvrocks in the INFO output.
# It is used by Redis Sentinel in order to select a slave to promote into a
# master if the master is no longer working correctly.
#
# A slave with a low priority number is considered better for promotion, so
# for instance if there are three slave with priority 10, 100, 25 Sentinel will
# pick the one with priority 10, that is the lowest.
#
# However a special priority of 0 marks the replica as not able to perform the
# role of master, so a slave with priority of 0 will never be selected by
# Redis Sentinel for promotion.
#
# By default the priority is 100.
slave-priority 100
# TCP listen() backlog.
#
# In high requests-per-second environments you need an high backlog in order
# to avoid slow clients connections issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
# in order to Get the desired effect.
tcp-backlog 511
# If the master is an old version, it may have specified replication threads
# that use 'port + 1' as listening port, but in new versions, we don't use
# extra port to implement replication. In order to allow the new replicas to
# copy old masters, you should indicate that the master uses replication port
# or not.
# If yes, that indicates master uses replication port and replicas will connect
# to 'master's listening port + 1' when synchronization.
# If no, that indicates master doesn't use replication port and replicas will
# connect 'master's listening port' when synchronization.
master-use-repl-port no
# Master-Slave replication. Use slaveof to make a kvrocks instance a copy of
# another kvrocks server. A few things to understand ASAP about kvrocks replication.
#
# 1) Kvrocks replication is asynchronous, but you can configure a master to
# stop accepting writes if it appears to be not connected with at least
# a given number of slaves.
# 2) Kvrocks slaves are able to perform a partial resynchronization with the
# master if the replication link is lost for a relatively small amount of
# time. You may want to configure the replication backlog size (see the next
# sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
# network partition slaves automatically try to reconnect to masters
# and resynchronize with them.
#
# slaveof <masterip> <masterport>
# slaveof 127.0.0.1 6379
# When a slave loses its connection with the master, or when the replication
# is still in progress, the slave can act in two different ways:
#
# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will
# still reply to client requests, possibly with out of date data, or the
# data set may just be empty if this is the first synchronization.
#
# 2) if slave-serve-stale-data is set to 'no' the slave will reply with
# an error "SYNC with master in progress" to all the kind of commands
# but to INFO and SLAVEOF.
#
slave-serve-stale-data yes
# To guarantee slave's data safe and serve when it is in full synchronization
# state, slave still keep itself data. But this way needs to occupy much disk
# space, so we provide a way to reduce disk occupation, slave will delete itself
# entire database before fetching files from master during full synchronization.
# If you want to enable this way, you can set 'slave-delete-db-before-fullsync'
# to yes, but you must know that database will be lost if master is down during
# full synchronization, unless you have a backup of database.
#
# This option is similar redis replicas RDB diskless load option:
# repl-diskless-load on-empty-db
#
# Default: no
slave-empty-db-before-fullsync no
# If replicas need full synchronization with master, master need to create
# checkpoint for feeding replicas, and replicas also stage a checkpoint of
# the master. If we also keep the backup, it maybe occupy extra disk space.
# You can enable 'purge-backup-on-fullsync' if disk is not sufficient, but
# that may cause remote backup copy failing.
#
# Default: no
purge-backup-on-fullsync no
# The maximum allowed rate (in MB/s) that should be used by Replication.
# If the rate exceeds max-replication-mb, replication will slow down.
# Default: 0 (i.e. no limit)
max-replication-mb 0
# The maximum allowed aggregated write rate of flush and compaction (in MB/s).
# If the rate exceeds max-io-mb, io will slow down.
# 0 is no limit
# Default: 500
max-io-mb 500
# The maximum allowed space (in GB) that should be used by RocksDB.
# If the total size of the SST files exceeds max_allowed_space, writes to RocksDB will fail.
# Please see: https://github.com/facebook/rocksdb/wiki/Managing-Disk-Space-Utilization
# Default: 0 (i.e. no limit)
max-db-size 0
# The maximum backup to keep, server cron would run every minutes to check the num of current
# backup, and purge the old backup if exceed the max backup num to keep. If max-backup-to-keep
# is 0, no backup would be keep. But now, we only support 0 or 1.
max-backup-to-keep 1
# The maximum hours to keep the backup. If max-backup-keep-hours is 0, wouldn't purge any backup.
# default: 1 day
max-backup-keep-hours 24
# Ratio of the samples would be recorded when the profiling was enabled.
# we simply use the rand to determine whether to record the sample or not.
#
# Default: 0
profiling-sample-ratio 0
# There is no limit to this length. Just be aware that it will consume memory.
# You can reclaim memory used by the perf log with PERFLOG RESET.
#
# Default: 256
profiling-sample-record-max-len 256
# profiling-sample-record-threshold-ms use to tell the kvrocks when to record.
#
# Default: 100 millisecond
profiling-sample-record-threshold-ms 100
################################## SLOW LOG ###################################
# The Kvrocks Slow Log is a system to log queries that exceeded a specified
# execution time. The execution time does not include the I/O operations
# like talking with the client, sending the reply and so forth,
# but just the time needed to actually execute the command (this is the only
# stage of command execution where the thread is blocked and can not serve
# other requests in the meantime).
#
# You can configure the slow log with two parameters: one tells Kvrocks
# what is the execution time, in microseconds, to exceed in order for the
# command to get logged, and the other parameter is the length of the
# slow log. When a new command is logged the oldest one is removed from the
# queue of logged commands.
# The following time is expressed in microseconds, so 1000000 is equivalent
# to one second. Note that -1 value disables the slow log, while
# a value of zero forces the logging of every command.
slowlog-log-slower-than 100000
# There is no limit to this length. Just be aware that it will consume memory.
# You can reclaim memory used by the slow log with SLOWLOG RESET.
slowlog-max-len 128
# If you run kvrocks from upstart or systemd, kvrocks can interact with your
# supervision tree. Options:
# supervised no - no supervision interaction
# supervised upstart - signal upstart by putting kvrocks into SIGSTOP mode
# supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
# supervised auto - detect upstart or systemd method based on
# UPSTART_JOB or NOTIFY_SOCKET environment variables
# Note: these supervision methods only signal "process is ready."
# They do not enable continuous liveness pings back to your supervisor.
supervised no
################################## CRON ###################################
# Compact Scheduler, auto compact at schedule time
# time expression format is the same as crontab(currently only support * and int)
# e.g. compact-cron 0 3 * * * 0 4 * * *
# would compact the db at 3am and 4am everyday
# compact-cron 0 3 * * *
# The hour range that compaction checker would be active
# e.g. compaction-checker-range 0-7 means compaction checker would be worker between
# 0-7am every day.
compaction-checker-range 0-7
# Bgsave scheduler, auto bgsave at schedule time
# time expression format is the same as crontab(currently only support * and int)
# e.g. bgsave-cron 0 3 * * * 0 4 * * *
# would bgsave the db at 3am and 4am everyday
# Command renaming.
#
# It is possible to change the name of dangerous commands in a shared
# environment. For instance the KEYS command may be renamed into something
# hard to guess so that it will still be available for internal-use tools
# but not available for general clients.
#
# Example:
#
# rename-command KEYS b840fc02d524045429941cc15f59e41cb7be6c52
#
# It is also possible to completely kill a command by renaming it into
# an empty string:
#
# rename-command KEYS ""
# The key-value size may so be quite different in many scenes, and use 256MiB as SST file size
# may cause data loading(large index/filter block) ineffective when the key-value was too small.
# kvrocks supports user-defined SST file in config(rocksdb.target_file_size_base),
# but it still too trivial and inconvenient to adjust the different sizes for different instances.
# so we want to periodic auto-adjust the SST size in-flight with user avg key-value size.
#
# If enabled, kvrocks will auto resize rocksdb.target_file_size_base
# and rocksdb.write_buffer_size in-flight with user avg key-value size.
# Please see #118.
#
# Default: yes
auto-resize-block-and-sst yes
################################ ROCKSDB #####################################
# Specify the capacity of metadata column family block cache. Larger block cache
# may make request faster while more keys would be cached. Max Size is 200*1024.
# Default: 2048MB
rocksdb.metadata_block_cache_size 2048
# Specify the capacity of subkey column family block cache. Larger block cache
# may make request faster while more keys would be cached. Max Size is 200*1024.
# Default: 2048MB
rocksdb.subkey_block_cache_size 2048
# Metadata column family and subkey column family will share a single block cache
# if set 'yes'. The capacity of shared block cache is
# metadata_block_cache_size + subkey_block_cache_size
#
# Default: yes
rocksdb.share_metadata_and_subkey_block_cache yes
# Number of open files that can be used by the DB. You may need to
# increase this if your database has a large working set. Value -1 means
# files opened are always kept open. You can estimate number of files based
# on target_file_size_base and target_file_size_multiplier for level-based
# compaction. For universal-style compaction, you can usually set it to -1.
# Default: 4096
rocksdb.max_open_files 8096
# Amount of data to build up in memory (backed by an unsorted log
# on disk) before converting to a sorted on-disk file.
#
# Larger values increase performance, especially during bulk loads.
# Up to max_write_buffer_number write buffers may be held in memory
# at the same time,
# so you may wish to adjust this parameter to control memory usage.
# Also, a larger write buffer will result in a longer recovery time
# the next time the database is opened.
#
# Note that write_buffer_size is enforced per column family.
# See db_write_buffer_size for sharing memory across column families.
# default is 64MB
rocksdb.write_buffer_size 16
# Target file size for compaction, target file size for Leve N can be caculated
# by target_file_size_base * (target_file_size_multiplier ^ (L-1))
#
# Default: 128MB
rocksdb.target_file_size_base 16
# The maximum number of write buffers that are built up in memory.
# The default and the minimum number is 2, so that when 1 write buffer
# is being flushed to storage, new writes can continue to the other
# write buffer.
# If max_write_buffer_number > 3, writing will be slowed down to
# options.delayed_write_rate if we are writing to the last write buffer
# allowed.
rocksdb.max_write_buffer_number 4
# Maximum number of concurrent background compaction jobs, submitted to
# the default LOW priority thread pool.
rocksdb.max_background_compactions 4
# Maximum number of concurrent background memtable flush jobs, submitted by
# default to the HIGH priority thread pool. If the HIGH priority thread pool
# is configured to have zero threads, flush jobs will share the LOW priority
# thread pool with compaction jobs.
rocksdb.max_background_flushes 4
# This value represents the maximum number of threads that will
# concurrently perform a compaction job by breaking it into multiple,
# smaller ones that are run simultaneously.
# Default: 2 (i.e. no subcompactions)
rocksdb.max_sub_compactions 2
# In order to limit the size of WALs, RocksDB uses DBOptions::max_total_wal_size
# as the trigger of column family flush. Once WALs exceed this size, RocksDB
# will start forcing the flush of column families to allow deletion of some
# oldest WALs. This config can be useful when column families are updated at
# non-uniform frequencies. If there's no size limit, users may need to keep
# really old WALs when the infrequently-updated column families hasn't flushed
# for a while.
#
# In kvrocks, we use multiple column families to store metadata, subkeys, etc.
# If users always use string type, but use list, hash and other complex data types
# infrequently, there will be a lot of old WALs if we don't set size limit
# (0 by default in rocksdb), because rocksdb will dynamically choose the WAL size
# limit to be [sum of all write_buffer_size * max_write_buffer_number] * 4 if set to 0.
#
# Moreover, you should increase this value if you already set rocksdb.write_buffer_size
# to a big value, to avoid influencing the effect of rocksdb.write_buffer_size and
# rocksdb.max_write_buffer_number.
#
# default is 512MB
rocksdb.max_total_wal_size 512
# We impl the repliction with rocksdb WAL, it would trigger full sync when the seq was out of range.
# wal_ttl_seconds and wal_size_limit_mb would affect how archived logswill be deleted.
# If WAL_ttl_seconds is not 0, then WAL files will be checked every WAL_ttl_seconds / 2 and those that
# are older than WAL_ttl_seconds will be deleted#
#
# Default: 3 Hours
rocksdb.wal_ttl_seconds 10800
# If WAL_ttl_seconds is 0 and WAL_size_limit_MB is not 0,
# WAL files will be checked every 10 min and if total size is greater
# then WAL_size_limit_MB, they will be deleted starting with the
# earliest until size_limit is met. All empty files will be deleted
# Default: 16GB
rocksdb.wal_size_limit_mb 16384
# Approximate size of user data packed per block. Note that the
# block size specified here corresponds to uncompressed data. The
# actual size of the unit read from disk may be smaller if
# compression is enabled.
#
# Default: 4KB
rocksdb.block_size 2048
# Indicating if we'd put index/filter blocks to the block cache
#
# Default: no
rocksdb.cache_index_and_filter_blocks yes
# Specify the compression to use.
# Accept value: "no", "snappy"
# default snappy
rocksdb.compression snappy
# If non-zero, we perform bigger reads when doing compaction. If you're
# running RocksDB on spinning disks, you should set this to at least 2MB.
# That way RocksDB's compaction is doing sequential instead of random reads.
# When non-zero, we also force new_table_reader_for_compaction_inputs to
# true.
#
# Default: 2 MB
rocksdb.compaction_readahead_size 2097152
# he limited write rate to DB if soft_pending_compaction_bytes_limit or
# level0_slowdown_writes_trigger is triggered.
# If the value is 0, we will infer a value from `rater_limiter` value
# if it is not empty, or 16MB if `rater_limiter` is empty. Note that
# if users change the rate in `rate_limiter` after DB is opened,
# `delayed_write_rate` won't be adjusted.
#
rocksdb.delayed_write_rate 0
# If enable_pipelined_write is true, separate write thread queue is
# maintained for WAL write and memtable write.
#
# Default: no
rocksdb.enable_pipelined_write no
# Soft limit on number of level-0 files. We start slowing down writes at this
# point. A value <0 means that no writing slow down will be triggered by
# number of files in level-0.
#
# Default: 20
rocksdb.level0_slowdown_writes_trigger 20
# Maximum number of level-0 files. We stop writes at this point.
#
# Default: 40
rocksdb.level0_stop_writes_trigger 40
# if not zero, dump rocksdb.stats to LOG every stats_dump_period_sec
#
# Default: 0
rocksdb.stats_dump_period_sec 0
# if yes, the auto compaction would be disabled, but the manual compaction remain works
#
# Default: no
rocksdb.disable_auto_compactions no
################################ NAMESPACE #####################################
# namespace.test change.me
backup-dir .//backup
log-dir ./

View File

@ -1,6 +0,0 @@
#!/bin/bash
set -e
set -x
../../ardb/src/ardb-server ardb.conf

6
storage/run_kvrocks.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
set -x
../../kvrocks/src/kvrocks -c kvrocks.conf

View File

@ -1,6 +0,0 @@
#!/bin/bash
set -e
set -x
../../redis/src/redis-cli -s ./storage.sock shutdown save

30
tools/3rdparty.py Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
from bgpranking.default import get_homedir
d3js_version = '7'
bootstrap_select_version = "1.13.18"
if __name__ == '__main__':
dest_dir = get_homedir() / 'website' / 'web' / 'static'
d3 = requests.get(f'https://d3js.org/d3.v{d3js_version}.min.js')
with (dest_dir / f'd3.v{d3js_version}.min.js').open('wb') as f:
f.write(d3.content)
print(f'Downloaded d3js v{d3js_version}.')
bootstrap_select_js = requests.get(f'https://cdn.jsdelivr.net/npm/bootstrap-select@{bootstrap_select_version}/dist/js/bootstrap-select.min.js')
with (dest_dir / 'bootstrap-select.min.js').open('wb') as f:
f.write(bootstrap_select_js.content)
print(f'Downloaded bootstrap_select js v{bootstrap_select_version}.')
bootstrap_select_css = requests.get(f'https://cdn.jsdelivr.net/npm/bootstrap-select@{bootstrap_select_version}/dist/css/bootstrap-select.min.css')
with (dest_dir / 'bootstrap-select.min.css').open('wb') as f:
f.write(bootstrap_select_css.content)
print(f'Downloaded bootstrap_select css v{bootstrap_select_version}.')
print('All 3rd party modules for the website were downloaded.')

68
tools/migrate.py Normal file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from typing import Set
from redis import Redis
redis_src = Redis(unix_socket_path='../storage/storage.sock')
redis_dst = Redis('127.0.0.1', 5188)
chunk_size = 100000
def process_chunk(src: Redis, dst: Redis, keys: Set[str]):
src_pipeline = src.pipeline()
[src_pipeline.type(key) for key in keys]
to_process = {key: key_type for key, key_type in zip(keys, src_pipeline.execute())}
src_pipeline = src.pipeline()
for key, key_type in to_process.items():
if key_type == b"string":
src_pipeline.get(key)
elif key_type == b"list":
raise Exception('Lists should not be used.')
elif key_type == b"set":
src_pipeline.smembers(key)
elif key_type == b"zset":
src_pipeline.zrangebyscore(key, '-Inf', '+Inf', withscores=True)
elif key_type == b"hash":
src_pipeline.hgetall(key)
else:
raise Exception(f'{key_type} not supported {key}.')
dest_pipeline = dst.pipeline()
for key, content in zip(to_process.keys(), src_pipeline.execute()):
if to_process[key] == b"string":
dest_pipeline.set(key, content)
elif to_process[key] == b"set":
dest_pipeline.sadd(key, *content)
elif to_process[key] == b"zset":
dest_pipeline.zadd(key, {value: rank for value, rank in content})
elif to_process[key] == b"hash":
dest_pipeline.hmset(key, content)
dest_pipeline.execute()
def migrate(src: Redis, dst: Redis):
keys = set()
pos = 0
for key in src.scan_iter(count=chunk_size, match='2017*'):
keys.add(key)
if len(keys) == chunk_size:
process_chunk(src, dst, keys)
pos += len(keys)
print(f'{datetime.now()} - {pos} keys done.')
keys = set()
# migrate remaining keys
process_chunk(src, dst, keys)
pos += len(keys)
print(f'{datetime.now()} - {pos} keys done.')
if __name__ == '__main__':
migrate(redis_src, redis_dst)

24
bgpranking/monitor.py → tools/monitoring.py Normal file → Executable file
View File

@ -1,20 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
try:
import simplejson as json
except ImportError:
import json
import json
from redis import StrictRedis
from .libs.helpers import get_socket_path, get_ipasn
from redis import Redis
from bgpranking.default import get_socket_path
from bgpranking.helpers import get_ipasn
class Monitor():
def __init__(self):
self.intake = StrictRedis(unix_socket_path=get_socket_path('intake'), db=0, decode_responses=True)
self.sanitize = StrictRedis(unix_socket_path=get_socket_path('prepare'), db=0, decode_responses=True)
self.cache = StrictRedis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
self.intake = Redis(unix_socket_path=get_socket_path('intake'), db=0, decode_responses=True)
self.sanitize = Redis(unix_socket_path=get_socket_path('prepare'), db=0, decode_responses=True)
self.cache = Redis(unix_socket_path=get_socket_path('cache'), db=1, decode_responses=True)
self.ipasn = get_ipasn()
def get_values(self):
@ -26,5 +24,11 @@ class Monitor():
if len(ipasn_meta['cached_dates']['caida']['v6']['cached']) > 15:
ipasn_meta['cached_dates']['caida']['v6']['cached'] = 'Too many entries'
return json.dumps({'Non-parsed IPs': ips_in_intake, 'Parsed IPs': ready_to_insert,
'running': self.cache.hgetall('running'), 'IPASN History': ipasn_meta},
'running': self.cache.zrangebyscore('running', '-inf', '+inf', withscores=True),
'IPASN History': ipasn_meta},
indent=2)
if __name__ == '__main__':
m = Monitor()
print(m.get_values())

94
tools/validate_config_files.py Executable file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import logging
import argparse
from bgpranking.default import get_homedir
def validate_generic_config_file():
user_config = get_homedir() / 'config' / 'generic.json'
with user_config.open() as f:
generic_config = json.load(f)
with (get_homedir() / 'config' / 'generic.json.sample').open() as f:
generic_config_sample = json.load(f)
# Check documentation
for key in generic_config_sample.keys():
if key == '_notes':
continue
if key not in generic_config_sample['_notes']:
raise Exception(f'###### - Documentation missing for {key}')
# Check all entries in the sample files are in the user file, and they have the same type
for key in generic_config_sample.keys():
if key == '_notes':
continue
if generic_config.get(key) is None:
logger.warning(f'Entry missing in user config file: {key}. Will default to: {generic_config_sample[key]}')
continue
if not isinstance(generic_config[key], type(generic_config_sample[key])):
raise Exception(f'Invalid type for {key}. Got: {type(generic_config[key])} ({generic_config[key]}), expected: {type(generic_config_sample[key])} ({generic_config_sample[key]})')
if isinstance(generic_config[key], dict):
# Check entries
for sub_key in generic_config_sample[key].keys():
if sub_key not in generic_config[key]:
raise Exception(f'{sub_key} is missing in generic_config[key]. Default from sample file: {generic_config_sample[key][sub_key]}')
if not isinstance(generic_config[key][sub_key], type(generic_config_sample[key][sub_key])):
raise Exception(f'Invalid type for {sub_key} in {key}. Got: {type(generic_config[key][sub_key])} ({generic_config[key][sub_key]}), expected: {type(generic_config_sample[key][sub_key])} ({generic_config_sample[key][sub_key]})')
# Make sure the user config file doesn't have entries missing in the sample config
for key in generic_config.keys():
if key not in generic_config_sample:
raise Exception(f'{key} is missing in the sample config file. You need to compare {user_config} with {user_config}.sample.')
return True
def update_user_configs():
for file_name in ['generic']:
with (get_homedir() / 'config' / f'{file_name}.json').open() as f:
try:
generic_config = json.load(f)
except Exception:
generic_config = {}
with (get_homedir() / 'config' / f'{file_name}.json.sample').open() as f:
generic_config_sample = json.load(f)
has_new_entry = False
for key in generic_config_sample.keys():
if key == '_notes':
continue
if generic_config.get(key) is None:
print(f'{key} was missing in {file_name}, adding it.')
print(f"Description: {generic_config_sample['_notes'][key]}")
generic_config[key] = generic_config_sample[key]
has_new_entry = True
elif isinstance(generic_config[key], dict):
for sub_key in generic_config_sample[key].keys():
if sub_key not in generic_config[key]:
print(f'{sub_key} was missing in {key} from {file_name}, adding it.')
generic_config[key][sub_key] = generic_config_sample[key][sub_key]
has_new_entry = True
if has_new_entry:
with (get_homedir() / 'config' / f'{file_name}.json').open('w') as fw:
json.dump(generic_config, fw, indent=2, sort_keys=True)
return has_new_entry
if __name__ == '__main__':
logger = logging.getLogger('Config validator')
parser = argparse.ArgumentParser(description='Check the config files.')
parser.add_argument('--check', default=False, action='store_true', help='Check if the sample config and the user config are in-line')
parser.add_argument('--update', default=False, action='store_true', help='Update the user config with the entries from the sample config if entries are missing')
args = parser.parse_args()
if args.check:
if validate_generic_config_file():
print(f"The entries in {get_homedir() / 'config' / 'generic.json'} are valid.")
if args.update:
if not update_user_configs():
print(f"No updates needed in {get_homedir() / 'config' / 'generic.json'}.")

View File

@ -1,13 +0,0 @@
#!/bin/bash
set -e
set -x
mkdir -p web/static/
wget https://d3js.org/d3.v5.js -O web/static/d3.v5.js
BOOTSTRAP_SELECT="1.13.5"
wget https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/${BOOTSTRAP_SELECT}/css/bootstrap-select.min.css -O web/static/bootstrap-select.min.css
wget https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/${BOOTSTRAP_SELECT}/js/bootstrap-select.min.js -O web/static/bootstrap-select.min.js

View File

@ -1,40 +1,40 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import pkg_resources
from collections import defaultdict
from datetime import date, timedelta
from urllib.parse import urljoin
try:
import simplejson as json
except ImportError:
import json
import os
from typing import Dict, Any, Tuple, List, Optional, Union
import pycountry # type: ignore
import requests
from flask import Flask, render_template, request, session, Response, redirect, url_for
from flask_bootstrap import Bootstrap
from flask_bootstrap import Bootstrap # type: ignore
from flask_restx import Api # type: ignore
from bgpranking.querying import Querying
from bgpranking.libs.exceptions import MissingConfigEntry
from bgpranking.libs.helpers import load_general_config, get_homedir, get_ipasn
from datetime import date, timedelta
import pycountry
from collections import defaultdict
from bgpranking.bgpranking import BGPRanking
from bgpranking.default import get_config
from bgpranking.helpers import get_ipasn
from .genericapi import api as generic_api
from .helpers import get_secret_key
from .proxied import ReverseProxied
app = Flask(__name__)
secret_file_path = get_homedir() / 'website' / 'secret_key'
app.wsgi_app = ReverseProxied(app.wsgi_app) # type: ignore
if not secret_file_path.exists() or secret_file_path.stat().st_size < 64:
with open(secret_file_path, 'wb') as f:
f.write(os.urandom(64))
with open(secret_file_path, 'rb') as f:
app.config['SECRET_KEY'] = f.read()
app.config['SECRET_KEY'] = get_secret_key()
Bootstrap(app)
app.config['BOOTSTRAP_SERVE_LOCAL'] = True
bgpranking = BGPRanking()
# ############# Helpers #############
@ -42,7 +42,7 @@ def load_session():
if request.method == 'POST':
d = request.form
elif request.method == 'GET':
d = request.args
d = request.args # type: ignore
for key in d:
if '_all' in d.getlist(key):
@ -79,27 +79,25 @@ def index():
# Just returns ack if the webserver is running
return 'Ack'
load_session()
q = Querying()
sources = q.get_sources(date=session['date'])['response']
sources = bgpranking.get_sources(date=session['date'])['response']
session.pop('asn', None)
session.pop('country', None)
ranks = q.asns_global_ranking(limit=100, **session)['response']
r = [(asn, rank, q.get_asn_descriptions(int(asn))['response']) for asn, rank in ranks]
ranks = bgpranking.asns_global_ranking(limit=100, **session)['response']
r = [(asn, rank, bgpranking.get_asn_descriptions(int(asn))['response']) for asn, rank in ranks]
return render_template('index.html', ranks=r, sources=sources, countries=get_country_codes(), **session)
@app.route('/asn', methods=['GET', 'POST'])
def asn_details():
load_session()
q = Querying()
if 'asn' not in session:
return redirect(url_for('/'))
asn_descriptions = q.get_asn_descriptions(asn=session['asn'], all_descriptions=True)['response']
sources = q.get_sources(date=session['date'])['response']
asn_descriptions = bgpranking.get_asn_descriptions(asn=session['asn'], all_descriptions=True)['response']
sources = bgpranking.get_sources(date=session['date'])['response']
prefix = session.pop('prefix', None)
ranks = q.asn_details(**session)['response']
ranks = bgpranking.asn_details(**session)['response']
if prefix:
prefix_ips = q.get_prefix_ips(prefix=prefix, **session)['response']
prefix_ips = bgpranking.get_prefix_ips(prefix=prefix, **session)['response']
prefix_ips = [(ip, sorted(sources)) for ip, sources in prefix_ips.items()]
prefix_ips.sort(key=lambda entry: len(entry[1]), reverse=True)
else:
@ -111,20 +109,20 @@ def asn_details():
@app.route('/country', methods=['GET', 'POST'])
def country():
load_session()
q = Querying()
sources = q.get_sources(date=session['date'])['response']
sources = bgpranking.get_sources(date=session['date'])['response']
return render_template('country.html', sources=sources, countries=get_country_codes(), **session)
@app.route('/country_history_callback', methods=['GET', 'POST'])
def country_history_callback():
history_data = request.get_json(force=True)
history_data: Dict[str, Tuple[str, str, List[Any]]]
history_data = request.get_json(force=True) # type: ignore
to_display = []
mapping = defaultdict(dict)
mapping: Dict[str, Any] = defaultdict(dict)
dates = []
all_asns = set([])
for country, data in history_data.items():
for d, r_sum, details in data:
for country, foo in history_data.items():
for d, r_sum, details in foo:
dates.append(d)
for detail in details:
asn, r = detail
@ -146,7 +144,7 @@ def country_history_callback():
@app.route('/ipasn', methods=['GET', 'POST'])
def ipasn():
d = None
d: Optional[Dict] = None
if request.method == 'POST':
d = request.form
elif request.method == 'GET':
@ -160,12 +158,11 @@ def ipasn():
else:
ip = d['ip']
ipasn = get_ipasn()
q = Querying()
response = ipasn.query(first=(date.today() - timedelta(days=60)).isoformat(),
aggregate=True, ip=ip)
for r in response['response']:
r['asn_descriptions'] = []
asn_descriptions = q.get_asn_descriptions(asn=r['asn'], all_descriptions=True)['response']
asn_descriptions = bgpranking.get_asn_descriptions(asn=r['asn'], all_descriptions=True)['response']
for timestamp in sorted(asn_descriptions.keys()):
if r['first_seen'] <= timestamp <= r['last_seen']:
r['asn_descriptions'].append(asn_descriptions[timestamp])
@ -185,16 +182,11 @@ def ipasn():
@app.route('/ipasn_history/', defaults={'path': ''}, methods=['GET', 'POST'])
@app.route('/ipasn_history/<path:path>', methods=['GET', 'POST'])
def ipasn_history_proxy(path):
config, general_config_file = load_general_config()
if 'ipasnhistory_url' not in config:
raise MissingConfigEntry(f'"ipasnhistory_url" is missing in {general_config_file}.')
path_for_ipasnhistory = request.full_path.replace('/ipasn_history', '')
if '/?' in path_for_ipasnhistory:
path_for_ipasnhistory = path_for_ipasnhistory.replace('/?', '/ip?')
print(path_for_ipasnhistory)
proxied_url = urljoin(config['ipasnhistory_url'], path_for_ipasnhistory)
print(proxied_url)
proxied_url = urljoin(get_config('generic', 'ipasnhistory_url'), path_for_ipasnhistory)
if request.method in ['GET', 'HEAD']:
to_return = requests.get(proxied_url).json()
elif request.method == 'POST':
@ -206,17 +198,17 @@ def ipasn_history_proxy(path):
def json_asn():
# TODO
# * Filter on date => if only returning one descr, return the desription at that date
query = request.get_json(force=True)
to_return = {'meta': query, 'response': {}}
query: Dict[str, Any] = request.get_json(force=True) # type: ignore
to_return: Dict[str, Union[str, Dict[str, Any]]] = {'meta': query, 'response': {}}
if 'asn' not in query:
to_return['error'] = f'You need to pass an asn - {query}'
return Response(json.dumps(to_return), mimetype='application/json')
q = Querying()
asn_description_query = {'asn': query['asn']}
if 'all_descriptions' in query:
asn_description_query['all_descriptions'] = query['all_descriptions']
to_return['response']['asn_description'] = q.get_asn_descriptions(**asn_description_query)['response']
responses = bgpranking.get_asn_descriptions(**asn_description_query)['response']
to_return['response']['asn_description'] = responses # type: ignore
asn_rank_query = {'asn': query['asn']}
if 'date' in query:
@ -228,60 +220,65 @@ def json_asn():
if 'ipversion' in query:
asn_rank_query['ipversion'] = query['ipversion']
to_return['response']['ranking'] = q.asn_rank(**asn_rank_query)['response']
to_return['response']['ranking'] = bgpranking.asn_rank(**asn_rank_query)['response'] # type: ignore
return Response(json.dumps(to_return), mimetype='application/json')
@app.route('/json/asn_descriptions', methods=['POST'])
def asn_description():
query = request.get_json(force=True)
to_return = {'meta': query, 'response': {}}
query: Dict = request.get_json(force=True) # type: ignore
to_return: Dict[str, Union[str, Dict[str, Any]]] = {'meta': query, 'response': {}}
if 'asn' not in query:
to_return['error'] = f'You need to pass an asn - {query}'
return Response(json.dumps(to_return), mimetype='application/json')
q = Querying()
to_return['response']['asn_descriptions'] = q.get_asn_descriptions(**query)['response']
to_return['response']['asn_descriptions'] = bgpranking.get_asn_descriptions(**query)['response'] # type: ignore
return Response(json.dumps(to_return), mimetype='application/json')
@app.route('/json/asn_history', methods=['GET', 'POST'])
def asn_history():
q = Querying()
if request.method == 'GET':
load_session()
if 'asn' in session:
return Response(json.dumps(q.get_asn_history(**session)), mimetype='application/json')
return Response(json.dumps(bgpranking.get_asn_history(**session)), mimetype='application/json')
query = request.get_json(force=True)
to_return = {'meta': query, 'response': {}}
query: Dict = request.get_json(force=True) # type: ignore
to_return: Dict[str, Union[str, Dict[str, Any]]] = {'meta': query, 'response': {}}
if 'asn' not in query:
to_return['error'] = f'You need to pass an asn - {query}'
return Response(json.dumps(to_return), mimetype='application/json')
to_return['response']['asn_history'] = q.get_asn_history(**query)['response']
to_return['response']['asn_history'] = bgpranking.get_asn_history(**query)['response'] # type: ignore
return Response(json.dumps(to_return), mimetype='application/json')
@app.route('/json/country_history', methods=['GET', 'POST'])
def country_history():
q = Querying()
if request.method == 'GET':
load_session()
return Response(json.dumps(q.country_history(**session)), mimetype='application/json')
return Response(json.dumps(bgpranking.country_history(**session)), mimetype='application/json')
query = request.get_json(force=True)
to_return = {'meta': query, 'response': {}}
to_return['response']['country_history'] = q.country_history(**query)['response']
query: Dict = request.get_json(force=True) # type: ignore
to_return: Dict[str, Union[str, Dict[str, Any]]] = {'meta': query, 'response': {}}
to_return['response']['country_history'] = bgpranking.country_history(**query)['response'] # type: ignore
return Response(json.dumps(to_return), mimetype='application/json')
@app.route('/json/asns_global_ranking', methods=['POST'])
def json_asns_global_ranking():
query = request.get_json(force=True)
to_return = {'meta': query, 'response': {}}
q = Querying()
to_return['response'] = q.asns_global_ranking(**query)['response']
query: Dict = request.get_json(force=True) # type: ignore
to_return: Dict[str, Union[str, Dict[str, Any]]] = {'meta': query, 'response': {}}
to_return['response'] = bgpranking.asns_global_ranking(**query)['response']
return Response(json.dumps(to_return), mimetype='application/json')
# ############# Json outputs #############
# Query API
api = Api(app, title='BGP Ranking API',
description='API to query BGP Ranking.',
version=pkg_resources.get_distribution('bgpranking').version)
api.add_namespace(generic_api)

32
website/web/genericapi.py Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pkg_resources
from flask import Flask
from flask_restx import Api, Resource # type: ignore
from bgpranking.bgpranking import BGPRanking
from .helpers import get_secret_key
from .proxied import ReverseProxied
app: Flask = Flask(__name__)
app.wsgi_app = ReverseProxied(app.wsgi_app) # type: ignore
app.config['SECRET_KEY'] = get_secret_key()
api = Api(app, title='BGP Ranking API',
description='API to query BGP Ranking.',
version=pkg_resources.get_distribution('bgpranking').version)
bgpranking: BGPRanking = BGPRanking()
@api.route('/redis_up')
@api.doc(description='Check if redis is up and running')
class RedisUp(Resource):
def get(self):
return bgpranking.check_redis_up()

27
website/web/helpers.py Normal file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from functools import lru_cache
from pathlib import Path
from bgpranking.default import get_homedir
def src_request_ip(request) -> str:
# NOTE: X-Real-IP is the IP passed by the reverse proxy in the headers.
real_ip = request.headers.get('X-Real-IP')
if not real_ip:
real_ip = request.remote_addr
return real_ip
@lru_cache(64)
def get_secret_key() -> bytes:
secret_file_path: Path = get_homedir() / 'secret_key'
if not secret_file_path.exists() or secret_file_path.stat().st_size < 64:
if not secret_file_path.exists() or secret_file_path.stat().st_size < 64:
with secret_file_path.open('wb') as f:
f.write(os.urandom(64))
with secret_file_path.open('rb') as f:
return f.read()

17
website/web/proxied.py Normal file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import Any, MutableMapping
class ReverseProxied():
def __init__(self, app: Any) -> None:
self.app = app
def __call__(self, environ: MutableMapping[str, Any], start_response: Any) -> Any:
scheme = environ.get('HTTP_X_FORWARDED_PROTO')
if not scheme:
scheme = environ.get('HTTP_X_SCHEME')
if scheme:
environ['wsgi.url_scheme'] = scheme
return self.app(environ, start_response)

View File

@ -1,3 +1,5 @@
"use strict";
function linegraph(call_path) {
var svg = d3.select("svg"),
margin = {top: 20, right: 80, bottom: 30, left: 50},
@ -16,12 +18,11 @@ function linegraph(call_path) {
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.rank); });
d3.json(call_path, {credentials: 'same-origin'}).then(function(data) {
var country_ranks = d3.entries(data.response).map(function(country_rank) {
d3.json(call_path, {credentials: 'same-origin'}).then(data => {
var country_ranks = $.map(data.response, function(value, key) {
return {
country: country_rank.key,
values: d3.values(country_rank.value).map(function(d) {
country: key,
values: $.map(value, function(d) {
return {date: parseTime(d[0]), rank: d[1]};
})
};
@ -72,8 +73,9 @@ function linegraph(call_path) {
{credentials: 'same-origin',
method: 'POST',
body: JSON.stringify(data.response),
}).then(function(data) {
})
.then(function(data) {
d3.select('#asn_details').html(data);
});
});
});
};

View File

@ -1,21 +1,28 @@
{% extends "bootstrap/base.html" %}
{% block scripts %}
{{ super() }}
<script src='{{ url_for('static', filename='d3.v5.js') }}'></script>
<script src='{{ url_for('static', filename='bootstrap-select.min.js') }}'></script>
<script>
$(document).ready(function(){
$('[data-toggle="popover"]').popover();
});
</script>
{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap-select.min.css') }}">
{{ super() }}
{% endblock %}
{% block content %}
{{ super() }}
{% endblock %}
<!doctype html>
<html lang="en">
<head>
{% block head %}
<!-- Required meta tags -->
<meta charset="utf-8">
{% block styles %}
{{ bootstrap.load_css() }}
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap-select.min.css') }}">
{% endblock %}
{% endblock %}
</head>
<body>
<div class="container">
{% block content %}{% endblock%}
</div>
{% block scripts %}
{{ bootstrap.load_js() }}
<script src='{{ url_for('static', filename='bootstrap-select.min.js') }}'></script>
<script src='{{ url_for('static', filename='d3.v7.min.js') }}'></script>
<script>
$(document).ready(function(){
$('[data-toggle="popover"]').popover();
});
</script>
{% endblock %}
</body>
</html>